用Python实现数据结构之映射
映射与字典
字典dict是Python中重要的数据结构,在字典中,每一个键都对应一个值,其中键与值的关系就叫做映射,也可以说是每一个键都映射到一个值上。
映射(map)是更具一般性的数据类型,具体到Python中就是字典。
一个简单实现
在使用字典的同时我们一定会有一个疑问,它是怎样通过键去映射到值的呢,它怎么知道这个键的值是谁?
于是我们有了一个这样的想法:
使用列表来存储一项一项的键值对象,寻找的时候就遍历一遍列表,找到当键是你所要找的键时,取出该对象中的值value。
这个想法很简单,我们可以很快的实现一下:
这里先介绍一些相关的抽象基类,Mapping与MutableMapping,它们在collections模块中,供我们实现自定义的map类。Mapping包含dict中的所有不变方法,MutableMapping扩展包含了所有可变方法,但它们两个都不包含那五大核心特殊方法:getitem、setitem、delitem、len、iter。也就是说我们的目标就是实现这五大核心方法使该数据结构能够使用。
from collections import MutableMapping
class MyMap(MutableMapping):
class item():
def __init__(self,key,value):
self.key = key
self.value = value
def __eq__(self, other):
return self.key == other.key
def __ne__(self, other):
return self.key != other.key
def __init__(self):
self.table = []
def __getitem__(self, item):
for i in self.table:
if i.key == item:
return i.value
raise KeyError('Key Error: '+ repr(item))
def __setitem__(self, key, value):
for i in self.table:
if i.key == key:
i.value = value
return
self.table.append(self.item(key,value))
def __delitem__(self, key):
for n,i in enumerate(self.table):
if i.key == key:
self.pop(n)
return
raise KeyError('Key Error: '+ repr(key))
def __len__(self):
return len(self.table)
def __iter__(self):
for i in self.table:
yield i.key
上面这个办法很简单,但是却不是很有效率,我们每次都需要遍历一遍列表才能找到该键的索引,所以时间复杂的为O(n),我们希望这个映射的时间复杂度为O(1)或者是一个常数级别的,于是我们使用叫做哈希表的结构来实现
哈希表
首先先介绍一下哈希表的实现方式
1.对于一个键,我们需要计算出一个值来代表这个键,也就是将键映射到一个整数上,这个整数可以是正数也可以是负数,这一步就是求哈希值
2.这些哈希值有正有负,互相之间没有什么关系,并且位数也可能是好几位,我们想要把这些哈希值再次映射到一个区间[0,N-1]中,使得可以通过列表的整数索引去查找,这一步就是对哈希码的压缩,使用的函数叫做压缩函数
3.在经过压缩函数处理后,就可以得到原先的键对应的列表索引了,但是求哈希值与执行压缩函数的过程中,可能会有冲突发生,也就是得出的值不一定只是属于本键唯一的,可能一个其他的键也会得到同样的值。这时就要在最后把这种冲突处理掉,这一步就叫做冲突处理。
下面具体介绍一下这三个步骤
1.哈希码
求哈希码有很多种方式
将位作为整数处理
举个例子,Python中的哈希码是32位的,如果一个浮点数是64位,我们可以采取取其高32位为哈希码,或者低32位为哈希码,但这样极易出现冲突,所以可以采取高32位与低32位按位相加,或者按位异或
多项式哈希码
对于像是字符串这样的对象,如果按照求和或异或的方式,可能会产生更多的冲突,比如temp10与temp01就会得到相同的哈希码。在字符串中,字符的位置非常重要,所以需要采取一种与位置有关系的哈希码计算方法,如下面这个式子:
x0a^(n-1)+x1a^(n-2)+……+x(n-2)a+x(n-1)
(x0,x1,x2,……,xn-1)是一个32位整数的n元组,是对象x的二进制表示
采用这种计算方式就可以与位置有关联了
循环移位哈希码
利用二进制位循环移位方式,如下面这个字符串循环移位哈希码计算的实现:
def hash_code(s):
mask = (1 << 32) - 1
h = 0
for character in s:
h = (h << 5 & mask) | (h >> 27)
h += ord(character)
return h
<<是左移,>>是右移,&是按位与,|是按位或,ord()函数返回一个字符的ascii码或unicode值
Python中的哈希码
Python中提供了hash()函数,传入对象x,返回一个整型值作为它的哈希码
在Python中只有不可变类型才可以使用hash,如果把我们自定义的对象作为参数传入则会报错。
若想让我们自定义的对象能够使用,可以在类中实现一个叫做hash的特殊方法,在该函数中调用hash函数,并传入该对象的一些不可变属性组合,将值再返回,例如:
def __hash__(self):
return hash((self.red,self.green,self.blue))
2.压缩函数
划分方法
要把哈希码映射到[0,N-1]的区间中,最简单的方式就是进行求余数,例如f(i) = i modN
可是这样显然会有大量的冲突,一种稍微能够减小冲突的办法是将N改为一个素数
这样能够得到一些改善,但是pN+q类型的哈希码还是会被压缩成同一个数
MAD方法
MAD即Multiply-Add-and-Divide,这个方法通过下面这个式子进行映射
[(ai+b) mod p] mod N
N是区间的大小,p是比N大的素数,a和b是从区间[0,p-1]任意选择的整数且a>0
这个函数会尽可能的使映射均匀的分配到[0,N-1]中
3.冲突处理
尽管在求哈希值与压缩函数的过程中我们尽可能避免发生冲突,但还是会有可能造成冲突的,为此还需要进行冲突的处理
使用二级容器
把列表中的每一项都存储为一个二级容器,将映射到该项的键值存入到二级容器中,查找键时先定位到二级容器,再在二级容器中寻找。这里的二级容器的效率要求就不是那么高了,可以使用上文中最开始定义的映射的简单实现来做这个二级容器。在整个哈希表中,我们希望存储的键值项的数量n小于N,也就是n/N<1,n/N叫做这个哈希表的负载因子。
线性探测
这个简单说就是如果映射到这个地方已经有其他键值占上了,那么就向它的后一位放,如果后一位也有了,就继续向后放,知道找到一个空位,然后放进去。
查找的时候,映射到一个位置时要判断一下是不是要找的那个key,如果不是就向后一位找,知道找到是相同键了或者出现空位了,就停止
删除的时候,一样是先找到,然后为了不影响查找,不能简单的将其设置为空,应该用一个标记的对象填住该位置,这时查找的方法也要进行一些改动使其能够跳过这种标记位置。
这种方法的缺点是每一对键值会连续的存储,这种聚集的现象会导致效率的问题。
二次探测
为了改善线性探测聚集现象的发生,原先采用的(j+i)mod N(j为压缩函数得出的值,i为1,2,3….)探测方式改为(j+i^2)mod N
但是当元素超过了哈希表的一半时,这种方式无法保证找到空闲的位置。而且这种方式的删除或其他操作也会更复杂
双哈希策略
这种方式选择了再次进行哈希,如将探测方式改为(j+i*h(k))mod N,h()为一个哈希函数,k为键。
Python字典所采用的方式
字典采用的是(j+f(i))mod N的方式,f(i)是一个基于伪随机数产生器的函数,它提供一个基于原始位的可重复的但是随机的,连续的地址探测序列。
用Python具体实现
首先是一个哈希表的基类,采用MAD的压缩函数
class HashMapBase(MutableMapping):
"""哈希表的基类,需要在子类中实现_inner_getitem,_inner_setitem,
_inner_delitem与__iter__"""
class item():
def __init__(self, key, value):
self.key = key
self.value = value
def __eq__(self, other):
return self.key == other.key
def __ne__(self, other):
return self.key != other.key
def __init__(self,cap=11,p=109345121):
self._table = cap*[None]
self._n = 0 # 元素数量
self._prime = p # MAD中的参数
self._scale = 1 + random.randrange(p+1) # MAD中的参数
self._shift = random.randrange(p) # MAD中的参数
def _hash_func(self,key):
return (hash(key)*self._scale+self._shift)%self._prime%len(self._table)
def __len__(self):
return self._n
def __getitem__(self, k):
j = self._hash_func(k)
return self._inner_getitem(j,k)
def __setitem__(self, key, value):
j = self._hash_func(key)
self._inner_setitem(j,key,value)
if self._n>len(self._table)//2: #调整大小,使负载因子小于等于0.5
self._resize(2*len(self._table)-1)
def __delitem__(self, key):
j = self._hash_func(key)
self._inner_delitem(j,key)
self._n -= 1
def _resize(self,cap):
old = list(self.items())
self._table = cap*[None]
self._n = 0
for (k,v) in old:
self[k] = v
其中innergetitem,_inner_setitem,_inner_delitem的实现需要结合处理冲突的方式,猜测self.items()是内部调用了__iter方法实现的
使用二级容器
class HashMapOne(HashMapBase):
"""使用二级容器解决冲突的方式实现的哈希表"""
def _inner_getitem(self,j,k):
bucket = self._table[j] #把二级容器叫做桶
if bucket is None:
raise KeyError('Key Error: '+ repr(k))
return bucket[k]
def _inner_setitem(self,j,k,v):
if self._table[j] is None:
self._table[j] = MyMap()
oldsize = len(self._table[j])
self._table[j][k] = v
if len(self._table[j])>oldsize:
self._n += 1
def _inner_delitem(self,j,k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
del bucket[k]
def __iter__(self):
for bucket in self._table:
if bucket is not None:
for key in bucket:
yield key
使用线性探测
class HashMapTwo():
"""使用线性探测解决冲突实现的哈希表"""
_AVAIL = object() # 标记删除位置
def _is_available(self, j):
"""判断该位置是否可用"""
return self._table[j] is None or self._table[j] is HashMapTwo._AVAIL
def _find_slot(self, j, k):
"""寻找键k所在的索引
如果找到了,返回(True,索引)
如果没找到,返回(False,第一个可提供的索引位置)"""
firstAvail = None
while True:
if self._is_available(j):
if firstAvail is None: # _AVAIL标记可以是第一个可提供的位置
firstAvail = j
if self._table[j] is None: # 跳过_AVAIL标记
return (False, firstAvail)
elif k == self._table[j].key:
return (True, j)
j = (j + 1) % len(self._table) # 向下一个查找
def _inner_getitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
return self._table[s].value
def _inner_setitem(self, j, k, v):
found, s = self._find_slot(j, k)
if not found: # 使用第一个可提供的位置
self._table[s] = self.Item(k, v)
self._n += 1
else:
self._table[s].value = v
def _inner_delitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
self._table[s] = HashMapTwo._AVAIL # 删除标记
def __iter__(self):
for j in range(len(self._table)):
if not self._is_available(j):
yield self._table[j].key
参考《数据结构与算法Python语言实现》