1. 程式人生 > >二叉樹的建立以及三種遍歷操作

二叉樹的建立以及三種遍歷操作

      由二叉樹結點的性質可以確定的是,二叉樹結構相比普通的連結串列結點而複雜,需要通過其左/右指標訪問其左/右子樹結點。而在熟悉了二叉樹的結構後,需要注意的是二叉樹的建立以及遍歷操作。而建立與遍歷兩種操作,需要利用的是遞迴的思想,即保持每一個子集函式操作與其父函式相同。

    首先明確三個概念,前序,中序,後序。這三種概念主要是訪問結點及其子樹的方式區別,在二叉樹的構造以及遍歷操作之中均有應用。

前序:又稱先序,輸出 /輸入順序是先輸出根結點的資料,再訪問該結點的左子樹以及右子樹。

中序:輸出 /輸入順序是左子樹->根結點->右子樹

後序:輸出 /輸入順序是左子樹->右子樹->根結點

1. 二叉樹的構造操作

    二叉樹的構造操作應用主要有兩種,其一是通過讀取資料,以空格表示資料域為空的二叉結點(空樹),在確定是哪一種順序讀入的情況下,對於二叉樹進行構造。其二是根據任意兩種順序的輸出情況確定二叉樹的構造,進而可以進行第三種順序的輸出。

    先開始二叉樹構造的第一種操作,即通過讀取字元進行構造。通過讀取字元進行構造的基本思路如下:1)新建二叉樹結點,命名為結點t  2)進行條件判斷,當某個字元是空字元時,使得結點為NULL   3)按照讀入的順序進行判斷,這裡假設先序讀入(即輸入字元時結點對應的順序是根結點->左子樹->右子樹)  4)若讀入是字元不為空字元時,新建結點,將結點的資料域賦值為字元,此時完成的步驟相當於輸入根結點的字元   5)按先序的概念,開始輸入左子樹對應的字元,即將該結點的左結點作為左子樹的根結點,繼續進行構造二叉樹操作,在每一棵左子樹內進行的也是先序輸入   6)輸入右子樹對應的字元,將結點的右子樹結點作為其右子樹的根結點   具體程式碼如下:

BiTree* Creat(BiTree *t)
{
    char a;
    cin>>a;
    if(a=='#')
    {
        (*t)=NULL;
    }
    else
    {
        if(!((*t)=(BiTree)malloc(sizeof(BiTnode))))
        {
            exit(-1);
        }
        (*t)->data=a;
        Creat(&((*t)->left));
        Creat(&((*t)->right));
    }
    return t;
}

在主程式中的使用方式如下:

int main()
{
    BiTree root=NULL;
    Creat(&root);
    return 0;
}

繼續二叉樹的第二種構造方式:即通過兩種順序的讀入來確定二叉樹的構成,這裡以前序和中序順序來確定二叉樹結構為例,主要使用的思想便是遞迴,即在函式中反覆地呼叫自身函式達到最終的目的。

首先來看案例:

   假設有一棵二叉樹t,其前序序列為1,3,4,5,2,6,其中序序列為4,3,5,1,6,2,求二叉樹的結構。

   首先觀察兩個序列的結構,如果沒有使用程式語言,則完成的基本思路如下  1)由先序序列入手,第一個結點為1,也就是二叉樹的根結點,再進入中序序列進行比較,得出此處二叉樹的在根結點的左側結點為4,3,5,右側結點為6,2。可以較容易地判斷出,結點435構成二叉樹的左子樹,而62構成二叉樹的右子樹。 2)由遞迴的思路,先進行左子樹的操作,左子樹的先序序列為345,中序序列為435,則可以判斷出,3是該左子樹根結點的左子樹,5是該左子樹根結點的右子樹   3)再度進行右子樹的操作,得出2為根結點,6為右子樹根結點2的左子樹,根結點2的右子樹為NULL  3)繼續進行最後一步驟,進行條件的判斷,若某一棵二叉樹的前序序列與其中序序列判斷得左右子樹均為空時,則一定是僅僅只剩下一個結點的情形,而該結點便作為二叉樹的葉子結點。

   按照遞迴的流程會有如下的示意圖:

根據示意圖得出的遞迴程式碼基本思路如下:1)將先序序列與中序序列按照字串陣列的形式儲存,記作pre[]與mid[]並且用指標來指出字串陣列的首位     2)建立一個根結點為root,將root的資料域賦值為先序序列的首位(即為一整棵樹的根結點)   3)利用for迴圈,按照示意圖的思路,將中序序列中所有在根結點對應字元左側的字元賦值給根結點左子樹的中序序列lmid[],將所有在根結點對應字元右側的字元賦值給根結點右子樹的中序序列rmid[],並同時得出左子樹的長度n1,右子樹的長度n2   4)先序序列首字元往後的n1位字元賦值給結點root左子樹的先序序列lpre[],剩餘的位數賦值給root右子樹的先序序列rpre[]    5)將root的左子樹結點賦值作為新子樹的根結點,右子樹結點也進行一樣的操作。採用遞迴的思路,而此時左子樹的先序序列為lpre[],中序序列為lmid[],序列的長度為n1.右子樹的先序序列為rpre[],中序序列為rmid[],序列的長度為n2.    程式碼如下:

BiTnode* Creat(char *pre,char *mid,int n)
{
    BiTnode*root=NULL;
    int i;
    int n1=0;
    int n2=0;
    int m1=0;
    int m2=0;
    char lpre[max],rpre[max];
    char lmid[max],rmid[max];
    if(n==0)
    {
        return NULL;
    }
    root =(BiTnode*)malloc(sizeof(BiTnode));
    if(root==NULL)
    {
        return NULL;
    }
    memset(root,0,sizeof(BiTnode));
    root->data=pre[0];
    for(i=0;i<n;i++)
    {
        if((i<=n1)&& (mid[i]!=pre[0]))
        {
            lmid[n1++]=mid[i];
        }
        else if(mid[i]!=pre[0])
        {
            rmid[n2++]=mid[i];
        }
    }
    for(int i=1;i<n;i++)
    {
        if(i<n1+1)
        {
            lpre[m1++]=pre[i];
        }
        else
        {
            rpre[m2++]=pre[i];
        }
    }
    root->left=Creat(lpre,lmid,n1);
    root->right=Creat(rpre,rmid,n2);
    return root;
}

在主程式中的使用方式如下:

int main()
{
    char pre[max];
    char mid[max];
    int n=0;
    char ch;
    BiTnode *root =NULL;
    while((ch = getchar())&&ch!='\n')
    {
        pre[n++]=ch;
    }
	n=0;
    while((ch=getchar())&&ch!='\n')
    {
        mid[n++]=ch;
    }
    root=Creat(pre,mid,n);
    return 0;
}

2.二叉樹的遍歷操作

(1)先序遞迴遍歷

按照先序的概念,在構造完成二叉樹後,先輸出其根結點的資料域,再按照左子樹,右子樹的順序來進行。按照遞迴的思路而言,先行設定先序輸出的函式名為preorder(),根結點輸出後,將二叉樹根結點的左孩子結點定義為左子樹的根結點,再將左子樹內部進行先序遍歷。在左子樹內部為空後,返回至遍歷右子樹的函式,即將根結點的右孩子結點定義為右子樹的根結點。具體程式碼如下:

void preorder(BiTnode* root)
{
    if(root!=NULL)
    {
        cout<<root->data;
        preorder(root->left);
        preorder(root->right);
    }
}

(2)中序遞迴遍歷

按照中序的概念以及先序遍歷的思路,先中序輸出根結點的左子樹,再輸出其根結點的資料,最後才是其右子樹,程式碼如下:

void midorder(BiTnode* root)
{
    if(root!=NULL)
    {
        midorder(root->left);
        cout<<root->data;
        midorder(root->right);
    }
}

(3)後序遞迴遍歷

與先序遍歷相反,先後序遍歷其左子樹,再遍歷右子樹,最後輸出其根結點,程式碼如下:

void lastorder(BiTnode*root)
{
    if(root!=NULL)
    {
        lastorder(root->left);
        lastorder(root->right);
        cout<<root->data;
    }
}

(4)後序非遞迴遍歷

不同於二叉樹的遞迴遍歷演算法,非遞迴演算法要求忠實地記錄下每一個經過的結點,之後繼續按照一定的輸出順序將它們重新輸出。而如何輸出則是最大的難點,按照二叉樹構造的學習思路,先手動建立一棵二叉樹再將其改寫成程式語言,二叉樹示意圖如下:

     按照樹的結構,不難得出其後序遍歷輸出的順序為DEBFCA。根據記錄結點的要求,先行假設有兩個BitNode型別的指標p與q,p用於指示當前的結點,而與之對應的,q指示的是前驅結點。按後序遍歷的要求,首先應該輸出的是D的值,顯而易見地,p指標由根結點出發,只能直接地一路訪問左孩子結點到達結點D,而同時地q指標指向的結點為結點B.在此之後,指標p應該返回的是指標q所指的結點,再訪問至結點E並將其輸出。之後的一步,指標q應退回至結點A,指標p則退回至q原有指示的結點。

     由這樣一種,前經歷後輸出的過程,有沒有什麼相似的資料結構可以用來表達呢?按照目前已有的知識,只能是棧這種結構,將先經歷的結點存放入棧內,再將某些滿足條件的點一步步輸出。將各個點壓入棧以及輸出棧順序的示意圖如下:

  由此可見,壓棧的順序為由根結點開始,由一個指標p指向根結點。先將根結點以及其左孩子結點壓入,再壓入左孩子結點的左孩子結點,直到結點的左孩子結點為空為止。當結點為空時,輸出棧頂結點的資料域,同時將棧頂的結點出棧操作(指標p指示的是棧頂的結點)。結點出棧後,將指標p指向的結點定為棧頂結點下一位結點的右孩子結點(棧頂結點下一位結點是已出棧的結點的根結點,這裡用指標q指向的結點來表示)。 具體程式碼如下:

void Lastorder(BiTnode *root)
{
    BiTnode *p=root;
    BiTnode *q=NULL;
    stack<BiTnode*>s;
    while(p!=NULL||!s.empty())
    {
        while(p!=NULL)
        {
            s.push(p);
            p=p->left;
        }
        p=s.top();
        cout<<p->data;
        s.pop();
        if(!s.empty())
        {
            q=s.top();
        }
        if(p==q->left)
        {
            p=q->right;
        }
        else
        {
            p=NULL;
        }
    }
}

(5)先序非遞迴遍歷

先序非遞迴遍歷的過程相對於後序簡單,基本的處理思路便是建立一個返回型別為BiTnode指標的棧命名為s,建立棧後,將根結點root推入棧中。基本思路如下:1)新建一個返回型別為BiTnode型別的指標p,將p初始化指向根結點root  2)由於輸出的緣故,先行壓入的根結點需要被輸出,所以輸出語句在前   3)由於棧的結構,決定了先被壓入的後輸出,根據先序遍歷的特性,先輸出左孩子結點後輸出右孩子結點,所以必須先壓入根結點的右子樹後壓入左子樹  4)將問題的規模擴大,壓入了root結點為根結點的子樹後,需要壓入的是根結點的左子樹,而將指標p指向左孩子結點的方法便是訪問位於棧頂的左孩子結點作為新的根結點 ,對根結點進行同樣的操作,即先壓入右孩子後壓入左孩子    5)在左孩子結點被輸出後便可以訪問位於左孩子結點下的右孩子。     基本程式碼如下:

void Preorder(BiTnode*root)
{
    BiTnode *p=root;
    stack<BiTnode*>s;
    s.push(root);
    while(!s.empty())
    {
        p=s.top();//p為空時相當於棧空
        s.pop();
        if(p!=NULL)
        {
            cout<<p->data;
            s.push(p->right);//先壓右孩子結點,再壓左孩子結點
            s.push(p->left);
        }
    }
}

(6)中序非遞迴遍歷

基本思路與先序非遞迴遍歷相同,即建立一個元素返回型別為BiTnode的棧s,定義一個指標p用於指示當前的結點,初始化為根結點root,基本思路為先將結點以及其左孩子結點一直推入棧中,直到沒有左孩子結點為止,再將棧頂的值輸出,作為左孩子,定位至新的棧頂,將右孩子結點推入棧中。若此時右孩子為空,則繼續輸出棧頂結點的值。否則,將右孩子結點作為右子樹的根結點繼續進行將左孩子結點推入棧中的操作。 右孩子結點倘若不存在左孩子,由於推結點的過程中已經將該結點推入棧中,所以該結點仍然會被輸出。  程式碼如下:

void Midorder(BiTnode*root)
{
    BiTnode *p=root;
    stack<BiTnode*>s;
    while(p!=NULL||!s.empty())
    {
        while(p!=NULL)
        {
            s.push(p);
            p=p->left;
        }
        if(!s.empty())
        {
            p=s.top();
            cout<<p->data;
            s.pop();
            p=p->right;
        }
    }
}