代码随想录day22
活着就很好呀
目录
前置知识:二叉搜索树
要学二叉搜索树,那首先什么是二叉搜索树? 二叉搜索树,又叫二叉排序树或二叉查找树。
见名知意,二叉搜索树首先是棵二叉树,从它的别名可以看出,二叉搜索树是有数值的树(不然怎么排序呢),且有序(不然干嘛排序呢)。既然有数且有序,肯定有合乎这俩的性质。
二叉搜索树有以下的性质:
- 若左子树不空,那左子树所有节点的值均 < 根节点的值。
- 若右子树不空,那右子树所有节点的值均 > 根节点的值。
- 左右子树也均为二叉搜索树。
仔细观察上面的二叉搜索树,你会发现当对二叉搜索树进行中序遍历时,得到的结果是一个有序的序列,比如最左侧二叉树遍历结果是:
知道了概念,那二叉搜索树有什么用处呢?其实二叉搜索树为了搜索(查找)而生,但它又不仅仅是为了搜索(查找)。
还有另外的目的,那就是提高【插入数据】和【删除数据】的速度。
那下面就来看一下二叉搜索树的几个操作是怎么整的。
二叉搜索树操作
二叉搜索树的操作主要有 3 种:
- 查找操作
- 插入操作
- 删除操作
下面我们就来看下这 3 个操作是怎么实现的。
查找操作
根据二叉搜索树的定义,在二叉搜索树中查找一个节点,其实就 3 步:
- 将查找的节点根节点比较,如果相等,则直接返回。
- 如果查找的节点 < 根节点,则在左子树中递归查找。
- 如果查找的节点 > 根节点,则在右子树中递归查找。
比如对于下图:
我们此时要查找 node = 31 的节点。
第 1 步:与根节点比较,根节点的值为 29,node > 29,所以去右子树中继续查找。
第 2 步:此时右子树的根节点值为 30,node > 30,则继续在右子树中查找。
第 3 步:当前右子树的根节点值为 31,与 node 相等,直接返回。
插入操作
二叉搜素树的插入操作,其实和二叉树的插入一样,将要插入的节点 node 放在树中合适的位置。
这个“寻找合适位置”的过程和二叉搜索树的查找操作类似,只不过多了一步“插入”,而且对于新插入的节点来说,这个“位置”一般都是在叶子节点上。
具体的步骤就是:
- 如果插入的节点值比根节点的数值小:
- 如果根节点的左子树为空,那该节点直接插入到根节点的左孩子位置。
- 如果根节点的左子树不为空,继续遍历左子树寻找插入的位置。
- 如果插入的节点值比根节点的数值大:
- 如果根节点的右子树为空,那该节点直接插入到根节点的右孩子位置。
- 如果根节点的右子树不为空,继续遍历右子树寻找插入的位置。
比如对于下图:
我们此时要插入 node = 3 的节点。
第 1 步:根节点的值为 29,node < 29,且节点的左孩子不为空,继续遍历左子树寻找插入的位置
第 2 步:左子树的根节点值为 4,node < 4,且节点的左孩子不为空,继续遍历左子树寻找插入的位置。
第 3 步:左子树的根节点值为 0,node > 0,且节点的右孩子为空,插入该节点右孩子的位置。
删除操作
删除操作是二叉搜索树相对较麻烦的一个操作,因为我们在删除节点之后仍然要保持剩下的二叉树仍是二叉搜索树,即依然满足二叉搜索树的特性。
你不能一刀下去,李逵变李鬼。二叉搜索树的删除操作涉及的情况比较多,主要是三种,我们一一来看。
情况 1:删除节点为二叉树的叶子节点。
这种情况很好处理,通过二叉搜索树的查找操作找到节点,然后直接删掉就好了。删掉叶子节点对二叉树的其它节点没有什么影响。
比如对于下图:
我们此时要删除 node = 0 的节点,查找到该节点,然后直接删除。
情况 2 :删除的节点只有左子树或者右子树。
碰到这种情况也比较好解决,那就是直接用删除节点的父节点指向它的子树即可。
比如对于下图:
我们此时要删除 node = 30 的节点,查找到该节点,然后删除后,二叉搜索树的变化如下所示:
情况 3:删除的节点既有左子树又有右子树。
这个就稍微复杂点了,过程我就略过了,也不是很重要,直接说答案:
就是将要删除节点 node 位置上替换成左子树最右边的节点或者右子树最左边的节点。
即左子树的最大值或者右子树的最小值。
比如对于下图:
我们此时要删除 node = 4 的节点,它的左子树的最大值为 0,右子树的最小值为 5。所以删除完 node = 4 后,二叉搜索树可以变成下面这样:
或者
好啦,二叉搜索树的入门基础到这就讲完了。主要的内容其实就是在二叉搜索树的性质和操作做了简单明了且通俗易懂的讲解。
基本上重要的内容都在这了,只要能认认真真看到这的,肯定已经对二叉树搜索树有了直观的认识。
下面就是在实战中来体味二叉搜索树的魅力了~
以上内容来自李狗蛋,感谢。
235. 二叉搜索树的最近公共祖先
今天解决二叉搜索树的最近公共祖先,之前我们一起做过【二叉树的最近公共祖先】这道题,如果你认真看过,那这道题对你来说么的问题。
什么?你说你没看过?问题不大,看这篇也一样~
题意:
给定一个二叉搜索树,找到该树中两个指定节点的最近公共祖先。
示例
输入:root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出:2
解释:节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
提示
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉搜索树中
题目解析:
二叉搜索树的最近公共祖先这道题,难度简单,因为自带的性质所以比之前的二叉树的最近公共祖先难度低了一个Level。
题目中对于公共祖先的定义是这样的:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大。
你看,说的真好,听君一席话,如听一席话!
我在二叉树的最近公共祖先中说过,对于最近公共祖先,我感觉你们就记住,对于节点 p 和 q 来说,如果 node 为其最近公共祖先,那么 node 的左孩子和右孩子一定不是 p 和 q 的公共祖先。
比如对于下图, 如果 p = 7、q = 0,3 就是 p 和 q 的最近公共祖先(3 的左孩子 5 和右孩子 1 都不是 p 和 q 的公共祖先)。
也正是根据这个,我们得出对于普通的二叉树,如果节点 node 为 p 和 q 的最近公共祖先,那么会有 3 种情况:
- p 和 q 分别在节点 node 的左右子树中。
- node 即为节点 p,而q 在节点 p 的左子树或右子树中。
- node 即为节点 q,而p 在节点 q 的左子树或者右子树中。
本来按照上面的情况枚举 5 种情况就完事了,但是咱二叉搜索树带性质啊:二叉搜索树是一棵有序树!
就这一个【有序】就赢了!简单了好多:
- 如果当前节点的值 node.val 在 [p, q] 之间,那 node 就是最近公共祖先。
- 如果当前节点的值 node.val 大于 p 和 q 的值,那证明 p 和 q 在 node 的左子树中,向左子树继续遍历。
- 如果当前节点的值 node.val 小于 p 和 q 的值,那证明 p 和 q 在 node 的右子树中,向右子树继续遍历。
递归法:
(1) 找出重复的子问题。
这里的重复子问题就很简单,就是递归左子树,递归右子树:
- 如果当前节点的值 node.val 大于 p 和 q 的值,那证明 p 和 q 在 node 的左子树中,向左子树继续遍历。
- 如果当前节点的值 node.val 小于 p 和 q 的值,那证明 p 和 q 在 node 的右子树中,向右子树继续遍历。
// 如果当前节点的值 node.val 大于 p 和 q 的值,那证明 p 和 q 在 node 的左子树中
if root.Val > p.Val && root.Val > q.Val {
// 遍历左子树
return digui(root.Left, p , q)
}
// 如果当前节点的值 node.val 小于 p 和 q 的值,那证明 p 和 q 在 node 的右子树中
if root.Val < p.Val && root.Val < q.Val {
// 遍历右子树
return digui(root.Right, p, q)
}
(2) 确定终止条件。
对于本题来说,终止条件就一个,那就是找到了就结束:
# 如果当前节点的值 node.val 在 [p, q] 之间,那 node 就是最近公共祖先。
return root
整体代码如下:
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
// 如果当前节点的值 node.val 大于 p 和 q 的值,那证明 p 和 q 在 node 的左子树中
if root.Val > p.Val && root.Val > q.Val {
return lowestCommonAncestor(root.Left, p , q)
}
// 如果当前节点的值 node.val 小于 p 和 q 的值,那证明 p 和 q 在 node 的右子树中
if root.Val < p.Val && root.Val < q.Val {
return lowestCommonAncestor(root.Right, p, q)
}
// 处于p和q之间。
return root
}
701、二叉搜索树中的插入操作
题意:
给定二叉搜索树的根节点 root 和要插入的数值 value,将值插入二叉搜索树,返回插入后二叉搜索树的根节点。
输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
示例
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
输出存在两种情况:
或者
提示
注意:可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可,你可以返回任意有效的结果。
题目解析:
二叉搜索树的插入操作是一道经典问题,难度中等。当然这个难度中等是对于没看过我写的二叉搜索树入门文章的同学而言,对于看过的,这道题说实话,洒洒水。
二叉搜素树的插入操作,其实和二叉树的插入一样,将要插入的节点 node 放在树中合适的位置。
这个“寻找合适位置”的过程和二叉搜索树的查找操作类似,只不过多了一步“插入”,而且对于新插入的节点来说,这个“位置”一般都是在叶子节点上。
具体的步骤就是:
- 如果插入的节点值比根节点的数值小:
- 如果根节点的左子树为空,那该节点直接插入到根节点的左孩子位置。
- 如果根节点的左子树不为空,继续遍历左子树寻找插入的位置。
- 如果插入的节点值比根节点的数值大:
- 如果根节点的右子树为空,那该节点直接插入到根节点的右孩子位置。
- 如果根节点的右子树不为空,继续遍历右子树寻找插入的位置。
比如对于下图:
我们此时要插入 node = 3 的节点。
第 1 步:根节点的值为 29,node < 29,且节点的左孩子不为空,继续遍历左子树寻找插入的位置。
第 2 步:左子树的根节点值为 4,node < 4,且节点的左孩子不为空,继续遍历左子树寻找插入的位置。
第 3 步:左子树的根节点值为 0,node > 0,且节点的右孩子为空,插入该节点右孩子的位置
递归三部曲:
1、确定递归函数参数以及返回值
参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢?
可以有,也可以没有,但递归函数如果没有返回值的话,实现是比较麻烦的。
有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作。(下面会进一步解释)递归函数的返回类型为节点类型TreeNode * 。
代码如下:
func insertIntoBST(root *TreeNode, val int) *TreeNode {
}
2、确定终止条件
终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。
代码如下:
if root == nil {
root = &TreeNode{Val: val}
return root
}
// 这里把添加的节点返回给上一层,就完成了父子节点的赋值操作了,详细再往下看。
3、确定单层递归的逻辑:
此时要明确,需要遍历整棵树么?别忘了这是搜索树,遍历整棵搜索树简直是对搜索树的侮辱,哈哈。搜索树是有方向了,可以根据插入元素的数值,决定递归方向。
代码如下:
if root.Val > val {
root.Left = insertIntoBST(root.Left, val)
}
if root.Val < val {
root.Right = insertIntoBST(root.Right, val)
}
return root
整体代码如下:
func insertIntoBST(root *TreeNode, val int) *TreeNode {
if root == nil { // 终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。
root = &TreeNode{Val: val}
return root
}
if root.Val > val {
root.Left = insertIntoBST(root.Left, val)
} else {
root.Right = insertIntoBST(root.Right, val)
}
return root
}
450、删除二叉搜索树
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
说明: 要求算法时间复杂度为 $O(h)$,h 为树的高度。
示例:
思路:
搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑,做好心里准备
递归:
1.确定递归函数参数及返回值。
func deleteNode(root *TreeNode, key int){
}
2.确定终止条件。
遇到空返回,其实这也说明没找到删除的节点,遍历到空节点就直接返回了。
if root == nil {
return root
}
3.确定单层递归逻辑:
在这里就把二叉搜索树中删除节点遇到的情况都搞清楚。
第一种情况:没找到删除的节点,遍历到空节点直接返回了。
第二种情况:找到要删除的节点
- 1、左右孩纸都为空,直接删除节点,返回nil为根节点。
- 2、要删除节点的左孩子为空,右边孩子不为空,则删除节点后,右孩子补位,返回右孩子为根节点。
- 3、要删除节点的右孩子为空,左边孩子不为空,则删除节点后,左孩子补位,返回左孩子为根节点。
- 4、要删除节点的左右孩子都不为空,则将要删除节点的左子树头节点放到要删除节点的右子树的最左面节点的左孩子上,返回要删除节点的右孩子为新根节点。
最后一种情况有点难以理解,看下面动画:
动画中棵二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。
将删除节点(元素7)的左孩子放到删除节点(元素7)的右子树的最左面节点(元素8)的左孩子上,就是把5为根节点的子树移到了8的左孩子的位置。
要删除的节点(元素7)的右孩子(元素9)为新的根节点。这样就完成删除元素7的逻辑,最好动手画一个图,尝试删除一个节点试试。
func deleteNode(root *TreeNode, key int) *TreeNode {
if root == nil {
return root
}
if root.Val < key {
root.Right = deleteNode(root.Right, key)
}
if root.Val > key {
root.Left = deleteNode(root.Left, key)
}
// root为要删除的节点
if root.Val == key {
// 1. 左孩子为空,返回右孩子
if root.Left == nil {
root = root.Right
return root
}
// 2. 右孩子为空,返回左孩子
if root.Right == nil {
root = root.Left
return root
}
// 3. 左右孩子不为空,找到右孩子的最左侧孩子
cur := root.Right
for cur.Left != nil {
cur = cur.Left
}
// 把root左孩子,接到root右节点最左侧孩子的左侧
cur.Left = root.Left
// 把root更新为root的右孩子
root = root.Right
return root
}
return root
}
以上内容来自代码随想录和编程文青李狗蛋。