樹形結構是一種十分重要的數據結構。二叉樹、樹與樹林都屬于樹形結構。
樹形結構每個結點最多只有一個前驅結點,但可以有多個后繼的結構。
5.1 二叉樹及其抽象數據類型
5.1.1 基本概念
二叉樹可以定義為結點的有限集合,這個集合或者為空集,或者由根結點、左子樹和右子樹的二叉樹組成。二叉樹是一個遞歸定義。
相關概念
父結點、左右孩子結點、邊:
若x是某二叉樹的根結點,結點y是x的左(右)子樹的根,則稱x是y的父結點,y是x的左(右)孩子結點。有序對<x, y>稱作從x到y的邊。
兄弟:
具有同一父結點的結點彼此稱作兄弟。
祖先、子孫:
如果結點y在以結點x為根的左右子樹中,且 y != x,則稱x是y的祖先,y是x的子孫。
路徑、路徑長度:
如果x是y的一個祖先,又有x = x0, x1,..., xn = y,滿足 xi 為xi+1的父結點則稱x0, x1,..., xn 為x到y的一條路徑,n稱為路徑長度
結點的層數:
規定根的層數為0,其余結點的層數等于其父結點的層數加一。
結點的度數(邊數):
結點的非空子樹(即后綴)個數叫作結點的度數。在二叉樹中,結點的度數最大為2,即最多有兩條邊。 度數 = 總結點數 - 1
二叉樹的高度(深度):
二叉樹中結點的最大層數稱為二叉樹的高度。
樹葉、分支結點:
左(右)子樹均為空二叉樹的結點稱為樹葉,否則稱為分支結點。
特殊二叉樹
滿二叉樹:
如果一顆二叉樹的任何結點或是樹葉,或有兩顆非空子樹,則稱為滿二叉樹,結點度數一定為0或者2.
完全二叉樹:
如果一顆二叉樹中,只有最下面兩層結點度數小于2,其余各層結點度數都等于2,并且最下面一層的結點都集中在該層最左邊的若干位置上,則此二叉樹稱為完全二叉樹。
擴充的二叉樹:
擴充的二叉樹是對一個已有二叉樹的擴充,擴充后原二叉樹的結點都變為度數為2的分支結點。也就是說,如果結點的度數為2,則不變,度數為1,則增加一個分支,度數為0則增加兩個分支。增加的結點稱為外部結點,原有的結點稱為內部結點。
5.1.2 主要性質
一般二叉樹的性質
性質1 在非空二叉樹的i層上,至多有2^i個結點(i >= 0)
性質2 在高度為k的二叉樹上,最多有2^(k+1) - 1個結點(k >= 0)
性質3 對于任意一顆非空的二叉樹,如果葉結點的個數為n0,度數為2的結點個數為n2,則 n0 = n2 + 1。
證明: B為邊的總數,n為結點總數,則 n = n0 + n1 + n2, B = n - 1。 則 B = n0 * 0 + n1 * 1 + n2 * 2, 由此可以求得 n0 = n2 + 1。
完全二叉樹的性質
性質4 具有n個結點的完全二叉樹高度k為lgn
性質5 對于具有n個結點的完全二叉樹,如果按照以上(從根結點)到下(葉結點)和從左到右的順序對二叉樹中的所有結點從0開始到n-1進行編號,則對于任意的下標為i的結點,有:
(1) 如果i = 0, 則它是根結點,如果i>0, 則它的父結點的下標為 (i - 1) / 2
(2) 如果2i+1 <= n-1, 則下標為i的結點的左孩子結點的下標為2i+1, 否則下標為i的結點沒有左孩子結點
(3) 如果2i+2 <= N-1, 則下標為i的結點的右孩子結點的下標為2i+2,否則下標為i的結點沒有右孩子結點
滿二叉樹性質
性質6 在滿二叉樹中,葉節點的個數比分支結點個數多1.
擴充的二叉樹性質
性質7 在擴充二叉樹中,外部結點的個數比內部結點的個數多1。這個由性質6和內外部結點的定義可以得到
性質8 對任意擴充的二叉樹,外部路徑長度E和內部路徑長度I之間滿足以下關系: E = I + 2n, 其中n是內部結點的個數
5.1.3 抽象數據類型
假設 BinTree 表示二叉樹類型,用BinTreeNode 表示二叉樹中結點的類型,作為抽象數據類型二叉樹可以提供的操作十分豐富。在ADT BinTree中,定義了最常見的操作如下:
ADT BinTree is
operations
// 創建一顆空的二叉樹
BinTree createEmptyBinTree(void);
// 返回一顆二叉樹,其根結點是root,左右二叉樹分別為left 和 right
BinTree consBinTree(BinTreeNode root, BinTree left, BinTree right);
// 判斷二叉樹是否為空
int isEmpty(BinTree tree);
// 返回二叉樹的根結點
BinTreeNode root(BinTree tree);
// 返回結點p的雙親結點
BinTree parent(BinTree tree, BinTreeNode p);
// 返回p結點的左子樹
BinTree leftChild(BinTree tree, BinTreeNode p);
// 返回p結點的右子樹
BinTree rightChild(BinTree tree, BinTreeNode p);
end ADT BinTree
5.2 二叉樹的遍歷
5.2.1 什么是遍歷?
二叉樹的遍歷是一種按某種方式系統地訪問二叉樹中的所有結點的過程,使每個結點都被訪問一次且只被訪問一次。
5.2.2 遍歷的分類
遍歷的方法尅分成兩類,一類是廣度優先遍歷,一類是深度優先遍歷。
深度優先遍歷
二叉樹的遍歷有6種,如果限定從左到右,則只能采用三種,即先根次序遍歷、后根次序遍歷和中根次序遍歷。
先根次序 先訪問根,然后先序遍歷左子樹,再先序遍歷右子樹
后根次序 先后序遍歷左子樹,然后后序遍歷右子樹,再遍歷根
中根次序 先中序遍歷左子樹,然后遍歷根,然后中序遍歷右子樹
廣度優先遍歷
若二叉樹的高度為h,則從0到h逐層如下處理:從左到右逐個訪問存在的結點
廣度優先遍歷一顆二叉樹所得到的結點序列,叫作這顆二叉樹的層次序列
5.2.3 一個例子 (略過)
5.2.4 遍歷的抽象算法
二叉樹的先序遍歷遞歸描述:
void preTreeWalk(BinTree tree) {
if (tree == NULL) {
return;
}
visit(root(tree));
preTreeWalk(leftChild(tree));
preTreeWalk(rightChild(tree));
}
二叉樹的先序遍歷非遞歸描述:
void iterativePreTreeWalk(BinTree tree) {
Stack s;
BinTreeNode *c;
if (tree == NULL) {
return;
}
s = createEmptyStack();
push(s, t);
while(!isEmptyStack(s)) {
c = top(s);
pop(s);
if (c != NULL) {
visit(root(c));
push(s, rightChild(c));
push(s, leftChild(c));
}
}
}
二叉樹的中序遍歷遞歸描述:
void inTreeWalk(BinTree tree) {
if (tree == NULL) {
return;
}
inTreeWalk(leftChild(tree));
visit(root(tree));
inTreeWalk(rightChild(tree));
}
二叉樹的中序遍歷非遞歸描述:
void iterativeInTreeWalk(BinTree tree) {
Stack s = createEmptyStack();
BinTree c = t;
if (c == NULL) {
return;
}
do {
while (c != NULL) {
push(s, c);
c = leftChild(c);
}
c = top(s);
pop(s);
visit(root(c));
c = rightChild(c);
} while (c != NULL || !isEmptyStack(s));
}
二叉樹的后序遍歷遞歸描述:
void postTreeWalk(BinTree tree) {
if (tree == NULL) {
retur;
}
postTreeWalk(leftChild(tree));
postTreeWalk(rightChild(tree));
visit(root(tree));
}
二叉樹的后序遍歷非遞歸描述:
void iterativePostTreeWalk(BinTree tree) {
Stack s = createEmptyStack();
BinTree pr;
BinTree p = tree;
while (p != NULL || !isEmptyStack(s)) {
// 將左子樹壓入棧中
while (p != NULL) {
push(s, p);
pr = rightChild(p);
p = leftChild(p);
if (p == NULL) {
p = pr;
}
}
// 從棧頂取出元素
p = top(s);
pop(s);
// 訪問元素
visit(root(p));
// 取得右子樹
if (!isEmptyStack(s) && leftChild(top(s)) == p) {
p = rightChild(top(s));
} else {
p = NULL;
}
}
}
廣度優先遍歷二叉樹的偽代碼描述如下:
void levelTreeWalk(BinTree tree) {
BinTree c, cc
Queue q = createEmptyQueue();
if (tree == NULL) {
return;
}
c = tree;
enQueue(q,c)
while (!isEmptyQueue(q)) {
c = frontQueue(q);
deQueue(q);
visit(root(c));
cc = leftChild(c);
if (cc != NULL) {
enQueue(q, cc);
}
cc = rightChild(c);
if (cc != NULL) {
enQueue(q, cc);
}
}
}
5.3 二叉樹的實現
5.3.1 順序表示
二叉樹的順序表示,也是采用一組連續的存儲單元來存放二叉樹中的結點。對于完全二叉樹,只要通過數組下標的關系,就可以確定結點之間的邏輯關系,其他類型二叉樹無法根據存儲的先后順序確定
順序表示的二叉樹定義如下:
struct SeqBinTree {
int MAXIMUM; // 允許結點的最大個數
int n; // 改造成完全二叉樹后,結點的實際個數
DataType * nodelist; // 存放結點的數組
};
typedef struct SeqBinTree *PSeqBinTree; // 順序二叉樹的指針類型
運算的實現
下標為p的結點雙親結點的下標:
int parent(PSeqBinTree tree, int p) {
if (p < 0 || p >= tree->n) {
return -1;
}
return (p - 1) / 2;
}
下標為p的結點左孩子結點的下標:
int leftChild(PSeqBinTree tree, int p) {
if (p < 0 || p >= tree-> n) {
return -1;
}
return 2 * p + 1;
}
下標為p的結點左孩子結點的下標:
int rightChild(PSeqBinTree tree, int p) {
if (p < 0 || p >= tree-> n) {
return -1;
}
return 2 * (p + 1);
}
顯然,順序表示對完全二叉樹比較合適,既可以節省空間,又可以利用數組元素的下標確定結點在二叉樹中的位置以及結點之間的關系。
5.3.2 鏈表表示
二叉樹的鏈表表示是用一個鏈表來存儲一顆二叉樹,二叉樹中的每個結點對應鏈表中的一個結點。
每個結點可以形象地描述為:
|left|right|info|
C語言描述如下:
typedef struct BinTreeNode {
DataType info;
struct BinTreeNode * left;
struct BinTreeNode * rightl
} BinTreeNode;
運算的實現
返回p結點的左孩子結點的指針:
BinTreeNode *leftChild(BinTreeNode *p) {
if (p == NULL) {
return NULL;
}
return p->left;
}
返回p結點的右孩子結點的指針:
BinTreeNode *rightChild(BinTreeNode *p) {
if (p == NULL) {
return NULL;
}
return p->right;
}
實現求雙親結點的操作比較困難,需要從根結點出發查找當前結點的位置。為了方便使用,可以增加一個雙親指針parent
5.3.3 線索二叉樹
線索二叉樹是對左-右指針表示法的一種修改
它利用空的左指針存儲該節點的某種遍歷序列前驅結點的位置,利用空的右指針在同種遍歷序列中的后繼結點的位置
這種附加的指向前驅和后繼結點額指針稱為線索。
為了區分左右指針和線索,需要在每個結點里面增加兩個標志位ltag 和rtag,當tag置為1時,表示線索
用C語言表述如下:
typedef struct ThreadTreeNode {
DataType *info;
struct ThreadTreeNode * left;
struct ThreadTreeNode * right;
int ltag, rtag;
} ThreadTreeNode, ThreadTree;
中序線索化二叉樹:
void threadTree(ThreadTree *tree) {
// 創建一個M大小的空順序棧,M一般為樹的高度
SeqStack *st = createEmptyStack(M);
ThreadTree *p, *pr;
if (tree == NULL) {
return;
}
p = tree;
pr = NULL;
do {
while (p != NULL) {
push(st, p);
p = p->left;
}
p = top(st);
pop(st);
if (pr != NULL) {
if (pr->right == NULL) {
pr->right = p;
pr->rtag = 1;
}
if (p->left == NULL) {
p->left = pr;
p->ltag = 1;
}
}
pr = p;
p = p->right;
} while (!isEmptyStack(st) || p != NULL);
}
構造中序線索二叉樹的最大意義是:可以很方便地從中找到指定結點在中序序列中的前驅和后繼,而不必重新遍歷二叉樹
中序遍歷中序線索二叉樹:
void threadInTreeWalk(ThreadTree *tree) {
ThreadTree *p = tree;
if (tree == NULL) {
return;
}
while (p->left != NULL && p->ltag == 0) {
p = p->left;
}
while (p != NULL) {
visit(*p);
if (p->right != NULL && p->rtag == 0) {
p = p->right;
// 右子樹的左子樹一直向下
while (p->left != NULL && p->ltag == 0) {
p = p->left;
}
} else {
p = p->right;
}
}
}
5.4 二叉樹的應用
5.4.1 堆與優先隊列
首先給出堆得定義:n個元素的序列 K = (k0, k1,..., kn-1) 稱為堆,當且僅當滿足條件:
ki >= k(2i+1) && ki >= k(2i+2)
或者
ki <= k(2i+1) && ki <= k(2i+2)
這個特征稱為堆序性。 如果堆根結點最小,則稱為小根堆,根結點最大,則稱為大根堆
優先隊列
優先隊列是一種常見的抽象數據類型,跟普通的隊列不同,不遵循“先進先出”的原則,而遵循“最小元素先出”的原則。優先隊列的基本操作有三種:
添加元素,找出做小元素和刪除優先隊列中的最小元素
優先隊列的抽象數據類型如下:
ADT PriorityQueue is
Operations
// 創建一個空的優先隊列
PriorityQueue createEmptyPriQueue(void);
// 判斷隊列是否為空
int isEmpty(PriorityQueue s);
// 添加元素
void add(PriorityQueue s, DataType data);
// 返回最小元素
DataType min(PriorityQueue s);
// 刪除最小元素
void removeMin(PriorityQueue s);
end ADT PriorityQueue
在優先隊列中找出最小元素并刪除:
DataType deleteMin(PriorityQueue pq) {
DataType result;
result = min(pq);
removeMin(pq);
return result;
}
優先隊列的實現
(1) 存儲結構
優先隊列的定義與二叉樹的順序表示基本一樣:
typedef struct PriorityQueue {
int MAXNUM; // 堆中的元素個數上限
int n; // 堆中的實際元素個數
DataType *pq; //堆中元素的順序表示
} PriorityQueue;
(2) 操作的實現
向優先隊列中插入一個元素:
void addHeap(PriorityQueue *queue, DataType x) {
int i;
if (queue->n >= MAXNUM - 1) {
printf("Full !\n");
return;
}
for (i = queue->n; i >0 && queue->pq[(i - 1) / 2] > x; i = (i - 1) / 2) {
queue->pq[i] = queue->pq[(i - 1) / 2];
}
queue->pq[queue->i] = x;
queue->n++;
}
從優先隊列中刪除最小元素:
void removeMin(PriorityQueue *queue) {
int s;
if (isEmptyHeap(queue)) {
printf("Empty!\n");
return;
}
s = --queue->n;
queue->pq[0] = queue->pq[s];
sift(queue, s, 0);
}
把完全二叉樹從指定結點調整為堆:
void sift(PriorityQueue *queue, int size, int p) {
DataType temp;
int i, child;
temp = queue->pq[queue->p];
i = p;
child = 2 * i + 1;
while (child < size) {
if (child < size-1 && queue->pq[child].key >queue->pq[child + 1].key) {
child++;
}
if (temp.key > queue->pq[child].key) {
queue->pq[i] = queue->pq[child];
i = child;
child = 2 * i + 1;
} else {
break;
}
}
queue->pq[i] = temp;
}
5.4.2 哈夫曼樹及其應用
若用E表示某擴充二叉樹的外部路徑長度,則有:
E = ∑li, i = 1 to m
其中li為從根到第i個外部結點的路徑長度,m為外部結點的個數。
設擴充二叉樹具有m個帶有權值得外部結點,那么從根結點到外部結點的路徑長度與相應權值的乘積和,叫做擴充二叉樹的帶權外部路徑:
WPL = ∑wili,i = i to m
wi是第i個外部結點的權值
假設有一組(無序)實數{w1,w2,w3,...,wm}, 現要構造一顆以wi為權的m個外部結點的擴充二叉樹,使得帶權的外部路徑長度WPL最小,滿足這一要求的擴充二叉樹就被稱作哈夫曼樹,又稱最優二叉樹。
例子:
給出帶權是{ 2,3,4,11 }, 可以構造出不同的擴充二叉樹,其中三種如下:
O O O
/ \ / \ / \
11 O O 2 O O
/ \ / \ / \ / \
4 O 3 O 2 11 3 4
/ \ / \
2 3 4 11
(a) (b) (c)
上面的帶權外部路徑長度分別為:
(a) WPL = 1x11 + 2x4 + 3x2 + 3x3 = 34
(b) WPL = 1x2 + 2x3 + 3x4 + 3x11 = 53
(c) WPL = 2x2 + 2x11 + 2x3 + 2x4 = 40
由此可見,對于一組帶有確定權值的外部結點,構造出不同擴充二叉樹,帶權外部路徑長度并不相同。
哈夫曼樹的構造
從上面的例子可以看出,一棵擴充二叉樹要使得WPL最小,必須使權值越大的外部結點離根越近,權值越小離根越遠。使用哈夫曼算法可以構造一棵最優二叉樹。
算法的基本思想:
(1) 由給定的m個權值{w1,w2,w3,...,wm},構造m棵由空二叉樹擴充得到的擴充二叉樹{T1,T2,...,Tm}。每個Ti(1<= i <= m)只有一個外部結點,其權值外wi.
(2) 在已經構造的所有擴充二叉樹中,選取根結點的權值最小和次最小的兩棵,將其作為左、右子樹,構造成一棵新的擴充二叉樹,根結點的權值置為左、右子樹根結點權值之和
重復步驟(2),每次都使擴充二叉樹的個數減一,當只剩一棵擴充二叉樹時,它便是所要構造的哈夫曼樹。
數據結構:
C語言定義為:
typedef struct HTNode {
int wpl; // WPL權值
int parent; // 雙親結點下標,無則置為-1
int left; // 左孩子結點下標,無則置為-1
int right; // 右孩子結點下標,無則置為-1
} HTNode;
typedef struct HTTree {
int m; // 外部結點個數
int root; // 根結點下標
HTNode *hTree; // 存放 2xm-1個結點的數組
} HTTree;
哈夫曼算法:
HTTree *huffmanTree(int m, int *w) {
HTTree *pht;
int i, j, x1, x2, m1, m2;
pht = (HTTree *) malloc(sizeof(HTTree));
if (pht == NULL) {
printf("Out of space!\n");
return pht;
}
pht->hTree = (HTNode *) malloc(sizeof(HTNode));
// 設置數組初始值
for (i = 0; i <2 * m-1; i++) {
pht->hTree[i].left = -1;
pht->hTree[i].right = -1;
pht->hTree[i].parent = -1;
if (i < m) {
pht->hTree[i].wpl = w[i];
} else {
pht->hTree[i].wpl = -1;
}
}
for (i = 0; i < m - 1; i++) {
m1 = MAXINT; // 最小權值
m2 = MAXINT; // 次最小權值
x1 = -1; // 最小下標
x2 = -1; // 次最小下標
// 找出最小權的無雙親結點的結點
for (j = 0; j < m+i; j++) {
if (pht->hTree[j].wpl < m1 && pht->hTree[i].parent == -1) {
m2 = m1;
x2 = x1;
m1 = pht->hTree[j].wpl;
x1 = j; // x1存放最小權的無雙親結點的結點下標
} else if (pht->hTree[j].wpl < m2 && pht->hTree[j].parent == -1) {
m2 = pht->hTree[j].wpl;
x2 = j; // x2存放次最小權的無雙親結點的結點下標
}
}
// 構造內部結點
pht->hTree[x1].parent = m + i;
pht->hTree[x2].parent = m + i;
pht->hTree[m+i].wpl = m1 + m2;
pht->hTree[m+i].left = x1;
pht->hTree[m+i].right = x2;
}
// 根結點的位置
pht->root = 2 * m - 2;
return pht;
}
哈夫曼編碼:
設
d = {d1,d2,...,dn}為需要編碼的字符集合
w = {w1,w2,...,wn}為d中各個字符出現的概率
現要對d進行二進制編碼,使得:
(1) 按給出的編碼傳輸文件時,通訊編碼總長最短
(2) 若di != dj,則di的編碼不可能是dj編碼的開始部分(前綴)
滿足上述要求額二進制編碼稱為最優前綴編碼
最優前綴編碼(哈夫曼編碼)可以用哈夫曼樹來實現:
d1,d2,..,dn作為外部結點,w1,w2,...,wn作為外部結點的權值,構建哈夫曼樹。在哈夫曼樹中,把從每個結點的指向左孩子結點的邊標上二進制數"0",指向右孩子的邊標上二進制數"1"。從根到每個葉結點路徑上的二進制數連接起來,就是這個葉節點所代表的最優前綴編碼。這種編碼叫作哈夫曼編碼。
編碼的結果是,出現概率大的字符其編碼較短,出現概率小的字符其編碼較長。
解碼時,從二叉樹的根結點開始,用需要編碼的二進制位串,從頭開始與二叉樹根結點到子結點邊上標的0、1相匹配,確定一條到達樹葉結點的路徑,一旦到達樹葉結點,則譯出一個字符,然后再回到根結點,從二進制位串中的下一位開始繼續解碼。
5.5 樹及其抽象數據類型
樹形結構在客觀世界是大量存在的。一棵樹幾種不同的表現形式:樹形、文氏圖、凹入表、嵌套括號
5.5.1 基本概念
樹氏包含 n(n>=0) 個結點的有窮集合T,當T非空時滿足:
(1) 有且僅有一個特別標出的稱作根的結點
(2) 除了根結點外,其余結點分別為若干個不相交的非空集合T1,T2,...,Tm,這些集合中的每一個又都是樹。樹T1,T2,...,Tm稱作這個根結點的子樹
只包括一個結點的樹是僅由根結點構成。不包含任何結點的樹稱作空樹。
樹中的一個結點的子結點個數叫作這個結點的度數。其中度數最大的結點的度數叫作樹的度數。
對于子樹的次序不加區別的樹叫作無序樹,對于子樹之間的次序加以區別的樹叫作有序樹。
5.5.2 抽象數據類型
樹型結構的抽象數據結構如下:
ADT Tree is
Operations
// 創建一棵空樹
Tree createEmptyTree(void)
// 以p為根,t1,...,ti為子樹創建一顆樹
Tree consTree(Node p, Tree t1, ... Tree ti)
// 判斷樹是否為空
int isEmpty(Tree t)
// 父結點
Node parent(Node p)
// 左孩子結點
Tree leftChild(Tree t)
// 右兄弟樹
Tree rightSibling(Tree t);
end ADT Tree
5.5.3 樹的遍歷
樹的遍歷是一種按某種方式系統地訪問樹中的所有結點的過程,它使每個結點都被訪問一次并且只訪問一次。
深度優先遍歷
先序遍歷 —— 首先訪問根結點,然后從左到右按先序遍歷根結點的每棵子樹
后序遍歷 —— 首先從左到右按后序遍歷根結點的每棵子樹,最后訪問根結點
先序遍歷的遞歸算法:
void preTreeWalk(Tree *tree) {
Tree *subTree;
if (tree == NULL) {
return;
}
visit(root(tree));
subTree = leftChild(tree);
while (subTree != NULL) {
preTreeWalk(subTree);
subTree = rightSibling(subTree);
}
}
先序遍歷的非遞歸算法:
void iterativeTreeWalk(Tree *tree) {
Tree *subTree = tree;
Stack *s = createEmptyStack();
do {
while (subTree != NULL) {
visit(root(subTree));
push(s, subTree);
subTree = leftChild(subTree);
}
while ((subTree == NULL) && !isEmptyStack(s)) {
subTree = rightSibling(top(s));
pop(s);
}
} while (subTree != NULL);
}
后序遍歷的遞歸算法:
void postTreeWalk(Tree *tree) {
Tree *subTree;
if (tree == NULL) {
return;
}
subTree = leftChild(tree);
while(subTree != NULL) {
postTreeWalk(subTree);
subTree = rightSibling(subTree);
visit(root(tree));
}
}
廣度優先遍歷算法:
void levelTreeWalk(Tree *tree) {
Tree *subTree;
Queue *queue;
queue = createEmptyQueue();
subTree = tree;
if (subTree == NULL) {
return;
}
// 將子樹入隊
enQueue(queue, subTree);
while (!isEmptyQueue(queue)) {
// 不斷從隊列中取出子樹
subTree = frontQueue(queue);
deQueue(queue);
// 訪問子樹的根結點
visit(root(subTree));
// 找到長子
subTree = leftChild(subTree);
while (c != NULL) {
// 子樹入隊
enQueue(queue, subTree);
// 找到當前子樹的右兄弟子樹入隊
subTree = rightSibling(subTree);
}
}
}
5.6 樹的實現
5.6.1 父指針表示法
用一組連續的存儲空間,存儲樹中的各個結點,數組中的一個元素為一個結構,其中包含結點本身的信息以及本結點的父結點在數組中的下標,樹的這種存儲放方法稱為父指針表示法。
結構體定義如下:
struct ParTreeNode{
DataType info;
int parent;
};
樹的定義如下:
typedef struct ParTree {
int MAXNUM;
int n;
ParTreeNode *nodeList;
} ParTree;
求兄弟結點的位置:
int rightSibling(ParTree *tree, int p) {
int i;
if ( p >= 0 && p < tree->n) {
for (i = p + 1; i < tree->n; i++) {
if (tree->nodeList[i].parent == tree->nodeList[p].parent) {
return i;
}
}
}
return -1;
}
求左孩子結點的位置:
int leftChild(ParTree *tree, int p) {
if (tree->nodeList[p + 1].parent == p) {
return p + 1;
} else {
return -1;
}
}
父指針表示法比較節省存儲空間,但求某個結點的兄弟運算比較慢。
5.6.2 子表表示法
重要而常用的表示方法。把整棵樹表示成一個結點表,而結點表中的每個元素又包含一個表,它記錄了這個結點的所有子結點的位置,稱為子表。結點表的長度即樹中結點的個數們一般用一維數組順序存儲。
子表表示法定義如下:
typedef struct EdgeNode {
int nodePosition;
struct EdgeNode *link;
} EdgeNode;
結點表中每個結點定義如下:
typedef struct ChildTreeNode {
DataType info;
EdgeNode *children;
} ChildTreeNode;
子表表示的樹結構定義如下:
typedef struct ChildTree {
int MAXNUM;
int n;
ChildTreeNode *nodeList;
} ChildTree;
求右兄弟結點的位置:
int rightSibling(ChildTree *tree, int p) {
int i;
EdgeNode *v;
for (i = 0; i < tree->n; i++) {
v = tree->nodeList[i].children;
while (v != NULL) {
if (v->nodePosition == p) {
if (v->link == NULL) {
return -1;
} else {
return v->link->nodePosition;
}
} else {
v = v->link;
}
}
}
return -1;
}
求父結點的位置:
int parent(ChildTree *tree, int p) {
int i;
EdgeNode *v;
for (i = 0; i < tree->n; i++) {
v = tree->nodeList[i].children;
while (v != NULL) {
if (v->nodePosition == p) {
return i;
} else {
v = v->link;
}
}
}
return -1;
}
5.6.3 長子-兄弟表示法
這種表示法是在樹中的每個結點中除其信息域,再增加一個紙箱其最左子結點的指針域lChild和指向其右兄弟指針域rSibling
結點定義如下:
typedef struct CSNode {
DataType info;
struct CSNode *lchild;
struct CSNode *rsibing;
} CSNode;
5.6.4 樹的其他表示法
除了前面介紹的各種表示方法以外,樹還有帶右兄弟指針和子結點標記的先根次序表示法、帶有右兄弟和子結點雙標記的先根次序表示法、帶長子指針和右兄弟標記的層次次序表示法以及帶度數的后根次序表示法等
5.7 樹林
樹林是由零個或多個不相交的樹所組成的集合。樹林中的樹也是有序的,彼此稱為兄弟。這里的樹林可以是一個空集,也可以由一棵樹構成。
5.7.1 樹林的遍歷
先根次序遍歷 —— 首先訪問樹林中第一棵樹的根結點,然后先根次序遍歷第一棵樹除去根結點剩下的所有子樹構成的樹林,最后先根次序遍歷除去第一棵樹之后剩下的樹林
后根次序遍歷 —— 首先后根次序遍歷第一棵樹的根結點的所有子樹構成的樹林,然后訪問樹林中第一棵樹的根結點,最后后根次序遍歷除去第一棵樹之后剩下的樹林
5.7.2 樹林的存儲表示
所有樹的表示方法都可以推廣到樹林的表示。
5.7.3 樹林與二叉樹的轉換
在樹林(包括樹)與二叉樹之間有一個自然的一一對應關系。任何樹都唯一地對應到一棵二叉樹。反過來也成立。
樹林轉為二叉樹
步驟如下:
首先在所有相鄰的兄弟結點之間加一條線
然后對每個非終端結點,只保留它的其最左子結點的連線,刪去其他孩子結點之間原有的連線。
最后以根結點為軸心,將整棵樹順時針旋轉一定角度,使其層次分明
二叉樹轉為樹林
步驟如下:
(1) 若某結點是其父母的左子結點,則把該結點的右結子結點遞歸用虛線連起來
(2) 去掉原二叉樹中所有父母到右子結點的連線。