python數據結構之哈希表


哈希表(Hash table)

眾所周知,HashMap是一個用於存儲Key-Value鍵值對的集合,每一個鍵值對也叫做Entry。這些個鍵值對(Entry)分散存儲在一個數組當中,這個數組就是HashMap的主干。

使用哈希表可以進行非常快速的查找操作,查找時間為常數,同時不需要元素排列有序;python的內建數據類型:字典,就是用哈希表實現的。

python中的這些東西都是哈希原理:字典(dictionary)、集合(set)、計數器(counter)、默認字典Defaut dict)、有序字典(Order dict).

 

我來嘗試在不使用字典的情況下實現哈希表結構,我們需要定義一個包含 鍵->值 映射 的數據結構,同時實現以下兩種操作:GetAdd

Add方法的原理

比如調用 hashMap.Add("apple", 0) ,插入一個Key為"apple"的元素。這時候我們需要利用一個哈希函數來確定Entry的插入位置(index):

Get方法的原理

Get方法根據Key來查找Value的時候, 首先會把輸入的Key做一次Hash映射,得到對應的index:

index =  Hash(“apple”)

 

一種簡單的實現方法

建立一個線性表,使用元組來實現 key-value 的映射關系

""" 
線性表結構
"""
class LinearMap(object):

    def __init__(self):
         self.items = []
     
    # 往表中添加元素    
    def add(self, k, v):  
        self.items.append((k,v))

    # 線性方式查找元素
    def get(self, k): 
        for key, value in self.items:      
            if key == k:      # 鍵存在,返回值,否則拋出異常
                return value
        raise KeyError

'''
我們可以在使用add添加元素時讓items列表保持有序,而在使用get時采取二分查找方式,時間復雜度為O(log n)。 
然而往列表中插入一個新元素實際上是一個線性操作,所以這種方法並非最好的方法。
同時,我們仍然沒有達到常數查找時間的要求。
'''

改進版本:

盡管get操作的增長依然是線性,但BetterMap類使得我們離哈希表更近一步

'''
將總查詢表分割為若干段較小的列表,比如100個子段。
通過hash函數求出某個鍵的哈希值,再通過計算,得到往哪個子段中添加或查找。
相對於從頭開始搜索列表,時間會極大的縮短。
'''
class BetterMap(object):
      #利用LinearMap對象作為子表,建立更快的查詢表
    def __init__(self,n=100):
        self.maps = []          # 總表格
        for i in range(n):      # 根據n的大小建立n個空的子表
            self.maps.append(LinearMap())
      
    def find_map(self,k):       # 通過hash函數計算索引值
        index = hash(k) % len(self.maps)
        return self.maps[index] # 返回索引子表的引用     
 
     # 尋找合適的子表(linearMap對象),進行添加和查找
    def add(self, k, v):
        m = self.find_map(k)        
        m.add(k,v)
     
    def get(self, k):
        m = self.find_map(k)
        return m.get(k)

由於每個鍵的hash值必然不同,所以對hash值取余的值基本也是不同的;

當n=100時, BetterMap的查找速度大約是LinearMap的100倍。

Hashtable的實現

class HashMap(object):
    def __init__(self):
        # 初始化總表為,容量為2的表格(含兩個子表)
        self.maps = BetterMap(2)
        self.num = 0        # 表中數據個數
      
    def get(self,k):        
        return self.maps.get(k)
      
    def add(self, k, v):
        # 若當前元素數量達到臨界值(子表總數)時,進行重排操作
        # 對總表進行擴張,增加子表的個數為當前元素個數的兩倍!
        if self.num == len(self.maps.maps): 
            self.resize()
         
        # 往重排過后的 self.map 添加新的元素
        self.maps.add(k, v)
        self.num += 1
         
    def resize(self):
        #重排操作,添加新表, 注意重排需要線性的時間
        # 先建立一個新的表,子表數 = 2 * 元素個數
        new_maps = BetterMap(self.num * 2)
         
        for m in self.maps.maps:  # 檢索每個舊的子表
            for k,v in m.items:   # 將子表的元素復制到新子表
                new_maps.add(k, v)
         
        self.maps = new_maps      # 令當前的表為新表

重點關注 add 部分,該函數檢查元素個數與BetterMap的大小,如果相等,則“平均每個LinearMap中的元素個數為1”,然后調用resize方法。

resize創建一個新表,大小為原來的兩倍,然后對舊表中的元素“rehashes 再哈希”一 遍,放到新表中。

resize過程是線性的,聽起來好像很不怎么好,因為我們要求的hashtable具有常數時間。但是,要知道我們並不需要經常進行重排操作,所以add操作在絕大部分時間中都是常數的,偶然出現線性。由於對n個元素進行add操作的總時間與n成比例,所以每次add的平均時間就是一個常數!

假設我們要添加32個元素,過程如下:

1. 由於初始長度為2,前兩次add不需要重排,第1,2次 總時間為 2

2. 第3次add,重排為4,耗時2,第3次時間為 3

3. 第4次add,耗時1    到目前為止,總時間為 6

4. 第5次add,重排為 8,耗時4,第5次時間為5

5. 第6~8次   共耗時3      到目前為止,總時間為 6+5+3 = 14

6. 第9次add,重排16,  耗時8,第9次時間為9

7. 第10~16次,共耗時7, 到目前為止,總時間為 14+9+7 = 30

在32次add后,總時間為62的單位時間,由以上過程可以發現一個規律,在n個元素add之后,當n為2的冪,則當前總單位時間為 2n-2,所以平均add時間絕對小於2單位時間。

當n為2的冪時,為最合適的數量,當n變大之后,平均時間為稍微上升,但重要的是,我們達到了O(1)。

 

學習筆記參考:算法分析哈希表學習


免責聲明!

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



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