映射與字典
字典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語言實現》