数据结构之——跳表实现增删查

     我们知道链表在插入删除很快,但是查找很慢,单链表查询某个数据的时间复杂度为O(n)。而今天讲的跳表具有快速插入、删除和查找的操作。

首先,对于一个有序的单链表,如果我们想要查找其中的某个数据,一般是从头到尾遍历链表。这种查找的效率很低。

为了提高查找的效率,我们在原始链表的基础上建立一级“索引”,也就是说在每两个结点取一个结点到上一级结点,抽出来的那一级我们叫它“索引”或者“索引层”

如上图,加入我们要查找16,我们可以现在索引层遍历,当遍历到13的结点时,发现下一个结点是17,那么查找的数据16就在结点13和17之间。然后我们通过索引层结点的down指针,降到原始链表,继续遍历。此时我们只需再遍历2个结点,即可找到16。原先查找16需要遍历10个结点,加了索引层之后只要遍历7个结点。这种链表加多级索引层的结构,就是跳表。

查找效率:

我们知道,单个链表中查询某个数据的时间复杂度为O(n),那么用跳表来查询的时间复杂度是多少呢?

从上面的图我们可以发现规律,每两个结点抽出来一个结点作为上一级索引的结点,那么第一级索引的结点数为原始链表结点数的n/2,第二级索引的结点数大约为n/4,第三级索引为n/8,以此类推,第k级索引的结点数为n/2^{k}。 假设索引层有h级,最高级的索引有2个结点。通过以上公式,我们可以得到,n/2^{h}=2,从而求出h=log n-1  (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;
}

 

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值