前言
主要描述二叉樹。
李柱明部落格:https://www.cnblogs.com/lizhuming/p/15487394.html
樹的定義
樹:
- 樹是 n(n>=0) 個結點的有限集。
- n = 0 時為空樹。
- n > 0 時,即是非空樹時,有且僅有一個根結點。
- m > 0 時,子樹的個數沒有限制,但它們一定互不相交。
結點:
-
結點的度:結點擁有的子樹數。
-
葉結點或終結點:度為 0 的結點。
-
非終結點或分支結點:度不為 0 的結點。
-
內部結點:除根結點外,分支結點也稱為內部結點。
-
樹的度:樹內各結點的度的最大值。
結點關係:
- 孩子(child):結點的子樹的根稱為該結點的孩子。
- 雙親(parent):該結點稱為孩子的雙親(父母同體,唯一的一個)。
- 兄弟(sibling):同一個雙親的孩子之間互稱兄弟。
- 祖先:結點的祖先是從根到該結點所經分支上的所有結點。
- 子孫:以某結點為根的子樹中的任一結點都稱為該節點的子孫。
樹的其它相關概念:
- 層次(level):從根開始定義起,根為第一層,根的孩子為第二層。
- 堂兄弟:雙親在同一層的結點互為堂兄弟。
- 深度(depth)或高度:樹中結點的最大層次稱為樹的深度或高度。
- 有序樹/無序樹:如果將樹中結點的各子樹看成從左至右有次序的,不能互換的,則稱該樹為有序樹,否則為無序樹。
- 森林(forest):m(m>=0)棵互不相交的樹的集合。
樹的儲存結構
簡單的順序儲存結構無法直接反映邏輯關係,不能滿足樹的實現要求。
故充分利用順序儲存和鏈式儲存結構的特點,介紹三種不同的表示法:
- 雙親表示法。
- 孩子表示法。
- 孩子兄弟表示法。
雙親表示法
引入:除根節點外,其餘每個結點,不一定有孩子,但一定有且僅有一個雙親
定義:設以一組連續空間儲存樹的結點,同時在每個結點中,附設一個指示器指示其雙親結點到連結串列中的位置。
- data:資料域,儲存結點的資料資訊。
- parent:指標域,儲存該結點的雙親在陣列中的下標。
- 約定:根節點的位置域為-1。
缺點:
- 找一個結點的孩子需要遍歷樹。
上述第一個缺點引發的思考:
-
需要關注什麼資料域就在資料結構中新增什麼資料域。如雙親域、長子域、兄弟域等等。
- 如需要關注結點的孩子,則新增結點的長子域。
參考程式碼:
/* 樹的雙親表示法結點資料結構 */
#define MAX_TREE_SIZE 100
typedef int tree_elem_type;
/* 結點結構 */
typedef struct tree_node
{
int parent; // 雙親位置
// int firstchild; // 長子位置
// int rightsib; // 右兄弟位置
tree_elem_type data; // 資料
}tree_node_t;
/* 樹結構 */
typedef struct tree
{
int root; // 根節點位置
int num; // 當前節點數
tree_node_t nodes[MAX_TREE_SIZE];
}tree_t;
孩子表示法
多重連結串列表示法:
- 每個結點有多個指標域,其中每個指標指向一棵子樹的根節點,這種方法叫做多重連結串列表示法。
方案 1:
- 設定指標域的個數為樹的度。
- 即是結點資料結構的內容為:data 和 n(樹的度)個孩子域。
- 特點:可能存在空間浪費。
方案 2:
- 設定每個結點指標域的個數等於該結點的度,取一個位置來儲存結點指標域的個數。
- 特點:空間利用率提高,但是各個結點的連結串列結構不同,要維護結點的度的數值,時間損耗提高。
孩子表示法:
-
把每個結點的孩子結點排列起來,以單連結串列作儲存結構,則 n 個結點有 n 個孩子連結串列,如果是葉子結點,則此單連結串列為空。然後 n 個頭指標又組成一個線性表,採用順序儲存結構,存放進一個一維陣列中。
-
孩子表示法的兩種結點資料結構:
- 孩子連結串列的孩子結點:
- child:表示該孩子結點在表頭陣列中的下標。
- next:下一個孩子結點的指標。
- 表頭陣列的表頭結點:
- data:資料域。
- firstchild:孩子連結串列頭指標。
- 孩子連結串列的孩子結點:
-
缺點:找雙親需要遍歷樹。
- 解決:表頭陣列的表頭結點資料結構新增雙親域。
參考程式碼:
/* 樹的孩子表示法結點資料結構 */
#define MAX_TREE_SIZE 100
typedef int tree_elem_type;
/* 孩子結點結構 */
typedef struct c_tree_node
{
int child; // 孩子下標
struct c_tree_node *next; // 下一個
}c_tree_node_t;
/* 表頭結構 */
typedef struct tree_top
{
c_tree_node_t *firstchild; // 頭結點
tree_elem_type data; // 資料
}tree_top_t;
/* 樹結構 */
typedef struct tree
{
int root; // 根節點位置
int num; // 當前節點數
tree_top_t nodes[MAX_TREE_SIZE];
}tree_t;
孩子兄弟表示法
引入:
- 任意一棵樹,它的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。
- 所以可以設定兩個指標,分別指向該結點的第一個孩子和此結點的右兄弟。
參考程式碼:
/* 樹的孩子兄弟表示法結點資料結構 */
typedef int tree_elem_type;
typedef struct tree_node
{
struct tree_node *firstchild; // 長子域
struct tree_node *rightsib; // 右兄域
tree_elem_type data; // 資料
}tree_node_t;
二叉樹
定義
二叉樹的定義:
- 二叉樹是 n(n>=0) 個結點的有限集合,該集合或為空集,或由一個根結點和兩顆互不相交的、分別稱為根結點的左子樹和右子樹的二叉樹組成。
- 是有序樹。
特點
二叉樹特點:
- 二叉樹中不存在大於 2 的結點。
- 左子樹和右子樹是有序樹。
- 只有一顆子樹也要區分左右子樹。
形態
二叉樹的五種基本形態:
- 空二叉樹。
- 只有一個根結點。
- 根結點只有左子樹。
- 根結點只有右子樹。
- 根結點有左、右子樹。
特殊二叉樹
斜樹
左斜樹&右斜樹:
- 左斜樹:
- 右斜樹:
滿二叉樹
滿二叉樹:
-
定義:所有分支結點都存在左右子樹,並且所有葉子都在同一層。
-
特點:
- 葉子只能出現在最下一層。
- 非葉子結點的度一定是 2。
- 在同樣深度的二叉樹中,滿二叉樹的結點個數最多,葉子樹最多。
完全二叉樹
完全二叉樹:
-
定義:對一棵具有 n 個結點的二叉樹按層序編號,如果編號 i(1<=i<=n)的結點與同樣深度的滿二叉樹中編號為 i 的結點在二叉樹中位置完全相同,則此二叉樹為完全二叉樹。
-
特點:
- 葉子結點只能出現在最下兩層。
- 最下層的葉子一定集中在左邊連續位置。
- 倒數第二層若有葉子結點,一定都在右邊連續位置。
- 若結點的度為 1,則該結點只有左孩子。即是不存在只有右子樹的情況。
- 同樣結點數的二叉樹,完全二叉樹的深度最小。
-
判斷方法:給每個結點按滿二叉樹的結構逐層排序,如果編號出現空檔,就不是,否則就是。
二叉樹的性質
性質 1:在二叉樹的第 i 層上至多有 2i-1 個結點(i>=1)。
性質 2:深度為 k 的二叉樹至多有 2k-1 個結點(k>=1)。
性質 3:對任何一棵二叉樹 T,如果其終端結點數為 n0,度為 2 的結點數為 n2,則 n0 = n2+1。
性質 4:具有 n 個結點的完全二叉樹的深度為[log2n ] + 1([X]表示不大於 X 的最大整數)。
性質 5:如果對一顆有 n 個結點的完全二叉樹(其深度為[log2n ] + 1)的結點按層序編號(從第 1 層到第[log2n ] + 1 層,每層從左到右),對任一結點 i(1<=i<=n)有:
- 如果 i=1,則結點 i 是二叉樹的根,無雙親。
- 如果 i>1,則其雙親是結點[i/2]。
- 如果 2i>n,則結點 i 無左孩子(結點 i 為葉子結點);否則其左孩子是結點 i。
- 如果 2i+1>n,則結點 i 無右孩子;否則其右孩子是結點 2i+1。
二叉樹的儲存結構
有順序儲存結構和鏈式儲存結構。
二叉樹的順序儲存結構
二叉樹的順序儲存結構:
- 儲存方法:按完全二叉樹編號,編號就是下標。
- 缺點:當樹不為完全二叉樹時存在空間浪費。
二叉樹的鏈式儲存結構
二叉樹的鏈式儲存結構:
-
連結串列每個結點包含一個資料域和兩個指標域:
- data:資料
- lchild:左孩子
- rchild:右孩子
二叉樹的遍歷
遍歷是二叉樹中非常重要的操作。
遍歷原理
二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問二叉樹中的所有結點,使得每個結點都被訪問 1 次。
遍歷方法
四種遍歷方法:
- 前序遍歷
- 中序遍歷
- 後序遍歷
- 層序遍歷
前、中、後序表示的是節點與它的左右子樹節點遍歷列印的先後順序。
實現思路:遞迴。
前序遍歷
前序遍歷是指,對於樹中的任意節點來說,先列印這個節點,然後再列印它的左子樹,最後列印它的右子樹。
程式碼實現思路:
-
中-> 左 -> 右。使用棧輔助實現。
- 方法 1:使用遞迴思想。(相當於使用系統棧)
- 方法 2:非遞迴,採用自實現的棧輔助。
參考程式碼(遞迴):
/* 順序儲存結構 */
void pre_order_traverse(bi_tree tree,int e)
{
visit(tree[e]); // 列印父節點
if(tree[2*e+1]!=nil) /* 左子樹不空 */
pre_traverse(tree,2*e+1); // 遞迴
if(tree[2*e+2]!=nil) /* 右子樹不空 */
pre_traverse(tree,2*e+2); // 遞迴
}
/* 鏈式儲存結構 */
void pre_order_traverse(bi_tree *tree)
{
if(tree==NULL)
return;
printf("%c",tree->data);/* 顯示結點資料,可以更改為其它對結點操作 */
pre_order_traverse(tree->lchild); /* 再先序遍歷左子樹 */
pre_order_traverse(tree->rchild); /* 最後先序遍歷右子樹 */
}
中序遍歷
中序遍歷是指,對於樹中的任意節點來說,先列印它的左子樹,然後再列印它本身,最後列印它的右子樹
程式碼實現思路:
-
左-> 中 -> 右。使用棧輔助實現。
- 方法一:使用遞迴思想。
- 方法 2:非遞迴,採用自實現的棧輔助。
參考程式碼(遞迴):
/* 順序儲存結構 */
void in_order_traverse(bi_tree tree,int e)
{
if(tree[2*e+1]!=nil) /* 左子樹不空 */
in_traverse(tree,2*e+1); // 遞迴
visit(tree[e]); // 列印父節點
if(tree[2*e+2]!=nil) /* 右子樹不空 */
in_traverse(tree,2*e+2); // 遞迴
}
/* 鏈式儲存結構 */
void in_order_traverse(bi_tree *tree)
{
if(tree==NULL)
return;
in_order_traverse(tree->lchild); /* 再先序遍歷左子樹 */
printf("%c",tree->data);/* 顯示結點資料,可以更改為其它對結點操作 */
in_order_traverse(tree->rchild); /* 最後先序遍歷右子樹 */
}
後序遍歷
後序遍歷是指,對於樹中的任意節點來說,先列印它的左子樹,然後再列印它的右子樹,最後列印這個節點本身。
程式碼實現思路:
-
左-> 右 -> 中。
- 使用遞迴思想。
- 方法 2:非遞迴,採用自實現的棧輔助。
參考程式碼(遞迴):
/* 順序儲存結構 */
void post_order_traverse(bi_tree tree,int e)
{
if(tree[2*e+1]!=nil) /* 左子樹不空 */
post_traverse(tree,2*e+1); // 遞迴
if(tree[2*e+2]!=nil) /* 右子樹不空 */
post_traverse(tree,2*e+2); // 遞迴
visit(tree[e]); // 列印父節點
}
/* 鏈式儲存結構 */
void post_order_traverse(bi_tree *tree)
{
if(tree==NULL)
return;
post_order_traverse(tree->lchild); /* 再先序遍歷左子樹 */
post_order_traverse(tree->rchild); /* 最後先序遍歷右子樹 */
printf("%c",tree->data);/* 顯示結點資料,可以更改為其它對結點操作 */
}
層序遍歷
根起,從上而下,從左至右。
對於順序儲存,只需要按下標順序輸出即可。
但是對於鏈式儲存結構就複雜點,思路如下:藉助佇列的方式實現:
- 先把跟節點入隊。
- 獲取隊頭並列印,然後把當前隊頭節點的左右孩子入隊。
- 重複步驟 2。
/* 順序儲存結構:直接列印陣列 */
void level_order_traverse(bi_tree tree)
{
int i=MAX_TREE_SIZE-1;
int j=0;
while(tree[i]==nil)
i--; /* 找到最後一個非空結點的序號 */
for(j=0;j<=i;j++) /* 從根結點起,按層序遍歷二叉樹 */
if(tree[j]!=nil)
visit(tree[j]); /* 只遍歷非空的結點 */
printf("\n");
}
/* 鏈式儲存結構:藉助佇列 */
void level_order_traverse(bi_tree_node* tree)
{
bi_tree_node* temp = NULL;
queue_push(tree); // 跟節點入隊
while (!queue_empty())
{
temp = queue_pop();
printf("%d ", temp->data); //輸出隊首結點
if (temp->left) //把Pop掉的結點的左子結點加入佇列
queue_push(temp->left);
if (temp->right) // 把Pop掉的結點的右子結點加入佇列
queue_push(temp->right);
}
}
二叉樹的建立
二叉樹的擴充套件二叉樹:
- 為了能讓每個結點確認是否有左右孩子,將每個結點的空指標引出一個虛結點,其值為一特定值,比如"#"
- 這種處理後的二叉樹為原二叉樹的擴充套件二叉樹。
- 擴充套件二叉樹就可以做到一個遍歷序列確定一棵二叉樹。
樹、森林和二叉樹的轉換
樹轉換為二叉樹
二叉樹除了根節點,其餘節點最多有三條線:
- 與雙親。(注意:在該節點上沒有雙親域)
- 做孩子。
- 右孩子。
樹轉換為二叉樹的步驟:
-
加線:所有兄弟結點之間加一條線。
-
去線:對樹中每個結點,只保留與第一個孩子的線。刪除與其它孩子的線。
-
層次調整:
- 第一個孩子是二叉樹的左孩子。
- 右兄弟是右孩子。
森林轉換為二叉樹
森林轉換為二叉樹的步驟:
- 把每棵樹都轉換為二叉樹。
- 從第二棵樹起,將其根節點插入到前一棵樹的根節點作為其右孩子。
二叉樹轉換為樹
二叉樹轉為樹的步驟:
- 加線:當前節點與左孩子的右孩子、左孩子的右孩子的右孩子、左孩子的右孩子的右孩子的右孩子......連線。
- 去線:去掉原二叉樹中所有節點與其右孩子的連線。
- 層次調整。
二叉樹轉換為森林
二叉樹的根節點有右孩子,則說明該二叉樹可以就可以轉換為森林。
二叉樹轉換為森林的步驟:
- 去線:從根節點其,取出根節點與右孩子的線,得出的右孩子樹,也去除與右孩子的線,迴圈下去直至右孩子樹沒有右孩子為止。
- 將每棵二叉樹轉換為樹。
樹和森林的遍歷
樹的遍歷
樹的遍歷有兩種:
- 先序遍歷:先訪問根再依次訪問子。
- 後序遍歷:先訪問依次訪問子,再訪問根。
森林的遍歷
森林的遍歷也有兩種:
- 先序遍歷:一棵樹先序遍歷完再下一棵樹。
- 後序遍歷:一棵樹後序遍歷完再下一棵樹。