1. 程式人生 > >『嗨威說』數據結構 - 第五章學習內容小結

『嗨威說』數據結構 - 第五章學習內容小結

方法 you b樹 二叉 log microsoft 三種 back isp

本文主要內容

  一、樹的概念

  二、樹的重中之重——二叉樹

  三、樹的升級應用:哈夫曼樹

  四、本節應用習題

  五、個人反思與未來計劃

一、樹的基本概念

    (1)樹的定義:

        樹(Tree):n(n >= 0)個節點構成的有限集合。

        • n = 0時,稱為空樹;
        • 對任意一棵空樹(n > 0),它具備以下性質:
        • 樹中有一個稱為**根(Root)**的特殊節點,用r(root)表示;
        • 其余節點可分為m(m > 0)個互不相交的有限集T1,T2,...Tm,其中每個集合本省又是一棵樹,稱為原來樹的子樹(SubTree)

        註意:   ①子樹不能相交

              ②除了根節點外,每個節點有且僅有一個父節點

              ③一個N個節點的樹有N-1條邊

    (2)樹的基本術語:

        • 節點的度(Degree):節點的子樹個數。
        • 樹的度:樹的所有節點中最大的度數(樹的度通常為節點個數的N-1)。
        • 葉節點(Leaf):度為0的節點(也稱葉子節點)。
        • 父節點(Parent):有子樹的節點是其子樹的父節點。
        • 子節點(Child):若A節點是B節點的父節點,則稱B節點是A節點的子節點。
        • 兄弟節點(Sibling):具有同一個父節點的各節點彼此是兄弟節點。
        • 路徑和路徑長度:從節點n1nk的路徑為一個節點序列n1,n2,n3,...,nknini+1的父節點。路徑所包含邊的個數為路徑長度。
        • 節點的層次(Level):規定根節點在第0層,它的子節點是第1層,子節點的子節點是第2層,以此類推。
        • 樹的深度(Depth):樹中所有節點中的最大層次是這棵樹的深度(因為上面是從第0層開始,深度 = 第最大層數 + 1)

技術分享圖片

    

    (3)樹的四種遍歷方式:

          先序遍歷、中序遍歷、後序遍歷、層次遍歷。

          前三種遍歷方式都是以根的遍歷先後為基準,層次遍歷就比較簡單了,按深度層次遍歷,不再贅述。先中後序遍歷做了一張圖,可以用下面這張圖來清晰解釋:

技術分享圖片

二、樹的重中之重——二叉樹:

  (1)二叉樹的性質:

      二叉樹的每個結點至多有二棵子樹(不存在度大於2的結點),二叉樹的子樹有左右之分,次序不能顛倒。

      ① 在二叉樹的第K層上,最多有 2k-1 (K >= 1)個結點
      ② 深度為K的二叉樹,最多有 2k - 1 個結點(K>=1)
      ③ 對於任何一棵二叉樹,如果其葉子結點的個數為K,度為2的結點數為M,則K=M+1
      ④ 對於一棵有 n 個結點的完全二叉樹的結點按層次進行編號(如上圖,從第一層到第 (log 2n 向下取整),每層從左到右),對任意結點 i (1<i<n),有:
        →如果i=1,則結點i無父結點,是二叉樹的根,如果i>1,則父結點為 i/2 向下取整
        →如果2i>n,則結點i為葉子結點,無左子結點,否則,其左子結點為2i
        →如果2i+1>n,則結點i無右子結點,否則,其右子結點是結點2i+1

  (2)二叉樹的存儲結構表示方法:

      ① 孩子表示法:用指針指出每個節點的孩子節點

        #優點:尋找一個節點的孩子節點比較方便。

        #缺點:尋找一個節點得雙親節點很不方便。

      ② 雙親表示法:用指針表示出每個節點的雙親節點

        #優點:尋找一個節點得雙親節點操作實現很方便

        #缺點:尋找一個節點的孩子節點很不方便

      ③ 孩子雙親表示法:用指針既表示出每個節點得雙親節點,也表示出每個節點的孩子節點

        #優點:找某個節點的雙親節點和孩子節點非常方便

      ④ 孩子兄弟表示法:即表示出每個節點的第一個孩子節點,也表示出每個節點的下一個兄弟節點

        #優點:找某個節點的兄弟結點節點和孩子節點非常方便

  (3)二叉樹的基本建立與使用:

      此處應用老師作業1的編程例題進行講解——List Leaves

      題目:

技術分享圖片
Given a tree, you are supposed to list all the leaves in the order of top down, and left to right.

Input Specification:
Each input file contains one test case. For each case, the first line gives a positive integer N (≤10) which is the total number of nodes in the tree -- and hence the nodes are numbered from 0 to N−1. Then N lines follow, each corresponds to a node, and gives the indices of the left and right children of the node. If the child does not exist, a "-" will be put at the position. Any pair of children are separated by a space.

Output Specification:
For each test case, print in one line all the leaves indices in the order of top down, and left to right. There must be exactly one space between any adjacent numbers, and no extra space at the end of the line.

Sample Input:
8
1 -
- -
0 -
2 7
- -
- -
5 -
4 6
Sample Output:
4 1 5
7-1 List Leaves (30 分)題目

      ①首先:二叉樹的基本數據結構的建立

        對於樹的建立,主要以打包結構體來實現,可打包加入左右孩子、數據data、編號等,這裏以最簡單的只保留左右孩子編號的結構體進行初涉講解。

struct BiTree{
    int l;
    int r;
};

      ②其次:建樹,找出根節點編號返回

int buildTree(BiTree tree[])
{
    //標記節點是否出現 
    bool number[MAX] = {false};
    
    //輸入節點數 
    int times;
    scanf("%d",&times);
    
    //存儲節點 
    for(int i = 0;i<times;i++)
    {
        char a,b;
        int Ta,Tb;
        getchar();
        scanf("%c %c",&a,&b);
        Ta = changeLegal(a);
        Tb = changeLegal(b);
         tree[i].l = Ta;
         tree[i].r = Tb;
         number[Ta] = true;
        number[Tb] = true;    
    }
    
    //搜索根節點 
    for(int i = 0;i<times;i++)
        if(number[i] == false)
            return i;
}

      ③對簡單二叉樹的應用:實現本題目的要求——找葉子節點

void findLeaves(BiTree tree[],int rt)
{
    //輸出空格標記 
    int mark = 0;
    
    //申請隊列並讓根節點入隊 
    queue<int> temp;
    temp.push(rt);
    
    //隊列循環 
    while(!temp.empty())
    {
        //輸出隊列出隊首個元素 
        int T = temp.front();
        temp.pop();
        
        //節點入隊 
        if(tree[T].l!=-1) temp.push(tree[T].l);
        if(tree[T].r!=-1) temp.push(tree[T].r);
        //如果搜到左右孩子都是空的,那就是葉子節點了 直接輸出即可 因為是層序遍歷 
        if(tree[T].l == -1 && tree[T].r == -1) 
        {
            if(mark == 0)
            {
                printf("%d",T);
                mark = 1;
            }
            else printf(" %d",T);
        }
    }
}

      完整代碼展示:

技術分享圖片
#include<stdio.h>
#include<queue> 
#define MAX 11
using namespace std;

//結構體樹節點建立 
struct BiTree{
    int l;
    int r;
};

//函數原型聲明 
int buildTree(BiTree tree[]);
void findLeaves(BiTree tree[],int rt);

int main()
{
    //建立基於數組的樹 
    BiTree tree[MAX];
    //存儲樹節點並識別根節點 
    int root = buildTree(tree);
    //搜索葉子節點並輸出 
    findLeaves(tree,root);
    return 0;
}

//轉換輸入的內容,將char類型轉為int 
int changeLegal(char x)
{
    if(x == -) return -1;
    else return x - 0;
}

int buildTree(BiTree tree[])
{
    //標記節點是否出現 
    bool number[MAX] = {false};
    
    //輸入節點數 
    int times;
    scanf("%d",&times);
    
    //存儲節點 
    for(int i = 0;i<times;i++)
    {
        char a,b;
        int Ta,Tb;
        getchar();
        scanf("%c %c",&a,&b);
        Ta = changeLegal(a);
        Tb = changeLegal(b);
         tree[i].l = Ta;
         tree[i].r = Tb;
         number[Ta] = true;
        number[Tb] = true;    
    }
    
    //搜索根節點 
    for(int i = 0;i<times;i++)
        if(number[i] == false)
            return i;
}
 
void findLeaves(BiTree tree[],int rt)
{
    //輸出空格標記 
    int mark = 0;
    
    //申請隊列並讓根節點入隊 
    queue<int> temp;
    temp.push(rt);
    
    //隊列循環 
    while(!temp.empty())
    {
        //輸出隊列出隊首個元素 
        int T = temp.front();
        temp.pop();
        
        //節點入隊 
        if(tree[T].l!=-1) temp.push(tree[T].l);
        if(tree[T].r!=-1) temp.push(tree[T].r);
        //如果搜到左右孩子都是空的,那就是葉子節點了 直接輸出即可 因為是層序遍歷 
        if(tree[T].l == -1 && tree[T].r == -1) 
        {
            if(mark == 0)
            {
                printf("%d",T);
                mark = 1;
            }
            else printf(" %d",T);
        }
    }
}
本題作者答案寫法

三、樹的升級拓展之一:哈夫曼樹

  (1)定義:

      哈夫曼樹,又稱最優樹,是一類帶權路徑長度最短的樹。首先有幾個概念需要清楚:

      1、路徑和路徑長度

      從樹中一個結點到另一個結點之間的分支構成兩個結點的路徑,路徑上的分支數目叫做路徑長度。樹的路徑長度是從樹根到每一個結點的路徑長度之和。

      2、帶權路徑長度

      結點的帶權路徑長度為從該結點到樹根之間的路徑長度與結點上權的乘積。樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和,通常記作WPL。

      若有n個權值為w1,w2,...,wn的結點構成一棵有n個葉子結點的二叉樹,則樹的帶權路徑最小的二叉樹叫做哈夫曼樹或最優二叉樹。

技術分享圖片

      在上圖中,3棵二叉樹都有4個葉子結點a、b、c、d,分別帶權7、5、2、4,則它們的帶權路徑長度為

      (a)WPL = 7*2 + 5*2 + 2*2 + 4*2 = 36

      (b)WPL = 4*2 + 7*3 + 5*3 + 2*1 = 46

      (c)WPL = 7*1 + 5*2 + 2*3 + 4*3 = 35

      其中(c)的WPL最小,可以驗證,(c)恰為哈夫曼樹。

  (2)創建哈夫曼樹:

      假設有n個結點,n個結點的權值分別為w1,w2,...,wn,構成的二叉樹的集合為F={T1,T2,...,Tn},則可構造一棵含有n個葉子結點的哈夫曼樹。步驟如下:

      (1)從F中選取兩棵根結點權值最小的樹作為左右子樹構造一棵新的二叉樹,其新的二叉樹的權值為其左右子樹根結點權值之和;

      (2)從F中刪除上一步選取的兩棵二叉樹,將新構造的樹放到F中;

      (3)重復(1)(2),直到F只含一棵樹為止。

      技術分享圖片

  (3)哈夫曼編碼:

      我們約定左分支表示字符‘0‘,右分支表示字符‘1‘,在哈夫曼樹中從根結點開始,到葉子結點的路徑上分支字符組成的字符串為該葉子結點的哈夫曼編碼。上面代碼所創建的哈夫曼樹如下所示:

技術分享圖片

      可以看出3被編碼為00,1為010,2為011,4為10,5為11。在這些編碼中,任何一個字符的編碼均不是另一個字符編碼的前綴。

   註:因本內容在本學期要求僅理解創建過程和哈夫曼編碼即可,故具體代碼實現暫未總結。

四、本章習題練習

    (1)深入虎穴:

技術分享圖片
深入虎穴 (30 分)
著名的王牌間諜 007 需要執行一次任務,獲取敵方的機密情報。已知情報藏在一個地下迷宮裏,迷宮只有一個入口,裏面有很多條通路,每條路通向一扇門。每一扇門背後或者是一個房間,或者又有很多條路,同樣是每條路通向一扇門…… 他的手裏有一張表格,是其他間諜幫他收集到的情報,他們記下了每扇門的編號,以及這扇門背後的每一條通路所到達的門的編號。007 發現不存在兩條路通向同一扇門。

內線告訴他,情報就藏在迷宮的最深處。但是這個迷宮太大了,他需要你的幫助 —— 請編程幫他找出距離入口最遠的那扇門。

輸入格式:
輸入首先在一行中給出正整數 N(<10
?5
?? ),是門的數量。最後 N 行,第 i 行(1≤i≤N)按以下格式描述編號為 i 的那扇門背後能通向的門:

K D[1] D[2] ... D[K]
其中 K 是通道的數量,其後是每扇門的編號。

輸出格式:
在一行中輸出距離入口最遠的那扇門的編號。題目保證這樣的結果是唯一的。

輸入樣例:
13
3 2 3 4
2 5 6
1 7
1 8
1 9
0
2 11 10
1 13
0
0
1 12
0
0
輸出樣例:
12
深入虎穴-題目

        簡單級難度,理清思路一個dfs就搞定了,關鍵部分:

void dfs(int rt,int step)
{
    if(step>step_max) {
        ans=rt;
        step_max=step;
    }
    for(int i=0;i<E[rt].size();++i){
        int to=E[rt][i];
        dfs(to,step+1);
    }    
}

        完整代碼展示:幹凈整潔

#include <iostream>
#include <queue>
#include <stdio.h>
#include <vector>
using namespace std;
const int maxn = 1e5+10;
queue<int>q;
vector<int>E[maxn];
int N,to,ans=0,step_max=0;
void dfs(int rt,int step)
{
    if(step>step_max) {
        ans=rt;
        step_max=step;
    }
    for(int i=0;i<E[rt].size();++i){
        int to=E[rt][i];
        dfs(to,step+1);
    }    
}
int main()
{
    int step=1;
    scanf("%d",&N);
    for(int i=1;i<=N;++i){
        int num;
        scanf("%d",&num);
        while(num--)
        {
            scanf("%d",&to);
            E[i].push_back(to);
        }
    }
    dfs(1,1);
    printf("%d",ans);
    return 0;
}

  (2)樹的同構:

技術分享圖片
樹的同構 (30 分)
給定兩棵樹T1和T2。如果T1可以通過若幹次左右孩子互換就變成T2,則我們稱兩棵樹是“同構”的。例如圖1給出的兩棵樹就是同構的,因為我們把其中一棵樹的結點A、B、G的左右孩子互換後,就得到另外一棵樹。而圖2就不是同構的。
圖1
圖2
現給定兩棵樹,請你判斷它們是否是同構的。
輸入格式:
輸入給出2棵二叉樹樹的信息。對於每棵樹,首先在一行中給出一個非負整數N (≤10),即該樹的結點數(此時假設結點從0到N−1編號);隨後N行,第i行對應編號第i個結點,給出該結點中存儲的1個英文大寫字母、其左孩子結點的編號、右孩子結點的編號。如果孩子結點為空,則在相應位置上給出“-”。給出的數據間用一個空格分隔。註意:題目保證每個結點中存儲的字母是不同的。

輸出格式:
如果兩棵樹是同構的,輸出“Yes”,否則輸出“No”。

輸入樣例1(對應圖1):
8
A 1 2
B 3 4
C 5 -
D - -
E 6 -
G 7 -
F - -
H - -
8
G - 4
B 7 6
F - -
A 5 1
H - -
C 0 -
D - -
E 2 -
輸出樣例1:
Yes
輸入樣例2(對應圖2):
8
B 5 7
F - -
A 0 3
C 6 -
H - -
D - -
G 4 -
E 1 -
8
D 6 -
B 5 -
E - -
H - -
C 0 2
G - 3
F - -
A 1 4
輸出樣例2:
No
樹的同構 - 題目

      本題的難度主要在於如何判斷兩個樹是否同構,一開始給自己挖坑了,想用叠代法去寫,結果發現越寫越復雜,後來換了一種騷遞歸的方法居然發現是如此的簡單,把握好老師說的:停留在本層的方法,一切就很容易了。

      ①首先:搜索到當前的節點沒有孩子,即自己是葉子,那他的兩個孩子一定是判為相等的,true返回

      ②其次:如果當前節點兩邊都有孩子,並且當前兩棵樹的孩子一樣,那麽就繼續往下搜,並且搜索有四種方向:

        A樹左孩子與B樹左孩子、A樹左孩子與B樹右孩子、A樹右孩子與B樹左孩子、A樹右孩子與B樹右孩子

      因此關鍵的判定函數就可以寫出來了:

//judge function
bool isEqual(Tree *a,int root_a,Tree *b,int root_b)
{
    if(root_a == -1 && root_b == -1)
        return true;
        
    if((root_a != -1 && root_b !=-1))
        if(a[root_a].name == b[root_b].name)
        {
            if(isEqual(a,a[root_a].lchild,b,b[root_b].lchild) && isEqual(a,a[root_a].rchild,b,b[root_b].rchild))
                return true;
            if(isEqual(a,a[root_a].lchild,b,b[root_b].rchild) && isEqual(a,a[root_a].rchild,b,b[root_b].lchild))
                return true;
        }
    return false;
}

      完整代碼展示:(時刻提醒自己模塊化書寫

#include<stdio.h>

//define Node
typedef struct{
    char name;
    int lchild;
    int rchild;
}Tree;

//define variable
int times1,times2;

//define function
int buildTree(Tree *&t,int node);
bool isEqual(Tree *a,int root_a,Tree *b,int root_b);

//MAIN
int main()
{
    //create Tree
    Tree *a,*b;
    
    //build Tree
    scanf("%d",&times1);
    int root_a = buildTree(a,times1);
    scanf("%d",&times2);
    int root_b = buildTree(b,times2);
    
    //find equal Tree
    if(isEqual(a,root_a,b,root_b)) printf("Yes");
    else printf("No");
    
    return 0;
}

//build Tree Function
int buildTree(Tree *&t,int node)
{
    t = new Tree[node];
    bool* mark = new bool[node];
    for(int i = 0;i<node;i++) mark[i] = false;
    for(int i = 0;i<node;i++)
    {
        //Warning!↓ 
        getchar();
        //Warning!↑
         
        char temp_n,temp_l,temp_r;
        scanf("%c %c %c",&temp_n,&temp_l,&temp_r);
        t[i].name = temp_n;
        if(temp_l != -) 
        {
            t[i].lchild = temp_l - 0;
            mark[temp_l - 0] = true;
        }
        else t[i].lchild = -1;
        if(temp_r != -) 
        {
            t[i].rchild = temp_r - 0;
            mark[temp_r - 0] = true;
        }
        else t[i].rchild = -1;
    }
    for(int i = 0;i<node;i++)
        if(mark[i] != true)
            return i;
    return -1;
}

//judge function
bool isEqual(Tree *a,int root_a,Tree *b,int root_b)
{
    if(root_a == -1 && root_b == -1)
        return true;
        
    if((root_a != -1 && root_b !=-1))
        if(a[root_a].name == b[root_b].name)
        {
            if(isEqual(a,a[root_a].lchild,b,b[root_b].lchild) && isEqual(a,a[root_a].rchild,b,b[root_b].rchild))
                return true;
            if(isEqual(a,a[root_a].lchild,b,b[root_b].rchild) && isEqual(a,a[root_a].rchild,b,b[root_b].lchild))
                return true;
        }
    return false;
}

五、個人反思及未來計劃

    (1)對樹和圖比較抽象的數據結構上基礎有些不牢,特別是碰到鏈式存儲的指針使用的時候,需要找時間給自己多加強這方面的學習。

    (2)2019年5月12日即將參加CCPC廣東省ACM省賽,在此之前熟練學習『BFS』『DFS』『雙搜』『啟發式搜索』『A*』『叠代加深搜索』『IDA*』『回溯法』

    (3)為樹蛙預備隊員轉型樹蛙正式成員準備復習高數以及Python    

    (4)完成論文標解Improving patch-based scene text script identification with ensembles of conjoined networks

『嗨威說』數據結構 - 第五章學習內容小結