HashMap(python實現原理)


一、什么是字典?

字典是一堆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
  • 乘法哈希法
  • 全域哈希法

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM