這兩天在作二叉樹相關的算法題,作一點學習筆記。(連二叉樹都不會?確實不熟練,平時工做也沒有要去寫二叉樹相關的算法或者數據結構的場景。由於本身菜,因此更加要努力學!)java
先看下維基百科的解釋:在計算機科學中,二叉樹(英語:Binary tree)是每一個節點最多隻有兩個分支(即不存在分支度大於2的節點)的樹結構。一般分支被稱做「左子樹」或「右子樹」。二叉樹的分支具備左右次序,不能隨意顛倒。算法
因爲二叉樹自己定義的特色,具備高度的局部重複性,因此在深度優先遍歷二叉樹時,一般採用遞歸的方式去實現,這樣實現出來的代碼很是簡潔漂亮,也比較容易看懂。數據結構
通常咱們深度優先遍歷二叉樹有三種最多見的順序遍歷:前序、中序、後序。學習
前序的遍歷順序爲:訪問根結點 -> 遍歷左子樹 -> 遍歷右子樹spa
中序的遍歷順序爲:遍歷左子樹 -> 訪問根結點 -> 遍歷右子樹設計
後序的遍歷順序爲:遍歷左子樹 -> 遍歷右子樹 -> 訪問根結點rest
注意這裏的左右是整個子樹,而不是一個結點,由於咱們須要遍歷整棵樹,因此每次遍歷都是按照這個順序去執行,直到葉子結點。code
舉個例子,假若有以下二叉樹:
blog
前序遍歷獲得的序列就是 A - B - C - D - E排序
中序遍歷獲得的序列就是 B - A - D - C - E
後序遍歷獲得的序列就是 B - D - E - C - A
思路咱們就用前序遍從來講(很是不建議去人肉遞歸,至少個人腦子吃不消三層。。。):
第一層遞歸:
先訪問根結點,因此輸出根結點 A,而後遍歷左子樹(L1),再遍歷右子樹(R1);
第二層遞歸:
對於 L1,先訪問根結點,因此輸出此時的根結點 B,而後發現 B 的左右子樹爲空,結束遞歸;
對於 R1,先訪問根結點,因此輸出此時的根結點 C,而後遍歷左子樹(L2),再遍歷右子樹(R2);
第三層遞歸:
對於 L2,先訪問根結點,因此輸出此時的根結點 D,而後發現 D 的左右子樹爲空,結束遞歸;
對於 R2,先訪問根結點,因此輸出此時的根結點 E,而後發現 E 的左右子樹爲空,結束遞歸;
根據前中後序的定義,其實咱們不難發現有以下特徵:
• 前序的第一個必定是 root 節點,後序的最後一個必定是 root 節點
• 每種排序的左子樹和右子樹分佈都是有規律的
• 對於每個子樹都遵循上面兩個規律的樹
這些特徵也就是定義中對順序的表現。
這邊列舉一下對於二叉樹的遍歷最基本的幾個算法題:
• 給定二叉樹得出其前/中/後序遍歷的序列;
• 根據前序和中序推導後序(或者推導整顆二叉樹);
• 根據後序和中序推導前序(或者推導整顆二叉樹);
對於二叉樹的遍歷,前面也講過,一般採用遞歸來作,對於遞歸,有模版能夠直接套用:
public void recur(int level, int param) { // terminator if (level > MAX_LEVEL) { // process result return; } // process current logic process(level, param); // drill down recur(level+1, newParam); // restore current status }
這個是我這兩天看極客時間的算法訓練營中超哥(覃超)講到的比較實用的小技巧(這個模版對於新手特別好),遵循上面的三步驟(若是有局部變量須要釋放或者額外處理則第四步去作)能比較有條理的寫出遞歸代碼。
這裏拿根據前序和中序推導後序來舉例:
先初始化兩個序列:
int[] preSequence = {1, 2, 3, 4, 5, 6, 7, 8, 9}; int[] inSequence = {2, 3, 1, 6, 7, 8, 5, 9, 4};
經過上面說到的幾個特徵,咱們已經能夠找到最小重複子問題了,每次遞歸
根據前序的第一個結點值去匹配中序中該結點值所在的索引 i,這樣咱們就能獲得索引 i 的先後兩部份分別對應左右子樹,接着分別去遍歷這兩個左右子樹,而後輸出當前前序的第一個結點值,也就是根結點。
根據自頂向下的程序設計方法,咱們能夠先寫出以下初始遞歸調用:
List<Integer> result = new ArrayList<>(); preAndInToPost(0, 0, preSequence.length, preSequence, inSequence, result);
第一個參數表示前序序列的第一個元素索引;
第二個參數表示中序序列的第一個元素索引;
第三個參數表示序列長度;
第四個參數表示前序序列;
第五個參數表示後序序列;
第六個參數用於保存結果;
先來考慮終止條件是什麼,也就是何時結束遞歸,當咱們的根結點爲空的時候終止,對應這裏就是序列長度爲零的時候。
if (length == 0) { return; }
接着考慮處理邏輯,也就是找到索引 i:
int i = 0; while (inSequence[inIndex + i] != preSequence[preIndex]) { i++; }
而後開始向下遞歸:
preAndInToPost(preIndex + 1, inIndex, i, preSequence, inSequence, result); preAndInToPost(preIndex + i + 1, inIndex + i + 1, length - i - 1, preSequence, inSequence, result); result.add(preSequence[preIndex]);
由於推導的是後序序列,因此順序如上,添加根結點的操做是在最後的。前三個參數如何得出來的呢,咱們走一下第一次遍歷就能夠得出來。
前序序列的第一個結點 1 在中序序列中的索引爲 2,此時
左子樹的中序系列起始索引爲總序列的第 1 個索引,長度爲 2;
左子樹的前序序列起始索引爲總序列的第 2 個索引,長度爲 2;
右子樹的中序系列起始索引爲總序列的第 3 個索引,長度爲 length - 3;
右子樹的前序序列起始索引爲總序列的第 3 個索引,長度爲 length - 3;
完整代碼以下:
/** * 根據前序和中序推導後序 * * @param preIndex 前序索引 * @param inIndex 中序索引 * @param length 序列長度 * @param preSequence 前序序列 * @param inSequence 中序序列 * @param result 結果序列 */ private void preAndInToPost(int preIndex, int inIndex, int length, int[] preSequence, int[] inSequence, List<Integer> result) { if (length == 0) { return; } int i = 0; while (inSequence[inIndex + i] != preSequence[preIndex]) { i++; } preAndInToPost(preIndex + 1, inIndex, i, preSequence, inSequence, result); preAndInToPost(preIndex + i + 1, inIndex + i + 1, length - i - 1, preSequence, inSequence, result); result.add(preSequence[preIndex]); }