32.STL中的heap的实现

发布时间 2023-08-03 07:57:34作者: CodeMagicianT

32.STL中的heap的实现

版本1:

1.堆的原理

堆(Heap)是一种数据结构,通常用于实现优先队列。堆是一种树形结构,通常由一个完全二叉树构成,因此它只有两个指针,即左子节点和右子节点。堆有两种类型:最大堆(Max Heap)和最小堆(Min Heap)。在最大堆中,父节点的值大于或等于其子节点的值;在最小堆中,父节点的值小于或等于其子节点的值。

堆的主要操作包括:

  1. 插入(Insertion):将一个元素插入到堆中。在最大堆中,插入元素时将其放入最后一个位置,然后将其与父节点比较并交换位置,直到满足堆的性质。在最小堆中,插入元素时将其放入第一个位置,然后将其与子节点比较并交换位置,直到满足堆的性质。
  2. 删除(Deletion):从堆中删除一个元素。在最大堆中,删除最后一个元素,然后将最后一个元素与第一个元素交换位置,然后将其与子节点比较并交换位置,直到满足堆的性质。在最小堆中,删除第一个元素,然后将第一个元素与最后一个元素交换位置,然后将其与父节点比较并交换位置,直到满足堆的性质。
  3. 查找(Finding):查找堆中最大或最小的元素。在最大堆中,查找最后一个元素;在最小堆中,查找第一个元素。

堆是一种高效的数据结构,可以用于实现优先队列、快速排序等算法。

image-20230612083848735

最大堆特点:

1.每个节点最多可以有两个节点
2.根节点的键值是所有堆节点键值中最大者,且每个节点的值都比孩子大
3.除了根节点没有兄弟节点,最后一个左子节点可以没有兄弟节点,其他节点必须有兄弟节点

看图识最大堆: A B 不是堆,C 是最大堆

image-20230612084315044

堆是你见过的最有个性的树!它是用数组表示的树

i的子节点左:2i+1  i的右子节点:2i+2
i的父节点:(i-1)/2

image-20230612084525220

在数组中快速创建堆

image-20230612084958778

  1. 首先我们需要找到最后一个结点的父结点如图(a),我们找到的结点是87,然后找出该结点的最大子节点与自己比较,若该子节点比自身大,则将两个结点交换。
    图(a)中,87 比左子节点95 小,则交换之。如图(b)所示

image-20230612085225860

2.我们移动到前一个父结点93,如图(c)所示。同理做第一步的比较操作,结果不需要交换。

image-20230612085407288

3.继续移动结点到前一个父结点82,如图(d)所示,82 小于右子节点95,则82 与95 交换,如图(e)所示,82 交换后,其值小于左子节点,不符合最大堆的特点,故需要继续向下调整,如图(f)所示

image-20230612085522994

4.所有节点交换完毕,最大堆构建完成

2.堆的算法实现

堆数据结构的定义

#define DEFAULT_CAPCITY 128
typedef struct _Heap
{
    int *arr; //存储堆元素的数组
    int size; //当前已存储的元素个数
    int capacity; //当前存储的容量
}Heap;

构建最大堆

bool initHeap(Heap& heap, int* original, int size);//将数组用堆的结构存储
static void buildHeap(Heap& heap);//建成最大堆 static阻止外部访问
static void adjustDown(Heap& heap, int index);//向下调整

//初始化堆
bool initHeap(Heap& heap, int* original, int size)
{
	int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size;

	heap.arr = new int[capacity];
	if (!heap.arr) return false;

	heap.capacity = capacity;
	heap.size = 0;

	//如果存在原始数据则构建堆
	if (size > 0)
	{
		memcpy(heap.arr, original, size * sizeof(int));
		heap.size = size;
		buildHeap(heap);
	}
	return true;
}

/* 从最后一个父节点(size/2-1 的位置)逐个往前调整所有父节点(直到根节
点),确保每一个父节点都是一个最大堆,最后整体上形成一个最大堆*/
void buildHeap(Heap& heap)
{
	int i;
	for (i = heap.size / 2 - 1; i >= 0; i--)
	{
		adjustDown(heap, i);
	}
}

//将当前的节点和子节点调整成最大堆
void adjustDown(Heap& heap, int index)
{
	int cur = heap.arr[index];//当前待调整的节点
	int parent, child;

	/*判断否存在大于当前节点子节点,如果不存在,则堆本身是平衡的,不需要
	调整;如果存在,则将最大的子节点与之交换,交换后,如果这个子节点还有
	子节点,则要继续按照同样的步骤对这个子节点进行调整*/
	for (parent = index; (parent * 2 + 1) < heap.size; parent = child)
	{
		child = parent * 2 + 1;

		//取两个子节点的最大的节点
		if (((child + 1) < heap.size) && (heap.arr[child] < heap.arr[child + 1]))
		{
			child++;
		}

		//判断最大的节点是否大于当前的父节点
		if (cur >= heap.arr[child])//不大于,则不需要调整,跳出循环
		{
			break;
		}
		else//大于当前的父节点,进行交换,然后从子节点位置继续向下调整
		{
			heap.arr[parent] = heap.arr[child];
			heap.arr[child] = cur;
		}
	}

}

插入新元素

将数字99 插入到上面大顶堆中的过程如下:
1.原始的堆,如图a

image-20230612123343672

对应的数组:{95, 93, 87, 92, 86, 82}
  1. 将新进的元素插入到大顶堆的尾部,如下图b所示:

image-20230612123510498

对应的数组:{95, 93, 87, 92, 86, 82, 99}
  1. 此时最大堆已经被破坏,需要重新调整,因加入的节点比父节点大,则新节点跟父节点调换即可,如图c所示;调整后,新节点如果比新的父节点小,则已经调整到位,如果比新的父节点大,则需要和父节点重新进行交换,如图d,至此,最大堆调整完成。

image-20230612123806350

代码实现:

#include <stdio.h> 
#include <stdlib.h>
#include <string.h>
#define DEFAULT_CAPCITY 128

typedef struct _Heap 
{
    int *arr; //存储堆元素的数组
    int size; //当前已存储的元素个数
    int capacity; //当前存储的容量
}Heap;

bool initHeap(Heap &heap, int *orginal, int size);
bool insert(Heap &heap, int value);
static void buildHeap(Heap &heap);
static void adjustDown(Heap &heap, int index);
static void adjustUp(Heap &heap, int index);

/*初始化堆*/
bool initHeap(Heap &heap, int *orginal, int size)
{
    int capacity = DEFAULT_CAPCITY>size ? DEFAULT_CAPCITY : size;
    heap.arr = new int[capacity];
    if (!heap.arr)   return false;
    heap.capacity = capacity;
    heap.size = 0;
   
    //如果存在原始数据则构建堆
    if(size > 0)
    {
        /*方式一: 直接调整所有元素
        memcpy(heap.arr, orginal, size*sizeof(int));
        heap.size = size;
        //建堆
        buildHeap(heap);
        */
        
        //方式二: 一次插入一个
        for(int i=0; i<size; i++)
        {
            insert(heap, orginal[i]);
        }
    }
    return true;
}

/* 从最后一个父节点(size/2-1 的位置)逐个往前调整所有父节点(直到根节点),确保每一个父节点都是一个最大堆,最后整体上形成一个最大堆*/
void buildHeap(Heap &heap) 
{
    int i;
    for (i = heap.size / 2 - 1; i >= 0; i--) 
    {
        adjustDown(heap, i);
    }
}

/*将当前的节点和子节点调整成最大堆*/
void adjustDown(Heap &heap, int index)
{
    int cur = heap.arr[index];//当前待调整的节点
    int parent, child;
    /*
    判断否存在大于当前节点子节点,如果不存在,则堆本身是平衡的,不需要调整;
    如果存在,则将最大的子节点与之交换,交换后,如果这个子节点还有子节
    点,则要继续按照同样的步骤对这个子节点进行调整
    */
    for (parent = index; (parent * 2 + 1)<heap.size; parent = child) 
    {
        child = parent * 2 + 1;
        
        //取两个子节点中的最大的节点
        if (((child + 1)<heap.size) && (heap.arr[child]<heap.arr[child +1]))
        {
            child++;
        }
        
        //判断最大的节点是否大于当前的父节点
        if (cur >= heap.arr[child])
        {
            //不大于,则不需要调整,跳出循环
            break;
        }
        else//大于当前的父节点,进行交换,然后从子节点位置继续向下调整
        {
            heap.arr[parent] = heap.arr[child];
            heap.arr[child] = cur;
        }
    }
}

/*将当前的节点和父节点调整成最大堆*/
void adjustUp(Heap &heap, int index)
{
    if(index<0 || index >= heap.size)//大于堆的最大值直接return
    {
        return;
    }
    
    while(index>0)
    {
        int temp = heap.arr[index];
        int parent = (index - 1) / 2;
        if(parent >= 0)//如果索引没有出界就执行想要的操作
        {
            if(temp > heap.arr[parent])
            {
                heap.arr[index] = heap.arr[parent];
                heap.arr[parent] = temp;
                index = parent;
            }
            else//如果已经比父亲小直接结束循环
            {
                break;
            }
        }
        else//越界结束循环
        {
            break;
        }
    }
}

/*最大堆尾部插入节点,同时保证最大堆的特性*/
bool insert(Heap &heap, int value)
{
    if (heap.size == heap.capacity)
    {
        fprintf(stderr, "栈空间耗尽!\n");
        return false;
    }
   
    int index = heap.size;
    heap.arr[heap.size++] = value;
    adjustUp(heap, index);
    return true;
}

int main(void)
{
    Heap hp;
    int origVals[] = { 1, 2, 3, 87, 93, 82, 92, 86, 95 };
    int i = 0;
    if(!initHeap(hp, origVals, sizeof(origVals)/sizeof(origVals[0])))
    {
        fprintf(stderr, "初始化堆失败!\n");
        exit(-1);
    }
    for (i = 0; i<hp.size; i++) 
    {
        printf("the %dth element:%d\n", i, hp.arr[i]);
    }
    insert(hp, 99);
    printf("在堆中插入新的元素99,插入结果:\n");
    for (i = 0; i<hp.size; i++)
    {
        printf("the %dth element:%d\n", i, hp.arr[i]);
    }
    system("pause");
    return 0;
}

参考资料来源:

奇牛学院

版本2:

1.简介

要想真正了解堆,就需要先了解[二叉树](树和二叉树(Tree&Binary Tree))。

堆是所有树中最具有特点的树,因为它是用数组存储的,并且总是完全二叉树。

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1}把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

最大堆的特点:

1.每个节点最多有两个字节点;

2.根节点的键值是所有堆节点中的最大者,并且每个节点都比自己的孩子节点的值大;

3.除了根节点没有兄弟节点,最后一个左子节点可以没有兄弟节点之外,其它节点必须有兄弟节点。

最小堆就是将最大堆反过来,根节点为最小值。

堆的结构图如下:

img

2.代码实现

注:以下内容以小堆为例。

(1)堆的定义

typedef struct heap
{
    int* data;
    int size;
    int capacity;
}heap;

(2)堆的交换

void swap(int* a, int* b)
{
    int temp=*a;
    *a=*b;
    *b=temp;
}

(3)检查堆的容量

void checkCapcity(heap* hp)
{
    if (hp->capacity==hp->size)
    {
        int newC;
        if(hp->capacity==0)
        {
            newC=1;
        }
        else
        {
            newC=2*hp->capacity;
        }
        hp->data=(int*)realloc(hp->data, sizeof(int)*newC);
        hp->capacity=newC;
    }
}

(4)初始化

void heapInit(heap* hp)
{
    assert(hp);
    hp->data=NULL;
    hp->capacity=hp->size=0;
}

(5)创建

void heapCreate(heap* hp,int* arr,int n)
{
    assert(hp);
    hp->data=(int*)malloc(sizeof(int)*n);
    memcpy(hp->data,arr,sizeof(int)*n);
    hp->capacity=hp->size=n;
    for(int i=(n-2)/2; i>=0; i--)
    {
        shiftDown(hp->data,hp->size,i);
    }
}

从堆的最后一个非叶子节点开始,依次对每个节点进行shiftDown操作,将它们调整到正确的位置上,以满足堆的性质。这样就可以确保堆中的元素按照特定的顺序排列,以便后续的插入、删除等操作能够正确执行。

(6)销毁

void destory(heap* hp)
{
    assert(hp);
    free(hp->data);
    hp->data=NULL;
    hp->capacity=hp->size=0;
}

(7)插入

void heapPush(heap* hp,int val)
{
    assert(hp);
    checkCapcity(hp);
    hp->data[hp->size++]=val;
    shiftUp(hp->data,hp->size,hp->size-1);
}

执行 shiftUp 操作,将新插入的元素沿着树向上移动,以确保满足堆的性质。

(8)删除

void heapPop(heap* hp)
{
    if(hp->size>0)
    {
        swap(&hp->data[0],&hp->data[hp->size-1]);
        hp->size--;
        shiftDown(hp->data,hp->size,0);
    }
}

这个函数的目的是从堆中删除最大元素,并保持堆的有序性。通过与最后一个元素进行交换来删除最大元素,然后执行 shiftDown 操作,将新的最大元素调整到正确的位置上,以满足堆的性质。

要删除堆中的最大元素,我们需要将堆的第一个元素(最大元素)与最后一个元素进行交换,然后通过执行 shiftDown 操作将新的最大元素调整到正确的位置上。这样做的原因是,删除堆中的元素时,需要保持堆的有序性,即满足最大堆的性质。通过将最大元素与最后一个元素交换位置,可以将最大元素移到堆的最后一个位置,然后执行 shiftDown 操作将新的最大元素调整到正确的位置上,以确保堆的性质仍然满足。这样做可以确保堆在删除元素后仍然是一个有效的最大堆,以便进行后续的插入、删除等操作。

(9)获取堆顶元素

int heapTop(heap* hp)
{
    assert(hp);
    return hp->data[0];
}

(10)判断是不是空堆

int heapEmpty(heap* hp)
{
    if (hp==NULL||hp->size==0)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

(11)堆排序

void heapSort(heap* hp)
{
    assert(hp);
    for (int i=(hp->size-2)/2; i>=0; i--)
    {
        shiftDown(hp->data,hp->size,i);
    }
    int end=hp->size-1;
    while(end>0)
    {
        Swap(&hp->data[0],&hp->data[end]);
        shiftDown(hp->data,end,0); 
        end--;
    }
}

(12)打印

void HeapPrint(heap* hp)
{
    assert(hp);
    for(int i=0; i<hp->size; i++)
    {
        cout<<hp->data[i]<<" ";
    }
    cout<<endl;
}

参考:

[堆(Heap)](