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