第7章 二叉樹
數據結構與算法_師大完整教程目錄(更有python、go、pytorch、tensorflow、爬蟲、人工智能教學等着你):https://www.cnblogs.com/nickchen121/p/13768298.html
一、二叉樹的基本概念
-
二叉樹:一個根結點及兩顆互不相交的分別稱作這個根結點的左子樹和右子樹的二叉樹組成
-
二叉樹與普通樹的區別:二叉樹的子樹一定有序;普通樹子樹可以有序也可以無序
-
滿二叉樹:所有終端結點都位於同一層次,且其他非終端結點的度都為 \(2\)
-
完全二叉樹:一顆二叉樹扣除其最大層次后為一顆滿二叉樹,且層次最大那層的所有結點都向左靠齊
-
注:滿二叉樹一定是完全二叉樹;完全二叉樹不一定是滿二叉樹
-
二叉樹的性質:
- 一顆非空二叉樹的第 \(i\) 層上至多有 \(2^{i-1}\) 個結點 (\(i>=1\))
- 深度為 \(h\) 的二叉樹至多有 \(2^h-1\) 個結點(\(h>=1\))
- 對於任何一顆二叉樹 \(T\),如果其終端結點(葉子結點)數為 \(n_0\),度為 \(1\) 的結點樹為 \(n_1\),度為 \(2\) 的結點樹為 \(n_2\),則 \(n_0=n_2+1\)
注:\(n_0+n_1+n_2 = n_1+2*n_2+1\)
5. 對於具有 \(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\)
否則將該子樹放入棧中,並且搜索該子樹的左子樹
如果二叉樹不存在,證明左子樹搜索完畢,從棧中取出子樹搜索其右子樹
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答案
- 圖例題7-9答案