1. 程式人生 > >樹及其應用c語言實現(資料結構複習最全筆記)

樹及其應用c語言實現(資料結構複習最全筆記)

一.樹的基本概念

 

 

二.二叉樹

1.二叉樹的定義

2.二叉樹的性質 

此外在這裡在介紹下完美二叉樹的概念及重要性質

 完全二叉樹是效率很高的資料結構,完全二叉樹是由滿二叉樹而引出來的。對於深度為K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度為K的滿二叉樹中編號從1至n的結點一一對應時稱之為完全二叉樹。

性質:

(1)所有的葉結點都出現在第k層或k-l層(層次最大的兩層)

(2)對任一結點,如果其右子樹的最大層次為L,則其左子樹的最大層次為L或L+l。

3.二叉樹的儲存結構

4.二叉樹的遍歷(重點) 

二叉樹遍歷的應用:

【例】二叉樹的建立

Status CreateBiTree(BiTree &T) //&的意思是傳進來節點指標的引用,括號內等價於 BiTreeNode* &T,目的是讓傳遞進來的指標發生改變
{                        
    char c;
    cin >> c;
    if(c == ' ')             //當遇到 時,令樹的根節點為NULL,從而結束該分支的遞迴
        T = NULL;
    else
    {
        if(!(T = (BiTNode*)malloc(sizeof(BiTNode))));
            exit(OVERFLOW);
        //T = new BiTreeNode;

        T->data=c;//生成根節點
        createBiTree(T->lchild);//構造左子樹
        createBiTree(T->rchild);//構造右子樹
    }
}

 

重點例題:已知兩種遍歷序列讓你還原二叉樹

5.線索二叉樹

<1>線索二叉樹的原理

    通過考察各種二叉連結串列,不管兒叉樹的形態如何,空鏈域的個數總是多過非空鏈域的個數。準確的說,n各結點的二叉連結串列共有2n個鏈域,非空鏈域為n-1個,但其中的空鏈域卻有n+1個。如下圖所示。

    因此,提出了一種方法,利用原來的空鏈域存放指標,指向樹中其他結點。這種指標稱為線索

    記ptr指向二叉連結串列中的一個結點,以下是建立線索的規則:

    (1)如果ptr->lchild為空,則存放指向中序遍歷序列中該結點的前驅結點。這個結點稱為ptr的中序前驅;

    (2)如果ptr->rchild為空,則存放指向中序遍歷序列中該結點的後繼結點。這個結點稱為ptr的中序後繼;

    顯然,在決定lchild是指向左孩子還是前驅,rchild是指向右孩子還是後繼,需要一個區分標誌的。因此,我們在每個結點再增設兩個標誌域ltag和rtag,注意ltag和rtag只是區分0或1數字的布林型變數,其佔用記憶體空間要小於像lchild和rchild的指標變數。結點結構如下所示。

    其中:

    (1)ltag為0時指向該結點的左孩子,為1時指向該結點的前驅;

    (2)rtag為0時指向該結點的右孩子,為1時指向該結點的後繼;

    (3)因此對於上圖的二叉連結串列圖可以修改為下圖的養子。

<2>線索二叉樹結構實現

    二叉線索樹儲存結構定義如下:


/* 二叉樹的二叉線索儲存結構定義*/
typedef enum{Link, Thread}PointerTag;    //Link = 0表示指向左右孩子指標;Thread = 1表示指向前驅或後繼的線索
 
typedef struct BitNode
{
       char data;                                      //結點資料
       struct BitNode *lchild, *rchild;                //左右孩子指標
       PointerTag  Ltag;                               //左右標誌
       PointerTag  rtal;
}BitNode, *BiTree;

  線索化的實質就是將二叉連結串列中的空指標改為指向前驅或後繼的線索。由於前驅和後繼資訊只有在遍歷該二叉樹時才能得到,所以,線索化的過程就是在遍歷的過程中修改空指標的過程。

    中序遍歷線索化的遞迴函式實現如下:

BiTree pre;                 //全域性變數,始終指向剛剛訪問過的結點
//中序遍歷進行中序線索化
void InThreading(BiTree p)
{
    if(p)
    {
        InThreading(p->lchild);          //遞迴左子樹線索化
                //===
        if(!p->lchild)           //沒有左孩子
        {
            p->ltag = Thread;    //前驅線索
            p->lchild = pre; //左孩子指標指向前驅
        }
        if(!pre->rchild)     //沒有右孩子
        {
            pre->rtag = Thread;  //後繼線索
            pre->rchild = p; //前驅右孩子指標指向後繼(當前結點p)
        }
        pre = p;
                //===
        InThreading(p->rchild);      //遞迴右子樹線索化
    }
}

上述程式碼除了//===之間的程式碼以外,和二叉樹中序遍歷的遞迴程式碼機會完全一樣。只不過將列印結點的功能改成了線索化的功能。

    中間部分程式碼做了這樣的事情:

因為此時p結點的後繼還沒有訪問到,因此只能對它的前驅結點pre的右指標rchild做判斷,if(!pre->rchild)表示如果為空,則p就是pre的後繼,於是pre->rchild = p,並且設定pre->rtag = Thread,完成後繼結點的線索化。如圖:

    if(!p->lchild)表示如果某結點的左指標域為空,因為其前驅結點剛剛訪問過,賦值了pre,所以可以將pre賦值給p->lchild,並修改p->ltag = Thread(也就是1)以完成前驅結點的線索化。

    完成前驅和後繼的判斷後,不要忘記當前結點p賦值給pre,以便於下一次使用。

    有了線索二叉樹後,對它進行遍歷時,其實就等於操作一個雙向連結串列結構。

    和雙向連結串列結點一樣,在二叉樹連結串列上新增一個頭結點,如下圖所示,並令其lchild域的指標指向二叉樹的根結點(圖中第一步),其rchild域的指標指向中序遍歷訪問時的最後一個結點(圖中第二步)。反之,令二叉樹的中序序列中第一個結點中,lchild域指標和最後一個結點的rchild域指標均指向頭結點(圖中第三和第四步)。這樣的好處是:我們既可以從第一個結點起順後繼進行遍歷,也可以從最後一個結點起順前驅進行遍歷。

 中序遍歷二叉線索樹的非遞迴函式實現:

//t指向頭結點,頭結點左鏈lchild指向根結點,頭結點右鏈rchild指向中序遍歷的最後一個結點。
int InOrderThraverse_Thr(BiTree t)
{
    BiTree 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)程式碼中,p = t->lchild;意思就是上圖中的第一步,讓p指向根結點開始遍歷;     (2)while(p != t)其實意思就是迴圈直到圖中的第四步出現,此時意味著p指向了頭結點,於是與t相等(t是指向頭結點的指標),結束迴圈,否則一直迴圈下去進行遍歷操作;     (3)while(p-ltag == Link)這個迴圈,就是由A->B->D->H,此時H結點的ltag不是link(就是不等於0),所以結束此迴圈;     (4)然後就是列印H;     (5)while(p->rtag == Thread && p->rchild != t),由於結點H的rtag = Thread(就是等於1),且不是指向頭結點。因此列印H的後繼D,之後因為D的rtag是Link,因此退出迴圈;     (6)p=p->rchild;意味著p指向了結點D的右孩子I;     (7).....,就這樣不斷的迴圈遍歷,直到打印出HDIBJEAFCG,結束遍歷操作。

    從這段程式碼可以看出,它等於是一個連結串列的掃描,所以時間複雜度為O(n)。     由於充分利用了空指標域的空間(等於節省了空間),又保證了建立時的一次遍歷就可以終生受用後繼的資訊(意味著節省了時間)。所以在實際問題中,如果所用的二叉樹需要經過遍歷或查詢結點時需要某種遍歷序列中的前驅和後繼,那麼採用線索二叉連結串列的儲存結構就是非常不錯的選擇。

6.樹與森林

<1>樹的定義

  • 有且僅有一個特點的稱為根的節點
  • 當n>1時,其餘節點可分為m(m>0)個互不相干的有限交集,每個交集稱為根的子樹。

<2>森林的定義

  • m個互不相交的森林樹的集合,子樹的集合稱為子樹的森林。

<3>樹的儲存結構

a.雙親表示法

  • 在樹中,除了根節點沒有雙親外,其他節點的雙親的唯一確定的。

//雙親儲存的結構型別定義
typedef struct PTNode{
  TElemType data; //資料域
  int parent;     //雙親的位置,根節點的雙親為-1
}PTNode;        //雙親的節點型別
typedef struct {
  PTNode *nodes;  //初始化分配的結點陣列
  int r,nodeNum; //根的位置和結點數
}PTree;         //樹的雙親儲存結構型別

優點: 可用parent直接找到雙親,並且很容易找到祖先。

缺點:需要查詢節點的孩子及其子孫時要遍歷整個結構。

b.雙親孩子表示法

  • 是對雙親表示法的擴充套件,為各節點構造孩子單鏈表,以便於訪問結點的孩子及其子孫,在結點的陣列元素中增加firstchild域作為結點的孩子連結串列頭的頭指標。

//雙親孩子儲存結構的型別定義
typedef struct ChildNode{
  int childIndex;       //孩子在結點陣列的位置
  struct ChildNode *nextChild;  //下一個孩子
}ChildNode;         //孩子連結串列中的節點型別
​
typdef  struct{
  TElemType data;       //元素值
  int parent;           //雙親的位置
  struct ChildNode *firstChild; //孩子連結串列頭指標
}PCTreeNode;            //雙親節點的節點型別
​
typedef struct{
  PCTreeNode *nodes;    //節點陣列
  int nodeNum,r;    //結點元素的個數,根位置
}PCTree;            //樹的雙親孩子儲存結構型別

c.孩子兄弟表示法

  • 在樹中,結點的最左孩子(第一個孩子)和右兄弟如果存在則都是唯一的。採取二叉鏈式儲存結構,每個結點包含三個域,元素值data,最左邊孩子指標firstchild和右兄弟指標nextsibling。

//孩子兄弟連結串列的型別定義
typedef struct CSTNode{
    TElemType data;         //資料域
    struct CSTNode *firstChild,*nextSibling; //最左孩子指標,右兄弟指標
}CSTnode,*CSTree,*CSForest;     //孩子兄弟連結串列

d.孩子兄弟表示法的樹的介面

Status InitTree(CSTree &T); //構造空樹
CSTree MakeTree(TElemType e,int n....)  //建立根結點為e和n顆子樹的樹
Status DestroyTree(CSTree &T)  //銷燬樹
int TreeDepth(CSTree T)  //返回樹的深度
CSNode *Search(CSTree T,TElemType e); //查詢樹T中的節點e並返回其指標
Status InesertChild(CSTree &T,int i,CSTree c) //插入c為T的第i顆子樹,c非空並且與T不相交
Status DeleteChild(CSTree &T,int i) //刪除第i棵子樹

建立樹

#include<stdarg.h>// 標準標頭檔案,提供巨集va_start、va_arg和va_end,
​
CSTree MakeTree(TElemType e,int n....){
  int i;
  CSTree t,p,pi;
  va_list argptr; //存放變長引數表資訊的陣列
  t=(CSTree)malloc(sizeof(CSTNode));
  if(t=NULL) return NULL;
  t->data=e;    //根結點的值為e;
  t->firstChild=t->nextSibling=NULL;
  if(n<=0) return t;  //若無子樹,則返回根結點
  va_start(argptr,n) //令argptr 指向n後的第一個實參
  p=va_arg(argptr,CSTree); //取第一棵子樹的實參轉化為CSTree型別
  t->firstChild=p;
  pi=p;
  for(i=1;i<n;i++){
    p=va_arg(argptr,CSTree); //取下一顆子樹的實參並轉換為CSTree型別
    pi->nextSibling=p;
    pi=p;
  }
  va_end(argptr);
  return t;
}
​
//可變引數的使用
使用可變引數應該有以下步驟(要加入<stdarg.h>): 
​
1)首先在函式裡定義一個va_list型的變數,這裡是argptr,這個變 量是指向引數的指標. 
​
2)然後用va_start巨集初始化變數argptr,這個巨集的第二個引數是第一個可變引數的前一個引數,是一個固定的引數. 
​
3)然後用va_arg返回可變的引數,並賦值給變數p(CSTree型別). va_arg的第二個 引數是你要返回的引數的型別,這裡是CSTree型別.然後你就可以在函式裡使用第二個引數了.如果函式有多個可變引數的,依次呼叫va_arg獲取各個引數. 
​
4)最後用va_end巨集結束可變引數的獲取.

插入第i個子樹

Status InesertChild(CSTree &T,int i,CSTree c){
  int j;
  CSTree p;
  if(NULL==T||i<1) return ERROR;
  if(i==1){ //c為第一棵插入子樹
    c->nextSibling=T->firstChild;
    T->firstChild=c; //T成為T的第一棵子樹
  }else{
    p=T->firstChild;
    for(j=2;p!=NULL&&j<i;j++){
      p=p->nextSibling; //尋找插入位置
    }
    if(j==i){
      c->nextSibling = p->nextSibling;
      p->nextSibling = c;
    }else return ERROR;
  }
  return OK;
}

求樹的深度

int TreeDepth(CSTree T) {  // 求樹T的深度  
    int dep1, dep2, dep;
    if(NULL==T) dep = 0; // 樹為空,深度則為0
    else {   
        dep1 = TreeDepth(T->firstChild);    // 求T的子樹森林的深度
        dep2 = TreeDepth(T->nextSibling);   // 求除T所在樹以外的其餘樹的深度
        dep = dep1+1>dep2 ? dep1+1 : dep2;  // 樹的深度 
     }
     return dep;
} 

查詢樹

CSTreeNode* Search(CSTree T, TElemType e) {  
    // 查詢樹T中的結點e並返回其指標
    CSTreeNode* result = NULL;       
    if(NULL==T) return NULL;  // 樹為空,返回NULL 
    if(T->data==e) return T;  // 找到結點,返回其指標
    if((result = Search(T->firstChild, e))!=NULL) // 在T的子樹森林查詢
        return result; 
    return Search(T->nextSibling, e); 
        // 在除T所在樹以外的其餘樹構成的森林查詢
}
​

<4>樹、森林與二叉樹的轉換(重點)

7.哈夫曼樹及其應用

哈夫曼樹這裡涉及到一些關於其他樹的知識,比如二叉搜尋樹,二叉平衡樹,堆(最好這幾個知識都要學會,因為非常重要)

本文對這些知識不做詳解,詳細的可以參考我的這幾篇部落格

程式碼實現:

這裡其實就用了堆的思想!!!

這個建立的過程最主要的就是每次選兩個最小的,這裡其實就是堆的思想,你把結點的權值構造出最小堆,每次取兩個最小堆,並在一起,形成的新堆插進去