跳表&散列表

跳表

基本不要求具体实现,如果有需要的话可以看 https://github.com/wangzheng0822/algo
参考书:《数据结构与算法之美》

折半查找的那一章里讲过折半查找只支持数组结构,并且计算出折半查找的时间复杂度为O(n),但是链表就真的没办法使用折半查找了么?
其实只需要对链表稍加改造,就可以支持类似“二分”的查找算法。我们把改造之后的数据结构——链表加多级索引的结构,叫做跳表(Skip list)。

 

对链表建立一级“索引”,查找起来就会快一些,每两个结点提取一个结点到上一级,我们把抽出来的那一级叫做索引或索引层。如下图,图中的 down 表示 down 指针,指向下一级结点。
这样我们查找某一个值,只需要从上级索引开始遍历,比如查找16,原本需要遍历10个节点,有了索引后只需要遍历6个节点。



 

使用索引可以节省大量的链表查找时间
使用索引可以节省大量的链表查找时间

时间复杂度

每两个节点抽出一个作为索引节点,一直到最顶层只有两个节点为止。假设有n个数据,第k层就是n/2k个节点,最顶层n/2k=2 ,得出k=log2n - 1 。 k可以视作层、整个跳表的索引层高,包括原始链表则从0开始计算,可知整个跳表的索引层高为log2n 。
计算时间复杂度,需要得知每一层里遍历了多少节点,每一个节点可以视为执行时间+1。
在跳表中查询某个数据的时候,如果每一层都要遍历 m 个结点,那在跳表中查询一个数据的时间复杂度就是 O(m*logn)。那这个m可以求出来么?
答案是可以的。
以两个节点抽一个做索引的跳表为例,它在每一层的遍历节点不超过3个。假设我们要查找的数据是 x,在第 k 级索引中,我们遍历到 y 结点之后,发现 x 大于 y,小于后面的结点 z,所以我们通过 y 的 down 指针,从第 k 级索引下降到第 k-1 级索引。在第 k-1 级索引中,y 和 z 之间只有 3 个结点(包含 y 和 z),所以,我们在 K-1 级索引中最多只需要遍历 3 个结点,依次类推,每一级索引都最多只需要遍历 3 个结点。

由此得到 m=3,忽略常量级,所以在跳表中查询任意数据的时间复杂度就是 O(logn)。

空间复杂度

跳表是使用空间换时间的数据结构,它使用了额外的空间存储索引结构。
以两个节点抽一个做索引的跳表为例,它占用的额外空间为n/2+n/4+…+2=n-2,空间复杂度为O(n)。
为了节省空间,如果我们间隔三个节点呢?占用的额外空间为n/3+n/9+n/27+…+3 = n/2 -1,可以比之前节省近一半的空间。
在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。

动态更新跳表索引

随着插入删除节点,如果不及时更新索引,可能会出现某两个索引间节点特别长的情况,极端情况下退化为单链表查询、时间复杂度退化为O(n),所以我们需要及时更新内部索引保证两个索引间的节点数据数量基本均衡。
如果你了解红黑树、AVL 树这样平衡二叉树,你就知道它们是通过左右旋的方式保持左右子树的大小平衡(后续笔记),而跳表是通过随机函数来维护前面提到的“平衡性”。
当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。可以通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到最底级到第 K 级的索引中。

散列表(哈希表)

参考书:《数据结构与算法之美》

散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。

 

散列表中有三个概念:键值(key)、散列值(哈希值)、散列函数 hash(key)。
通过散列函数,将数据对象的关键值编号对应到某一个散列值上,这个散列值就是数组对应的下标,这样需要某个数据的时候可以通过下标还原出对应的编号,从而找到原数据。\

  1. 散列函数计算得到的散列值是一个非负整数;
  2. 如果 key1 = key2,那 hash(key1) == hash(key2);
  3. 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
    第三点其实在实际运用中非常难做到,再好的散列函数也无法避免散列冲突的问题,常用的散列冲突解决方法有两类,开放寻址法(open addressing)和链表法(chaining)。

     

散列冲突

1、开放寻址法。(使用数组实现)

开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。
探测新的位置的方式:

  1. 线性探测
    通过哈希函数计算出对应哈希值,若key值无法对应,则顺着下标地址线性探测,直到找到匹配值或者找到第一个为空的值(表示连续存储的值都不是要找的)。
    但是删除数据的时候需要特别注意,因为删除数据后不能简单地把数据置空,否则我们线性探测的时候探测到一个被删除的空值会以为是【结束】标志,都已经探测到空了还没找到哈希值那铁是没有了,于是直接停止返回-1。
    所以有一个方式是 将删除的元素,特殊标记为 deleted。当线性探测查找的时候,遇到标记为 deleted 的空间,并不停下来,而是继续往下探测。
    当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,我们可能需要探测整个散列表,所以最坏情况下的时间复杂度为 O(n)。同理,在删除和查找时,也有可能会线性探测整张散列表,才能找到要查找或者删除的数据。
  2. 二次探测
  3. 双重散列
2、链表法

构造一个链表,链表除了next指针外再加上sbling指针,如果遇到冲突则在冲突的兄弟指针后继续排列即可。\

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值