文章目录
前言:python
以前,咱们前后学习了线性表、数组、字符串和树,它们广泛都存在这样的缺陷,那就是数据数值条件的查找,都须要对所有数据或者部分数据进行遍历。那么,有没有一种方法能够省去数据比较的过程,从而进一步提高数值条件查找的效率呢?数组
答案固然是:有!这一课时咱们就来介绍这样一种高效率的查找神器:哈希表。bash
1、什么是哈希表
哈希表名字源于 Hash,也能够叫做散列表。哈希表是一种特殊的数据结构,它与数组、链表以及树等咱们以前学过的数据结构相比,有很明显的区别。数据结构
1.1 哈希表的原理
哈希表是一种数据结构,它使用哈希函数组织数据,以支持快速插入和搜索。哈希表的核心思想就是使用哈希函数将键映射到存储桶。更确切地说:app
- 当咱们插入一个新的键时,哈希函数将决定该键应该分配到哪一个桶中,并将该键存储在相应的桶中;
- 当咱们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。
下面举一个简单的例子,咱们来理解下:less
在示例中,咱们使用 y = x % 5 做为哈希函数。让咱们使用这个例子来完成插入和搜索策略:
函数
- 插入:咱们经过哈希函数解析键,将它们映射到相应的桶中。 例如,1987 分配给桶 2,而 24 分配给桶 4。
- 搜索:咱们经过相同的哈希函数解析键,并仅在特定存储桶中搜索。 例如,若是咱们搜索 23,将映射 23 到 3,并在桶 3 中搜索。咱们发现 23 不在桶 3 中,这意味着 23 不在哈希表中。
1.2 设计哈希函数
哈希函数是哈希表中最重要的组件,该哈希表用于将键映射到特定的桶。在以前的示例中,咱们使用 y = x % 5 做为散列函数,其中 x 是键值,y 是分配的桶的索引。学习
散列函数将取决于键值的范围和桶的数量。下面是一些哈希函数的示例:
哈希函数的设计是一个开放的问题。其思想是尽量将键分配到桶中,理想状况下,完美的哈希函数将是键和桶之间的一对一映射。然而,在大多数状况下,哈希函数并不完美,它须要在桶的数量和桶的容量之间进行权衡。
this
固然,咱们也能够自定义一些哈希函数。通常的方法有:spa
- 直接定制法。哈希函数为关键字到地址的线性函数。如,H (key) = a * key + b。 这里,a 和 b 是设置好的常数。
- 数字分析法。假设关键字集合中的每一个关键字 key 都是由 s 位数字组成(k1,k2,…,Ks),并从中提取分布均匀的若干位组成哈希地址。
- 平方取中法。若是关键字的每一位都有某些数字重复出现,而且频率很高,咱们就能够先求关键字的平方值,经过平方扩大差别,而后取中间几位做为最终存储地址。
- 折叠法。若是关键字的位数不少,能够将关键字分割为几个等长的部分,取它们的叠加和的值(舍去进位)做为哈希地址。
- 除留余数法。预先设置一个数 p,而后对关键字进行取余运算。即地址为 key % p。
2、解决哈希冲突
理想状况下,若是咱们的哈希函数是完美的一对一映射,咱们将不须要处理冲突。不幸的是,在大多数状况下,冲突几乎是不可避免的。例如,在咱们以前的哈希函数(y = x % 5)中,1987 和 2 都分配给了桶 2,这就是一个哈希冲突。
解决哈希冲突应该要思考如下几个问题:
- 如何组织在同一个桶中的值?
- 若是为同一个桶分配了太多的值,该怎么办?
- 如何在特定的桶中搜索目标值?
那么一旦发生冲突,咱们该如何解决呢?
经常使用的方法有两种:开放定址法和链地址法。
2.1 开放定址法
即当一个关键字和另外一个关键字发生冲突时,使用某种探测技术在哈希表中造成一个探测序列,而后沿着这个探测序列依次查找下去。当碰到一个空的单元时,则插入其中。
经常使用的探测方法是线性探测法。 好比有一组关键字 {12,13,25,23},采用的哈希函数为 key % 11。当插入 12,13,25 时能够直接插入,地址分别为 一、二、3。而当插入 23 时,哈希地址为 23 % 11 = 1。
然而,地址 1 已经被占用,所以沿着地址 1 依次往下探测,直到探测到地址 4,发现为空,则将 23 插入其中。以下图所示:
2.2 链地址法
将哈希地址相同的记录存储在一张线性链表中。例如,有一组关键字 {12,13,25,23,38,84,6,91,34},采用的哈希函数为 key % 11。以下图所示:
3、哈希表的应用
3.1 哈希表的基本操做
在不少高级语言中,哈希函数、哈希冲突都已经在底层完成了黑盒化处理,是不须要开发者本身设计的。也就是说,哈希表完成了关键字到地址的映射,能够在常数级时间复杂度内经过关键字查找到数据。
至于实现细节,好比用了哪一个哈希函数,用了什么冲突处理,甚至某个数据记录的哈希地址是多少,都是不须要开发者关注的。接下来,咱们从实际的开发角度,来看一下哈希表对数据的增删查操做。
哈希表中的增长和删除数据操做,不涉及增删后对数据的挪移问题(数组须要考虑),所以处理就能够了。
哈希表查找的细节过程是:对于给定的 key,经过哈希函数计算哈希地址 H (key)。
- 若是哈希地址对应的值为空,则查找不成功。
- 反之,则查找成功。
虽然哈希表查找的细节过程还比较麻烦,但由于一些高级语言的黑盒化处理,开发者并不须要实际去开发底层代码,只要调用相关的函数就能够了。
3.2 哈希表的优缺点
- 优点:它能够提供很是快速的插入-删除-查找操做,不管多少数据,插入和删除值须要接近常量的时间。在查找方面,哈希表的速度比树还要快,基本能够瞬间查找到想要的元素。
- 不足:哈希表中的数据是没有顺序概念的,因此不能以一种固定的方式(好比从小到大)来遍历其中的元素。在数据处理顺序敏感的问题时,选择哈希表并非个好的处理方法。同时,哈希表中的
key 是不容许重复的,在重复性很是高的数据中,哈希表也不是个好的选择。
4、 设计哈希映射
4.1 设计要求
要求:
不使用任何内建的哈希表库设计一个哈希映射具体地说,设计应该包含如下的功能:
- put(key, value):向哈希映射中插入(键,值)的数值对。若是键对应的值已经存在,更新这个值。
- get(key):返回给定的键所对应的值,若是映射中不包含这个键,返回-1。
- remove(key):若是映射中存在这个键,删除这个数值对。
示例:
MyHashMap hashMap = new MyHashMap(); hashMap.put(1, 1); hashMap.put(2, 2); hashMap.get(1); // 返回 1 hashMap.get(3); // 返回 -1 (未找到) hashMap.put(2, 1); // 更新已有的值 hashMap.get(2); // 返回 1 hashMap.remove(2); // 删除键为2的数据 hashMap.get(2); // 返回 -1 (未找到)
注意:
全部的值都在 [0, 1000000]的范围内。 操做的总数目在[1, 10000]范围内。 不要使用内建的哈希库。
4.2 设计思路
哈希表是一个在不一样语言中都有的通用数据结构。例如,Python 中的 dict 、C++中的 map 和 Java 中的 Hashmap。哈希表的特性是能够根据给出的 key 快速访问 value。
最简单的思路就是用模运算做为哈希方法,为了下降哈希碰撞的几率,一般取素数的模,例如 模 2069。
定义 array 数组做为存储空间,经过哈希方法计算数组下标。为了解决 哈希碰撞 (即键值不一样,但映射下标相同),利用桶来存储全部对应的数值。桶能够用数组或链表来实现,在下面的具体实现中, Python 中用的是数组。
定义哈希表方法,get(),put() 和 remove(),其中的寻址过程以下所示:
- 对于一个给定的键值,利用哈希方法生成键值的哈希码,利用哈希码定位存储空间。对于每一个哈希码,都能找到一个桶来存储该键值所对应的数值。
- 在找到一个桶以后,经过遍从来检查该键值对是否已经存在。
4.3 实际案例
Python实现以下:
class Bucket: def __init__(self): self.bucket = [] def get(self, key): for (k, v) in self.bucket: if k == key: return v return -1 def update(self, key, value): found = False for i, kv in enumerate(self.bucket): if key == kv[0]: self.bucket[i] = (key, value) found = True break if not found: self.bucket.append((key, value)) def remove(self, key): for i, kv in enumerate(self.bucket): if key == kv[0]: del self.bucket[i] class MyHashMap(object): def __init__(self): """ Initialize your data structure here. """ # better to be a prime number, less collision self.key_space = 2069 self.hash_table = [Bucket() for i in range(self.key_space)] def put(self, key, value): """ value will always be non-negative. :type key: int :type value: int :rtype: None """ hash_key = key % self.key_space self.hash_table[hash_key].update(key, value) def get(self, key): """ Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key :type key: int :rtype: int """ hash_key = key % self.key_space return self.hash_table[hash_key].get(key) def remove(self, key): """ Removes the mapping of the specified value key if this map contains a mapping for the key :type key: int :rtype: None """ hash_key = key % self.key_space self.hash_table[hash_key].remove(key) # Your MyHashMap object will be instantiated and called as such: # obj = MyHashMap() # obj.put(key,value) # param_2 = obj.get(key) # obj.remove(key)
复杂度分析:
- 时间复杂度:每一个方法的时间复杂度都为 O(N/K),其中 N 为全部可能键值的数量,K 为哈希表中预约义桶的数量,在这里 K 为 2069。这里咱们假设键值是均匀地分布在全部桶中的,桶的平均大小为 N/K,在最坏状况下须要遍历完整个桶,所以时间复杂度为 O(N/K)。
- 空间复杂度:O(K+M),其中 K 为哈希表中预约义桶的数量,M 为哈希表中已插入键值的数量。
今天的分享就到这里啦,但愿对你的学习有所帮助!