网站设计思路怎么写,旅游类网站设计模板下载,郑州网站优化排名,中国商业网点建设中心今天继续介绍分布式系统当中常用的数据结构#xff0c;今天要介绍的数据结构非常了不起#xff0c;和之前介绍的布隆过滤器一样#xff0c;是一个功能强大原理简单的数据结构。并且它的缺点和短板更少#xff0c;应用更加广泛#xff0c;比如广泛使用的Redis就有用到它。 …今天继续介绍分布式系统当中常用的数据结构今天要介绍的数据结构非常了不起和之前介绍的布隆过滤器一样是一个功能强大原理简单的数据结构。并且它的缺点和短板更少应用更加广泛比如广泛使用的Redis就有用到它。 SkipList简介 SkipList是一个实现快速查找、增删数据的数据结构可以做到O(logN)O(logN)复杂度的增删查。从时间复杂度上来看似乎和平衡树差不多但是和平衡树比较起来它的编码复杂度更低实现起来更加简单。学过数据结构的同学应该都有了解平衡树经常需要旋转操作来维护两边子树的平衡不仅编码复杂理解困难而且debug也非常不方便。SkipList克服了这些缺点原理简单实现起来也非常方便。 原理 SkipList的本质是List也就是链表。我们都知道链表是线性结构的每次只能移动一个节点这也是为什么链表获取元素和删除元素的复杂度都是O(n) 如果我们要优化这个问题可以在当中一般的节点上增加一个指针指向后面两个的元素。这样我们遍历的速度可以提升一倍最快就可以在O(n/2)O(n/2)的时间内遍历完整个链表了。 同样的道理如果我们继续增加节点上指针的个数那么这个速度还可以进一步加快。理论上来说如果我们设置lognlogn个指针完全可以在lognlogn的时间内完成元素的查找这也是SkipList的精髓。
但是有一个问题是我们光实现快速查找是不够的我们还需要保证元素的有序性否则查找也就无从谈起。但是元素添加的顺序并不一定是有序的我们怎么保证节点分配到的指针数量合理呢
为了解决这个问题SkipList引入了随机深度的机制也就是一个节点能够拥有的指针数量是随机的。同样这种策略来保证元素尽可能分散均匀使得不会发生数据倾斜的情况。 我觉得这个图放出来应该都能看懂可以把每一个节点想象成一栋小楼。每个节点的多个指针可以看成是小楼的各个楼层很显然由于所有的小楼都排成一排所以每栋楼的每一层都只能看到同样高度最近的一栋。
比如上图当中的2只有一层那么它只能看到最近的一楼也就是3的位置。4有三层它的第一层只能看到5但是第二和第三层可以看到6。6也有三层由于6之后没有节点有超过两层的所以它的第三层可以直接看到结尾。
由于每个节点的高度是随机的所以每个节点能看到的情况是分散的可以防止数据聚集不平均等问题从而可以保证运行效率。 实现Node 数据结构的原理我想大家都可以看懂但是想要上手实现的话会发现还是有些困难会有很多细节和边界问题。这是正常的我个人的经验是可以先从简单的部分开始写把困难的部分留到最后。随着进度的推进对于问题的理解和解决问题的能力都会提升这样受到的痛苦最小半途而废的可能性最低。
在接下来的内容当中我们也遵守这个原则从简单的部分开始说起。 定义节点结构 整个SkipList本质是一个链表既然是链表当然存在节点所以我们可以先从定义节点的结构开始。由于我们需要一个字段来查找一个字段存储结果所以显然key和value是必须的字段。另外就是每个节点会有一个指针列表记录可以指向的位置。于是这个Node类型的结构就出来了
class Node:def __init__(self, key, valueNone, depth1):self._key keyself._value value# 一开始全部赋值为Noneself._next [None for _ in range(depth)]self._depth depthpropertydef key(self):return self._keykey.setterdef key(self, key):self._key keypropertydef value(self):return self._valuevalue.setterdef value(self, value):self._value value可能会有同学看不明白方法上面的注解这里做一个简单的介绍。这是Python当中面向对象的规范因为Python不像C或者是Java做了public和private字段的区分在Python当中所有的字段都是public的。显然这是不安全的有时候我们并不希望调用方可以获取我们所有的信息。所以在Python当中大家规定变量名前面添加下划线表示private变量这样无论是调用方还是阅读代码的开发者都会知道这是一个private变量。
在Java当中我们默认会为需要用到的private变量提供public的get和set方法Python当中也是一样。不过Python当中提供了强大的注解器我们可以通过添加property和param.setter注解来简化代码的编写有了注解之后Python会自动将方法名和注解名映射起来。比如我们类内部定义的变量名是_key但是通过注解我们在类外部一样可以处通过node.key来调用Python的解释器会自动执行我们加了注解的方法。以及我们在为它赋值的时候也一样会调用对应的方法。
比如当我们运行: node.key 3Python内部实际上是执行了node.key(3)。当然我们也可以不用注解自己写set和get这只是习惯问题并没有什么问题。 添加节点方法 我们定义完了Node结构之后并没有结束因为在这个问题当中我们需要访问节点第n个指针当然我们也可以和上面一样为_next添加注解然后通过注解和下标进行访问。但是这样毕竟比较麻烦尤其是我们还会涉及到节点是否是None以及是否能够看到tail的等等判断为了方便代码的编写我们可以将它们抽象成Node类的方法。
我们在Node类当中添加以下方法
# 为第k个后向指针赋值
def set_forward_pos(self, k, node):self._next[k] node# 获取指定深度的指针指向的节点的key
def query_key_by_depth(self, depth):# 后向指针指向的内容有可能为空并且深度可能超界# 我们默认链表从小到大排列所以当不存在的时候返回无穷大作为keyreturn math.inf if depth self._depth or self._next[depth] is None else self._next[depth].key# 获取指定深度的指针指向的节点
def forward_by_depth(self, depth):return None if depth self._depth else self._next[depth]这三个方法应该都不难看懂唯一有点问题的是query_key_by_depth这个方法在这个方法当中我们对不存在的情况范围了无穷大。这里返回无穷大的逻辑我们可以先放一放等到后面实现skiplist的部分就能明白。
把这三个方法添加上去之后我们Node类就实现好了就可以进行下面SkipList主体的编写了。 实现SkipList 接下来就到了重头戏了我们一样遵循先易后难的原则先来实现其中比较简单的部分。
首先我们来实现SkipList的构造函数以及随机生成节点深度的函数。关于节点深度SkipList当中会设计一个概率p。每次随机一个0-1的浮点值如果它大于p那么深度加一否则就返回当前深度为了防止极端情况深度爆炸我们也会设定一个最大深度。
在SkipList当中除了需要定义head节点之外还需要节点tail节点它表示链表的结尾。由于我们希望SkipList来实现快速查询所以SkipList当中的元素是有序的为了保证有序性我们把head的key设置成无穷小tail的key设置成无穷大。以及我们默认head的后向指针是满的全部指向tail。这些逻辑理清楚之后代码就不难了 class SkipList:def __init__(self, max_depth, rate0.5):# head的key设置成负无穷tail的key设置成正无穷self.root Node(-math.inf, depthmax_depth)self.tail Node(math.inf)self.rate rateself.max_depth max_depthself.depth 1# 把head节点的所有后向指针全部指向tailfor i in range(self.max_depth):self.root.set_forward_pos(i, self.tail)def random_depth(self):depth 1while True:rd random.random()# 如果随机值小于p或者已经到达最大深度就返回if rd self.rate or depth self.max_depth:return depthdepth 1到这里我们又往前迈进了一步距离最终实现只剩下增删查三个方法了。改和查的逻辑基本一致并且在这类数据结构当中一般不会实现修改因为修改可以通过删除和添加来代替并且对于大数据的场景而言也很少会出现修改。 query方法 这三个方法当中query是最简单的因为我们之前已经理解了查找的逻辑。是一个类似于贪心的算法说起来也很简单我们每次都尝试从最高的楼层往后看如果看到的数值小于当前查找的key那么就跳跃过去否则说明我们一下看得太远了我们应该看近一些于是往楼下走再重复上述过程。 比如上图当中假设我们要查找20首先我们在head的位置的最高点往后看直接看到了正无穷它是大于20的说明我们看太远了应该往下走一层。于是我们走到4层这次我们看到了17它是小于20的所以就移动过去。
移动到了17之后我们还是从4层开始看起然后发现每一层看到的元素都大于等于20那么说明17就是距离20最近的元素有可能20不存在。那么我们从17开始往后移动一格就是20可能出现的位置如果这个位置不是20那么说明20不存在。
这个逻辑应该很好理解结合我们之前Node当中添加的几个工具方法代码只有几行
def query(self, key):# 从头开始pnt self.root# 遍历当下看的高度高度只降不增for i in range(self.depth-1, -1, -1):# 如果看到比目标小的元素则跳转while pnt.query_key_by_depth(i) key:pnt pnt.forward_by_depth(i)# 走到唯一可能出现的位置pnt pnt.forward_by_depth(0)# 判断是否相等如果相等则说明找到if pnt.key key:return True, pnt.valueelse:return False, Nonedelete方法 query方法实现了delete就不远了。因为我们要删除节点显然需要先找到节点所以我们可以复用查找的代码来找到待删除的节点可能存在的位置。
找到了位置并不是一删了之我们删除它可能会影响其他的元素。
还拿上图举个例子假设我们要删除掉25这个元素。那么会发生什么 对于25以后的元素其实并不会影响因为节点之后后向指针会影响的是指向25的这些节点在这个例子当中是17这个节点。由于25被删除17的指针需要穿过25的位置继续往后指向后面的元素也就是55和31 。 比较容易想明白的是如果我们找到这些指向25的指针它们修改之后的位置是比较容易确定的因为其实就是25这个元素指向的位置。但是这些指向25的元素怎么获取呢
如果光想似乎没有头绪但是结合一下图不难想明白还记得我们查找的时候每次都看得尽量远的贪心法吗我们每次发生”下楼“操作的元素不就是最近的一个能看到25的位置吗也就是说我们把查找过程中发生下楼的位置都记录下来即可。
想明白了代码也就呼之欲出和query的代码基本一样无非多了几行关于这点的处理。
def delete(self, key):# 记录下楼位置的数组heads [None for _ in range(self.max_depth)]pnt self.rootfor i in range(self.depth-1, -1, -1):while pnt.query_key_by_depth(i) key:pnt pnt.forward_by_depth(i)# 记录下楼位置heads[i] pntpnt pnt.forward_by_depth(0)# 如果没找到当然不存在删除if pnt.key key:# 遍历所有下楼的位置for i in range(self.depth):# 由于是从低往高遍历所以当看不到的时候就说明已经超了breakif heads[i].forward_by_depth(i).key ! key:break# 将它看到的位置修改为删除节点同样楼层看到的位置heads[i].set_forward_pos(i, pnt.forward_by_depth(i))# 由于我们维护了skiplist当中的最高高度所以要判断一下删除元素之后会不会出现高度降低的情况while self.depth 1 and self.root.forward_by_depth(self.depth - 1) self.tail:self.depth - 1else:return Falseinsert 方法 最后是插入元素的insert方法了在insert之前我们也同样需要查找因为我们要将元素放到正确的位置。
如果这个位置已经有元素了那么我们直接修改它的value其实这就是修改操作了如果设计成禁止修改也可以返回失败。插入的过程同样会影响其他元素的指针指向的内容我们分析一下就会发现插入的过程和删除其实是相反的。删除的过程当中我们需要将指向x的指向x指向的位置而插入则是相反我们要把指向x后面的指针指向x并且也需要更新x指向的位置如果能理解delete那么理解insert其实是板上钉钉的。
我们直接来看代码
def insert(self, key, value):# 记录下楼的位置heads [None for _ in range(self.max_depth)]pnt self.rootfor i in range(self.depth-1, -1, -1):while pnt.query_key_by_depth(i) key:pnt pnt.forward_by_depth(i)heads[i] pntpnt pnt.forward_by_depth(0)# 如果已经存在直接修改if pnt.key key:pnt.value valuereturn# 随机出楼层new_l self.random_depth()# 如果楼层超过记录if new_l self.depth:# 那么将头指针该高度指向它for i in range(self.depth, new_l):heads[i] self.root# 更新高度self.depth new_l# 创建节点new_node Node(key, value, self.depth)for i in range(0, new_l):# x指向的位置定义成能看到x的位置指向的位置new_node.set_forward_pos(i, self.tail if heads[i] is None else heads[i].forward_by_depth(i))# 更新指向x的位置的指针if heads[i] is not None:heads[i].set_forward_pos(i, new_node)到这里整个代码就结束了。怎么说呢虽然它的原理不难理解但是代码写起来由于涉及到了指针的操作和运算所以还是挺麻烦的想要写对并且调试出来不容易。但相比于臭名昭著的各类平衡树而言已经算是非常简单的了。
SkipList在各类分布式系统和应用当中广泛使用算是非常重要的基础构建因此非常值得我们学习。并且我个人觉得这个数据结构非常巧妙无论是原理还是编码都很有意思希望大家也能喜欢。