1. 程式人生 > >資料結構筆記整理第5章:樹和二叉樹

資料結構筆記整理第5章:樹和二叉樹

第5章 樹和二叉樹

本章內容

本章主要介紹樹、二叉樹的概念,遍歷方法以及應用等,本章在考研中是重點內容。

5.1 樹相關的基本概念

樹是一種非線性的資料結構,是若干結點的集合,有唯一的根結點和若干棵互不相交的子樹構成。其中每一棵子樹又是一棵樹,也是由唯一的根結點和若干棵互不相交的子樹組成的,由此可知:樹的定義是遞迴的。樹的結點數目可以為0,為0的時候是一棵空樹。

結點:結點不僅包含資料元素,而且包含指向子樹的分支。
結點的度:結點擁有子樹的個數或者分支的個數。
樹的度:樹中各結點度的最大值。
葉子結點:終端結點,度為0的結點。
非終端結點:分支結點,內部結點,度不為0的結點。
以根結點為第一層,以此類推。


樹的高度(深度):樹中結點的最大層次。
結點的深度:從根結點開始計算,根結點深度為1。
結點的高度:從底層葉子結點開始計算,葉子結點高度為1。
有序樹:樹中結點子樹從左到右有序,不能交換。
豐滿樹:出最底層外,其他層都是滿的。
森林:若干棵互不相交的樹的集合。

5.2 樹的儲存結構

雙親儲存結構:一種順序儲存結構,知道了i,tree[i]為其雙親結點。
孩子儲存結構:同圖中的鄰接表儲存。

5.3 二叉樹的相關的基本概念

一般的樹加上如下的兩個限制條件,就得到了二叉樹:每個結點最多隻有兩棵子樹,即二叉樹中結點的度只能為0,1以及2;子樹有左右之分,不能顛倒。
滿二叉樹:

所有的分支結點都有左孩子和右孩子結點,並且葉子結點都集中在二叉樹的最下層,這樣的二叉樹稱為滿二叉樹。對滿二叉樹編號:編號從1開始,從上到下、從左到右進行。
完全二叉樹:一棵深度為K有n個結點的二叉樹進行編號後,各結點的編號與深度為K的滿二叉樹上相同位置的結點的編號相同。一棵完全二叉樹一定是由一棵滿二叉樹從右至左、從下至上挨個刪除結點所得到的。

5.4 二叉樹的主要性質

1.非空二叉樹上葉子結點數等於雙分支結點數加1,即總分支數 = 總結點數 - 1
2.二叉樹的第i層上最多有(i >= 1)個結點
3.高度(或深度)為K的二叉樹最多有個結點,換句話說:滿二叉樹中前K層結點個數為:


4.有n個結點的完全二叉樹,對各結點從上到下,從左到右依次編號(範圍1~n),則結點之間的關係,若i為某結點編號,則:
若i≠1,雙親編號⌊i/2⌋;
若2i≤n,左孩子為2i,若2i>n,無左孩子;
若2i+1≤n,右孩子為2i+1,若2i+1>n,無右孩子;
5.n個結點,構成h(n)種不同的二叉樹,則:

6.n個結點的完全二叉樹高度(深度)為:

5.5 二叉樹的儲存結構

順序儲存:最適合於完全二叉樹,使用順序儲存結構要從陣列下標為1開始。這裡要注意區分樹的順序儲存結構和二叉樹的順序儲存結構:
樹:陣列下標代表結點的編號,內容表示結點之間的關係;
二叉樹:陣列下標既指示了編號又指示了關係。
鏈式儲存:

typedef struct BTNode {
    char data;
    struct BTNode *lchild;
    struct BTnode *rchild;
}BTNode;

5.6 樹、森林與二叉樹的轉換

將樹轉換為二叉樹:同一結點的各孩子結點用線串起來,將每個結點的分支從左往右除了第一個以外,其餘全部剪掉,擦掉虛線,連成實線,得到二叉樹。
【例子1】樹轉換為二叉樹


將森林轉換為二叉樹:根據孩子兄弟的法則,根結點沒有右兄弟,所以轉換為二叉樹沒有右孩子。將森林中第二棵樹轉換成的二叉樹,當作第一棵樹的右子樹,依此類推。
【例子2】森林轉換為二叉樹


5.7 二叉樹的遍歷演算法

包括:先序遍歷、中序遍歷、後序遍歷和層次遍歷。
先序遍歷:
如果二叉樹為空數,什麼都不做。
否則,訪問根節點->先序遍歷左子樹->先序遍歷右子樹

/* pre order by recursion method */
void preOrder(BTNode *p) {
    if (p != null) {
        visit(p->data);
        preOrder(p->lchild);
        preOrder(p->rchild);
    }
}

/* pre order by no recursion method */
void preOrder(BTNode *p) {
    Stack <BTNode *> s;
    BTNode *q;
    q = p;
    s.initial();
    while(q != null || !s.empty()) {
        if (q != null) {
            visit(q->data);
            s.push(q);
            q = q->lchild;  
        }
        else {
            s.pop(q);
            q = q->rchild;
        }
    } 
}

中序遍歷:
如果二叉樹為空數,什麼都不做。
否則,中序遍歷左子樹->訪問根結點->中序遍歷右子樹

/* in order by recursion method */
void inOrder(BTNode *p) {
    if (p != null) {
        inorder(p->lchild);
        visit(p->data);
        inorder(p->rchild);
    }
}

/* in order by no recursion method */
void inOrder(BTNode *p) {
    Stack <BTNode *> s;
    BTNode *q = p;
    s.initial();
    while(q != null || !s.empty()) {
        if (q != null) {
            s.push(q);
            q = q->lchild;
        }
        else {
            s.pop(q);
            visit(q->data);
            q = q->rchild;
        }
    }
}

後序遍歷:
如果二叉樹為空數,什麼都不做。
否則,後序遍歷左子樹->後序遍歷右子樹->訪問根結點

/* post order by recursion method */
void postOrder(BTNode *p) {
    if (p != null) {
        postOrder(p->lchild);
        postOrder(p->rchild);
        visit(p->data);
    }
}

/* post order by no recursion method */
void postOrder(BTNode *p) {
    Stack <BTNode *> s;
    Stack <int> tag;
    BTNode *q = p;
    int f;
    s.initial();
    tag.initial();
    while(q != null || !s.empty()) {
        if (q != null) {
            s.push(q);
            q = q->lchild;
            tag.push(1);
        }
        else {
            s.pop(q);
            tag.pop(f);
            if (f == 1) {
                s.push(q);
                tag.push(2);
                q = q->rchild;
            }
            else {
                visit(q->data);
                q = null;
            }
        }
    }
}

層次遍歷:
按照一定的方向(如從左至右,從上至下)每一層次對二叉樹各個結點進行訪問。要進行層次遍歷,需要建立一個迴圈佇列,先將二叉樹頭結點入佇列,然後出佇列,訪問該結點,如果有左子樹,則左子樹根結點入隊;如果有右子樹,則右子樹根結點入隊。出佇列,訪問出佇列結點,如此反覆直到佇列空為止。

void levelOrder(BTNode *p) {
    Queue <BTNode *> q;
    BTNode *r;
    q.initial();
    if (p != null) {
        q.push(p);
        while(!q.empty()) {
            q.pop(r);
            visit(r->data);
            if (r->lchild != null) {
                q.push(r->lchild);
            }
            if (r->rchild != null) {
                q.push(r->rchild);
            }
        }
    }
}

【例子3】二叉樹的遍歷結果

先序遍歷:A->B->C->D->E->F->G->H
中序遍歷:C->B->E->D->F->A->H->G
後序遍歷:C->E->F->D->B->H->G->A
層次遍歷:A->B->G->C->D->H->E->F

5.8 樹和森林的遍歷演算法

樹的遍歷:
先根遍歷:先訪問根結點、再訪問子樹。
後根遍歷:先訪問子樹,再訪問根結點。
樹的先根遍歷對應二叉樹的先序遍歷。
樹的後根遍歷對應二叉樹的中序遍歷。
森林的遍歷:
先序遍歷:對應二叉樹的先序遍歷,先訪問第一棵樹的根結點,先序遍歷第一棵樹中根結點的子樹,先序遍歷森林中除去第一棵樹的其他樹。
中序遍歷:對應二叉樹的中序遍歷,中序遍歷第一棵樹根結點的子樹,訪問第一棵樹的根結點,中序遍歷森林中除去第一棵樹的其他樹。

5.9 線索二叉樹


ltag=0:lchild為指標,指向結點左孩子;ltag=1,表示lchild為線索,指向結點直接前驅。
rtag=0:rchild為指標,指向結點右孩子;rtag=1,表示rchild為線索,指向結點直接後驅。

typedef struct TBTNode {
    char data;
    int ltag, rtag;
    struct TBTNode *lchild;
    struct TBTNode *rchild;
} TBTNode;

5.10 二叉排序樹(BST)

這部分內容主要應用在“查詢或者排序部分”。二叉排序樹或者是空樹,或者是滿足以下性質的二叉樹:
若左子樹不空,則左子樹所有值 < 根
若右子樹不空,則右子樹所有值 > 根
左右子樹各是一棵二叉排序樹
輸出二叉排序樹的中序遍歷,則該輸出序列為遞增序列。

typedef struct BTNode {
    int key;
    struct BTNode *lchild;
    struct BTNode *rchild;
} BTNode;

5.11 平衡二叉樹(AVL)

這部分內容主要應用在“查詢或者排序部分”。
左右子樹都是平衡二叉樹,並且左右子樹的高度之差的絕對值不超過1(引入了平衡因子的概念)。
一個結點的平衡因子:左子樹高度 - 右子樹高度。取值:-1,0,1
當失去平衡的最小子樹被調整為平衡子樹之後,無需調整原有其他所有不平衡子樹,整個二叉排序樹會成為一棵平衡二叉樹。
最小子樹:以距離插入結點最近,且平衡因子絕對值大於1的結點作為根的子樹。

將失去平衡的二叉樹進行平衡調整有4種情況:LL型、RR型、LR型和RL型。
LL型:在左子樹根結點的右子樹上插入結點 -> 右旋
LR型:在左子樹根結點的右子樹上插入結點 -> 左旋 + 右旋
RR型:在右子樹根結點的右子樹上插入結點 -> 左旋
RL型:在右子樹根結點的左子樹上插入結點 -> 右旋 + 左旋

【例子4】平衡二叉樹的插入刪除和平衡調整
以關鍵字{16、3、7、11、9、26、18、14、15}構造一棵AVL樹,構造完成後依次刪除16、15、11

插入部分:


刪除部分:

5.12 B-樹與B+樹

B-樹可以看作是二叉排序樹的擴充套件,二叉排序樹是二路查詢,B-樹的多路查詢。因為B-樹結點內的關鍵字是有序的,在結點內查詢的時候除了順序查詢外,可以用折半查詢。B-樹需要滿足以下條件:
每個結點最多有m個分支(子樹);而最少分支棵樹要看是否為根結點,根結點最少兩個分支,非根結點最少有個分支;
有n個分支的結點有n-1個關鍵字,從左至右遞增順序排列;
各個底層是葉結點,葉結點下面是失敗結點(空指標表示)。

B+樹是B-樹的一種變形。它們之間的差別主要有:
B+樹中,n個關鍵字的結點含有n個分支,B-樹中有n+1個分支;
B+樹中葉子結點包含資訊,並且包含了全部關鍵字,B-樹的葉子只包含關鍵字(索引)
由於這裡並不是考研重點考察的部分,所以關於B+樹和B-樹的詳細操作會在後面的文章中單獨描述。

5.13 哈夫曼樹和哈夫曼編碼

哈夫曼樹是最優二叉樹,帶權路徑最短。
路徑:樹中一個結點到另一個結點分支構成的路線。
路徑長度:路徑上的分支數目。
樹的路徑長度:從根到每個結點的路徑長度之和。
帶權路徑長度:結點有權值,結點到根之間的路徑長度乘以結點的權值。
**樹的帶權路徑長度:**WPL,樹中所有葉子結點帶權路徑長度之和。

哈夫曼樹的特點:權值越大的結點,距離根結點越近。樹中沒有度為1的結點的哈夫曼樹稱為嚴格(正則)二叉樹。

哈夫曼樹的構造方法:
給定n個權值
(1)將n個權值看作只有根的n棵二叉樹,集合記為F。
(2)F中挑選兩棵根結點權值最小的(a, b)作為左右子樹,構成新樹c。
(3)F中刪除a與b,加入c。

【例子5】哈夫曼樹的構建

哈夫曼編碼:
字首編碼:任一字元的編碼都不是另一個字元編碼的字首。
而哈夫曼編碼就是長度最短的字首編碼。對構造好的哈夫曼樹,左分支表示0,右分支表示1(根據題目要求來)

【例子6】哈夫曼編碼
A、B、C、D的權值(或者出現頻率)為0.4、0.3、0.1和0.2,求哈夫曼編碼。