跳表 - 简明教程 in Python

栏目: 数据库 · 发布时间: 5年前

内容简介:# 1. 什么是跳表跳表(Skip List)是基于链表 + 随机化实现的一个有序数据结构,可以达到平均 O(logN) 的查找、插入、删除效率,在实际运行中的效率往往超过 AVL 等平衡二叉树,而且其实现相对更简单、内存消耗更低。Redis 的 ZSET 底层实现就是用的 Skip List,这里是 [Antirez对此的说明](

# 1. 什么是跳表

跳表(Skip List)是基于链表 + 随机化实现的一个有序数据结构,可以达到平均 O(logN) 的查找、插入、删除效率,在实际运行中的效率往往超过 AVL 等平衡二叉树,而且其实现相对更简单、内存消耗更低。

Redis 的 ZSET 底层实现就是用的 Skip List,这里是 [Antirez对此的说明]( https://news.ycombinator.com/item?id=1171423)

这是一个典型的跳表:

[0] -> 0 -> 1 -> 3 -> 4 -> 5 -> 6 -> 7 -> 9 -> nil
[1] -> 0 ------> 3 ------> 5 ------> 7 ------> nil
[2]----------------------> 5-----------------> nil

解释一下:

1. SkipList 是一个多层的链表

2. 第[0]层的链表包含所有节点,其他层的链表包含部分节点,层次越高,节点越少

3. 每层链表之间会共享相同的节点(节省内存,但为了方便展示,每一层都输出了它的值)

4. 对于某个节点,在插入时通过概率判断它最高会出现在哪一层,并且也会出现在之下的每一层

通过这样的设计,当需要查找某个 key 时,可以从最高层的链表开始往前找,在这一层遇到末尾或者大于 key 的节点时往下走一个层,直到找到 key 节点。

例如:

引用

4 的查找路径为 [2] -> [1] -> 0 -> 3 -> 3@[0] -> 4

6 的查找路径为 [2] -> 5 -> 5@[1] -> 5@[0] -> 6

8 的查找路径为 [2] -> 5 -> 5@[1] -> 7 -> 7@[0] -> 9 (找不到)

# 2. 跳表的节点

从上面的描述,我们大概可以知道 (1) 每个节点需要保存一个 key; (2) 每个节点需要有多个next指针 (3) 其 next 指针的数量会在插入时确定

因此我们可以用下面这个 class 来表示节点:

class Node(object)
    def __init__(self, height, key):
        self.key = key
        self.next = [None] * height

    def height(self):
        return self.height()

# 3. 创建跳表

一个新创建的跳表是没有节点的。但为了实现的简单起见,可以添加一个头节点:

class SkipList(object):
    def __init__(self):
        self.head = Node(0, None) #头节点高度为0,不需要key

到目前为止都特别简单,但是还什么也干不了。

# 4. 创建节点

创建节点时,需要先按一定的概率分布确定其高度。

为了保证高层的节点比低层少,我们可以用这样的概率分布:

引用

Height(n) = p^n

实现其实非常简单:

import random

def randomHeight(self, p = 0.5):
    height = 1
    while random.uniform(0, 1) < p and self.head.height() >= height:
        height += 1
    return height

这样可以保证平均的路径长度是 log(n) 。

精确一点的话,实际上是 log(n-1, 1/p) / p,也就是说, p 的选择会影响跳表层数、平均路径长度。

具体的计算比较复杂,有兴趣可以参考跳表的原论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。(TL;DR)

然后我们就可以这样来创建一个新的节点:

node = Node(self.randomHeight(), key)

# 5. 添加节点

如果只是为空跳表添加一个新的节点,只要更新头结点的每一个next指针:

def insertFirstNode(self, key):
    node = Node(self.randomHeight(), key)
    while node.height > self.head.height():
        self.head.next.append(None) #保证头节点的next数组覆盖所有层次的链表

    for level in range(node.height()):
        node.next[level] = self.head.next[level]
        self.head.next[level] = node

但很显然这个方法只能用一次。

如果跳表中已经有多个节点,那我们就必须找到每一层中适合插入的位置:

def getUpdateList(self, key):
    update = [None] * self.head.height()
    for level in range(len(update)):
        x = self.head
        while x.next[level] is not None and x.next[level].key < key:
            x = x.next[level]
        update[level] = x
    return update

这个函数返回一个 update 节点数组,其中的每个节点都是在这一层中小于 key 的最后一个节点。

也就是说,在 level = i 层,总是可以把新的节点插入 update[i] 之后:

def insert(self, key):
    node = Node(self.randomHeight(), key)
    while node.height > self.head.height():
        self.head.next.append(None) #保证头节点的next数组覆盖所有层次的链表

    update = self.getUpdateList(key)
    next0 = update[0].next[0]
    if next0 is not None and next0.key == key:
        return # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。

    for level in range(node.height()):
        node.next[level] = update[level].next[level]
        update[level].next[level] = node

但是由于这一版 getUpdateList 是 O(n) 的,插入效率并没有达到跳表的设计目标。

# 6. 添加节点++

考虑这一点:跳表的每一层都是有序的。

也就是说,我们在找到 update[n] = x 以后,其实可以从节点 x 的 n - 1 层继续查找来查找 update[n-1] 。

由于查找路径的评价长度是 log(N) ,所以我们可以实现一个更快的 getUpdateList 方法

注意,需要从最高层开始查

def getUpdateList(self, key):
    update = [None] * self.head.height()
    x = self.head
    for level in reversed(range(len(update))):
        while x.next[level] is not None and x.next[level].key < key:
            x = x.next[level]
        update[level] = x
    return update

# 7. 里程碑1

把上面的代码整合起来,我们就可以得到第一版跳表代码:能够插入节点。

为了更好地展示我们的成果,我们可以用这样一个函数,把链表按第1节的例子样式输出:

def dump(self):
    for i in range(self.head.height()):
        sys.stdout.write('[H]')
        x = self.head.next[0]
        y = self.head.next[i]
        while x is not None:
            s = ' -> %s' % x.key
            if x is y:
                y = y.next[i]
            else:
                s = '-' * len(s)
            x = x.next[0]
            sys.stdout.write(s)
        print ' -> <nil>'
    print

试试看:

sl = SkipList()
for i in range(10):
    sl.insert(sl)
    s1.dump()
[H] -> 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> <nil>
[H]----- -> 1 -> 2 -> 3---------- -> 6 -> 7---------- -> <nil>
[H]---------- -> 2-------------------- -> 7---------- -> <nil>

多尝试几次,以及选择不同的 p 值,可以观察生成跳表的区别。

# 8. 查找节点

实际上查找节点的过程,已经包含在 insert 的实现里了:

def find(self, key):
    update = self.getUpdateList(key)
    if len(update) == 0:
        return None

    next0 = update[0].next[0]
    if next0 is not None and next0.key == key:
        return next0 # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。
    else:
        return None

# 9. 删除节点

既然已经能找出 update 节点数组,在 level = i 层,只要判断 update[i].next[i] 是否等于要删除的 key 就可以了:

def remove(self, key):
    update = self.getUpdateList(key)
    for i, node in enumerate(update):
        if node.next[i] is not None and node.next[i].key == key:
            node.next[i] = node.next[i].next[i]

# 10. 里程碑2

整合 find 和 update 数组,就可以实现跳表的基础操作了,试试看:

node = sl.find(3)
print node

for i in range(7, 14):
    sl.remove(i)
    sl.dump()

# 11. 其他

我们在 Node 中只添加了一个 key 属性,在具体的实现中,我们往往可能需要针对 key 存储一个 value,例如 Python 自带的 dict 实现。改造起来也很简单:

1. node 中添加一个 value 属性,并且添加相应的初始化逻辑(__init__方法)

2. 将 SkipList.insert 修改为 `insert(self, key, value)`,在新建 Node 时指定其 value

3. 再添加一个 `update(self, key, value)` API,方便调用方的使用

4. 可以考虑针对语言适配,例如实现 python 的 __getitem__ 、 __setitem__ 等魔术方法

# 12. 完整代码

#coding:utf-8

import random

class Node(object):
    def __init__(self, height, key=None):
        self.key = key
        self.next = [None] * height

    def height(self):
        return len(self.next)

class SkipList(object):
    def __init__(self):
        self.head = Node(0, None) #头节点高度为0,不需要key

    def randomHeight(self, p = 0.5):
        height = 1
        while random.uniform(0, 1) < p and self.head.height() >= height:
            height += 1
        return height

    def insert(self, key):
        node = Node(self.randomHeight(), key)
        print node.height(), node.key
        while node.height() > self.head.height():
            self.head.next.append(None) #保证头节点的next数组覆盖所有层次的链表

        update = self.getUpdateList(key)
        if update[0].next[0] is not None and update[0].next[0].key == key:
            return # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。

        for level in range(node.height()):
            node.next[level] = update[level].next[level]
            update[level].next[level] = node


    def getUpdateList(self, key):
        update = [None] * self.head.height()
        x = self.head
        for level in reversed(range(len(update))):
            while x.next[level] is not None and x.next[level].key < key:
                x = x.next[level]
            update[level] = x
        return update

    def dump(self):
        for i in range(self.head.height()):
            sys.stdout.write('[H]')
            x = self.head.next[0]
            y = self.head.next[i]
            while x is not None:
                s = ' -> %s' % x.key
                if x is y:
                    y = y.next[i]
                else:
                    s = '-' * len(s)
                x = x.next[0]
                sys.stdout.write(s)
            print ' -> <nil>'
        print

    def find(self, key):
        update = self.getUpdateList(key)
        if len(update) == 0:
            return None

        next0 = update[0].next[0]
        if next0 is not None and next0.key == key:
            return next0 # 0层总是包含所有元素;如果 update[0] 的下一个节点与key相等,则无需插入。
        else:
            return None

    def remove(self, key):
        update = self.getUpdateList(key)
        for i, node in enumerate(update):
            if node.next[i] is not None and node.next[i].key == key:
                node.next[i] = node.next[i].next[i]

完。

转载请注明出自,如是转载文则注明原出处,谢谢:)

RSS订阅地址: http://www.felix021.com/blog/feed.php


以上所述就是小编给大家介绍的《跳表 - 简明教程 in Python》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

极简算法史:从数学到机器的故事

极简算法史:从数学到机器的故事

[法] 吕克•德•布拉班迪尔 / 任轶 / 人民邮电出版社 / 2019-1 / 39.00元

数学、逻辑学、计算机科学三大领域实属一家,彼此成就,彼此影响。从古希腊哲学到“无所不能”的计算机,数字、计算、推理这些貌似简单的概念在三千年里融汇、碰撞。如何将逻辑赋予数学意义?如何从简单运算走向复杂智慧?这背后充满了人类智慧的闪光:从柏拉图、莱布尼茨、罗素、香农到图灵都试图从数学公式中证明推理的合理性,缔造完美的思维体系。他们是凭天赋制胜,还是鲁莽地大胆一搏?本书描绘了一场人类探索数学、算法与逻......一起来看看 《极简算法史:从数学到机器的故事》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具