跳跃列表

477 阅读5分钟

跳跃列表,平衡树的概率替代方法。平衡树算法在执行操作时重新排列树,以保持一定的平衡条件并保证良好的性能。跳跃列表是使用概率平衡而不是严格强制平衡的数据结构。因此,在跳跃列表中插入和删除的算法要比平衡树的等效算法简单得多,而且要快得多

跳跃表介绍

跳跃表的每个节点包含一个高度k值,而每层高度都有一个指针,指向后一个节点,形成这层高度的有序的单链表。实际中,每个节点的这个高度k值是随机的,并且永远不会被修改。表现为一种带有跳过中间节点的额外指针的链表,即跳跃链表

在单链表中搜索时,我们可能需要检查列表的每个节点(下图)。

我们要查询值为12的节点,那么我们需要从head节点开始,依次向后对比,需要检查4次

而如果将该有序链表改为如下结构 从head节点开始,每隔2个节点的节点都增加一个指针,分别指向同高度的下一个节点,那么检查的节点数不超过n/2 + 1

我们要查询值为12的节点,那么我们需要从head节点开始,同时从上至下,依次向后对比。检查路径是head->node(6)->node(12)->node(17),累计检查3次

如果在此改造的链表基础上,在进行改造,将该链表的每隔4个节点,在新增一个指针,指向下一个同高度的节点

那么检查节点数 不会超过n/4 + 1

我们要查询值为12的节点,那么我们需要从head节点开始,同时从上至下,依次向后对比。检查路径是head->node(12)->node(17),累计检查2次

如果我们的链表的总长度为n的话,每隔2^n个节点,都新增一个指针的话,那么这个检查路径的复杂度减少为O(logn)

跳过列表算法

在跳跃表这种数据结构中,如何查找、插入和删除元素。下面分别用伪代码的分别演示

查找元素

我们通过遍历指针来查找元素,指针不会超出包含要查找的元素的节点。 假设当前的跳跃表结构如下

Search(list, searchKey)
	x := list->header
	for i:=list->level downto 1 do
		while x->forward[i]->key < searchKey do
			x := x->forward[i]
	x := x->forward[i]
	if x->key = searchKey then return x->value
		else return failure

解析:

  1. 获取当前跳跃表的头节点x := list->header
  2. 此头节点含有一个level字段,用于记录当前跳跃表中,除了头节点外节点中的高度k的最大值,此图中的k值为3
  3. 从最高层级别开始向下进行循环。
  4. 在当前级别中,从左向有右遍历当前层级中的链表,x->forward[i]是当前节点中的指针,指向下一个节点,如果下一个节点中的key值小于要搜索的searchkey值,则将下一个节点赋值给x,并回到while循环继续查找
  5. 在for循环中依次向下层遍历,最终获取的x要么是找到对应的searchkey值的接口,要么当前跳跃表中,不存在此值
插入元素

要插入元素,首先也是通过查询方法,找到准备插入新节点的位置。如下图,插入一个值为17的节点 插入节点的伪代码

Insert(list, searchKey, newValue)
	local update[maxLevel]
	x := list->header
	// 查找要插入的节点位置
	for i:=list->level downto 1 do
		while x->forward[i]->key < searchKey do
			x := x->forward
		// 保存i级高度,即将插入节点位置的左侧节点
		// 从最高层开始,i=4时,update[4] = node(6)
		// i = 3时,update[3] = node(6)
		// i = 2时,update[2] = node(9)
		// i = 1时,update[1] = node(12)
		update[i] := x	
	// 设置x为当前找到的插入位置的前一个接口,即node(12)
	x := x->forward
	// 如果当前的key值和要插入的searchKey是同一个节点,则直接赋值为新值即可,无需插入节点
	if x->key == searchKey then x->value := newValue
	else
		// 否则
		newLevel := randomLevel() // 获取随机高度,假设为2
		// 如果生成的随机高度 大于当前的跳跃表高度
		if nweLevel > list->level then
			// 循环高出的级别部分,并且分别设置指向此节点的前一个节点为头节点
			for i:=list->level+1 to newLevel do
				update[i] = list->header
			// 设置跳跃表的高度为最新的高度值
			list->level := newLevel
		// 创建新节点
		x := makeNode(newLevel, searchKey, value)
		// 执行插入节点
		// 由下向上进行循环新创建的节点,在搜索节点的过程中,保存了update[]的内容
		// update[4] = node(6)
		// update[3] = node(6)
		// update[2] = node(9)
		// update[1] = node(12)
		for i:=1 to newLevel do
			x->forward[i] := update[i]->forward[i]
			update[i]->forward[i] := x
删除元素

删除元素和插入元素类似,也需要先通过查询元素方法,找到想要删除的节点,然后删除节点,依然使用下图示例,删除值为17的这个节点

删除元素的伪代码如下

Delete(list, searchKey)
	local update[MaxLevel]
	x := list->header
	// 搜索节点
	for i:=list->level downto 1 do
		while x->forward[i]->key < searchKey do
			x := x->forward[i]
		// 保存i级高度,即将插入节点位置的左侧节点
		update[i] := x
	x := x->forward[i]
	// 如果搜索的节点key值,等于准备删除的searchKey值
	if x->key = searchKey then
		// 由下到上,开始重新拼接每层链表关系
		for i:=1 to list->level do
			// 如果i级别高度的forward指针,指向的不是当前x节点,则退出执行
			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] = NULL do
			list->level := list->level - 1