二叉树总结

节点定义

Java

public class TreeNode {
    // 都有默认的零值
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode() {}
    public TreeNode(int val) { this.val = val; }
    public TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}
function TreeNode(val) {
    this.val = val;
    this.left = null;
    this.right = null;
}

遍历方式

深度优先遍历

  • 前序遍历(迭代:Stack + while 先右后左入栈)
  • 中序遍历(迭代:Stack + 沿左深入栈,左空右替换)
  • 后序遍历(迭代:同前序,但先左后右入栈,最后反转序列)(迭代:Stack + prev,左完,看 prev 是否为中右,是则中右入栈,否则中出栈访问)

前序遍历 - 迭代法

public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();

        if (root == null) return res;

        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val);

            // 前序遍历的迭代法不让 null 入栈
            if (node.right != null) {
                stack.push(node.right);
            }

            if (node.left != null) {
                stack.push(node.left);
            }
        }

        return res;
    }

中序遍历 - 迭代法

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
         List<Integer> res = new LinkedList<>();
        Stack<TreeNode> st = new Stack<>();

        // 其实直接用 root 遍历也行
        TreeNode curr = root;

        while (curr != null || !st.isEmpty()) {
            if (curr != null) {
                // 中序遍历的迭代法也不让 null 入栈
                st.push(curr);
                curr = curr.left;
            } else {
                TreeNode top = st.pop();
                res.add(top.val);
                if (top.right != null) {
                    curr = top.right;
                }
            }
        }

        return res;
    }
}

后序遍历 - 迭代法

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> res = new LinkedList<>();
    Stack<TreeNode> st = new Stack<>();

    if (root == null) return res;

    // 当前遍历的节点
    TreeNode curr = root;
    // 上次遍历的节点
    TreeNode prev = null;

    while (curr != null || !st.isEmpty()) {
        if (curr != null) {
            // 一路向左,不为空则进栈
            // 后序遍历也不让 null 入栈
            st.push(curr);
            curr = curr.left;
        } else {
            // 左到头了,看看栈顶
            TreeNode top = st.peek();
            // 栈顶元素有右孩子
            if (top.right != null) {
                // 如果右孩子不是上次访问的节点
                if (top.right != prev) {
                    // 则以右孩子做为新子树的根,继续遍历
                    curr = top.right;
                } else {
                    // 如果右孩子是上次访问的节点,说明该访问栈顶元素了
                    res.add(top.val);
                    st.pop();
                    // 记录 prev
                    prev = top;
                }
            } else {
                // 栈顶元素没有右孩子,则直接访问栈顶元素
                res.add(top.val);
                st.pop();
                // 记录 prev
                prev = top;
            }
        }
    }

    return res;
}

广度优先遍历

  • 层序遍历(迭代:Queue + 按个遍历、用 size 按层遍历)

二叉树的属性

高度和深度

后序求高度,前序求深度。

对于节点来说,高度是从该节点到叶子节点的最大路径长度,深度是从根节点到该节点的路径长度。
对于二叉树来说,高度和深度是一样的,高度从根节点算起。

所以对于 104.二叉树的最大深度111.二叉树的最小深度 要使用后序遍历,先处理左右子树的深度。

// 求最大深度
class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        int ld = maxDepth(root.left);
        int rd = maxDepth(root.right);
        return Math.max(ld, rd) + 1;
    }
}

// 求最小深度
public int minDepth(TreeNode root) {
        if (root == null) return 0;

        int ld = minDepth(root.left);
        int rd = minDepth(root.right);

        // 如果当前节点左孩子为空,右孩子不为空,那么当前节点的最小深度就是右子树的最小深度 +1
        if (root.left == null && root.right != null) {
            return 1 + rd;
        }

        // 如果当前节点右孩子为空,左孩子不为空,那么当前节点的最小深度就是左子树的最小深度 +1
        if (root.left != null && root.right == null) {
            return 1 + ld;
        }

        // 如果左右孩子都不为空,则就取左右子树最小深度的较小者 +1
        return Math.min(ld, rd) + 1;
    }

求节点数量

222. 完全二叉树的节点个数

后序遍历,返回左右子树的节点数量之和 +1。

是否平衡

110. 平衡二叉树

后序遍历,看左右子树的高度差是否小于等于 1。下面有个技巧,函数既能返回高度,也能返回是否是平衡二叉树的标志,不是则及时返回,不再深入递归下去。

public boolean isBalanced(TreeNode root) {
    // 不等于 -1,说明是平衡的
    return getHeightX(root) != -1;
}

public int getHeightX(TreeNode root) {
    if (root == null) return 0;

    int lh = getHeightX(root.left);

    // 有一个子树不平衡,则整个树都不是平衡二叉树,用 -1 标志,及时返回
    if (lh == -1) return -1;

    int rh = getHeightX(root.right);

    // 有一个子树不平衡,则整个树都不是平衡二叉树,用 -1 标志,及时返回
    if (rh == -1) return -1;

    // 高度差大于 1,则不是平衡二叉树
    if (Math.abs(lh - rh) > 1) return -1;

    return Math.max(lh, rh) + 1;
}

找所有路径

257. 二叉树的所有路径

前序,涉及到回溯的知识。先修改相关标志,再递归处理,然后再撤回相关标志的修改。

public List<String> binaryTreePaths(TreeNode root) {
    List<String> res = new LinkedList<>();
    helper(root, "", res);
    return res;
}

public void helper(TreeNode root, String path, List<String> res) {
    if (root == null) return;

    // 进来就是访问,先拼接访问路径
    path += root.val;

    // 到达叶子节点,放入结果集
    if (root.left == null && root.right == null) {
        res.add(path);
        return;
    }

    // 否则继续深入遍历,这里的 path + "->" 就是深入
    // 而深入递归完,path 没变,就是回溯
    helper(root.left, path + "->", res);
    helper(root.right, path + "->", res);
}

路经总和

112. 路径总和

和遍历顺序无关,涉及到了回溯,找到答案立即返回即可。

public boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) return false;

    if (root.left == null && root.right == null && root.val == targetSum) return true;

    if (hasPathSum(root.left, targetSum - root.val)) return true;
    if (hasPathSum(root.right, targetSum - root.val)) return true;

    return false;
}

二叉树的修改与构造

反转二叉树

226. 翻转二叉树

前序、后序遍历,交换节点即可。

构造二叉树

106. 从中序与后序遍历序列构造二叉树

前序 + 中序、后序 + 中序。核心思路就是根据前序或者后序找到根节点在中序中的位置,然后划分区间,递归构造。

构造最大的二叉树

654. 最大二叉树

指定一个序列(非前中后序序列),要求构造二叉树。一般采用前序遍历的方式,然后递归构造左右子树。

这里区间内最大值的查找采用了一次遍历的方式。

合并二叉树

617. 合并二叉树

这里开始同时遍历两棵树,即函数中传递两棵树的节点,采用一致的顺序遍历。

public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
    if (root1 == null) return root2;
    if (root2 == null) return root1;

    // root1、root2 都不为空,这里原地修改 root1,root2 也行,不用创建新节点
    root1.val += root2.val;
    root1.left = mergeTrees(root1.left, root2.left);
    root1.right = mergeTrees(root1.right, root2.right);

    return root1;
}

二叉搜索树的属性

二叉搜索树要记住的关键词:有序、中序、左边小于中间,中间小于右边、划区间(选方向)深入、双指针。

二叉搜索树中的搜索

700. 二叉搜索树中的搜索

// 迭代法
public TreeNode searchBST(TreeNode root, int val) {
    // 划区间,选方向深入
    // 直接修改 root 即可
    while (root != null) {
        if (val < root.val) {
            root = root.left;
        } else if (val > root.val) {
            root = root.right;
        } else {
            return root;
        }
    }
    return null;
}

是否为二叉搜索树

98. 验证二叉搜索树

中序遍历,双指针技巧,记住上一个遍历的节点。根据二叉搜索树的有序性,中序遍历中上一个节点的值一定比当前节点的值小。

// 记录上一个遍历的节点
private TreeNode prev;
public boolean isValidBST(TreeNode root) {
    if (root == null) return true;

    // 左子树是不是 BST,不是就返回 false
    boolean lb = isValidBST(root.left);
    if (!lb) return false;

    // 上个节点的值是否大于当前节点,是则失序,返回 false
    if (prev != null && prev.val >= root.val) return false;
    // root 作为 prev,开始遍历右子树
    prev = root;

    // 右子树是不是 BST,不是就返回 false
    boolean rb = isValidBST(root.right);
    if (!rb) return false;

    return true;
}

二叉搜索树的最小绝对差

530. 二叉搜索树的最小绝对差

还是借助有序性(中序),最小的差一定在相邻节点之间。

private int min = Integer.MAX_VALUE;
private TreeNode prev;

public int getMinimumDifference(TreeNode root) {
    traversal(root);
    return min;
}

public void traversal(TreeNode root) {
    if (root == null) return;
    getMinimumDifference(root.left);
    if (prev != null) {
        int diff = root.val - prev.val;
        if (diff < min) {
            min = diff;
        }
    }
    prev = root;
    getMinimumDifference(root.right);
}

二叉搜索树的众数

501. 二叉搜索树中的众数

出现次数最多的树,众数可能有多个,一般思路是拿 Map 记录节点出现的次数,两次遍历,一次是找最大值,一次是将和最大值相等的节点记录在案。但这样的方法对什么树都有效,白瞎了二叉搜索树的特性。

// 记录最大次数
private int maxCount;
// 记录当前次数,每个节点都可使用
private int count;
// 记录上一个节点
private TreeNode prev;
// 存储结果
private List<Integer> list;

public int[] findMode(TreeNode root) {
    maxCount = Integer.MIN_VALUE;
    count = 0;
    prev = null;
    list = new LinkedList<>();

    traversal(root);

    // 将结果放入数组
    int[] res = new int[list.size()];
    int i = 0;
    for (int e: list) {
        res[i++] = e;
    }

    return res;
}

public void traversal(TreeNode root) {
    if (root == null) return;

    traversal(root.left);

    if (prev == null) {
        // 第一个节点,频率记为 1
        count = 1;
    } else if (prev.val == root.val) {
        // 重复的节点,频率 +1
        count++;
    } else {
        // 新节点,频率重置为 1(多个节点公用 count 的关键)
        count = 1;
    }

    // 和当前最大频率相等,放进预结果列表
    if (count == maxCount) {
        list.add(root.val);
    }

    // 频率更大,更新 maxCount,
    // 之前的预结果列表作废,因为有了频率更大的,并将最新的大频率值放进预结果列表
    if (count > maxCount) {
        maxCount = count;
        list.clear();
        list.add(root.val);
    }

    prev = root;

    traversal(root.right);
}

二叉搜索树转成累加树

538. 把二叉搜索树转换为累加树

借助有序性(右边比中间大),逆中序遍历。

private int prevVal;
public TreeNode convertBST(TreeNode root) {
    if (root == null) return null;

    // 先遍历右子树
    convertBST(root.right);
    // 将上一个节点的值加上
    root.val += prevVal;
    // 当前节点变为 prev
    prevVal = root.val;
    // 开始遍历左子树
    convertBST(root.left);

    return root;
}

二叉搜索树的修改与构造

插入

701. 二叉搜索树中的插入操作

插入节点都是作为叶子节点,借助有序性,选择正确的方向深入下去,找到叶子节点并插在下面。

// 递归法
public TreeNode insertIntoBST(TreeNode root, int val) {
    // 往空树插入节点
    if (root == null) return new TreeNode(val);

    if (val < root.val) {
        if (root.left != null) {
            // 如果有左子树,则继续递归
            root.left = insertIntoBST(root.left, val);
        } else {
            // 否则作为左子树
            root.left = new TreeNode(val);
        }
    } else if (val > root.val) {
        if (root.right != null) {
            // 如果有右子树,则继续递归
            root.right = insertIntoBST(root.right, val);
        } else {
            // 否则作为右子树
            root.right = new TreeNode(val);
        }
    }

    return root;
}

// 迭代法
public TreeNode insertIntoBST(TreeNode root, int val) {
    if (root == null) return new TreeNode(val);

    // 记录父节点
    TreeNode parent = null;
    TreeNode curr = root;

    while (curr != null) {
        // 每次遍历更新父节点
        parent = curr;
        if (curr.val < val) {
            curr = curr.right;
        } else {
            curr = curr.left;
        }
    }

    // 挂载节点
    if (val < parent.val) {
        parent.left = new TreeNode(val);
    } else {
        parent.right = new TreeNode(val);
    }

    return root;
}

删除

450. 删除二叉搜索树中的节点

else 里的代码值得反复观看。写代码前先探讨可能的情况,不要一股脑的就进行递归。

public TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;

    if (key < root.val) {
        root.left = deleteNode(root.left, key);
    } else if (key > root.val) {
        root.right = deleteNode(root.right, key);
    } else {
        // root 就是待删除的节点
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        // 左右均不为空,则寻找左子树中的最大值
        TreeNode curr = root.left;
        while (curr.right != null) {
            curr = curr.right;
        }
        // 将待删除节点的右子树接到最大值节点的右子树上
        curr.right = root.right;
        // 既然 root.right 已经没了,那么新 root 就是老 root 的左子树
        root = root.left;
    }

    return root;
}

修剪

669. 修剪二叉搜索树

借助二叉搜索树的有序性,如果当前节点的值小于下界,则当前节点及其左子树都不用考虑,如果当前节点的值大于上界,则当前节点及其右子树都不用考虑。

public TreeNode trimBST(TreeNode root, int low, int high) {
    if (root == null) return null;

    // root 不在区间内,直接返回,换根
    if (root.val < low) {
        return trimBST(root.right, low, high);
    } else if (root.val > high) {
        return trimBST(root.left, low, high);
    }

    // root 在区间内,保留,查看左右子树
    root.left = trimBST(root.left, low, high);
    root.right = trimBST(root.right, low, high);

    return root;
}

构造

108. 将有序数组转换为二叉搜索树

根据有序的节点序列,构造二叉搜索树,要求平衡。从数组中间开始(保证平衡),选节点作为根节点,然后两边递归构造。

public TreeNode sortedArrayToBST(int[] nums) {
    return traversal(nums, 0, nums.length - 1);
}

public TreeNode traversal(int[] nums, int l, int r) {
    if (l > r) return null;
    // 优化,如果 l、r 相等,则直接生成节点并返回,不用再往下递归了
    if (l == r) return new TreeNode(nums[l]);
    int mid = (l + r) / 2;
    TreeNode node = new TreeNode(nums[mid]);
    node.left = traversal(nums, l, mid - 1);
    node.right = traversal(nums, mid + 1, r);
    return node;
}

二叉树公共祖先问题

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

左子树出现目标值,右子树出现目标值,则 root 就是最近公共祖先。

235. 二叉搜索树的最近公共祖先

借助有序性,root 在区间内,root 就是最近公共祖先。

代码碎片

求节点的高度

// 后序遍历
int getHeight(TreeNode node) {
    if (node == null) return 0;

    int lh = getHeight(node.left);
    int rh = getHeight(node.right);

    return Integer.max(lh, rh) + 1;
}

求平衡二叉树的高度,如果不是平衡二叉树,则返回 -1

// 后序遍历
int getHeight(TreeNode node) {
    if (root == null) return 0;

    int lh = getHeight(node.left);
    if (lh == -1) return -1;
    int rh = getHeight(node.right);
    if (rh == -1) return -1;

    if (Math.abs(lh - rh) > 1) return -1;

    return Integer.max(lh, rh) + 1;
}

层序遍历

void layer(TreeNode root) {
    if (root == null) return;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    while (!q.isEmpty()) {
        TreeNode node = q.poll();
        // 处理当前 node
        if (node.left != null) {
            q.offer(node.left);
        }
        if (node.right != null) {
            q.offer(node.right);
        }
    }
}

上面的方法每次循环只处理一个节点,下面的方法每次循环处理一层的节点。

void layer(TreeNode root) {
    if (root == null) return;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    while (!q.isEmpty()) {
        // size 就是当前层的节点数
        int size = q.size();
        // 一次处理一层
        for (int i = 0; i < size; i++) {
            TreeNode node = q.poll();
            // 处理 node
            if (node.left != null) {
                q.offer(node.left);
            }
            if (node.right != null) {
                q.offer(node.right);
            }
        }
    }
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注