美文网首页Algorithm
Algorithm小白入门 -- 二叉树

Algorithm小白入门 -- 二叉树

作者: 开心wonderful | 来源:发表于2021-09-03 15:58 被阅读0次

二叉树

  • 二叉树
  • 构造二叉树
  • 寻找重复子树
图片来源于网络

1. 二叉树

基本二叉树节点如下:

/**
 * 基本二叉树节点
 */
class TreeNode {
    int val;
    TreeNode left, right;

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

很多经典算法,比如回溯、动态规划、分治算法等都是树的问题,而树的问题就离不开树的递归遍历框架:

/**
 * 二叉树遍历框架
 */
void traverse(TreeNode root) {
    // 前序遍历:根左右
    traverse(root.left)
    // 中序遍历:左根右
    traverse(root.right)
    // 后序遍历:左右根
}

举个例子,经典算法「快速排序」就是个二叉树的前序遍历,「归并排序」就是个二叉树的后续遍历。

快速排序的代码框架如下:

    /**
     * 快速排序框架
     * <p>
     * 对 nums[low..high] 进行排序,先找一个分界点p,
     * 通过交换元素使得 nums[low..p-1] 都小于等于 nums[p],且 nums[p+1..high] 都大于 nums[p],
     * 然后递归地去 nums[low..p-1] 和 nums[p+1..high] 中寻找新的分界点,最后整个数组就被排序了
     */
    void quickSort(int[] nums, int low, int high) {
        /* **** 前序遍历位置 ******/
        // 通过交换元素构建分界点 p
        int p = partition(nums, low, high);
        /* ************************/

        quickSort(nums, low, p - 1);
        quickSort(nums, p + 1, high);
    }

归并排序的代码框架如下:

    /**
     * 归并排序的框架
     * <p>
     * 对 nums[low..high] 进行排序,先对 nums[low..mid] 排序,再对 nums[mid+1..high] 排序,
     * 最后把这两个有序的子数组合并,整个数组就排好序了。
     */
    void mergeSort(int[] nums, int low, int high) {
        int mid = (low + high) / 2;
        mergeSort(nums, low, mid);
        mergeSort(nums, mid + 1, high);

        /* ***** 后序遍历位置 ******/
        // 合并两个排好序的子数组
        merge(nums, low, mid, high);
        /* ************************/
    }

如何写递归算法?

写递归算法的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要试图跳入递归

如计算一棵二叉树共有几个节点:

    /**
     * 计算一棵二叉树共有几个节点
     * <p>
     * 定义:count(root) 返回以 root 为根的树有多少节点
     */
    private int count(TreeNode root) {
        // base case
        if (root == null) return 0;
        // 自己加上子树的节点数就是整棵树的节点数
        return 1 + count(root.left) + count(root.right);
    }

写树相关的算法,先搞清楚当前 root 节点该做什么,然后根据函数定义递归调用子节点,递归调用会让孩子节点做相同的事情。

1.1 翻转二叉树

力扣第 226 题「翻转二叉树」:


翻转二叉树

上述题目只要把二叉树上的每一个节点的左右子节点进行交换,最后的结果就是完全翻转之后的二叉树,代码如下:

    /**
     * 翻转二叉树:将整棵树的节点翻转
     */
    private TreeNode invertTree(TreeNode root) {
        // base case
        if (root == null) return null;

        /* *** 前序遍历位置 ****/
        // root 节点需要交换它的左右子节点
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;

        // 让左右子节点继续翻转它们的子节点
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }

值得注意的是:二叉树题目的一个难点就是,如何把题目的要求细化成每个节点需要做的事情

1.2 填充二叉树节点的右侧指针

力扣第 116 题「填充每个节点的下一个右侧节点指针」:

填充二叉树节点的右侧指针

输入是一棵「完美二叉树」,即除了最右侧的节点 next 指针会指向 null,其他节点的右侧一定有相邻的节点,容易写下如下代码:

TreeNode connect(TreeNode root) {
    if(root == null || root.left == null) return root;

    root.left.next = root.right;

    connect(root.left);
    connect(root.right);

    return root;
}

上述代码存在一个问题,当两个子节点不属于同一个父节点时,就没法连接起来,即只依赖一个节点的话,是没办法连接「跨父节点」的两个相邻节点的。

因此需要增加函数参数,给他安排两个节点,「将每一层二叉树节点连接起来」可以细化成「将每两个相邻节点都连接起来」:

    /**
     * 填充二叉树节点的右侧指针
     * <p>
     * 给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。
     * 填充它的每个 next 指针,让这个指针指向其下一个右侧节点,若无右侧节点将 next 指针设为 null
     * 初始状态下,所有 next 指针都被设置为 null
     */
    private TreeNode connect(TreeNode root) {
        if (root == null) return null;
        connectTwoNode(root.left, root.right);
        return root;
    }

    /**
     * 定义:输入两个节点,将它俩连接起来
     * <p>
     * connectTwoNode 函数不断递归,可以无死角覆盖整棵二叉树,将所有相邻节点都连接起来
     */
    private void connectTwoNode(TreeNode node1, TreeNode node2) {
        if (node1 == null || node2 == null) return;

        /* *** 前序遍历位置 ****/
        // 将传入的两个节点连接
        node1.next = node2;

        // 连接相同父节点的两个子节点
        connectTwoNode(node1.left, node1.right);
        connectTwoNode(node2.left, node2.right);
        // 连接跨越父节点的两个子节点
        connectTwoNode(node1.right, node2.left);
    }

1.3 将二叉树展开为链表

力扣第 114 题「二叉树展开为链表」:

二叉树展开为链表

函数签名如下:

void flatten(TreeNode root);

尝试给出这个函数的定义,给 flatten 函数输入一个节点 root,那么以 root 为根的二叉树就会被拉平为一条链表:

    /**
     * 将二叉树展开为链表
     * <p>
     * 定义:将以 root 为根的树拉平为链表
     */
    private void flatten(TreeNode root) {
        // base case
        if (root == null) return;

        flatten(root.left);
        flatten(root.right);

        /* *** 后序遍历位置 ****/
        // 1、左右子树已经被拉平成一条链表
        TreeNode left = root.left;
        TreeNode right = root.right;

        // 2、将左子树作为右子树
        root.left = null;
        root.right = left;

        // 3、将原先的右子树接到当前右子树的末端
        TreeNode p = root;
        while (p.right != null) {
            // 遍历 root 直到 root.right == null
            p = p.right;
        }
        p.right = right; // 接上原先的右子树
    }

还是那句话:只要知道 flatten 的定义如此,相信这个定义,让 root 做它该做的事情,然后 flatten 函数就会按照定义工作

2. 构造二叉树

树的算法,关键思路如下:

把题目的要求细化,搞清楚根节点应该做什么,然后剩下的事情抛给前/中/后序的遍历框架就行了,而不要试图跳进递归的细节里

2.1 构造最大二叉树

对于构造二叉树的问题,根节点要做的就是把想办法把自己构造出来。

    /**
     * 最大二叉树
     * <p>
     * 给定一个不含重复元素的整数数组。最大二叉树定义如下:
     * 1. 二叉树的根是数组中的最大元素
     * 2. 左子树是通过数组中最大值左边部分构造出的最大二叉树
     * 3. 右子树是通过数组中最大值右边部分构造出的最大二叉树
     * <p>
     * 通过给定的数组构建最大二叉树,并输出这个树的根节点
     * 如 输入 [3, 2, 1, 6, 0, 5],输出如下:
     * //                6
     * //             /    \
     * //            3      5
     * //            \     /
     * //             2   0
     * //              \
     * //               1
     */
    private TreeNode constructMaximumBinaryTree(int[] nums) {
        return build(nums, 0, nums.length - 1);
    }

    /**
     * 将 nums[low..high] 构造成符合条件的树,返回根节点
     */
    private TreeNode build(int[] nums, int low, int high) {
        // base case
        if (low > high) return null;

        // 找到数组中的最大值和对应的索引
        int index = -1, maxVal = Integer.MIN_VALUE;
        for (int i = low; i <= high; i++) {
            if (maxVal < nums[i]) {
                index = i;
                maxVal = nums[i];
            }
        }

        TreeNode root = new TreeNode(maxVal);
        // 递归调用构造左右子树
        root.left = build(nums, low, index - 1);
        root.right = build(nums, index + 1, high);

        return root;
    }

上述代码,遍历数组把找到最大值 maxVal,把根节点 root 做出来,然后对 maxVal 左边的数组和右边的数组进行递归调用,作为 root 的左右子树。

对于每个根节点,只需要找到当前 nums 中的最大值和对应的索引,然后递归调用左右数组构造左右子树即可。

2.2 通过前序和中序遍历结果构造二叉树

类似上一题,想办法确定根节点的值,把根节点做出来,然后递归构造左右子树即可。

   /**
     * 通过前序和中序遍历结果构造二叉树
     * <p>
     * 根据一棵树的前序遍历与中序遍历构造二叉树
     * 假设树中没有重复的元素,例如给出:
     * 前序遍历 preorder = [3, 9, 20, 15, 7]
     * 中序遍历 inorder  = [9, 3, 15, 20, 7]
     * 返回如下的二叉树:
     * //           3
     * //          / \
     * //         9  20
     * //           / \
     * //          15  7
     * <p>
     * tip: 想办法确定根节点的值,把根节点做出来,然后递归构造左右子树即可
     */
    private TreeNode buildTree(int[] preorder, int[] inorder) {
        return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
    }

    /**
     * 前序遍历数组为 preorder[preStart..preEnd],
     * 中序遍历数组为 inorder[inStart..inEnd],
     * 构造二叉树,返回该二叉树的根节点
     */
    private TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {

        // base case
        if (preStart > preEnd) return null;

        // root 节点对应的值就是前序遍历数组的第一个元素
        int rootVal = preorder[preStart];
        // rootVal 在中序遍历数组中的索引
        int index = 0;
        for (int i = inStart; i < inEnd; i++) {
            if (inorder[i] == rootVal) {
                index = i;
                break;
            }
        }

        TreeNode root = new TreeNode(rootVal);
        // 递归构造左右子树
        int leftSize = index - inStart; // 左子树的节点数
        root.left = build(preorder, preStart + 1, preStart + leftSize, inorder, inStart, index - 1);
        root.right = build(preorder, preStart + leftSize + 1, preEnd, inorder, index + 1, inEnd);
        return root;
    }

根据前序遍历和中序遍历结果的特点,确定根节点、左右数组对应的起始索引和终止索引。

2.3 通过后序和中序遍历结果构造二叉树

和上面的区别是,后序遍历和前序遍历相反,根节点对应的值为后序遍历的最后一个元素:

    /**
     * 通过后序和中序遍历结果构造二叉树
     * <p>
     * 根据一棵树的前序遍历与中序遍历构造二叉树
     * 假设树中没有重复的元素,例如给出:
     * 中序遍历 inorder   = [9, 3, 15, 20, 7]
     * 后序遍历 postorder = [9, 15, 7, 20, 3]
     * 返回如下的二叉树:
     * //           3
     * //          / \
     * //         9  20
     * //           / \
     * //          15  7
     * <p>
     * tip: 想办法确定根节点的值,把根节点做出来,然后递归构造左右子树即可
     */
    private TreeNode buildTree2(int[] inorder, int[] postorder) {
        return build2(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1);
    }

    private TreeNode build2(int[] inorder, int inStart, int inEnd, int[] postOrder, int postStart, int postEnd) {

        if (inStart > inEnd) return null;

        // root 节点对应的值就是后序遍历数组的最后一个元素
        int rootVal = postOrder[postEnd];
        // rootVal 在中序遍历数组中的索引
        int index = 0;
        for (int i = 0; i < inEnd; i++) {
            if (inorder[i] == rootVal) {
                index = i;
                break;
            }
        }

        TreeNode root = new TreeNode(rootVal);
        // 递归构造左右子树
        int leftSize = index - inStart; // 左子树的节点数
        root.left = build2(inorder, inStart, index - 1, postOrder, postStart, postStart + leftSize - 1);
        root.right = build2(inorder, index + 1, inEnd, postOrder, postStart + leftSize, postEnd - 1);

        return root;
    }

3. 寻找重复子树

    /**
     * 652 题「寻找重复子树」
     * <p>
     * 给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,只需要返回其中任意一棵的根节点即可。
     * <p>
     * 两棵树重复是指它们具有相同的结构以及相同的节点值
     * <p>
     * 如:                    其中:                和               是重复的子树
     * //           1                       2               4
     * //          / \                     /
     * //         2   3                   4
     * //        /   / \
     * //       4   2  4
     * //          /
     * //         4
     */
    private List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        traverseTree(root);
        return res;
    }

    // 记录所有子树以及出现的次数
    private HashMap<String, Integer> memo = new HashMap<>();
    // 记录重复的子树根节点
    private LinkedList<TreeNode> res = new LinkedList<>();

    // 辅助函数
    private String traverseTree(TreeNode root) {
        if (root == null) return "#";// 对于空节点,可以用一个特殊字符表示

        // 将左右子树序列化成字符串
        String left = traverseTree(root.left);
        String right = traverseTree(root.right);

        // 左右子树加上自己,就是以自己为根的二叉树序列化结果
        String subTree = left + "," + right + "," + root.val;

//        if (memo.containsKey(subTree)){
//            // 有人和我重复,把自己加入结果列表
//            res.add(root);
//        } else {
//            // 暂时没人跟我重复,把自己加入集合
//            memo.put(subTree)
//        }

        // 获取 subTree 在 memo 中的次数,默认是 0
        int freq = memo.getOrDefault(subTree, 0);
        if (freq == 1) {
            // 多次重复也只会被加入结果集一次
            res.add(root);
        }
        // 给子树对应的出现次数加一
        memo.put(subTree, freq + 1);

        return subTree;
    }

总结:做二叉树的问题,关键是把题目的要求细化,搞清楚根节点应该做什么,然后剩下的事情抛给前/中/后序的遍历框架就行了。


参考链接:

手把手带你刷二叉树(第一期)

手把手带你刷二叉树(第二期)

手把手带你刷二叉树(第三期)

相关文章

  • Algorithm小白入门 -- 二叉树

    二叉树二叉树构造二叉树寻找重复子树 1. 二叉树 基本二叉树节点如下: 很多经典算法,比如回溯、动态规划、分治算法...

  • Algorithm小白入门 -- 数组

    数组双指针技巧数组删除、去重 1. 双指针技巧 1.1 快慢指针 快慢指针一般都初始化指向链表的头结点head,前...

  • Algorithm小白入门 -- 单链表

    单链表递归反转链表k个一组反转链表回文链表 1. 递归反转链表 单链表节点的结构如下: 1.1 递归反转整个单链表...

  • Algorithm小白入门 -- 队列和栈

    队列和栈队列实现栈、栈实现队列单调栈单调队列运用栈去重 1. 队列实现栈、栈实现队列 队列是一种先进先出的数据结构...

  • [每日一道算法题] 从上往下打印二叉树

    Algorithm OJ address OJ website : 从上往下打印二叉树 Description 从...

  • 每周 ARTS 第 18 期

    1. Algorithm 110. 平衡二叉树(简单) 描述: 给定一个二叉树,判断它是否是高度平衡的二叉树。本题...

  • Algorithm小白入门 -- 二叉搜索树

    二叉搜索树二叉搜索树 BSTBST 的基本操作计算合法的 BST 1. 二叉搜索树 BST 二叉搜索树(Binar...

  • 每周 ARTS 第 8 期

    1. Algorithm 101. 对称二叉树(简单) 描述: 给定一个二叉树,检查它是否是镜像对称的。 示例: ...

  • ARTS-第六周

    Algorithm 二叉树根据值删除节点 Review Flink自定义TableSource和TableSink...

  • 每周 ARTS 第 10 期

    1. Algorithm 226. 翻转二叉树(简单) 描述: 翻转一棵二叉树 示例: 思路: 递归法:翻转一个二...

网友评论

    本文标题:Algorithm小白入门 -- 二叉树

    本文链接:https://www.haomeiwen.com/subject/uztkwltx.html