在学习数据结构的过程中,无意中看到一则关于跳跃表的优秀博文。(原帖:http://www.cnblogs.com/acfox/p/3688607.html)
当时觉得很有意思,不过原博主给出的代码是Java语言,笔者并不熟悉Java,遂有此文。另外本篇更多的是记录下个人写代码时的一些思路,有关跳跃表更深入的结论与研究可以翻看原博客或者自行学习(笑)。
一:跳跃表的一些基础概念
跳跃表,按照我的理解是在有序链表(姑且称之为基层链表)的基础上通过某些手段搭建起“上层建筑”,每一层都是基于下层而建立的一个新链表。当进行查询时,从最高层的建筑开始,像通常的链表一样依次序查询,如果没有找到目标,则通过当前目标节点的down指针跳跃到下层元素值相同的目标节点上,接着依次序查询。
“某些手段”
其实就是随机。还是举例来讲比较容易理解,假设现在我们的基层链表有5个元素A、B、C、D、E按照升序排列,当我们需要查找某个节点时,从头开始,最好为A,而最坏情况无疑是E。换言之平均情况下的时间复杂度为O(n),有什么办法来提升这个平均时间呢?答曰:空间换取时间。
现在我们采取某种随机算法,并且使随机结果为true的概率为0.5(为什么是0.5不妨待会儿再讲)。同时我们新建一层,并且对于基层链表的所有元素,我们执行一次这个随机算法。如果结果为true,就在新层中加入这个元素(并将新节点的down指针指向下层对应的节点)。
基层链表:A、B、C、D、E(注意这里我们没有画出用于标记表头的指针)
我们来算一下平均比较次数:(1+2+3+4+5)/5 = 3次;
对所有节点采取一次随机算法,这里假设B、E的结果为true,建立新层,这时整个结构就变为:
新 层: B E
基层链表:A、B、C、D、E
每次查找都是从新层开始的(更准确来说是从整个结构的左上角开始)。显然,如果这时候我们要查找节点E,从新层表头开始,表头不参与比较,由B->E,只需要进行两次比较就能够找到目标。而这时候的最坏情况应该是D,从左上开始,B->E,发现E比要找的节点大,怎么办?跳到底下一层,也就是新层中B的down指针所指向的节点,即基层中的B节点,然后接着查询,(B)->C->D,目标get。由于跳跃之后我们所处的B是不需要再次比较的,一共需要比较4次。
再算一下平均比较次数(2+1+3+4+2)/5 = 12/5 次 < 3次。
这是只有5个元素的情况,但就算是只学过一点儿概率统计的我也能看出来,当元素个数非常大的时候,这个结构相比起单纯的链表,其平均查找性能的提升是相当之大的。当然非洲人随机的结果是B、E待查找的却是D,欧洲人随机的结果是B、E待查找的就是B,这很科学不是吗……
此外,既然我们尝到了建立新层的甜头,能不能更过分一点,多建立几层,这样平均查找次数不是会更少吗?
答案是Naive。还是以这个简单的链表为例,现在我们以新层为基准,换药不换汤,再建立一层,假设这一次只有E的随机结果是true:
同样未画出最左边的表头结点列,其实也可以把第一个元素成列作为每一层的头部。
最最新层: E
新 层: B E
基层链表:A、B、C、D、E
以查找C为例,最新层表头->E,发现E比C大,跳到最新层表头的down指针目标:新层表头;新层表头->B->E,发现E比C大,跳到B的down指针目标:基层的B,B->C,目标get,共须比较4次。
然而这时候再次计算平均比较次数(3+2+4+5+1)/5 = 3次。发现平均查找次数并没有降下来,但这其实是一种比较差的随机情况,事实上,这里如果最最新层是A-D任一个元素,平均查找次数都是得到优化的。
二:值得注意的地方
1.为什么是0.5?是直觉(笑)。事实上,仔细考虑这个查找的过程,如果上层数据个数与下层的比小于0.5(极端情况是0,即没有上层),它就逐步退化为一个链表查找过程。同样的,如果上层数据个数与下层的比大于0.5(极端情况是1,即上层拥有下层所有元素,它同样退化为一个链表查找过程。当然,这是默认所有输入数据出现频率相等的情况,实际应用中,可以根据要处理的数据特征来选择随机算法,确保常用的数据容易被提到上层,方便查找;
2.因为学习跳跃表的过程中也在学习写时复制的有关知识。但通过之前的分析我们知道跳跃表很容易是一个相当庞大的空间结构,由于我采用双向循环链表实现,如果真的要完整的复刻整个表的结构会是一个相当浩大的工程。但仔细一想跳跃表的核心其实还是基层的那个链表,所有的“上层建筑”都是随机生成的为查询优化而服务的,因此实际的复刻过程中我们可以只复刻基层链表,上层的建筑不妨重新搭建,只要随机算法不变就好了。
三:代码实现
(一)重点是元素的插入过程和写时复制的复刻过程:
元素的插入可以细分为以下2个步骤:
1.写时复制过程:如果这个对象与其他某对象是共享数据的,重新复刻数据,并将referenceCount重设为1(referenceCount用来记录共享该数据的对象数):
2.新建一个值为该元素值的节点,将该节点插入到底层的相应位置,如果插入元素后该层的节点数(算表头,下同)大于2,对该节点执行一次随机算法。
若随机算法结果为真,跳到上层,也新建节点并插入到相应位置。这里需要注意:
1)如果“上层”在插入节点后节点数大于2,继续运行随机算法,并在结果为真时跳到“上层”的上层新建节点、插入到相应位置,“上层”则指向新的上层;
2)如果没有“上层”,新建一层并新建、插入节点。但是新建这层之后就可以跳出循环了,因为你新建的这层节点数必定只有1个嘛;
3)不要忘了将新层表头节点的down指针指向下层的表头,更不要忘了将每层节点的down指针指向下层的相应节点。
若随机算法结果为假,或者该层的节点数不大于2,元素插入完毕,终止插入流程。
这是逐节点的建表过程,还可以采用逐层的递归建表过程,也是我们在写时复制的复刻过程中要采用的方法。
写时复制的复刻过程:
1.referenceCount–;
2.建立底层,链表的复刻过程就不做赘述了;
3.写一个函数,不妨称之为CreateNewLevel,该函数引入一个节点指针sentinel作为参数,sentinel为基准层(下层)的表头指针,输出结果为新建层(上层)的表头指针;一个起标记作用的指针ptr,初始化为底层的表头;
4.通过ptr算出该层的节点数,只要节点数大于2,ptr = CreateNewLevel(ptr)并重复此流程。
到这里CreateNewLevel函数的具体内容也呼之欲出:新建一层,并对基准层的每个节点运行一次随机算法,若结果为真则在新层中新建节点、插入到相应位置;若结果为假,直接转至下一个节点,一直到基准层的所有节点遍历完毕。最后返回新层的表头。
5.同样不要忘了将新层表头节点的down指针指向下层的表头,也不要忘了将每层节点的down指针指向下层的相应节点。
6.在引起写时复制的操作中将referenceCount重设为1,因为这时候咱已经“独立”了。
(二)其他的操作
查找过程:通过这个设计,可以发现在类中我们只需要保留最左上角的节点(就称为root吧)就行了,往下,它可以达到最底层,往右,它可以找到最右君……查找某元素时,从root开始,每次将其右边节点的元素值与待查找值进行比较(现假设升序表),右边节点值大于待查值,跳到下面的对应节点;小于,跳到右边一个节点,等于则返回之。
删除过程:本质上还是查找过程,不妨写一个保护的、返回节点指针的函数(找不到就返回nullptr),查找和删除都用这个函数。根据上述的查找过程可以知道,这个函数每次返回的都是整个结构中值为待查值且位于最左上的一个节点。
例如在:
表头 B E
表头 A B C D E
中,若查找B,返回的一定是左上的那个B,但是我们删除节点时不要忘记将整个B列(这么说也许不太准确……)都删除掉。
最后是一丢丢定义代码,起个抛砖引玉的作用。作为初学者我的代码还是太乱了,就不拿出来丢人了……
template<typename T>
struct skipListNode{
T value;
skipListNode *prior;
skipListNode *next;
skipListNode *down;
enum NodeType{Sentinel, NormalNode};
NodeType type;
skipListNode(){//default type is Sentinel
prior = next = this;
down = nullptr;
type = Sentinel;
}
skipListNode(T value, NodeType type = NormalNode) : value(value), type(type){//default type is NormalNode
prior = next = this;
down = nullptr;
}
skipListNode(const skipListNode<T>& copy){
value = copy.value;
prior = copy.prior;
next = copy.next;
down = copy.down;
type = copy.type;
}
~skipListNode(){
prior->next = next;
next->prior = prior;
}
skipListNode<T>& operator =(const skipListNode<T>& copy){
value = copy.value;
prior = copy.prior;
next = copy.next;
down = copy.down;
type = copy.type;
return *this;
}
};
template<typename T>
class skipList{
typedef skipListNode<T> Node;
public:
skipList();
skipList(std::initializer_list<T> list);
skipList(const skipList<T>& copy);
~skipList();
int insert(const T& elem);
int remove(const T& elem);
const T& at(size_t index) const;
bool contains(const T& elem) const;
bool empty() const;
T& first();//return default value case the skiplist is empty
const T& first() const;
T& last();
const T& last() const;
inline size_t size() const{
return elementCount;
}
inline size_t level() const{
return levelCount;
}
bool isShared(const skipList<T>& another) const;
skipList<T>& operator =(const skipList<T>& copy);
protected:
bool randomResult();//true result probability is equal to 0.5
Node *sentinel(size_t lev = 0) const;//0 is the top level, returns the sentinel node of each level or nullptr if lev is out of range
Node *findNode(const T& elem) const;//return the first node during our finding process (always the hignest one), or return nullptr if not found
private:
void reallocate();//copy on writing
void addRefCount();
void subRefCount();
size_t elementCount;
size_t levelCount;
Node *root;
size_t *refCount;
};
//那个CreateNewLevel函数我采用的是lambda函数,记得不要忘了捕获必要的值……