JFLong  
实验目的
  • 对比散列与跳跃表 增强对于映射这一抽象类型的理解
  • 对比链表与散列表加深对空间换时间这一升维策略的理解
  • 熟悉常用的链表操作:查找节点 删除节点 插入节点 修改节点
  • 通过实验了解数据结构在工业界的真实应用 提高学习动力和实践能力

 

实验原理
Map ADT

映射数据类型的实现往往有多种方法

之前的章节我们学习了用散列表和二叉搜索树来实现映射

它们各有优缺点,散列表搜索操作性能受到表的大小、元素冲突制约明显

BST树查找的O(logn)复杂度成立的前提是树的平衡,在实际操作中很难保证

基于上述两种方式的性能瓶颈,我们考虑设计新的方法实现映射,工业界有成熟应用的跳表是不错的解决方案.

 

 

Skip List

关于跳表,它其实就是一种二维链表,链接的方向向前或向下。

数据节点是跳表的元素,数据节点构成的纵列叫做塔。

 

实验内容
1.设计并实现跳表类

leetcode 1206 Hard

采用抛硬币(0 or 1)的策略决定是否需要增加高层索引

from random import randint
Max_levels = 16
class Node:
    def __init__(self,key,val = None):
        self.key = key
        self.val = val
        self.right, self.down = None, None


class Skiplist:
    def __init__(self):
        # 这是最基础的一步 建立节点的空间关系 并确定最大层数16
        self.heads = [Node(float("-inf")) for _ in range(Max_levels)]
        self.tails = [Node(float("inf")) for _ in range(Max_levels)]
        for h, t in zip(self.heads, self.tails): 
            h.right = t
        for up, down in zip(self.heads, self.heads[1:]): 
            up.down = down


    # 搜索其实很简单
    # 在两者之间就到下面去找
    # 比右边还大就去右边找
    # 由于 tail 设置了最大值所以就不会超过 tail
    def search(self, target: int) -> bool:
        cur = self.heads[0]
        while cur:
            if cur.key < target <= cur.right.key:
                if target == cur.right.key: return cur.right.val
                cur = cur.down
            else:
                cur = cur.right
        return False


    def add(self, key: int,val) -> None:
        s, cur = [], self.heads[0]
        while cur:
            if cur.key < key <= cur.right.key:
                s.append(cur)
                cur = cur.down
            else:
                cur = cur.right
        prev = None
        # 栈记录每次向下的值
        # 只需要在这些节点后插入就可
        # 最后判断是不是需要继续向上建立索引
        while s:
            cur, node = s.pop(), Node(key,val)
            node.down, node.right = prev, cur.right
            cur.right = node
            prev = node
            if randint(0, 1): break


    # 与搜索类似
    def erase(self, key: int) -> bool:
        cur, found = self.heads[0], False
        while cur:
            if cur.key < key <= cur.right.key:
                if key == cur.right.key:
                    cur.right = cur.right.right
                    found = True
                cur = cur.down
            else:
                cur = cur.right


        return found     

 

2.设计并实现映射类
class Map:
    def __init__(self):
        self.collection = Skiplist()


    def put(self,key,value):
        self.collection.add(key,value)


    def get(self,key):
        # key 不存在则返回异常信息
        if not self.collection.search(key):
            raise KeyError("This key does not exist")


        return self.collection.search(key)


my_dict = Map()
my_dict.put(0,"zero")
my_dict.put(1,"One")
my_dict.put(2,"Two")
my_dict.put(3,"Three")
my_dict.put(4,"Four")


param6 = my_dict.get(3)
print("3 is referred to",param6)
param7 = my_dict.get(4)
print("4 is referred to",param7)
param8 = my_dict.get(10)
print("10 is referred to",param8)

  

3.复杂度分析

让你的链表支持二分搜索

搜索 O(logN)

插入 O(logN)

空间 O(N)

如果采用长度为4的区间设计层级索引

索引个数为 \frac{n}{4} +\frac{n}{8} + ... + \frac{n}{4^k}

 

实验总结

跳表的核心是为了优化链表元素随机访问的时间复杂度过高的问题 (O(n))。

这个优化的中心思想其实是贯穿于整个算法数据结构,甚至也贯穿于整个数学与物理的世界。那就是升维思想 / 空间换时间

跳表是redis zset的底层数据结构 在LevelDB等业界数据库中也有应用

127.0.0.1:6379> ZADD myzset 1 "foo"
(integer) 1
127.0.0.1:6379> ZADD myzset 1.5 "bar"
(integer) 1
127.0.0.1:6379> ZADD myzset 1 "test"
(integer) 1
127.0.0.1:6379> ZADD myzset 2 "hello"
(integer) 1
127.0.0.1:6379> ZADD myzset 3 "world"
(integer) 1
127.0.0.1:6379> ZRANGE myzset 0 -1 WITHSCORES

  

因为键的本质还是一个计算出来的索引(整数)故而可以用跳表代替哈希表

跳表不能完全代替红黑树 许多高级语言的Map类型还是使用红黑树来实现

 

实验参考

https://leetcode-cn.com/problems/design-skiplist/solution/1206-she-ji-tiao-biao-pythonshi-xian-by-tuotuoli/

https://zhuanlan.zhihu.com/p/372695800

posted on 2025-04-24 16:49  鱼孜千  阅读(23)  评论(0)    收藏  举报