🔔前言 :为学日进,为道日损。与诸君携手共勉 💖💖💖
💓二叉搜索树(BST树)
⭐什么是二叉搜索树
二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树
二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根结点的键值。
- 非空右子树的所有键值大于其根结点的键值。
- 左、右子树都是二叉搜索树。
举个栗子:
二叉搜索树:
非二叉搜索树:
⭐二叉搜索树的特别函数
⭐Find函数
从二叉搜索树BST中查找元素X,返回其所在结点的地址
实现逻辑:
⏩从根节点开始,树为空,返回NULL
⏩若树不为空,将根节点和传入的数据X进行比较:
- 若X小于根节点的键值,进入左子树继续搜索
- 若X大于根节点的键值,进入右子树继续搜索
- 若两者比较结果相等,搜索完成,返回此结点的指针
常言道:一图值前言⛅
⭐所需结构体
//为了简化后文重复代码的书写,用typedef对指向树结构的指针取两个别名位置类型的"Position"和搜索二叉树类型的"SearchTree"
typedef struct TreeNode *Position;
typedef struct TreeNode *SearchTree;
struct TreeNode{
ElementType Element;
SearchTree Left;
SearchTree Right;
};
⭐具体实现的参考代码
Position Find(ElementType X, SearchTree T)
{
if(T == NULL) return NULL;
if(X < T->Element) return Find(X,T->Left);//带着X进入左子树
if(X > T->Element) return Find(X,T->Right);//带着X进入右子树
else return T;//搜索成功
}
⭐Find函数的性质挖掘
从二叉搜索树的特性来看,它的左子树总是比根节点的键值小,所以最小值只能出现在左子树,一个结点它下面没有比它小的左子树了,那它就是最小的了。最大值同理,右子树总是比根节点的键值大,一个结点它下面没有比它大的右子树了,那它就是最大的了。
🌻FindMin函数
从二叉搜索树BST中查找并返回最小元素所在结点的地址。
🌻具体实现的参考代码
Position FindMin(SearchTree T)
{
if(T == NULL) return NULL;
else
{
if(T->Left == NULL) return T;//搜索成功,没有比它小的了
else return FindMin(T->Left);//继续到左子树的左子树中查找
}
}
🌻FindMax函数
从二叉搜索树BST中查找并返回最大元素所在结点的地址。
🌻具体实现的参考代码
上述两份代码都是使用的递归的方式实现的。这里变一下~。用函数迭代法
//函数迭代法
Position FindMax(SearchTree T)
{
if(T == NULL) return NULL;
while(T->Right) return T->Right;
return T;
}
🌻闲谈一刻
递归常常被称为—— “一种优雅的解决问题的方式”。对于它的态度,分为了三种阵营,恨之入骨,爱不释手,恨了又爱(真香)。
笔者我就是真香狗。因为递归写代码可以让方案更整洁喔。比如在dfs中,只用写好当前这一步的逻辑,下一步则递归进去,实现和当前这步一样的操作就好。但是递归在性能上是不及循环的喔~!
每个递归函数都有两部分,基线条件和递归条件。递归条件是指函数自己调用自己,而基线条件(⊙o⊙)…,则是指函数不再自己调用自己了,可以获得其他值回溯回去了,从而避免形成无限循环。典型例子:斐波那契数列
⭐Insert函数
因为二叉搜索树独特的性质,导致了插入一个数据以后,仍然要保证左子树比根节点小,右子树比根节点大。所以对于插入而言,最主要的确认插入的位置
老规矩,上图~
🌻具体实现的参考代码
SearchTree Insert(ElementType X,SearchTree T)
{
//如果树为空,生成并返回一个有一个结点的二叉搜索树
if(T ==NULL)
{
T = malloc(sizeof(struct TreeNode));
T->Element = X;
T->Left = T->Right = NULL;
}else
{
if(X < T->Element) T->Left = Insert(X,T->Left);//如果比根节点小,就向左子树的方向进行插入
else
if(X > T->Element) T->Right = Insert(X,T->Right);//如果比根节点大,就向右子树的方向进行插入
}
return T;
}
Delete函数(难点)
🌻情况一
要删除的是叶结点:直接删除,并再修改其父结点指针—置为NULL
🌻情况二
要删除的结点只有一个孩子结点: 将其父结点的指针指向要删除结点的孩子结点
🌻情况三
要删除的结点有左、右两棵子树: 用另一结点替代被删除结点:用 右子树的最小元素 或者 左子树的最大元素
🌻具体实现的参考代码
SearchTree Delete(ElementType X, SearchTree T)
{
Position TmpCell;
//找到要删除的位置在哪里
if(T == NULL) printf("Element not found\n");
else
if(X < T->Element) T->Left = Delete(X,T->Left);
else
if(X > T->Element) T->Right = Delete(X,T->Right) ;
//具体的开始执行删除操作了
//两个儿子都存在的情况
else
if(T->Left && T->Right) //两个孩子
{
TmpCell = FindMin(T->Right) ;//找到右子树的最小值或者左子树的最大值来代替那个要被删除的节点
T->Element = TmpCell->Element;
//删除右边那个被抽上去替代的节点
T->Right = Delete(T->Element,T->Right);
}else //一个或者没有孩子
{
TmpCell = T;
if(T->Left == NULL) T = T->Right;//结合上图,只有一个孩子的时候,是让这孩子挪到要删除结点的位置,也就实现了 将其父结点的指针指向要删除结点的孩子结点
}
else if(T->Right == NULL) T = T->Left;
free(TmpCell);
}
return T;
}
💓 二叉平衡树(AVL树)⭐⭐
💓什么是平衡二叉树
⭐前戏~
⭐树的高度
树的深度(Depth):树中所有结点中的最大层次是这棵树的深度或者高度
⭐平衡因子
平衡因子(Balance Factor,简称BF): BF(T) = hL-hR,
其中hL和hR分别为T的左、右子树的高度
⭐平衡二叉树
平衡二叉树(Balanced Binary Tree)(AVL树),平衡二叉树中不存在平衡因子大于 1 的节点,即|BF(T) |≤ 1。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。
🗽注意:平衡二叉树也是一棵搜索树,所以搜索呀,删除呀等操作,对它一样是适用的,只是插入以后,可能会破坏平衡因子,所以待会要单独讲解平衡二叉树插入以后的调整问题,让树仍然保持是棵查找树
⭐平衡二叉树的性质
给定结点数为 n的AVL树的最大高度为O(log2n)
⭐平衡二叉树的作用
可能有小伙伴读完什么树高、平衡因子,吧啦吧啦又来了一串定义、要求、性质,头已经大了
关于平衡二叉树的作用,因为笔者暂时阅历也有限,除了在leetcode的面试题里面发现了它们的踪影
但是发现它和斐波那契数列有千丝万缕的关系喔,可爱的斐波那契数列又和耳熟能详的黄金分割率又捋不清的暧昧关系喔。感觉里面有一个大瓜呀
好啦好啦,吃瓜吃饱了,嗝~~~ 从仙术中回来啦,下面准备要修炼的适合咱们的术法了🎇🎇
💓平衡二叉树的调整(难点)
⭐右单旋
形象化例子:将Mar 、 May 、 Nov依次插入
不平衡的发现者是Mar,麻烦结点Nov 在发现者右子树的右边,因而叫 RR 插入,需要RR 旋转(右单旋)
右单旋原理图如下:
⭐右单旋理解
小伙伴们看着原理图。现在可以把这棵树想象得很柔软,然后你握住了平衡因子处于中间地位的B结点,闭上双眼,使劲摇动它,在重力的作用下,发现者B结点变成了新的根。由搜索二叉树的性质告诉我们,在原来的树中,B结点是大于A结点的,于是现在新树中A结点成为了B结点的左子树,BL因为比B结点小但是又比A结点大,所以挂在了A的右子树上。
⭐左单旋
形象化例子:将Aug 和 Apr插入到原本的平衡二叉树中
不平衡的发现者是Mar,麻烦结点Apr在发现者左子树的左边,因而叫 LL 插入,需要LL 旋转(左单旋)
左单旋原理图如下:
⭐左单旋理解
咱们继续看着原理图。现在可以把这棵树想象得很柔软,然后你握住了平衡因子处于中间地位的B,闭上双眼,使劲摇动它,在重力的作用下,发现者B变成了新的根。二叉查找树的性质告诉我们,在原来的树中,B是小于A的,于是新树中,A变成了B的右子树,BL因为还是比B小,依旧挂在B的左子树,BR了,也是根据二叉查找树的性质,它只能到现在A的左子树挂着了。
⭐左右双旋
形象化例子:将Jan插入到Mar的左子树上
不平衡的发现者是May,麻烦结点Jan在左子树的右边,
因而叫 LR 插入,需要LR 旋转(左右单旋)
左右双旋原理图如下:
⭐左右双旋理解
重点关注那三个结点,只要它们三平衡了,根据二叉搜索树的性质,其他点也可以相应调整平衡了。咱们继续看着原理图。现在子树C,对应上图的Mar。现在确实有一项插入进来了,无论它在插入到C的左子树还是C的右子树,它都来破坏平衡了。于是,我们可以将现在的树看作四棵子树由3个结点连接。为了重新平衡,我们可以看到不能让A再做根了,唯一的选择就是把平衡因子的大小是中间值C作为新的根结点,再结合二叉查找树的性质,迫使B做C的左儿子,A左C的右儿子,从而完全确定整棵树的最终位置。
⭐右左双旋
形象化例子:将Fer插入到原本平衡的Dec的上面
不平衡的发现者是Aug,麻烦结点Feb在右子树的左边,
因而叫 RL 插入,需要RL 旋转
右左双旋原理图:
⭐左右双旋理解
因为和上文的左右双旋类似,相信大家也理解啦。所以这里不在赘述啦~
💓总结
到这里为止,平衡二叉树为了调整插入而带来的不平衡的四种四种方式已经讲完啦~
数起来是四种,实际就是一种,有没有发现了。左单旋、右单旋要关注的核心结点是被破坏以后衡因子居中的结点
对于左右双旋和右左双旋也是类似的,我上文着重说,注意那三个结点,咱们要去处理它们三个中平衡因子居中的那个结点。其实也可以做这种想,小学集合站队的时候,中等高度的小盆友出来出来站好了,其他的位置就可以类推了,这里就是取平衡因子居中的点为标杆。
⭐留白
关于平衡二叉树调整的实现代码,笔者想后续结合习题讲解出来,有习题作为背景,可以更好的拿捏主它,得先委屈各位小伙伴等一阵子了💓