C++(数据结构与算法):29---跳表的实现(链表形式)

前言

一、跳表的表示

  • 在一个用有序链表描述的n个数对的字典中进行查找,至少需要n次关键字比较
  • 如果在链表的中部节点加一个指针,则比较次数可以减少到n/2+1。这时,为了查找一个数对,首先与中间的数对比较:
    • 如果查找的数对关键字比较小,则仅在链表的左半部分继续查找
    • 否则,在链表的有半部分继续查找

一级跳表

  • 下图的有序链表中有7个数对,该链表增加了一个头结点和尾节点,节点中的数是关键字。对链表的所搜最差的情况要进行7此关键字的比较

  • 如果在中间的节点加入一个指针,那么最坏的情况下的比较次数减少为4次。例如:
    • 例如我们要查找关键字26的数对:首先与中间的关键字40比较,发现26<40,则在40的左边进行查找
    • 例如我们要查找的关键字为75:首先与中间的关键字40比较,发现75>40,则在40的右边进行查找

多级跳表

  • 下面我们以三级跳表为例(也可以为更高级)
  • 下面的三级跳表是在一级跳表的基础上,分别在链表的左半部分和右半部分的中间节点增加一个指针
    • 第0级:是最下面的初始链表,包括所有7个熟读
    • 第1级:包括字典的第2、4、6个数对
    • 第2级:只包括中间的第4个数对

  • 查找案例,如果我们想要查找关键字30
    • 首先与2级比较,所需要的时间为Θ(1),发现30比40小
    • 再在链表的左半部分的1级链表查找,所需的时间也为Θ(1),发现30>24
    • 再在0级链表的右半部分查找

二、n级跳表的规则

  • 对于n个数而言:
    • 0级链表包括所有数对,有n/2^{0}个记录
    • 1级链表每2个数对取一个n/2^{1}个记录
    • 2级链表每4个数对取一个n/2^{2}个记录
    • ....
    • i级链表每2^{i}个数对取一个,并且i级链表有n/2^{i}个记录
    • 最高级的链表中一定只有一个记录
  • 对于每个数对而言:
    • 一个数对属于i级链表(i的取值范围为:0~i)
    • 每个数对只属于某一级链表,而不同时属于多个级别的链表。例如上图中的关键字20属于0级链表,关键字24属于1级链表,关键字40属于3级链表

三、跳表的插入与删除

跳表的插入

  • 插入的新数对属于i级链表的概率为1/2^{i}
  • 在实际确定新数对所属的链表级别时,应考虑各种可能的情况:
    • 把新数对插入i级链表的可能性为p^{i},在上图中,p=0.5
    • 对一般的p,链表的级数为
    • 在一个规则的跳表中,i级链表包含1/p个i-1级链表的节点
  • 例如:我们想要插入键值为77的数对:
    • 首先通过搜索来确定链表没有这个数对
    • 搜索时,用到了关键字为40的节点中一个2级链表指针、关键字为75的节点中的一个1级指针、一个0级链表的指针。在下面的1张图中,这3个指针被一条虚线切割
    • 新数对插入位置在75和80之间,下面的2张图图是插入后的结果

  • 总结:
    • 插入时,要为新数对分配一个级(即确定它属于哪一级链表),分配过程由下面将要介绍的随机数生成器来完成
    • 若新数对属于i级链表,则插入结果仅影响由虚线切割的0-i级链表指针

跳表的删除

  • 对于删除操作,我们无法控制结果
  • 例如要删除下面的节点77,首先要找到77。所遇到的链表指针有2级指针、1级指针、0级指针。因为77位1级链表中数对的关键字,所以只需要改变0级和1级链表指针即可

  • 删除之后,得到下面的结构

四、级的分配

  • 在规则的跳表结构中,i-1级链表的数对个数与i级链表的数对个数之比是一个分数p。因此,属于i-1级链表的数对同时属于i级链表的概率为p
  • 假设用一个统一的随机数生成器产生0和1之间的实数,产生的随机数<=p的概率为p:
    • 若下一个随机数<=p,则新数对应在1级链表上
    • 要确定该数对是否在2级链表上,要由下一个随机数来决定
    • 若新的随机数<=p,则该元素也属于2级链表
    • 重复这个过程,直到一个随机数>p为止
  • 上面的方法有潜在的缺点,某些数对被分配的级数可能特别大,远远超过了,其中N为字典数对的最大预期数目。为避免这种情况,可以设定一个级数的上限maxLevel,最大值为
  • 上面的方法还有一个缺点,即使采用了级数的上限maxLevel,还可能出现这样的情况:在插入一个新数对之前有3个链表,而在插入之后就有了10个链表。也就是所,尽管3~8级链表没有数对,新数对却被分配到9级链表。换句话说,在插入前后,没有3~8级链表。因此这些空级链表并没有什么好处,我们可以把新纪录的链表等级调整为3

例如:

  • 用跳表描述一个最多有1024个数对的字典。设p=0.5,则maxLevel为
  • 假定从一个空字典开始,用一个具有头结点和尾节点的跳表结构描述,头结点有10个指针,每个指针对应一条链表,且从头结点指向尾节点
  • 当插入第一个数对时,为其在0~9之间分配一个等级。若分配的等级为9,则因为跳表还没有0~8级的记录,所以可以把等级改为0,这时只需要修改一个指针即可
  • 还有另一种等级分配的方法,把随机数生成器产生的数分为几段。第一段是1~1/p,第二段是1/p~1/p^{2},等等。若产生的随机数出现在第i段,则把数对插入i-1级链表

五、跳表的编码实现

头文件的定义

#include <iostream>
#include <stdio.h>
#include <sstream>
#include <string>
#include <utility>
#include <math.h>

using std::cout;
using std::cin;
using std::endl;
using std::string;
using std::pair;
using std::ceil;
using std::logf;

异常类定义

class illegalParameter 
{
private:
	std::string message;
public:
	illegalParameter(const char* theMessage="Illegal Paramter"):message(theMessage){}
	const char* what() {
		return message.c_str();
	}
};

字典抽象类定义

  • 跳表类继承于这个抽象类,然后实现抽象类中的方法

节点的定义 

template<class K,class E>
struct skipNode
{
	typedef std::pair<const K, E> pairType;

	pairType element;
	/*
		一个节点可能拥有大于自身级数的指针,
		因此定义一个指针域数组,next[0]代表0级链表的指针域,next[1]代表1级链表的指针域,
		以此类推,next[i]代表i级链表的指针
	*/
	skipNode<K,E> **next;

	/*
		size代表这个节点的指针域最大为多少,因为跳表的级数从0开始,
		因此size代表这个节点最大域指针所在的跳表级别为size-1
	*/
	skipNode(const pairType& thePair,int size):element(thePair) {
		next = new skipNode<K, E>*[size];
	}
}; 
  • 因为每个节点都可能拥有多级链表的指针,因此我们定义了一个next数组,next[0]就代表0级链表、next[1]就代表1级链表
  • 例如下面的节点40,next[0]就是其所在最底层的链表的指针,next[1]就是其在第2级链表中的指针,其没有next[2],因为跳表最高为2级

跳表类skipLits定义

template<class K,class E>
class skipList :public dictionary<K, E>
{
public:
	skipList(K largeKey, int maxPairs = 10000, float prob = 0.5);
	~skipList();

	bool empty()const override;
	int size()const override;
	std::pair<const K, E>* find(const K& theKey)const override;
	void erase(const K& theKey) override;
	void insert(const std::pair<const K, E>& thePair) override;

public:
	int level()const;
	skipNode<K, E>* search(const K& theKey)const;
protected:
	int cutOff;  //见博客注释
	int levels;  //当前最大的非空链表
	int dSize;   //链表节点数(链表大小)
	int maxLevel;//跳表允许的最大层数
	K tailKey;   //链表中的最大关键字
	skipNode<K, E>* headerNode; //头结点指针
	skipNode<K, E>* tailNode; //尾节点指针
	skipNode<K, E>** last; //在插入和删除时会用到,见博客注释
};
  • 其继承于dictionary抽象类,然后重写其中的方法
  • 下面是数据成员的解释
    • cutOff:当我们需要向跳表中插入元素时,需要随机一个链表来进行遍历,但是不知道最初应该选择哪一级链表,这个变量就是用来产生一个随机的数字,用产生的随机数来作为最初选择的链表级数。RAND_MAX详情见文章:https://blog.csdn.net/qq_41453285/article/details/103456053
    • levels:代表当前跳表最高的级数
    • dSize:链表中的节点数
    • maxLevel:系统允许跳表的最大级数
    • tailKey:当前跳表允许的最大关键字值
    • headNode、tailNode:头结点指针、尾节点指针
    • last:在插入和删除时会用到,见下面

构造函数

/*参数:
	1.字典中最大的关键字 2.字典数对最大预期数目 3.是i-1级链表也同时是i级链表数对的概率*/
template<class K, class E>
skipList<K, E>::skipList(K largeKey, int maxPairs = 10000, float prob = 0.5)
{
	/*
	当我们需要向跳表中插入元素时,需要随机一个链表来进行遍历,但是不知道最初应该选择哪一级链表,
	这个变量就是用来产生一个随机的数字,用产生的随机数来作为最初选择的链表级数
	RAND_MAX详情见文章:https://blog.csdn.net/qq_41453285/article/details/103456053
	*/
	this->cutOff = prob*RAND_MAX;
	this->maxLevel = (int)std::ceil(std::logf((float)maxPairs) / std::logf(1 / prob)) - 1;
	this->levels = 0;
	this->dSize = 0;
	this->tailKey = largeKey;

	//当前最大的字典
	std::pair<K, E> tailPair;
	tailPair.first = this->tailKey;

	//链表为空时,最大的字典放置于头结点与尾节点中
	this->headerNode = new skipNode<K, E>(tailPair,this->maxLevel+1);
	this->tailNode = new skipNode<K, E>(tailPair, 0);

	this->last = new skipNode<K, E>*[this->maxLevel + 1];

	//链表为空时,任意级别链表的头指针都指向于尾节点
	for (int i = 0; i <= this->maxLevel; ++i)
		this->headerNode->next[i] = this->tailNode;
}
  • 参数解释:
    • largeKey:字典中最大的关键字,用来赋值给tailKey数据成员,插入和查找元素时不能大于这个键值
    • maxPairs:跳表中最大可以达到的元素个数,也同时作为上面介绍过的公式中的N的值
    • prob:上面介绍过的层与层之间对比的分数p,见文章上面的级分配介绍
  • 代码解析:
    • maxLevel的赋值:调用库函数,构造公式,来对跳表中的最大元素个数进行分配
    • 链表为空时,头结点全部指向与尾节点

析构函数

template<class K, class E>
skipList<K, E>::~skipList()
{
	skipNode<K, E>* nextNode;
	while (this->headerNode != this->tailNode) {
		nextNode = this->headerNode->next[0];
		delete this->headerNode;
		this->headerNode = nextNode;
	}
	delete this->tailNode;

	delete[] this->last;
	this->last = nullptr;
}
  • 直接释放链表就可以了,然后再把last成员释放

判断大小与释放为空

  • 检查dSize成员即可
template<class K, class E>
bool skipList<K, E>::empty()const
{
	return this->dSize == 0;
}

template<class K, class E>
int skipList<K, E>::size()const
{
	return this->dSize;
}

查找元素

template<class K, class E>
std::pair<const K, E>* skipList<K, E>::find(const K& theKey)const
{
	//如果插入的关键字大于链表中要求的最大限制,返回nullptr
	if (theKey >= this->tailKey)
		return nullptr;

	skipNode<K, E>* beforeFindNode=this->headerNode;
	//从最顶级的链表开始遍历跳表,直到寻找到不小于要寻找的键值的节点
	for (int i = this->levels; i >= 0; --i)
	{
		while (beforeFindNode->next[i]->element.first < theKey) {
			beforeFindNode = beforeFindNode->next[i];
		}
	}

	//判断下一个节点的值是否等于要查找的键值,是的话返回节点
	if (beforeFindNode->next[0]->element.first == theKey)
		return &beforeFindNode->next[0]->element;

	//不等于返回nullptr
	return nullptr;
}
  • 根据关键字查找节点,返回节点。如果不存在关键字则返回空
  • 函数思想:
    • 从最高级链表开始查找。直到查找到0级链表
    • 在每一级链表,从最左端开始遍历,如果找到就终止查找
    • 查找到的时候,返回的是要查找的关键字所在位置的前一个节点指针,接着在下面用一个if判断,这个节点的下一节点指针是否使我们要查找的节点

level函数

  • 插入元素时,我们不采用从最顶级或者最低级的链表来进行遍历查找需要插入的位置,而是使用level函数来随机产生一个层数,来供插入元素时使用
//返回一个表示链表级的随机数,这个数不大于maxLevel
template<class K, class E>
int skipList<K, E>::level()const
{
	int lev = 0;
	while (std::rand() <= this->cutOff)
		lev++;
	
	return ((lev <= this->maxLevel) ? lev : this->maxLevel);
}

search函数

  • 用在查找元素时使用,并且更改类中的last指针,函数传入要查找的关键字key
  • 函数思想:
/*
	搜索关键字theKey,并把每一级链表中小于theKey值的节点中的最大的那个保
	存在last数组中,并且返回不比关键字theKey小的那个节点,然后我们进一步根据返回的
	节点判断这个节点是否是我们要查找的节点
*/
template<class K, class E>
skipNode<K, E>* skipList<K, E>::search(const K& theKey)const
{
	skipNode<K, E>* beforeNode = this->headerNode;
	for (int i = this->levels; i >= 0; --i)
	{
		while (beforeNode->next[i]->element.first < theKey)
			beforeNode = beforeNode->next[i];
		last[i] = beforeNode;
	}

	return beforeNode->next[0];
}

insert函数

  • insert函数首先调用search函数来搜索键值对所对应的键,然后初始化last数组
  • 然后再判断如果插入的数对的键值已经存在,那么就更新键的值
  • 如果没有插入那么就先调用level函数来获取一个链表级,从该级开始插入
  • 然后遍历插入
template<class K, class E>
void skipList<K, E>::insert(const std::pair<const K, E>& thePair)
{
	if (thePair.first >= this->tailKey) {
		std::ostringstream s;
		s << "The pair key is " << thePair.first << ",must <=tailkey";
		throw illegalParameter(s.str().c_str());
	}

	//如果原跳表中有这个节点,就将源节点的值更新为新的值
	skipNode<K, E>* tempNode = this->search(thePair.first);
	if (tempNode->element.first == thePair.first) {
		tempNode->element.second = thePair.second;
		return;
	}

	//否则创建一个新的节点,然后将新的节点插入到跳表中
	int newLevel = level();  //随机产生一个层数
    //如果随机产生的层数大于当前跳表的最高级层数,就当前跳表的最高级层数更新为其值
	if (newLevel > this->levels) { 
		newLevel = ++this->levels;
		this->last[newLevel] = this->headerNode;
	}
	skipNode<K, E>* newNode = new skipNode<K, E>(thePair,newLevel);

	//从newLevel级别的链表开始遍历,将新节点插入到跳表中
	for (int i = newLevel; i >= 0; --i) {
		newNode->next[i] = this->last[i]->next[i];
		this->last[i]->next[i] = newNode;
	}
	
	++this->dSize;
}

erase函数

  •  下面的代码是删除关键字为theKey的数对。while循环用来修改levels的值,除非跳表为空,否则levels不会成为0
template<class K, class E>
void skipList<K, E>::erase(const K& theKey)
{
	if (theKey >= this->tailKey) {
		std::ostringstream s;
		s << "The pair key is " << theKey << ",must <=tailkey";
		throw illegalParameter(s.str().c_str());
	}

	//如果要查找的节点不存在,就退出
	skipNode<K, E>* tempNode = this->search(theKey);
	if (tempNode->element.first != theKey) {
		return;
	}

	//如果存在,就删除
	//循环遍历链表,更改每一级的指针
	for (int i = 0; ((i <= this->levels) && (this->last[i]->next[i] == tempNode)); ++i) {
		this->last[i]->next[i] = tempNode->next[i];
	}
	/*for (int i = 0; ((i < = this->levels) && (this->last[i]->next[i] == tempNode)); ++i) {
		this->last[i]->next[i] = tempNode->next[i];
	}*/

	//从头开始遍历,如果某层的头结点指向于尾节点了,说明这一层没有元素了,就将最高层数降低一级
	while ((this->levels > 0)&&(this->headerNode->next[this->levels]==this->tailNode)) {
		--this->levels;
	}

	delete tempNode;
	tempNode = nullptr;
	--this->dSize;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值