跳表的背景(skipList)
在一般的有序链表中,如果要在n个数对中查询一个数对,至少要进行n次比较,如果我们在链表的中间结点加一个指针,则比较的次数会变成n/2+1;此时只需要先于中间的数对进行比较,如果大于此数对的的关键字,则只需要在链表的右半部分查找,反之,在链表的左半部分查找即可。
跳表的结构描述(stucture)
对n个数对来说,0级链表包含所有数对,1级链表每2个数对取一个,2级链表每4个数对取一个,i级链表每2i个数对取一个(相对于0级链表);
注:规则的跳表结构中,i-1级链表的数对数与i级链表的数对数之比为p;下图p=0.5
跳表的主要参数
- headNode(头节点指针)
- tailNode(尾结点指针)
- p (i-1级链表的数对数与i级链表的数对数之比为p 或 属于i-1级链表的数对同时属于i级链表的概率为p)
- level(当前最大的非空链表级数)
- maxLevel(最大层数)
maxLevel=log1/pN -1
- tailKey(最大关键字)
跳表的主要操作
级的分配
假设使用一个随机数生成器产生0到1之间的实数,产生的随机数<=p的概率为p,则这个数属于1级链表,下一个随机数如果也是<=p,那么则属于2级链表
if(rand()/RAND_MAX<=p)
level++;
查找操作
查找操作是从最高级进行查找的,也就是level级进行查询,直到0级链表;
在每一级链表中,从左边尽可能逼近要查找的数对;
此时每一级没有必要去判断是否与theKey相等,原因是每一级链表的比较下来的平均时间是大于0级链表只进行一次的比较时间,且大多数的数对是在第0级的;
template<class K, class E>
pair<const K, E>* skipList<K, E>::find(const K& theKey) const
{// 返回匹配的数对的指针
// 如果没有找到,返回NULL
if (theKey >= tailKey)
return NULL; // 没有匹配的数对
// beforNode 是距离theKey最近的结点
skipNode<K, E>* beforeNode = headerNode;
for (int i = levels; i >= 0; i--) // 级数递减
// 在每一级链表,从左边尽可能的逼近theKey
while (beforeNode->next[i]->element.first < theKey)
beforeNode = beforeNode->next[i];
// 查看beforeNode的下一个节点是不是代查询的数对
if (beforeNode->next[0]->element.first == theKey)
return &beforeNode->next[0]->element;
return NULL; // 没有匹配
}
插入操作
在进行新数对的插入之前,首先是要进行新数对级的分配(theLevel);
在进入插入的时候,需要对 theLevel下每一级(包括theLevel) 的链表进行调整,所以需要知道每一级链表应该调整的位置;
template<class K, class E>
skipNode<K, E>* skipList<K, E>::search(const K& theKey) const
{// 查找每一级应该修改的位置
// 返回关键字可能是theKey的结点
// 同上查找操作,beforNode是最靠近theKey的结点
skipNode<K, E>* beforeNode = headerNode;
for (int i = levels; i >= 0; i--)
{
while (beforeNode->next[i]->element.first < theKey)
beforeNode = beforeNode->next[i];
last[i] = beforeNode; //将每一个需要修改的位置记录
}
return beforeNode->next[0];
}
具体过程:在插入的时候,查找是否存在theKey关键字的结点,如果存在,覆盖即可;如果不存在,则需要进行级的分配,然后进行theLevel下每一级(包括theLevel) 的插入.
注:如果分配的级数大于最大级数,则应该进行更新记录修改位置的数组, 即search代码中的last数组;
template<class K, class E>
void skipList<K, E>::insert(const pair<const K, E>& thePair)
{// 插入数对,覆盖已经存在的
if (thePair.first >= tailKey) // 插入的数对关键字过大
{
ostringstream s;
s << "Key = " << thePair.first << " Must be < " << tailKey;
throw illegalParameterValue(s.str());
}
// 看字典中是否存在
skipNode<K, E>* theNode = search(thePair.first);
if (theNode->element.first == thePair.first)
{//更新存在的数据
theNode->element.second = thePair.second;
return;
}
// 如果不存在,进行级数的分配
int theLevel = level();
// 确保theLevel<=levels+1
if (theLevel > levels)
{
theLevel = ++levels;
last[theLevel] = headerNode;
}
//插入数据,在thelevel下的(包括thelevel)每一级进行插入
skipNode<K, E>* newNode = new skipNode<K, E>(thePair, theLevel + 1);
for (int i = 0; i <= theLevel; i++)
{// 在每一级进行插入
newNode->next[i] = last[i]->next[i];
last[i]->next[i] = newNode;
}
dSize++;
return;
}
删除操作
在删除操作中,需要去search这个需要删除的数对,找到每一级左边与它最邻近的结点,然后进行每一级的删除.
删除完毕之后要进行级的更新(如果删除的结点恰好是最高级存在结点的链表)
template<class K, class E>
void skipList<K, E>::erase(const K& theKey)
{// 删除字典中theKey对应的数对
if (theKey >= tailKey) // too large
return;
// 看最左边的结点的下一个节点是不是需要删除的结点
//此时不直接用find()函数的原因是要进行每一级的调整
skipNode<K, E>* theNode = search(theKey);
if (theNode->element.first != theKey) // 不存在
return;
// 从跳表中删除
for (int i = 0; i <= levels &&
last[i]->next[i] == theNode; i++)
last[i]->next[i] = theNode->next[i];
// 更新级数
while (levels > 0 && headerNode->next[levels] == tailNode)
levels--;
delete theNode;
dSize--;
}
复杂度分析
- 时间复杂性: find/insert/erase最坏的O(n+maxLevel) ,平均复杂度为O(logn)
- 空间复杂性: 最坏的情况是,每一个数对都是maxLevel级,所以除过n个数对的空间,还要提供O(nMaxLevel)个指针空间 ;在一般情况下,第i级链有npi个数对,所以总共是 n ∑ ( p i ) = n ( 1 − p ) n\sum(p^i) = n(1-p) n∑(pi)=n(1−p)个指针,在本例中,p=0.5,则只需要2n个指针空间.