1. 程式人生 > 其它 >堆——最小堆程式碼實現與分析(C++)

堆——最小堆程式碼實現與分析(C++)

技術標籤:資料結構c++

最近跟著黑皮書學習資料結構,主要程式碼均與書上相仿。程式碼量有些大,所以全程式碼之後丟在結尾。先摘抄部分來進行分析吧。

優先佇列(堆):

一種能夠賦予每個節點不同優先順序的資料結構。有“最小堆”和“最大堆”兩種基礎型別。實現的根本原理有兩種,一種是“陣列”,另外一種則是“樹”(大多是指二叉樹)。但在實現最大/最小堆時,使用陣列更優。因為堆並不像樹那樣需要很多功能支援,自然也不需要用到指標(當然,高階結構還是會用到的,比如“左式堆”等)。

如果您此前已經看過堆的基本結構概念,那應該大致明白堆長什麼樣了,基礎結構的堆就是一顆符合特定條件的二叉樹罷了。

特定條件:對每一個節點的關鍵字,都要比其子樹中的任何一個關鍵字都小(任何一個節點的關鍵字是其子樹以及其自身中最小的那個)。這個條件是針對最小堆的,最大堆則反之。

因為基礎的堆結構只支援“插入”和“最小值出堆”這兩種操作。在處理任務程序的時候,對應的也有“增加任務”和“處理任務量最少的任務”這種解釋,或許這樣更容易讓人明白堆的作用。而最大堆則可將其解釋為“處理優先順序最高的任務”。(當然,實際上還需要對任務量/優先順序進行變動,包括增/減關鍵字的大小這樣的操作,自然也能夠進行特定關鍵字的刪改了)。

//-----------宣告部分----------//
struct HeapStruct;
struct ElementType;
typedef struct HeapStruct* PriorityQueue;//堆指標
#define MinElements 1
#define Max 99999
bool IsEmpty(PriorityQueue H);//是否為空堆
bool IsFull(PriorityQueue H);//是否為滿堆

void Insert(ElementType key, PriorityQueue H);//插入關鍵字
ElementType DeleteMin(PriorityQueue H);//刪除最小值
PriorityQueue BuiltHeap(ElementType* Key, int N);//成堆
void PercolateDown(int i, PriorityQueue H);
PriorityQueue Initialize(int MaxElements);//建空堆
void IncreaseKey(int P, int Add, PriorityQueue H);//增加關鍵字值
void DecreaseKey(int P, int sub, PriorityQueue H);//降低關鍵字值
void Delete(int P, PriorityQueue H);//刪除關鍵字
struct ElementType//關鍵字資料塊
{
	int Key;
};
struct HeapStruct//堆結構
{
	int Capacity;
	int Size;
	ElementType* Element;
};
ElementType MinData;//最小資料塊

//-----------宣告部分----------//

看註釋大概就能明白了。但值得說明的是,因為最終是通過陣列來實現的,而陣列必須先行規定好它的尺寸,所以建立的堆也必須面臨“被裝滿”的情況(當然,用new函式重新開闢也行,誰讓這是C++呢)

建立空堆Initialize:

PriorityQueue Initialize(int MaxElements)//形參為堆的總節點數
{
	PriorityQueue H;
	if (MaxElements < MinElements)
		return NULL;
	H = new HeapStruct;
	H->Element = new ElementType[MaxElements + 1];
	H->Capacity = MaxElements;
	H->Size = 0;
	H->Element[0] = MinData;
	return H;
}

注:我並沒有把new函式失敗的情形寫出來,但那些內容並不影響對資料結構的學習。對這方面有需求請自行新增。

注:MinData是一個最小資料塊,同時也只是一個冗餘塊。在之後的任何操作中,都不會對存有MinData的Element[0]進行任何操作。只是通過佔用[0]節點,使得之後的操作變得更加可行了。需要注意的是,這個0節點並不是根節點(當時沒繞過來,在這裡浪費了太多時間)。

插入Insert:

void Insert(ElementType key, PriorityQueue H)
{
	int i;
	if (IsFull(H))
		exit;
	++H->Size;
	for (i = H->Size; H->Element[i / 2].Key > key.Key; i /= 2)
		H->Element[i] = H->Element[i / 2];
	H->Element[i] = key;
}

for迴圈中的判斷方式被稱之為“上濾”,也是這種方式得以實現的重要規則。對於根節點從 Element[1] 開始的這個堆,Element[i]的左兒子必然是Element[2*i],除非它沒有左兒子。

回到這個函式,因為int型會自動取整捨棄小數位,所以 Element[i/2] 必定指向 Element[i] 的父節點,不論它是不是單數。

而這個尋路條件則是在不斷的比較子節點與父節點的大小。流程如下:

①先將新節點放在陣列的最後一位(並不是指陣列的末尾,而是按照順序裝填的最後一位),然後比較它與父節點的大小。

②若它小於父節點,那麼將其與父節點交換位置,此時 i/=2 , Element[i]再次指向它。

③繼續相同操作。直到父節點小於它,或是沒有父節點為止。

不得不承認,這種操作很棒。因為它讓函式的最壞時間複雜度降到了logN(因為實際操作中,不一定都要上履到最頂層)。

最小值出堆DeleteMin:

ElementType DeleteMin(PriorityQueue H)
{
	int i, Child;
	ElementType MinElement, LastElement;
	if (IsEmpty(H))
		return H->Element[0];
	MinElement = H->Element[1];
	LastElement = H->Element[H->Size--];
	for (i = 1; i * 2 <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (LastElement.Key > H->Element[Child].Key)
			H->Element[i] = H->Element[Child];
		else
			break;
	}
	H->Element[i] = LastElement;
	return MinElement;
}

同“上濾‘相近,在這個函式中運用的方法為”下濾“。簡要談談過程吧:

①宣告各種各樣的變數,並判斷H是不是一個空堆。

②將堆中最小的值Element[1]拷貝到MinElement中,同理將最後一個值放進LastElement中。(這個Element[1]將會被新值替換,而這個LastElement則要用來填補某個空缺)

③從i=1開始,Child則指向根節點Element[1]的左兒子,同時比較根節點的左右兒子大小,將Child指向小的那一個,我是說,H->Element[Child]會指向小的那個。

④然後再判斷最後一個數和H->Element[Child] 的大小。如果最後一個比較大,那就把父節點Element[i]用它的子節點替代。

⑤重新回到迴圈,現在的 i 已經指向了本來的子節點,並開始重複上述從③開始的操作,直到當前Element[i]的子節點中較小的那一個Element[Child]比最後一個節點的值要小為止。

⑥將現在的父節點Element[i]用最後一個替代。

或許從途中就會覺得有些怪異,這究竟是個怎麼回事。

事實上,經過上述操作直到步驟⑤,最終的Element[i]將會指向某片葉子,這片葉子是根據其上的操作逐層篩選出來的。最後通過

	H->Element[i] = LastElement;

將這個位置用最後一位來替代,並返回了剛開始拷貝好的最小值,實現了刪除最小值的操作。當然,實際上,這個陣列的最後一位仍然儲存著某個關鍵字,但並不需要太擔心,因為經過了H->Size--,當下次插入節點的時候,遇到合適的數值,將會直接把這個位置覆蓋掉。並且,也如您所見,所有的操作單元均在[1,H->Size]的範圍內,對於範圍外的元素,即便它還留有關鍵字,也不會再造成影響了。

成堆BuiltHeap:

通常,我們將會匯入一整串陣列,然後再利用它們來生成一個堆結構。實際上,當然也可以通過Insert來一個個安置。以下是沒套用Insert的例程,主要通過遞迴來實現。

PriorityQueue BuiltHeap(ElementType *Key,int N)//Key指向將要匯入的資料陣列
{
	int i;
	PriorityQueue H;
	H = Initialize(N);
	for (i = 1; i <= N; i++)
		H->Element[i] = Key[i - 1];
	H->Size = N;
	for (i = N / 2; i > 0; i--)
		PercolateDown(i, H);
	return H;
}
void PercolateDown(int i,PriorityQueue H)
{
	int MinSon;
	ElementType Tmp;
	if (i <( H->Size / 2))
	{
		if (2 * i + 1 <= H->Size && H->Element[2 * i].Key > H->Element[2 * i + 1].Key)
			MinSon = 2 * i+1;
		else
			MinSon = 2 * i;
		if (H->Element[i].Key > H->Element[MinSon].Key)
		{
			Tmp = H->Element[i];
			H->Element[i] = H->Element[MinSon];
			H->Element[MinSon] = Tmp;
		}
		PercolateDown(MinSon, H);
	}
}

①宣告,並建立空堆,然後把所有元素全都不按規則的塞進去,再指定好H->Size。

②從最後一個具有“父節點”性質的節點進入下濾函式。過程與DeleteMin相近:選出子節點中小的,再與父節點比較,將較小的那一個放在父節點的位置,而較大的那一個下沉到子節點。並且再次進入這個函式。

③實現全部的過濾之後,返回H。

遞迴在這裡是非常好用的。在BuiltHeap函式中,for迴圈實現了對每一個具有“父節點”性質的節點進行下濾(這是根據陣列節點的排列順序實現的,父節點必然都能按順序排下去)。而遞迴則實現了對整條路徑的下濾操作。假設從根節點開始下濾,那麼必然會進入PercolateDown(MinSon,H)中,將較小的那個子節點作為本次遞迴的新的父節點同樣進行下濾。最終實現了堆序(Heap order)。

剩下的就是一些無關緊要的函數了,看看思路就行。因為是我自己寫的,可能會有錯誤,如有發現,還請務必告知我,我會盡量修正。

void DecreaseKey(int P,int sub,PriorityQueue H)//降低關鍵字的值
{
	H->Element[P].Key -= sub;
	int i;
	ElementType Tmp;
	for (i = P; H->Element[i / 2].Key > H->Element[i].Key; i /= 2)
	{
		Tmp = H->Element[i / 2];
		H->Element[i / 2] = H->Element[i];
		H->Element[i] = Tmp;
	}
}
void IncreaseKey(int P, int Add, PriorityQueue H)//提高關鍵字的值
{
	int i,Child;
	ElementType Tmp;
	H->Element[P].Key += Add;
	for (i = P; 2 * i <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (H->Element[i].Key > H->Element[Child].Key)
		{
			Tmp = H->Element[Child];
			H->Element[Child] = H->Element[i];
			H->Element[i] = Tmp;
		}
		else
			break;
	}
}
void Delete(int P,PriorityQueue H)//刪除指定關鍵字
{
	DecreaseKey(P, Max, H);
	DeleteMin(H);
}

原理同上面的其他函式一樣的,建議自己實現一下。

//----------------------最後是完整程式碼----------------------------//

#include<iostream>
using namespace std;
//------------------------//
struct HeapStruct;
struct ElementType;
typedef struct HeapStruct* PriorityQueue;
#define MinElements 1
#define Max 99999
bool IsEmpty(PriorityQueue H);
bool IsFull(PriorityQueue H);

void Insert(ElementType key, PriorityQueue H);
ElementType DeleteMin(PriorityQueue H);
PriorityQueue BuiltHeap(ElementType* Key, int N);
void PercolateDown(int i, PriorityQueue H);
PriorityQueue Initialize(int MaxElements);
void IncreaseKey(int P, int Add, PriorityQueue H);
void DecreaseKey(int P, int sub, PriorityQueue H);
void Delete(int P, PriorityQueue H);
struct ElementType
{
	int Key;
};
struct HeapStruct
{
	int Capacity;
	int Size;
	ElementType* Element;
};
ElementType MinData;
//-----------------------//
PriorityQueue Initialize(int MaxElements)//形參為堆的總節點數//最小堆
{
	PriorityQueue H;
	if (MaxElements < MinElements)
		return NULL;
	H = new HeapStruct;
	H->Element = new ElementType[MaxElements + 1];
	H->Capacity = MaxElements;
	H->Size = 0;
	H->Element[0] = MinData;
	return H;
}
void Insert(ElementType key, PriorityQueue H)
{
	int i;
	if (IsFull(H))
		exit;
	++H->Size;
	for (i = H->Size; H->Element[i / 2].Key > key.Key; i /= 2)
		H->Element[i] = H->Element[i / 2];
	H->Element[i] = key;
}
bool IsEmpty(PriorityQueue H)
{
	if (H->Size == 0)
		return true;
	else
		return false;
}
bool IsFull(PriorityQueue H)
{
	if (H->Size == H->Capacity)
		return true;
	else
		return false;
}
ElementType DeleteMin(PriorityQueue H)
{
	int i, Child;

	ElementType MinElement, LastElement;
	if (IsEmpty(H))
		return H->Element[0];
	MinElement = H->Element[1];
	LastElement = H->Element[H->Size--];
	for (i = 1; i * 2 <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (LastElement.Key > H->Element[Child].Key)
			H->Element[i] = H->Element[Child];
		else
			break;
	}
	H->Element[i] = LastElement;
	return MinElement;
}
void DecreaseKey(int P,int sub,PriorityQueue H)
{
	H->Element[P].Key -= sub;
	int i;
	ElementType Tmp;
	for (i = P; H->Element[i / 2].Key > H->Element[i].Key; i /= 2)
	{
		Tmp = H->Element[i / 2];
		H->Element[i / 2] = H->Element[i];
		H->Element[i] = Tmp;
	}
}
void IncreaseKey(int P, int Add, PriorityQueue H)
{
	int i,Child;
	ElementType Tmp;
	H->Element[P].Key += Add;
	for (i = P; 2 * i <= H->Size; i = Child)
	{
		Child = 2 * i;
		if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
			Child++;
		if (H->Element[i].Key > H->Element[Child].Key)
		{
			Tmp = H->Element[Child];
			H->Element[Child] = H->Element[i];
			H->Element[i] = Tmp;
		}
		else
			break;
	}
}
void Delete(int P,PriorityQueue H)
{
	DecreaseKey(P, Max, H);
	DeleteMin(H);
}
PriorityQueue BuiltHeap(ElementType *Key,int N)
{
	int i;
	PriorityQueue H;
	H = Initialize(N);
	for (i = 1; i <= N; i++)
		H->Element[i] = Key[i - 1];
	H->Size = N;
	for (i = N / 2; i > 0; i--)
		PercolateDown(i, H);
	return H;
}
void PercolateDown(int i,PriorityQueue H)
{
	int MinSon;
	ElementType Tmp;
	if (i <( H->Size / 2))
	{
		if (2 * i + 1 <= H->Size && H->Element[2 * i].Key > H->Element[2 * i + 1].Key)
			MinSon = 2 * i+1;
		else
			MinSon = 2 * i;
		if (H->Element[i].Key > H->Element[MinSon].Key)
		{
			Tmp = H->Element[i];
			H->Element[i] = H->Element[MinSon];
			H->Element[MinSon] = Tmp;
		}
		PercolateDown(MinSon, H);
	}
}