| title | 『数据结构』散列表 | ||
|---|---|---|---|
| date | 2018-07-08 23:25 | ||
| categories | 数据结构与算法 | ||
| tags |
|
||
| keywords | |||
| mathjax | true | ||
| description | 散列表的原理与实现, 包括直接寻址, 链接法, 开放寻址法等 |
哈希表 (hash table) , 可以实现
其实数组也能实现, 只是数组用来索引的关键字是下标, 是整数. 而哈希表就是将各种关键字映射到数组下标的一种"数组"
由于关键字是用来索引数据的, 所以要求它不能变动(如果变动,实际上就是一个新的关键字插入了), 在python 中表现为 immutable. 常为字符串.
将关键字 k 进行映射, 映射函数
- 简单一致假设:元素散列到每个链表的可能性是相同的, 且与其他已被散列的元素独立无关.
- 简单一致散列(simple uniform hashing): 满足简单一致假设的散列
好的散列函数应 满足简单一致假设 例如 $$ \begin{aligned} &(1) \text{除法散列} \quad h(k) = k \ mod\ m \ &(2) \text{乘法散列} \quad h(k) = \lfloor {m(kA \ mod\ 1)\rfloor} \text{,(0< A< 1)}\ &\quad\text{任何 A 都适用,最佳的选择与散列的数据特征有关.}\ &\quad\text{ Knuth 认为,最理想的是黄金分割数}\frac{\sqrt{5} -1}{2} \approx 0.618 \end{aligned} $$
由于关键字值域大于映射后的地址值域, 所以可能出现两个关键字有相同的映射地址
可以先用 ascii 值,然后
- 各位相加
- 两位叠加
- 循环移位
- ...
将关键字直接对应到数组地址, 即
缺点: 如果关键字值域范围大, 但是数量小, 就会浪费空间, 有可能还不能储存这么大的值域范围.
通过链接法来解决碰撞
记有 m 个链表, n 个元素
则查找成功,或者不成功的时间复杂度为
设一组散列函数
对于 m 个槽位的表, 只需
选择足够大的 prime p, 记
每一个散列函数
所有表项都在散列表中, 没有链表.
且散列表装载因子$\alpha=\frac{n}{m}\leqslant1$
这里散列函数再接受一个参数, 作为探测序号
逐一试探
探测序列一般分有三种
- 线性$\ 0,1,\ldots,m-1$
存在一次聚集问题
- 二次$\ 0,1,\ldots,(m-1)^2$
存在二次聚集问题
- 双重探查
注意删除时, 不能直接删除掉(如果有元素插入在其后插入时探测过此地址,删除后就不能访问到那个元素了), 应该 只是做个标记为删除
对于开放寻址散列表,且
所以, 插入一个关键字, 也最多需要
成功查找的探查过程与插入是一样的. 所以查找关键字 k 相当于 插入它, 设为第 i+1 个插入的(前面插入了i个,装载因子$\alpha=\frac{i}{m}$. 那么期望探查数就是
则成功查找的期望探查数为 $$ \begin{aligned} \frac{1}{n}\sum_{i=0}^{n-1}\frac{m}{m-i}=\frac{m}{n}\sum_{i=0}^{n-1}\frac{1}{m-i} &= \frac{m}{n}\sum_{i=m-n+1}^{m}\frac{1}{i}\ &\leqslant \frac{1}{\alpha} \int_{m-n}^m\frac{1}{x}dx\ &=\frac{1}{\alpha}ln\frac{1}{1-\alpha} \end{aligned} $$
代码
class item:
def __init__(self,key,val,nextItem=None):
self.key = key
self.val = val
self.next = nextItem
def to(self,it):
self.next = it
def __eq__(self,it):
'''using keyword <in> '''
return self.key == it.key
def __bool__(self):
return self.key is not None
def __str__(self):
li = []
nd = self
while nd:
li.append(f'({nd.key}:{nd.val})')
nd = nd.next
return ' -> '.join(li)
def __repr__(self):
return f'item({self.key},{self.val})'
class hashTable:
def __init__(self,size=100):
self.size = size
self.slots=[item(None,None) for i in range(self.size)]
def __setitem__(self,key,val):
nd = self.slots[self.myhash(key)]
while nd.next:
if nd.key ==key:
if nd.val!=val: nd.val=val
return
nd = nd.next
nd.next = item(key,val)
def myhash(self,key):
if isinstance(key,str):
key = sum(ord(i) for i in key)
if not isinstance(key,int):
key = hash(key)
return key % self.size
def __iter__(self):
'''when using keyword <in>, such as ' if key in dic',
the dic's __iter__ method will be called,(if hasn't, calls __getitem__
then ~iterate~ dic's keys to compare whether one equls to the key
'''
for nd in self.slots:
nd = nd.next
while nd :
yield nd.key
nd = nd.next
def __getitem__(self,key):
nd =self.slots[ self.myhash(key)].next
while nd:
if nd.key==key:
return nd.val
nd = nd.next
raise Exception(f'[KeyError]: {self.__class__.__name__} has no key {key}')
def __delitem__(self,key):
'''note that None item and item(None,None) differ with each other,
which means you should take care of them and correctly cop with None item
especially when deleting items
'''
n = self.myhash(key)
nd = self.slots[n].next
if nd.key == key:
if nd.next is None:
self.slots[n] = item(None,None) # be careful
else:self.slots[n] = nd.next
return
while nd:
if nd.next is None: break # necessary
if nd.next.key ==key:
nd.next = nd.next.next
nd = nd.next
def __str__(self):
li = ['\n\n'+'-'*5+'hashTable'+'-'*5]
for i,nd in enumerate(self.slots):
li.append(f'{i}: '+str(nd.next))
return '\n'.join(li)
