一、什么是字典?
字典是一堆key、value配對組成的元素的集合。字典是一個可變容器,可以存儲任意類型對象。
二、字典是否是有序的?
在python3.6之前,字典是無序的,但是python3.7+,字典是有序的。在python3.7中,字典有序正式成為語言特性。
三、字典的各種操作時間復雜度?
字典的查詢、添加、刪除的平均時間復雜度都是O(1),相比列表與元組,性能更優。
四、字典的實現原理
1. python3.6及之前的版本
字典的底層是維護了一張哈希表,我們可以把哈希表看成一個二維數組,哈希表中的每一個元素又存儲了哈希值(hash)、鍵(key)、值(value)3個元素。
enteies = [ ['--', '--', '--'], [hash, key, value], ['--', '--', '--'], ['--', '--', '--'], [hash, key, value], ]
由上可見哈希表的存儲結構,我們也可以看出,元素之間有一些空元素,我們通過增加一個元素來講解具體實現。
- 計算key的hash值【hash(key)】,在和mask做與操作【mask=字典的最小長度(DictMinSize)-1】,運算后會得到一個數字index,這個index就是要插入enteies哈希表中的下標位置。
- 若index下標位置已經被占用,則會判斷enteies的key是否與要插入的key相等。
- 如果key已經存在,則更新vlaue值
- 如果key不存在,就表示hash沖突,會繼續向下尋找空位置
以上介紹了老字典的實現過程,下面我們帶入具體的數值來介紹。
# 給字典添加一個值,key為hello, value為word mydict['hello'] = 'word' # 假設是一個空列表,hash表初始如下 enteies = [ ['--', '--', '--'], ['--', '--', '--'], ['--', '--', '--'], ['--', '--', '--'], ['--', '--', '--'], ]
注:以下計算值為假設值,不等於實際值 hash_value = hash('hallo') # 假設值為12345 index = hash_value & (len(enteies) - 1) # 假設index值計算后等於3, # 下面會將值存在enteies中 enteies = [ ['--', '--', '--'], ['--', '--', '--'], ['--', '--', '--'], [12345, 'hello', 'word'], # index=3 ['--', '--', '--'], ] # 我們繼續向字典中添加值 my_dict['color'] = 'green' hash_value = hash('color') # 假設值為 同樣為12345 index = hash_value & ( len(enteies) - 1) # 假設index值計算后同樣等於3 # 下面會將值存在enteies中 enteies = [ ['--', '--', '--'], ['--', '--', '--'], ['--', '--', '--'], [12345, 'hello', 'word'], # 由於index=3的位置已經被占用,且key不一樣,所以判定位hash沖突,繼續向下尋找 [12345, 'color', 'green'], # 找到空余位置,則保存 ]
上訴就是字典的插入過程,理解了直接,其實刪除和查詢也是差不多的方法。我們可以看到,不同的key計算出的index是不一樣的,在enteies中插入的位置不一樣,在entiese中插入的位置就不一樣,所以我們在遍歷字典的時候,字典的順序與我們插入的順序是不一樣的。
我們可以發現,enteies表是稀疏的,隨着我們插入的值不同,表會越來越稀疏(hash表也是一個會動態擴展長度的,每一次擴展,都會重新計算所有的key和hash值),所以新的字典的實現就出現了。
2. python3.7+后的新實現方法
老版的字典使用一張hash表,新字典在此基礎上,再使用了一張新的indices表來輔助。
indices = [None, None, index, None, index, None, index] enteies = [ [hash0, key0, value0], [hash1, key1, value1], [hash2, key2, value2] ]
具體實現過程:
- 計算key的hash值【hash(key)】,在和mask做與操作,運算后得到一個數字【index】,這個index就是要插入的indices的下標位置
- 得到index后,會找到indices的位置,但是此位置不是存的hash值,hash值、key、value存在enteies表中,indices的index存到對應在enteies中存放數據的下標
- 如果出現hash沖突,按照老字典的處理方式處理
# 給字典添加一個值,key為hello,value為word my_dict['hello'] = 'word' # 假設是一個空列表,hash表初始如下 indices = [None, None, None, None, None, None] enteies = [] hash_value = hash('hello') # 假設值為 12343543 index = hash_value & ( len(indices) - 1) # 假設index值計算后等於3 # 會找到indices的index為3的位置,並插入enteies的長度 indices = [None, None, None, 0, None, None] # 此時enteies會插入第一個元素 enteies = [ [12343543, 'hello', 'word'] ] # 我們繼續向字典中添加值 my_dict['haimeimei'] = 'lihua' hash_value = hash('haimeimei') # 假設值為 34323545 index = hash_value & ( len(indices) - 1) # 假設index值計算后等於 0 # 會找到indices的index為0的位置,並插入enteies的長度 indices = [1, None, None, 0, None, None] # 此時enteies會插入第一個元素 enteies = [ [12343543, 'hello', 'word'], [34323545, 'haimeimei', 'lihua'] ]
查詢字典的具體過程:
# 下面是一個字典與字典的存儲 more_dict = {'name': '張三', 'sex': '男', 'age': 10, 'birth': '2019-01-01'} # 數據實際存儲 indices = [None, 2, None, 0, None, None, 1, None, 3] enteies = [ [34353243, 'name', '張三'], [34354545, 'sex', '男'], [23343199, 'age', 10], [00956542, 'birth', '2019-01-01'], ] print(more_dict['age']) # 當我們執行這句時 hash_value = hash('age') # 假設值為 23343199 index = hash_value & ( len(indices) - 1) # index = 1 entey_index = indices[1] # 數據在enteies的位置是2 value = enteies[entey_index] # 所以找到值為 enteies[2]
由上可以看出,新字典存儲數據的hash表並不會稀疏,由indices來維護具體存儲的位置,enteies中的數據的順序跟插入數據的前后是一樣的,所以字典是有序的。
五、時間復雜度說明
我們在上面提到了,字典的平均時間復雜度是O(1),因為字典是通過哈希算法來實現的,哈希算法不可避免的問題就是hash沖突,Python字典發生哈希沖突時,會向下尋找空余位置,直到找到位置。如果在計算key的hash值時,如果一直找不到空余位置,則字典的時間復雜度就變成了O(n)了,所以Python的哈希算法就顯得非常重要了。Python字典的哈希算法,會盡量保證哈希值計算出的index是平均分布且每一個值之間有剩余位置,例如:
[index, None, None, None, index, None, None, None]
index盡量只為 0, 3, 5, 7類似值,保證在發生哈希沖突時,能很快的找到空余位置。
六、字典的key能使用什么值?
Python字典的key可以使用str, int, tuple等不變數據類型。因為字典是通過hash算法來計算key的值在進行字典操作的,所以key必須為可哈希的,list不能作為字典的key,因為list是可變的。
l1 = [1] l2 = [1, 2] test_d = {l1: '1', l2: '2'} # 如果此時把l1.append(2),則字典的兩個key hash出來的結果是一樣的 那么字典該給你返回"1",還是"2"呢? l1.append(2)
字典的刪除操作
對於刪除操作,python會暫時對這個位置的元素,賦一個特殊的值,等到重新調整哈希表大小的時候,再將其刪除
不難理解,哈希沖突的發生,往往會降低字典和集合操作的速度,因此,為了保證其高效性,字典內的哈希表,通常會保證至少留有1/3的剩余空間。隨着元素不斷插入,當剩余空間小於1/3時,python會重新獲取更大的內存空間,擴充哈希表,不過,這種情況下,表內所有的元素位置會被重新排放
雖然哈希沖突和哈希表大小的調整,都會導致速度緩慢,但是這種情況發生的次數極少,所以,平均情況下,這仍能保證插入、查找和刪除的時間復雜度都為O(1)
七、解決哈希沖突的方法
1.開放尋址法:如果哈希函數計算出來的index位置已經有值,則可以向后探查新的位置來存儲這個值。
- 線性探查:如果位置i被占用,則探查i+1, i+2...
- 二次探查:如果位置i被占用,則探查i+1^2, i-1^2, i+2^2,i-2^2..
- 二度哈希:有n個哈希函數,當使用第一個hash函數h1發生沖突時,則嘗試使用h2,h3..
2.拉鏈法:hash表每個位置都鏈接一個鏈表,當沖突發生時,沖突的元素將被加到該位置鏈表的最后
拉鏈法解決hash沖突代碼實現
# 鏈表類 class LinkList: class Node: def __init__(self, item=None): self.item = item self.next = None class LinkListIterator: def __init__(self, node): self.node = node def __next__(self): if self.node: cur_node = self.node self.node = cur_node.next return cur_node.item else: raise StopIteration def __iter__(self): return self def __init__(self, iterable=None): self.head = None self.tail = None if iterable: self.extend(iterable) def append(self, obj): s = LinkList.Node(obj) if not self.head: self.head = s self.tail = s else: self.tail.next = s self.tail = s def extend(self, iterable): for obj in iterable: self.append(obj) def find(self, obj): for n in self: if n == obj: return True else: return False def __iter__(self): return self.LinkListIterator(self.head) def __repr__(self): return "<<"+", ".join(map(str, self))+">>" # 類似於集合的結構 class HashTable: def __init__(self, size=101): self.size = size self.T = [LinkList() for i in range(self.size)] def h(self, k): return k % self.size def insert(self, k): i = self.h(k) if self.find(k): print("Duplicated Insert") else: self.T[i].append(k) def find(self, k): i = self.h(k) return self.T[i].find(k) ht = HashTable() ht.insert(0) ht.insert(1) ht.insert(3) ht.insert(102) ht.insert(508) print(",".join(map(str, ht.T))) print(ht.find(203))
常見hash函數:
- 除法哈希法:h(k) = k % m
- 乘法哈希法
- 全域哈希法