深度閱讀:二叉樹操做詳解

【導讀】:樹是數據結構中的重中之重,尤爲以各種二叉樹爲學習的難點。在面試環節中,二叉樹也是必考的模塊。本文主要講二叉樹操做的相關知識,梳理面試常考的內容。請你們跟隨小編一塊兒來複習吧。node

本篇針對面試中常見的二叉樹操做做個總結:web

  1. 前序遍歷,中序遍歷,後序遍歷;面試

  2. 層次遍歷;算法

  3. 求樹的結點數;數組

  4. 求樹的葉子數;數據結構

  5. 求樹的深度;編輯器

  6. 求二叉樹第k層的結點個數;工具

  7. 判斷兩棵二叉樹是否結構相同;學習

  8. 求二叉樹的鏡像;開發工具

  9. 求兩個結點的最低公共祖先結點;

  10. 求任意兩結點距離;

  11. 找出二叉樹中某個結點的全部祖先結點;

  12. 不使用遞歸和棧遍歷二叉樹;

  13. 二叉樹前序中序推後序;

  14. 判斷二叉樹是否是徹底二叉樹;

  15. 判斷是不是二叉查找樹的後序遍歷結果;

  16. 給定一個二叉查找樹中的結點,找出在中序遍歷下它的後繼和前驅;

  17. 二分查找樹轉化爲排序的循環雙鏈表;

  18. 有序鏈表轉化爲平衡的二分查找樹;

  19. 判斷是不是二叉查找樹。

 

小編推薦一個學C語言/C++的學習裙【  712,284,705】,不管你是大牛仍是小白,是想轉行仍是想入行均可以來了解一塊兒進步一塊兒學習!裙內有開發工具,不少乾貨和技術資料分享!

 

1 前序遍歷,中序遍歷,後序遍歷;

1.1 前序遍歷

 

 

對於當前結點,先輸出該結點,而後輸出它的左孩子,最後輸出它的右孩子。以上圖爲例,遞歸的過程以下:

  1. 輸出 1,接着左孩子;

  2. 輸出 2,接着左孩子;

  3. 輸出 4,左孩子爲空,再接着右孩子;

  4. 輸出 6,左孩子爲空,再接着右孩子;

  5. 輸出 7,左右孩子都爲空,此時 2 的左子樹所有輸出,2 的右子樹爲空,此時 1 的左子樹所有輸出,接着 1 的右子樹;

  6. 輸出 3,接着左孩子;

  7. 輸出 5,左右孩子爲空,此時 3 的左子樹所有輸出,3 的右子樹爲空,至此 1 的右子樹所有輸出,結束。

而非遞歸版本只是利用 stack 模擬上述過程而已,遞歸的過程也就是出入棧的過程。

 

 

1.2 中序遍歷

對於當前結點,先輸出它的左孩子,而後輸出該結點,最後輸出它的右孩子。以(1.1)圖爲例:

  1. 1-->2-->4,4 的左孩子爲空,輸出 4,接着右孩子;

  2. 6 的左孩子爲空,輸出 6,接着右孩子;

  3. 7 的左孩子爲空,輸出 7,右孩子也爲空,此時 2 的左子樹所有輸出,輸出 2,2 的右孩子爲空,此時 1 的左子樹所有輸出,輸出 1,接着 1 的右孩子;

  4. 3-->5,5 左孩子爲空,輸出 5,右孩子也爲空,此時 3 的左子樹所有輸出,而 3 的右孩子爲空,至此 1 的右子樹所有輸出,結束。

 

 

1.3 後序遍歷

對於當前結點,先輸出它的左孩子,而後輸出它的右孩子,最後輸出該結點。依舊以(1.1)圖爲例:

  1. 1->2->4->6->7,7 無左孩子,也無右孩子,輸出 7,此時 6 無左孩子,而 6 的右子樹也所有輸出,輸出 6,此時 4 無左子樹,而 4 的右子樹已所有輸出,接着輸出 4,此時 2 的左子樹所有輸出,且 2 無右子樹,輸出 2,此時 1 的左子樹所有輸出,接着轉向右子樹;

  2. 3->5,5 無左孩子,也無右孩子,輸出 5,此時 3 的左子樹所有輸出,且 3 無右孩子,輸出 3,此時 1 的右子樹所有輸出,輸出 1,結束。

非遞歸版本中,對於一個結點,若是咱們要輸出它,只有它既沒有左孩子也沒有右孩子或者它有孩子可是它的孩子已經被輸出(由此設置 pre 變量)。若非上述兩種狀況,則將該結點的右孩子和左孩子依次入棧,這樣就保證了每次取棧頂元素的時候,先依次遍歷左子樹和右子樹。

 

 

2 層次遍歷

 

 

3 求樹的結點數

 

 

4 求樹的葉子數

 

 

5 求樹的深度

 

 

6 求二叉樹第k層的結點個數

 

 

7 判斷兩棵二叉樹是否結構相同

不考慮數據內容。結構相贊成味着對應的左子樹和對應的右子樹都結構相同。

bool StructureCmp(Node * node1, Node * node2)
{
    if (node1 == nullptr && node2 == nullptr)
        return true;
    else if (node1 == nullptr || node2 == nullptr)
        return false;

    return StructureCmp(node1->left, node2->left) && Str1uctureCmp(node1->right, node2->right);
}

8 求二叉樹的鏡像

對於每一個結點,咱們交換它的左右孩子便可。

 

 

9 求兩個結點的最低公共祖先結點

最低公共祖先,即 LCA(Lowest Common Ancestor),見下圖:

 

 

結點 3 和結點 4 的最近公共祖先是結點 2,即 LCA(3,4)=2。在此,須要注意到當兩個結點在同一棵子樹上的狀況,如結點 3 和結點 2 的最近公共祖先爲 2,即 LCA(3,2)=2。同理 LCA(5,6)=4,LCA(6,10)=1。

 

 

10 求任意兩結點距離

 

 

 

 

11 找出二叉樹中某個結點的全部祖先結點

 

 

若是給定結點 5,則其全部祖先結點爲 4,2,1。

 

 

12 不使用遞歸和棧遍歷二叉樹

1968 年,高德納(Donald Knuth)提出一個問題:是否存在一個算法,它不使用棧也不破壞二叉樹結構,可是能夠完成對二叉樹的遍歷?隨後 1979 年,James H. Morris 提出了二叉樹線索化,解決了這個問題。(根據這個概念咱們又提出了一個新的數據結構,即線索二叉樹,因線索二叉樹不是本文要介紹的內容,因此有興趣的朋友請移步線索二叉樹)

前序,中序,後序遍歷,不論是遞歸版本仍是非遞歸版本,都用到了一個數據結構--棧,爲什麼要用棧?那是由於其它的方式無法記錄當前結點的 parent,而若是在每一個結點的結構裏面加個 parent 份量顯然是不現實的,而線索化正好解決了這個問題,其含義就是利用結點的右孩子空指針,指向該結點在中序序列中的後繼。下面具體來看看如何使用線索化來完成對二叉樹的遍歷。

 

 

  1. 若是當前結點的左孩子爲空,則輸出當前結點並將其右孩子做爲當前結點;

  2. 若是當前結點的左孩子不爲空,在當前結點的左子樹中找到當前結點在中序遍歷下的前驅結點;

  • 2.1若是前驅結點的右孩子爲空,將它的右孩子設置爲當前結點,輸出當前結點並把當前結點更新爲當前結點的左孩子;

  • 2.2若是前驅結點的右孩子爲當前結點,將它的右孩子從新設爲空,當前結點更新爲當前結點的右孩子;

  1. 重複以上步驟 1 和 2,直到當前結點爲空。

 

 

再來看中序遍歷,和前序遍歷相比只改動一句代碼,步驟以下:

  1. 若是當前結點的左孩子爲空,則輸出當前結點並將其右孩子做爲當前結點;

  2. 若是當前結點的左孩子不爲空,在當前結點的左子樹中找到當前結點在中序遍歷下的前驅結點;

  • 2.1. 若是前驅結點的右孩子爲空,將它的右孩子設置爲當前結點,當前結點更新爲當前結點的左孩子;

  • 2.2. 若是前驅結點的右孩子爲當前結點,將它的右孩子從新設爲空,輸出當前結點,當前結點更新爲當前結點的右孩子;

  1. 重複以上步驟 1 和 2,直到當前結點爲空。

 

 

 

 

最後看下後序遍歷,後序遍歷有點複雜,須要創建一個虛假根結點 dummy,令其左孩子是 root。而且還須要一個子過程,就是倒序輸出某兩個結點之間路徑上的各個結點。步驟以下:

  1. 若是當前結點的左孩子爲空,則將其右孩子做爲當前結點;

  2. 若是當前結點的左孩子不爲空,在當前結點的左子樹中找到當前結點在中序遍歷下的前驅結點;

  • 2.1. 若是前驅結點的右孩子爲空,將它的右孩子設置爲當前結點,當前結點更新爲當前結點的左孩子;

  • 2.2. 若是前驅結點的右孩子爲當前結點,將它的右孩子從新設爲空,倒序輸出從當前結點的左孩子到該前驅結點這條路徑上的全部結點,當前結點更新爲當前結點的右孩子;

  1. 重複以上步驟 1 和 2,直到當前結點爲空。

 

 

dummy 用的很是巧妙,建議讀者配合上面的圖模擬下算法流程。

13 二叉樹前序中序推後序

 

 

以上面圖表爲例,步驟以下:

  1. 根據前序可知根結點爲1;

  2. 根據中序可知 4 7 2 爲根結點 1 的左子樹和 8 5 9 3 6 爲根結點 1 的右子樹;

  3. 遞歸實現,把 4 7 2 當作新的一棵樹和 8 5 9 3 6 也當作新的一棵樹;

  4. 在遞歸的過程當中輸出後序。

 

 

固然咱們也能夠根據前序和中序構造出二叉樹,進而求出後序。

14 判斷二叉樹是否是徹底二叉樹

若設二叉樹的深度爲 h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層全部的結點都連續集中在最左邊,這就是徹底二叉樹(Complete Binary Tree)。以下圖:

 

 

首先若一個結點只有右孩子,確定不是徹底二叉樹;其次若只有左孩子或沒有孩子,那麼接下來的全部結點確定都沒有孩子,不然就不是徹底二叉樹,所以設置 flag 標記變量。

 

 

15 判斷是不是二叉查找樹的後序遍歷結果

在後續遍歷獲得的序列中,最後一個元素爲樹的根結點。從頭開始掃描這個序列,比根結點小的元素都應該位於序列的左半部分;從第一個大於跟結點開始到跟結點前面的一個元素爲止,全部元素都應該大於跟結點,由於這部分元素對應的是樹的右子樹。根據這樣的劃分,把序列劃分爲左右兩部分,咱們遞歸地確認序列的左、右兩部分是否是都是二元查找樹。

 

 

16 給定一個二叉查找樹中的結點(存在一個指向父親結點的指針),找出在中序遍歷下它的後繼和前驅

一棵二叉查找樹的中序遍歷序列,正好是升序序列。假如根結點的父結點爲 nullptr,則:

  1. 若是當前結點有右孩子,則後繼結點爲這個右孩子的最左孩子;

  2. 若是當前結點沒有右孩子;

  • 2.1. 當前結點爲根結點,返回 nullptr;

  • 2.2. 當前結點只是個普通結點,也就是存在父結點;

  • 2.2.1. 當前結點是父親結點的左孩子,則父親結點就是後繼結點;

  • 2.2.2. 當前結點是父親結點的右孩子,沿着父親結點往上走,直到 n-1 代祖先是 n 代祖先的左孩子,則後繼爲 n 代祖先或遍歷到根結點也沒找到符合的,則當前結點就是中序遍歷的最後一個結點,返回 nullptr。

 

 

仔細觀察上述代碼,總以爲有點囉嗦。好比,過多的 return,步驟 2 的層次太多。綜合考慮全部狀況,改進代碼以下:

 

 

上述的代碼是基於結點有 parent 指針的,若題意要求沒有 parent 呢?網上也有人給出了答案,我的以爲沒有什麼價值,有興趣的朋友能夠到這裏查看。

而求前驅結點的話,只需把上述代碼的 left 與 right 互調便可,很簡單。

17 二分查找樹轉化爲排序的循環雙鏈表

二分查找樹的中序遍歷即爲升序排列,問題就在於如何在遍歷的時候更改指針的指向。一種簡單的方法時,遍歷二分查找樹,將遍歷的結果放在一個數組中,以後再把該數組轉化爲雙鏈表。若是題目要求只能使用 O(1)O(1) 內存,則只能在遍歷的同時構建雙鏈表,即進行指針的替換。

咱們須要用遞歸的方法來解決,假定每一個遞歸調用都會返回構建好的雙鏈表,可把問題分解爲左右兩個子樹。因爲左右子樹都已是有序的,當前結點做爲中間的一個結點,把左右子樹獲得的鏈表鏈接起來便可。

 

 

18 有序鏈表轉化爲平衡的二分查找樹(Binary Search Tree)

咱們能夠採用自頂向下的方法。先找到中間結點做爲根結點,而後遞歸左右兩部分。因此咱們須要先找到中間結點,對於單鏈表來講,必需要遍歷一邊,可使用快慢指針加快查找速度。

 

 

由 f(n)=2f(\frac n2)+\frac n2f(n)=2f(2n)+2n 得,因此上述算法的時間複雜度爲 O(nlogn)O(nlogn)。

不妨換個思路,採用自底向上的方法:

 

如此,時間複雜度降爲 O(n)O(n)。

19 判斷是不是二叉查找樹

咱們假定二叉樹沒有重複元素,即對於每一個結點,其左右孩子都是嚴格的小於和大於。

下面給出兩個方法:

方法 1:

 

 

方法 2:

利用二叉查找樹中序遍歷時元素遞增來判斷。

相關文章
相關標籤/搜索