关于二叉搜索树的题目,貌似普遍用迭代法比递归法简单。目前做到的除了98验证二叉搜索树都是如此。
701其实很简单,只是之前自己想不到直接添加到叶子节点这个方法。
注意一个问题:判断需要返回 root
还是 newRoot
- 返回
root
:当操作不改变树的根节点时,返回root
。例如,在插入或删除时,根节点没有被替换,树的结构依然连贯。 - 返回
newRoot
:当操作导致树的根节点发生变化时,返回newRoot
。这通常发生在根节点为空(即空树)或删除操作导致根节点被替换的情况下。
判断是否需要返回 root
还是 newRoot
的关键是看操作是否会改变树的根节点结构。如果根节点没有发生变化,通常返回 root
即可;否则,返回新的根节点。
235. 二叉搜索树的最近公共祖先
因为刚做了236. 二叉树的最近公共祖先,一看到这个题目的时候思考方式还是从底向上遍历,又想利用二叉搜索树的性质,毫无头绪,没有想法。看了题解才觉得这道题的想法这么妙,就很简单。
总结
- 对于二叉搜索树的最近祖先问题,其实要比普通二叉树的最近公共祖先问题简单的多。
- 不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。
- 最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。
思路:
- 不需要遍历整棵树,找到结果直接返回!
- 因为二叉搜索树是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。
注意:如何保证该节点就是最近公共祖先呢?
从根节点搜索,第一次遇到 cur节点是数值在[q, p]区间中,即 节点5,此时可以说明 q 和 p 一定分别存在于 节点 5的左子树,和右子树中。
此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。
所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[q, p]区间中,那么cur就是 q和p的最近公共祖先。
递归法
递归遍历顺序,本题就不涉及到 前中后序了(这里没有中节点的处理逻辑,遍历顺序无所谓了)。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 递归法
// 如果 cur->val 大于 p->val,同时 cur->val 大于 q->val,那么就应该向左遍历(目标区间在左子树)。
if (root.val > p.val && root.val > q.val)
return lowestCommonAncestor(root.left,p,q);
// 如果 cur->val 小于 p->val,同时 cur->val 小于 q->val,那么就应该向右遍历(目标区间在右子树)。
if (root.val < p.val && root.val < q.val)
return lowestCommonAncestor(root.right,p,q);
// 剩下的情况,就是cur节点在区间[p,q]或者[q,p]
// 那么root就是最近公共祖先了,直接返回root
return root;
}
}
235和236递归函数返回值的区别
如果递归函数有返回值,如何区分要搜索一条边(235),还是搜索整个树(236)。
搜索一条边的写法:
if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;
搜索整个树写法:
left = 递归函数(root->left);
right = 递归函数(root->right);
left与right的逻辑处理;
235就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,立刻返回。
迭代法(简单)
利用其有序性,迭代的方式还是比较简单的,解题思路在递归中已经分析了。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 迭代法
while (root != null) {
if (root.val > p.val && root.val > q.val)
root = root.left;
else if (root.val < p.val && root.val < q.val)
root = root.right;
else
return root;
}
return null;
}
}
701. 二叉搜索树中的插入操作
本题一开始看到没有思路。当不考虑题目中提示所说的改变树的结构的插入方式,直接插入到叶子节点就简单了。在二叉搜索树中的插入操作,其实根本不用重构搜索树。
只要按照二叉搜索树的规则去遍历,遇到空节点(末尾节点)就插入节点就可以了。
递归法(看这个)
搜索树是有方向的,可以根据插入元素的数值,决定递归方向。
遍历整棵搜索树简直是对搜索树的侮辱。
不需遍历整棵树!
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
// 递归法
// 如果当前节点为空,也就意味着val找到了合适的位置,此时创建节点直接返回。
if (root == null)
return new TreeNode(val);
if (root.val < val) {
root.right = insertIntoBST(root.right, val);// 递归创建右子树
} else if (root.val > val) {
root.left = insertIntoBST(root.left, val);// 递归创建左子树
}
return root;
}
}
迭代法(供参考)
在迭代法遍历的过程中,需要记录一下当前遍历的节点的父节点,这样才能做插入节点的操作。
用记录pre和cur两个指针的技巧。
- 代码的整体思路是使用一个指针
root
遍历树,寻找合适的插入位置。遍历过程中通过pre
记录父节点,最终在树的叶节点处插入新的值。 - 最后返回的是最初保存的根节点
newRoot
,因为二叉搜索树的插入操作不会改变根节点的位置。
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
// 迭代法
if (root == null) return new TreeNode(val);
TreeNode newRoot = root; // 保存初始根节点
TreeNode pre = root; // pre用于保存root的前一个节点,即插入过程中最后一次遍历到的父节点。因为当root遍历到null时,需要通过pre来插入新节点到合适的位置。
while (root != null) {
pre = root; // 每次移动时,将pre更新为当前节点
if (root.val > val) {
root = root.left;
} else if (root.val < val) {
root = root.right;
}
}
// 插入新节点
if (pre.val > val)
pre.left = new TreeNode(val);
else
pre.right = new TreeNode(val);
return newRoot; // 返回初始的根节点
}
}
450. 删除二叉搜索树中的节点
递归法1(看这个)
二叉搜索树中删除节点有以下五种情况:
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点(此时相当于左空右不空,情况三)。
这里用父节点直接接收返回值。很妙,不需要单独定义一个父节点。(把新的节点返回给上一层,上一层就要用 root->left 或者 root->right接住)具体见下面代码。
if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);
整体代码如下:
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
// 递归法
if (root == null) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了
if (root.val == key) {
if (root.left == null) { // 第二三种情况,左右都为空(叶子节点),左空右不空
return root.right;
} else if (root.right == null) { // 第四种情况,左不空右空
return root.left;
} else { // 第五种情况,左不空右不空
TreeNode cur = root.right;
while (cur.left != null)
cur = cur.left; // 找到右子树中的最左边的节点
cur.left = root.left; // 将要删除节点root的左子树放到cur的左子树下
// 此时要删除的节点相当于左空右不空,第三种情况
return root.right;
}
}
if (root.val > key) root.left = deleteNode(root.left, key);
if (root.val < key) root.right = deleteNode(root.right, key);
return root;
}
}
普通二叉树的删除方式
普通二叉树的删除方式(没有使用搜索树的特性,遍历整棵树),用交换值的操作来删除目标节点。
代码中目标节点(要删除的节点)被操作了两次:
- 第一次是和目标节点的右子树最左面节点交换。
- 第二次直接被NULL覆盖了。
(我没理解,如果是普通二叉树,为什么要用右子树最左面节点交换,普通二叉树没有排序呀)
递归法2:
对每个节点进行递归处理,寻找需要删除的节点,然后根据节点的不同情况(无子节点、一个子节点、两个子节点)执行相应的删除操作。
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
root = delete(root, key);
return root;
}
private TreeNode delete(TreeNode root, int key) {
if (root == null) return null; // 没有找到要删除的节点
if (root.val > key) {
root.left = delete(root.left, key); // 递归左右子树
} else if (root.val < key) {
root.right = delete(root.right, key); // 递归左右子树
} else {
if (root.left == null) return root.right;
if (root.right == null) return root.left;
// 处理有两个子节点的情况
// 用右子树中最小的节点(即右子树的最左边的节点)替换当前节点的值。
TreeNode tmp = root.right;
while (tmp.left != null)
tmp = tmp.left;
root.val = tmp.val;
root.right = delete(root.right, tmp.val); // 再递归地删除右子树中的这个最小节点
}
return root;
}
}
迭代法
迭代法,就是模拟递归法中的逻辑来删除节点,但需要一个pre记录cur的父节点,方便做删除操作。(迭代法较难)麻烦
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
// 迭代法,需要pre记录父节点,麻烦
if (root == null) return null;
//寻找对应的对应的前面的节点,以及他的前一个节点
//查找待删除节点
TreeNode cur = root;
TreeNode pre = null;
while (cur != null) {
if (cur.val < key) {
pre = cur;
cur = cur.right;
} else if (cur.val > key) {
pre = cur;
cur = cur.left;
} else {
break;
}
}
// 特殊情况处理:根节点就是目标节点
if (pre == null)
return deleteOneNode(cur);
// 处理目标节点是父节点的左子节点或右子节点
if (pre.left != null && pre.left.val == key)
pre.left = deleteOneNode(cur); // 删除节点,并更新父节点的 left
if (pre.right != null && pre.right.val == key)
pre.right = deleteOneNode(cur);
return root;
}
// 删除当前的节点,并处理它的子树连接
public TreeNode deleteOneNode(TreeNode node) {
if (node == null)
return null;
if (node.right == null)
return node.left;
TreeNode cur = node.right;
while (cur.left != null)
cur = cur.left;
cur.left = node.left; // 一旦找到这个最小节点,把node的左子树连接到 这个右子树最小的左子树节点上
return node.right; // 返回右子树作为删除节点后的新的根节点。
}
}
第二十天的总算是结束了,直冲Day21!