跳表
它是一种各方面性能都比较优秀的动态数据结构,可以支持快速的插入、删除、查找操作。
对于一个存储数据有序的单链表来说,我们可以通过建立索引提高查找效率,降低查找的时间复杂度。
跳表、散列表
跳表、散列表
跳表、散列表
跳表、散列表
这种链表加多级索引的结构,就是跳表。

在跳表中查询任意数据的时间复杂度是O(logn)。这个查找的时间复杂度与二分查找是一样的,换句话说,我们基于单链表实现了二分查找。不过,这种查询效率的提升,前提是建立了很多级索引,也就是用空间换时间的设计思想。
跳表的空间复杂度是O(n)。

跳表的插入和删除操作的时间复杂度也是O(logn)。
跳表、散列表
跳表、散列表
在执行删除操作时,如果这个结点在索引中也有出现,我们除了要删除原始链表中的结点,还要删除索引中的。

散列表/哈希表
散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。
学校运动会,选手参赛号问题:
跳表、散列表
这就是典型的散列思想。
参赛选手的编号叫作键(key)或者关键字,用它来标识一个选手。
把参赛编号转化为数组下表的映射方法叫作散列函数或者哈希函数。
散列函数计算得到的值就叫做散列值或者哈希值。
规律:散列表用的就是数组支持按照下标随机访问的时候,时间复杂度是O(1)的特性。

  1. 我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。
  2. 当我们按照键值查询元素时,我们用同样的散列函数,将键值转化为数组下标,从对应的数组下标的位置取数据。

散列函数
我们把它定义成hash(key),其中key表示元素的键值,hash(key)的值表示经过散列函数计算得到的散列值。

散列函数设计的基本要求:

  1. 散列函数计算得到的散列值是一个非负整数;因为数组下标是从0开始的,所以散列函数生成的散列值也要是非负整数。
  2. 如果key1 = key2,那hash(key1) == hash(key2);相同的key经过散列函数得到的散列值也应该是相同的。
  3. 如果key1 != key2,那hash(key1) != hash(key2)。这个要求看起来合情合理,但是在真实的情况下,要想找到一个不同的key对应的散列值都不一样的散列函数,几乎是不可能的。所以,实际情况中无法避免散列冲突的发生。

散列冲突解决方法—开放寻址法和链表法

开放寻址法:当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
跳表、散列表
图中黄色色块表示空闲位置,橙色色块表示已经存储了数据。

链表法:(相比于开放寻址法,它要简单很多)如下图所示,在散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素都会放到相同的槽位对应的链表中。
跳表、散列表
当插入的时候,我们只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,所以插入的时间复杂度是O(1)。
当查找、删除一个元素时,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。这两个操作的时间复杂度与链表的长度k成正比,也就是O(k)。对于散列比较均匀的散列函数来说,k=n/m,其中n表示散列中数据的个数,m表示散列表中“槽”的个数。

如何设计散列函数
散列函数设计的好坏,决定了散列表冲突的概率大小,也决定了散列表的性能。
首先,散列表的设计不能太复杂。
其次,散列表函数生成的值要尽可能随机并且均匀分配。

散列表的装载因子=填入表中的元素 / 散列表的长度
装载因子越大,说明空闲位置越小,冲突越大,散列表的性能会下降。
**插入一个数据,最好情况下,不需要扩容,最好的时间复杂度是O(1)。最坏情况下,散列表装载因子过高,启动扩容,就需要重新申请内存空间,重新计算哈希位置,并且搬移数据,所以时间复杂度是O(n)。

总结:
当数据量比较小、装载因子小的时候,适合采用开放寻址法。
基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址,它更加灵活,支持更多的优化策略。

相关文章: