递归

一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。

1. 树的高度

  1. Maximum Depth of Binary Tree (Easy)
  • 1.1 题目
1
  • 1.2 分析解答
class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }
}

2. 平衡树

  1. Balanced Binary Tree (Easy)
  • 2.1 题目
2
  • 2.2 分析解答
    • 每次获得左右子树高度都进行高度差计算,如果出现高度差大于1的情况,就把结果设置false
    • 计算树高度的方法
      • 终止条件: 当 root 为空,即越过叶子节点,则返回高度 0 ;
      • 返回值: 返回左 / 右子树的最大高度加 1
    • 时间复杂度O(NlogN),空间复杂度O(N),最差情况递归需要使用O(N)的栈空间
class Solution {
    private boolean res = true;
    public boolean isBalanced(TreeNode root) {
        maxDepth(root);
        return res;
    }
    private int maxDepth(TreeNode root){
        if(root == null) return 0;
        int l = maxDepth(root.left);
        int r = maxDepth(root.right);
        if(Math.abs(l - r) > 1) res = false;
        return Math.max(l, r) + 1;
    }
}
  • 出现不平衡的子树时不用继续遍历所有代码,可以直接返回-1
class Solution {
    public boolean isBalanced(TreeNode root) {
        return recur(root) != -1;
    }

    private int recur(TreeNode root) {
        if (root == null) return 0;
        int left = recur(root.left);
        if(left == -1) return -1;
        int right = recur(root.right);
        if(right == -1) return -1;
        return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;
    }
}

3. 两节点的最长路径

  1. Diameter of Binary Tree (Easy)
  • 3.1 题目
3
  • 3.2 分析解答
    • 辨析:树的深度和树的高度,深度是从上到下数,高度是从下往上数。根节点的深度为1,叶子节点的高度为1。对于树(整棵树或者子树)来说,树的深度和高度是相等的。图中节点2的深度为2,高度为4。
3.2
  • 任意一条路径均可以被看作由某个节点为起点,从其左儿子和右儿子向下遍历的路径拼接得到。
  • 一条路径的长度为该路径经过的节点数减一,所以求直径(即求路径长度的最大值)等效于求路径经过节点数的最大值减一。
  • 假设我们知道对于该节点的左儿子向下遍历经过最多的节点数 L (即以左儿子为根的子树的深度) 和其右儿子向下遍历经过最多的节点数 R (即以右儿子为根的子树的深度),那么以该节点为起点的路径经过节点数的最大值即为 L+R+1 。
  • 时间复杂度:O(N),其中 N 为二叉树的节点数,即遍历一棵二叉树的时间复杂度,每个结点只被访问一次。
  • 空间复杂度:O(Height),其中 Height 为二叉树的高度。
class Solution {
    int ans;
    public int diameterOfBinaryTree(TreeNode root) {
        ans = 1;
        depth(root);
        return ans - 1;
    }
    public int depth(TreeNode node) {
        if (node == null) return 0; // 访问到空节点了,返回0
        int L = depth(node.left); // 左儿子为根的子树的深度
        int R = depth(node.right); // 右儿子为根的子树的深度
        ans = Math.max(ans, L+R+1); // 计算经过root节点的最大路径的节点总数,即L+R+1 并更新ans
        return Math.max(L, R) + 1; // 返回该节点为根的子树的深度
    }
}

4. 翻转树

  1. Invert Binary Tree (Easy)
  • 4.1 题目
4
  • 4.2 分析解答
  • 递归实现
    • 终止条件:当前节点为null时返回
    • 时间复杂度:每个元素都必须访问一次,所以是O(n)
    • 空间复杂度:最坏的情况下,需要存放O(h)个函数调用(h是树的高度)
public TreeNode invertTree(TreeNode root) {
    if (root == null) return null;
    TreeNode left = root.left;  // 后面的操作会改变 left 指针,因此先保存下来
    root.left = invertTree(root.right); //根节点的左子树=翻转后的右子树
    root.right = invertTree(left); //根节点的右子树=翻转后的左子树
    return root;
}
  • 迭代实现
    • 需要交换树中所有节点的左孩子和右孩子。因此可以创一个队列来存储所有左孩子和右孩子还没有被交换过的节点。只要这个队列不空,就一直从队列中出队节点,然后互换这个节点的左右孩子节点,接着再把孩子节点入队到队列,对于其中的空节点不需要加入队列。最终队列一定会空,这时候所有节点的孩子节点都被互换过了,直接返回最初的根节点就可以了。
    • 时间复杂度O(n),n是节点个数,所有节点只需入队一次
    • 空间复杂度O(n)
public TreeNode invertTree(TreeNode root) {
    if (root == null) return null;
    Queue queue = new LinkedList();
    queue.add(root);
    while (!queue.isEmpty()) {
        TreeNode current = queue.poll();
        TreeNode temp = current.left;
        current.left = current.right;
        current.right = temp;
        if (current.left != null) queue.add(current.left);
        if (current.right != null) queue.add(current.right);
    }
    return root;
}

5. 归并两棵树

  1. Merge Two Binary Trees (Easy)
  • 5.1 题目
5
  • 5.2 分析解答
  • 递归实现
    • 对这两棵树同时进行前序遍历,并将对应的节点进行合并。在遍历时,如果两棵树的当前节点均不为空,我们就将它们的值进行相加,并对它们的左孩子和右孩子进行递归合并;如果其中有一棵树为空,那么我们返回另一颗树作为结果;如果两棵树均为空,此时返回任意一棵树均可(因为都是空)。
    • 时间复杂度:O(N),其中 N 是两棵树中节点个数的较小值。
    • 空间复杂度:O(N),在最坏情况下,会递归 N 层,需要 O(N) 的栈空间。
public class Solution {
    public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
        if (t1 == null)
            return t2;
        if (t2 == null)
            return t1;
        t1.val += t2.val;
        t1.left = mergeTrees(t1.left, t2.left);
        t1.right = mergeTrees(t1.right, t2.right);
        return t1;
    }
}
  • 采用新建节点的方式,性能比第一个更差
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
    if (t1 == null && t2 == null) return null;
    if (t1 == null) return t2;
    if (t2 == null) return t1;
    TreeNode root = new TreeNode(t1.val + t2.val);
    root.left = mergeTrees(t1.left, t2.left);
    root.right = mergeTrees(t1.right, t2.right);
    return root;
}
  • 迭代实现
    • 首先把两棵树的根节点入栈,栈中的每个元素都会存放两个根节点,并且栈顶的元素表示当前需要处理的节点。在迭代的每一步中,我们取出栈顶的元素并把它移出栈,并将它们的值相加。随后我们分别考虑这两个节点的左孩子和右孩子,如果两个节点都有左孩子,那么就将左孩子入栈;如果只有一个节点有左孩子,那么将其作为第一个节点的左孩子;如果都没有左孩子,那么不用做任何事情。对于右孩子同理。
    • 返回第一棵树的根节点
public class Solution {
    public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
        if (t1 == null)
            return t2;
        Stack < TreeNode[] > stack = new Stack < > ();
        stack.push(new TreeNode[] {t1, t2});
        while (!stack.isEmpty()) {
            TreeNode[] t = stack.pop();
            if (t[1] == null) {
                continue;
            }
            t[0].val += t[1].val;
            if (t[0].left == null) {
                t[0].left = t[1].left;
            } else {
                stack.push(new TreeNode[] {t[0].left, t[1].left});
            }
            if (t[0].right == null) {
                t[0].right = t[1].right;
            } else {
                stack.push(new TreeNode[] {t[0].right, t[1].right});
            }
        }
        return t1;
    }
}

6. 判断路径和是否等于一个数

Leetcdoe : 112. Path Sum (Easy)

  • 6.1 题目
6
  • 6.2 分析解答
  • 递归实现
    • 利用递归,遍历整棵树:如果当前节点不是叶子,对它的所有孩子节点,递归调用 hasPathSum 函数,其中 sum 值减去当前节点的权值;如果当前节点是叶子,检查 sum 值是否为 0,也就是是否找到了给定的目标和。
    • 时间复杂度:我们访问每个节点一次,时间复杂度为 O(N) ,其中 N 是节点个数。
    • 空间复杂度:最坏情况下,整棵树是非平衡的,例如每个节点都只有一个孩子,递归会调用 N 次(树的高度),因此栈的空间开销是 O(N) 。但在最好情况下,树是完全平衡的,高度只有 log(N),因此在这种情况下空间复杂度只有 O(log(N)) 。
public boolean hasPathSum(TreeNode root, int sum) {
    if (root == null) return false;
    if (root.left == null && root.right == null && root.val == sum) return true;
    return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}
  • 迭代实现
    • 时间复杂度:和递归方法相同是 O(N)。
    • 空间复杂度:当树不平衡的最坏情况下是 O(N) 。在最好情况(树是平衡的)下是 O(logN)。
class Solution {
  public boolean hasPathSum(TreeNode root, int sum) {
    if (root == null)
      return false;

    LinkedList node_stack = new LinkedList();
    LinkedList sum_stack = new LinkedList();
    node_stack.add(root);
    sum_stack.add(sum - root.val);

    TreeNode node;
    int curr_sum;
    while ( !node_stack.isEmpty() ) {
      node = node_stack.pollLast();
      curr_sum = sum_stack.pollLast();
      if ((node.right == null) && (node.left == null) && (curr_sum == 0))
        return true;

      if (node.right != null) {
        node_stack.add(node.right);
        sum_stack.add(curr_sum - node.right.val);
      }
      if (node.left != null) {
        node_stack.add(node.left);
        sum_stack.add(curr_sum - node.left.val);
      }
    }
    return false;
  }
}

7. 统计路径和等于一个数的路径数量

  1. Path Sum III (Easy)
  • 7.1 题目
7
  • 7.2 分析解答

public int pathSum(TreeNode root, int sum) {
    if (root == null) return 0;
    int ret = pathSumStartWithRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
    return ret;
}

private int pathSumStartWithRoot(TreeNode root, int sum) {
    if (root == null) return 0;
    int ret = 0;
    if (root.val == sum) ret++;
    ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val);
    return ret;
}

8. 子树

  1. Subtree of Another Tree (Easy)
  • 8.1 题目
8
  • 8.2 分析解答
    • 首先判断一个树是否是另一棵树的子树,很明显想到可以用递归,但是两棵树完全相同也可以看做一棵树是另一棵树的子树。
class Solution {
    public boolean isSubtree(TreeNode s, TreeNode t) {
        if (t == null) return true;   // t 为 null 一定都是 true
        if (s == null) return false;  // 这里 t 一定不为 null, 只要 s 为 null,肯定是 false
        return isSubtree(s.left, t) || isSubtree(s.right, t) || isSameTree(s,t);
    }

    /**
     * 判断两棵树是否相同
     */
    public boolean isSameTree(TreeNode s, TreeNode t){
        if (s == null && t == null) return true;
        if (s == null || t == null) return false;
        if (s.val != t.val) return false;
        return isSameTree(s.left, t.left) && isSameTree(s.right, t.right);
    }
}

9. 树的对称

  1. Symmetric Tree (Easy)
  • 9.1 题目
9
  • 9.2 分析解答
  • 递归实现
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;
        return isSymmetricRef(root.left, root.right);
    }
    private boolean isSymmetricRef(TreeNode t1, TreeNode t2){
        if(t1 == null && t2 == null) return true;
        if(t1 == null || t2 == null) return false;
        if(t1.val != t2.val) return false;
        return isSymmetricRef(t1.left, t2.right) && isSymmetricRef(t1.right, t2.left);
    } 
}
  • 迭代实现
    • 首先我们引入一个队列,这是把递归程序改写成迭代程序的常用方法。初始化时我们把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像),然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者我们检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。
class Solution {
    public boolean isSymmetric(TreeNode root) {
        return check(root, root);
    }

    public boolean check(TreeNode u, TreeNode v) {
        Queue q = new LinkedList();
        q.offer(u);
        q.offer(v);
        while (!q.isEmpty()) {
            u = q.poll();
            v = q.poll();
            if (u == null && v == null) {
                continue;
            }
            if ((u == null || v == null) || (u.val != v.val)) {
                return false;
            }

            q.offer(u.left);
            q.offer(v.right);

            q.offer(u.right);
            q.offer(v.left);
        }
        return true;
    }
}

10. 最小路径

  1. Minimum Depth of Binary Tree (Easy)
  • 10.1 题目
10
  • 10.2 分析解答
  • 递归实现
    • 时间复杂度:我们访问每个节点一次,时间复杂度为 O(N) ,其中 N 是节点个数。
    • 空间复杂度:最坏情况下,整棵树是非平衡的,例如每个节点都只有一个孩子,递归会调用 N (树的高度)次,因此栈的空间开销是 O(N) 。但在最好情况下,树是完全平衡的,高度只有 log(N),因此在这种情况下空间复杂度只有 O(log(N)) 。
class Solution {
    public int minDepth(TreeNode root) {
        if(root == null) return 0;
        int left = minDepth(root.left);
        int right = minDepth(root.right);
        if(left == 0 || right == 0) return left + right + 1;
        return Math.min(left, right) + 1;
    }
}

11. 统计左叶子节点的和

  1. Sum of Left Leaves (Easy)
  • 11.1 题目
11
  • 11.2 分析解答
  • 递归实现
    • 时间复杂度,由于每个结点只访问了一次,所以O(n)。空间复杂度,就是函数调用栈的空间,其O(h),h为树的高度。对于平衡二叉树为logn,完全不平衡二叉树则是n。
class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if(root == null) return 0;
        if(isLeaf(root.left)) return root.left.val + sumOfLeftLeaves(root.right);
        return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
    }
    private boolean isLeaf(TreeNode node){
        if(node == null) return false;
        return node.left == null && node.right == null;
    }
}

12. 相同节点值的最大路径长度

  1. Longest Univalue Path (Easy)
  • 12.1 题目
12
  • 12.2 分析解答
  • 递归实现
    • 时间复杂度:O(N),其中 N 是树中节点数。我们处理每个节点一次。
    • 空间复杂度:O(H),其中 H 是树的高度。我们的递归调用栈可以达到 H 层的深度。
    • 对某个节点node,有:
      计算左子树到右子树的同值路径的长度:若root.left.val == root.right.val == root.val,则为:左子树单侧最长同值路径 + 2 + 右子树单侧最长同值路径
      node本身最长的单侧同值路径长度,为左、右子树最长的单侧同值路径长度中的最大值(root.val要各与root.left.val或root.right.val相等)
    • 辅助函数既求得了该节点的最长单侧同值路径长度,也维护了经过该节点的最大同值路径长度
class Solution {
    int max = 0;

    public int longestUnivaluePath(TreeNode root) {
        doLongestUnivaluePath(root);
        return max;
    }

    private int doLongestUnivaluePath(TreeNode root) {
        if (root == null) return 0;
        int left = doLongestUnivaluePath(root.left);
        int right = doLongestUnivaluePath(root.right);
        if (root.left != null && root.right != null && root.left.val == root.right.val && root.left.val == root.val) {
            max = Math.max(left + right + 2, max);
        }
        int length = 0;
        if (root.left != null && root.val == root.left.val) {
            length = left + 1;
        }
        if (root.right != null && root.val == root.right.val) {
            length = Math.max(right + 1, length);
        }
        max = Math.max(length, max);
        return length;

    }
}
class Solution {
    private int path = 0;
    public int longestUnivaluePath(TreeNode root) {
        dfs(root);
        return path;
    }
    private int dfs(TreeNode root){
        if(root == null) return 0;
        //root节点左子树和右子树的最大路径
        int left = dfs(root.left);
        int right = dfs(root.right);
        //和root节点等值的左右最大路径
        int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0;
        int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0;
        //维护一个全局的路径最大值
        path = Math.max(path, leftPath + rightPath);
        //返回的是和root节点值相同的最大路径
        return Math.max(leftPath, rightPath);
    }
}

13. 间隔遍历

  1. House Robber III (Medium)
  • 13.1 题目
13
  • 13.2 分析解答
  • 1、暴力递归 - 最优子结构
    使用爷爷、两个孩子、4 个孙子来说明问题。
    4 个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构
public int rob(TreeNode root) {
    if (root == null) return 0;

    int money = root.val;
    if (root.left != null) {
        money += (rob(root.left.left) + rob(root.left.right));
    }

    if (root.right != null) {
        money += (rob(root.right.left) + rob(root.right.right));
    }

    return Math.max(money, rob(root.left) + rob(root.right));
}
  • 2、记忆化 - 解决重复子问题
    问题:爷爷在计算自己能偷多少钱的时候,同时计算了 4 个孙子能偷多少钱,也计算了 2 个儿子能偷多少钱。这样在儿子当爷爷时,就会产生重复计算一遍孙子节点。
    优化:针对重复子问题进行优化,在做斐波那契数列时,使用的优化方案是记忆化,但是之前的问题都是使用数组解决的,把每次计算的结果都存起来,下次如果再来计算,就从缓存中取,不再计算了,这样就保证每个数字只计算一次。
    由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode 当做 key,能偷的钱当做 value
public int rob(TreeNode root) {
    HashMap memo = new HashMap<>();
    return robInternal(root, memo);
}

public int robInternal(TreeNode root, HashMap memo) {
    if (root == null) return 0;
    if (memo.containsKey(root)) return memo.get(root);
    int money = root.val;

    if (root.left != null) {
        money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
    }
    if (root.right != null) {
        money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
    }
    int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
    //每次都将当前节点能偷到的钱进行记录
    memo.put(root, result);
    return result;
}
  • 3、大成
    每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷
    • 当前节点选择偷时,那么两个孩子节点就不能选择偷了
    • 当前节点选择不偷时,两个孩子节点只需要拿最多的钱出来就行(两个孩子节点偷不偷没关系)

我们使用一个大小为 2 的数组来表示 int[] res = new int[2] 0 代表不偷,1 代表偷
任何一个节点能偷到的最大钱的状态可以定义为

  • 当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱

  • 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数

  • 表示为公式如下
    root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) + Math.max(rob(root.right)[0], rob(root.right)[1])
    root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;

public int rob(TreeNode root) {
    int[] result = robInternal(root);
    return Math.max(result[0], result[1]);
}

public int[] robInternal(TreeNode root) {
    if (root == null) return new int[2];
    int[] result = new int[2];

    int[] left = robInternal(root.left);
    int[] right = robInternal(root.right);

    result[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
    result[1] = left[0] + right[0] + root.val;

    return result;
}

14. 找出二叉树中第二小的节点

  1. Second Minimum Node In a Binary Tree (Easy)
  • 14.1 题目
14
  • 14.2 分析解答
class Solution {
    public int findSecondMinimumValue(TreeNode root) {
        if(root == null) return -1;
        if(root.left == null) return -1;
        int leftValue = root.left.val;
        int rightValue = root.right.val;
        if(leftValue == root.val) leftValue = findSecondMinimumValue(root.left);
        if(rightValue == root.val) rightValue = findSecondMinimumValue(root.right);
        if(leftValue == -1) return rightValue;
        if(rightValue == -1) return leftValue;
        return Math.min(leftValue, rightValue);
    }
}
public int findSecondMinimumValue(TreeNode root) {
        if(root == null)
            return -1;
        return find(root, root.val);
    }

    /**
     * 按照题目描述,这个二叉树是个最小堆,最上面的元素就是最小的元素。用rootValue表示最小值
     * 找到和rootValue值不相同的最小值,与rootValue不相同的最小值其实就是第二小的值。
     */
    private int find(TreeNode x, int rootValue){
        if(x.val != rootValue) // 如果当前结点不等于根结点至,那么当x值为以x为根的最小的非rootValue的值
            return x.val;
        // 这之下都是 当前结点值为根结点值的情况
        if(x.left == null) // 递归到叶子结点 且其值为根结点值,说明没有找到第二小的值,返回失败标志-1。
            return -1;
        int leftMin = find(x.left, rootValue);
        int rightMin = find(x.right, rootValue);
        if(leftMin == -1)
            return rightMin;
        if(rightMin == -1)
            return leftMin;
        return Math.min(leftMin, rightMin);
    }

层次遍历

使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,使用一个队列存储节点就可以了,每次取出队头的节点,有左节点和右节点则压入队尾。这样在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。

  • 代码实现
class TreeNode(){
    TreeNode left;
    TreeNode right;
    int x;
    TreeNode(int x){
        this.x = x;
    }
}
public void levelOrder(TreeNode root){
    LinkedList queue = new LinkedList();
    TreeNode temp;
    queue.offer(root);
    while(!queue.isEmpty()) {
        temp = queue.poll();
        System.out.println(temp);
        if(temp.left != null) queue.offer(temp.left);
        if(temp.right != null) queue.offer(temp.right);
    }
}

15. 一棵树每层节点的平均数

  1. Average of Levels in Binary Tree (Easy)
  • 15.1 题目
15
  • 15.2 分析解答
    • 时间复杂度:O(N),其中 N 是树中的节点个数。
    • 空间复杂度:O(M),其中 M 是树中每一层节点个数的最大值,即为广度优先搜索中使用队列存储同一层节点需要的空间。
public List averageOfLevels(TreeNode root) {
    List ret = new ArrayList<>();
    if (root == null) return ret;
    Queue queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int cnt = queue.size();
        double sum = 0;
        for (int i = 0; i < cnt; i++) {
            TreeNode node = queue.poll();
            sum += node.val;
            if (node.left != null) queue.add(node.left);
            if (node.right != null) queue.add(node.right);
        }
        ret.add(sum / cnt);
    }
    return ret;
}

16. 得到左下角的节点

  1. Find Bottom Left Tree Value (Easy)
  • 16.1 题目
16
  • 16.2 分析解答
    • BFS算法,先将右边的子节点压入队列,再将左边子节点压入队列中,这样层次遍历结束,最后一个节点就是整棵树的最后一行最左边的节点。
public int findBottomLeftValue(TreeNode root) {
    Queue queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        root = queue.poll();
        if (root.right != null) queue.add(root.right);
        if (root.left != null) queue.add(root.left);
    }
    return root.val;
}

前中后序遍历

  • 层次遍历顺序:[1 2 3 4 5 6]
  • 前序遍历顺序:[1 2 4 5 3 6] 中左右
  • 中序遍历顺序:[4 2 5 1 3 6] 左中右
  • 后序遍历顺序:[4 5 2 6 3 1] 左右中

层次遍历使用 BFS 实现,利用的就是 BFS 一层一层遍历的特性;而前序、中序、后序遍历利用了 DFS 实现。
前序、中序、后序遍只是在对节点访问的顺序有一点不同,其它都相同。

递归实现

  • ① 前序
void dfs(TreeNode root) {
    visit(root);
    dfs(root.left);
    dfs(root.right);
}
  • ② 中序
void dfs(TreeNode root) {
    dfs(root.left);
    visit(root);
    dfs(root.right);
}
  • ③ 后序
void dfs(TreeNode root) {
    dfs(root.left);
    dfs(root.right);
    visit(root);
}

非递归实现

17. 非递归实现二叉树的前序遍历

  1. Binary Tree Preorder Traversal (Medium)
  • 解答
public List preorderTraversal(TreeNode root) {
    List ret = new ArrayList<>();
    Stack stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        if (node == null) continue;
        ret.add(node.val);
        stack.push(node.right);  // 先右后左,保证左子树先遍历
        stack.push(node.left);
    }
    return ret;
}

18. 非递归实现二叉树的后续遍历

  1. Binary Tree Postorder Traversal (Medium)
  • 解答
public List postorderTraversal(TreeNode root) {
    List ret = new ArrayList<>();
    Stack stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        if (node == null) continue;
        ret.add(node.val);
        stack.push(node.left);
        stack.push(node.right);
    }
    Collections.reverse(ret);
    return ret;
}
class Solution {
    public List postorderTraversal(TreeNode root) {
        LinkedList ret = new LinkedList<>();
        Stack stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node == null) continue;
            ret.addFirst(node.val);
            stack.push(node.left);
            stack.push(node.right);
        }
        return ret;
    }
}

19. 非递归实现二叉树的中序遍历

  1. Binary Tree Inorder Traversal (Medium)
  • 解答
public List inorderTraversal(TreeNode root) {
    List ret = new ArrayList<>();
    if (root == null) return ret;
    Stack stack = new Stack<>();
    TreeNode cur = root;
    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        TreeNode node = stack.pop();
        ret.add(node.val);
        cur = node.right;
    }
    return ret;
}

BST

二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。
二叉查找树中序遍历有序。

20. 修剪二叉查找树

  1. Trim a Binary Search Tree (Easy)
  • 20.1 题目
20
  • 20.2 分析解答
    • 递归
      • 要求返回BST被修剪后的根结点,那么我们从根结点开始修剪。
        如果根结点太小,根结点的左子树的所有结点只会更小,说明根结点及其左子树都应该剪掉,因此直接返回右子树的修剪结果。
        如果根结点太大,根结点的右子树的所有结点只会更大,说明根结点及其右子树都应该剪掉,因此直接返回左子树的修剪结果。
        如果根结点没问题,则递归地修剪左子结点和右子结点。
        如果结点为空,说明无需修剪,直接返回空即可。
        左右子结点都修剪完后,返回自身。
public TreeNode trimBST(TreeNode root, int L, int R) {
    if (root == null) return null;
    if (root.val > R) return trimBST(root.left, L, R);
    if (root.val < L) return trimBST(root.right, L, R);
    root.left = trimBST(root.left, L, R);
    root.right = trimBST(root.right, L, R);
    return root;
}

21. 寻找二叉查找树的第k个元素

  1. Kth Smallest Element in a BST (Medium)
  • 21.1 题目
21
  • 21.2 分析解答
  • 中序遍历解法
    • 递归中序遍历
int num = 0;
int res;

public int kthSmallest(TreeNode root, int k) {
    inorderTraversal(root, k);
    return res;
}

private void inorderTraversal(TreeNode node, int k) {
    if (node == null) {
        return;
    }
    inorderTraversal(node.left, k);
    num++;
    if (num == k) {
        res = node.val;
        return;
    }
    inorderTraversal(node.right, k);
}
  • 非递归中序遍历
public int kthSmallest(TreeNode root, int k) {
    Stack stack = new Stack<>();
    int num = 0;
    int res = -1;
    TreeNode cur = root;
    while (cur != null || !stack.isEmpty()) {
        // 节点不为空一直压栈
        while (cur != null) {
            stack.push(cur);
            cur = cur.left; // 考虑左子树
        }
        // 节点为空,就出栈
        cur = stack.pop();
        // 当前值加入
        num++;
        if (num == k) {
            res = cur.val;
            break;
        }
        // 考虑右子树
        cur = cur.right;
    }
    return res;
}
  • 递归法
public int kthSmallest(TreeNode root, int k) {
    int leftCnt = count(root.left);
    if (leftCnt == k - 1) return root.val;
    if (leftCnt > k - 1) return kthSmallest(root.left, k);
    return kthSmallest(root.right, k - leftCnt - 1);
}

private int count(TreeNode node) {
    if (node == null) return 0;
    return 1 + count(node.left) + count(node.right);
}

22. 把二叉查找树每个节点的值都加上比它大的节点的值

Convert BST to Greater Tree (Easy)

  • 22.1 题目
22
  • 22.2 分析解答
  • 递归中序遍历
    • 时间复杂度O(N),空间复杂度O(N)
class Solution {
    private int sum = 0;

    public TreeNode convertBST(TreeNode root) {
        if (root != null) {
            convertBST(root.right);
            sum += root.val;
            root.val = sum;
            convertBST(root.left);
        }
        return root;
    }
}
  • 迭代中序遍历
    • 时间复杂度O(N),空间复杂度O(N)
class Solution {
    public TreeNode convertBST(TreeNode root) {
        Stack stack = new Stack<>();
        TreeNode cur = root;
        int sum = 0;
        //TreeNode pre = new TreeNode(0); //和后面注销的语句一组
        while(cur != null || !stack.empty()){
            while(cur != null) {
                stack.push(cur);
                cur = cur.right;
            }
            cur = stack.pop();
            sum += cur.val;
            cur.val = sum;
            //cur.val += pre.val;
            //pre = cur;
            cur = cur.left;
        }
        return root;
    }
}

23. 二叉查找树的最近公共祖先

  1. Lowest Common Ancestor of a Binary Search Tree (Easy)
  • 23.1 题目
23
  • 23.2 分析解答
    • 时间复杂度:O(N)
      其中 N 为 BST 中节点的个数,在最坏的情况下我们可能需要访问 BST 中所有的节点。
    • 空间复杂度:O(H)
      所需开辟的额外空间主要是递归栈产生的,之所以是 H 是因为 BST 的高度为 H。
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
        if(root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
        return root;
    }
}
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        int pVal = p.val;
        int qVal = q.val;
        TreeNode node = root;
        while (node != null) {
            int parentVal = node.val;
            if (pVal > parentVal && qVal > parentVal) {
                node = node.right;
            } else if (pVal < parentVal && qVal < parentVal) {
                node = node.left;
            } else {
                return node;
            }
        }
        return null;
    }
}

24. 二叉树的最近公共祖先

  1. Lowest Common Ancestor of a Binary Tree (Medium)
  • 24.1 题目
24
  • 24.2 分析解答
    • 采用后序遍历,左右中,最后才判断中,先看中的左右子树是否有p、q两个节点,有的话他就是最近祖宗并返回。
    • 若 root 是 p,q 的 最近公共祖先 ,则只可能为以下情况之一:
      • p 和 q 在 root 的子树中,且分列 root 的 异侧(即分别在左、右子树中);
      • p = root ,且 q 在 root 的左或右子树中;
      • q = root ,且 p 在 root 的左或右子树中;
  • 复杂度分析:
    • 时间复杂度 O(N) : 其中 N 为二叉树节点数;最差情况下,需要递归遍历树的所有节点。
    • 空间复杂度 O(N) : 最差情况下,递归深度达到 N ,系统使用 O(N)大小的额外空间。
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null || root == p || root == q) return root;
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        if(left == null) return right;
        if(right == null) return left;
        return root;
    }
}

25. 从有序数组中构造二叉查找树

  1. Convert Sorted Array to Binary Search Tree (Easy)
  • 25.1 题目
25
  • 25.2 分析解答
  • 递归法
    • 二叉查找树的中序遍历得到的就有有序数组,所以本题可以转化为已知中序遍历结果如何还原二叉树,通过中序遍历加前序遍历或者中序遍历加后序遍历来还原一棵树。前序(后序)遍历的作用呢?提供根节点!然后根据根节点,就可以递归的生成左右子树。平衡二叉树,既然要做到平衡,我们只要把根节点选为数组的中点即可。
    • 时间复杂度:O(N),每个元素只访问一次。
      -空间复杂度:O(N),二叉搜索树空间 O(N),递归栈深度 O(logN)。
public TreeNode sortedArrayToBST(int[] nums) {
    return toBST(nums, 0, nums.length - 1);
}

private TreeNode toBST(int[] nums, int sIdx, int eIdx){
    if (sIdx > eIdx) return null;
    int mIdx = (sIdx + eIdx) / 2;
    TreeNode root = new TreeNode(nums[mIdx]);
    root.left =  toBST(nums, sIdx, mIdx - 1);
    root.right = toBST(nums, mIdx + 1, eIdx);
    return root;
}

26. 根据有序链表构造平衡的二叉查找树

  1. Convert Sorted List to Binary Search Tree (Medium)
  • 26.1 题目
26
  • 26.2 分析解答
  • 递归解答
    • 利用快慢指针找到链表的中点的前一个节点,便于将链表以mid节点为中心分成左右两半,进行递归找到左右半边的pre节点
    • 时间复杂度:O(NlogN)
      对于 N 个元素我们需要 N/2 步得到中间元素。当找到中间元素之后我们将剩下两个大小为 N/2 的子列表,对这两个部分都需要找中间元素的操作,需要时间开销为 2×N/4 步,同理对于更小的列表也是递归的操作。



      每次将列表分成一半,需要 logN 的时间结束。


    • 空间复杂度O(logN)
      要求维护一棵平衡二叉树,所以保证树的高度上界为O(logN)
class Solution {
    public TreeNode sortedListToBST(ListNode head) {
        if(head == null) return null;
        if(head.next == null) return new TreeNode(head.val);
        ListNode pre = helpFindMidPre(head);
        ListNode mid = pre.next;
        pre.next = null; //断开链表以使得左半边的链表寻找pre节点时可以停止
        TreeNode root = new TreeNode(mid.val);
        root.left = sortedListToBST(head);
        root.right = sortedListToBST(mid.next);
        return root;
    }
    //用快慢指针找到链表中间节点的前一个节点
    private ListNode helpFindMidPre(ListNode head){
        ListNode pre = head;
        ListNode low = head;
        ListNode fast = head;
        while(fast != null && fast.next != null) {
            pre = low;
            low = low.next;
            fast = fast.next.next;
        }
        return pre;
    }
}

27.在二叉查找树中寻找两个节点,使它们的和为一个给定值

  1. Two Sum IV - Input is a BST (Easy)
  • 27.1 题目
27
  • 27.2 分析解答
    • 使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。
    • 应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。
public boolean findTarget(TreeNode root, int k) {
    List nums = new ArrayList<>();
    inOrder(root, nums);
    int i = 0, j = nums.size() - 1;
    while (i < j) {
        int sum = nums.get(i) + nums.get(j);
        if (sum == k) return true;
        if (sum < k) i++;
        else j--;
    }
    return false;
}

private void inOrder(TreeNode root, List nums) {
    if (root == null) return;
    inOrder(root.left, nums);
    nums.add(root.val);
    inOrder(root.right, nums);
}

28. 在二叉查找树中查找两个节点之差的最小绝对值

  1. Minimum Absolute Difference in BST (Easy)
  • 28.1 题目
28
  • 28.2 分析解答
  • 利用二叉查找树的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。
  • 暴力解法
class Solution {
    public  int getMinimumDifference(TreeNode root) {
        List nums = new ArrayList<>();
        help(root, nums);
        int res = Integer.MAX_VALUE;
        for(int i = 0; i < nums.size() - 1; i++){
            res = Math.min(res, nums.get(i + 1) - nums.get(i));
        }
        return res;
    }
    private void help(TreeNode root, List nums){
        if(root == null) return ;
        help(root.left, nums);
        nums.add(root.val);
        help(root.right, nums);
    }
}
  • 在中序遍历过程中获得最小值
    • 搜索二叉树的特性,中序遍历是一个升序数组,而最小值的产生一定是在数组中相邻两个元素的差之中,因此,在中序遍历时候抓住前一个数,和当前数字的差 于最小值作比较.
private int minDiff = Integer.MAX_VALUE;
private TreeNode preNode = null;

public int getMinimumDifference(TreeNode root) {
    inOrder(root);
    return minDiff;
}

private void inOrder(TreeNode node) {
    if (node == null) return;
    inOrder(node.left);
    if (preNode != null) minDiff = Math.min(minDiff, node.val - preNode.val);
    preNode = node;
    inOrder(node.right);
}

29. 寻找二叉查找树中出现次数最多的值

  1. Find Mode in Binary Search Tree (Easy)
  • 29.1 题目
29
  • 29.2 分析解答
  • 非递归中序用遍历,用map存储出现次数
class Solution {
    public int[] findMode(TreeNode root) {
        HashMap map = new HashMap<>();
        Stack stack = new Stack<>();
        TreeNode cur = root;
        int max = Integer.MIN_VALUE, temp, count = 0;
        while(cur != null || !stack.isEmpty()){
            while(cur != null){
                stack.push(cur);
                cur = cur.left;
            }
            TreeNode node = stack.pop();
            temp = map.getOrDefault(node.val, 0) + 1;
            map.put(node.val, temp);
            if(temp > max){
                max = temp;
                count = 1;
            }else if(temp == max){
                count++;
            }
            cur = node.right;
        }
        int[] res = new int[count];
        for(Integer key : map.keySet()){
            if(map.get(key) == max){
                res[--count] = key;
            }
        }
        return res;
    }
}
  • 递归
  • 时间复杂度O(N),空间复杂度O(1)。
    private List items;
    private int maxCount;
    private int curCount;
    private TreeNode pre;

    public int[] findMode(TreeNode root) {
        if (root == null)
            return new int[0];
        items = new ArrayList<>();
        // 这里设置为1是因为 在递归中 BST中最左边的结点被跳过了,作为初状态 该结点值出现一次,记录的出现最多次数一开始也为1
        maxCount = 1;
        curCount = 1;
        midTraversal(root);
        // 最右端结点的处理,从递归中看,最后一个结点与前一个结点相等只更新了curCount的值;不相等则只考虑到倒数第二个结点。
        if(curCount > maxCount)
            return new int[]{pre.val};
        if(curCount == maxCount)
            items.add(pre.val);
        int[] res = new int[items.size()];
        for (int i = 0; i < res.length; i++)
            res[i] = items.get(i);
        return res;
    }

    private void midTraversal(TreeNode x) {
        if (x == null) return;
        midTraversal(x.left);
        if(pre != null){
            if(x.val != pre.val){ // 说明上一个值的结点数量已经统计完成 要看出现次数的情况
                if(curCount > maxCount){ // 出现次数更多,清空之前的出现少的数,更新最大出现次数
                    maxCount = curCount;
                    items.clear();
                    items.add(pre.val);
                }
                else if(curCount == maxCount){ // 不止一个众数
                    items.add(pre.val);
                }
                curCount = 1; // 当前值与上一个结点值不同,重置计数
            }
            else curCount++; // 当前值与上一个结点值相同,当前值的出现次数增加。
        }
        pre = x;
        midTraversal(x.right);
    }

Trie字典树


Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。

30. 实现一个Trie

  1. Implement Trie (Prefix Tree) (Medium)
  • 30.1 题目
30
  • 30.2 分析解答
class Trie {

    private class Node {
        Node[] childs = new Node[26];
        boolean isLeaf;
    }

    private Node root = new Node();

    public Trie() {
    }

    public void insert(String word) {
        insert(word, root);
    }

    private void insert(String word, Node node) {
        if (node == null) return;
        if (word.length() == 0) {
            node.isLeaf = true;
            return;
        }
        int index = indexForChar(word.charAt(0));
        if (node.childs[index] == null) {
            node.childs[index] = new Node();
        }
        insert(word.substring(1), node.childs[index]);
    }

    public boolean search(String word) {
        return search(word, root);
    }

    private boolean search(String word, Node node) {
        if (node == null) return false;
        if (word.length() == 0) return node.isLeaf;
        int index = indexForChar(word.charAt(0));
        return search(word.substring(1), node.childs[index]);
    }

    public boolean startsWith(String prefix) {
        return startWith(prefix, root);
    }

    private boolean startWith(String prefix, Node node) {
        if (node == null) return false;
        if (prefix.length() == 0) return true;
        int index = indexForChar(prefix.charAt(0));
        return startWith(prefix.substring(1), node.childs[index]);
    }

    private int indexForChar(char c) {
        return c - 'a';
    }
}

31.实现一个 Trie,用来求前缀和

  1. Map Sum Pairs (Medium)
  • 31.1 题目
    31
  • 31.2 分析解答
class MapSum {

    private class Node {
        Node[] child = new Node[26];
        int value;
    }

    private Node root = new Node();

    public MapSum() {

    }

    public void insert(String key, int val) {
        insert(key, root, val);
    }

    private void insert(String key, Node node, int val) {
        if (node == null) return;
        if (key.length() == 0) {
            node.value = val;
            return;
        }
        int index = indexForChar(key.charAt(0));
        if (node.child[index] == null) {
            node.child[index] = new Node();
        }
        insert(key.substring(1), node.child[index], val);
    }

    public int sum(String prefix) {
        return sum(prefix, root);
    }
    
    //找到prefix最后停下的节点,则以prefix为前缀的所有值的和为该节点的子节点的值的总和
    //递归获得该节点往下的所有节点,并对value累加
    private int sum(String prefix, Node node) {
        if (node == null) return 0;
        if (prefix.length() != 0) { //找到prefix的最后停下的长度
            int index = indexForChar(prefix.charAt(0));
            return sum(prefix.substring(1), node.child[index]);
        }
        // 能进行到这说明prefix一定为空,接下去就是找单次终点的节点值
        int sum = node.value;
        for (Node child : node.child) {
            sum += sum(prefix, child);
        }
        return sum;
    }

    private int indexForChar(char c) {
        return c - 'a';
    }
}

你可能感兴趣的:(树)