剑指Offer(专项突破版)刷题笔记 | 第八章 树

214 阅读12分钟

树的基本知识

常常考察树的遍历

二叉树节点的数据类型定义

public class TreeNode{
    int val;
    TreeNode left;
    TreeNode right;
    
    TreeNode(int val){
    this.val =val;
    }
}

二叉树的深度优先搜索

中序遍历

先遍历二叉树的左子树,然后遍历二叉树的根结点,最后遍历二叉树的右子树

递归实现

  • 代码简单
  • 对于深度大的二叉树可能存在栈溢出的问题
public List<Integer> inorderTraversal(TreeNode root){
    List<Integer> nodes = new List<>();
    dfs(root,nodes);
    return nodes;
}

public void dfs(TreeNode root,List nodes){
    if(root != null){
        dfs(root.left,nodes);
        nodes.add(root.val);
        dfs(root.right,nodes);
    }
}

迭代实现

  • 需要用到栈
public List<Integer> inorderTravarsal(TreeNode root){
    List<Integer> nodes = new List<>();
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(cur != null || !stack.empty()){
        while(cur != null){
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();
        nodes.add(cur.val);
        cur = cur.right;
    }
    return nodes;
}

前序遍历

先遍历二叉树的根结点,然后遍历二叉树的左子树,最后遍历二叉树的右子树

递归实现

public List<Integer> inorderTraversal(TreeNode root){
    List<Integer> nodes = new List<>();
    dfs(root,nodes);
    return nodes;
}

public void dfs(TreeNode root,List nodes){
    if(root != null){
        nodes.add(root.val);
        dfs(root.left,nodes);
        dfs(root.right,nodes);
    }
}

迭代实现

public List<Integer> inorderTravarsal(TreeNode root){
    List<Integer> nodes = new List<>();
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(cur != null || !stack.empty()){
        while(cur != null){
            nodes.add(cur.val);
            stack.push(cur);//保存节点
            cur = cur.left;
        }
        cur = stack.pop();  
        cur = cur.right;
    }
    return nodes;
}

后序遍历

先遍历二叉树的左子树,然后遍历二叉树的右子树,最后遍历二叉树的根结点

递归实现

public List<Integer> inorderTraversal(TreeNode root){
    List<Integer> nodes = new List<>();
    dfs(root,nodes);
    return nodes;
}

public void dfs(TreeNode root,List nodes){
    if(root != null){
        dfs(root.left,nodes);
        dfs(root.right,nodes);
        nodes.add(root.val);
    }
}

迭代实现

public List<Integer> inorderTravarsal(TreeNode root){
    List<Integer> nodes = new List<>();
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    TreeNode prev = null;
    while(cur != null || !stack.empty()){
        while(cur != null){
            stack.push(cur);//保存节点
            cur = cur.left;
        }
        cur = stack.peek();
        if(cur.right != null && cur.right != prev){//有右子节点,且之前没有遍历过右子树
            cur = cur.right;
        }else{//遍历当前节点
            stack.pop();
            nodes.add(cur.val);
            prev = cur;
            cur = null;
        }
    }
    return nodes;
}

小结

  • 时间复杂度都是O(n)O(n),空间复杂度都是O(h)O(h)(深度)
  • 前序遍历一边顺着指向左子节点的指针移动一边遍历当前节点并入栈
  • 中序、后序遍历顺着指向左子节点的指针移动时将节点入栈,不遍历
  • 后序遍历需要判断前一个遍历的节点是否为当前节点的右子节点来决定是否可以遍历当前节点

Q47:二叉树剪枝

题目(中等):给定一个二叉树 根节点 root ,树的每个节点的值要么是 0,要么是 1。请剪除该二叉树中所有节点的值为 0 的子树。

节点 node 的子树为 node 本身,以及所有 node 的后代。

示例 1:

输入: [1,null,0,0,1]
输出: [1,null,0,null,1] 
解释: 只有红色节点满足条件“所有不包含 1 的子树”。

示例 2:

输入: [1,0,1,0,0,0,1]
输出: [1,null,1,null,1]

解题思路

每遍历一个节点就判断它是否有左右子树,如果左右子树为空,且节点值是0,就可以删除这个节点,先左右,后根结点属于后序遍历

public TreeNode pruneTree(TreeNode root) {
    if(root == null){return null;}
    root.left = pruneTree(root.left);
    root.right = pruneTree(root.right);
    if((root.left == null && root.right == null && root.val == 0)){
        return null;
    }
    return root;
}

Q48:序列化和反序列化二叉树

题目(困难):序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

示例 1:

输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]

示例 2:

输入:root = []
输出:[]

解题思路

  • 序列化时按前序遍历,遇到一个节点输出一个
  • 反序列化时由上至下构建树
// 序列化
public String serialize(TreeNode root) {
    if(root == null ) return "#";

    String leftStr = serialize(root.left);
    String rightStr = serialize(root.right);

    return String.valueOf(root.val) + "," + leftStr + "," + rightStr;
}

// 反序列化
public TreeNode deserialize(String data) {
    String [] nodeStrs = data.split(",");
    int [] i = {0};//引用传递
    return dfs(nodeStrs,i);
}

private TreeNode dfs(String[] nodeStrs,int[] i){
    String  str = nodeStrs[i[0]];
    i[0]++;
    if(str.equals("#")) return null;
    TreeNode node = new TreeNode(Integer.valueOf(str));
    
    node.left = dfs(nodeStrs,i);
    node.right = dfs(nodeStrs,i);
    return node;
}

Q49:从根节点到叶节点的路径数字之和

题目(中等):给定一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。 每条从根节点到叶节点的路径都代表一个数字:例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。 计算从根节点到叶节点生成的 所有数字之和 。

示例 1:

输入:root = [1,2,3]
输出:25
解释:从根到叶子节点路径 1->2 代表数字 12;从根到叶子节点路径 1->3 代表数字 13;因此,数字总和 = 12 + 13 = 25

示例 2:

输入:root = [4,9,0,5,1]
输出:1026
解释:从根到叶子节点路径 4->9->5 代表数字 495;从根到叶子节点路径 4->9->1 代表数字 491;从根到叶子节点路径 4->0 代表数字 40;因此,数字总和 = 495 + 491 + 40 = 1026
public int sumNumbers(TreeNode root) {
    return dfs(root,0);
}
public int dfs(TreeNode root,int result){
    //如果是空的就返回0
    if(root == null){return 0;}

    result = result*10 + root.val;
    //访问到叶节点是返回结果
    if(root.left == null && root.right == null){
        return result;
    }
    return dfs(root.left,result) + dfs(root.right,result);
}

Q50:向下的路径节点值之和

题目(中等):给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例 1:

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。

示例 2:

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

解题思路

  • 在移动的路径上把所有累加的节点值都(可以用map)保存下来
public int pathSum(TreeNode root, int targetSum) {
    Map<Integer,Integer> map = new HashMap<>();
    map.put(0,1);
    return dfs(root,targetSum,map,0);
}

public int dfs(TreeNode root,int sum,Map<Integer,Integer> map,int path){
    if(root == null){
        return 0;
    }

    path += root.val;
    int count = map.getOrDefault(path - sum,0);
    map.put(path,map.getOrDefault(path,0)+1);

    count += dfs(root.left,sum,map,path);
    count += dfs(root.right,sum,map,path);
    //返回上一个节点时,现在的这条path也就返回了,所以要-1
    map.put(path,map.get(path) - 1);
    return count;
}

Q50:节点值之和最大的路径

题目(困难):路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。路径和 是路径中各节点值的总和。给定一个二叉树的根节点 root ,返回其 最大路径和,即所有路径上节点值之和的最大值。

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

解题思路

  • 先求出左右子树中路径节点值之和的最大值
  • 再求出经过根结点的路径节点值之和的最大值
  • 记录下最大值,返回左右子树的最大值+当前节点值
public int maxPathSum(TreeNode root) {
    int [] maxSum = {Integer.MIN_VALUE};
    dfs(root,maxSum);
    return maxSum[0];
}

public int dfs(TreeNode root,int [] maxSum){
    if(root == null){
        return 0;
    }

    int [] maxLeftSum = {Integer.MIN_VALUE};
    int left = Math.max(0,dfs(root.left,maxLeftSum));

    int [] maxRightSum = {Integer.MIN_VALUE};
    int right = Math.max(0,dfs(root.right,maxRightSum));

    maxSum[0] = Math.max(maxLeftSum[0],maxRightSum[0]);
    maxSum[0] = Math.max(maxSum[0],root.val + left + right);

    return root.val + Math.max(left,right);
}

二叉搜索树

左子节点总是小于或等于根结点,右子节点总是大于或等于根结点。中序遍历是解题最常用的思路,因为由于二叉搜索树如前所述的特点,正好是增序遍历。

public TreeNode searchBST(TreeNode root,int val){
    TreeNode cur = root;
    while(cur != null){
        if(cur.val == val){break;}
        
        if(cur.val < val){
            cur = cur.right;
        }else{
            cur = cur.left;
        }
    }
    return cur;
} 

Q52:展开二叉搜索树

题目(简单):给你一棵二叉搜索树,请 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。

示例 1:

输入:root = [5,3,6,2,4,null,8,1,null,null,null,7,9]
输出:[1,null,2,null,3,null,4,null,5,null,6,null,7,null,8,null,9]

示例 2:

输入:root = [5,1,7]
输出:[1,null,5,null,7]

解题思路

中序遍历,只是需要将便利到的前一个节点的右指针指向当前节点

public TreeNode increasingBST(TreeNode root) {
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    TreeNode prev = null;
    TreeNode first = null;
    while(cur != null || !stack.isEmpty()){
        while(cur != null){
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();
        if(prev != null){
            prev.right = cur;
        }else{
            first = cur;
        }
        prev = cur;
        cur.left = null;
        cur = cur.right;
    }
    return first;
}

Q53:二叉搜索树的中序后继

题目(中等):给定一棵二叉搜索树和其中的一个节点 p ,找到该节点在树中的中序后继。如果节点没有中序后继,请返回 null 。节点 p 的后继是值比 p.val 大的节点中键值最小的节点,即按中序遍历的顺序节点 p 的下一个节点

示例 1:

输入:root = [2,1,3], p = 1
输出:2
解释:这里 1 的中序后继是 2。请注意 p 和返回值都应是 TreeNode 类型。

示例 2:

输入:root = [5,3,6,2,4,null,null,1], p = 6
输出:null
解释:因为给出的节点没有中序后继,所以答案就返回 null 了。

解题思路

中序遍历,找到之后置true,O(n)O(n)

public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    boolean found = false;
    while (cur != null || !stack.isEmpty()){
        while(cur != null){
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();
        if(found){
            break;
        }else if(cur == p){
            found = true;
        }
        cur = cur.right;
    }
    return cur;
}

层序遍历,O(h)O(h)

public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
    TreeNode cur = root;
    TreeNode result = null;
    while (cur != null){
        if (cur.val > p.val){
            result = cur;
            cur = cur.left;
        }else{   
            cur = cur.right;
        }
    }
    return result;
}

Q54:所有大于等于节点的值之和

题目(中等):给定一个二叉搜索树,请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。

示例 1:

输入:root = [4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]

解题思路

先遍历右子树的中序遍历(颠倒的中序遍历)

public TreeNode convertBST(TreeNode root) {
    Stack<TreeNode> stack = new Stack<>();
    int sum = 0;
    TreeNode cur = root;
    while (cur != null || !stack.isEmpty()){
        while(cur != null){
        stack.push(cur);
        cur = cur.right;
        }
        cur = stack.pop();
        sum += cur.val;
        cur.val = sum;
        cur = cur.left;
    }   
    return root;
}

Q54:二叉搜索树迭代器

题目(中等):实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 falseint next()将指针向右移动,然后返回指针处的数字。

示例:

输入
inputs = ["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
inputs = [[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出
[null, 3, 7, true, 9, true, 15, true, 20, false]

解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next();    // 返回 3
bSTIterator.next();    // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 20
bSTIterator.hasNext(); // 返回 False
class BSTIterator {
    TreeNode cur;
    Stack<TreeNode> stack;
        
    public BSTIterator(TreeNode root) {
        cur = root;
        stack = new Stack<>();
    }
    
    public int next() {
        while(cur != null){
            stack.push(cur);
            cur = cur.left;
        }

        cur = stack.pop();
        int val = cur.val;
        cur = cur.right;
    
        return val;
    }
    
    public boolean hasNext() {
        return cur != null || !stack.isEmpty();
    }
}

Q56:二叉搜索树中两个节点的值之和

题目(简单):给定一个二叉搜索树的 根节点 root 和一个整数 k , 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 k 。假设二叉搜索树中节点的值均唯一。

示例 1:

输入: root = [8,6,10,5,7,9,11], k = 12
输出: true
解释: 节点 5 和节点 7 之和等于 12

示例 2:

输入: root = [8,6,10,5,7,9,11], k = 22
输出: false
解释: 不存在两个节点值之和为 22 的节点

解题思路

利用双指针分别从前后遍历

class BSTIteratorReversed{
    TreeNode cur;
    Stack<TreeNode> stack;

    public BSTIteratorReversed(TreeNode root){
        cur = root;
        stack = new Stack<>();
    }

    public boolean hasPrev(){
        return cur != null || !stack.isEmpty();
    }

    public int prev(){
        while(cur != null){
            stack.push(cur);
            cur = cur.right;
        }
        cur = stack.pop();
        int result = cur.val;
        cur = cur.left;

        return result;
    }
}

class Solution {
    public boolean findTarget(TreeNode root, int k) {
        if(root ==null) return false;
        BSTIterator bst = new BSTIterator(root);
        BSTIteratorReversed bstr = new BSTIteratorReversed(root);

        int next = bst.next();
        int prev = bstr.prev();

        while(next != prev){
            if(next + prev == k) return true;
            if (next + prev > k){
                prev = bstr.prev();
            }else{
                next = bst.next();
            }
        }
        return false;
    }
}

TreeSet和TreeMap的应用

TreeSet和TreeMap可以保证其内部的二叉搜索树是平衡的

Q57:值和下标之差都在给定的范围内

题目(中等):给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。

示例 1:

输入:nums = [1,2,3,1], k = 3, t = 0
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1, t = 2
输出:true

示例 3:

输入:nums = [1,5,9,1,5,9], k = 2, t = 3
输出:false

查看离当前值最近的左右两个值是否在t范围内 O(nlogk)O(nlogk)

public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
    TreeSet<Long> set = new TreeSet<>();
    for (int i = 0;i < nums.length;i++){
        Long lower = set.floor((long)nums[i]);
        if(lower !=null && lower >= (long)nums[i] - t){
            return true;
        }

        Long upper = set.ceiling((long)nums[i]);
        if(upper != null && upper - (long)nums[i] <= t){
            return true;
        }

        set.add((long)nums[i]);
        if(i >= k){
            set.remove((long)nums[i-k]);
        }
    }
    return false;
}

考虑下标范围为k的值有没有放在同一个桶里的,或者有没有在相邻桶里满足要求的,O(n)O(n)

public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
    Map<Integer,Integer> buckets = new HashMap<>();
    int bucketsize = t + 1;
    for (int i = 0;i < nums.length;i++){
        int num = nums[i];
        int id = getBucketID(num,bucketsize);

        if(buckets.containsKey(id) 
        || (buckets.containsKey(id - 1) && buckets.get(id - 1) + t >= num)
        || (buckets.containsKey(id + 1) && buckets.get(id + 1) - t <= num)) return true;

        buckets.put(id,num);
        if(i >= k){
            buckets.remove(getBucketID(nums[i-k],bucketsize));
        }
    }
    return false;
}

private int getBucketID(int num,int bucketsize){
    return num >= 0 ? num / bucketsize : (num + 1) / bucketsize-1;
}

Q58:日程表

题目(中等):请实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内没有其他安排,则可以存储这个新的日程安排。MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为,  start <= x < end。当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。每次调用 MyCalendar.book方法时,如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。

请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

示例:

输入:
["MyCalendar","book","book","book"]
[[],[10,20],[15,25],[20,30]]
输出: [null,true,false,true]
解释: 
MyCalendar myCalendar = new MyCalendar();
MyCalendar.book(10, 20); // returns true 
MyCalendar.book(15, 25); // returns false ,第二个日程安排不能添加到日历中,因为时间 15 已经被第一个日程安排预定了
MyCalendar.book(20, 30); // returns true ,第三个日程安排可以添加到日历中,因为第一个日程安排并不包含时间 20 
class MyCalendar {
    private TreeMap<Integer,Integer> events;
    public MyCalendar() {
        events = new TreeMap<>();
    }
    
    public boolean book(int start, int end) {
        Map.Entry<Integer,Integer> event = events.floorEntry(start);
        if(event != null && event.getValue() > start) return false;//event.getValue()前一件事的end

        event = events.ceilingEntry(start);
        if(event != null && event.getKey() < end) return false;//event.getKey()后一件事的start

        events.put(start,end);
        return true;
    }
}

补充

递归

  1. 基准情形
  2. 不断推进。递归调用必须朝着基准的情形推进

尾递归:本质就是不需返回,直接不断向下计算,将每一步计算结果传入下一层,这样最后一层就包含了前面所有层的结果,最后一层的结果就是需要的输出。

  1. 确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

  2. 确定终止条件:写完了递归算法,  运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

  3. 确定单层递归的逻辑:确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

以下以前序遍历为例

  1. 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
public void dfs(TreeNode root,List nodes)
  1. 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,这里反过来设定继续递归的条件,代码如下:
if(root != null)
  1. 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
    nodes.add(root.val);
    dfs(root.left,nodes);
    dfs(root.right,nodes);

引文:二叉树:一入递归深似海,从此offer是路人 (qq.com)