二叉樹的前序、中序、後序的遞迴和迭代實現

MaiDouYT發表於2020-09-30

前言

二叉樹遍歷常用的一般分為前序、中序、後序三種,下面使用遞迴和迭代兩種方法來實現這三種遍歷,這三種遍歷的順序分別為:

  • 前序:中左右
  • 中序:左中右
  • 後序:左右中

記得時候就是左右不變,中跟著遍歷的方式走

遞迴

每次寫遞迴,都按照這三要素來寫,可以保證大家寫出正確的遞迴演算法!

  • 確定遞迴函式的引數和返回值:
    確定哪些引數是遞迴的過程中需要處理的,那麼就在遞迴函式里加上這個引數, 並且還要明確每次遞迴的返回值是什麼進而確定遞迴函式的返回型別。

  • 確定終止條件:
    寫完了遞迴演算法, 執行的時候,經常會遇到棧溢位的錯誤,就是沒寫終止條件或者終止條件寫的不對,作業系統也是用一個棧的結構來儲存每一層遞迴的資訊,如果遞迴沒有終止,作業系統的記憶體棧必然就會溢位。

  • 確定單層遞迴的邏輯:
    確定每一層遞迴需要處理的資訊。在這裡也就會重複呼叫自己來實現遞迴的過程。

所以以前序遍歷為例看上面三要素:

  • 確定遞迴函式的引數和返回值:

    因為要列印出前序遍歷節點的數值,所以需要傳入list存放返回結果

    void traversal(TreeNode root, List<Integer> ret)
    
  • 確定終止條件:

    如果當前遍歷的這個節點是空,就直接return

    if (root == null) {
        return;
    }
    
  • 確定單層遞迴的邏輯:

    前序遍歷是中左右,所以在單層遞迴的時候,需要先取中間節點的值

    ret.add(root.val); // 中
    traversal(root.left, ret); // 左
    traversal(root.right, ret); // 右
    

所以根據上面的分析,三種遍歷的遞迴方式就不難寫了,程式碼如下:

/**
 * 遍歷
 */
public List<Integer> traversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    // preOrderTraversal(root, ret);
    // postOrderTraversal(root, ret);
    return ret;
}
/**
 * 前序
 */
private void preOrderTraversal(TreeNode root, List<Integer> ret) {
    if (root == null) {
        return;
    }
    ret.add(root.val);
    traversal(root.left, ret);
    traversal(root.right, ret);
}
/**
 * 中序
 */
private void preOrderTraversal(TreeNode root, List<Integer> ret) {
    if (root == null) {
        return;
    }
    traversal(root.left, ret);
    ret.add(root.val);
    traversal(root.right, ret);
}
/**
 * 後序
 */
private void postOrderTraversal(TreeNode root, List<Integer> ret) {
    if (root == null) {
        return;
    }
    traversal(root.left, ret);
    traversal(root.right, ret);
    ret.add(root.val);
}

迭代

前序遍歷

前序遍歷是中左右,每次先處理的是中間節點,那麼先將跟節點放入棧中,然後將右孩子加入棧,再加入左孩子。

為什麼要先加入 右孩子,再加入左孩子呢? 因為這樣出棧的時候才是中左右的順序。程式碼如下:

// 前序迭代 按照 中左右 的順序依次訪問節點,並將資料處理入陣列中
private List<Integer> preOrderTraversal(TreeNode root) {
    Stack<TreeNode> stack = new Stack<TreeNode>();
    List<Integer> ret = new ArrayList<Integer>();

    if (root == null) {
        return ret;
    }

    stack.push(root);

    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        ret.add(node.val); // 中
        if (node.right != null) {
            stack.push(node.right); // 右 // 要先加入右孩子,出棧的時候才能先出做孩子
        }
        if (node.left != null) {
            stack.push(node.left); // 左
        }
    }
    return ret;
}

後序遍歷

先序遍歷是中左右,後續遍歷是左右中,那麼我們只需要調整一下先序遍歷的程式碼順序,就變成中右左的遍歷順序,然後在反轉result陣列,輸出的結果順序就是左右中了,程式碼如下:

/**
 * 利用先序遍歷的方式來簡化
 * 後序:左右中 <= 反轉 <= 中右左 <= 換左和右的順序 <= 中左右 : 前序
 */
private List<Integer> postOrderTraversal(TreeNode root) {
    Stack<TreeNode> stack = new Stack<TreeNode>();
    List<Integer> ret = new ArrayList<Integer>();

    if (root == null) {
        return ret;
    }

    stack.push(root);

    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        ret.add(node.val); // 中
        if (node.left != null) {
            stack.push(node.left); // 左
        }
        if (node.right != null) {
            stack.push(node.right); // 右
        }
    }

    Collections.reverse(ret); // 將結果反轉之後就是左右中的順序了 
    return ret;
}

中序遍歷

在前序和後序遍歷的迭代中,有兩個比較關鍵的操作:

  • 處理:將元素放進result陣列中
  • 訪問:遍歷節點

分析一下為什麼剛剛寫的前序遍歷的程式碼,不能和中序遍歷通用呢,因為前序遍歷的順序是中左右,先訪問的元素是中間節點,要處理的元素也是中間節點,所以剛剛才能寫出相對簡潔的程式碼,因為要訪問的元素和要處理的元素順序是一致的,都是中間節點

那麼再看看中序遍歷,中序遍歷是左中右,先訪問的是二叉樹頂部的節點,然後一層一層向下訪問,直到到達樹左面的最底部,再開始處理節點(也就是在把節點的數值放進result陣列中),這就造成了處理順序和訪問順序是不一致的

那麼在使用迭代法寫中序遍歷,就需要借用指標的遍歷來幫助訪問節點,棧則用來處理節點上的元素

/**
 * 用指標的遍歷來幫助訪問節點,棧則用來處理節點上的元素
 * 中序:左中右,所以堅持的原則就是左子樹存在就先處理左子樹
 */
private List<Integer> inOrderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<Integer>();
    Stack<TreeNode> stack = new Stack<TreeNode>();

    TreeNode cur = root; // 指標,標識訪問節點

    while (cur != null || !stack.isEmpty()) {
        if (cur != null) { // 指標來訪問節點,訪問到最底層
            stack.push(cur);  // 將訪問的節點放進棧
            cur = cur.left; // 左
        } else {
            cur = stack.pop(); // 從棧裡彈出的資料,就是要處理的資料(放進result陣列裡的資料)
            ret.add(cur.val); // 中
            cur = cur.right; // 右
        }
    }

    return ret;
}

附言

上文解釋了迭代實現的時候中序比較特殊的地方,原文中給出了三種迭代方式如何實現歸一化的實現方法,主要的思想就是解決上文說的訪問節點和處理節點不一致的情況,思路是將訪問的節點放入棧中,把要處理的節點也放入棧中但是要做個標記,當遇到這個標記的時候表示下一個節點時一個處理節點,可以放進結果集,不過個人覺得沒上文直接寫的直觀,有興趣的可以直接到原文中檢視

轉自這裡,點原文檢視

相關Leetcode

相關文章