Redis06:底层:跳跃链表skiplist
底层:跳跃链表skiplist
跳表
每个node的结构:含键和值,还有一个由多个node组成的list,就是本node在不同层指向下一个node的指针。
每次初始化时初始化最初的节点,也就是最左边的节点,它的高度就是跳表最大高度,初始每个位置指针指向null。
插入元素时首先判断该元素在集合中是否存在,从最初节点的最高位置开始寻找,向前的指针键值比要找的值大就向下移动,小就向前移动。如果不存在就插入,首先用随机函数每次随机一个数0或1,出现一次1就停止,出现0的个数就是层数。然后从初始节点的对应层开始,如果前一个节点值大,就生成一个node,令初始节点层指针指向node,node向前指向本来前面的值,如果前一个节点值小就向下移动然后继续判断,直到走完第一层,对应节点的所有向前指针都已经创建完毕了。
删除时只需要找到该节点该层谁指向它,把指针指向node前面的node即可。
java实现的跳表
跳表的node:
class SkipListNode{
public Integer value;
//指向下一个node,如果本node是5层,那么这个list大小为5,分别指向1-5层的下一个node
public ArrayList<SkipListNode> nextNodes;
public SkipListNode(Integer value) {
this.value = value;
nextNodes = new ArrayList<MySkipList.SkipListNode>();
}
}
跳表list的字段和构造方法:
class SkipList{
//最小值,跳表的起始节点,它的高度和跳表中node的最大高度一致
private SkipListNode head;
//跳表达到的最大高度
private int maxLevel;
//跳表中元素的个数
private int size;
//跳表决定高度的概率,以0.5的概率产生0,如果是1就晋升一层
private static final double PROBABILITY = 0.5;
public SkipList() {
size = 0;
maxLevel = 0;
head = new SkipListNode(null);
head.nextNodes.add(null);
}
}
跳表的add方法:
//添加元素
public void add(Integer newValue) {
//如果不包含这个元素,就插入跳表
if(!contains(newValue)) {
//随机确定新的层高
size++;
int level = 0;
while(Math.random() < PROBABILITY) {
level++;
}
while(level > maxLevel) {
head.nextNodes.add(null);
maxLevel++;
}
//建立node,确定head为查找时的起始node
SkipListNode newNode = new SkipListNode(newValue);
SkipListNode current = head;
//遍历的时候每次要么向前走,要么向下走,按照层数遍历
//每一层都要建立新node,然后连接前后指针
do {
//获得这一层里最后一个大于newValue的node
current = findNext(newValue, current, level);
//生成newNode其中一个向前的指针(指向更大的node),每次都从0添加
//这样最后如果一共有5层,相当于先添加第五层然后第四层。。
//每个指针指向的就是current的前一个节点
//连接从node到前
newNode.nextNodes.add(0, current.nextNodes.get(level));
//使newNode和它后面的节点产生联系,将后面节点的向前指针连接到newNode
//连接从node到后(指向更小的node)
current.nextNodes.set(level, newNode);
}while(level-- > 0);
}
}
查找方法findNext:
//找到要找的目标值e在这一层level里刚刚大于e的node,current是寻找的起始位置
private SkipListNode findNext(Integer e, SkipListNode current, int level) {
//找到current节点在当前层的前一个节点
SkipListNode next = current.nextNodes.get(level);
while(next != null) {
Integer value = next.value;
//如果本节点是8,前一个节点是10,要找的值是9就会出现这种情况
//此时直接返回,进入下一层
if(e < value) {
break;
}
//如果本节点是8,前一个节点是10,要找的值是12就会继续向前找
//更新current和next
//每次如果前一个节点比要找的值小就前进,否则找下一层
current = next;
next = current.nextNodes.get(level);
}
return current;
}
contains方法:
public boolean contains(Integer value) {
SkipListNode node = find(value);
return node != null && node.value != null && node.value == value;
}
//找到e对应的节点
private SkipListNode find(Integer e) {
return find(e, head, maxLevel);
}
delete方法:
public void delete(Integer deleteValue) {
if(contains(deleteValue)) {
SkipListNode deleteNode = find(deleteValue);
size--;
int level = maxLevel;
SkipListNode current = head;
do {
//找到刚刚比deleteNode稍大一点的node,然后将该node的各向前指针修正为
//原来deleteNode向前指针的指向,相当于删除了deleteNode
current = findNext(deleteNode.value, current, level);
if(deleteNode.nextNodes.size() > level) {
current.nextNodes.set(level, deleteNode.nextNodes.get(level));
}
}while(level-- > 0);
}
}
跳表的迭代器(遍历结果一定是从小到大的):
class SkipListIterator implements Iterator<Integer>{
SkipList list;
SkipListNode current;
public SkipListIterator(SkipList list) {
this.list = list;
this.current = list.getHead();
}
@Override
public boolean hasNext() {
// TODO Auto-generated method stub
return current.nextNodes.get(0) != null;
}
@Override
public Integer next() {
// TODO Auto-generated method stub
current = current.nextNodes.get(0);
return current.value;
}
}
redis中的跳表
在组织score时redis采用跳表,而从member到score的映射redis使用字典来存储。
在redis中每一层的晋升概率是25%,它是一种更扁平化的跳表,在单个层上遍历的节点个数就会稍多一些。
在调整元素权重时,redis采用对该节点先删后加的方式来进行。
如果权重都相同redis还会比较value值,使redis中的跳表有序。
redis计算元素排名rank时,是对跳表功能的一种加强,对于每个元素都有它的字段rank值,跳表在变化时会更新这个值。
在redis中跳跃表是有序集合sorted set的底层实现之一。跳跃表通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。支持平均OlogN、最坏ON复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
redis的链表由两种结构组成,一个是结点zskiplistNode,一个是表zskiplist。下图为一个跳表例子:
最左边就是一个zskiplist,它包括header(指向跳跃表的表头节点)、tail(指向跳跃表的表尾节点)、level(当前跳跃表的最大层数)、length(跳跃表的长度,也就是数据节点的数量)。
右侧是4个zskiplistNode,它有几个属性:
1、层level,也就是L1/L2/../L4等,每个层都有两个属性,分别是指针(指向表尾方向的其他节点)、跨度(记录跨度方向两点的距离),当从表头向表尾遍历跳表时,就会借助前进指针。
2、后退指针BW,它用来指向当前节点的前一个节点,在从表尾向表头遍历跳表时,就会借助后退指针。
3、分值:每个节点中的1.0、2.0、3.0。节点按照分值从小到大排列。
4、成员对象obj:o1、o2和o3,它是节点保存的值。
zskiplistNode的定义如下:
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
} zskiplistNode;
每次生成一个新跳跃表节点的时候,程序都会自动生成一个介于1到32之间的值作为level数组的大小,这个大小就是层的高度,每一层的晋升概率是25%,这是一个更偏于扁平的跳表。
span跨度这个字段是为了计算某个元素的排位rank使用的,在查找某个节点的过程中,将沿途访问的所有层的跨度都累加起来,得到的结果就是目标节点在跳跃表中的排位。
obj是一个指向SDS的指针,在同一个跳表中,各节点保存的对象obj必须是唯一的,而分值score可以是相同的,分值相同的节点会按照对象obj的字典序大小来进行排序。
zskiplist的定义如下:
typedef struct zskiplist{
//表头节点和表尾节点
structz zskiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
} zskiplist;