【資料結構&演算法】11-樹基礎&二叉樹遍歷

李柱明發表於2021-11-11


前言

主要描述二叉樹。

李柱明部落格: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)棵互不相交的樹的集合。

樹的儲存結構

簡單的順序儲存結構無法直接反映邏輯關係,不能滿足樹的實現要求。

故充分利用順序儲存和鏈式儲存結構的特點,介紹三種不同的表示法:

  1. 雙親表示法。
  2. 孩子表示法。
  3. 孩子兄弟表示法。

雙親表示法

引入:除根節點外,其餘每個結點,不一定有孩子,但一定有且僅有一個雙親
定義:設以一組連續空間儲存樹的結點,同時在每個結點中,附設一個指示器指示其雙親結點到連結串列中的位置。

  • data:資料域,儲存結點的資料資訊。
  • parent:指標域,儲存該結點的雙親在陣列中的下標。
  • 約定:根節點的位置域為-1。

缺點:

  1. 找一個結點的孩子需要遍歷樹。

上述第一個缺點引發的思考:

  1. 需要關注什麼資料域就在資料結構中新增什麼資料域。如雙親域、長子域、兄弟域等等。

    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 個頭指標又組成一個線性表,採用順序儲存結構,存放進一個一維陣列中。

  • 孩子表示法的兩種結點資料結構:

    1. 孩子連結串列的孩子結點:
      1. child:表示該孩子結點在表頭陣列中的下標。
      2. next:下一個孩子結點的指標。
    2. 表頭陣列的表頭結點:
      1. data:資料域。
      2. 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 的結點。
  • 左子樹和右子樹是有序樹。
  • 只有一顆子樹也要區分左右子樹。

形態

二叉樹的五種基本形態:

  1. 空二叉樹。
  2. 只有一個根結點。
  3. 根結點只有左子樹。
  4. 根結點只有右子樹。
  5. 根結點有左、右子樹。

特殊二叉樹

斜樹

左斜樹&右斜樹:

  • 左斜樹:

  • 右斜樹:

滿二叉樹

滿二叉樹:

  • 定義:所有分支結點都存在左右子樹,並且所有葉子都在同一層。

  • 特點:

    • 葉子只能出現在最下一層。
    • 非葉子結點的度一定是 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)有:

  1. 如果 i=1,則結點 i 是二叉樹的根,無雙親。
  2. 如果 i>1,則其雙親是結點[i/2]。
  3. 如果 2i>n,則結點 i 無左孩子(結點 i 為葉子結點);否則其左孩子是結點 i。
  4. 如果 2i+1>n,則結點 i 無右孩子;否則其右孩子是結點 2i+1。

二叉樹的儲存結構

有順序儲存結構和鏈式儲存結構。

二叉樹的順序儲存結構

二叉樹的順序儲存結構:

  • 儲存方法:按完全二叉樹編號,編號就是下標。
  • 缺點:當樹不為完全二叉樹時存在空間浪費。

二叉樹的鏈式儲存結構

二叉樹的鏈式儲存結構:

  • 連結串列每個結點包含一個資料域和兩個指標域:

    • data:資料
    • lchild:左孩子
    • rchild:右孩子

二叉樹的遍歷

遍歷是二叉樹中非常重要的操作。

遍歷原理

二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問二叉樹中的所有結點,使得每個結點都被訪問 1 次。

遍歷方法

四種遍歷方法:

  1. 前序遍歷
  2. 中序遍歷
  3. 後序遍歷
  4. 層序遍歷

前、中、後序表示的是節點與它的左右子樹節點遍歷列印的先後順序。

實現思路:遞迴。

前序遍歷

前序遍歷是指,對於樹中的任意節點來說,先列印這個節點,然後再列印它的左子樹,最後列印它的右子樹。

程式碼實現思路:

  • 中-> 左 -> 右。使用棧輔助實現。

    1. 方法 1:使用遞迴思想。(相當於使用系統棧)
    2. 方法 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); /* 最後先序遍歷右子樹 */
}
中序遍歷

中序遍歷是指,對於樹中的任意節點來說,先列印它的左子樹,然後再列印它本身,最後列印它的右子樹

程式碼實現思路:

  • 左-> 中 -> 右。使用棧輔助實現。

    1. 方法一:使用遞迴思想。
    2. 方法 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); /* 最後先序遍歷右子樹 */
}
後序遍歷

後序遍歷是指,對於樹中的任意節點來說,先列印它的左子樹,然後再列印它的右子樹,最後列印這個節點本身。

程式碼實現思路:

  • 左-> 右 -> 中。

    1. 使用遞迴思想。
    2. 方法 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);/* 顯示結點資料,可以更改為其它對結點操作 */
}
層序遍歷

根起,從上而下,從左至右。

對於順序儲存,只需要按下標順序輸出即可。

但是對於鏈式儲存結構就複雜點,思路如下:藉助佇列的方式實現:

  1. 先把跟節點入隊。
  2. 獲取隊頭並列印,然後把當前隊頭節點的左右孩子入隊。
  3. 重複步驟 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);
    }
}

二叉樹的建立

二叉樹的擴充套件二叉樹:

  • 為了能讓每個結點確認是否有左右孩子,將每個結點的空指標引出一個虛結點,其值為一特定值,比如"#"
  • 這種處理後的二叉樹為原二叉樹的擴充套件二叉樹。
  • 擴充套件二叉樹就可以做到一個遍歷序列確定一棵二叉樹。

樹、森林和二叉樹的轉換

樹轉換為二叉樹

二叉樹除了根節點,其餘節點最多有三條線:

  1. 與雙親。(注意:在該節點上沒有雙親域)
  2. 做孩子。
  3. 右孩子。

樹轉換為二叉樹的步驟:

  1. 加線:所有兄弟結點之間加一條線。

  2. 去線:對樹中每個結點,只保留與第一個孩子的線。刪除與其它孩子的線。

  3. 層次調整:

    • 第一個孩子是二叉樹的左孩子
    • 右兄弟是右孩子

森林轉換為二叉樹

森林轉換為二叉樹的步驟:

  1. 把每棵樹都轉換為二叉樹。
  2. 從第二棵樹起,將其根節點插入到前一棵樹的根節點作為其右孩子。

二叉樹轉換為樹

二叉樹轉為樹的步驟:

  1. 加線:當前節點與左孩子的右孩子、左孩子的右孩子的右孩子、左孩子的右孩子的右孩子的右孩子......連線。
  2. 去線:去掉原二叉樹中所有節點與其右孩子的連線。
  3. 層次調整。

二叉樹轉換為森林

二叉樹的根節點有右孩子,則說明該二叉樹可以就可以轉換為森林。

二叉樹轉換為森林的步驟:

  1. 去線:從根節點其,取出根節點與右孩子的線,得出的右孩子樹,也去除與右孩子的線,迴圈下去直至右孩子樹沒有右孩子為止。
  2. 將每棵二叉樹轉換為樹。

樹和森林的遍歷

樹的遍歷

樹的遍歷有兩種:

  1. 先序遍歷:先訪問根再依次訪問子。
  2. 後序遍歷:先訪問依次訪問子,再訪問根。

森林的遍歷

森林的遍歷也有兩種:

  1. 先序遍歷:一棵樹先序遍歷完再下一棵樹。
  2. 後序遍歷:一棵樹後序遍歷完再下一棵樹。

相關文章