python数据结构与算法——哈希表
2018-02-26 15:02
267 查看
python数据结构与算法——哈希表
参考翻译自:《复杂性思考》 及对应的online版本:http://greenteapress.com/complexity/html/thinkcomplexity004.html使用哈希表可以进行非常快速的查找操作,查找时间为常数,同时不需要元素排列有序
python的内建数据类型:字典,就是用哈希表实现的
为了解释哈希表的工作原理,我们来尝试在不使用字典的情况下实现哈希表结构。
我们需要定义一个包含 键->值 映射 的数据结构,同时实现以下两种操作:
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.
get(target):
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 的映射关系
class LinearMap(object): """ 线性表结构 """ def __init__(self): self.items = [] def add(self, k, v): # 往表中添加元素 self.items.append((k,v)) def get(self, k): # 线性方式查找元素 for key, val in self.items: if key==k: # 键存在,返回值,否则抛出异常 return val raise KeyError
我们可以在使用add添加元素时让items列表保持有序,而在使用get时采取二分查找方式,时间复杂度为O(log n)。 然而往列表中插入一个新元素实际上是一个线性操作,所以这种方法并非最好的方法。同时,我们仍然没有达到常数查找时间的要求。
我们可以做以下改进,将总查询表分割为若干段较小的列表,比如100个子段。通过hash函数求出某个键的哈希值,再通过计算,得到往哪个子段中添加或查找。相对于从头开始搜索列表,时间会极大的缩短。尽管get操作的增长依然是线性,但BetterMap类使得我们离哈希表更近一步:
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)
测试一下:
if __name__=="__main__": table = BetterMap() pricedata = [("Hohner257",257), ("SW1664",280), ("SCX64",1090), ("SCX48",830), ("Super64",2238), ("CX12",1130), ("Hohner270",620), ("F64C",9720), ("S48",1988)] for item, price in pricedata: table.add(k=item, v=price) print table.get("CX12") # >>> 1130 print table.get("QIMEI1248") # >>> raise KeyError
由于每个键的hash值必然不同,所以对hash值取余的值基本也是不同的。
当n=100时, BetterMap的查找速度大约是LinearMap的100倍。
明显,BetterMap的查找速度受到参数n的限制,同时其中每个LinearMap的长度不固定,使得子段中的元素依然是线性查找。如果,我们能够限制每个子段的最大长度,这样在单个子段中查找的时间负责度就有一个固定上限,则LinearMap.get方法的时间复杂度就成为了一个常数。由此,我们仅仅需要跟踪元素的数量,每当某个LinearMap中的元素数量超过阈值时, 对整个hashtable进行重排,同时增加更多的LinearMap,这样子就可以保证查找操作为一个常数啦。
以下是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个元素,过程如下:
由于初始长度为2,前两次add不需要重排,第1,2次 总时间为 2
第3次add,重排为4,耗时2,第3次时间为 3
第4次add,耗时1 到目前为止,总时间为 6
第5次add,重排为 8,耗时4,第5次时间为5
第6~8次 共耗时3 到目前为止,总时间为 6+5+3 = 14
第9次add,重排16, 耗时8,第9次时间为9
第10~16次,共耗时7, 到目前为止,总时间为 14+9+7 = 30
在32次add后,总时间为62的单位时间,由以上过程可以发现一个规律,在n个元素add之后,当n为2的幂,则当前总单位时间为 2n-2,所以平均add时间绝对小于2单位时间。
当n为2的幂时,为最合适的数量,当n变大之后,平均时间为稍微上升,但重要的是,我们达到了O(1)。
相关文章推荐
- python数据结构与算法——哈希表
- python数据结构与算法——图的最短路径(Dijkstra算法)
- 数据结构及算法(Python)---队列
- 用Python实现的数据结构与算法:链表
- Python数据结构与算法 7栈的应用之进制转换
- 逆序数_python_算法与数据结构
- python数据结构与算法——链表
- python数据结构与算法——图的最短路径(Bellman-Ford算法)解决负权边
- Python中的数据结构和面向对象设计模式的算法
- python 下的数据结构与算法---6:6大排序算法
- Python数据结构与算法之图的最短路径(Dijkstra算法)完整实例
- Python数据结构与算法之二叉树结构定义与遍历方法详解
- Python数据结构与算法之完全树与最小堆实例
- python算法和数据结构笔记--递归--二分查找
- Python实现各类数据结构和算法---计数排序
- python数据结构与算法——完全树 与 最小/大堆
- python数据结构与算法——字典树
- 【算法与数据结构】哈希表-链地址法
- Python实现各类数据结构和算法---折半查找
- python 下的数据结构与算法---4:线形数据结构,栈,队列,双端队列,列表