学算法刷LeetCode:二叉树的遍历和重建(一)

286 阅读6分钟

简述二叉树

二叉树是每个节点最多两个分支的树形结构。遍历方式可以从上向下逐层遍历,先访问离根最近的节点,称为广度优先遍历,也可以从根节点开始,向最远的节点遍历,称为深度优先遍历,此外,再根据根节点的访问先后,分为前序遍历,中序遍历,后序遍历。

其实,只要记住如何访问根节点就可以了。

  • 访问离根最近的节点:广度优先遍历

  • 访问离根最远的节点:深度优先遍历

    • 先访问根节点,再访问左右子树:前序遍历
    • 中间访问根节点,即先左子树、根节点、右子树:中序遍历
    • 最后访问根节点:即先访问左右子树,再访问根节点:后续遍历

所以,理清了访问顺序之后,二叉树的遍历和重建就不难理解了。

我发现除了二叉树的遍历需要用迭代写外,其余的题目都是用递归去写的。因为可能递归做最基础的节点访问太简单了,换个位置就可以了。那就先说说递归的写法吧。

下文先讲深度优先遍历的写法。

递归

前序遍历

题目描述

image.png

二叉树的前序遍历

完整代码
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */

const traverse = (root, res) => {
    if(!root) return null;
    
    // 前序遍历的位置: 根左右
    res.push(root.val);
    
    traverse(root.left, res);
    
    traverse(root.right, res);
}

var preorderTraversal = function(root) {
    let res = []
    traverse(root, res)
    return res;
};

中序遍历

题目描述

image.png

二叉树的中序遍历

完整代码
const traverse = (root, res) => {
    if(!root) return null;
    traverse(root.left, res);
    
    // 中序遍历的位置: 左根右
    res.push(root.val);
    
    traverse(root.right, res);
}

var preorderTraversal = function(root) {
    let res = []
    traverse(root, res)
    return res;
};

后序遍历

题目描述

image.png

二叉树的后序遍历

根据经验,后续遍历就是把 res.push(root.val 搬到 traverse(root.right, res)的后面就可以了。

完整代码
const traverse = (root, res) => {
    if(!root) return null;
    traverse(root.left, res);
 
    traverse(root.right, res);
    
    // 后序遍历的位置: 左右根
    res.push(root.val);
}

var preorderTraversal = function(root) {
    let res = []
    traverse(root, res)
    return res;
};

迭代

迭代的写法需要用到栈,递归可以看作是对我们隐藏了栈,但是实际上函数调用也是通过栈去完成的。访问位置也是由位置决定的。

前序遍历(先根,后左右)

完整代码

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */

var preorderTraversal = function(root) {
    let result = [];
    let stack = [];
    let cur = root;
    while(cur || stack.length){
        while(cur){
            // 把当前节点(根节点)放到结果中
            result.push(cur.val);

            // 把当前节点压入栈中,并遍历它的左子树,依次添加到栈中
            stack.push(cur);
            cur = cur.left;
        }
        
        // 节点出栈,再将右节点入栈
        cur = stack.pop();
        cur = cur.right;
    }

    return res;
};

步骤拆解

  • 我们需要定义三个变量: resultstackcur

    • let result = [] 用于存放结果
    • let stack = [] 额外需要的栈,用于改变节点的访问顺序,即让最后出栈的顺序变成根左右,所以入栈的时候顺序就要变成右左根。
    • let cur = root, 用于当前节点的访问。
  • 遍历之前,我们需要先检查一下传入的树的根节点是否存在,并且因为 stack 之后会一直入栈,当它栈内元素为空的时候,循环停止。

while(cur || stack.length){
    // ...
}
  • 开始遍历啦!

    • 如果当前 cur 存在,则先把根节点存在 result 中,然后将当前节点压入栈中,并将当前节点的左节点全部压入栈中
        while(cur || stack.length){
            while(cur){
                // 第一次循环的时候,cur = root, 因此直接将根节点先 push 到 result 中
                result.push(cur.val);
                
                // 将当前节点压入栈中
                stack.push(cur);
                
                // 将当前节点的所有左节点压入栈中
                cur = cur.left;
            }
            //...
        }     
    
    • 此时,根节点和它的左子树都压入栈中了,然后出栈,查看当前节点的右子树,

      while(cur || stack.length){
          //...
          
          // 没有左子树之后,将当前节点出栈,并查看当前节点的右子树,如果有,则继续压入栈
          cur = stack.pop();
          cur = cur.right;
      }   
      

中序遍历(左中右,根中间遍历,左先,右最后)

其实可以理解成对于棵二叉树,把根节点和左子树一把梭哈入栈,栈里的元素一个一个出栈,这个时候,当前出栈的元素没有左子树了,它自己就是当前小树的根节点,把它放入结果中,然后看看该元素有没有右子树,没有的话出栈,否则继续入栈。

步骤拆解

  • 遍历的时候先把根节点和所有左子树的节点入栈

  • 再让节点一个一个出栈,并且将当前节点的值存入结果中,查看当前节点的右子树,如果没有节点,则继续出栈,否则也将其压入栈中。

完整代码

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */


var inorderTraversal = function(root) {
    let result = [];
    let stack = [];
    let cur = root;
    
    while(cur || stack.length){
        // 先把根节点和所有左子树都压入栈中
        while(cur){
            stack.push(cur);
            cur = cur.left;
        }
        // 出栈,当前节点就是离根节点最远的左子树的节点,这时候可以出栈了,把当前值添加到结果中,检查当前节点是否有右子树,没有则继续出栈,有的话将右节点入栈。让出栈顺序为 左、根、右
        cur = stack.pop();
        
        result.push(cur.val);
        
        cur = cur.right;
    }
    return result;
};

后序遍历(左右中,先左右,后根节点)

后续遍历和前面的写法差不多,唯一需要注意的是需要一个变量保存前一个节点,因为对于一颗树来说,根和左节点先被访问到,而我们结果是需要输出顺序是先左右节点,后根节点,所以我们需要标记一下这个节点是否被访问过。

var postorderTraversal = function(root) {
    let stack = [];
    let result = [];
    let prev = null;
    let cur = root;
    while(cur || stack.length){
    
    // 先从根节点开始,到左数遍历完为止,全部压入栈。
    while(cur){
        stack.push(cur);
        cur = cur.left;
    }
    
    // 获取当前节点
    cur = stack[stack.length - 1];
    
    // 判断右节点是否存在,且不是前一个节点,表示这个节点有右子树,且没有被遍历过
    if(cur.right && cur.right != prev){
        cur = cur.right;
    }else{
        // cur 节点没有右子树且它的右子树已经遍历过,就可以遍历这个节点,于是出栈并遍历它
        stack.pop();
        result.push(cur.val);
        prev = cur;
        cur = null;
    }
}
 return result;
};

迭代遍历总结

迭代和递归其实很类似,只不过递归的写法面试的时候没有难度,面试官也不会考,但是迭代的写法稍微难一点,但是其实其实只要按照下面步骤做也可以轻易写出来。

  • 迭代需要搞清楚循环种植条件,需要一个辅助栈,需要一个保存结果的数组
let stack = [];
let result = [];
let cur = root;
while(cur || stack.length){
    // ...
}
  • 搞清楚什么时候执行 result.push(cur.val), 后续遍历还需要保存前一个节点,用于判断该节点的右子树是否访问过。

    • 前序遍历
    • 中序遍历
    let stack = [];
    let result = [];
    let cur = root;
    while(cur || stack.length){
        while(cur){
            // 前序遍历的位置
            // result.push(cur.val);
    
            stack.push(cur);
            cur = cur.left;
        }
       cur = stsack.pop();
       // 中序遍历的位置
       // result.push(cur.val);
       cur = cur.right;
    }
    
    • 后续遍历
    let stack = [];
    let result = [];
    let prev = null;
    let cur = root;
    while(cur || stack.length){
        while(cur){
            stack.push(cur);
            cur = cur.left;
        }
       cur = stack[stack.length - 1]
       if(cur.right && cur.right != prev){
          cur = cur.right;
       }else {
           stack.pop();
           // 后序遍历的位置
           result.push(cur.val);
           prev = cur;
           cur = null;
       }
    }
    

复杂度分析

无论迭代还是递归,它们复杂度都是一样的。

时间复杂度: O(n)

空间复杂度:O(h)h为二叉树的深度。二叉树中深度 h 的最小值为 log(n+1),最大值为 n