代码随想录训练第十八天|LeetCode669.修剪二叉搜索树、LeetCode108.将有序数组转换为二叉搜索树、LeetCode538.把二叉搜索树转换为累加树、二叉树总结

669.修剪二叉搜索树

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

示例 1:

img

输入:root = [1,0,2], low = 1, high = 2
输出:[1,null,2]

示例 2:

img

输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3
输出:[3,2,null,1]

提示:

  • 树中节点数在范围 [1, 104]
  • 0 <= Node.val <= 104
  • 树中每个节点的值都是 唯一
  • 题目数据保证输入是一棵有效的二叉搜索树
  • 0 <= low <= high <= 104

Related Topics

  • 深度优先搜索

  • 二叉搜索树

  • 二叉树

思路

相信看到这道题目大家都感觉是一道简单题(事实上leetcode上也标明是简单)。

但还真的不简单!

递归法

直接想法就是:递归处理,然后遇到 root.val < low || root.val > high 的时候直接return NULL,一波修改,赶紧利落。

然而[1, 3]区间在二叉搜索树的中可不是单纯的节点3和左孩子节点0就决定的,还要考虑节点0的右子树

我们在重新关注一下第二个示例,如图:

669.修剪二叉搜索树

所以以上的代码是不可行的!

从图中可以看出需要重构二叉树,想想是不是本题就有点复杂了。

其实不用重构那么复杂。

在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图:

669.修剪二叉搜索树1

理解了最关键部分了我们再递归三部曲:

  1. 确定递归函数的参数以及返回值

这里我们为什么需要返回值呢?

因为是要遍历整棵树,做修改,其实不需要返回值也可以,我们也可以完成修剪(其实就是从二叉树中移除节点)的操作。

但是有返回值,更方便,可以通过递归函数的返回值来移除节点。

这样的做法在二叉树:搜索树中的插入操作 (opens new window)二叉树:搜索树中的删除操作 (opens new window)中大家已经了解过了。

代码如下:

private TreeNode travel(TreeNode root, int low, int high){
    
}
  1. 确定终止条件

修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。

    if (root == null) {
        return null;
    }
  1. 确定单层递归的逻辑

如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。

代码如下:

    //如果当前节点的值小于最小值,则说明还需要遍历,使用
    if (root.val < low) {//返回当前左目标节点()
        return travel(root.right, low, high);//向右子树继续遍历,寻找符合节点
    }
    //如果当前节点值还是大于最大值,则向内收缩,继续往左子树寻找目标值
    if (root.val > high) {
        return travel(root.left, low, high);//向左子树继续遍历,寻找符合节点
    }
    //这时已经找到了目标值,使用当前节点的左右节点接住就可以
    root.left = travel(root.left, low, high);
    root.right = travel(root.right, low, high);
    return root;

此时大家是不是还没发现这多余的节点究竟是如何从二叉树中移除的呢?

在回顾一下上面的代码,针对下图中二叉树的情况:

669.修剪二叉搜索树1

如下代码相当于把节点0的右孩子(节点2)返回给上一层,

    //如果当前节点的值小于最小值,则说明还需要遍历,使用
    if (root.val < low) {//返回当前左目标节点()
        return travel(root.right, low, high);//向右子树继续遍历,寻找符合节点
    }

然后如下代码相当于用节点3的左孩子 把下一层返回的 节点0的右孩子(节点2) 接住。

    root.left = travel(root.left, low, high);

此时节点3的左孩子就变成了节点2,将节点0从二叉树中移除了。

整体代码

public TreeNode trimBST(TreeNode root, int low, int high) {
    return travel(root, low, high);
}

private TreeNode travel(TreeNode root, int low, int high) {
    if (root == null) {
        return null;
    }
    //如果当前节点的值小于最小值,则说明还需要遍历,使用
    if (root.val < low) {//返回当前左目标节点()
        return travel(root.right, low, high);//向右子树继续遍历,寻找符合节点
    }
    //如果当前节点值还是大于最大值,则向内收缩,继续往左子树寻找目标值
    if (root.val > high) {
        return travel(root.left, low, high);//向左子树继续遍历,寻找符合节点
    }
    //这时已经找到了目标值,使用当前节点的左右节点接住就可以
    root.left = travel(root.left, low, high);
    root.right = travel(root.right, low, high);
    return root;
}

时间复杂度O(n)

空间复杂度O(n)

迭代法

因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。

在剪枝的时候,可以分为三步:

  • 将root移动到[L, R] 范围内,注意是左闭右闭区间
  • 剪枝左子树
  • 剪枝右子树

代码如下:

/**
 * @Description 迭代法
 * @Param root
 * @Param low
 * @Param high
 * @Return {@link TreeNode}
 * @Author 君君
 * @Date 2024/7/11 15:12
 */
public TreeNode trimBST(TreeNode root, int low, int high) {
    if (root == null) {
        return null;
    }
    //首先处理头结点,让头结点先处于[low,high]区间内,这个区间是左闭右闭,保证节点在区间范围内
    while (root != null && (root.val < low || root.val > high)) {
        if (root.val < low) {
            root = root.right;
        } else {
            root = root.left;
        }
    }
    TreeNode cur = root;
    // 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况
    while (cur != null) {
        //当前节点左节点的值已经小于low,所以将左节点的右子树作为当前节点的左节点
        while (cur.left != null && cur.left.val < low) {
            cur.left = cur.left.right;
        }
        //继续向左侧遍历
        cur = cur.left;
    }
    cur = root;
    // 此时root已经在[L, R] 范围内,处理右孩子元素大于h的情况
    while (cur != null) {
        //当前节点右节点的值已经大于high,所以将右节点的左子树作为当前节点的右节点
        while (cur.right != null && cur.right.val > high) {
            cur.right = cur.right.left;
        }
        //继续向左侧遍历
        cur = cur.right;
    }
    return root;
}

时间复杂度O(n)

空间复杂度O(n)

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

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。

示例 1:

img

输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

示例 2:

img

输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums严格递增 顺序排列

Related Topics

  • 二叉搜索树

  • 数组

  • 分治

  • 二叉树

思路

进入正题:

题目中说要转换为一棵高度平衡二叉搜索树。为什么强调要平衡呢?

因为只要给我们一个有序数组,如果不强调平衡,都可以以线性结构来构造二叉搜索树。

例如 有序数组[-10,-3,0,5,9] 就可以构造成这样的二叉搜索树,如图。

img

上图中,是符合二叉搜索树的特性吧,如果要这么做的话,是不是本题意义就不大了,所以才强调是平衡二叉搜索树。

其实数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取。所以想构成不平衡的二叉树是自找麻烦

二叉树:构造二叉树登场! (opens new window)二叉树:构造一棵最大的二叉树 (opens new window)中其实已经讲过了,如果根据数组构造一棵二叉树。

本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间

本题其实要比二叉树:构造二叉树登场! (opens new window)二叉树:构造一棵最大的二叉树 (opens new window)简单一些,因为有序数组构造二叉搜索树,寻找分割点就比较容易了。

分割点就是数组中间位置的节点。

那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个?

取哪一个都可以,只不过构成了不同的平衡二叉搜索树。

例如:输入:[-10,-3,0,5,9]

如下两棵树,都是这个数组的平衡二叉搜索树:

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

如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。

这也是题目中强调答案不是唯一的原因。 理解这一点,这道题目算是理解到位了

递归

递归三部曲:

  1. 确定递归函数返回值及其参数

删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。

相信大家如果仔细看了二叉树:搜索树中的插入操作 (opens new window)二叉树:搜索树中的删除操作 (opens new window),一定会对递归函数返回值的作用深有感触。

那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。

再来看参数,首先是传入数组,然后就是左下标left和右下标right,我们在二叉树:构造二叉树登场! (opens new window)中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。

所以代码如下:

private TreeNode travel(int[] nums, int left, int right){
    
}

这里注意,我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到我们讲过的循环不变量

左闭右闭区间主要是为了保证遍历的每一个值都是需要去处理的值

二叉树:构造二叉树登场! (opens new window)35.搜索插入位置 (opens new window)59.螺旋矩阵II (opens new window)都详细讲过循环不变量。

  1. 确定递归终止条件

这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。

代码如下:

    if (left > right) {
        return null;
    }
  • 确定单层递归的逻辑

首先取数组中间元素的位置,不难写出int mid = (left + right) / 2;这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在二分法 (opens new window)中尤其需要注意!

所以可以这么写:int mid = left + ((right - left) / 2);

但本题leetcode的测试数据并不会越界,所以怎么写都可以。但需要有这个意识!

取了中间位置,就开始以中间位置的元素构造节点,代码:TreeNode* root = new TreeNode(nums[mid]);

接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。

最后返回root节点,单层递归整体代码如下:

    //找到中点值
    int mid = left + ((right - left) / 2);
    //int mid = left + ((right - left) >> 1);
    //中
    TreeNode node = new TreeNode(nums[mid]);
    //左
    node.left = travel(nums, left, mid - 1);
    //右
    node.right = travel(nums, mid + 1, right);
    return node;

整体代码

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

/**
 * @Description 递归法
 * @Param nums
 * @Param left
 * @Param right
 * @Return {@link TreeNode}
 * @Author 君君
 * @Date 2024/7/11 17:04
 */
private TreeNode travel(int[] nums, int left, int right) {
    if (left > right) {
        return null;
    }
    //找到中点值
    int mid = left + ((right - left) / 2);
    //int mid = left + ((right - left) >> 1);
    //中
    TreeNode node = new TreeNode(nums[mid]);
    //左
    node.left = travel(nums, left, mid - 1);
    //右
    node.right = travel(nums, mid + 1, right);
    return node;
}

注意:在调用travel的时候传入的left和right为什么是0和nums.size() - 1,因为定义的区间为左闭右闭

时间复杂度O(n)

空间复杂度O(LogN)

迭代法

迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。

模拟的就是不断分割的过程,代码如下:(我已经详细注释)

/**
 * @Description 迭代法
 * @Param nums
 * @Return {@link TreeNode}
 * @Author 君君
 * @Date 2024/7/11 17:26
 */
public TreeNode sortedArrayToBST(int[] nums) {
    if (nums.length == 0) {
        return null;
    }
    //根节点初始化
    TreeNode root = new TreeNode(-1);
    Deque<TreeNode> nodeDeque = new LinkedList<>();
    Deque<Integer> leftDeque = new LinkedList<>();
    Deque<Integer> rightDeque = new LinkedList<>();
    //根节点
    nodeDeque.offer(root);
    // 0为左区间下标初始位置
    leftDeque.offer(0);
    // nums.size() - 1为右区间下标初始位置
    rightDeque.offer(nums.length - 1);
    while (!nodeDeque.isEmpty()) {
        TreeNode cur = nodeDeque.poll();
        int left = leftDeque.poll();
        int right = rightDeque.poll();
        int mid = left + ((right - left) / 2);
        //将mid对应元素给中间节点
        cur.val = nums[mid];
        //处理左区间
        if (left <= mid - 1) {
            cur.left = new TreeNode(-1);
            nodeDeque.offer(cur.left);
            leftDeque.offer(left);
            rightDeque.offer(mid - 1);
        }
        // 处理右区间
        if (right >= mid + 1) {
            cur.right = new TreeNode(-1);
            nodeDeque.offer(cur.right);
            leftDeque.offer(mid + 1);
            rightDeque.offer(right);
        }
    }
    return root;
}

时间复杂度O(n)

空间复杂度O(n)

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

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。

**注意:**本题和 1038: https://leetcode-cn.com/problems/binary-search-tree-to-greater-sum-tree/ 相同

示例 1:

img

输入:[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]

示例 2:

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

示例 3:

输入:root = [1,0,2]
输出:[3,3,2]

示例 4:

输入:root = [3,2,4,1]
输出:[7,9,4,10]

提示:

  • 树中的节点数介于 0104 之间。
  • 每个节点的值介于 -104104 之间。
  • 树中的所有值 互不相同
  • 给定的树为二叉搜索树。

Related Topics

  • 深度优先搜索

  • 二叉搜索树

  • 二叉树

思路

一看到累加树,相信很多小伙伴都会疑惑:如何累加?遇到一个节点,然后再遍历其他节点累加?怎么一想这么麻烦呢。

然后再发现这是一棵二叉搜索树,二叉搜索树啊,这是有序的啊。

那么有序的元素如何求累加呢?

其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。

为什么变成数组就是感觉简单了呢?

因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。

那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,

所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了

反中序遍历:右中左

递归

遍历顺序如图所示:

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

本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。

pre指针的使用技巧,我们在二叉树:搜索树的最小绝对差 (opens new window)二叉树:我的众数是多少? (opens new window)都提到了,这是常用的操作手段。

  • 递归函数参数以及返回值

这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。

同时需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了。

private int pre = 0; // 记录前一个节点的数值
private void travel(TreeNode root){
    
}
  • 确定终止条件

遇空就终止。

    if (root == null) {
        return;
    }

注意要右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。

代码如下:

    //反中序遍历
    //右
    travel(root.right);
    //中:返回上一个节点加上当前节点的值
    root.val = root.val + pre;
    pre = root.val;
    //左
    travel(root.left);

最终代码

public TreeNode convertBST(TreeNode root) {
    travel(root);
    return root;
}

private int pre = 0; // 记录前一个节点的数值

/**
 * @Description 递归法(反中序遍历)
 * @Param root 传入当前节点进行遍历
 * @Return
 * @Author 君君
 * @Date 2024/7/12 1:20
 */
private void travel(TreeNode root) {
    if (root == null) {
        return;
    }
    //反中序遍历
    //右
    travel(root.right);
    //中:返回上一个节点加上当前节点的值
    root.val = root.val + pre;
    pre = root.val;
    //左
    travel(root.left);
}

时间复杂度O(n)

空间复杂度O(h)

迭代法

迭代法其实就是中序模板题了

这里我给出其中的一种,代码如下:

/**
 * @Description 迭代法
 * @Param root
 * @Return {@link TreeNode}
 * @Author 君君
 * @Date 2024/7/12 1:34
 */
public TreeNode convertBST(TreeNode root) {
    if (root == null) {
        return null;
    }
    //使用栈实现反中序遍历
    Stack<TreeNode> stack = new Stack<>();
    int pre = 0;
    stack.push(root);
    while (!stack.isEmpty()) {
        //找到当前节点
        TreeNode cur = stack.peek();
        if (cur != null) {
            //如果当前节点不为空,则继续遍历
            stack.pop();
            //因为是栈,所以入栈顺序为左中右
            if (cur.left != null) {
                stack.push(cur.left);
            }

            stack.push(cur);
            stack.push(null);
            if (cur.right != null) {
                stack.push(cur.right);
            }
        } else {
            //找到当前节点并进行处理
            stack.pop();
            cur = stack.pop();
            cur.val = cur.val + pre;
            pre = cur.val;
        }
    }
    return root;
}

时间复杂度O(n)

空间复杂度O(n)

二叉树总结

基础理论

遍历方式

二叉树属性

  • 二叉树:是否对称(opens new window)
    • 递归:后序,比较的是根节点的左子树与右子树是不是相互翻转
    • 迭代:使用队列/栈将两个节点顺序放入容器中进行比较
  • 二叉树:求最大深度(opens new window)
    • 递归:后序,求根节点最大高度就是最大深度,通过递归函数的返回值做计算树的高度
    • 迭代:层序遍历
  • 二叉树:求最小深度(opens new window)
    • 递归:后序,求根节点最小高度就是最小深度,注意最小深度的定义
    • 迭代:层序遍历
  • 二叉树:求有多少个节点(opens new window)
    • 递归:后序,通过递归函数的返回值计算节点数量
    • 迭代:层序遍历
  • 二叉树:是否平衡(opens new window)
    • 递归:后序,注意后序求高度和前序求深度,递归过程判断高度差
    • 迭代:效率很低,不推荐
  • 二叉树:找所有路径(opens new window)
    • 递归:前序,方便让父节点指向子节点,涉及回溯处理根节点到叶子的所有路径
    • 迭代:一个栈模拟递归,一个栈来存放对应的遍历路径
  • 二叉树:递归中如何隐藏着回溯(opens new window)
    • 详解二叉树:找所有路径 (opens new window)中递归如何隐藏着回溯
  • 二叉树:求左叶子之和(opens new window)
    • 递归:后序,必须三层约束条件,才能判断是否是左叶子。
    • 迭代:直接模拟后序遍历
  • 二叉树:求左下角的值(opens new window)
    • 递归:顺序无所谓,优先左孩子搜索,同时找深度最大的叶子节点。
    • 迭代:层序遍历找最后一行最左边
  • 二叉树:求路径总和(opens new window)
    • 递归:顺序无所谓,递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树。
    • 迭代:栈里元素不仅要记录节点指针,还要记录从头结点到该节点的路径数值总和

二叉树的修改和构造

  • 翻转二叉树(opens new window)
    • 递归:前序,交换左右孩子
    • 迭代:直接模拟前序遍历
  • 构造二叉树(opens new window)
    • 递归:前序,重点在于找分割点,分左右区间构造
    • 迭代:比较复杂,意义不大
  • 构造最大的二叉树(opens new window)
    • 递归:前序,分割点为数组最大值,分左右区间构造
    • 迭代:比较复杂,意义不大
  • 合并两个二叉树(opens new window)
    • 递归:前序,同时操作两个树的节点,注意合并的规则
    • 迭代:使用队列,类似层序遍历

二叉搜索树的属性

二叉树公共祖先

  • 二叉树的公共祖先问题(opens new window)
    • 递归:后序,回溯,找到左子树出现目标值,右子树节点目标值的节点。
    • 迭代:不适合模拟回溯
  • 二叉搜索树的公共祖先问题(opens new window)
    • 递归:顺序无所谓,如果节点的数值在目标区间就是最近公共祖先
    • 迭代:按序遍历

二叉树的修改与构造

  • 二叉搜索树中的插入操作(opens new window)
    • 递归:顺序无所谓,通过递归函数返回值添加节点
    • 迭代:按序遍历,需要记录插入父节点,这样才能做插入操作
  • 二叉搜索树中的删除操作(opens new window)
    • 递归:前序,想清楚删除非叶子节点的情况
    • 迭代:有序遍历,较复杂
  • 修剪二叉搜索树(opens new window)
    • 递归:前序,通过递归函数返回值删除节点
    • 迭代:有序遍历,较复杂
  • 构造二叉搜索树(opens new window)
    • 递归:前序,数组中间节点分割
    • 迭代:较复杂,通过三个队列来模拟

最后总结

  • 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。
  • 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
  • 求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。

注意在普通二叉树的属性中,我用的是一般为后序,例如单纯求深度就用前序,二叉树:找所有路径 (opens new window)也用了前序,这是为了方便让父节点指向子节点。

所以求普通二叉树的属性还是要具体问题具体分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值