第7章 二叉樹
第7章 二叉樹
目錄- 一、二叉樹的基本概念
- 二、二叉樹的基本運算
- 三、二叉樹的儲存結構
- 四、二叉樹的遍歷
- 五、二叉樹其他運算的實現
- 六、穿線二叉樹
- 七、樹、森林和二叉樹的轉換
- 八、通過前中後序遍歷確定二叉樹(補充)
- 九、演算法設計題
- 十、錯題集
一、二叉樹的基本概念
- 二叉樹:一個根結點及兩顆互不相交的分別稱作這個根結點的左子樹和右子樹的二叉樹組成
- 二叉樹與普通樹的區別:二叉樹的子樹一定有序;普通樹子樹可以有序也可以無序
- 滿二叉樹:所有終端結點都位於同一層次,且其他非終端結點的度都為 \(2\)
- 完全二叉樹:一顆二叉樹扣除其最大層次後為一顆滿二叉樹,且層次最大那層的所有結點都向左靠齊
- 注:滿二叉樹一定是完全二叉樹;完全二叉樹不一定是滿二叉樹
- 二叉樹的性質:
- 一顆非空二叉樹的第 \(i\) 層上至多有 \(2^{i-1}\) 個結點 (\(i>=1\))
- 深度為 \(h\) 的二叉樹至多有 \(2^h-1\) 個結點(\(h>=1\))
- 對於任何一顆二叉樹 \(T\),如果其終端結點(葉子結點)數為 \(n_0\),度為 \(1\)
- 注:\(n_0+n_1+n_2 = n_1+2*n_2+1\)
- 對於具有 \(n\) 個結點的完全二叉樹,如果按照從
上到下、同一層次上的結點按從左到右的順序對二叉樹中的所有結點從$ 1$ 開始順序編號,則對於序號為 \(i\) 的
結點,有:- 如果 \(i>1\),則序號為 \(i\) 的雙親結點的序號為 \([i/2](取整函式)\)
- 如果 \(2*i>n\),則結點 \(i\) 無左子女(此時結點 \(i\) 為終端結點);否則其左子女為結點 \(2*i\)
- 如果 \(2*i+1>n\),則結點 \(i\) 無右子女;否則其右子女為節點 \(2*i+1\)
二、二叉樹的基本運算
- 略
三、二叉樹的儲存結構
3.1 順序儲存結構
- 略
3.2 鏈式儲存結構
3.2.1 二叉樹鏈式儲存結構
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
// struct node *parent; // 指向雙親的指標 (可有可無)
} bintnode;
typedef bintnode *bintree;
bintree root; // 指向二叉樹根結點的指標
四、二叉樹的遍歷
4.1 二叉樹遍歷的定義
-
前序遍歷:首先訪問根結點;
然後按照前序遍歷的順序訪問根結點的左子樹;
再按照前序遍歷的順序訪問根結點的右子樹 -
中序遍歷:首先按照中序遍歷的順序訪問根結點的左子樹;
然後訪問根結點;最後按照中序遍歷的順序訪問根結點的右子樹 -
後序遍歷:首先按照後序遍歷的順序訪問根結點的左子樹;
然後按照後序遍歷的順序訪問根結點的右子樹;最後訪問根結點 -
圖二叉樹的遍歷:
4.2 二叉樹遍歷的遞迴實現
-
二叉樹遍歷的遞迴實現 :按照遍歷規定的次序,訪問根結點時就輸出根結點的值
-
常用操作:
- 中序遍歷的二叉樹的遞迴演算法
- 後序遍歷的二叉樹的遞迴演算法
4.2.1 前序遍歷的二叉樹的遞迴演算法(演算法)
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
void preorder(bintree t) {
if (t) {
printf("%c", t->data);
preorder(t->lchild);
preorder(t->rchild);
}
}
4.2.2 前序遍歷時二叉樹的建立演算法(演算法)
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
bintree createbintree() {
char ch;
bintree t;
if ((ch = getchar()) == '#') t = NULL;
else {
t = (bintnode *) malloc(sizeof(bintnode)); // 生成二叉樹的根結點
t->data = ch;
t->lchild = createbintree(); // 遞迴實現左子樹的建立
t->rchild = createbintree(); // 遞迴實現右子樹的建立
}
return t;
}
- 圖建立二叉樹:
4.3 二叉樹遍歷的非遞迴實現
- 常用操作:
- 中序遍歷的二叉樹的非遞迴演算法
- 後序遍歷的二叉樹的非遞迴演算法
4.3.1 前序遍歷的二叉樹的非遞迴演算法(演算法)
- 演算法步驟:
- 對於一顆樹(子樹)\(t\)
- 訪問完 \(t\) 的根結點值後,進入 \(t\) 的左子樹,但是此時需要將 \(t\) 儲存起來
- 在 \(t\) 處設定一個回溯點
- 訪問完左子樹後,通過回溯點 \(t\) 進入 \(t\) 的右子樹訪問
- 注:棧頂元素即將出棧時,意味著根結點和左子樹訪問完成,出棧後需要進入其右子樹訪問
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
void preorder1(bintree t) {
seqstack s;
s.top = 0;
// 當前處理的子樹不為空或棧不為空則迴圈
while ((t) || (s.top != 0)) {
if (t) {
printf("%c ", t->data);
push(&s, t);
t = t->lchild;
} else {
t = pop(&s);
t = t->rchlid;
}
}
}
五、二叉樹其他運算的實現
5.1 二叉樹的查詢(演算法)
- 演算法步驟:
- 首先判斷樹是否為空
- 如果樹(子樹)結點值為 \(x\),則返回
- 否則前往左子樹查詢,找到返回值
- 否則前往右子樹查詢,找到返回值
- 否則前往左子樹查詢,找到返回值
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
bintree locate(bintree t, dataype x) {
bintree p;
if (t == NULL) return NULL;
else {
if (t->data == x) return t;
else {
p = locate(t->lrchild);
if (p) return p;
else return locate(t->rchild)
}
}
}
5.2 統計二叉樹中的結點的個數(演算法)
- 演算法步驟:
- 判斷樹(子樹)是否為空
- 不為空遞迴查詢左子樹和右子樹,並且返回左子樹結點總數+右子樹結點總數+根結點
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
int numofnode(bintree t) {
if (t == NULL) return 0;
else return (numofnode(t->lchild) + numofnode(t->rchild) + 1);
}
5.3 判斷二叉樹是否等價(演算法)
- 演算法步驟:
- 判斷兩個二叉樹是否都為空,都為空則等價
- 如果兩個二叉樹不都為空
- 首先判斷根結點是否相同
- 其次遞迴判斷左子樹是否相同
- 最後遞迴判斷右子樹是否相同,是否等價的標準取決於右子樹是否也相同
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
int isequal(bintree t1, bintree t2) {
int t;
t = 0;
if (t1 == NULL && t2 == NULL) t = 1; // t1 和 t2 均為空,則二者等價
else {
// 處理 t1 和 t2 均不為空的情況
if (t1 != NUll && t2 != NULL)
if (t1->data == t2->data) // 如果根結點的值相等
if (isequeal(t1->lchild, t2->lchild)) // 如果 t1 和 t2 的左子樹等價
t = isequeal(t1->rchild, t2->rchild); // 返回值取決於 t1 和 t2 的右子樹是否等價
}
return (t);
}
5.4 求二叉樹的高度(演算法)
- 演算法步驟:
- 首先處理空二叉樹的情況
- 其次遞迴得出左子樹的高度
- 最後遞迴得出右子樹的高度
- 遞迴期間,如果左子樹高度大於右子樹高度,左子樹高度加 \(1\),否則右子樹高度加 \(1\)
- 注:遞迴出口應該用第三個變數 \(h\) 來返回子樹高度
typedef char datatype; // 結點屬性值的型別
// 二叉樹結點的型別
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
int depth(bintree t) {
int h, lh, rh;
if (t == NULL) h = 0; // 處理空二叉樹的情況
else {
lh = depth(t->lchild); // 求左子樹的高度
rh = depth(t->rchild); // 求右子樹的高度
if (lh >= rh) h = lh + 1; // 求二叉樹t的高度
else h = rh + 1;
}
return h;
}
六、穿線二叉樹
6.1 穿線二叉樹的定義
-
穿線二叉樹的指標:結點的左、右指標指向其左、右子女
-
中序穿線二叉樹的線索:結點的左、右指標指向其中序遍歷的前驅、後繼結點
-
為了區別結點的左右指標是指標還是線索,一般加上 \(ltag\) 和 \(rtag\) 兩個標誌位
- \(ltag=0\) 表示結點的左指標指向其左子女
- \(ltag=1\) 表示結點的左指標指向其中序遍歷的前驅
- \(rtag=0\) 表示結點的右指標指向其右子女
- \(rtag=1\) 表示結點的右指標指向其中序遍歷的後繼
-
圖中序穿線二叉樹:
6.2 中序穿線二叉樹的基本運算
- 略
6.3 中序穿線二叉樹的儲存結構及其實現
- 常用操作:
- 建立一顆中序二叉樹
6.3.1 中序穿線二叉樹的儲存結構
typedef char datatype;
typedef struct node {
datatype data;
int ltag, rtag; // 左右標誌位
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *binthrtree;
6.3.2 中序遍歷中序穿線二叉樹(真題)(演算法)
- 演算法步驟:
- 找到中序遍歷下的第一個結點(從根結點出發,沿著左指標不斷往左走,直至左指標為空)
- 從中序遍歷的第一個結點開始,不斷地尋找當前結點在中序遍歷下的後繼結點並輸出
- 在中序穿線二叉樹中找後繼結點步驟:
- 若右標誌為 \(1\),則表明右指標正好指向其中序遍歷下的後繼結點
- 若右標誌為 \(0\),則說明他有右子樹,因此其中序遍歷下的後繼結點應該是該右子樹中序遍歷下的第一個結點(右子樹的最左下的結點,與第一步步驟完全相同)
- 在中序穿線二叉樹中找後繼結點步驟:
typedef char datatype;
typedef struct node {
datatype data;
int ltag, rtag; // 左右標誌位
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *binthrtree;
// 尋找結點 p 在中序遍歷下的後繼結點
binthrtree insuccnode(binthrtree p) {
binthrtree q;
if (p->rtag == 1) // p 的右指標為線索,恰巧指向p的後繼結點
return p->rchild;
else {
q = p->rchild; // 尋找 p 的右子樹中最左下的結點
while (q->ltag == 0) q = q->lchild; // 求該右子樹下中序遍歷下的第一個結點
return q;
}
}
// 中序遍歷中序穿線二叉樹
void inthrtree(binthrtree p) {
if (p) {
while (p->ltag == 0) p = p->lchild; // 求 p 中序遍歷下的第一個結點
do {
printf("%c ", p->data);
p = insuccnode(p); // 求 p 中序遍歷下的後繼結點
} while (p);
}
}
七、樹、森林和二叉樹的轉換
- 注:任意一顆樹(森林)都唯一地對應一顆二叉樹;相反,任何一顆二叉樹都唯一地對應一顆樹(森林)
7.1 樹、森林到二叉樹的轉換
-
樹、森林到二叉樹的轉換步驟
- 在所有兄弟結點之間新增一條連線,如果是森林,則在其所有樹的樹根之間同樣也新增一條連線
- 對於樹、森林中的每個結點,除保留其到第一個子女的連線外,撤消其到其它子女的連線;
- 將以上得到的樹按照順時針方向旋轉45度
-
圖樹到二叉樹的轉換:
-
圖森林到二叉樹的轉換:
7.2 二叉樹到樹、森林的轉換
-
首先將二叉樹按照逆時針方向旋轉45度
-
若某結點是其雙親的左子女,則把該結點的右子女,右子女的右子女,……都與該結點的雙親用線連起來
-
最後去掉原二叉樹中所有雙親到其右子女的連線
-
注:最後連線子結點,只能連線右子女,而不能連線左子女
-
圖二叉樹到森林的轉換:
八、通過前中後序遍歷確定二叉樹(補充)
8.1 前序+中序遍歷
- 已知一棵二叉樹的前序和中序序列,構造該二叉樹的過程如下:
-
根據前序序列的第一個元素建立根結點;
-
在中序序列中找到該元素,確定根結點的左右子樹的中序序列;
-
在前序序列中確定左右子樹的前序序列;
-
由左子樹的前序序列和中序序列建立左子樹;
-
由右子樹的前序序列和中序序列建立右子樹。
-
如:已知一棵二叉樹的先序遍歷序列和中序遍歷序列分別是 abdgcefh、dgbaechf,求二叉樹及後序遍歷序列。
先序:abdgcefh—>a bdg cefh
中序:dgbaechf—->dgb a echf
得出結論:a 是樹根,a 有左子樹和右子樹,左子樹有 bdg 結點,右子樹有 cefh 結點
先序:bdg—>b dg
中序:dgb —>dg b
得出結論:b 是左子樹的根結點,b 無右子樹,有左子樹
先序:dg—->d g
中序:dg—–>dg
得出結論:d 是 b 左子樹的根節點,d 無左子樹,g 是 d 的右子樹
然後對於 a 的右子樹類似可以推出
然後還原
8.2 中序+後序遍歷
- 已知一棵二叉樹的中序和後序序列,構造該二叉樹的過程如下:
- 根據後序序列的最後一個元素建立根結點
- 在中序序列中找到該元素,確定根結點的左右子樹的中序序列
- 在後序序列中確定左右子樹的後序序列
- 由左子樹的後序序列和中序序列建立左子樹
- 由右子樹的後序序列和中序序列建立右子樹
如:已知一棵二叉樹的後序遍歷序列和中序遍歷序列分別是gdbehfca、dgbaechf,求二叉樹
後序:gdbehfca—->gdb ehfc a
中序:dgbaechf—–>dgb a echf
得出結論:a是樹根,a有左子樹和右子樹,左子樹有 bdg 結點,右子樹有 cefh 結點
後序:gdb—->gd b
中序:dgb—–>dg b
得出結論:b 是 a 左子樹的根節點,無右子樹,有左子樹 dg
後序:gd—->g d
中序:dg—–>d g
得出結論:d 是 b 的左子樹根節點,g 是 d 的右子樹
然後對於 a 的右子樹類似可以推出
然後還原
8.3 前序+後序遍歷
- 注:前序和後序在本質上都是將父節點與子結點進行分離,但並沒有指明左子樹和右子樹的能力,因此得到這兩個序列只能明確父子關係,而不能唯一地確定一棵二叉樹。
九、演算法設計題
9.1 求一顆給定二叉樹中葉子結點的個數(遞迴和非遞迴)(真題)(演算法)
分別採用遞迴和非遞迴方式編寫兩個函式,求一棵給定二叉樹中葉子結點的個數
- 演算法步驟(遞迴):
- 判斷二叉樹是否為空,為空返回 \(0\)
- 判斷二叉樹的子樹(子樹的子樹)的兩個左右孩子是否為空,為空返回 \(1\)
- 左右遞迴搜尋兩棵子樹
- 演算法步驟(非遞迴):
- 二叉樹不為空或棧不為空則開啟迴圈
- 如果二叉樹存在,並且左右子樹為空,計數加 \(1\)
- 否則將該子樹放入棧中,並且搜尋該子樹的左子樹
- 如果二叉樹不存在,證明左子樹搜尋完畢,從棧中取出子樹搜尋其右子樹
- 如果二叉樹存在,並且左右子樹為空,計數加 \(1\)
- 二叉樹不為空或棧不為空則開啟迴圈
typedef char datatype;
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *bintree;
// 遞迴方法求二叉樹葉子結點的個數
int leaf1(bintree t) {
if (t == NULL) return 0;
else if (!t->lchild && !t->rchild)
return 1;
else
return leaf1(t->lchild) + leaf1(t->rchild);
}
// 非遞迴方法求二叉樹葉子結點的個數
int leaf2(bintree t) {
seqstack s; // 順序棧
int count = 0; // 葉子結點計數變數
init(&s); // 初始化空棧
while (t || !empty(&s)) {
if (t) {
if (!t->lchild && !t->rchild) count++;
push(&s, t);
t = t->lchild;
} else {
t = pop(&s);
t = t->rchild;
}
}
return count;
}
9.2 返回一顆給定二叉樹在中序遍歷下的最後一個結點(真題)(演算法)
試編寫一個函式,返回一顆給定二叉樹在中序遍歷下的最後一個結點
- 演算法 步驟:
- 不斷地查詢右子樹的右子結點
- 返回最後一個右子結點
typedef char datatype;
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *bintree;
// 遞迴實現
bintree midlast(bintree t) {
if (t && t->rchild) t = midlast(t->rchild);
return t;
}
// 非遞迴實現
bintree midlast(bintree t) {
bintree p = t;
while (p && p->rchild) p = p->rchild;
return p;
}
十、錯題集
-
前序(根左右)和中序(左根右)遍歷結果相同的二叉樹(去掉左都為“根右”)為(所有結點只有右子樹的二叉樹);前序(根左右)和後序(左右根)遍歷結果相同的二叉樹(哪一個子樹都不可以去掉)為(只有根結點的二叉樹)
-
有 \(n\) 個結點的二叉樹,已知葉結點個數為 \(n_0\),則該樹中度為 \(1\) 的結點的個數為(\(n-2*n_0+1\));若此樹是深度為 \(k\) 的完全二叉樹,則 \(n\) 的最小值為 (\(2^{k-1}\))
- 注:完全二叉樹的最後一層,一定有一個結點,因此 \(n\) 的最小值為 \(k-1\) 層總結點數加 \(1\) 為 \(2^{k-1}-1+1\)
-
(真題)對於一顆具有 \(n\) 個結點的二叉樹,該二叉樹中所有結點的讀書之和為(\(n-1\))
-
(真題)試分別畫出具有 \(3\) 個結點的樹和具有 \(3\) 個結點的二叉樹的所有不同形態。(此題注意樹具有無序性)
- 圖例題7-9答案