平衡树从入门到入土【待更新】
O.写在前面
本文的题目叫「平衡树从入门到入土」。因为我想让每一个学过树形结构的同学,都能够学会这种十分重要的数据结构。不论是上课睡觉没有听还是准备提前预习的同学,都能从这篇文章受益。
平衡树的核心思想在于如何保证「平衡」——显然,也是最难理解的。大部分平衡树是通过「旋转」来保持平衡性质的。说句实话,我在分清楚左旋、右旋的时候就花了好长时间。因此,在这篇文章里,我会尽可能清楚的说明什么是左旋、什么是右旋。如果没有理解旋转,那平衡树就是没学。
本文会从最基础的「BST
二叉搜索树」开始,过渡到「AVL
二叉平衡树」、「Treap
树堆」以及「红黑树」,最后到「Splay
伸展树」。我会尽可能的说清楚「怎么想到的」「为什么」「凭什么」等我自己曾经有的困惑,争取让每一个读者都能理解这些伟大且重要的数据结构。
那么,让我们先从最基础的「BST
二叉搜索树」开始吧!
(注:本文中如果在引用块中使用了``xxx``
标记,表示作者想强调此处的xxx是在代码里定义的变量名。这种写法只会在定义函数的时候出现)
I.万恶之源「BST
二叉搜索树」
事先声明1:「
BST
二叉搜索树」并不是平衡树,请注意。
事先声明2:本文「排序」指的是升序排序。
1. 引入
每个算法或数据结构的出现都有其需要解决的问题。「BST
二叉搜索树」用来解决如下问题(对应例题:P5076 【深基16.例7】普通二叉树(简化版):
- 集合中有一些数;
- 支持「添加」操作;
- 求某一个数的「排名」;
排名:将数组元素排序后第一个相同元素之前的数的个数加一。
e.g. 中, 的排名是 。 - 求某一「排名」对于的数;
- 求某一个数的「前驱」和「后继」;
前驱: 的前驱定义为数组元素排序后小于 且最大的数。简而言之,即为数组排序且去重后 前面的一个数。
后继:同「前驱」,只不过是要求大于 且最小的数。简而言之,即为数组排序且去重后 后面的一个数。 - 求「最大/最小值」;
- 「删除」某一个数。
可以看到,所有操作的核心即为动态排序去重,如果使用std::sort()
和线性表肯定是无法在如此庞大的询问量下通过的。此时,我们需要一个新的数据结构。这里,我们引入「BST
二叉搜索树」来解决这个问题。
Q: 为什么想到使用树形结构?
A: 线性表很难胜任动态排序(如果你感兴趣可以去学学「跳表」,就我个人感官而言也不像是线性表)。那么我们也许需要找一个不线性的数据结构来解决问题。于是想到最简单的树——二叉树,并且二叉树的中序遍历和数组的按顺序输出差异不大,我们也可以很方便的完成「插入」操作,那么我们也许可以对二叉树进行一些改造,使之能达成我们的目的。
2. 核心思路
事先声明3:在叙述中,「左/右儿子」会简写为「 」,「父结点」会简写为「 」,某一结点 的权值大小会记为「 」。
如何让其「有序」?不难想到,如果让每个结点的 , 和 有序的话,整棵树的中序遍历也许就是有序的。那么,我们先规定:对于BST
上所有的结点,均满足:
即左比父小,父比右小。
本文称这个不等式为「重要性质1」。
那么,一棵可能的二叉搜索树长这样:
接下来,我们就要对其添加操作了……
2.1 操作
事先声明4:在代码中,
treesize
为结点个数;tree[x]
为结点 ;tree[x].val
为结点 处存的数值;tree[x].cnt
为结点 存的值所出现的次数;tree[x].left
和tree[x].right
分别为结点 的左儿子和右儿子结点;tree[x].size
为结点 作为根时的子树大小。考虑到指针操作对于一部分入门者还是有些困难,我们这里使用数组实+结构体实现:
并且,我强烈建议大家拿纸拿笔画一画,更利于理解。
鲁迅说过,想要对结点操作,首先得有结点。(划掉)。那么我们首先来实现添加结点的操作。
所有的结点,我们可以从上到下找到他应该存在的位置——即把他从根结点下降到叶结点位置——再把他插入。这样我们就维护了「重要性质1」。
给出函数
insert(int x, int v)
,表示在x
为根的树上插入一个结点:
- 如果是一个空树,那么直接新建结点就好了捏!
- 如果不是,不管怎样,这个结点的子树大小得自增一下诶!
- 如果插入的权值等于自身,那么这个结点的
cnt
就要自增一下~这个操作是为了解决“去重”的问题而不改变输入的数据。
- 如果插入的权值大于自身,就应该放到「 」对应的子树里面去;
- 如果插入的权值小于自身,就应该放到「 」对应的子树里面去。
完成了插入,我们接下来继续考虑其他操作。
我们发现,求「最大/最小值」似乎是最好做的。为什么呢?一个直观的感受就是,最左边的是最小的,最右边的是最大的。其实也是这样。
根据「重要性质1」,我们从根结点开始, 肯定比 小,那么 的 肯定比 小,那么 的 的 肯定比 的 小……也就是说,一直向左边跑,找到的就是最小值。
给出函数
getMin(int x)
和getMax(int x)
,分别用于在根x
下找最小值和最大值。
接下来来到一个有点烧脑的操作——找排名。
所谓找排名,换个角度理解,就是找这个数左边有多少个数,再加一即为这个数的排名(因为排名从1开始)。
给出
queryrk(int x, int rk)
用于寻找根x
下排名为rk
的数。
- 如果这个树没有根,显然不存在(在本题中为
INF
) - 如果有根:
- 如果 比 大,就说明左子树里最大的数的排名比 要大,此时应该往左边找;
- 否则,比较左子树大小与该结点重复的次数之和(记为 )与 的大小(请注意,因为结点会重复,所以一定要把重复的给加在一起)
- 如果 :说明要找的元素就在重复结点的里面;
- 否则,就在右子树里找。即寻找在右子树里,相对排名为 的数。
这句话:
否则,就在右子树里找。即寻找在右子树里,相对排名为 的数。
我困惑了很久,我们还是得回到
queryrk
的定义上来。由于我们的
queryrk(int x, int rk)
函数的根不是 而是 ,那么在往右边找的过程中,相对于 排名为 的数,在 中就应该为 。如图:
那为什么往左跳的时候就不需要用相对排名了呢?图都在这里了,相信大家拿出纸笔画一画就知道了。
接下来即为已知数找对应的排名。和上一步操作类似,只不过这次我们只需要利用「二分查找」的思想,不断地向左子树或右子树跳即可。
给出
queryval(int x, int val)
,用于在根x
下寻找val
的排名。
- 如果树是空的,当然不存在排名(本题中为 );
- 如果有根:
- 如果与当前节点的值相等,那么直接返回左子树的大小;
- 否则:
- 如果小一些,就应该往左边跳。
- 如果大一些,就应该往右边跳。并且将答案加上左子树的大小以及根节点的大小。
这句话:
并且将答案加上左子树的大小以及根节点的大小。
我们还是看到上一个图片:
当你在往右跳的时候,假设你从10号跳到了13号,如果直接返回左子树的大小,你会发现其实上一步跳来的根节点所对应的大小,以及上一步跳来的根节点对应左子树的大小,被忽略了。
例如在我们的假设下,如果直接返回13对应左子树的大小,你就忽略了10号和9号。
接下来我们来寻找前继和后驱。如果你真正明白了上面两个操作的实现思路,那么找前驱和找后继就会变得很显然了。这里,我们不给出具体的操作思路而只给出代码,希望大家能自行理解。
给出函数
getbefore(int x, int val, int ans = -INF)
,用于在根x
下寻找val
的前继,如果没找到则返回-INF
;
给出函数getnext(int x, int val, int ans = INF)
,用于在根x
下寻找val
的后驱,如果没找到则返回INF
。
最后一个操作是本题中没有涉及到的,但我认为还是需要提一下:删除。
给出函数
delete(int x, int val)
,用于在根x
下删除值为val
的节点。
- 找到这个节点;
- 考虑这个节点的 :
- 如果 比 大,直接减少即可;
- 如果 就是 :
- 如果该节点为叶节点,直接删除;
- 如果该节点在只有一个儿子,那么直接用他的儿子替代他;
- 否则,用左子树的最大值/右子树的最小值替代他。
本题并不需要删除操作,故此处不给出删除的代码。在下一小节中,我们会从代码层面实现删除操作。
3. 小结
感觉如何?如果你是第一次接触这种有点复杂的数据结构,接受起来可能还是需要花点时间。
需要注意的是,我们在处理二叉树问题时「总是改变当前根节点,把一个大的问题转化为一个小的问题」。
稍作休息,接下来,我们就要进入「AVL
二叉平衡树」的介绍了。
II.升级开始「AVL
二叉平衡树」
__EOF__

本文链接:https://www.cnblogs.com/sdltf/p/17611277.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现