羊羊刷题笔记Day23/60 | 第六章 二叉树P9 | 669. 修剪二叉搜索树、108. 将有序数组转换为二叉搜索树、538. 把二叉搜索树转换为累加树

986 阅读8分钟

669 修剪二叉搜索树

虽然力扣这题标注简单,但其实并不简单~

递归法

要注意的是:不是简单判断子节点是不是大于大的或小于小的就否定子树,子树中可能存在符合条件的点。
理解了这点,我们再递归三部曲:

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

这里是要遍历整棵树,做修改,其实不需要返回值也可以,我们也可以完成修剪(其实就是从二叉树中移除节点)的操作。
但是有返回值,更方便,可以通过递归函数的返回值来移除节点
类似这种思路在701. 二叉搜索树中的插入操作450. 删除二叉搜素树中的节点中出现过。
代码如下:

public TreeNode trimBST(TreeNode root, int low, int high) {}
  • 确定终止条件

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

// 终止条件 判空
if (root == null) return null;
  • 确定单层递归的逻辑

如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。
如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。
简单来说,大于大的,小于小的就继续向下递归,直到找到合适的节点。(找定root / 节点)
代码如下:

// 单层递归逻辑 不在[low,high]范围内(即小于最小 大于最大)
if (root.val < low) return trimBST(root.right,low,high);
if (root.val > high) return trimBST(root.left,low,high);

确定根节点(root)后,接下来左右子树递归,把符合条件的子树返回,做root的左右子树(用root.left 和 root.right 表示)
最后返回root节点,代码如下:

root.left = trimBST(root.left,low,high);
root.right = trimBST(root.right,low,high);

return root;

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

最后整体代码如下:

public TreeNode trimBST(TreeNode root, int low, int high) {
    // 终止条件 判空
    if (root == null) return null;

    // 单层递归逻辑 不在[low,high]范围内(即小于最小 大于最大)
    if (root.val < low) return trimBST(root.right,low,high);
    if (root.val > high) return trimBST(root.left,low,high);

    root.left = trimBST(root.left,low,high);
    root.right = trimBST(root.right,low,high);

    return root;
}

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

注意要求的是平衡二叉搜索树(只满足二叉搜索树一个线性树就好了)

思路

做这道题目之前大家可以了解一下这几道:

以上两题为数组构造一棵二叉树。

进入正题:
题目中说要转换为一棵高度平衡二叉搜索树。为什么强调要平衡呢?
因为只要给我们一个有序数组,如果强调平衡,都可以以线性结构来构造二叉搜索树
例如 有序数组[-10,-3,0,5,9] 就可以构造成这样的二叉搜索树,如图。
image.png
上图符合二叉搜索树的特性,但如果这样,这题意义就不大。因此要使用平衡二叉搜索树。
其实数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取。所以想构成不平衡的二叉树是自找麻烦
106. 从中序与后序遍历序列构造二叉树654. 最大二叉树中其实已经讲过了,如果根据数组构造一棵二叉树
**本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
本题其实要比那两题简单一些,因为
有序数组构造二叉搜索树,**分割点就是数组中间位置的节点。
注意:如果数组长度为偶数,中间节点有两个,取哪一个都可以,只需要定一个统一标准。(默认向下取 与Java运算一致)
例如:输入:[-10,-3,0,5,9]
如下两棵树,都是这个数组的平衡二叉搜索树:(左图为向下取整 右图为向上取整)
image.png
因此,题目中答案不是唯一的原因。运行结果如果报红可能也是对的。(只要画图是正确的)

递归

递归三部曲:

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

删除二叉树节点,增加二叉树节点,这样是比较方便的。
701. 二叉搜索树中的插入操作450. 删除二叉搜素树中的节点中,都是用递归函数的返回值来完成,结合这两题后对递归函数返回值的作用会有进一步理解。
本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。
再来看参数,首先是传入数组,然后就是左下标left和右下标right。此处同样要定一个标准,要遵守什么原则(左闭右开 or 左闭右闭)
此处选用左闭右闭,代码如下:

public TreeNode traversal(int[] nums, int start, int end){}
  • 确定递归终止条件

由于定义的是左闭右闭的区间,所以当区间 left > right的时候,是非法区间,即空节点。
代码如下:

// 约定:左闭右闭
// 终止条件
if (start > end) return null;
  • 确定单层递归的逻辑

首先取数组中间元素的位置,前面定了向下取整,不难写出int mid = (left + right) / 2;,
(这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,但此处没有这个问题)
因此可以这么写:int mid = left + ((right - left) / 2);
取了中间位置,就开始以中间位置的元素构造节点,然后划分左右子树。
root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。
最后返回root节点,单层递归整体代码如下:

// 递归逻辑
// 计算出中间坐标,分割得左右子树(此处注意start end递归后本身有值)
int mid = (start + end) / 2;
TreeNode root = new TreeNode(nums[mid]);
// 构建左右子树
root.left = traversal(nums,start,mid - 1);
root.right = traversal(nums,mid + 1, end);

// 左右子树构建好后返回root到上一层
return root;

递归整体代码如下:

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

public TreeNode traversal(int[] nums, int start, int end){
    // 约定:左闭右闭
    // 终止条件
    if (start > end) return null;

    // 递归逻辑
    // 计算出中间坐标,分割得左右子树(此处注意start end递归后本身有值)
    int mid = (start + end) / 2;
    TreeNode root = new TreeNode(nums[mid]);
    // 构建左右子树
    root.left = traversal(nums,start,mid - 1);
    root.right = traversal(nums,mid + 1, end);

    // 左右子树构建好后返回root到上一层
    return root;
}

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

掌握了遍历逻辑就和处理有序数组一样简单

其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。
为什么变成数组就是感觉简单?因为数组大家都知道怎么遍历,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些。
因此知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,然后顺序累加就可以了

递归

遍历顺序如图所示:
image.png
本题依然可以使用一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。
pre指针的使用技巧,我们在530. 二叉搜索树的最小绝对差501. 二叉搜索树中的众数都提到了,这是常用的操作手段。

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

这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。
同时定义一个sum更新总和。(这里sum就是前面说的pre root为前面说的cur)
代码如下:

private int sum;
public void traversal(TreeNode root){}
  • 确定终止条件

遇空就终止。

// 终止条件
if (root == null) return;
  • 确定单层递归的逻辑

注意要右中左来遍历二叉树, 重点是中的逻辑比较重要。先更新sum数值后给节点赋值
代码如下:

// 单层递归逻辑 - 右中左
traversal(root.right); // 右
// 中
sum += root.val; // 更新sum
root.val = sum; // 给节点赋值
traversal(root.left); // 左

递归法整体代码如下:

public TreeNode convertBST(TreeNode root) {
    sum = 0;
    // 遍历顺序:右中左
    traversal(root);
    return root;
}

public void traversal(TreeNode root){
    // 终止条件
    if (root == null) return;

    // 单层递归逻辑 - 右中左
    traversal(root.right); // 右
    // 中
    sum += root.val; // 更新sum
    root.val = sum; // 给节点赋值
    traversal(root.left); // 左
}

到此二叉树篇目已结束了,就部分看来,对比其他章节知识量更多,更考验逻辑能力。当然收获也更多了,需要梳理整合,计划周末整顿休息出一篇二叉树总结篇,总结一下二叉树章节~

学习资料:

669. 修剪二叉搜索树

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

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