樹&二叉樹&哈夫曼樹
Ⅰ 樹
由於樹的應用場合很少,不是很實用,所以在此只做簡單介紹。
A. 樹的概念
樹狀圖是一種數據結構,它是由n(n>=1)個有限結點組成一個具有層次關係的集合。把它叫做“樹”是因爲它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:
每個結點有零個或多個子結點;沒有父結點的結點稱爲根結點;每一個非根結點有且只有一個父結點;除了根結點外,每個子結點可以分爲多個不相交的子樹
樹的存儲結構:非線性結構,一對多
關於樹,有幾個相關的詞需要了解:節點, 葉子節點, 節點的度,樹的度,我簡單介紹一下有關樹的這幾個詞。
節點的度:一個節點的子節點數量,稱爲該節點的度。
葉子節點:度爲0的節點。
節點(非葉子節點):度不爲0的節點。
樹的度:樹中節點的度的最大值。
以以下這個樹爲例,我們來搞清楚關於樹的這幾個名詞。
樹的度:4
樹的層數(高度,深度):4
樹中的節點總數:18
葉子節點數:11
B. 樹的表達形式(存儲結構)
由於樹的每一個節點的最大子節點數量是不確定的,意味着其子節點數量不確定,因此,很難給出一個相對穩定的存儲結構。
typedef struct TREE_NODE {
USER_TYPE data;
int childCount;
struct TREE_NODE **children;
}TREE_NODE;
USER_TYPE 根據用戶需要存入的數據類型改變。
也可以用連續存儲空間表示一棵樹,如上面這顆可以表示爲👇
左邊一列爲下標。
但是這樣表示有個很大的缺陷:
當對樹中的節點做刪除操作時,不能移動其它節點,會造成空間浪費。
所以由於樹的操作複雜且耗時,所以樹的應用場合很少。
C. 樹的遍歷
樹的遍歷:無缺失、不重複地訪問樹中所有的節點,稱爲樹的遍歷。
a. 廣度優先遍歷(隊列)
將根節點入隊列;
while (隊列非空) {
隊首節點N出隊列,並訪問;
將N節點的所有子節點入隊列;
}
以下圖的樹爲例
按照上面僞代碼的步驟,先將根節點入隊列
[A]
隊首節點N出隊列,並訪問;
A [ ]
將N節點的所有子節點入隊列;
[B, C, D]
隊首節點N出隊列,並訪問;
B [C, D]
將N節點的所有子節點入隊列;
[C, D, E, F]
按照這樣循環,就可以得到這樣的結果:
這樣就完成了廣度優先遍歷。
b. 深度優先遍歷(堆棧)
將根節點入棧;
while (棧非空) {
棧頂節點N出棧,並訪問;
將N節點的所有子節點入棧;
}
同樣還是這個圖
按照上面僞代碼的步驟,先將根節點入棧。
(A)
棧頂節點N出棧,並訪問;
A ()
將N節點的所有子節點入棧;
(B, C, D)
然後再循環,棧頂節點N出棧,並訪問;
B (C, D)
將N節點的所有子節點入棧;
(E, F, C, D)
按此循環,將得到以下結果👇
這樣就完成了深度優先遍歷。
Ⅱ. 二叉樹
A. 二叉樹的有關概念
首先我們需要明確一件事情,二叉樹不是樹!
在計算機科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查找樹和二叉堆。
一棵深度爲k,且有2^k-1個結點的二叉樹,稱爲滿二叉樹。這種樹的特點是每一層上的結點數都是最大結點數。
完全二叉樹是效率很高的數據結構,完全二叉樹是由滿二叉樹而引出來的。對於深度爲K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度爲K的滿二叉樹中編號從1至n的結點一一對應時稱之爲完全二叉樹。
若設二叉樹的深度爲h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。如下圖。
完全二叉樹有以下幾種形態:
二叉樹和樹最大的區別是:二叉樹嚴格區分左右孩子
三個節點能構成幾種二叉樹?答案是5種。
B. 二叉樹中相關公式
N0 = N2 + 1, 即度爲0的節點數等於度爲2的節點數加1。
一顆高度爲h的二叉樹中,節點個數最多是2h - 1
一顆滿二叉樹的第h層的節點個數:2h-1
高h的滿二叉樹的節點個數:2h - 1
一個一維數組可以看成是一顆完全二叉樹。
一顆擁有n個節點的完全二叉樹,其最後一個非葉子節點的編號(從0開始編號):n / 2 - 1;
一顆擁有n個節點的完全二叉樹,且n爲偶數,其葉子節點的個數爲:n0 = n總 / 2;
假設完全二叉樹節點編號從0開始,則父節點與子節點(若存在的話),其編號的關係爲:假設父節點編號爲t,則其左孩子節點編號爲2 * t + 1,其有孩子節點編號爲2 * t + 2。
C. 二叉樹的存儲結構
typedef struct B_TREE_NODE {
USER_TYPE data;
struct B_TREE_NODE *leftChild;
struct B_TREE_NODE *rightChild;
}B_TREE_NODE, B_TREE;
有一個需要警惕的寫法👇
typedef struct B_TREE_NODE {
USER_TYPE data;
struct B_TREE_NODE *leftChild;
struct B_TREE_NODE *rightChild;
}B_NODE, *B_TREE;
有的資料甚至教材上直接在此定義了*B_TREE,這個寫法對程序的可讀性造成了極大的影響,因爲這個類型並不能反映這是指針類型,有的材料爲了書寫方便這樣定義了指針,使得代碼無比醜陋。
在定義類型的時候一定要注意程序的可讀性以及合理性。
Ⅲ 哈夫曼樹及編碼
哈夫曼壓縮方式:頻度壓縮
假設一個文本文件全部由英文字母構成,那麼哈夫曼壓縮就是將出現次數(頻度)最高的字母用最短的編碼來表示。ASCII碼佔1byte,即8bit,那麼將其用2bit的編碼來取代,則每個字符會節省6bit的空間。
A. 構造哈夫曼樹
我用一個字符串當作例子來構造一棵哈夫曼樹。
egeaefebaeeauefffeeffeebe
a. 頻度統計
要構造一棵哈夫曼樹,我們需要先知道每個字符的頻度。
字符 | 頻度 |
---|---|
a | 3 |
b | 2 |
e | 12 |
f | 6 |
g | 1 |
u | 1 |
b. 生成哈夫曼樹
我們需要找到最小頻度的節點,然後生成新節點,生成過程如下👇
第一步
第二步
第三步
第四步
第五步
這樣,一棵哈夫曼樹就構造完成了。
B. 哈夫曼編碼
上一步我們構造出了一棵哈夫曼樹,根據這棵樹我們現在進行編碼。
首先,我們對方向編碼,可以向左是0向右是1,也可以反過來,這只是一個編碼規則。我定爲向左爲0.
接下來就是從根節點沿着這棵樹編碼。
比如e,從根節點向左一次就到了,所以e的編碼爲:0
同樣的,比如f,需要從根節點先向右,再向左,所以f的編碼是:10
按照以上規律,我們可以寫出哈夫曼編碼
字符 | 編碼 |
---|---|
a | 110 |
b | 1110 |
e | 0 |
f | 10 |
g | 11110 |
u | 11111 |
所以egeaefebaeeauefffeeffeebe轉化成哈夫曼編碼爲:
011110011001001110110001101111101010100010100011100
哈夫曼編碼完成。
C. 解碼
解碼依舊是根據構造的哈夫曼樹,以及你制定的編碼規則。
011110011001001110110001101111101010100010100011100
根據這個編碼,我做幾個解碼。
從根節點,先向左,到了e,所以第一個是e,然後回到根節點。
向右,向右,向右,向右,向左,到了g,所以第二個是g,再回到根節點。
根據這個規律,按照之前構造的哈夫曼樹還原出這串字符串。
這就是解碼的實現。
關於哈夫曼編碼的應用,請看我後面的博文,用哈夫曼編碼完成文件的壓縮及解壓縮,是真真實實可以用的程序。
【C語言->數據結構與算法】->哈夫曼壓縮&解壓縮->第一階段->哈夫曼編碼&解碼的實現
【C語言->數據結構與算法】->哈夫曼壓縮&解壓縮->終局->壓縮率百分之八十六