計算機考研之數據結構-樹

數據結構-樹

概念

定義

  1. 根結點只有一個
  2. 除根結點之外其餘全部結點有且僅有一個前驅
  3. 全部結點均可以用任意個後驅

術語


以A-B-E-K路徑爲例:python

  • 祖先結點:結點到根結點路徑上的全部前驅,A,B,E都是K的祖先結點。
  • 子孫結點:結點的全部後驅,B,E,K都是A的子孫結點。
  • 雙親結點:結點的直接前驅,E是K的雙親結點。
  • 孩子結點:結點的直接後驅,K是E的孩子結點。
  • 兄弟結點:相同雙親的結點,E,F是兄弟結點。數組

  • 結點的度:結點的子結點個數
  • 樹的度:結點的最大度數
  • 分支結點:度大於0的結點。
  • 葉子結點:度等於0的結點。數據結構

  • 結點的層次:從樹根開始數層數。
  • 結點的深度:自上向下。
  • 結點的高度:自下向上。
  • 樹的高度(深度):結點的最大層數。post

  • 有/無序樹:結點的子樹是否能夠有序。
  • 平衡/豐滿樹:除最底層,其餘層都是滿的。
  • 森林:不相交樹的集合。ui

性質

  1. 樹的結點數等於全部結點的度之和+1

二叉樹

定義

  1. 最大度爲2
  2. 能夠爲空
  3. 有序樹

特殊

幾個特殊的二叉樹:編碼

  • 滿二叉樹:葉子結點都集中在最後一層的二叉樹。
  • 徹底二叉樹:若是對滿二叉樹的結點進行編號,如上圖所示。編號連續滿二叉樹子集稱爲徹底二叉樹。
  • 二叉排序樹:左子樹結點的關鍵字均小於右子樹的結點繁榮關鍵字。
  • 平衡二叉樹:樹中任意一個結點的左右子樹的深度差不超過1。

性質

  1. 非空二叉樹上的葉子結點數等於雙分支結點數加1。
  2. 二叉樹第i層上最多有\(2^{i-1}\)個結點。
  3. 徹底二叉樹對各結點從上到下,從左到右分別從1開始進行編號則對\(a_i\)有:
    1. 若i≠1,雙親結點編號爲[i/2]。
    2. 若2i≤n,a左孩編號爲2i,反之無左孩。
    3. 若2i+1≤n,a右孩編號爲2i+1,反之無右孩。
      若0開始編號,雙親[i/2]-1,左孩2i+1,右孩2i+2。

存儲

存儲結構通常分兩種,順序或者鏈式。spa

  1. 順序存儲
    由於咱們已經知道了徹底二叉樹是知足必定性質的,這樣即便是順序存儲也能很方便的找到其雙親和孩子結點。可是對於非徹底二叉樹的狀況會很浪費存儲空間。
  2. 鏈式存儲
typedef struct BNode{
    int data;
    struct BNode *lchild;
    struct BNode *rchild;
}BNode, *BTree;

遍歷

遞歸

遍歷有先序,中序,和後序三種方式,區別在於訪問根結點的順序。
遞歸遍歷比較簡單,這裏就舉一個前序的例子。假設visit是對結點的操做。3d

void PreOrder(BTree T){ //先序遍歷
    if(T==NULL) return;
    visit(T); //訪問根結點
    PreOrder(T->lchild); //遞歸遍歷左子樹
    PreOrder(T->rchild); //遞歸遍歷右子樹
}

時間複雜度O(n),空間複雜度O(n)。

以上圖爲例:指針

  • 前序:1 2 4 6 3 5
  • 中序:2 6 4 1 3 5
  • 後序:6 4 2 5 3 1

非遞歸

重點在於非遞歸的實現方式:

前序
這裏要利用到棧的性質,咱們向左一直遍歷樹,而後保存這些左結點的,等遍歷到了左下角,開始彈棧,轉向遍歷右結點。

void PreOrder(BTree T){
    InitStack(S); BTree p=T;
    while(p||!isEmpty(S)){
        while(p){
            visit(p);
            stack.push(p);
            p=p.lchild;
        }
        p=stack.pop();
        p=p.rchild;
    }
}

中序:
中序和後序惟一的區別就是:訪問根結點的順序不同。

void PreOrder(BTree T){
    InitStack(S); BTree p=T;
    while(p||!isEmpty(S)){
        while(p){
            Push(S,p);
            p=p.lchild;
        }
        Pop(S,p);
        visit(p); // 彈棧後才訪問根結點
        p=p.rchild;
    }
}

後序:
後序的狀況稍微複雜一點。

void PreOrder(BTree T){
    InitStack(S); BTree p=T; BTree last=NULL;
    while(p||!isEmpty(S)){
        while(p){
            Push(S,p);
            p=p.lchild;
        }
        GetTop(S, p);
        if(p.rchild==NULL && p==last){
            visit(p);
            Pop(S);
            last=p;
            p=NULL;
        }
        else{
            p=p.rchild;
        }
    }
}

層次遍歷

逐層遍歷二叉樹

void LevelOrder(BTree T){
    InitQueue(Q); BTree p;
    EnQueue(Q,T);
    while(!IsEmpty(Q)){
        DeQueue(Q, p);
        visit(p);
        if(p->lchild != NULL) EnQueue(Q, P->lchild);
        if(p->rchild != NULL) EnQueue(Q, P->rchild);
    }
}

遍歷構造

給定前序+中序或者後序+中序的遍歷序列,根據序列構造二叉樹。注意:前序和後序不必定惟一肯定二叉樹

BNode* create(vector<int> &inorder, vector<int> &postorder, int is, int ie, int ps, int pe){
    if(ps > pe){
        return nullptr;
    }
    BNode* node = new BNode(postorder[pe]);
    int pos;
    for(int i = is; i <= ie; i++){
        if(inorder[i] == node->val){
            pos = i;
            break;
        }
    }
    node->left = create(inorder, postorder, is, pos - 1, ps, ps + pos - is - 1);
    node->right = create(inorder, postorder, pos + 1, ie, pe - ie + pos, pe - 1);
    return node;
}

若是方便對數組進行切割的話,代碼會更簡單,舉個例子:

def buildTree(self, inorder, postorder):
    if not inorder or not postorder:
        return None
    
    root = TreeNode(postorder.pop())
    inorderIndex = inorder.index(root.val)
    
    root.right = self.buildTree(inorder[inorderIndex+1:], postorder)
    root.left = self.buildTree(inorder[:inorderIndex], postorder)
    
    return root

注意若是是前序+中序的話,right和left的位置要調換

線索二叉樹

在二叉樹中,存在大量空指針域,能夠利用這些空指針域來加快遍歷二叉樹。

定義

線索規則:

  • ptr->lchild爲空,則lchild指向其中序遍歷的前驅結點。
  • ptr->lchild爲空,則rchild指向其中序遍歷的後繼結點。
typedef struct ThreadNode{
    int data;
    struct ThreadNode *lchild, *rchild;
    int ltag, rtag;
}ThreadNode, *ThreadTree

這裏的ltag和rtag用於指示指針指向的是子結點仍是線索。

構造

在中序遞歸遍歷中插入線索:

void CreateInThread(ThreadTree T){
    ThreadTree pre=NULL;
    InThread(T,pre);
    pre->rchild=NULL;
    pre->rtag=1;
}

void InThread(ThreadTree &p, ThreadTree &pre){
    if(p!NULL){
        InThread(p->lchild,pre); //線索化左子樹
        
        // 線索化過程,除了線索化,其餘跟普通的遍歷二叉樹同樣
        if(p->lchild==NULL){
            p->lchild=pre;
            p->ltag=1;
        }
        if(pre!=NULL&&pre->rchild==NULL){
            pre->rchild=p;
            pre->rtag=1;
        }
        pre=p;
        // 線索化結束

        InThread(p->rchild,pre); //線索化右子樹
    }
}

遍歷

這裏能夠看出,二叉樹被線索化以後近似於一個線性的結構。

//t指向頭結點,頭結點左鏈lchild指向根結點,頭結點右鏈rchild指向中序遍歷的最後一個結點。
//中序遍歷二叉線索樹表示二叉樹t
int InOrder(BTree T)
{
    BTree *p;
    *p = t->lchild;                               //p指向根結點
    while(p != t)                               //空樹或遍歷結束時p == t
    {
        while(p->ltag == Link)                       //當ltag = 0時循環到中序序列的第一個結點
        {
            p = p->lchild;
        }
        printf("%c ", p->data);                      //顯示結點數據,能夠更改成其餘對結點的操做
        while(p->rtag == Thread && p->rchild != t)
        {
            p = p->rchild;
            printf("%c ", p->data);
        }
 
        p = p->rchild;                         //p進入其右子樹
    }
 
    return OK;
}

樹與森林

轉化

樹轉二叉樹
樹轉化爲二叉樹能夠理解爲使用一個二叉鏈表來存儲樹的結構,使得鏈表中的指針一個指向本身的孩子結點一個指向本身的兄弟結點,這樣這課樹就表示成了二叉樹。
這種存儲結構通常稱之爲孩子兄弟存儲結構

過程以下:

  1. 將同一結點的孩子串起。
  2. 將每一個結點的分支從左到右除第一個之外所有剪掉。

二叉樹轉化樹
這個其實就是樹轉二叉樹的逆操做。

  1. 將二叉樹從左上到右下進行斜向的分層。
  2. 爲每層的結點找到父結點。
  3. 鏈接父結點,並刪除層之間的結點鏈接。

森林轉二叉樹
根據孩子兄弟表示法,根結點是隻有左孩子可是沒有右兄弟的,因此能夠把第二棵樹接到第一個棵樹的右孩上,第三棵樹接到第二課樹根結點的右孩上,以此類推。

  1. 先將森林中的樹按照樹轉二叉樹的步驟進行二叉樹轉化
  2. 將根結點的右孩與其餘樹進行拼接。

二叉樹轉森林

  1. 斷開二叉樹的右孩,重複此操做直到全部二叉樹都沒有右孩。
  2. 把這些二叉樹按照二叉樹轉樹的操做轉化爲樹

遍歷

樹的遍歷
遍歷分先序和後序,也叫先跟和後根。區別在於對跟結點的訪問在遍歷子樹以前仍是以後。

先序:ABEFCGDHIJ
後序:EFBGCHIJDA
當樹轉化爲二叉樹以後,樹的先序對應二叉樹的先序,樹的後序對應二叉樹的中序

森林的遍歷
森林遍歷與樹同理。

對於樹與森林,中序遍歷和後序遍歷是一個意思

哈夫曼樹

概念

哈夫曼樹是帶權路徑長度(WPL)最小的樹。
那麼首先明確帶權路徑長度(WPL)的概念。

\[WPL=\sum_{i=1}^nw_i\times l_i\]

w爲結點的權值,l爲路徑長度。

對於上圖有WPL:
a: 7x2+5x2+2x2+4x2=36
b: 7x3+5x3+2x1+4x2=46
c: 7x1+5x2+2x3+4x3=35

構造

給定n個權值,利用這n個權值構造哈夫曼二叉樹。

  1. 將這n的權值視做n棵根爲n的樹,記作F集合。
  2. 從F選擇兩棵根結點權值最小的樹構造新的二叉樹(新的根結點的權值等於兩個根結點之和)。
  3. 從F刪去這兩個結點,並加入新結點。
  4. 重複2,3直到F中只剩一棵樹。

因而能夠看出:

  1. 權值越大離根越近。
  2. 沒有度爲1的結點,也叫正則(嚴格)二叉樹
  3. 樹的帶權路徑最短

哈夫曼編碼

哈夫曼樹最經常使用的一個例子就是利用哈夫曼樹進行文件壓縮。
咱們能夠根據字符出現次序爲其進行哈夫曼編碼,次數越多越短,不然反之。
若是有一個文本,a出現了45次,b13,c12,f5,e9,d16。共100個。
能夠構造獲得哈夫曼樹及其編碼。


結點
計算WPL獲得是224,比起3x100來壓縮了76個字符的長度。

哈夫曼n叉樹

注意哈夫曼樹不必定是二叉樹,也有多是多叉樹,但有可能須要0權值的結點來補齊,構造過程與二叉樹區別在於從集合拿出樹的個數。

小結

習題

在一棵度爲4的樹T中,如有20個度爲4的結點,10個度爲3的結點,1個度爲2的結點,10個度爲1的結點,則樹T的葉結點的個數是():
答案:82
解析:
結點度數之和爲:\(20\times 4+10\times 3+1\times 2+10\times 1=122\)
樹的結點數量爲結點度數之和+1,即123個結點。
葉結點即度數爲0的結點,度數大於0的結點數量爲:\(20+10+1+10=41\),總結點數量-度數大於0結點的數量,即82

相關文章
相關標籤/搜索