算法:跳跃表

Redis-跳跃(skip List) 什么是跳跃(skip list) 跳全称叫做跳跃,简称跳。跳是一个随机化的数据结构跳跃(skiplist)是一个用于有序元素序列快速搜索的随机化的数据结构,由美国计算机科学家William Pugh发明于1989年,论文《Skip lists: a probabilistic alternative to balanced trees》中提出。 它的效率和红黑树以及 AVL 树不相上下,但实现起来比较容易。是一种可以于平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下 阅读详情

1、跳跃表的定义

跳跃表(Skip List):增加了向前指针的链表叫做指针。跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质是一种可以进行二分查找的有序链表。跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

跳表是一个随机化的数据结构,可以被看做是二叉树的一个变种,它在性能上和红黑树、AVL树不相上下,但是跳表的原理非常简单, 举生活中一个简单例子,例如我们做公交车,有快线和慢线,想起我之前在广州念书,学校距离市中心比较远,也没有到达,有快线和慢线,快线的站比较少,慢线的站比较多,所以一般我得去快线。

2、跳跃表的原理分析

接下来我们详细得来谈谈跳跃表:

对于一个单链表来说,即使链表中的数据是有序的,如果我们想要查找某个数据,也必须从头到尾的遍历链表,很显然这种查找效率是十分低效的,时间复杂度为O(n)。

那么我们如何提高查找效率呢?我们可以对链表建立一级“索引”,每两个结点提取一个结点到上一级,我们把抽取出来的那一级叫做索引或者索引层,如下图所示。

假设我们要查找59这个结点,你可以先查找第一级索引,依次是14,34, 50,59在50和66之间,所以你查找他的下一级链表,也就是原始链表,查到59这个结点。大大提高了其效率。

当然我们可以在这个基础之上在建立多一级“索引”,如下图所示,

我们还是查找59这个结点,先查找14,50, 介于50和79之间,下一级链表,跳入下一级数,59介于50与66之间,跳入下一级链表,然后找到59。

上面的数量不大,不能够体现其威力,例如我的数据有1000000000个,这个时候的威力也就体现出来,虽然看起来简单,但威力是十分powerful。

3、跳跃表的时间复杂度

单链表的查找时间复杂度为:O(n),下面分析下跳表这种数据结构的查找时间复杂度:

我们首先考虑这样一个问题,如果链表里有n个结点,那么会有多少级索引呢?按照上面讲的,每两个结点都会抽出一个结点作为上一级索引的结点。那么第一级索引的个数大约就是n/2,第二级的索引大约就是n/4,第三级的索引就是n/8,依次类推,也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那么第k级的索引结点个数为:2/n^{k}。

假设索引有h级,最高级的索引有2个结点,通过上面的公式,我们可以得到n/(2^{h}) = 2,从而可得:h = log{2}n - 1。如果包含原始链表这一层,整个跳表的高度就是\log_{2}n。我们在跳表中查找某个数据的时候,如果每一层都要遍历m个结点,那么在跳表中查询一个数据的时间复杂度就为:O(m*logn)。

如上面的例子,m=2,所以跳表查找任意数据的时间复杂度为O(logn),这个查找的时间复杂度和二分查找是一样的,但是我们却是基于单链表这种数据结构实现的。不过,天下没有免费的午餐,这种查找效率的提升是建立在很多级索引之上的,即空间换时间的思想。其具体空间复杂度见下文详解。

4、跳跃表的空间复杂度

比起单纯的单链表,跳表就需要额外的存储空间去存储多级索引。假设原始链表的大小为n,那么第一级索引大约有n/2个结点,第二级索引大约有4/n个结点,依次类推,每上升一级索引结点的个数就减少一半,直到剩下最后2个结点,如下图所示,其实就是一个等比数列。

 这几级索引结点总和为:n/2 + n/4 + n/8 + ... + 8 + 4 + 2 = n - 2。所以跳表的空间复杂度为O(n)。也就是说如果将包含n个结点的单链表构造成跳表,我们需要额外再用接近n个结点的存储空间。

5、跳跃表的改进

其实从上面的分析,我们利用空间换时间的思想,已经把时间压缩到了极致,因为每一级每两个索引结点就有一个会被抽到上一级的索引结点中,所以此时跳表所需要的额外内存空间最多,即空间复杂度最高。其实我们可以通过改变抽取结点的间距来降低跳表的空间复杂度,在其时间复杂度和空间复杂度方面取一个综合性能,当然也要看具体情况,如果内存空间足够,那就可以选择最小的结点间距,即每两个索引结点抽取一个结点到上一级索引中。如果想降低跳表的空间复杂度,则可以选择每三个或者每五个结点,抽取一个结点到上级索引中。

 如上图所示,每三个结点抽取一个结点到上一级索引中,则第一级需要大约n/3个结点,第二级索引大约需要n/9个结点。每往上一级,索引的结点个数就除以3,为了方便计算,我们假设最高一级的索引结点个数为1,则可以得到一个等比数列,去下图所示:

通过等比数列的求和公式,总的索引结点大约是:n/3 + n /9 + n/27 + ... + 9 + 3 + 1 = n/2。尽管空间复杂度还是O(n),但是比之前的每两个结点抽一个结点的索引构建方法,可以减少了一半的索引结点存储空间。

实际上,在软件开发中,我们不必太在意索引占用的额外空间。在讲数据结构的时候,我们习惯性地把要处理的数据看成整数,但是在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。

6、跳跃表的操作

6.1、跳跃表的插入操作

跳表插入的时间复杂度为:O(logn),支持高效的动态插入。

在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是O(1)。但是为了保证原始链表中数据的有序性,我们需要先找到要插入的位置,这个查找的操作就会比较耗时。

对于纯粹的单链表,需要遍历每个结点,来找到插入的位置。但是对于跳表来说,查找的时间复杂度为O(logn),所以这里查找某个数据应该插入的位置的时间复杂度也是O(logn),

 6.3、跳跃表的更新

当我们不断地往跳表中插入数据时,我们如果不更新索引,就有可能出现某2个索引节点之间的数据非常多的情况,在极端情况下,跳表还会退化成单链表,如下图所示:

 

作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中的结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入和删除操作性能的下降。

如果你了解红黑树、AVL树这样的平衡二叉树,你就会知道它们是通过左右旋的方式保持左右子树的大小平衡,而跳表是通过随机函数来维护“平衡性”。

当我们往跳表中插入数据的时候,我们可以通过一个随机函数,来决定这个结点插入到哪几级索引层中,比如随机函数生成了值K,那我们就将这个结点添加到第一级到第K级这个K级索引中。如下图中要插入数据为45,K=1的例子:

 随机函数的选择是非常有讲究的,从概率上讲,能够保证跳表的索引大小和数据大小平衡性,不至于性能的过度退化。至于随机函数的选择,见下面的代码实现过程,而且实现过程并不是重点,掌握思想即可。

7、跳跃表的应用

应用场景:

节点增加和更新比较少,查询频次较多的情况。

使用跳跃表的产品:

1、Lucene, elasticSearch

2、Redis:

Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的 是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

8、跳跃表的代码实现

#pragma once
#include <vector>

struct SkipListNode
{
	SkipListNode(int data, int level);

	int m_data;
	std::vector<SkipListNode*> m_forward;
};

class SkipList
{
public:
	SkipList();
	~SkipList();

public:
	bool Find(int data);
	bool Insert(int data);
	bool Remove(int data);

public:
	void Print();

private:
	void FirstInsert(int data);
	bool IsEmpty();

private:
	SkipListNode* m_pHead;
	int m_count;
	int m_level;
};
#include "SkipList.h"
#include <random>
#include <iostream>
#include <vector>
#include <algorithm>

int GetRandLevel()
{
	int destLevel = 1;
	while (1 == rand() % 2) ++destLevel;
	return destLevel;
}

SkipListNode::SkipListNode(int data, int level)
	: m_data(data)
	, m_forward(level)
{

}

SkipList::SkipList()
	: m_pHead(nullptr)
	, m_count(0)
	, m_level(1)
{
	auto pHead = new SkipListNode(INT_MIN, m_level);
	m_pHead = pHead;
}

SkipList::~SkipList()
{
	auto* pNode = m_pHead;
	while (pNode != nullptr)
	{
		auto pNextNode = pNode->m_forward[0];
		delete pNode;
		pNode = pNextNode;
	}
}

bool SkipList::Find(int data)
{
	if (IsEmpty()) return false;

	auto pCurNode = m_pHead;
	for (int curLevel = m_level - 1; curLevel >= 0; --curLevel)
	{
		// 判断是否已经走到该层最后一个元素
		if (nullptr == pCurNode->m_forward[curLevel]) continue;
		while (pCurNode->m_forward[curLevel]->m_data < data)
		{
			pCurNode = pCurNode->m_forward[curLevel];

			// 判断是否已经走到该层最后一个元素
			if (nullptr == pCurNode->m_forward[curLevel]) break;
		}
	}

	if (data == pCurNode->m_forward[0]->m_data)
	{
		return true;
	}

	return false;
}

bool SkipList::IsEmpty()
{
	return 0 == m_count;
}

void SkipList::Print()
{
	auto* pCurNode = m_pHead;
	while (pCurNode != nullptr)
	{
		if (pCurNode->m_data >= 0)
		{
			std::cout << pCurNode->m_data << "(" << int(pCurNode->m_forward.size()) << ")" << " ";	
		}
		pCurNode = pCurNode->m_forward[0];
	}

	std::cout << std::endl;
}

void SkipList::FirstInsert(int data)
{
	int destLevel = GetRandLevel();
	if (destLevel > m_level)
	{
		destLevel = ++m_level; // 更新最高层数, 只增加一层
		m_pHead->m_forward.resize(m_level);
	}

	auto pNewNode = new SkipListNode(data, destLevel);

	for (int curLevel = m_level - 1; curLevel >= 0; --curLevel)
	{
		m_pHead->m_forward[curLevel] = pNewNode;
		pNewNode->m_forward[curLevel] = nullptr;
	}

	++m_count;
	return;
}

bool SkipList::Insert(int data)
{
	if (0 == m_count) // 首次插入
	{
		FirstInsert(data);
		return true;
	}

	auto pNode = m_pHead;
	// 需要更新下一跳指针的节点
	std::vector<SkipListNode*> updates(m_level);
	for (int curLevel = m_level - 1; curLevel >= 0; --curLevel)
	{
		if (nullptr == pNode->m_forward[curLevel])
		{
			updates[curLevel] = pNode;
			continue;
		}

		while (pNode->m_forward[curLevel]->m_data < data)
		{
			pNode = pNode->m_forward[curLevel];
			if (nullptr == pNode->m_forward[curLevel])
				break;
		}

		updates[curLevel] = pNode;
	}
	if ((pNode->m_forward[0] != nullptr) && (data == pNode->m_forward[0]->m_data))
	{
		// 已有节点处理
		return false;
	}
	int destLevel = GetRandLevel();
	if (destLevel > m_level)
	{
		destLevel = ++m_level; // 更新最高层数
		updates.resize(m_level);
		updates[m_level - 1] = m_pHead;
		m_pHead->m_forward.resize(m_level);
	}
	// 待插入元素
	auto* pNewNode = new SkipListNode(data, destLevel);
	// 从插入节点的最高层开始向下处理,对插入节点最高层之上的指针无影响
	for (int curLevel = destLevel - 1; curLevel >= 0; --curLevel)
	{
		pNewNode->m_forward[curLevel] = updates[curLevel]->m_forward[curLevel];
		updates[curLevel]->m_forward[curLevel] = pNewNode;
	}
	// 增加元素个数
	++m_count;
	return true;
}

bool SkipList::Remove(int data)
{
	auto* pCurNode = m_pHead;
	if (IsEmpty()) return false;

	// 需要更新下一跳指针的节点
	std::vector<SkipListNode*> updates(m_level);
	for (int curLevel = m_level - 1; curLevel >= 0; --curLevel)
	{
		if (nullptr == pCurNode->m_forward[curLevel])
		{
			updates[curLevel] = pCurNode;
			continue;
		}

		while (pCurNode->m_forward[curLevel]->m_data < data)
		{
			pCurNode = pCurNode->m_forward[curLevel];
			if (nullptr == pCurNode->m_forward[curLevel])
				break;
		}

		updates[curLevel] = pCurNode;
	}

	if (data != pCurNode->m_forward[0]->m_data)
	{
		// 没有该节点
		return false;
	}

	// 待删除元素
	auto* pRmoveNode = pCurNode->m_forward[0];

	// 将原来指向待删除元素的指针指向待删除元素的下一个元素
	for (int curLevel = m_level - 1; curLevel >= 0; --curLevel)
	{
		if (updates[curLevel]->m_forward[curLevel] == pRmoveNode)
			updates[curLevel]->m_forward[curLevel] = pRmoveNode->m_forward[curLevel];
	}

	// 修改层高,将为空的层删除
	for (int curLevel = m_level - 1; curLevel >= 0; --curLevel)
	{
		if (nullptr == m_pHead->m_forward[curLevel])
			--m_level;
	}

	m_pHead->m_forward.resize(m_level + 1);

	// 元素个数-1
	--m_count;
}

int main()
{
	SkipList skipList;

	std::vector<int> datas;
	datas.reserve(100);
	for (int idx = 0; idx < 100; ++idx)datas.push_back(idx);
	std::random_shuffle(datas.begin(), datas.end());

	for (auto& data : datas) skipList.Insert(data);
	skipList.Print();

	//std::cout << "--------------------------------" << std::endl;

	//for (auto& data : datas) skipList.Remove(data);
	//skipList.Print();
	return 0;
}

跳跃原理和实战 算法和数据机构 阅读详情

相关推荐

随机化算法跳跃

摘要:跳跃主要是用来减少查找的代价,作为单向链表,如果搜寻一个数据需要O(N)的时间代价,但是如果采用随机化跳跃则可以大大减少搜寻代价.(1)注意几个特点:[1]跳跃m阶指针所连接的元素至少具有m阶的指针.[2]主要难点集中在查询,插入,以及删除操作.这比普通链表要复杂的多.[3]首先看基本数据结构.#include "stdafx.h" #include "malloc.h" #includ

pyb的博客 1864

跳跃介绍

跳跃的查询、创建、删除

白小白的小白的博客 2990

高效算法:左偏树与跳跃的结合应用

本文深入探讨了左偏树在最小成本单调序列转换中的应用,并详细解释了跳跃的原理及其在查找、插入和删除操作中的性能优化。通过算法设计、状态转移方程和代码实现,揭示了如何利用左偏树与动态规划结合来解决复杂问题,以及跳跃如何通过增加索引层级来实现高效的二分查找。

weixin_35671843的博客 468

MIUI 14/澎湃OS性能模式实战:3种方法调用隐藏API与自动化方案

本文深度解析MIUI 14/澎湃OS性能模式的隐藏API调用与自动化方案,提供三种技术路径激活性能模式,包括ADB命令、反射调用和自动化工具配置。通过优化GPU渲染参数和性能监控,显著提升游戏帧率与稳定性,适合安卓开发者和小米设备用户。

djph26741的博客 461

跳跃skipList 跳 - 一种快速查、增、改的随机化链表数据结构

什么是跳跃 ​ Skip list(跳)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。 在Java的API中已经有了实现:分别是 ConcurrentSkipListMap(在功能上对应HashTable、HashMap、TreeMap) ; ConcurrentSkip

CoderZ's blog 1401

算法学习——跳跃(Skiplist)

尤其要注意销毁顺序,我们把跳跃想象成一个晾衣杆,中间的是一个个长度不定的一维数组,这样销毁的时候一定要注意找到数组的首地址。过程:和查找一样,从左上开始向右下找,这个过程会走出一条折线,这条折线每一行的最右侧节点就是插入位置前一个节点。例:假设插入元素10,这样第0行就是6这个节点,第1行也是6这个节点,第2行是1这个节点,以此类推。这个过程其实就是从左上往右下的走楼梯过程,需要注意比较的是当前节点下一个节点的值 与 待查找的值。是一个加强版链表,对链表的每个元素都设置一个层高,将链表竖向拉长的结果。

2301_80216298的博客 632

漫画算法:什么是跳跃

漫画算法:什么是跳跃? // Copyright (C) 2017 // 版权所有。 // // 文件名:SkipList // 文件功能描述: 跳跃的实现 增删查改 // 创建者:zhaoguanghui // 时间:2017/9/1 // 版本:V1.0.0 //---------------------------------------------------------

宁静致远 1327

算法导论》:跳跃(Skip List)

跳跃简介  Skip lists  are data structures  that use probabilistic  balancing rather  than  strictly  enforced balancing. As a result, the algorithms  for insertion  and deletion in skip lists  are much...

Salmon_lee的博客 1505

算法数据结构:二分查找与跳跃

本笔记属于船说系列课程之一,课程链接:哔哩哔哩_bilibili你也可以选择购买『船说系列课程-年度会员』产品『船票』,畅享一年内无限制学习已上线的所有船说系列课程:船票购买入口。

qq_58240849的博客 969

JAVA:实现一个SkipList跳跃算法(附带源码)

JAVA:实现一个SkipList跳跃算法(附带源码)

欢迎来到我的博客!这里是一个专注于计算机技术的分享平台,涵盖编程开发、算法研究、系统架构、软件工程等多个领域。无论你是初学者还是资深开发者,都能在这里找到有价值的内容。 1891

Python 算法高级篇:跳跃和布隆过滤器的应用

在计算机科学中,数据结构算法是构建强大应用的基础。本文将介绍两个非常有用的数据结构跳跃和布隆过滤器。这些数据结构可以在各种应用中提供高效的数据存储和检索解决方案。

小蓝枣的博客 371

数据结构算法】第45篇:跳跃(Skip List)

跳跃是一种随机化的数据结构,可以看作“带索引的链表”。它用空间换时间,通过多层索引实现近似二分查找,插入、删除、查找的平均时间复杂度都是 O(log n),且实现比平衡树简单得多。Redis 的有序集合(ZSet)底层就是用跳跃实现的。这一篇我们实现跳跃的基本结构,并完成增删改查操作

2301_80672009的博客 592

Redis第3讲——跳跃详解

在Redis中,跳跃是有序集合(zSet)数据类型的实现之一,也在集群节点中用作内部数据结构,除此之外,跳跃在Redis里面没有其它用途。跳跃支持O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点,在大部分情况下,跳跃的效率可以和红黑树、AVL树不相上下,但跳原理更加简单、实现起来也更简单直观。

让~学习~成为一种习惯(橡 皮 人の技术博客) 2904

图解Redis之数据结构篇——跳跃

文章导航-readme 前言 跳跃是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。这么说,我们可能很难理解,我们可以先回忆一下链表。 一、复习跳跃 1.1 什么是跳跃 对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是...

yufaxingxing的博客 977

Redis学习(四)跳跃(skiplist)

1. 什么是跳跃(skiplist) 跳跃(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。 Redis 使用跳跃作为有序集合键(ZSET)的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis 就会使用跳跃来作为有序集合键的底层实现。 Redis 只在两个地方用到了跳跃(skiplist) 实现有序集合键 在集群节点中用作内部数据结构 1.2 为什么需要跳跃(skipli

wifiiii的博客 1330

hbase---跳跃(skiplist)

跳跃(SkipList)是一种能高效实现插入、删除、查找的内存数据结构,这些操作的期望复杂度都是O(logN)。与红黑树以及其他的二分查找树相比,跳跃的优势在于实现简单,而且在并发场景下加锁粒度更小,从而可以实现更高的并发性。正因为这些优点,跳跃广泛使用于KV数据库中,诸如Redis、LevelDB、HBase都把跳跃作为一种维护有序数据集合的基础数据结构。 众所周知,链表这种数据结构的查询复杂度为O(N),这里N是链表中元素的个数。在已经找到要删除元素的情况下,再执行链表的删除操作其实非常高效,

cyq6239075的博客 1561

Redis数据结构跳跃 skiplist

跳跃(Skiplist)是一种随机化的数据结构,它允许快速的搜索、删除和插入操作,其平均时间复杂度为O(log N)。跳跃是由William Pugh在1990年发明的一种数据结构,最初是为了在无序数组中查找元素而设计的。Redis中的跳跃与Pugh的原始设计略有不同,但基本原理相同。跳跃作为Redis有序集合的底层实现,以其高效、有序、空间复杂度低和实现简单的特点,为Redis提供了强大的支持。无论是搜索、插入、删除操作,还是范围查询,跳跃都能提供优秀的性能。

Python老吕的博客 1881
上一篇: 算法:和为S的两个数字VS和为S的连续正数序列
下一篇: C/C++:把字符串转换成整数
chen_jin_sheng
博客等级 码龄8年 25粉丝 · 53原创
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值