前言:
我现在是一个大二的学生,因为下个学期就要开始学习数据结构这门课程,出于兴趣我提前将数据结构自学了一遍,在学习的过程中发现了许多有趣的结构,最近终于有了一点时间,决定开始写点博客,记录一下自己的想法;这是我第一次写博客,如果有不好的地方欢迎大家指出,我们可以一同讨论共同进步。
原理:
伸展树(SplayTree)是一种平衡树的结构,是二叉搜索树的一种,他保证从空树开始的任意连续M次对树的操作最多花费O(M logN)时间。虽然与AVL树以及红黑树相比它的时间复杂度似乎要高一些,但是其空间要求以及编程复杂度要更低。接下来,我就来介绍一下伸展树的特性。
有一种原则叫做叫做"二八原则",也就是说百分之八十的搜索都发生在百分之二十的数据上,伸展树也就是满足这种需求出现的。为了满足这些需求,伸展树在每一次操作后都会把操作的元素,通过一系列的旋转放置到树的根部(插入,删除,查找),在图一中,我们可以看到被查找元素被移动到树根。这样,根据二八原则,访问次数更多的元素,总是在离树根更近的地方,也就减少了检索的时间消耗,增加了效率。在我看来,这是一种非常有想法的一种数据结构,也让人印象深刻。
C++实现:
1.旋转操作:
如果学习过了AVL树,那么你对旋转操作一定不会陌生,在AVL树中旋转被分为了双旋转和单旋转两种情况(当然,双旋转也就是两个单旋转)。而在我们的伸展树中,也将使用到旋转操作,不过与AVL树有点不同。
旋转操作可以说是平衡树结构中最最最基础的东西了,它是移动节点的一种有效方式,以图二右旋转为例,我们将当前节点(
N节点)变为父节点(
F节点),父节点变为当前节点的右儿子,同时将原来的右儿子(
S节点)链接到
F节点的左儿子,完成旋转操作。左旋转操作与右旋转操作相同,只是方向相反,这里就不再讲解了。(其实是不想画图了)
下面是我们的实现代码:
/* 右旋转函数:将目标节点向右旋转
* 返回值:无
* 参数:Tree:想要进行右旋转的目标节点
*/
void SplayTree::SingleRotateWithLeft(TreeNode &Tree) {
TreeNode LeftChild; // 储存左子树
// 进行旋转操作
LeftChild = Tree->Left;
Tree->Left = LeftChild->Right;
LeftChild->Right = Tree;
Tree = LeftChild; // 更新目标节点
}
/* 左旋转函数:将目标节点向左旋转
* 返回值:无
* 参数:Tree:想要进行左旋转的目标节点
*/
void SplayTree::SingleRotateWithRight(TreeNode &Tree) {
TreeNode RightChild; // 储存右子树
// 进行旋转操作
RightChild = Tree->Right;
Tree->Right = RightChild->Left;
RightChild->Left = Tree;
Tree = RightChild; // 更新目标节点
}
PS:有些朋友可能会觉得奇怪,因为这里的旋转代码并没有将父节点与祖父节点链接,难道不会出错吗?不用担心,这是因为伸展树的特殊性,同时也与我们使用的伸展方法有关系,会在后面具体解释。
2. 伸展操作:
接下来的就是伸展操作,可以说伸展操作是整个伸展树的核心功能,因为所有的功能,都是根据伸展操作进行的(查找,插入,删除)。而伸展操作有两种实现方式。
自底向上伸展:
这是一种很容易想到的伸展方式,我们通过从树根处开始检索,当我们检索到目标元素时,我们就开始向上伸展,通过旋转的方式将目标元素旋转到树根上。但是,我们可以想到,我们从顶部向下检索的过程中,以及从底部向上伸展的过程中,都需要保存许多的父指针来完成,或将路径储存到某个栈中进行保存,这样讲会带来大量的开销!因此我们更加常用的是另外一个方法——自顶向下伸展。
(有兴趣的朋友可以自己尝试用自底向上方法实现,我在这里就不再多讲了~~)
自顶向下伸展:
在这里,我们先使用一种叫做标志节点的东西来代替空节点,它的代码如下:
typedef struct SplayNode *TreeNode;
TreeNode NullNode; // 储存空标志节点
N