在python中的 dict 和 set 查找速度很快,内部就是散列表(也叫作哈希表)
-
数组通过下标访问,时间O(1),删除要O(n)
-
链表遍历访问,时间O(n)
-
哈希表就是一种快速定位和删除元素的方法。
- 除了利用元素下标找到,还有一种就是找到【逻辑下标】,再找到这个元素。他通过哈希函数计算一个元素应该放在哪个位置,然后对于一个特定的元素,哈希函数每次计算的下标要一样,而且范围不能超过给定的数组长度
- 举例:
- 有一个数组T,包含M=13个元素,定义一个哈希函数h
h(key) = key % M
- 这样取模运算使得结果在数组范围内
-
哈希冲突(collision):
- 解决办法
- 链接法(chaining)
- 在当前位置多出一条支链,进行数据存储
- 而如果链接过长,时间复杂度会退化变复杂
- 开放寻址法(open addressing)
- 线性探查(linear probing)
- 当一个槽被占用,找下一个可用的槽
h(k, i)=(h'(k)+i)%m, i==0,1,...,m-1
- k是插入的值,i是位置,m是
- 二次探查(quadratic probing)
- 一个槽被占用,以二次方作为偏移量
h(k, i)=(h'(k)+c_1+c_2*i**2)%m, i=0,1,...,m-1
- c_1,c_2常数,i**2; c_1+c_2*i**2偏移量
- 双重散列(double hashing)
- 重新计算结果
h(k,i)=(h_1(k)+ih_2(k))%m
- 线性探查(linear probing)
- 链接法(chaining)
- 哈希函数
- 上边用的是取模的简单函数
- 装载因子(load factor)
- 哈希表大小为13,槽数被占用4,则load factor为 4/13
- 当装载因子超过0.8,就要新开辟空间并且重新进行散列
- 重哈希(Rehashing)
- 装载因子超过0.8,扩容然后重新哈希操作
- 解决办法
-
HashTable ADT
# -*- coding:utf-8 -*-
# Author: Greed_Vic(PL Z)
# Product_name: PyCharm
# File_name: Hashtable
# @Time: 15:26 2021/6/10
class Array(object):
"""
定义一个数组
"""
def __init__(self, size=32, init=None):
self._size = size # 设置数组长度
self._items = [init] * size # 开辟数组,初始None
def __getitem__(self, index):
return self._items[index] # 特殊方法,用来获取此下标的元素
def __setitem__(self, index, value):
self._items[index] = value # 特殊方法,用来赋值
def __len__(self):
return self._size # 特殊方法,获取长度 接受 len(array)
def __iter__(self):
for item in self._items: # 特殊方法, 接受迭代
yield item
def clear(self, value=None):
for i in range(self._items):
self._items[i] = value # 重置
class Slot(object):
"""
定义一个槽,用来装入array的
"""
def __init__(self, key, val):
self.key, self.val = key, val
class HashTable(object):
"""
三种状态的表,没用过的,空的,以及用过的
"""
UNUSED = None # unused
EMPTY = Slot(None, None) # used but deleted
def __init__(self):
self._table = Array(8, init=HashTable.UNUSED) # init an array [None,None,...,None]
self.length = 0 # init length
@property # 很方便的创建只读属性 √
def _load_factor(self):
return self.length / float(len(self._table))
def __len__(self): # 求哈希表的占用空间
return self.length
def _hash(self, key): # 哈希函数,求对应下标
return abs(hash(key)) % len(self._table)
def _find_key(self, key):
"""
用来根据所给的key来进行同样的hash操作找到下标
:param key:
:return: 下标or None
"""
index = self._hash(key) # 第一次哈希操作
_len = len(self._table) # 求当前哈希表总大小
while self._table[index] is not HashTable.UNUSED: # 当前的下标索引的空间不是未使用空间
if self._table[index] is HashTable.EMPTY: # 当前空间是被使用但是被remove了的空间
index = (index * 5 + 1) % _len # cpython 使用的一种解决哈希冲突的方式
continue
elif self._table[index].key == key: # 如果存在这个key,返回当前下标
return index
else:
index = (index * 5 + 1) % _len
return None # 不符合条件的key,返回None
def _slot_can_insert(self, index):
"""
:param index:
:return: 当前槽的两种状态是能够插入的
"""
return (self._table[index] is HashTable.EMPTY
or self._table[index] is HashTable.UNUSED)
def _find_slot_for_insert(self, key):
"""
:param key:
:return:寻找能够插入的槽的下标
"""
index = self._hash(key) # 哈希操作
_len = len(self._table) # 哈希表总大小
while not self._slot_can_insert(index): # 当前下标不可用作插入的槽
index = (index * 5 + 1) % _len # 解决哈希冲突
return index
def __contains__(self, key):
"""
:param key:
:return: 查找key是否存在
"""
index = self._find_key(key)
return index is not None
def add(self, key, val):
"""
用来找到对应下标,然后进行插入
:param key:
:param val:
:return:
"""
if key in self: # key存在,获取下标
index = self._find_key(key)
self._table[index].val = val # 进行赋值
return False
else: # key不存在
index = self._find_slot_for_insert(key) # 找到可以插入的下标
self._table[index] = Slot(key, val) # 进行插入
self.length += 1 # 占用长度加一
if self._load_factor >= 0.8: # 装载因子达到条件
self._rehash() # 进行重新哈希操作
def _rehash(self): # 重哈希操作
old_table = self._table # 复制老的哈希表
newsize = len(self._table) * 2 # 定义一个长度为原来的两倍的哈希表
self._table = Array(newsize, HashTable.UNUSED) # 初始化哈希表
self.length = 0 # 初始化占用长度
for slot in old_table:
"""
每一个遍历进行插入
"""
if slot is not HashTable.UNUSED and slot is not HashTable.EMPTY:
index = self._find_slot_for_insert(slot.key)
self.length += 1
self._table[index] = slot
def get(self, key, default=None):
index = self._find_key(key) # 找到对应下标
if index is None:
return default
else:
return self._table[index].val # 返回对应的值 这里面试 slot(key, val) 的结构
def remove(self, key):
index = self._find_key(key) # 找到对应的下标
if index is None:
raise KeyError()
val = self._table[index].val # 获取将要remove的值
self.length -= 1
self._table[index] = HashTable.EMPTY # 被remove 定义为空的槽
return val
def __iter__(self):
"""
可进行迭代的特殊方法
:return: 只迭代回key
"""
for slot in self._table:
if slot not in (HashTable.EMPTY, HashTable.UNUSED):
yield slot.key
if __name__ == '__main__':
"""
单测
"""
def test():
h = HashTable()
h.add('a', 0)
h.add('b', 1)
h.add('c', 2)
assert len(h) == 3
assert h.get('a') == 0
assert h.get('b') == 1
assert h.get('asd') is None
h.remove('a')
assert h.get('a') is None
assert sorted(list(h)) == ['b', 'c']
n = 50
for i in range(n):
h.add(i, i)
for i in range(h):
assert h.get(i) == i
test()