散列表描述

散列表描述_第1张图片
上表展示了有序数组、有序链表、跳表和哈希表的渐近性能。

需要说明的是,有序数组支持时间复杂度为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),然后把散列表当做环表继续搜索下一个桶,直到以下情况之一发生为止:

  1. 存在关键字k的桶已经找到;
  2. 到达一个空桶;
  3. 回到起始桶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α

你可能感兴趣的:(数据结构,算法与应用(C++),算法,leetcode,c++)