樹 & 二叉樹
2018-01-04 19:13:46
一、樹
在計算機科學中,樹(英語:tree)是一種數據結構,用來模擬具有樹狀結構性質的數據集合。它是由n(n>0)個有限節點組成一個具有層次關系的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。
客觀世界中有很多具有層次關系的事物:
- 人類的社會家譜
- 社會組織結構
- 圖書管理信息
類似這種層次結構就非常適用使用樹來表示。
使用樹的結構有很多有點,比如更形象的表述了層次關系,並且可以加速查找。
- 樹的定義:
- 樹的一些術語:
比如這裏的L,M的深度就是4,因為他們在第4層。
- 樹的表示:
1)鏈表存儲
每個結點保存有一個指向其兒子結點的指針。這種方法為了程序的統一性,那麽對於n個結點的樹,就會產生3n個的指針樹,也就是3n的邊樹,事實上,一顆樹顯然只有n-1條邊,所以會有2n+1個指針為空,這就造成了空間的浪費。
2)兒子-兄弟表示法
每個結點有兩個指針,第一個指針指向他的第一個兒子,第二個指針指向他的兄弟,這樣就可以組織好一個樹,並且這種結構產生的指針為2n,那麽對於n-1條邊,總共浪費了n+1個指針空間,浪費明顯少了很多。
將這種結構旋轉45度,其實就是一種二叉樹的表示。
二、二叉樹
二叉樹(英語:Binary tree)是每個節點最多只有兩個分支(不存在分支度大於2的節點)的樹結構。通常分支被稱作“左子樹”和“右子樹”。二叉樹的分支具有左右次序,不能顛倒。
- 二叉樹的定義:
- 二叉樹的重要性質:
- 二叉樹的抽象數據類型定義
1)數組表示
對於完全二叉樹,可以很方便的放到數組中存儲,並且很容易就能得到兒子結點,得到父結點。
對於一般的二叉樹,也可以使用這種方法存儲,但是會造成空間的浪費。
2)鏈表存儲
三、二叉樹的遍歷的遞歸實現
- 先序遍歷
- 中序遍歷
- 後序遍歷
三種遞歸方式的總結:
四、二叉樹遍歷的非遞歸實現
- 中序遍歷的非遞歸實現
- 先序遍歷的非遞歸實現
第一次壓棧其實就是第一次碰到,所以直接在這裏輸出就可以實現先序遍歷。
- 後序遍歷的非遞歸實現
方法一、先序的訪問順序是root, left, right 假設將先序左右對調,則順序變成root, right, left,暫定稱之為“反序”。
後序遍歷的訪問順序為left, right,root ,剛好是“反序”結果的逆向輸出。於是方法如下:
1、反序遍歷二叉樹,具體方法為:將先序遍歷代碼中的left 和right 對調即可。
數據存在堆棧S中。
2、在先序遍歷過程中,每次Push節點後緊接著print結點。
對應的,在反序遍歷時,將print結點改為把當前結點 PUSH到堆棧Q中。
3、反序遍歷完成後,堆棧Q的壓棧順序即為反序遍歷的輸出結果。
此時再將堆棧Q中的結果pop並print,即為“反序”結果的逆向,也就是後序遍歷的結果。
缺點是堆棧Q的深度等於數的結點數,空間占用較大。
void PostOrderTraversal( BinTree BT ) { BinTree T BT; Stack S = CreatStack( MaxSize ); /*創建並初始化堆棧S*/ Stack Q = CreatStack( MaxSize ); /*創建並初始化堆棧Q,用於輸出反向*/ while( T || !IsEmpty(S) ){ while(T){ /*一直向右並將沿途結點壓入堆棧*/ Push(S,T); Push(Q,T);/*將遍歷到的結點壓棧,用於反向*/ T = T->Right; } if(!IsEmpty(S)){ T = Pop(S); /*結點彈出堆棧*/ T = T->Left; /*轉向左子樹*/ } } while( !IsEmpty(Q) ){ T = Pop(Q); printf(“%5d”, T->Data); /*(訪問)打印結點*/ } }
方法二、當然也可以使用一個棧進行實現:對於任一結點P,將其入棧,然後沿其左子樹一直往下搜索,直到搜索到沒有左孩子的結點,此時該結點出現在棧頂,但是此時不能將其出棧並訪問,因此其右孩子還為被訪問。所以接下來按照相同的規則對其右子樹進行相同的處理,當訪問完其右孩子時,該結點又出現在棧頂,此時可以將其出棧並訪問。這樣就保證了正確的訪問順序。可以看出,在這個過程中,每個結點都三次出現在棧頂,只有在第三次出現在棧頂時,才能訪問它。因此需要多設置一個變量標識。
void postOrder(BinTree *root) //非遞歸後序遍歷 { stack<BTNode*> s; BinTree *p=root; BTNode *temp; while(p!=NULL||!s.empty()) { while(p!=NULL) //沿左子樹一直往下搜索,直至出現沒有左子樹的結點 { BTNode *btn=(BTNode *)malloc(sizeof(BTNode)); btn->btnode=p; btn->isFirst=true; s.push(btn); p=p->lchild; } if(!s.empty()) { temp=s.top(); s.pop(); if(temp->isFirst==true) //表示是第二次出現在棧頂 { temp->isFirst=false; s.push(temp); p=temp->btnode->rchild; } else //第三次出現在棧頂 { cout<<temp->btnode->data<<" "; p=NULL; } } } }
方法三、前面的兩種方法都需要額外使用空間,那麽是否可以不額外使用更多的空間呢?答案是肯定的。後序遍歷的本質就是先遍歷左邊的,再遍歷右的,最後再遍歷中間。那麽我們只需要在輸出中間的數值之前先把他的左右結點壓棧就可以了,當然在將其左右子樹壓棧的時候需要進行判斷,具體的判斷方式是,定義兩個指針,一個指向當前棧頂元素,一個指向上一個出棧元素,不妨設第一個為cur,第二個為pre。能夠打印當前的結點的條件是要麽其左右子樹為空,要麽其左右子樹都打印完畢。
每次令cur等於當前棧頂的元素,但是不從棧頂彈出,此時分為三種情況:
1)如果cur的左孩子不為空,並且pre不等於cur的左孩子,也不等於其右孩子,說明cur的左孩子還沒有打印過,將之壓棧;
2)如果上面的條件不滿足,表明cur的左孩子已經打印完畢,現在考慮其右孩子。如果cur的右孩子不為空,並且pre不等於cur的右孩子,那麽說明cur的右孩子還沒有打印,將之壓棧。
3)如果上面兩個都不滿足,說明左右孩子都打印完畢,那麽將當前棧頂元素打印,並將pre置為當前打印的結點,彈出棧頂元素。
void postOrder(Node root) { if (root == null) { return; } else { Node cur = null; Node pre = null; Stack<Node> s = new Stack<>(); s.push(root); while (!s.empty()) { cur = s.peek(); if (cur.left != null && pre != cur.left && pre != cur.right) s.push(cur.left); else if (cur.right != null && pre != cur.right) { s.push(cur.right); } else { System.out.println(cur.data); pre = cur; s.pop(); } } } }
- 層次遍歷
上面談到的前序,中序,後序遍歷本質上都是一種深度優先遍歷,顯然,也可以使用廣度優先進行遍歷。也就是層次遍歷。
五、二叉樹遍歷的幾個應用
1、求葉結點
很簡單,在遍歷輸出的加上判斷就好。
void PreOrderPrintLeaves( BinTree BT ) { if( BT ) { if ( !BT-Left && !BT->Right ) printf(“%d”, BT->Data ); PreOrderPrintLeaves ( BT->Left ); PreOrderPrintLeaves ( BT->Right ); } }
2、求樹高度
int PostOrderGetHeight( BinTree BT ) { int HL, HR, MaxH; if( BT ) { HL = PostOrderGetHeight(BT->Left); /* 求左子樹的深度*/ HR = PostOrderGetHeight(BT->Right); /* 求右子樹的深度*/ MaxH = (HL > HR )? HL : HR; /* 取左右子樹較大的深度*/ return ( MaxH + 1 ); /* 返回樹的深度*/ } else return 0; /* 空樹深度為0 */ }
3、二元運算的表達式樹
表達式樹:葉結點表示運算數,中間結點是運算符。這樣通過三種遍歷就會得到三種表達式,這其中中綴表達式可能會受運算優先級影響導致不準確(可通過添加括號的方式予以解決)。
4、兩種遍歷來構建二叉樹
結論是:必須要有中序遍歷的結果,才能唯一確定。
因為前序是root,left,right;後序是left,right,root。根是容易確定的,一個在最前面,一個在最後面,但是左右子樹可能會發生混淆。
舉例使用前序,中序構建二叉樹:
樹 & 二叉樹