我们知道链表在插入删除很快,但是查找很慢,单链表查询某个数据的时间复杂度为O(n)。而今天讲的跳表具有快速插入、删除和查找的操作。
首先,对于一个有序的单链表,如果我们想要查找其中的某个数据,一般是从头到尾遍历链表。这种查找的效率很低。
为了提高查找的效率,我们在原始链表的基础上建立一级“索引”,也就是说在每两个结点取一个结点到上一级结点,抽出来的那一级我们叫它“索引”或者“索引层”
如上图,加入我们要查找16,我们可以现在索引层遍历,当遍历到13的结点时,发现下一个结点是17,那么查找的数据16就在结点13和17之间。然后我们通过索引层结点的down指针,降到原始链表,继续遍历。此时我们只需再遍历2个结点,即可找到16。原先查找16需要遍历10个结点,加了索引层之后只要遍历7个结点。这种链表加多级索引层的结构,就是跳表。
查找效率:
我们知道,单个链表中查询某个数据的时间复杂度为O(n),那么用跳表来查询的时间复杂度是多少呢?
从上面的图我们可以发现规律,每两个结点抽出来一个结点作为上一级索引的结点,那么第一级索引的结点数为原始链表结点数的n/2,第二级索引的结点数大约为n/4,第三级索引为n/8,以此类推,第k级索引的结点数为。 假设索引层有h级,最高级的索引有2个结点。通过以上公式,我们可以得到,
,从而求出
(2为底),如果包括原始链表,则整个跳表的高度为logn。我们在跳表中查询某个数据是,如果每一层都要遍历m个结点,那么在跳表中查询一个数据的时间复杂度为O(m*logn)。那m的值是多少呢?按照前面这种索引结构,我们每一级索引都最多只遍历3个结点,也就是m=3.为什么是3?解释如下:假设我们要查找的数据是X,在第k级索引中,我们遍历到y结点之后,发现X大于y,小于Z,所以我们通过y的down指针下降到k-1级索引,在第k-1级索引中,y和Z之间只有3个结点(包括y和z),所以,我们在看k-1级索引中最多遍历3个结点,以此类推,每一级索引都最多只需遍历3个结点。
所以在跳表中查询任意数据的时间复杂度是O(logn)。这个查找的时间复杂度跟二分查找是一样的。也就是说,我们其实是基于单链表实现了二分查找。
跳表是不是很浪费内存?我们知道,跳表需要存储多级索引,肯定要消耗更多的存储空间的。跳表的空间复杂度为O(n)。
在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需存储相关值和几个指针,并不存储对象,所以当对象比索引大很多时,那索引占用的空间就可以忽略。
插入和删除
我们知道,对于单链表,一旦定位好要插入的位置,插入结点的时间复杂度时很低的,O(1)。但是,为了保证原始链表中数据的有序性,我们需要找到插入的位置,这种查找也是很耗时的。
对于存粹的单链表,需要遍历每个结点,来找到插入的位置,但是对于跳表来说,我们讲过查找的时间复杂度是O(logn),所以这里找找某个数据应该插入的位置,方法是类似的,时间复杂度也是O(logn)。
删除操作,如果这个结点在索引中也有出现,我们除了要删除原始链表中的结点,还要删除索引中的。因为单链表中的删除操作需要拿到删除结点的前驱接待你,然后通过指针操作完成删除。所以在查找要删除的结点的时候,一定要获取前驱结点。当然,如果是用双链表,就不需要考虑这个问题。
跳表索引动态更新
当我们不停的往链表中插入数据时,索引也不断更新,可能出现2个索引结点之间数据非常多的情况,极端情况下,跳表会退化成单链表。
作为动态的数据结构,我们需要通过某种手段来维护索引与原始链表大小之间的平衡,也就是说当链表的结点多了,索引结点就相应的增加,避免复杂度退化。跳表采取的方式是通过随机函数来维护前面的”平衡性“。
总结:
跳表这种数据结构使用空间换时间的设计思路,通过构建多级索引来提高查询效率,实现了基于链表的“二分查找”。跳表是一种动态数据结构,支持快速的插入删除查找操作,时间复杂度都是O(logn).
这里有一篇讲跳表的好文章,能够帮助你理解https://juejin.im/post/57fa935b0e3dd90057c50fbc
以下是跳表增加删除查找的代码实现:
struct Node
{
Node *right, *down;
int val;
Node(Node *right, Node *down, int val) :right(right), down(down), val(val){}
};
class Skiplist
{
private:
Node *head;
public:
Skiplist()
{
head = new Node(NULL, NULL, -1);
};
~Skiplist()
{
if (head)
{
delete head;
head = nullptr;
}
}
bool search(int target)
{
Node *p = head;
while (p)
{
while (p->right && p->right->val < target)//思路:寻找目标区间
{
p = p->right;
}
if (!p->right || target < p->right->val)//没找到目标值,继续往下走
{
p = p->down;
}
else
{
return true;
}
}
return false;
}
void add(int val)
{
vector <Node*> pathList;
Node *p = head;
while (p)
{
while (p->right && p->right->val < val)
{
p = p->right;
}
pathList.push_back(p);
p = p->down;
}
bool insertUp = true;
Node *downNode = NULL;
while (insertUp && pathList.size()>0)//从下至上搜索路径回溯,50%概率
{
Node *insert = pathList.back();
pathList.pop_back();
insert->right = new Node(insert->right, downNode, val);
downNode = insert->right;//把新节点赋值为downNode
insertUp = (rand() & 1) == 0;//50%概率
}
if (insertUp)//插入新的头节点,加层
{
head = new Node(new Node(NULL, downNode, val), head, -1);
}
}
bool erase(int val)
{
Node *p = head;
bool seek = false;
while (p)
{
while (p->right && p->right->val < val)
{
p = p->right;
}
if (!p->right || p->right->val > val)
{
p = p->down;
}
else//搜索到目标结点,进行删除操作,结果记录为true,继续往下层搜索,删除一组目标节点
{
seek = true;
p->right = p->right->right;
p = p->down;
}
}
return seek;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Skiplist *skilist =new Skiplist();
skilist->add(1);
skilist->add(2);
skilist->add(3);
skilist->search(0);
skilist->search(2);
skilist->erase(2);
return 0;
}