我与我的跳跃表

来源:互联网 发布:c4d r18 mac 迅雷下载 编辑:程序博客网 时间:2024/05/10 01:03

在学习数据结构的过程中,无意中看到一则关于跳跃表的优秀博文。(原帖: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节点(为什么不是从E跳下去不必解释了吧233),然后接着查询,(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次。实际上仔细观察一下可以发现,虽然建立新层之后查找E所需的比较次数减少了1,但由于这个新层的存在,其他所有节点的比较次数都增加了1,这时候平均查找次数只可能是增加的。


小结论:当最新层的元素个数只有两个之后,再建立单元素新层只会增加负担。什么你说你两个元素随机结果都是true要建立一个一模一样的新层?脸太黑了下一个……
开个玩笑,但这也说明,我们在建立新层的过程中,要避免这两种情况的出现。


二:值得注意的地方
1.首先就是我们刚刚得出的小结论:每次我们建立基于下层构建新层,当新层的元素个数只剩两个的时候停止构建。建立新层的过程是一个递归的过程;
2.为什么是0.5?是直觉(笑),直觉告诉我上层元素个数如果是下层的0.5会得到一个比较好的平均查询效率23333;
3.因为学习跳跃表的过程中也在学习写时复制的有关知识。但通过之前的分析我们知道跳跃表很容易是一个相当庞大的空间结构,由于我采用双向循环链表实现,如果真的要完整的复刻整个表的结构会是一个相当浩大的工程。但仔细一想跳跃表的核心其实还是基层的那个链表,所有的“上层建筑”都是随机生成的为查询优化而服务的,因此实际的复刻过程中我们可以只复刻基层链表,上层的建筑不妨重新搭建,只要随机算法不变就好了。


三:代码实现
(一)重点是元素的插入过程写时复制的复刻过程


元素的插入可以细分为以下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 foundprivate:    void reallocate();//copy on writing    void addRefCount();    void subRefCount();    size_t elementCount;    size_t levelCount;    Node *root;    size_t *refCount;};

//那个CreateNewLevel函数我采用的是lambda函数,记得不要忘了捕获必要的值……

原创粉丝点击