modify some mistakes in <skiplist.md>.

This commit is contained in:
Shine wOng
2019-09-29 20:51:50 +08:00
parent 1f306b8809
commit aafab4ea12

View File

@@ -7,13 +7,13 @@
![skiplist_structure](skiplist.png)
用地铁站对跳转表打一个比方,设想有一个城市的地铁分为多层,其中最底层的地铁要经过该城市的所有站台;所有这些站台中,有一些人流量很密集的站台,为它们在上一层设立一条新的地铁线路,因为该线路只经过这些大站台,因此运行速度可以更快。当人们乘坐地铁的时候,为了到了某一个小站台,最快捷的方案显然是先做大站车到距离小站台最近的大站台,当然在乘坐最底层的慢车到达某个小站。跳转表就是这样一种思路,通过在上层列表节点间的快速跳转,来迅速查找到某个数据。
用地铁站对跳转表打一个比方,设想有一个城市的地铁分为多层,其中最底层的地铁要经过该城市的所有站台;所有这些站台中,有一些人流量很密集的站台,为它们在上一层设立一条新的地铁线路,因为该线路只经过这些大站台,因此运行速度可以更快。当人们乘坐地铁的时候,为了到了某一个小站台,最快捷的方案显然是先做大站车到距离小站台最近的大站台,然后再乘坐最底层的慢车到达某个小站。跳转表就是这样一种思路,通过在上层列表节点间的快速跳转,来迅速查找到某个数据。
从上面的图中也可以看到,跳转表的结构就类似于一棵搜索树,上层列表对应了搜索树中高度较高的节点,而底层列表对应了搜索树中的叶节点。实际上,根据跳转表发明者的定义,跳转表是基于概率实现的对平衡二叉树的一种替代方案,它使用概率来实现平衡,而非强制的平衡,因而对于插入和删除节点比传统平衡树更为高效。
## 跳转表的结构
从宏观上来看,跳转表是由多层的列表组成的,为了实现不同层之间列表的跳转,列表中的每个节点除了要有前向后向指针以外,还需要同时具有上下的指针,来表示不同层次地铁的“换乘路径”。为此,需要修改`list`的实现中关于`ListNode`的定义,为其增加上下的指针,即形成下面的`QuadNode`类型:
从宏观上来看,跳转表是由多层的列表组成的,为了实现不同层之间列表的跳转,列表中的每个节点除了要有前向后向指针以外,还需要同时具有上下的指针,来表示不同层次地铁的“换乘路径”。为此,需要修改`list`的实现中关于`ListNode`的定义,为其增加上下的指针,即形成下面的`QuadNode`类型:
```cpp
template <typename T>
@@ -67,7 +67,7 @@ n + n / 2 + n / 4 + ...... + 2 + 1 < 2n = O(n)
### 跳转表的搜索算法
实际上,在上面地铁那里已经叙述过跳转表的搜索算法了,即首先坐上层的大车快速到达距离小站最近的位置,然后换乘底层的小站车。用算法的语言描述,即自上而下,自左而右地搜索,对于每一层的列表,从左至右遍历它的每一个元素,直到发现第一个大于搜索值`key`的元素,然后后退一步,如果当前节点的`key`值等于搜索值,则成功返回,否则表示应该在这里下车换乘了。如果当前列表还不是最底层,则切换到下一层的列表,并且重复上面的操作,直到查找成功,或者因为不存在该`key`值而查找失败。
实际上,在上面地铁那里已经叙述过跳转表的搜索算法了,即首先坐上层的大车快速到达距离小站最近的位置,然后换乘底层的小站车。用算法的语言描述,即自上而下,自左而右地搜索,对于每一层的列表,从左至右遍历它的每一个元素,直到发现第一个大于搜索值`key`的元素,然后后退一步,如果当前节点的`key`值等于搜索值,则成功返回,否则表示应该在这里下车换乘了。如果当前列表还不是最底层,则切换到下一层的列表,并且重复上面的操作,直到查找成功,或者因为不存在该`key`值而查找失败。
这里的搜索算法将基于遵循前面列表的约定,即总是返回不大于`key`值节点中的最靠右者。跳转表的搜索算法实现如下:
```cpp
@@ -93,10 +93,12 @@ QuadNode<entry<K, V>>* SkipList<K, V>::skipSearch(K key) {
考虑搜索过程的逆过程,即从底层列表向右向上移动,该移动过程可以描述为:在某一层不断向右移动,直到发现了第一个向上生长的节点,或者直到到达第一个可以换乘的大站。设某一层横向移动的次数为`K`,有
$$
\begin{aligned}
P(K = 0) = p\\
P(K = 1) = (1 - p)p\\
...\\
P(K = k) = (1 - p)^{k - 1}p, k = 1, 2, ...
\end{aligned}
$$
即随机变量`K`服从几何分布,其期望`EK = 1`,即在每一层的期望移动次数仅为`1`,这样一次搜索过程中的横向跳转次数不超过`h`。故得证。
@@ -109,7 +111,7 @@ $$
在定位了插入位置后,为了将新的词条插入,首先需要将插入位置指针强制转移到底层,从最底层开始插入,完成实质性的插入操作只需要修改前后词条的指针即可。此后,还需要保证`生长概率逐层减半`,可以通过一个随机数来实现,如果生成的随机数是奇数(或偶数),则继续在上一层插入,这个过程不断重复,直到生成的随机数表示不继续插入。
为了将新的词条在上一层插入,需要找到该词条在上一层的前驱者后继节点。一种策略是遍历上一层的全部节点,直到找到新词条的前驱或者后继,其时间复杂度为`O(n)`,毋庸置疑,这种策略的开销太高了。为了快速地找到新词条的后继,可以沿用搜索过程中的跳转策略,具体说来就是在当前层次继续向后遍历,直到发现第一个向上生长的节点,在这里向上层跳转,就是新词条的后继节点了。在前面也已经证明过了,这种遍历进行次数的期望仅为`O(1)`,因此插入的时间复杂度,主要是消耗在了`search`过程中,其总体的时间复杂度也是`O(logn)`。插入过程的具体代码如下:
为了将新的词条在上一层插入,需要找到该词条在上一层的前驱者后继节点。一种策略是遍历上一层的全部节点,直到找到新词条的前驱或者后继,其时间复杂度为`O(n)`,毋庸置疑,这种策略的开销太高了。为了快速地找到新词条的后继,可以沿用搜索过程中的跳转策略,具体说来就是在当前层次继续向后遍历,直到发现第一个向上生长的节点,在这里向上层跳转,就是新词条的后继节点了。在前面也已经证明过了,这种遍历进行次数的期望仅为`O(1)`,因此插入的时间复杂度,主要是消耗在了`search`过程中,其总体的时间复杂度也是`O(logn)`。插入过程的具体代码如下:
```cpp
template <typename K, typename V>
@@ -152,7 +154,7 @@ bool SkipList<K, V>::put(K key, V value){
## 跳转表的删除算法
跳转表的删除策略基本是类似插入的策略的,而且还要更加简单,因为不需要寻找被删除词条的前驱或者后继节点。具体的算法是,首先调用一次`search`对被删除的词条进行定位,随后即可自上而下依次对各层次中的该词条进行删除,由于这种删除至多会进行`h`次,因此删除操作的时间复杂度也仍然是`O(h) = O(logn)`。删除操作的具体代码如下:
跳转表的删除策略基本是类似插入的策略的,而且还要更加简单,因为不需要寻找被删除词条的前驱或者后继节点。具体的算法是,首先调用一次`search`对被删除的词条进行定位,随后即可自上而下依次对各层次中的该词条进行删除,由于这种删除至多会进行`h`次,因此删除操作的时间复杂度也仍然是`O(h) = O(logn)`。删除操作的具体代码如下:
```cpp
template <typename K, typename V>