5.1 堆
當需要存儲一個不同優先級的優先隊列時(總是刪除最大/最小值,插入任意元素),我們就要用到堆,當然,用數組,鏈表,或樹都可以完成,但是算法複雜度不夠理想。
堆是一種特殊的樹,它的任意節點都比它的左右兒子大/小,所以它的根節點是最大的/最小的,刪除的時候只需要刪除最上面的根節點即可,插入時,爲了使空間利用率最大,按照完全二叉樹插入,這就是最大/小堆
還記得剛開始說二叉樹的時候,我們試過用數組存儲二叉樹,然而因爲利用率不高,只適合存完全二叉樹這種排的比較滿的樹,對於現在堆這種結構,我們就可以用數組來存,從上到下一層一層排序,第i個節點左兒子是2i,右兒子是2i+1,父節點是i/2。
操作集:
MaxHeap Create(int MaxSize);
Boolean IsFull(MaxHeap H);
Boolean IsEmpty(MaxHeap H);
MaxHeap Insert(ElementType x, MaxHeap H);
ElementType Delete(MaxHeap H);
- 最大堆的結構
typedef struct HeapStruct *MaxStruct;
struct HeapStruct{
//指向數組
ElementType *Elements;
//現有元素個數
int Size;
//最大容量
int Capacity;
};
- 創建一個最大堆
MaxHeap Create(int MaxSize){
MaxHeap H = malloc(sizeof(struct HeapStruct));
//數組空出一個位置存放最大值作爲哨兵
H->Elements = malloc(sizeof((MaxSize+1)*ElementType));
H->Size = 0;
H->Capacity = MaxSize;
//宏定義一個數據裏面的最大可能值,插入時可以減少一個判斷條件。
H->Elements[0] = MaxData;
return H;
}
- 插入
MaxHeap Insert(ElementType x, MaxHeap H){
int i;
if(IsFull(H)){
printf("已滿!");
return NULL;
}
//改變最大堆信息的同時,告訴i應該插入的位置
i = ++H->Size;
for(;H->Elements[i/2] < x;i /= 2){
H->Elements[i] = H->Elements[i/2];
}
H->Elements[i] = x;
return H;
}
- 刪除
ElementType Delete(MaxHeap H){
if( IsEmpty(H) ){
printf("堆空!");
return NULL;
}
ElementType MaxItem = H->Elements[1];
ElementType item;
int parent = 1;
int child;
item = H->Elements[H->Size--];
for(;2*parent <= H->Size;parent = child){
child = 2 * parent;
if( child != H->Size){
if( H->Elements[child] < H->Elements[child+1])
child++;
}
if(item >= H->Elements[child])break;
else
H->Elements[parent] = H->Elements[child];
}
H->Elements[parent] = item;
return MaxItem;
}
給定一個序列,創建一個堆,可以通過不斷插入的方式創建。但是有效率更高的方法,先把序列存入完全二叉樹中,然後從最後一個有兒子的節點開始調整,形成一個節點,他的左右兒子都是堆的遞歸
//這段代碼未經驗證,有什麼問題希望能反饋給我……
MaxHeap Create(ElementType T[Size]){
MaxHeap H;
for(int i = 0; i < Size; i++)
H->Elements[++H->Size] = T[i];
int parent = H->Size/2;
int child;
ElementType t;
for(;parent>0;parent--){
for(;2*parent <= H->Size;parent = child){
child = 2 * parent;
if( child != H->Size){
if( H->Elements[child] < H->Elements[child+1])
child++;
}
if(H->Elements[parent] >= H->Elements[child])break;
else{
t = H->Elements[parent];
H->Elements[parent] = H->Elements[child];
H->Elements[child] = t;
}
}
}
}
5.2 哈夫曼樹和哈夫曼編碼
查找或者使用數據時,不是平均的使用每一個數據,而是有的數據頻率高,有的數據頻率低,不同的頻率可以視爲數據不同的權重,這些帶權重的數據在樹中的分佈會影響到查找的效率。
樹中的每個葉節點都有自己的權重,從根節點到葉節點經過的路徑乘以葉節點的權重作爲某個葉節點的帶權路徑長度,所有的葉節點的帶權路徑長度之和就是該樹的帶權路徑,哈夫曼樹就是帶權路徑最小的樹。
構造哈夫曼樹: 在數據中找到權值最小的的兩個,組成一個樹,這棵樹的權值就是他們兩個權值的和,然後這棵新樹的根節點作爲新的節點放入原來的數據中。
typedef struct TreeNode *HuffmanTree;
struct TreeNode{
int weight;
HuffmanTree Right,Left;
};
//事先將元素按權值構成最小堆。傳入最小堆,返回哈夫曼樹。
HuffmanTree Huffman(MinHeap H){
HuffmanTree T;
for(int i = 1; i < H->Size; i++){
T = malloc(sizeof(struct TreeNode));
T->Left = Delete(H);
T->Right = Delete(H);
T->weight = T->Left->weight+T->Right->weight;
Insert(T,H);
}
T = Delete(H);
return T;
}
哈夫曼樹有幾個特點:
- 沒有隻有一個兒子的節點(也就是度爲1的節點)
- 如果有n個葉節點,總結點數爲2n-1
- 如果有權值一樣的元素,有可能會構成不同結構的樹,但是他們的總路徑是相同的
- 哈夫曼樹左右兒子交換位置之後任然爲哈夫曼樹。
哈夫曼編碼:對於使用頻率高的字符,我們需要用更簡潔的表示方法,但是用不等長的編碼方法就會出現二義性問題,同樣的二進制碼可以表示不同的意義,爲了避免這種情況,我們就要用到哈夫曼樹,每個節點的分支都有0和1來表示,要編碼的字符都在樹的葉節點上,這樣就可以避免二義性問題,再把不同頻率作爲權值,就可以用哈夫曼樹做哈夫曼編碼。(這讓我想到了有的CPU指令集)。
5.3 集合及其運算
把一個個的元素看成一個個的集合,如果兩個元素之間聯通,那麼就把這兩個集合並起來。
集合我們只關心兩個問題,並集和查找,查找就是找到元素所在的那個集合,爲此,我們用樹來存儲集合,用樹的根表示該集合,求並集的時候只需要把一棵樹接在另一個樹上即可。
用樹的結構表示集合,在計算機中,我們可以用結構數組來存儲這種樹,Parent表示該節點的父節點在數組中的位置,根節點的Parent爲-1,注意,一個數組可以存好幾個不同的集合,也可以只有一個集合。
其實就是數據庫中的那種遞歸字段
typedef struct {
ElementType Data;
int Parent;
}SetType;
- 查找,輸入結構數組和要查找的元素,返回元素所在的集合(即該集合樹的根節點)。
int Find(ElementType x, SetType S[]){
int i;
for(i = 0; i < MaxSize && S[i].Data != x; i++);
if(i >= MaxSize)return -1;
//此處的條件爲大於等於0,其實也可以用“不等於-1”,但是因爲後面的並操作會用
//根節點的Parent值存儲別的信息,所以不用“不等於-1”
for(;S[i].Parent >= 0;i = S[i].Parent);
return i;
}
- 並,parent值用集合元素個數的負值表示(負值小的個數多)
void Union(SetType S[], ElementType x1, ElementType x2){
int root1,root2;
root1 = Find(x1,S);
root2 = Find(x2,S);
if(root1 != root2){
if(S[root1].Parent < S[root2].Parent){
S[root1].parent += S[root2].parent;
S[root2].parent = root1;
}
else{
S[root2].parent += S[root1].parent;
S[root1].parent = root2;
}
}
}