本节是二叉搜索树的最后一节,主要内容围绕二叉搜索树的相关操作。
总结
- 二叉搜索树的插入:插入位置为空节点
- 二叉搜索树的删除:删除时要考虑被删除节点的度,不同的度操作不同
- 二叉搜索树不仅仅可以视为一个单调递增数组,反中序遍历(右中左)也可以是单调递减数组
LeetCode-701.二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
如何向二叉搜索树中插入值呢?
- 寻找过程中,如果其某孩子节点为空且可插入,再插入
插入节点并不需要遍历BST,因为插入的路径是可判断的,因此可能使用迭代的方法思路更加清晰明了
迭代
var insertIntoBST = function (root, val) {
if (!root) return new TreeNode(val);
//因为要返回二叉树,因此不能改变root,新增指针指向root
let curNode = root;
while (curNode) {
//当前节点有空子树
if (!curNode.left || !curNode.right) {
//判断是否可插入
if (!curNode.left && val < curNode.val) {
const node = new TreeNode(val);
curNode.left = node;
}
if (!curNode.right && val > curNode.val) {
const node = new TreeNode(val);
curNode.right = node;
}
}
//此时curNode无空子树,移动curNode
if (curNode.val > val) {
curNode = curNode.left;
} else {
curNode = curNode.right;
}
}
return root;
};
递归
递归分析:
- 返回值和入参:无返回值,入参为当前节点以及待插入值
- 终止条件:空节点时终止
- 单层循环逻辑:类似于前序遍历
- 中:判断当前节点是否有空子树,再判断是否可插入
- 左/右:依赖二叉搜索树的性质,递归寻找插入位置
function instertRecursive(node, val) {
if (!node) return;
if (!node.left || !node.right) {
const newNode = new TreeNode(val);
if (!node.left && node.val > val) {
node.left = newNode;
} else if (!node.right && node.val < val) {
node.right = newNode;
}
}
if (node.val > val) {
insertIntoBST(node.left, val);
} else {
insertIntoBST(node.right, val);
}
}
var insertIntoBST = function (root, val) {
if (!root) {
return new TreeNode(val);
}
instertRecursive(root, val);
return root;
};
LeetCode450.删除二叉搜索树中的节点
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
对于在二叉搜索树中删除节点,情况会相对复杂,我们先分析可能遇到的情况:
- 如果要删除的节点不存在,则直接返回原树
- 存在:
- 删除节点的度为0,即叶子节点,可直接删除
- 删除节点的度为1,将其存在的子树替代目标节点
- 删除节点的度为2,有两种方法:
- 方法一:
- 将该节点的左子树的最大值或右子树的最小值删除(一定是一个度为1的节点,因此递归即可)
- 然后使用被删除节点值覆盖当前节点值
- 方法二:
- 将该节点的左子树挂到右子树的最小值的左子树上
- 此时当前节点变为一个度为1的节点,将目标结点的右子树替代目标结点
- 方法一:
这里再给出方法一和方法二的图示过程:
方法一:
方法二:
分析了这么多,终于可以写代码了:
方法一
代码中如何删除某个节点?
- 删除时通常需要被删除节点的父节点来辅助删除操作,因此我们在寻找当前节点时,要保存其父节点(在二叉搜索树的搜索时,保存父节点实际就是上一个遍历的结点)
- 如果删除的是根节点,那么此时无父节点,因此需要注意删除前的逻辑判断
var deleteNode = function (root, key) {
if (!root) return root;
//寻找到目标节点和其父节点
let curNode = root;
let parentNode = null;
while (curNode) {
if (curNode.val === key) {
break;
}
parentNode = curNode;
if (curNode.val > key) {
curNode = curNode.left;
} else {
curNode = curNode.right;
}
}
//没找到目标节点
if (!curNode) return root;
if (!curNode.left && !curNode.right) {//curNode 的度为0 通过parentNode将其删除
if (root === curNode) {//如果删除节点为根节点且为叶子节点,此时parentNode为null,根节点直接置为null
root = null;
}
else if (parentNode.left === curNode) parentNode.left = null;
else if (parentNode.right === curNode) parentNode.right = null;
} else if (!curNode.left || !curNode.right) {//curNode 的度为1
if (root === curNode) {
root = root.left ? root.left : root.right;
} else if (parentNode.left === curNode) {
parentNode.left = curNode.left ? curNode.left : curNode.right;
} else if (parentNode.right === curNode) {
parentNode.right = curNode.left ? curNode.left : curNode.right;
}
}
//curNode 度为2
if (curNode.left && curNode.right) {
//找到其右子树最小元素
let temp = curNode.right;
while (temp.left) {
temp = temp.left;
}
//递归删除最小元素
deleteNode(root, temp.val);
//用temp覆盖curNode
curNode.val = temp.val;
}
return root;
};
方法二
var deleteNode = function (root, key) {
if (!root) return root;
if (root.val > key) {
root.left = deleteNode(root.left, key);
} else if (root.val < key) {
root.right = deleteNode(root.right, key);
}
if (root.val === key) {
//叶子节点
if (!root.left && !root.right) {
return null;
}
//只有一个子树
if (!root.left && root.right) {
return root.right;
} else if (root.left && !root.right) {
return root.left;
}
//两个都在 将左子树挂到右子树的最小值的左子树上,再将当前节点置为右子树
if (root.left && root.right) {
let rightMinNode = root.right;
while (rightMinNode.left) {
rightMinNode = rightMinNode.left;
}
rightMinNode.left = root.left;
root.left = null;
root = root.right;
return root;
}
}
return root;
};
总的来说,方法一的思路更加清晰简单,而方法二的代码更加干净。
LeetCode-669.修剪二叉搜索树
给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。
解决问题的关键在于分析出我们在修建过程中会遇到的情况:
- 如果当前结点大于范围,那么该节点与其右子树是要删减的,但其左子树不确定---继续修剪左子树,右子树无用
- 如果当前结点小于范围,那么该节点与其左子树是要删减的,但其右子树不确定---继续修剪右子树,左子树无用
- 如果当前结点在范围内,左右子树都不确定---左右子树都继续修剪
如何修剪?
- 实际上利用递归就可以完成修剪,我们可以将修剪完成的左/右子树赋值给该节点左右子树;
递归
递归分析:
- 返回值和入参:返回的是修剪好的子树根节点,入参为当前结点以及范围
- 终止条件:
- 空节点 null
- 单层循环逻辑:
- 如果当前结点在范围外,递归修剪其左子树或右子树,并只返回修建好的子树,因为另一边是无用的
- 如果当前节点在范围内,递归修剪其左子树和右子树,并且将修剪好的左子树和右子树分别挂到当前结点上
代码实现:
var trimBST = function (root, low, high) {
if (!root) return null;
//在范围左边,说明其右子树中可能存在有效值,修剪右子树并返回
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;
};
LeetCode-108.将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
思路
这不就是构造二叉树吗?相比做过二叉树构造那一章节题目的同学会觉得很简单,这里除了构造二叉树,还有就是维持高度平衡,然而对于有序数组,每次取中间的结点作为根,就不会打破平衡了。这么一说是不是很简单了。
直接上代码:
function buildTreeRecursive(nums) {
if (!nums.length) return null;
//创建根节点
const mid = Math.floor(nums.length / 2);//中间节点索引
const rootNode = new TreeNode(nums[mid]);
//将nums从mid处分成左子树和右子树数组
const leftNums = nums.slice(0, mid);
const rightNums = nums.slice(mid + 1);
//递归连接左右子树
rootNode.left = buildTreeRecursive(leftNums);
rootNode.right = buildTreeRecursive(rightNums);
return rootNode;
}
var sortedArrayToBST = function (nums) {
return buildTreeRecursive(nums);
};
LeetCode-538.把二叉搜索树转换为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
起初我想的是,那不就是每个节点的新值是右子树的和吗?然后忽然想起来,对于一个节点的左子树上的节点,比他大的值不仅仅是右子树,还有其右边的父节点,那就有点难办了。
转换思路,我们说过二叉搜索树中序遍历有序:
假设我们现在有一个中序遍历结果:[1,2,5,6],那么每个节点的新值等于什么?
- 从左向右看就是:1的新值是
1+2+5+6,2的新值是2+5+6.... - 从右向左看就是:6的新值是
6,5的新值是6+5,2的新值是(6+5)+2....
发现了吗,如果从右向左看,该节点的新值等于上一个值加当前节点值(上一个值在上一次已经被更新了)
那么问题就变得简单了,我们需要解决的问题是:
- 从右向左,也就是遍历出递减结果--------反中序呗,右中左就好了
- 保存上一个值,之前我们专门讲过,很简单吧----中间节点处理逻辑后更新
preNode
递归
var convertBST = function (root) {
let preNode = null;
//递归函数
function reverseInorderRecursive(node) {
if (!node) return;
//右
reverseInorderRecursive(node.right);
//中
if (preNode) {
node.val += preNode.val;
}
//更新preNode
preNode = node;
//左
reverseInorderRecursive(node.left);
}
reverseInorderRecursive(root);
return root;
};