1. 程式人生 > 實用技巧 >二叉樹遍歷的常用方法

二叉樹遍歷的常用方法

概述

二叉樹的遍歷可以說是解決二叉樹問題的基礎。我們常用的遍歷方式無外乎就四種前序遍歷中序遍歷後續遍歷層次遍歷這四種。其中前三種遍歷方式在實現時,即便採用不同的實現方式(遞迴方式、非遞迴),它們的演算法結構是有很大的相似性。因而針對前三種的遍歷我們會總結出對應通用的解決框架,便於在解決二叉樹問題時進行使用。

遞迴方式

遞迴方式遍歷二叉樹時,無論是前序遍歷中序遍歷還是後續遍歷的方式,它們最大的區別就是對節點資料的訪問位置不同。除此之外其結構完全一致,因而我們總結出如下的框架結構:

void traverse(TreeNode root) {
    //終止條件
    if(root == null) return;
    // 前序遍歷
    traverse(root.left);
    // 中序遍歷
    traverse(root.right);
    // 後序遍歷
}

對應註釋的位置訪問資料就可以實現不同的遍歷方式。

例如,前序遍歷:

void traverse(TreeNode root) {
    if(root == null) return;
    visit(root);
    traverse(root.left);
    traverse(root.right);
}

同樣的中序遍歷:

void traverse(TreeNode root) {
    if(root ==null) return;
    traverse(root.left);
    visit(root);
    traverse(root.right);
}

後續遍歷:

void traverse(TreeNode root) {
    if(root ==null) return;
    traverse(root.left);
    traverse(root.right)
}

是否非常easy!!

非遞迴方式

二叉樹非遞迴遍歷說實話有很多種實現方式,但本質上都是模擬整個遍歷的過程來實現的。

為了便於理解,其中前序遍歷、中序遍歷、後序遍歷我們採用一套類似的演算法框架。

整個演算法框架如下:

 public void tranverse(TreeNode root) {
    // 邊界判斷
    if (root == null) {
      return;
    }
    Stack<TreeNode> stack = new Stack<>();
    TreeNode current = root;
    while (current != null || !stack.isEmpty()) {
       //節點非空時,證明父節點的左側節點非空,直接入棧
      if (current != null) {
        //前序遍歷 visit(current)
        stack.push(current);
        current = current.left;
      } else {
        //節點為空,證明左側節點為空,出棧,更換遊標節點方向
        current = stack.pop();
		//中續遍歷 visit(current);
        current = current.right;
      }
    }
  }

後序遍歷它的遍歷順序為"左-->右-->根",較之與前序遍歷的"根-->左-->右",好像是有很大的相似性,我們能否針對上邊的框架進行修改,使由前序遍歷轉換成後序遍歷??
答案是肯定的,我們可以觀察到,可以先求出遍歷順序是"根-->右-->左""的節點序列,再倒序,便剛好是後序遍歷的順序:左右根。而遍歷順序是根右左的話,很好辦,從前序遍歷的程式碼中改兩行就是了。

故而,可以選擇使用兩個棧,其中一個用於遍歷,另一個用於結果的倒序。

實現程式碼如下:

//使用雙棧來實現後序遍歷
  public void postOrderTraverse(TreeNode root){
    Stack<TreeNode> stack = new Stack<>();
    Stack<Integer> res = new Stack<>();
    TreeNode cur = root;
    while (cur!=null || !stack.isEmpty()) {
      if (cur!=null){
        stack.push(cur);
        res.push(cur.val);
        cur = cur.right; //修改處
      }else{
        cur = stack.pop();
        cur = cur.left;  // 修改處
      }
    }
    while (!res.isEmpty()){
      visit(res.pop());
    }
  }

至此,非遞迴遍歷完成,是不是也很easy!!

下邊我們可以看一下最後一種層次遍歷

層次遍歷

層次遍歷本質上就是閹割版廣度優先遍歷,關於BFS我之前也寫了一篇文章BFS與DFS套路總結,感興趣的小夥伴可以去讀一下。我們此處就直接給出BFS演算法的框架:

/**
* 給定起始節點start和目標節點target,返回其最短路徑長度
**/
int BFS(Node start,Node target){
    Queue<Node> q; //核心資料結構
    Set<Node> visited: //某些情況下可以通過byte陣列來進行代替
    int step = 0; //記錄擴散步數
    //起始節點入佇列
    q.add(start);
    visited.offer(start);
    while(q not empty) {
        //必須要用sz來儲存q.size(),然後擴散sz不能直接使用q.size()
        int sz = q.size();
        //將佇列中的節點進行擴散
        for(int i =0 ; i < sz; i++) {
            Node cur = q.poll();
            // 目標節點判斷
            if(cur is target) {
                return step;
            }
            // 鄰接結點入佇列
            for(Node n:cur.adjs) {
                //未訪問節點入佇列
                if(n is not int visited) {
                    visitd.add(n);
                    q.offer(n);
                }
            }
        }
        // 更新步數
        step++;
    }
}

此處我們藉助BFS的框架,直接給出其實現方法:

void LevelOrder(TreeNode root){
    //初始化棧,並放入
    Queue<TreeNode> queue;
    queue.add(root);
    while( !queue.isEmpty()) {
        //出棧
        TreeNode cur = queue.poll();
        //訪問節點
        visit(cur);
        //向下一層級擴散
        if(cur.left !=null) queue.add(cur.left);
        if(cur.right !=null) queue.add(cur.right);
    }
}

較之於BFS,我們會發現,層次遍歷,少了好多東西,比如不需要visited來標記已訪問的節點(二叉樹本身結構的特點,不可能出現重複遍歷),也不需要將佇列中的節點進行擴散等。

總結

至此,二叉樹的四種遍歷方式總結完成。我們發現其實二叉樹所有的遍歷方式都有一種通用的演算法框架,只要掌握演算法本身的框架還是比較容易能夠寫出實現程式碼的。

參考

  1. https://www.cnblogs.com/kangna/p/11846156.html

  2. https://zhuanlan.zhihu.com/p/80578741