介紹
首先我們要將數據結構中的堆和內存中的堆區區分開來,內存中的堆區是操作系統管理的,和數據結構中的堆沒有半毛錢關係。堆只有兩種,大堆和小堆。
- 大堆(大根堆):父節點的值大於左右孩子結點的值,也就是說大堆的根節點是整個堆中的最大值,左右孩子結點的值毫無關係
- 小堆(小根堆):父節點的值小於左右孩子結點的值,也就是說小堆的根節點是整個堆中的最小值,左右孩子結點的值毫無關係
堆的表示
我們通常用一個數組來表示一個堆,數組中存放的是堆的層序遍歷結果,第一個元素即根節點
實現堆的基本操作
- 結構體聲明
//定義一個比較函數的函數指針,用來指明該堆是小堆還是大堆
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));
}