二叉樹遍歷的概念:
二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問二叉樹中的所有結點,使得每個結點被訪問一次且僅被訪問一次。
二叉樹的深度優先遍歷可細分為前序遍歷、中序遍歷、后序遍歷,這三種遍歷可以用遞歸實現
前序遍歷:根節點->左子樹->右子樹(根->左->右)
中序遍歷:左子樹->根節點->右子樹(左->根->右)
后序遍歷:左子樹->右子樹->根節點(左->右->根)
前序遍歷
1)依據上文提到的遍歷思路:根結點 ---> 左子樹 ---> 右子樹,非常easy寫出遞歸版本號:
/* 以遞歸方式 前序遍歷二叉樹 */
void PreOrderTraverse(BiTree t, int level) { if (t) {
printf("data = %c level = %d\n ", t->data, level);
PreOrderTraverse(t->lchild, level + 1);
PreOrderTraverse(t->rchild, level + 1);
}
2)如今討論非遞歸的版本號:
依據前序遍歷的順序,優先訪問根結點。然后在訪問左子樹和右子樹。所以。對於隨意結點node。第一部分即直接訪問之,之后在推斷左子樹是否為空,不為空時即反復上面的步驟,直到其為空。若為空。則須要訪問右子樹。注意。在訪問過左孩子之后。須要反過來訪問其右孩子。所以,須要棧這樣的數據結構的支持。對於隨意一個結點node,詳細過程例如以下:
a)訪問之,並把結點node入棧。當前結點置為左孩子;
b)推斷結點node是否為空,若為空。則取出棧頂結點並出棧,將右孩子置為當前結點;否則反復a)步直到當前結點為空或者棧為空(能夠發現棧中的結點就是為了訪問右孩子才存儲的)
代碼例如以下:
public void preOrderTraverse2(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); TreeNode pNode = root; while (pNode != null || !stack.isEmpty()) { if (pNode != null) { System.out.print(pNode.val+" "); stack.push(pNode); pNode = pNode.left; } else { //pNode == null && !stack.isEmpty()
TreeNode node = stack.top();
stack.pop(); pNode = node.right; } } }
中序遍歷
1)依據上文提到的遍歷思路:左子樹 ---> 根結點 ---> 右子樹,非常easy寫出遞歸版本號:
/* 以遞歸方式 中序遍歷二叉樹 */
void PreOrderTraverse(BiTree t, int level) { if (t == NULL) {
PreOrderTraverse(t->lchild, level + 1);
printf("data = %c level = %d\n ", t->data, level);
PreOrderTraverse(t->rchild, level + 1);
}
}
2)非遞歸實現,有了上面前序的解釋,中序也就比較簡單了。同樣的道理。僅僅只是訪問的順序移到出棧時。代碼例如以下:
public void inOrderTraverse2(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); TreeNode pNode = root; while (pNode != null || !stack.isEmpty()) { if (pNode != null) { stack.push(pNode); pNode = pNode.left; } else { //pNode == null && !stack.isEmpty()
TreeNode node = stack.top();
stack.pop(); System.out.print(node.val+" "); pNode = node.right; } } }
后序遍歷
1)依據上文提到的遍歷思路:左子樹 ---> 右子樹 ---> 根結點。非常easy寫出遞歸版本號:
/* 以遞歸方式 后序遍歷二叉樹 */
void PreOrderTraverse(BiTree t, int level) { if (t) { PreOrderTraverse(t->lchild, level + 1); PreOrderTraverse(t->rchild, level + 1); printf("data = %c level = %d\n ", t->data, level);
} }
2)后序遍歷的非遞歸實現是三種遍歷方式中最難的一種。由於在后序遍歷中,要保證左孩子和右孩子都已被訪問而且左孩子在右孩子前訪問才干訪問根結點,這就為流程的控制帶來了難題。以下介紹兩種思路。
第一種思路:對於任一結點P,將其入棧,然后沿其左子樹一直往下搜索。直到搜索到沒有左孩子的結點,此時該結點出在棧頂,可是此時不能將其出棧並訪問,因此其右孩子還未被訪問。
所以接下來依照同樣的規則對其右子樹進行同樣的處理,當訪問完其右孩子時。該結點又出在棧頂,此時能夠將其出棧並訪問。這樣就保證了正確的訪問順序。能夠看出,在這個過程中,每一個結點都兩次出在棧頂,僅僅有在第二次出如今棧頂時,才干訪問它。因此須要多設置一個變量標識該結點是否是第一次出如今棧頂。
void postOrder2(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; } } } }
另外一種思路:要保證根結點在左孩子和右孩子訪問之后才干訪問,因此對於任一結點P。先將其入棧。假設P不存在左孩子和右孩子。則能夠直接訪問它;或者P存在左孩子或者右孩子。可是其左孩子和右孩子都已被訪問過了。則相同能夠直接訪問該結點。若非上述兩種情況。則將P的右孩子和左孩子依次入棧。這樣就保證了每次取棧頂元素的時候,左孩子在右孩子前面被訪問。左孩子和右孩子都在根結點前面被訪問。
void postOrder3(BinTree *root) //非遞歸后序遍歷
{ stack<BinTree*> s; BinTree *cur; //當前結點
BinTree *pre=NULL; //前一次訪問的結點
s.push(root); while(!s.empty()) { cur=s.top(); if((cur->lchild==NULL&&cur->rchild==NULL)|| (pre!=NULL&&(pre==cur->lchild||pre==cur->rchild))) { cout<<cur->data<<" "; //假設當前結點沒有孩子結點或者孩子節點都已被訪問過
s.pop(); pre=cur; } else { if(cur->rchild!=NULL) s.push(cur->rchild); if(cur->lchild!=NULL) s.push(cur->lchild); } } }
三種遞歸遍歷方式對應的代碼幾乎相同,只是一條語句的位置發生了變化
printf("data = %c level = %d\n ", t->data, level);
只看文字和代碼來理解遍歷的過程是比較困難的,建議讀者親自去遍歷,為了理清遍歷的過程下面上題
前序遍歷
前序的遍歷的特點,根節點->左子樹->右子樹,注意看前序的遍歷的代碼printf語句是放在兩條遞歸語句之前的,所以先訪問根節點G,打印G,然后訪問左子樹D,此時左子樹D又作為根節點,打印D,再訪問D的左子樹A
A又作為根節點,打印A,A沒有左子樹或者右子樹,函數調用結束返回到D節點(此時已經打印出來的有:GDA)D節點的左子樹已經遞歸完成,現在遞歸訪問右子樹F,F作為根節點,打印F,F有左子樹訪問左子樹E,E作為
根節點,打印E,(此時已經打印出來的有:GDAFE),E沒有左子樹和右子樹,函數遞歸結束返回F節點,F的左子樹已經遞歸完成了,但沒有右子樹,所以函數遞歸結束,返回D節點,D節點的左子樹和右子樹遞歸全部完成,
函數遞歸結束返回G節點,訪問G節點的右子樹M,M作為根節點,打印M,訪問M的左子樹H,H作為根節點,打印H,(此時已經打印出來的有:GDAFEMH)H沒有左子樹和右子樹,函數遞歸結束,返回M節點,M節點的左子樹已經
遞歸完成,訪問右子樹Z,Z作為根節點,打印Z,Z沒有左子樹和右子樹,函數遞歸結束,返回M節點,M節點的左子樹右子樹遞歸全部完成,函數遞歸結束,返回G節點,G節點的左右子樹遞歸全部完成,整個二叉樹的遍歷就結束了
(MGJ,終於打完了··)
前序遍歷結果:GDAFEMHZ
總結一下前序遍歷步驟
第一步:打印該節點(再三考慮還是把訪問根節點這句話去掉了)
第二步:訪問左子樹,返回到第一步(注意:返回到第一步的意思是將根節點的左子樹作為新的根節點,就好比圖中D是G的左子樹但是D也是A節點和F節點的根節點)
第三步:訪問右子樹,返回到第一步
第四步:結束遞歸,返回到上一個節點
前序遍歷的另一種表述:
(1)訪問根節點
(2)前序遍歷左子樹
(3)前序遍歷右子樹
(在完成第2,3步的時候,也是要按照前序遍歷二叉樹的規則完成)
前序遍歷結果:GDAFEMHZ
中序遍歷
中序遍歷步驟
第一步:訪問該節點左子樹
第二步:若該節點有左子樹,則返回第一步,否則打印該節點
第三步:若該節點有右子樹,則返回第一步,否則結束遞歸並返回上一節點
(按我自己理解的中序就是:先左到底,左到不能在左了就停下來並打印該節點,然后返回到該節點的上一節點,並打印該節點,然后再訪問該節點的右子樹,再左到不能再左了就停下來)
中序遍歷的另一種表述:
(1)中序遍歷左子樹
(2)訪問根節點
(3)中序遍歷右子樹
(在完成第1,3步的時候,要按照中序遍歷的規則來完成)
所以該圖的中序遍歷為:ADEFGHMZ
后序遍歷步驟
第一步:訪問左子樹
第二步:若該節點有左子樹,返回第一步
第三步:若該節點有右子樹,返回第一步,否則打印該節點並返回上一節點
后序遍歷的另一種表述:
(1)后序遍歷左子樹
(2)后序遍歷右子樹
(3)訪問根節點
(在完成1,2步的時候,依然要按照后序遍歷的規則來完成)
該圖的后序遍歷為:AEFDHZMG
(讀者如果在紙上遍歷二叉樹的時候,仍然容易將順序搞錯建議再回去看一下三種不同遍歷對應的代碼)
進入正題,已知兩種遍歷結果求另一種遍歷結果(其實就是重構二叉樹)
第一種:已知前序遍歷、中序遍歷求后序遍歷
前序遍歷:ABCDEF
中序遍歷:CBDAEF
在進行分析前讀者需要知道不同遍歷結果的特點
1、前序遍歷的第一元素是整個二叉樹的根節點
2、中序遍歷中根節點的左邊的元素是左子樹,根節點右邊的元素是右子樹
3、后序遍歷的最后一個元素是整個二叉樹的根節點
(如果讀者不明白上述三個特點,建議再回去看一下三種不同遍歷對應的代碼,並在紙上寫出一個簡單的二叉樹的三種不同的遍歷結果,以加深對三種不同遍歷的理解)
用上面這些特點來分析遍歷結果,
第一步先看前序遍歷A肯定是根節點
第二步:確認了根節點,再來看中序遍歷,中序遍歷中根節點A的左邊是CBD,右邊是EF,所有可以確定二叉樹既有左子樹又有右子樹
第三步:先來分析左子樹CBD,那么CBD誰來做A的左子樹呢?這個時候不能直接用中序遍歷的特點(左->根->右)得出左子樹應該是這個樣子
因為有兩種情況都滿足中序遍歷為CBD
直接根據中序遍歷來直接得出左子樹的結構,這個時候就要返回到前序遍歷中去
觀察前序遍歷ABCDEF,左子樹CBD在前序遍歷中的順序是BCD,意味着B是左子樹的根節點(這么說可能不太好理解,換個說法就是B是A的左子樹),得出這個結果是因為如果一個二叉樹的根節點有左子樹,那么
這個左子樹一定在前序遍歷中一定緊跟着根節點(這個是用前序遍歷的特點(根->左->右)得出的),到這里就可以確認B是左子樹的根節點
第四步:再觀察中序遍歷CBDAEF,B元素左邊是C右邊是D,說明B節點既有左子樹又有右子樹,左右子樹只有一個元素就可以直接確定了,不用再返回去觀察前序遍歷
第五步:到這里左子樹的重建就已經完成了,現在重建右子樹,因為重建右子樹的過程和左子樹的過程一模一樣,步驟就不像上面寫這么細了((┬_┬)),觀察中序遍歷右子樹為EF,再觀察前序遍歷ABCDEF中右子樹
的順序為EF,所以E為A的右子樹,再觀察中序便利中E只有右邊有F,所有F為E的右子樹,最后得到的二叉樹是這個樣子的
所有求得的后序遍歷為:CDBFEA
總結一下上述步驟: 先觀察前序遍歷找到根節點->觀察中序遍歷將根節點左邊歸為左子樹元素,右邊歸為右子樹元素(可能會出現只有左子樹或者右子樹的情況)->觀察前序遍歷中左\右子樹幾個元素的順序,最靠前的為左\右子樹的根節點->重復前面的步驟
第二種:已知中序遍歷、后序遍歷求前序遍歷(題還是上面這道)
中序遍歷:CBDAEF
后序遍歷為:CDBFEA
仍然是根據不同遍歷方式結果的特點來重構二叉樹,過程很相似這里就不詳細說了,后序遍歷的最后一個元素A是根節點,在中序遍歷中以根節點A作為分界將元素分為左子樹(CBD)和右子樹(EF),再觀察后序遍歷中左子樹的順序是CDB
,可以判斷出B是左子樹的根節點(因為后序遍歷是:左->右->根),再觀察中序遍歷,B元素左邊是C右邊是D,說明B節點既有左子樹又有右子樹,左右子樹只有一個元素就可以直接確定了,不用再返回去觀察后序遍歷,左子樹重建完成,
現在來看右子樹,右子樹有兩個元素EF,觀察后序遍歷E在F的后面,所以E是右子樹的根節點,然后看中序遍歷中E只有右邊一個F元素了,即F是E的右子樹,此時整個二叉樹重構完成
總結一下上述步驟:先觀察后序遍歷找到根節點->觀察中序遍歷將根節點左邊歸為左子樹元素,右邊歸為右子樹元素(可能會出現只有左子樹或者右子樹的情況)->觀察后序遍歷中左\右子樹幾個元素的順序,最靠后的為左\右子樹的根節點->重復前面的步驟
注意:已知前序遍歷、后序遍歷無法求出中序遍歷(因為由前序后序重構出來的二叉樹不止一種)
舉個栗子
左圖這兩種二叉樹前序(BEFA)和后序(AFEB)一樣,但對應的中序遍歷結果不一樣(左邊的是AFEB右邊的是BEFA),所以僅靠前序后序是
重構出唯一的二叉樹
層序遍歷
層序遍歷。聽名字也知道是按層遍歷。我們知道一個節點有左右節點
。而每一層一層的遍歷都和左右節點有着很大的關系。也就是我們選用的數據結構不能一股腦的往一個方向鑽,而左右應該均衡考慮
。這樣我們就選用隊列來實現。
- 對於隊列,現進先出。從根節點的節點push到隊列,那么隊列中先出來的順序是第二層的左右(假設有)。
第二層
每個執行的時候添加到隊列
,那么添加的所有節點都在第二層后面
。 - 同理,假設開始
pop遍歷第n層
的節點,每個節點會push左右兩個節點進去
。但是隊列先進先出。它會放到隊尾(下一層
)。直到第n層的最后一個pop出來
,第n+1層的還在隊列中整齊排着。這就達到一個層序
的效果。
實現的代碼也很容易理解:
public void LevelOrder(node t) {//層序遍歷
Queue<node> q1 = new ArrayDeque<node>(); if (t == null) return; if (t != null) { q1.add(t); } while (!q1.isEmpty()) { node t1 = q1.poll(); if (t1.left != null) q1.add(t1.left); if (t1.right != null) q1.add(t1.right); System.out.print(t1.value + " "); } System.out.println(); }