上表展示了有序数组、有序链表、跳表和哈希表的渐近性能。
需要说明的是,有序数组支持时间复杂度为O(1)的访问,所以可以使用二分查找,让查找速度达到O(logn)。
因为链表需要有序,所以在插入或删除时都要进行查找的操作,自然而然,它的时间复杂度变为了O(n)。
字典
python中的dict,STL中的map。另外还提供了multimap,支持相同的关键词,被称为多重字典。
插入insert、删除erase,会自动按照字典序排列好。
另外还有专属map的下标操作。
find、count、lower_bound、upper_bound、equal_bound这些专属于关联容器的访问操作。
哈希表/散列表
理想散列
字典的另一种表示方法是散列。
概述可以参考散列表。
理想的散列的长度是固定的,就像预先知道了有多少球,只要按序号用哈希函数映射到对应的桶中即可。所以说其查找、插入、删除操作的时间都是 Θ ( 1 ) \Theta(1) Θ(1)。
但是由于关键字的变化范围很大,所以使得散列表没有意义或不切实际。
散列函数和散列表
桶
关键字范围太大,不能用理想方法表示时,可以采用不理想的散列表和散列函数。散列表位置的数量比关键字个数少时,散列函数把若干个不同的关键字映射到散列表的同一位置。
此时,散列表的每个位置叫一个桶。桶的数量等于散列表的长度或大小。
除法散列函数
在多种散列函数中,最常见的是除法散列函数:
f
(
k
)
=
k
%
D
f(k)=k\%D
f(k)=k%D
k是关键字,D是散列表的长度。
严格来说散列函数分为散列码和压缩函数两部分,哈希码就是把一些符号转化为一个值,压缩函数实现值与桶号的映射。
冲突和溢出
当两个不同的关键字所对应的起始桶相同时,冲突发生。
如果存储桶没有空间存储一个新数对,就是溢出发生。
如果每个桶只能存储一个数对,那么碰撞和溢出会同时发生。
一个好的散列函数
冲突并不可怕,可怕的是溢出。当映射到散列表中任何一个桶里的关键字数量大致相等时,冲突和溢出的平均数最少。均匀散列函数便是这样的函数。在实际应用中表现良好的均匀散列函数又被称为良好散列函数。
C++STL中的哈希表
C11采用unordered_map代替了原来的hash_map,其底层实现正是散列表。
一些功能参考无序容器。
关于unordered_map和map的区别可以参考这个。
C++STL中的压缩函数是值模上桶数。我们需要该写的是哈希码。
线性探查
方法描述
如果桶f(k)已经被填满,那么顺序找下一个可用的桶,这一方法被称为线性探查。
在寻找下一个可用的桶时,散列表被视为环形表。
明白了怎么用线性探查法插入,就可以设计散列表的搜索方法。假设要查找关键字为k 的数对,首先搜索起始桶f(k),然后把散列表当做环表继续搜索下一个桶,直到以下情况之一发生为止:
- 存在关键字k的桶已经找到;
- 到达一个空桶;
- 回到起始桶f(k)。
后面两种情况表示桶并不存在。
删除一个桶时,不能仅仅把桶置空,这样会影响继续搜索下一个桶(删除后就是一个空桶,阻断了之后的数的查找)。合理的做法是从删除位置的下一个位置开始,逐个检查每个桶,直到到达一个空桶或者回到要删除的位置。
常见的做法是为每个桶增加一个域neverUsed,起始时被设置为true,一旦被填入数对,就变成false,再也不改变(即使数对被删除,这是避免影响后续搜索或者移动)。当很多空桶的neverUsed变为false时,需要重新组织这个散列表。比如,可以把余下的数对都插到一个空的散列表中。
性能分析
设b为散列表的桶数,D为散列函数的除数,且b=D。散列表初始化的时间为O(b)。
当表中有n个记录时,最坏情况下插入和查找的时间均为 Θ ( n ) \Theta(n) Θ(n),这是出现在所有关键字都对应同一个起始桶。
令
U
n
U_n
Un和
S
n
S_n
Sn分别表示在一次成功搜索和不成功搜索中平均搜索的桶数,对于线性探查有以下近似公式:
U
n
=
1
2
(
1
+
1
(
1
−
α
)
2
)
U_n=\frac{1}{2}(1+\frac{1}{(1-\alpha)^2})
Un=21(1+(1−α)21)
S
n
=
1
2
(
1
+
1
1
−
α
)
S_n=\frac{1}{2}(1+\frac{1}{1-\alpha})
Sn=21(1+1−α1)
其中
α
=
n
b
\alpha=\frac{n}{b}
α=bn为负载因子。
随着负载因子的增加,所需查找的桶的数量也随之增大,一般要求 α ≤ 0.75 \alpha\le0.75 α≤0.75。
rehash(n)
,reverse(n)
,c.max_load_factor
都是维护负载因子的手段。
线性探测法属于开放寻址法中的一种,除此之外还有二次探测这种方法。
链式散列
该结构是解决溢出/冲突的另一种方法,又称为分离链表法。即给散列表的每一个位置配置一个线性表。
这也是C++STL中unordered_map的实现解决冲突所采用的方法,这是一种以空间换取时间的手段。
性能分析
U
n
=
α
(
α
+
3
)
2
(
α
+
1
)
U_n=\frac{\alpha(\alpha+3)}{2(\alpha+1)}
Un=2(α+1)α(α+3)
S
n
=
1
+
α
2
S_n=1+\frac{\alpha}{2}
Sn=1+2α