python数据结构与算法——哈希表

哈希表 学习笔记

参考翻译自:《复杂性思考》 及对应的online版本:http://greenteapress.com/complexity/html/thinkcomplexity004.htmlhtml

使用哈希表能够进行很是快速的查找操做,查找时间为常数,同时不须要元素排列有序python

python的内建数据类型:字典,就是用哈希表实现的数据结构

 

为了解释哈希表的工做原理,咱们来尝试在不使用字典的状况下实现哈希表结构。app

咱们须要定义一个包含 键->值 映射 的数据结构,同时实现如下两种操做:函数

add(k, v):学习

  Add a new item that maps from key k to value v.测试

  With a Python dictionary,d, this operation is written d[k] = v.this

get(target):spa

  Look up and return the value that corresponds to key target.翻译

  With a Python dictionary, d, this operation is written d[target] or d.get(target).

 

一种简单是实现方法是创建一个线性表,使用元组来实现 key-value 的映射关系

 1 class LinearMap(object):
 2     """ 线性表结构 """
 3     def __init__(self):
 4         self.items = []
 5     
 6     def add(self, k, v):    # 往表中添加元素
 7         self.items.append((k,v))
 8     
 9     def get(self, k):       # 线性方式查找元素
10         for key, val in self.items:
11             if key==k:      # 键存在,返回值,不然抛出异常
12                 return val
13         raise KeyError

咱们能够在使用add添加元素时让items列表保持有序,而在使用get时采起二分查找方式,时间复杂度为O(log n)。 然而往列表中插入一个新元素其实是一个线性操做,因此这种方法并不是最好的方法。同时,咱们仍然没有达到常数查找时间的要求。

 

咱们能够作如下改进,将总查询表分割为若干段较小的列表,好比100个子段。经过hash函数求出某个键的哈希值,再经过计算,获得往哪一个子段中添加或查找。相对于从头开始搜索列表,时间会极大的缩短。尽管get操做的增加依然是线性,但BetterMap类使得咱们离哈希表更近一步:

 1 class BetterMap(object):
 2     """ 利用LinearMap对象做为子表,创建更快的查询表 """
 3     def __init__(self,n=100):
 4         self.maps = []          # 总表格
 5         for i in range(n):      # 根据n的大小创建n个空的子表
 6             self.maps.append(LinearMap())
 7     
 8     def find_map(self,k):       # 经过hash函数计算索引值
 9         index = hash(k) % len(self.maps)
10         return self.maps[index] # 返回索引子表的引用     
11 
12     # 寻找合适的子表(linearMap对象),进行添加和查找
13     def add(self, k, v):
14         m = self.find_map(k)        
15         m.add(k,v)
16     
17     def get(self, k):
18         m = self.find_map(k)
19         return m.get(k)

测试一下:

 1 if __name__=="__main__":
 2     table = BetterMap()
 3     pricedata = [("Hohner257",257),
 4                  ("SW1664",280),
 5                  ("SCX64",1090),
 6                  ("SCX48",830),
 7                  ("Super64",2238),
 8                  ("CX12",1130),
 9                  ("Hohner270",620),
10                  ("F64C",9720),
11                  ("S48",1988)]
12     
13     for item, price in pricedata:
14         table.add(k=item, v=price)
15     
16     print table.get("CX12")
17     # >>> 1130
18     print table.get("QIMEI1248")
19     # >>> raise KeyError

因为每一个键的hash值必然不一样,因此对hash值取余的值基本也是不一样的。

当n=100时, BetterMap的查找速度大约是LinearMap的100倍。

 

明显,BetterMap的查找速度受到参数n的限制,同时其中每一个LinearMap的长度不固定,使得子段中的元素依然是线性查找。若是,咱们可以限制每一个子段的最大长度,这样在单个子段中查找的时间负责度就有一个固定上限,则LinearMap.get方法的时间复杂度就成为了一个常数。由此,咱们仅仅须要跟踪元素的数量,每当某个LinearMap中的元素数量超过阈值时, 对整个hashtable进行重排,同时增长更多的LinearMap,这样子就能够保证查找操做为一个常数啦。

如下是hashtable的实现:

 1 class HashMap(object):
 2     def __init__(self):
 3         # 初始化总表为,容量为2的表格(含两个子表)
 4         self.maps = BetterMap(2)
 5         self.num = 0        # 表中数据个数
 6     
 7     def get(self,k):        
 8         return self.maps.get(k)
 9     
10     def add(self, k, v):
11         # 若当前元素数量达到临界值(子表总数)时,进行重排操做
12         # 对总表进行扩张,增长子表的个数为当前元素个数的两倍!
13         if self.num == len(self.maps.maps): 
14             self.resize()
15         
16         # 往重排事后的 self.map 添加新的元素
17         self.maps.add(k, v)
18         self.num += 1
19         
20     def resize(self):
21         """ 重排操做,添加新表, 注意重排须要线性的时间 """
22         # 先创建一个新的表,子表数 = 2 * 元素个数
23         new_maps = BetterMap(self.num * 2)
24         
25         for m in self.maps.maps:  # 检索每一个旧的子表
26             for k,v in m.items:   # 将子表的元素复制到新子表
27                 new_maps.add(k, v)
28         
29         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)。

相关文章
相关标签/搜索