介绍
首先我们要将数据结构中的堆和内存中的堆区区分开来,内存中的堆区是操作系统管理的,和数据结构中的堆没有半毛钱关系。堆只有两种,大堆和小堆。
- 大堆(大根堆):父节点的值大于左右孩子结点的值,也就是说大堆的根节点是整个堆中的最大值,左右孩子结点的值毫无关系
- 小堆(小根堆):父节点的值小于左右孩子结点的值,也就是说小堆的根节点是整个堆中的最小值,左右孩子结点的值毫无关系
堆的表示
我们通常用一个数组来表示一个堆,数组中存放的是堆的层序遍历结果,第一个元素即根节点
实现堆的基本操作
- 结构体声明
//定义一个比较函数的函数指针,用来指明该堆是小堆还是大堆
typedef int (*Compare)(HeapType a, HeapType b);
typedef struct Heap{
HeapType data[HeapMaxSize];//数组表示,存放堆中数据
size_t size;//表示有效数据的个数
Compare com;//函数指针,用来决定实现的是小堆还是大堆
}Heap;
- 比较函数
//初始化的时候将堆初始化成大堆或者小堆(传函数名进去),在进行插入删除操作的时候用到该函数
int Greater(HeapType a, HeapType b)//表示大堆
{
return a > b?1:0;
}
int Less(HeapType a, HeapType b)//表示小堆
{
return a < b?1:0;
}
- 堆的初始化和销毁
//初始化堆
void HeapInit(Heap* heap,Compare cmp)
{
if(heap == NULL)
{
//非法输入
return;
}
heap->size = 0;
heap->com = cmp;
}
//销毁堆
void HeapDestroy(Heap* heap)
{
if(heap == NULL)
{
return;
}
heap->size = 0;
}
- 堆的插入和删除堆顶元素(堆主要是使用其堆顶元素)操作
- 堆的插入中最重要的就是
上浮
操作,意思是每次插入新数据时先放置到数组的末尾,然后和其父节点比较,这时就要用到比较函数,如果满足函数,就不动,不满足就要进行上移,也就是数据交换,直到移动到它合适的位置 - 堆的删除中最重要的就是
下潜
操作,先将堆顶元素,也就是0号下标元素和数组末尾元素交换数据,再将堆顶元素按堆的规则(大堆还是小堆)往下移动,直到移动到它合适的位置
- 堆的插入中最重要的就是
可以说 上浮
和下潜
是堆里最重要的两个操作
//交换函数
void Swap(HeapType *a, HeapType *b)
{
HeapType tmp = *a;
*a = *b;
*b = tmp;
}
//上浮函数
void AdjustUp(Heap* heap, size_t child)
{
if(heap == NULL)
{
return;
}
if(child == 0)
{
return;
}
size_t parents = (child - 1)/2;
if(!heap->com(heap->data[parents],heap->data[child]))
{
Swap(&heap->data[parents],&heap->data[child]);
AdjustUp(heap, parents);
}else
{
return;
}
}
//插入元素
void HeapInsert(Heap* heap, HeapType value)
{
if(heap == NULL)
{
//非法输入
return;
}
if(heap->size >= HeapMaxSize)
{
//堆满了
return;
}
heap->data[heap->size++] = value;
size_t child = heap->size - 1;
AdjustUp(heap, child);
}
//下潜函数
void AdjustDown(Heap* heap, size_t parents, size_t size)
{
if(heap == NULL || parents >= size - 1 )
{
return;
}
size_t child = parents*2 + 1;//先定义其左孩子结点
while(child > 0 && child < size)//如果左孩子结点存在,进入循环
{
//(以大堆为例)
//我们是不清楚其左右孩子结点谁大谁小的,要确定出大的那个值才能进行交换
if(child + 1 < size)//如果存在右孩子结点
{
//(以大堆为例)
//如果右孩子结点的值大于左孩子结点,child+1就定位到右孩子结点了
//否则就是左孩子结点
if(!heap->com(heap->data[child],heap->data[child+1]))
{
child += 1;
}
}
//如果父结点的值小于左右孩子结点中的最大值,就要进行交换
//否则退出循环,表明已找到合适位置
if(!heap->com(heap->data[parents],heap->data[child]))
{
Swap(&heap->data[parents],&heap->data[child]);
parents = child;
child = parents*2 + 1;
}else{
break;
}
}
return;
}
//删除堆顶元素
void HeapErase(Heap* heap)
{
if(heap == NULL)
{
//非法输入
return;
}
if(heap->size <= 0)
{
//空堆
return;
}
Swap(&heap->data[heap->size-1], &heap->data[0]);
heap->size--;
AdjustDown(heap,0,heap->size);
return;
}
- 取堆顶元素太简单了,就是返回数组下标为0的值,即根节点,代码就不粘了。
- 创建一个堆,即给定一个无序数组,将其实现为一个堆,遍历数组,循环的进行插入操作就好了。介绍这个主要是为了堆排序
//创建堆
void HeapCreate(HeapType array[], Heap* heap,size_t size)
{
if(heap == NULL || size <= 0)
{
return;
}
size_t index = 0;
while(size--)
{
HeapInsert(heap,array[index]);
index++;
}
}
- 堆排序
- 给定一个无序数组,使用堆排序的方式将其有序化,很简单,我们先把该数组创建成一个堆,我们只能保证父结点的值大于孩子结点的值,不能保证整个数组有序。
- 这时我们就要用到删除堆顶元素操作,每次删除的时候都是把堆顶元素交换到了数组末尾,有效数据-1,而并没有真正的将其移除,所以我们进行循环的删除操作,以大堆为例,每次被放置在数组末尾的元素都是堆中的最大值,所以当堆为空的时候,整个数组也就变成升序数组了(不考虑size)。最后将其memcpy到原来的数组中即可
- 注意:升序数组用大堆,降序数组用小堆
//堆排序
void HeapSort(HeapType array[], Heap* heap, size_t size)
{
if(heap == NULL || size <= 0)
{
return;
}
size_t num = size;
HeapCreate(array,heap,size);
while(size--)
{
HeapErase(heap);
}
memcpy(array,heap->data,num*sizeof(HeapType));
}