shan

跳表

2020-06-22

我们可以用很多有趣的方式实现映射,如散列表、二叉搜索树、跳表等。

下面是对上面数据结构一些简单的介绍:

  • 散列表:给定一组键和一个散列函数,将键放入集合中,并搜索和取出关联的值。这种搜索操作的的时间复杂度为$O(1)$,不过去性能会因为表的大小、冲突、冲突解决策略等因素而降低。
  • 二叉搜索树:把键存储在树中时,搜索操作的时间复杂度是$O(logn)$。 不过,这只有在树平衡时才成立,即树的左右子树差不多大时。
  • 跳表:其本质上是一个二维链表,链接的方向向前或者向下。表头在左上角,它是跳表的唯一入口。既能高效搜索,又避免了上述的缺点。

基本概念

跳表示例如下图所示:

image-20200624204951432

由上图可知,跳表是由节点组成,分为头节点数据节点,头节点保存了down和next的引用,数据节点每个节点存有一个及其关联的,每个节点有两个向外的引用,分别为down和next,down指向下一层的头节点,next引用指向数据节点的链表。由数据节点构成的纵列称为,塔是由数据节点中的down引用连起来的。跳表水平方向是由一系列节点集合组成的有序链表,其链表的名称为其层数,0层包含了所有数据的节点,层数越高,包含的节点越少,其有助于提高搜索效率。

实现

对跳表的实现主要分为节点的实现,相关方法的实现。

节点

首先对跳表的头部和其数据节点进行了实现,并且实现了相关的方法,其结构类似于链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class HeaderNode:
def __init__(self):
self.next = None
self.down = None

def get_next(self):
return self.next

def get_down(self):
return self.down

def set_next(self, new_next):
self.next = new_next

def set_down(self, new_down):
self.down = new_down


class DataNode:
def __init__(self, key, value):
self.key = key
self.data = value
self.next = None
self.down = None

def get_key(self):
return self.key

def get_data(self):
return self.data

def get_next(self):
return self.next

def get_down(self):
return self.down

def set_data(self, new_data):
self.data = new_data

def set_next(self, new_next):
self.next = new_next

def set_down(self, new_down):
self.down = new_down

搜索

搜索一般从表头开始,知道找到键,或者检查完所有的数据节点。下面是关于跳表搜索的相关python实现,其中foundstop用于条件的控制,其基本思路是从顶层的头节点开始往右查找,如果没有数据节点,就下降一层;如果有数据节点就比较大小,如果匹配则将found置为True

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SkipList:
def __init__(self):
self.head = None

def search(self, key):
current = self.head
found = False
stop = False
while not found and not stop:
if current is None:
stop = True
else:
if current.get_next() is None:
current = current.get_down()
else:
if current.get_next().get_key() == key:
found = True
else:
if key < current.get_next.get_key():
current = current.get_down()
else:
current.get_next()

if found:
return current.get_next().get_data()
else:
return None

加入键值对

要往跳表中新增键值对,本质上需要两步:

  • 搜索跳表,寻找插入位置;
  • 新建一个数据节点,并将它加入到第0层的链表中。
  • 为新的数据节点构建塔,其他的高度是随机的,本质上通过抛硬币的方式决定是否在塔中加入一层。

其代码实现的部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class SkipList:
...
"""
上面为已实现部分
"""
def insert(self, key, data):
if self.head is None:
self.head = HeaderNode()
temp = DataNode(key, data)
self.head.set_next(temp)
top = temp
while self.flip() == 1:
new_head = HeaderNode()
temp = DataNode(key, data)
temp.set_down(top)
new_head.set_next(temp)
new_head.set_down(self.head)
self.head = new_head
top = temp
else:
tower_stack = []
current = self.head
stop = False
while not stop:
if current is None:
stop = True
else:
if current.get_next() is None:
tower_stack.append(current)
current = current.get_down()
else:
if current.get_next().get_key() > key:
tower_stack.append(current)
current = current.get_down()
else:
current = current.get_next()

lowest_level = tower_stack.pop()
temp = DataNode(key, data)
temp.set_next(lowest_level.get_next())
lowest_level.set_next(temp)
top = temp

while self.flip() == 1:
if not tower_stack:
new_head = HeaderNode()
temp = DataNode(key, data)
temp.set_down(top)
new_head.set_next(temp)
new_head.set_down(self.head)
self.head = new_head
top = temp
else:
next_level = tower_stack.pop()
temp = DataNode(key, data)
temp.set_down(top)
temp.set_next(next_level.get_next())
next_level.set_next(temp)
top = temp

@staticmethod
def flip():
return randrange(2)

分析跳表

跳表是基于概率的数据结构,这意味着其性能基于某些事件的概率。从理论上来说,每添加一个数据节点,添加的概率为$1/2$,那么我们可以说有$n/2$个键的高度是2,$n/4$个键的高度是3,以此类推,那么这个它的高度是$log_2n + 1$ ,以大$O$记法,可以说跳表的高度为$O(logn)$。

给定一个键,在查找的时候要扫描两个方向,一个是向下,找到目标的概率是$O(logn)$,第二个方向是沿着每一层向前扫描,每当遇到以下两种情况
之一时,就下降一层:要么数据节点的键比目标键大,要么抵达这一层的终点。对于下一个节点,发生上述两种情况之一的概率是$1/2$。这意味着查看2 个链接后,就会下降一层(抛两次硬币后得到正面)。无论哪种情况,在任一层需要查看的节点数都是常数。因此,搜索操作的时间复杂度是$O(logn)$。

因为插入操作的大部分时间花在查找插入位置上,所以插入操作的时间复杂度也是$O(logn)$。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章