数据结构-----跳表

链表

同数组比较,链表的插入删除操作复杂度是O(1),然而如果想要查找一个节点,你可能会这么写:

LinkedNode<T> *targetNode = headerNode;
while(targetNode)
{
	if(targetNode->element == theElement)
		return targetNode;
	targetNode = targetNode->next;
}
return NULL;

你会发现每次都是从链表头节点开始,时间复杂度是n(节点个数)。为了提高算法速度,就有了跳表的概念。

二分法

介绍跳表之前,先简单解释一下二分法。
对于一个有序序列来说,通常在查找某一个元素时,可以不需要从头到尾一个个比较,而是使用二分的方式,步骤如下:
1.先将中间的元素和待查找元素比较。
2.1:如果相等, 返回
2.2:如果中间元素小于待查找元素,在右侧部分中查找(返回步骤1)
2.3:如果中间元素大于待查找元素,在左侧部分中查找(返回步骤1)

跳表

结合链表和二分法的特点,将链表进行加工,创造一个二者的结合体:
1.链表从头节点到尾节点是有序的
2.可以进行跳跃查找(形如二分法)

next数组
实现第一步,只需在插入的时候进行有序插入即可,核心步骤在第二步,构建一个可以实现在节点之间跳跃的链。
对于普通链表而言,你的节点也许会长这个样子(或者再多个构造析构函数?):

template<class T>
class linkedNode
{
public:
	T element;
	linkedNode<T> *next;
};

这里的next指针是我们用于将此节点同后面一个节点连接的桥梁,正如你知道的那样,next是下一个节点的指针。
需要你考虑的事情来了,如果需要在不挨着的节点之间实现跳转,那么对于某一个节点来说,它就需要存储一个以上的指向别的节点的next指针。多个同种类型的数据通常可以采用数组存储(vector, list等STL容器也可以啦),这样,我们的跳表节点或许大概差不多应该长这个样子:

template<class T>
class skipNode
{
public:
	T element;
	skipNode<T> **next; //一个存储着skipNode<T>*类型变量的一维数组
};

暂且不论next数组的大小是多少,先让我告诉你跳表的样子。
这里写图片描述
你可能会觉得跳表这个东西很奇怪,不过最好不是因为我画的比较丑。先不管竖着的线是什么,横着看每一层好像都是一个链表,对吗。另外我觉得你也应该注意一下竖着看的时候每一个节点的名字,它们也都是一样的(不用考虑为什么有的节点有一个,有的有两个甚至更多,这是插入函数应该解决的问题)。

所以我想你现在脑子中可能有了对next数组的一种假设,尽管事实可能并不是你想的那样,不过不要紧,让我借着这张图告诉你next数组是什么。

通常我们会定义跳表的级数,简单来解释,就是层数-1(最下面一级是0级)。所以这张图表示的跳表的级数是(0, 1, 2)。而在第2级的节点(比如node5),它的next数组大小就是(2 + 1) == 3, 在第1级的节点(比如node4),它的next数组的大小就是(1 + 1) == 2, 在第0级的节点(比如node3),它的next数组大小是1;

所以在申请一个skipNode的指针时,传入这个节点所在的级数好像是理所应当的事情,然后对next进行初始化,把skipNode加工一下,像这样:

template<class T>
class skipNode<T>
{
public:
	skipNode(const T& theElement, size_t size):
		element(theElement)
	{
		next = new skipNode<T>*[size+1];
	}

	T element;
	skipNode<T> **next;
};

现在开始解释next数组,你最好看着上面的图:
对于第2级的node5来说

node5->next[2] == tailNode;
node5->next[1] == node7;
node5->next[0] == node6;

对于第1级的node2来说

node2->next[1] == node4;
node2->next[0] == node3;

如果你还是不理解,我想你可以把每一列的同名节点看成是不同的节点,它们存的element数据是一样的,而且每个节点只有一个指向下一个节点的指针。再把每一层看成一个链表。然后再看一遍。最后把每一列合成一个节点,它们只是有一个指针数组而已,你应该这么想。

find
我觉得这个函数可以帮助你更好地理解next数组的作用。如果没有,唔,怪我表达能力太弱。。。
这里写图片描述
继续看着这张图,让我先解释一下headerNode和tailNode是什么(如果你因为它们的存在感到一丝困惑的话),它们是设计者(就是我们啦)人为添加的两端节点,很像存在头节点和尾节点的链表(又或者像队列?),它们不存储需要保存的有用的数据,但是需要人为给定一个数,让它比整个跳表中的element都大,仅仅是用来判断是否是头和尾。当跳表为空时,级数为0,headerNode->next[0] == tailNode;

现在回到上面那个图片上,如果我想要查找node6,想想二分法(但不是从中间开始)。
假设targetNode记录着此时的节点,初值赋为headerNode;

第一次你应该比较的是headerNode->next[2]->element和theElement(待查找数据),也就是node5->element和theElement。显然node5小于theElement(别忘了跳表的数据也是有序的),**这一步的结果导致下一次应该从第2级的node5开始查询。**也就是令targetNode = targetNode->next[2];

第二次你应该比较targetNode->next[2]->element和theElement,也就是tailNode->element和theElement。看看前面,tailNode->element是最大的,对吗。所以结果是大于,**这一步的结果导致下一次应该从第1级的node5开始查询。**这里从第2级跳到第1级。但是没有改变targetNode。

第三次你应该比较targetNode->next[1]->element和theElement,也就是node7->element和theElement。显然也是大于,**这一步的结果导致下一次应该从第0级的node5开始查询。**这里从第1级跳到第0级。也没有改变targetNode。

第四次你应该比较targetNode->next[0]->element和theElement,也就是node6->element和theElement。这时你应该庆幸终于相等了,此时结束。如果小于,改变targetNode = targetNode->next[0](像第一步那样),如果大于,结束,不然还往哪一级降,没了,对吗。

没懂?接着看:
比较的时候的三种情况,以targetNode->next[i]->element和theElement为例:
1.小于:令targetNode = targetNode->next[i]; //第i级链表的下一个
2.大于:向下降级,i- - //不改变targetNode
3.等于:向下降级,i- - //不改变targetNode

退出后,再次比较targetNode->next[0]和theElement,判断是否找到。
所以整个运算下来,targetNode是要查找的节点前面那个节点。如果你不明白,想想找不到那个节点时的情况,比如说,额,查找node6.6。
让我们看看代码:

template<class K, class E>
pair<const K, E> *skipNode<K, E>::find(const K& theKey)
{
	skipNode<K, E>* beforeNode = headerNode;
	for(int i = curLevels; i >= 0; --i) //降级
	{
		while(beforeNode->next[i]->element.first < theKey)
			beforeNode = beforeNode->next[i]; //跳转到同一级的下一个
	}
	//跳出存在两种可能
	//1.找到了,此时 if(beforeNode->next[0]->element.first == theKey)
	//2.没找到,此时 if(beforeNode->next[0]->element.first > theKey)
	if(beforeNode->next[0]->element.first == theElement.first)
		return &beforeNode->next[0]->element;
	return NULL;
}

insert
像红黑树,AVL树一样,跳表也是一种平衡结构,只不过这种平衡结构是利用随机函数产生的级数维持的(终于填了前面的坑,还记得吗,为什么有的节点有2级,有的只有0级)。

接着上面那个图,想想插入node6.6的过程中会发生什么(假设随机函数给它分配的是第2级)。。。静思一秒钟。
啊!插入之后会破坏每一级的结构,会破坏哪些节点的结构呢。。。静思一秒钟。
啊!自然是第2级的node5,第1级的node5,第0级的node6这些节点的next数组。

分析过后,第一步便是找到这几个节点。我觉得你应该停一下想想怎么找,实在想不出来,参考find函数。
find函数中,for循环内套while循环,想想每次while循环结束之后的beforeNode是什么。哇哦,它就是我们要找的节点。我们用一个last数组记录这些节点,last[i]表示第i级要找的节点。

第二步:利用随机函数分配级数level
找节点是为了改变这些节点的next数组,分配级数是为了只改变找到的那些节点中位于第0级到第level级的节点。啊!你又想了,万一随机出的level比目前最大级数还大怎么办。这时就将跳表增加一级呗,像这样:

int level = levels();
if(level > curLevels)
{
	level = ++curLevels;
	last->next[level] = heaerNode; 
	//增加一级的同时也要改变last数组
	//用于将新节点插入时newNode->next[level] = last[level]->next[level]
}

万事俱备后,开始更新涉及到的节点的next数组,看代码之前,先想想怎么在链表中插入节点。

skipNode<K,E> *newNode = new skipNode<K, E>(thePair, level+1);

//因为只影响了[0, level]级的节点,更高级的节点不受影响
for(int i = 0; i <= level; ++i)
{
	//每一级都插入新节点
	newNode->next[i] = last[i]->next[i];
	last[i]->next[i] = newNode;
}

总结一下插入函数,需要两个额外的函数:
1.随机函数,用于生成新节点的级数
2.初始化last数组,保存每一级中处在待插入位置前面的节点

//随机函数,为新节点生成级数
template<class K, class E>
int skipList<K, E>::levels()
{
	int level = 0;
	//cutOff预先设定的值,cutOff = prob * MAX_RAND;
	//prob:是第i级同时又是第i-1级的概率,人为设定
	while(rand() > cutOff) 
		level++;
	return level;
}


template<class K, class E>
skipNode<K, E>* skipList<K, E>::search(const K& theKey)
{
	//与find函数相同
	skipNode<K, E>* beforeNode = headerNode;
	for(int i = curLevels; i >= 0; --i)
	{
		while(beforeNode->next[i]->element.first < theKey)
			beforeNode = beforeNode->next[i];
		last[i] = beforeNode; //找到每一级待插入位置前面的节点
	}
	
	//返回找到的节点,用于判断是否已存在键为theKey的节点
	return beforeNode->next[0];
}


template<class K, class E>
void skipList<K, E>::insert(const pair<const K, E>& thePair)
{
	if(thePair.first >= tailNode->element.first)
		return;

	skipNode<K, E> *theNode = search(thePair.first);
	if(theNode->element.first == thePair.first) //判断是否存在该节点
	{
		theNode->element.second = thePair.second; //更新值
		return;
	}

	int level = levels();
	//cutLevels:记录当前最大级数
	if(level > curLevels)
	{
		level = ++curLevels; //级数加一,让新节点作为最高级节点
		last[level] = headerNode;
	}
	
	//每一级如同链表插入一样进行插入
	skipNode<K, E> *newNode = new skipNode<K, E>(thePair, level+1);
	for(int i = 0; i <= level; ++i)
	{
		newNode->next[i] = last[i]->next[i];
		last[i]->next[i] = newNode;
	}
	dSize++;
}

erase
如果你理解了插入函数,删除函数会带给你从未有过的轻松感,至少比起还在文章开头徘徊的你是这样。噢对,还有一件事,或许你应该在回忆链表删除的状态里待几秒钟。如果没有头绪,插入函数最后一个for循环说不定会帮到你。

要删除一个节点,自然是先找到这个节点啦,同时还要初始化last数组啦。然后从第0级开始到最高级,如果last[i]->next[i]是要删除的节点,就删除掉。直接上代码:

template<class K, class E>
void skipList<K, E>::erase(const K& theKey)
{
	if(theKey >= tailNode->first)
		return;

	skipNode<K, E> *theNode = search(theKey);
	if(theNode->element.first != theKey)
		return; //不存在要删除的节点

	int i = 0;
	while(i <= curLevels && last[i]->next[i] == theNode)
	{
		//每一级的链表删除
		last[i]->next[i] = theNode->next[i];
		i++;
	}
	
	//如果删除的是最高级,且未删除前,最高级就一个节点,则当前节点减一
	while(curLevles > 0 && headerNode->next[curLevels] == tailNode)
		curLevels--;

	delete theNode;
	dSize--;
}

现在唯一可能会让你感到头疼的就是构造函数了。想想构造函数应该做些什么,对数据成员进行初始化?啊,对,那是所有构造函数应该做的事情。我说的是具体应该怎么做。
我猜你看上述代码的时候会对像cutOff, curLevels这些凭空出现的变量有点不理解,没关系,正如你想的那样,把它们放到private数据成员里。总结一下让你感到困惑的变量:


int dSize; //记录着跳表中节点的个数,初始为0
int curLevels; //记录跳表当前最大级数,如果是图片那样的话,它的值是2
int cutOff; //用于随机函数的随机算法,一个很大的值
int maxLevel; //跳表可以存储的最大级数
skipNode<K, E> *heaerNode; //跳表头节点,键是跳表中最大的
skipNode<K, E> *tailNode; //尾节点,键同headerNode
skipNode<K, E> **last; //记录每一级中要查找的节点前面那个节点

构造函数首先初始化这些变量,然后给节点申请内存,将头节点和尾节点连接起来(因为此时跳表是空的)。就这么简单。

//prob: 是第i级同时又是第i-1级的概率
//maxPairs: 跳表可以存储的最多节点个数
//largetKey: 最大键
template<class K, class E>
skipList<K, E>::skipList(K largerKey, int maxPairs, double prob):
	cutOff(prob * RAND_MAX),
	maxLevel((int)ceil(logf((float)maxPairs) / logf(1 / prob)) - 1),
	cutLevels(0),
	dSize(0)
{
	pair<K, E> tailPair;
	tailPair.first = largerKey;
	headerNode = new skipNode<K, E>(tailPair, maxLevel+1);
	tailNode = new skipNode<K, E>(tailPair, 0);
	last = new skipNode<K, E>*[maxLevel+1];

	for(int i = 0; i <= maxLevel; ++i)
	{
		headerNode->next[i] = tailNode; 
	}
}

析构函数就没什么了,对第0级的每一个节点,释放它的next数组(可以写在skipNode的析构函数中),然后释放它自己。

完整代码下载

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值