文章目录
1.散列查找
涉及到字符串的比较,如果是int类型,可以用查找树、平衡树,效率较高,但字符串比较效率低。考虑将字符串类型进行变换。
查找的方法
- 顺序查找,O(N)
- 二分查找, O(log N)
- 二叉搜索树,O(h),h为二叉查找树的高度
- 平衡二叉树,O(log N),要求一个字符一个字符的查找
查找问题考虑的方法: 1.有序对象,半序(树)2.直接算出位置:散列
1.1 散列查找
1.1.1散列查找
两个问题:
- 计算位置:构造散列函数,确定关键词存储位置
- 解决冲突:应用某种策略解决多个关键词位置相同的问题
时间复杂度:O(1),查找问题与问题规模无关。
装填因子(Loading Factor):设散列表空间大小为m,填入表中元素个数是n,则称下式值为散列表的装填因子
α
=
n
/
m
\alpha = n/m
α=n/m
1)例1:
- 假设一个数组长11, 数组中的每个数对17求余,得到的值放在一个表中,这个表的key由0-16(有可能有多个值对应表中的同一个位置,需要某种解决此冲突的策略)。
- 当拿到一个数n时,想要查找表中是否存在此数,就可以用n%17,查看表中对应位置是否存在数据。
2)例2:
- 对于一个字符串数组,将字符串放入一个二维数组中,维度为[26][2]。将字符串的第一个字符 与’a’的差值做为第一维的index。
- 假设第一个字符串首位字符为’a’,那么将字符串放入[0][0]的位置;第二个首位为’a’的字符串放[0][1]的位置;当出现第三个首位字符为’a’的字符串时,发生冲突,会溢出。
- 如果没有溢出,散列表的复杂度为O(1)
1.1.2散列查找的基本思想
- 以关键字key为自变量,通过一个确定的函数 h h h(散列函数),计算 k e y key key对应的函数值 h ( k e y ) h(key) h(key),作为数据对象的存储地址
- 可能不同的关键字会映射到同一个散列地址上,即 h ( k e y 1 ) = h ( k e y 2 ) h(key_1)=h(key_2) h(key1)=h(key2),称为“冲突”,需要某种冲突解决策略
1.2散列函数的构造方法
散列函数的要求
- 方便计算
- 把不同对象均匀的映射
1.2.1 对于数字关键词
- 直接定址
h ( k e y ) = a × k e y + b h(key) = a \times key + b h(key)=a×key+b(例如年份) - 除留余数
h ( k e y ) = k e y % p h(key) = key \% p h(key)=key%p,p一般取表的大小,一般取素数 - 数字分析法(分析数组的规律,取比较随机的位做散列地址)
h ( k e y ) = a t o i ( k e y + 7 ) h(key) = atoi(key + 7) h(key)=atoi(key+7),即数组的指针为key,取数组的第7位
例如身份证号(130 626 1990 1030 2227):
h ( k e y ) = ( k e y [ 6 ] − ′ 0 ′ ) × 1 0 4 + ( k e y [ 10 ] − ′ 0 ′ ) × 1 0 3 + ( k e y [ 14 ] − ′ 0 ′ ) × 1 0 2 + ( k e y [ 16 ] − ′ 0 ′ ) × 10 + ( k e y [ 17 ] − ′ 0 ′ ) h(key) = (key[6]-'0')\times 10^4 + (key[10]-'0')\times 10^3 + (key[14]-'0')\times 10^2+ (key[16]-'0')\times 10 + (key[17]-'0') h(key)=(key[6]−′0′)×104+(key[10]−′0′)×103+(key[14]−′0′)×102+(key[16]−′0′)×10+(key[17]−′0′) - 折叠法(把关键词分割成位数相同的几个部分,然后叠加)
- 平方取中法(把数值取平方,取平方值中间的三位数)
1.2.2 对于字符关键字
- ASCII码加和法
h ( k e y ) = ( ∑ k e y ( i ) ) % T a b l e s i z e h(key) = (\sum key(i)) \% Tablesize h(key)=(∑key(i))%Tablesize把每位字符的ASCii码相加后取余,但会有严重冲突,且结果很容易聚集 - 前3个字符移位
h ( k e y ) = ( k e y [ 0 ] × 2 7 2 + k e y [ 1 ] × 27 + k e y [ 2 ] ) % T a b l e s i z e h(key) = (key[0] \times 27^2 + key[1] \times 27 + key[2]) \% Tablesize h(key)=(key[0]×272+key[1]×27+key[2])%Tablesize将结果扩展范围,会有空间浪费Tablesize=26^3 - 移位法
h ( k e y ) = ( ∑ i = 0 n − 1 k e y [ n − i − 1 ] × 3 2 i ) % T a b l e s i z e h(key)=(\sum_{i=0}^{n-1}key[n-i-1]\times 32^i) \% Tablesize h(key)=(∑i=0n−1key[n−i−1]×32i)%Tablesize
例如"abcde" :1)直接计算((a x 32+b)x 32 + c) x 32 + d ;2)将字符串左移5位 h = (h<<5) + *p++ (p是数组的指针,指针每向后移一次,h向左移5,相当于乘以32)
1.3解决冲突的方法
- 开放地址法(换个地址,一旦冲突,按照某种规则查找另一个地址 )
h i ( k e y ) = ( h ( k e y ) + d i ) % T a b l e S i z e h_i(key) = (h(key)+d_i) \% TableSize hi(key)=(h(key)+di)%TableSize,第i次冲突,为地址添加一个偏移量 d i d_i di- 线性探测, d i = i d_i = i di=i
- 平方探测, d i = ( − 1 ) i + 1 i 2 d_i = (-1)^{i+1} i^2 di=(−1)i+1i2,超过Tableszie后求余
- 双散列, d i = i ∗ h 2 ( k e y ) d_i = i*h_2(key) di=i∗h2(key),设计了两个散列
- 链地址法(同一位置的冲突对象用链表组织在一起)
1.3.1线性探测法
h
i
(
k
e
y
)
=
(
h
(
k
e
y
)
+
d
i
)
%
T
a
b
l
e
S
i
z
e
,
d
i
=
i
h_i(key) = (h(key)+d_i) \% TableSize,\quad d_i = i
hi(key)=(h(key)+di)%TableSize,di=i
如果在原计算位置已经有了数据,则发生一次冲突,找到下一个位置,查看是否为空。如果为空,放在这个位置,如果这个位置不为空,则又发生一次冲突,继续查找下个位置。
例:
- 设数组[47,7,29,11,9,84,54,20,30],数组长度为9,
- 设计散列表长13,那么装填因子 α = 9 / 13 = 0.69 \alpha=9/13=0.69 α=9/13=0.69
- 设散列函数为 h ( k e y ) = k e y % 11 h(key)=key \% 11 h(key)=key%11
- 用线性探测处理冲突,依次插入表
计算 h ( k e y ) h(key) h(key)
key | 47 | 7 | 29 | 11 | 9 | 84 | 54 | 20 | 30 |
---|---|---|---|---|---|---|---|---|---|
h(key) | 3 | 7 | 7 | 0 | 9 | 7 | 10 | 9 | 8 |
依次填入散列表对应地址中,若有冲突,用线性探测处理,最终结果为
h(key) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
key | 11 | 30 | – | 47 | – | – | – | 7 | 29 | 9 | 84 | 54 | 20 |
冲突次数 | 0 | 6 | – | 0 | – | – | – | 0 | 1 | 0 | 3 | 1 | 3 |
有聚集现象,当散列值有冲突为7时,冲突会越来越多
查找性能分析
- 成功平均查找长度(ASLs):查找每个数据需要比较次数(冲突次数+1)的平均值
即(1+7+1+1+2+1+4+2+4)/9=2.56 - 不成功平均查找长度(ASLu):查找不在表中的元素,确定此数值不在表中需要比较的次数。
- 将不在表中的数值分类为 n % 11 n\%11 n%11的余数为0,1,2,。。。,10的几个类别
- 例如22,33等余数为0的类别,按照冲突策略查找
- 先查找位置为0的值,发生冲突(即此位置有值,但不是期待值)
- 再查找位置为1的值,再次发生冲突,依次往后比较
- 直到位置为2,值为空,可以确信22,33等数不在表中,此时共比较3次
- 依次比较余数为1,2,…,12的值,计算比较多少次可以确信此值不在表中。(3+2+1+2+1+1+1+9+8+7+6)/11 = 3.73
1.3.2.平方探测法
h
i
(
k
e
y
)
=
(
h
(
k
e
y
)
+
d
i
)
%
T
a
b
l
e
S
i
z
e
,
d
i
=
(
−
1
)
i
+
1
i
2
h_i(key) = (h(key)+d_i) \% TableSize,\quad d_i = (-1)^{i+1} i^2
hi(key)=(h(key)+di)%TableSize,di=(−1)i+1i2
例:设表长11
key | 47 | 7 | 29 | 11 | 9 | 84 | 54 | 20 | 30 |
---|---|---|---|---|---|---|---|---|---|
h(key) | 3 | 7 | 7 | 0 | 9 | 7 | 10 | 9 | 8 |
依次填入散列表对应地址中,若有冲突,用平方探测处理
h(key) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
key | 11 | 30 | 20 | 47 | – | – | 84 | 7 | 29 | 9 | 54 |
冲突次数 | 0 | 3 | 3 | 0 | – | – | 2 | 0 | 1 | 0 | 0 |
- 平方探测最终有可能找不到空位置放入表格,但避免了线性探测的聚集现象。
- 如果表的大小设计成素数 4 k + 3 4k+3 4k+3,可以证明平方探测一定能找到空位置放入
1.3.3.双散列探测法
h i ( k e y ) = ( h 1 ( k e y ) + d i ) % T a b l e S i z e , d i = i ∗ h 2 ( k e y ) h_i(key) = (h_1(key)+d_i) \% TableSize, \quad d_i = i*h_2(key) hi(key)=(h1(key)+di)%TableSize,di=i∗h2(key)
- h 2 ( k e y ) h_2(key) h2(key)是另外一个散列函数,对于任意的key, h 2 ( k e y ) ≠ 0 h_2(key) \neq 0 h2(key)̸=0
- 探测序列需要保证所有的散列存储单元都能被探测到。可以用 h 2 ( k e y ) = p − ( k e y % p ) , p < T a b l e s i z e h_2(key) = p - (key\% p ),p < Tablesize h2(key)=p−(key%p),p<Tablesize,p和tablesize都是素数
1.3.4.再散列(rehashing)
- 当散列表装填因子太大,查找效率会下降;
- o . 5 < α < 0.85 o.5<\alpha <0.85 o.5<α<0.85比较实用的最大装填因子。最好小于0.5
- 再散列:当装填因子过大,需要加倍扩大散列表。此时需要重新计算所有的元素
1.3.5.分离链接法(separate chaining)
同一个位置上有冲突的值用链表串起来。表中存放的是链表的头指针。
1.4散列表的性能分析
- ASL(平均查找长度)
- 成功查找
- 不成功查找
影响产生冲突
- 散列函数是否均匀
- 处理冲突的方法
- 散列表的装填因子 α \alpha α。当装填因子超过0.5,期望次数增长很快
1.4.1.线性探测查找方法
期望探测次数
p
=
{
1
2
[
1
+
1
(
1
−
α
)
2
]
,
对插入和不成功查找而言
1
2
(
1
+
1
1
−
α
)
,
对成功查找而言
p = \begin{cases} \frac{1}{2}[1+\frac{1}{(1-\alpha)^2}], & \text{对插入和不成功查找而言}\\ \frac{1}{2}(1+ \frac{1}{1-\alpha}),& \text{对成功查找而言}\end{cases}
p={21[1+(1−α)21],21(1+1−α1),对插入和不成功查找而言对成功查找而言
1.4.2.平方探测/双散列查找方法
p = { 1 1 − α , 对插入和不成功查找而言 − 1 α l n ( 1 − α ) , 对成功查找而言 p = \begin{cases} \frac{1}{1-\alpha}, & \text{对插入和不成功查找而言}\\ -\frac{1}{\alpha}ln(1-\alpha),& \text{对成功查找而言}\end{cases} p={1−α1,−α1ln(1−α),对插入和不成功查找而言对成功查找而言
1.4.3.分离链接
这种情况下
α
\alpha
α有可能超过1,因为链表可以链很多
p
=
{
α
+
e
−
α
,
对插入和不成功查找而言
1
+
α
2
,
对成功查找而言
p = \begin{cases} \alpha + e^{-\alpha}, & \text{对插入和不成功查找而言}\\ 1+\frac{\alpha}{2},& \text{对成功查找而言}\end{cases}
p={α+e−α,1+2α,对插入和不成功查找而言对成功查找而言
1.5总结
- 选择合适的 h ( k e y ) h(key) h(key),没有冲突,时间复杂度为O(1)
- 很多情况下用于字符串管理
- 以 α \alpha α装填因子为前提,所以散列方法是以空间换时间
- 散列表的存储对关键字是随机的,不便于顺序查找、范围查找、最大值最小值查找
- 开放地址法
- 容易聚集
- 实现方法为数组,存储效率高
- 删除时需要“惰性删除”,即标记为"delete"状态,但不真的从空间中删除
- 分离链接法
- 顺序存储和链式存储结合,链表部分存储效率和查找效率比较低
- 关键字不需要 “懒惰删除”,没有存储“垃圾”(因为链表容易实现删除操作)
- 大的 α \alpha α容易导致空间浪费,但也容易付出更多的查找时间代价
- 不均匀的链表长度导致效率下降
应用实例:
- 例1:文件中单词词频统计,并输出词频最大的前10%的单词及词频(设计单词的管理,从词库中查找对应的单词)
- 例2:统计电话号码出现的次数,将手机号的最后5位做散列函数。用分离连接法解决冲突.
- 手机号码前三位为网络识别号,不适合做散列函数(容易聚集);又4位是地区编码;最后4位随机性比较好。
- p(表长度) > 2n(所有数据的个数)