SkipList

本文介绍了跳表的基本原理及特点,包括其如何通过多级索引来提高链表的查找效率,并详细解释了搜索、插入和删除操作的具体过程。此外,还提供了跳表的Java实现代码。

1、什么是SkipList

当使用链表存放有序数据的时候,查找某个数据或者添加某个数据的复杂度是O(n),每次查找都需要顺序遍历。加快搜索的方法有哪些呢?想想数据库的设计思路,对,使用索引,利用空间换取时间。跳表基本上采用的就是这个思路。


图1

在最底层的链表中随机抽取一定是数量的节点作为索引,形成另一个链表。然后继续从新生成的链表中随机抽取点作为新的索引,继续形成另一个链表。将这个步骤继续多次。
可以得到上面图中的结构。通过观察上面的图,可以发现越往上的链表越稀疏。如果对多叉树比较熟悉的话,可以将上面的结构立即为多叉树。因此可以猜测SkipList的时间复杂度是O(ogn)。

来看看SkipList如何工作的。


图2
由于上层的链表十分稀疏,所以上层链表遍历的时候可以忽略掉底层链表中许多的元素。比如上面的图中,插入的数据是80的时候,从30到50可以把40忽略掉。如果链表中的数据规模达到一定地步,上层链表的一次遍历可以跳过的元素是相当可观的。就像二叉树一样,没经过一个节点都可以跳过当前元素的一般。而由于SkipList每层链表都是按照一定概率生成的,虽然不是像二叉树那样经过一个节点可以忽略一半元素,但是效果也还是不错的。

2、SkipList中的概念

2.1、Node

每个节点有一个值,用于排序。并且有一个数组用于存放后面的节点。比如图1中,节点6的nexts数组大小是4, 数组的数据依次指向节点7,节点9,节点25,NIL节点。


2.2、SkipList

跳表中有一个head节点,同时设置一个level表示当前最大的层数。比如图2中,level = 4。

而且每个跳表都有一个常量的MAX_LEVEL,链表层数无论如何增加也不能超过这个值。

2.3、NIL

NIL值用于表示比任何值都大。一般存放在每个链表的结尾。表示当前链表搜索可以结束了。

3、SkipList的初始化

An element NIL is given a key greater than any legal key. All levels of all skip lists are terminated with NIL.A new list is initialized so that the levelof the list is equal to 1 and all forward pointers of the list’s header point to NIL.
(摘自论文  A Skip List Cookbook  Author: William Pugh)

4、SkipList的搜索

We search for an element by traversing forward pointers that do not overshoot the node containing the element being searched for (Figure 2). When no more progress can be made at the current level of forward pointers, the search moves down to the next level. When we can make no more progress at level 1, we must be immediately in front of the node that contains the desired element (if it is in the list).

Search(list, searchKey)
	x := list→header
	-- loop invariant: x→key < searchKey
	for i := list→level downto1 do
		whilex→forward[i]→key < searchKey do
			x := x→forward[i]
	-- x→key < searchKey ≤x→forward[1]→key
	x := x→forward[1]
	if x→key = searchKey then returnx→value
	else return failure

5、SkipList的插入和删除

To insert or delete a node, we simply search and splice.Figure 3 gives algorithms for insertion and deletion. A vector updateis maintained so that when the search is complete (and we are ready to perform the splice), update[i] contains a pointer to the rightmost node of level ior higher that is to the left of the location of the insertion/deletion. If an insertion generates a node with a level greater than the previous maximum level of the list, we update the maximum level of the list and initialize the appropriate portions of the update vector. After each deletion, we check if we have deleted the maximum element of the list and if so, decrease the maximum level of the list.

Insert(list, searchKey, newValue)
	localupdate[1..MaxLevel]
	x := list→header
	for i := list→level down to 1 do
		whilex→forward[i]→key < searchKey do
			x := x→forward[i]
		update[i] := x
	x := x→forward[1]
	if x→key = searchKey then x→value := newValue
	else
		newLevel := randomLevel()
		if newLevel > list→level then
			for i := list→level + 1 to newLevel do
				update[i] := list→header
			list→level := newLevel
		x := makeNode(newLevel , searchKey, value)
		for i := 1 tonewLevel do
			x→forward[i] := update[i]→forward[i]
			update[i]→forward[i] := x

Delete(list, searchKey)
	local update[1..MaxLevel]
	x := list→header
	for i := list→level down to 1 do
		while x→forward[i]→key < searchKey do
			x := x→forward[i]
		update[i] := x
	x := x→forward[1]
	if x→key = searchKey then
		for i := 1 to list→level do
			if update[i]→forward[i] ≠ x then break
			update[i]→forward[i] := x→forward[i]
		free(x)
		while list→level > 1 and list→header→forward[list→level] = NIL do
			list→level := list→level – 1	

6、新插入节点随机层数算法

randomLevel()
	lvl := 1
	while random() < p and lvl < MaxLevel do
		lvl := lvl + 1
	return lvl

这里设置p为1/2。
如果random()函数设计的足够好,那么每次random()产生的数据都有50%的概率小于1/2。也就是说层数为1的概率为100%,层数为2的概率为50%,层数为3的概率为25%,层数为4的概率为12.5%,依次衰减。所以可以看出越往上的链表,其中的节点数也就越少。

7、java版本的实现

import java.util.ArrayList;
import java.util.List;


public class Node<T extends Comparable<T>> {
	private T value;
	private List<Node<T>> nexts;
	
	public Node(){
		this.value = null;
		this.nexts = new ArrayList<Node<T>>(0);
	}
	
	public Node(T value){
		this.value = value;
		this.nexts = new ArrayList<Node<T>>(0);
	}
	
	public Node(int size){
		this.value = null;
		this.nexts = new ArrayList<Node<T>>(size);
	}
	
	public void setValue(T value) {
		this.value = value;
	}
	public T getValue() {
		return value;
	}
	public void setNexts(List<Node<T>> nexts) {
		this.nexts = nexts;
	}
	public List<Node<T>> getNexts() {
		return nexts;
	}
}

注意这里的nexts数组就是伪代码中的forward数组。

public class SkipList<T extends Comparable<T>> {
	public static final int MAX_LEVEL = 100;
	// the head of the SkipList
	private Node<T> head = null;

	private int level = 0;
	// initial of the SkipList
	public SkipList() {
		// 初始化头部
		setHead(new Node<T>(MAX_LEVEL));

		// 初始化head的nexts数组
		for (int i = 0; i < MAX_LEVEL; i++) {
			head.getNexts().add(null);
		}
		
		// 设置深度
		setLevel(1);
	}

	@SuppressWarnings("unchecked")
	public void insert(T newValue) {
		if (newValue == null) {
			return;
		}
		// 更新数组
		Node<T> update[] = new Node[MAX_LEVEL];
		// 获得头部
		Node<T> p = this.head;

		int j = this.level - 1;
		while (j >= 0) {
			// 下一个节点不为NULL 并且  值小于newValue
			while ((Node<T>) p.getNexts().get(j) != null
					&& newValue.compareTo(((Node<T>) p.getNexts().get(j))
							.getValue()) >= 0) {
				p = (Node<T>) p.getNexts().get(j);
			}
			update[j] = p;
			j--;
		}

		int newLevel = randomLevel();

		if (newLevel > this.level) {
			// 当newLevel大于当前的level时,设置更新数组中level到newLevel为head
			for (int i = newLevel - 1; i > level - 1; i--) {
				update[i] = this.head;
			}

			// 当newLevel大于当前的level时,需要更新当前level
			this.level = newLevel;
		}

		Node<T> newNode = new Node<T>();
		newNode.setValue(newValue);

		for (int i = 0; i < newLevel; i++) {
			newNode.getNexts().add(update[i].getNexts().get(i));
			update[i].getNexts().set(i, newNode);
		}
		
	}
	
	//获取随机层数
	private int randomLevel() {
		int level = 1;
		while (Math.random() < 1.0 / 2.0 && level < MAX_LEVEL) {
			level++;
		}
		return level;
	}
	public void setHead(Node<T> head) {
		this.head = head;
	}
	public Node<T> getHead() {
		return head;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public int getLevel() {
		return level;
	}

	public static void main(String[] args) {
		SkipList<Integer> skip = new SkipList<Integer>();

		for (int i = 0; i < 10; i++) {
			skip.insert(new Integer(i));
		}
	}
}





### Skiplist 数据结构概述 Skiplist 是一种基于概率的数据结构,用于高效地实现有序集合的操作。它通过构建多层链表的方式,在保持数据有序的同时支持快速的插入、删除和查找操作。每层链表中的节点数量逐级减少,从而形成类似于二分查找的效果。 在 Redis 中,Skiplist 被用来实现 `zset`(sorted set),即有序集合对象的一部分[^1]。具体来说,一个 `zset` 同时包含一个字典和一个跳跃表,其中字典负责映射成员与其分数的关系,而跳跃表则维护按分数排序的成员序列。 以下是关于如何实现或使用 Skiplist 的详细介绍: --- ### Skiplist 的基本组成 #### 1. **节点定义** 每个节点通常包含以下几个字段: - 成员值(member) - 分数值(score),表示成员的权重 - 下一层指向相同位置的指针数组(level) ```c typedef struct skiplistNode { char *member; // 成员名称 double score; // 成员对应的分数 struct skiplistLevel { struct skiplistNode *forward; // 指向下一层的指针 unsigned int span; // 当前跨度(可选优化项) } level[]; } skiplistNode; ``` #### 2. **头结点与层数控制** 为了方便管理整个跳跃表,还需要定义一个头部节点以及一些元信息来记录当前的最大层数和其他属性。 ```c typedef struct skiplist { struct skiplistNode *header; // 头部节点 unsigned long length; // 总节点数 int level; // 最大层数 } skiplist; ``` --- ### Skiplist 的核心算法 #### 1. **随机化层数** 每次创建新节点时,都需要为其分配合适的层数。这一步骤决定了性能的关键特性——时间复杂度接近于 O(log n)。Redis 使用如下方法计算随机层数: ```c #include <stdlib.h> #define MAX_LEVEL 32 // 假设最大可能层数为32 int randomLevel(void) { int level = 1; while ((rand() & 0xFFFF) < (REDIS_SKIPLIST_P << 16)) { // REDIS_SKIPLIST_P 表示提升的概率,默认为0.25 level += 1; } return (level < MAX_LEVEL) ? level : MAX_LEVEL; } ``` #### 2. **插入操作** 插入过程分为两步:先定位合适的位置并更新路径上的所有前置节点;再根据随机化的层数调整各级链接关系。 ```c void insert(skiplist *sl, double score, char *member) { skiplistNode *update[MAX_LEVEL]; // 记录各层需修改的前驱节点 skiplistNode *x; x = sl->header; for (int i = sl->level - 1; i >= 0; i--) { while (x->level[i].forward && compare(x->level[i].forward, member, score) < 0) { x = x->level[i].forward; } update[i] = x; // 更新第i层的前驱节点 } int new_level = randomLevel(); // 随机决定新增节点的高度 if (new_level > sl->level) { for (int i = sl->level; i < new_level; i++) { update[i] = sl->header; } sl->level = new_level; } x = createNode(new_level, score, member); // 创建新的节点 for (int i = 0; i < new_level; i++) { x->level[i].forward = update[i]->level[i].forward; update[i]->level[i].forward = x; } sl->length++; } ``` #### 3. **查找操作** 利用跳跃表的特点,从最高层开始逐步缩小范围直至找到目标元素为止。 ```c skiplistNode* find(skiplist *sl, double score, char *member) { skiplistNode *x = sl->header; for (int i = sl->level - 1; i >= 0; i--) { while (x->level[i].forward && compare(x->level[i].forward, member, score) < 0) { x = x->level[i].forward; } } x = x->level[0].forward; if (x != NULL && equal(x, member, score)) { return x; } return NULL; } ``` #### 4. **删除操作** 删除逻辑较为简单,只需沿着之前保存的路径逐一断开对应连接即可。 ```c void remove(skiplist *sl, double score, char *member) { skiplistNode *update[MAX_LEVEL]; skiplistNode *x = sl->header; for (int i = sl->level - 1; i >= 0; i--) { while (x->level[i].forward && compare(x->level[i].forward, member, score) < 0) { x = x->level[i].forward; } update[i] = x; } x = x->level[0].forward; if (x != NULL && equal(x, member, score)) { for (int i = 0; i < sl->level; i++) { if (update[i]->level[i].forward == x) { update[i]->level[i].forward = x->level[i].forward; } } freeNode(x); sl->length--; while (sl->level > 1 && sl->header->level[sl->level - 1].forward == NULL) { sl->level--; } } } ``` --- ### Skiplist 的实际应用 除了 Redis 中的 `zset` 实现外,Skiplist 还可以广泛应用于其他场景,例如分布式系统的路由表设计、日志文件的时间戳索引等。其主要优势在于能够以较低的空间代价换取较高的访问速度。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值