1. 程式人生 > >樹:二叉樹

樹:二叉樹

level -s tle 之一 rap 尺寸 stl lag ret

之前寫了一些鏈表和排序的blog,其中有說到多鏈表,堆,其中提到了一種特殊的數據結構:樹。

人們發明樹結構,用於儲存和搜索海量的數據。

樹的種類

無序樹:樹中任意節點的子結點之間沒有順序關系,這種樹稱為無序樹,也稱為自由樹;

有序樹:樹中任意節點的子結點之間有順序關系,這種樹稱為有序樹;

二叉樹:每個節點最多含有兩個子樹的樹稱為二叉樹;

完全二叉樹:二叉樹的所有子樹要麽沒有孩子,要麽一定有左孩子。堆是一種完全二叉樹。

紅黑樹:紅黑樹是把樹中的結點定義為紅、黑兩種顏色,並通過規則確保從根結點到葉結點的最長路徑的長度不超過最短路徑的兩倍。在c++的STL中,set、multiset、map、multimap等數據結構都是基於紅黑樹實現的。

滿二叉樹:除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點(最後一層上的無子結點的結點為葉子結點)。

霍夫曼樹:帶權路徑最短的二叉樹稱為哈夫曼樹或最優二叉樹;

如果二叉樹不是平衡的,那麽它就退化為一個鏈表,而搜索時間也退化為一個線性函數。

樹的深度
定義一棵樹的根結點層次為1,其他節點的層次是其父結點層次加1。一棵樹中所有結點的層次的最大值稱為這棵樹的深度。

樹的遍歷
所謂遍歷(Traversal)是指沿著某條搜索路線,依次對樹中每個結點均做一次且僅做一次訪問。訪問結點所做的操作依賴於具體的應用問 題。 遍歷是二叉樹上最重要的運算之一,是二叉樹上進行其它運算之基礎。

遍歷命名
根據訪問結點操作發生位置命名:
① NLR:前序遍歷(PreorderTraversal亦稱(先序遍歷))
——訪問根結點的操作發生在遍歷其左右子樹之前。如10、6、4、8、14、12、16。
② LNR:中序遍歷(InorderTraversal)
——訪問根結點的操作發生在遍歷其左右子樹之中(間)。如4、6、8、10、12、14、16。
③ LRN:後序遍歷(PostorderTraversal)
——訪問根結點的操作發生在遍歷其左右子樹之後。如4、8、6、12、16、14、10。

技術分享圖片技術分享圖片?


中序遍歷的投影法
技術分享圖片技術分享圖片?

一般二叉樹的定義:

struct BiTNode{
	TElemType data;
	BiTNode *lchild,*rchild;
};
技術分享圖片


二叉樹的遍歷操作:

1.先序遍歷(DLR)
先序遍歷的遞歸過程為:若二叉樹為空,遍歷結束。否則,
(1) 訪問根結點;
(2) 先序遍歷根結點的左子樹;
(3) 先序遍歷根結點的右子樹。

先序遍歷二叉樹的遞歸算法如下:

void PreOrder(BiTree bt) /*先序遍歷二叉樹bt*/
{
      if (bt==NULL) return; /*遞歸調用的結束條件*/
      Visite(bt->data);     /*訪問結點的數據域*/
      PreOrder(bt->lchild); /*先序遞歸遍歷bt 的左子樹*/
      PreOrder(bt->rchild); /*先序遞歸遍歷bt 的右子樹*/
}
技術分享圖片


2.中序遍歷(LDR)

中序遍歷的遞歸過程為:若二叉樹為空,遍歷結束。否則,
(1)中序遍歷根結點的左子樹;
(2)訪問根結點;
(3)中序遍歷根結點的右子樹。

中序遍歷二叉樹的遞歸算法如下:

void InOrder(BiTree bt) /*中序遍歷二叉樹bt*/
{
     if (bt==NULL) return; /*遞歸調用的結束條件*/
     InOrder(bt->lchild); /*中序遞歸遍歷bt 的左子樹*/
     Visite(bt->data);     /*訪問結點的數據域*/
     InOrder(bt->rchild); /*中序遞歸遍歷bt 的右子樹*/
}
技術分享圖片


3.後序遍歷(LRD)
後序遍歷的遞歸過程為:若二叉樹為空,遍歷結束。否則,
(1)後序遍歷根結點的左子樹;
(2)後序遍歷根結點的右子樹。
(3)訪問根結點;

後序遍歷二叉樹的遞歸算法如下:

void PostOrder(BiTree bt) /*後序遍歷二叉樹bt*/
{
     if (bt==NULL) return; /*遞歸調用的結束條件*/
     PostOrder(bt->lchild); /*後序遞歸遍歷bt 的左子樹*/
     PostOrder(bt->rchild); /*後序遞歸遍歷bt 的右子樹*/
     Visite(bt->data); /*訪問結點的數據域*/
}
技術分享圖片

從表面上看,從代碼中,遍歷語句的位置可以看出是什麽遍歷。

4.層次遍歷(有些面試題會叫寬度優先遍歷)

由層次遍歷的定義可以推知,在進行層次遍歷時,對一層結點訪問完後,再按照它們的訪問次序對各個結點的左孩子和右孩子順序訪問,這樣一層一層進行,先遇到的結點先訪問,這與隊列的操作原則比較吻合。因此,在進行層次遍歷時,可設置一個隊列結構,遍歷從二叉樹的根結點開始,首先將根結點指針入隊列,然後從對頭取出一個元素,每取一個元素,執行下面兩個操作:
(1) 訪問該元素所指結點;
(2) 若該元素所指結點的左、右孩子結點非空,則將該元素所指結點的左孩子指針和右孩子指針順序入隊。
此過程不斷進行,當隊列為空時,二叉樹的層次遍歷結束。

在下面的層次遍歷算法中,二叉樹以二叉鏈表存放,一維數組Queue[MAXNODE]用以實現隊列,變量front 和rear 分別表示當前對首元素和隊尾元素在數組中的位置。

void LevelOrder(BiTree bt) /*層次遍歷二叉樹bt*/
{ 
    BiTree Queue[MAXNODE];
    int front,rear;


    if (bt==NULL) 
        return;


    front=-1;
    rear=0;
    queue[rear]=bt;


    while(front!=rear)
    {
        front++;
        Visite(queue[front]->data); /*訪問隊首結點的數據域*/


        if (queue[front]->lchild!=NULL) /*將隊首結點的左孩子結點入隊列*/
        { 
            rear++;
            queue[rear]=queue[front]->lchild;
        }


        if (queue[front]->rchild!=NULL) /*將隊首結點的右孩子結點入隊列*/
        { 
            rear++;
            queue[rear]=queue[front]->rchild;
        }
    }
}
技術分享圖片

二叉樹遍歷的非遞歸實現

從二叉樹各種遍歷來說,各種遍歷都是從根結點開始的,且在遍歷過程中經過結點的路線是一樣的,只是訪問的時機不同而已。這一路線都是從根結點開始沿左子樹深入下去,當深入到最左端,無法再深入
下去時,則返回,再逐一進入剛才深入時遇到結點的右子樹,再進行如此的深入和返回,直到最後從根結點的右子樹返回到根結點為止。先序遍歷是在深入時遇到結點就訪問,中序遍歷是在從左子樹返回時遇到結點訪問,後序遍歷是在從右子樹返回時遇到結點訪問。

這種路線可以用棧來實現。其實遞歸在本質上就是一個棧結構。

在這一過程中,返回結點的順序與深入結點的順序相反,即後深入先返回,正好符合棧結構後進先出的點。因此,可以用棧來幫助實現這一遍歷路線。其過程如下。在沿左子樹深入時,深入一個結點入棧一個結點,若為先序遍歷,則在入棧之前訪問之;當沿左分支深入不下去時,則返回,即從堆棧中彈出前面壓入的結點,若為中序遍歷,則此時訪問該結點,然後從該結點的右子樹繼續深入;若為後序遍歷,則將此結點再次入棧,然後從該結點的右子樹繼續深入,與前面類同,仍為深入一個結點入棧一個結點,深入不下去再返回,直到第二次從棧裏彈出該結點,才訪問之。

1. 先序遍歷的非遞歸實現

在下面算法中,二叉樹以二叉鏈表存放,一維數組 stack[MAXNODE] 用以實現棧,變量top 用來表示當前棧頂的位置。

void NRPreOrder(BiTree bt) /*非遞歸先序遍歷二叉樹*/
{
    BiTree stack[MAXNODE],p;
    int top;


    if (bt==NULL) 
        return;


    top=0;
    p=bt;


    while(!(p==NULL&&top==0))
    { 
        while(p!=NULL)
        { 
            Visite(p->data);   /*訪問結點的數據域*/


            if (top<MAXNODE-1) /*將當前指針p 壓棧*/
            { 
                stack[top]=p;
                top++;
            }
            else 
            { 
                printf("棧溢出");
                return;
            }


            p=p->lchild; /*指針指向p 的左孩子*/


        }


        if (top<=0) 
            return; /*棧空時結束*/
        else
        { 
            top--;
            p=stack[top]; /*從棧中彈出棧頂元素*/
            p=p->rchild; /*指針指向p 的右孩子結點*/
        }
    }
}
技術分享圖片


2. 中序遍歷的非遞歸實現
中序遍歷的非遞歸算法的實現,只需將先序遍歷的非遞歸算法中的 Visite(p->data) 移到 p=stack[top] 和 p=p->rchild 之間即可。

3. 後序遍歷的非遞歸實現
由前面的討論可知,後序遍歷與先序遍歷和中序遍歷不同,在後序遍歷過程中,結點在第一次出棧後,還需再次入棧,也就是說,結點要入兩次棧,出兩次棧,而訪問結點是在第二次出棧時訪問。因此,為了區別同一個結點指針的兩次出棧,設置一標誌flag,令:
flag = 1 -> 第一次出棧,結點不能訪問
flag = 2 -> 第二次出棧,結點可以訪問
當結點指針進、出棧時,其標誌flag 也同時進、出棧。因此,可將棧中元素的數據類型定義為指針和標誌flag 合並的結構體類型。定義如下:

typedef struct 
{
    BiTree link;
    int flag;
} stacktype;
技術分享圖片


後序遍歷二叉樹的非遞歸算法如下。在算法中,一維數組 stack[MAXNODE] 用於實現棧的結構,指針變量p 指向當前要處理的結點,整型變量top 用來表示當前棧頂的位置,整型變量 sign 為結點 p 的標誌量。

void NRPostOrder(BiTree bt) /*非遞歸後序遍歷二叉樹bt*/
{ 
    stacktype stack[MAXNODE];
    BiTree p;
    int top,sign;


    if (bt==NULL) 
        return;


    top=-1   /*棧頂位置初始化*/
    p=bt;


    while (!(p==NULL && top==-1))
    { 
        if (p!=NULL)    /*結點第一次進棧*/
        { 
            top++;
            stack[top].link=p;
            stack[top].flag=1;
            p=p->lchild;   /*找該結點的左孩子*/
        }
        else 
        { 
            p=stack[top].link;
            sign=stack[top].flag;
            top--;


            if (sign==1)   /*結點第二次進棧*/
            {
                top++;
                stack[top].link=p;
                stack[top].flag=2;   /*標記第二次出棧*/
                p=p->rchild;
            }
            else 
            { 
                Visite(p->data);   /*訪問該結點數據域值*/
                p=NULL;
            }
        }
    }
}
技術分享圖片

樹:二叉樹