數據結構——堆以及堆排序

上一篇介紹了二叉樹,這一片我們來看堆。首先什麼是堆?其實堆也是一種二叉樹,只不過是一種特殊的二叉樹。堆分爲大根堆和小根堆。大根堆就是根節點的值是最大的,且他的兩個孩子節點也是一個大根堆,以此類推。比如下圖
在這裏插入圖片描述
根節點是100,比他的兩個孩子90和85大,而他的兩個孩子作爲根節點72同樣比他們的孩子值大,這樣的結構我們稱之爲大根堆,並且這個堆的根節點的值是這個堆裏面所有值的最大值,正是這一特點,爲堆排序埋下伏筆。同樣的小根堆也是類似,只不過是根節點爲最小值,而他的兩個孩子作爲根節點的話也是一個小根堆。

大根堆的創建

首先要明確一點,堆並不是我們直接寫好的,是需要創建堆,即根據一種算法,把不是堆的一組數據變成大根堆或者小根堆,還是上述的圖我們可以知道這個堆的順序是100,90,85,72,71,65,79,52,56,45,58。當然,這個堆的順序存在多個,只要滿足根節點和孩子節點之間的關係就行,所以堆不是唯一的。
接下來,我們看一下創建一個大根堆需要哪些數據類型以及函數接口

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef int hpdatatype;//數據類型
typedef struct heap //堆這個結構體
{
  hpdatatype* array;//堆數據
  int size; //堆的大小
  int capacity;//堆的容量
}heap;
//交換函數
void swap(hpdatatype* array,int a,int b)
{
  int tmp = array[a];
   array[a] = array[b];
   array[b] = tmp;
}
void shiftdownbig(hpdatatype* array,int size,int parent);//大堆向下調整
void shiftupbig(hpdatatype* array,int child)//大堆向上調整
void heapcreat(heap* hp,hpdatatype* arry,int size) //創建堆
void heappush(heap* hp,hpdatatype data)//向堆中插入一個數據
void heappopbig(heap* hp)//堆中刪除最值
hpdatatype heaptop(heap* hp)//獲取堆頂元素
int heapisempty(heap* hp)//判斷堆是否爲空

在創建堆之前,我們需要了解一下什麼是大堆向下調整。
顧名思義,每一次向下調整成一個大堆,那麼我們就需要找到這組數據的最後一個非葉子結點,來調整。我們默認葉子節點是有序的,則需要從這個葉子節點的父節點開始向下判斷,如果不是大堆,則需要與兩個孩子的最大值進行交換,依次向上判斷,直至變成一個大根堆。

創建堆
hp是指向這個堆的根節點的指針,hpdatatype* array則是給你的那組數據,你要將它變成一個大根堆,即要將這個數據複製到hp所指向的那個內存空間

void heapcreat(heap* hp,hpdatatype* array,int size)
{
	hp->array = (hpdatatype*)malloc(sizeof(hpdatatype)*size);//給這個堆空間初始化
	hp->array = memcpy(hp->array,array,sizeof(hpdatatype)*size);//將所需要創建成堆的數據複製到該空間
	hp->size = size;//當前大小爲size
	hp->capacity = size;//初始堆容量爲size
	//現在需要找到對後一個非葉子結點
	for(int parent = (size-2)/2;parent>=0;parent--)
	//這裏需要大家記住一個公式,(size-2)/2就是該二叉樹的最後一個非葉子節點,從這結點開始依次向上判斷,所以這裏需要一個循環,直到判斷到根節點是否滿足大根堆的要求爲止
	{
		//循環判斷並調整大根堆
		shiftdownbig(hp,hp->size,parent)
	}
} 

大堆向下調整
上面已經說過什麼是大堆向下調整,這裏我們仔細的來看。如下圖
在這裏插入圖片描述
很明顯這不是一個大根堆,那麼我們是如何調整呢?
首先我們找到最後一個非葉子結點,發現是90,那麼從這裏向下判斷是否爲一個大根堆呢?顯而易見這是一個大根堆,因爲90大於45和71,像上一個走,到了56,此時發現這不是一個大根堆,因爲72是大於56的,所以這裏調整一下,讓72與56交換得下圖

在這裏插入圖片描述
此時72這個結點向下看爲一個大根堆,調整好後繼續向上走,走到85,發現該結點向下看去是一個大根堆,則繼續走,走到58,發現這不是一個大根堆,則從兩個孩子中選出最大的和58交換,顯然90是最大的,交換得下圖
在這裏插入圖片描述
可是這裏發現,雖然90的確比他的兩個孩子大,但是一交換後,58這個結點又不是他的孩子中最大的,這裏怎麼辦呢?很簡單,當90這個結點交換後,我們只需要繼續向下走,判斷它的兩個孩子是否還滿足大根堆的要求,如果不滿足,則繼續交換。交換後就得到下圖
在這裏插入圖片描述
到這裏我們發現所有結點都滿足大根堆的條件了,也相當於這個大根堆創建好了
代碼實現如下

void shiftdownbig(hpdatatype* arry,int size,int parent)
{
	//當找到最後一個非葉子結點parent後,需要找到他的左孩子
	int child = parent*2+1;
	//找到左孩子後,則開始循環向下判斷
	while(child<size) //如果當前孩子結點小於這個堆的大小則向下繼續調整
	{
		//需要判斷是否有右孩子且右孩子的值是否大於左孩子
		if(child+1<size&&array[child+1]>array[child])//如果有右孩子,且右孩子值大於左孩子
			child++;//則取右孩子
		//當取到孩子結點中最大值後,需要和父結點比較大小,若大於則交換,小於則不交換
		if(array[child]>array[parent])
		{
			//交換父結點與最大孩子結點的值
			swap(array,parent,child)//交換完後,有可能會出現上述圖片中下面不是一個大根堆的情況
			//所以我們只需要繼續向下判斷即可
			parent = child; //讓父結點等於當前孩子節點
			child = parent*2+1;//尋找父結點的左孩子
		}
		else
			break;//不需要調整跳出循環
	}
}

大堆向上調整
如果向已經建好的一個大根堆中插入一個數據,則需要通過大根向上調整來重新調整好一個新的堆
與大堆向下調整思路類似,但值得注意的是大堆向上調整是從最後一個孩子結點判斷是否滿足大根堆的條件,依次類推

//參數傳入的是孩子
void shiftupbig(hpdatatype* arry,int child)
{
  int parent = (child-1)/2;//找到當前孩子結點的父結點
  //只要孩子節點>0則繼續調整
  while(child>0)
  {
    if(arry[child]>arry[parent])
    {
      swap(arry,child,parent);//交換當前父結點和最大孩子結點的值
      child = parent;//將父結點置爲孩子結點
      parent = (child-1)/2;//尋找當前孩子結點的父結點
    }
    else
      break;//不需要調整跳出循環
  }
}

堆中插入數據
在已建好的堆中插入一個數據,最好辦法就是尾插,即插入到堆的最後一個爲止,並且使size++,然後來一次大根堆向上調整即可

void heappush(heap* hp,hpdatatype data)
{
	if(hp->size == hp->capacity)//若當前堆爲滿堆則需要擴容
	{
		hp->capacity*=2;//新的堆的大小爲原來的2倍
		hp->array = (hpdatatype*)realloc(hp->array,sizeof(hpdatatype)*hp->capacity);//擴容該堆
	}
	hp->array[hp->size++] = data;
	//做一次向上調整,這裏直接傳入該堆的最後一個結點下標即可
	shifupbig(hp,hp->size - 1);
}

堆中刪除最值
我們創建好堆,刪除什麼值意義最大呢?很顯然,刪除堆中的最值意義最大,那麼我們能直接刪掉嗎?如果直接刪掉,那麼這個堆便沒有根節點,什麼也不是了,所以我們需要換一種刪除方法。我們可以將根節點與最後一個結點交換,再通過尾刪,將最值刪除後,同時走一遍向上調整構成一個新的堆,這時我們的刪除就結束了。

void heappopbig(heap* hp)
{
	//如果該堆爲空則直接返回
	if(hp->size == 0)
		return ;
	swap(array,0,hp->size-1);//交換根節點和最後一個結點
	hp->size--;//尾刪操作
	shiftdownbig(array,hp->size,0);//從根節點開始做一次向下調整
}

獲取堆頂元素

hpdatatype heaptop(heap* hp)
{
	return hp->array[0];
}

判斷堆是否爲空

int heapisempty(heap* hp)
{
	if(hp->size == 0)
		return 1;//爲空則返回1
	return 0;//非空返回0
}

堆排序

上面已經說過創建堆後,那麼堆排序便已經呼之欲出了。堆排序,即我們每次從大根堆或者小根堆中取出一個最後,做一次堆的調整,直到該堆爲空後,就會得到一組有序的數值,即排序成功。也不難想到通過尾刪的方法來實現堆排序,當然這裏的尾刪是假尾刪。代碼如下

首先要想堆排序,我們先得建堆,具體怎麼建堆,上面已經說過。

void heapsort(int* array,int size)
{
	//建堆
	for(int parent = (size-2)/2;parent>=0;parent--)
	{
		shiftdownbig(array,size,parent);
	}
	//排序
	int end = size - 1;//取最後一個元素
	while(end>0)
	{
		swap(array,0,end);//交換根結點和最後一個結點
		end--;//長度假-1
		shiftdownbig(array,end,0);//調整當前爲止到根結點成一個新的堆
	}
	//該循環每結束一次,都會將最值放到最後,依次形成一個有序序列
}

值得注意的是,如果想要升序排序則需要建立大根堆,如果是降序排列則需要建立小根堆,小根堆的建立與大根堆建立類似,只是每個根結點都是最小值。大家可以下去自己寫。
今天的分享就到這裏,如果大家對創建堆以及堆排序有什麼不懂的地方,歡迎留言討論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章