散列表
在直接寻址方式中,具有关键字
k
k
k的元素被存放在槽
k
k
k中,在散列方式下,该元素存放在槽
h
(
k
)
h(k)
h(k)中,即利用散列函数
h
h
h,由关键字
k
k
k计算出槽的位置。函数
h
h
h将关键字的全域
U
U
U映射到散列表
T
[
0..
m
−
1
]
T[0..m-1]
T[0..m−1]的槽位上,这里散列表的大小
m
m
m一般要比
∣
U
∣
|U|
∣U∣小的多,下图描述了这个基本方法。
这里存在一个问题,两个关键字可能映射到同一个槽中,我们称这种情形为冲突。我们可以
h
h
h尽可能的“随机”,从而避免冲突或者使冲突的次数最小化。
- 通过链接法解决冲突:把散列到同一槽中的所有元素都放在一个链表中,如下图所示,槽
j
j
j中有一个指针,它指向存储所有散列到
j
j
j的元素的链表的表头,如果不存在这样的元素,则槽
j
j
j中为
N
I
L
NIL
NIL。
- 链接法散列的分析:散列方法的平均性能依赖于所选取的散列函数
h
h
h,将所有关键字集合分布在在
m
m
m个槽位上的均匀程度。给定一个能存放
n
n
n个元素,具有
m
m
m个槽位的散列表
T
T
T,定义
T
T
T的装载因子
a
a
a为
n
/
m
n/m
n/m,即一个链的平均存储元素数。
定理1:在简短均匀散列的假设下,对于用链接法解决冲突的散列表,一次不成功查找的平均时间 Θ ( 1 + a ) \Theta(1+a) Θ(1+a)
定理2:在简短均匀散列的假设下,对于用链接法解决冲突的散列表,一次成功查找的平均时间 Θ ( 1 + a ) \Theta(1+a) Θ(1+a)
散列函数
有时,我们知道关键字的概率分布,如果各关键字都是随机的实数
k
k
k,他们独立均匀地分布于
0
≤
k
<
1
0\le k<1
0≤k<1范围中,那么散列函数
h
(
k
)
=
⌊
k
m
⌋
h(k)=\lfloor km \rfloor
h(k)=⌊km⌋
就能满足简单均匀散列的假设条件,好的散列函数应能将这些相近符号散列到相同槽中的可能性最小化,常见的散列函数如下:
- 除法散列法:通过取 k k k除以 m m m的余数,将关键字 k k k映射到 m m m个槽中的某一个,即散列函数为: h ( k ) = k m o d m h(k)=k{\ }mod{\ } m h(k)=k mod m
- 乘法散列法:用关键字 k k k乘上常数A(0<A<1),提取 k A kA kA的小数部分,再用 m m m乘以这个值,再向下取整,散列函数为: h ( k ) = ⌊ m ( k A m o d 1 ) ⌋ h(k)= \lfloor m(kA{\ }mod{\ }1) \rfloor h(k)=⌊m(kA mod 1)⌋
- 全域散列法:随机的选择散列函数,使之独立于要存储的关键字,避免出现最坏的情况,所有的关键字散列到同一个槽中
开放寻址法
在开放寻址法中,所有元素都存放在散列表里,每个表项或包含动态集合的一个元素,或包含 N I L NIL NIL,开放寻址的好处在于它不用指针,而是要计算出要存取的槽序列,于是,不用存储指针而节省的空间,使得可以用同样的空间来提供更多的槽,潜在地减少了冲突,提高了检索速度。
- 探查序列:为了使用开放寻址法插入一个元素,需要连续地检查散列表,或称为探查,直到找到一个空槽来放置待插入的关键字为止,检查的顺序不一定是0,1,…,m-1,而是依赖待插入的关键字。对每个关键字 k k k,使用开放寻址法的探查序列 < h ( k , 0 ) , h ( k , 1 ) , . . . , h ( k , m − 1 ) > <h(k,0),h(k,1),...,h(k,m-1)> <h(k,0),h(k,1),...,h(k,m−1)>是<0,1,…,m-1>的一个排列
- 探查序列方法:有三种技术常用来计算开放寻址中的探查序列:线性探查、二次探查和双重探查。
线性探查: h ( k , i ) = ( h ∗ ( k ) + i ) m o d m h(k,i)=(h^*(k)+i) {\ }mod {\ }m h(k,i)=(h∗(k)+i) mod m
二次探查: h ( k , i ) = ( h ∗ ( k ) + c 1 i + c 2 i 2 ) m o d m h(k,i)=(h^*(k)+c_1i+c_2i^2) {\ }mod {\ }m h(k,i)=(h∗(k)+c1i+c2i2) mod m
双重散列: h ( k , i ) = ( h 1 ( k ) + i h 2 ( k ) ) m o d m h(k,i)=(h_1(k)+ih_2(k)) {\ }mod {\ }m h(k,i)=(h1(k)+ih2(k)) mod m
其中 h 1 ( k ) h_1(k) h1(k)和 h 2 ( k ) h_2(k) h2(k)均为辅助散列函数,我们可取 m m m为素数,并取 h 1 ( k ) = k m o d m , h 2 ( k ) = 1 + ( k m o d m ∗ ) h_1(k)=k{\ }mod{\ }m,h_2(k)=1+(k{\ }mod{\ }m^*) h1(k)=k mod m,h2(k)=1+(k mod m∗),其中 m ∗ m^* m∗稍小于 m m m(比如 m − 1 m-1 m−1),双重散列是用于开放寻址法的最好方法之一,它产生的排列具有随机选择排列的许多性质,如下图所示,双重散列过程如下:
基于双重散列的开放寻址法 python实现:
# -*-coding:utf8 -*-
import sys
class OpenAddrssHash(object):
def __init__(self,size):
#初始化散列表A,大小为size
self.T = [None]*size
self.size = size
#双重散列表定义
def double_hash(self, k,i):
h1 = k % self.size
h2 = 1 + k%(self.size-2)
h = (h1 + i*h2) % self.size
return h
#向散列表T中插入k,并返回槽位j
def hash_insert(self, k):
i = 0
while i<self.size:
j = self.double_hash(k, i)
if self.T[j] == None:
self.T[j] = k
return j
i+=1
return "hash table overflow"
#向散列表T中搜索关键词k,返回槽位j
def hash_search(self, k):
i = 0
j = self.double_hash(k,i)
while self.T[j]!=None and i<self.size:
j = self.double_hash(k,i)
if self.T[j]==k:
return j
i+=1
return None
if __name__=='__main__':
example = OpenAddrssHash(13)
#插入
for k in [79,69,98,72,14,50]:
print(example.hash_insert(k))
#查找
for k in [79,98,14,900]:
print(example.hash_search(k))