从原理到应用分析什么是哈希?
一、什么是哈希?
哈希(hash):将任意长度的输入(关键字),通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值,通常哈希值代表了关键字的存储位置。
但是为什么要这样做呢?或者说,哈希是怎样来的呢?
哈希的出现解决了两个问题:存储和搜索。
1. 存储(数据结构):如果在容器中保存对象及其关联的键,并且不用键来决定 (键/对象) 对的顺序,那就必须对键值釆用其他方式来确定元素在内存中的位置。
如果使用像 string 这样的对象作为键,就会遇到一些问题,可能的变量的数目是巨大的。具有 10 个字符的字母字符串可能的个数是 26^10。这个索引范围没有多大用处。我们需要一种机制来将它变为可接受的范围;而且理想情况下,这个机制可以为每个键生成唯一的值,根据这个值所在的位置来找到对应的字符串。这正是哈希需要做的事情之一。
2. 搜索(算法/思想):当我们处理数据的时候,不外乎使用顺序结构、链表和树结构。但是使用这些结构时,元素的关键字与其存储位置之间通常没有对应的逻辑关系,这时候想要查找一个元素,我们根本不知道它在结构中的存储位置,甚至不知道它在不在结构中,这要求我们必须要不断地比较关键字进行查找,如顺序结构查找的时间是O(N),树结构是O(logN),通常这带来的结果是不断for循环遍历,极大地增加了代码时间复杂度。
那么可不可以有更好的搜索方法呢?哈希函数应运而生:由上面的定义可知,hash完美解决了这一问题,可以不经过任何比较,直接从哈希表/集中得到要搜索的元素或判断是否存在。
二、哈希会出现的问题
哈希有两种缺点:实际缺点和根本缺点。
1. 根本缺点:
哈希表的缺点它是基于数组的,数组创建后难于扩展某些哈希表被基本填满时,性能下降得非常严重,所以程序虽必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。
而且,也没有一种简便的方法可以以任何一种顺序〔例如从小到大〕遍历表中数据项。如果需要这种能力,就只能选择其他数据结构。
然而如果不需要有序遍历数据,井且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。
2. 实际缺点:
哈希冲突:由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突,即不同的关键字得到的值相同。
这样的话我们前面所有的理想情况就成了笑谈,所以处理问题时要解决哈希冲突。
三、构造哈希算法
基本原则:便于计算(函数计算时间<=查找时间);(散列地址)分布均匀
(1)直接定址法
直接定值法是取关键字的某个线性函数值为散列地址,即 f(key) = a*key + b。其优点是简单、均匀,不会产生冲突;但缺点是需要知道关键字的分布情况,希望数值是连续的。
(2)数字分析法
数字分析法通常适合处理关键字位数比较大的情况,例如我们现在要存储外卖信息登记表,如果用手机号作为关键字,那么抽取后面的四位数字作为散列地址是不错的选择。
(3)平方取中法
平方取中法是将关键字平方之后取中间若干位数字作为散列地址。这种方法适用于不知道关键字的分布,且数值的位数又不是很大的情况。
(4)折叠法
折叠法是将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和,并按散列表表长取后几位作为散列地址。
(5)除留余数法
此方法最常用:f(key) = key mod p(p <= n) n:散列表的长度。
p的选择非常非常非常重要。
(6)随机数法
f(key) = random(key)
当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
四、处理哈希冲突
(1)开放定址法
当发生哈希冲突时,寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,处理冲突的过程结束,记录填入的位置:
fi(key) = (f(key)+di) MOD n
di为增量序列,可以有下列三种取法:
1. di = 1,2,...,n-1:线性探测再散列;
2. di = 1^2,-1^2,...,k^2,-k^2(k<=n/2) 二次探测再散列
3. di = 伪随机数序列:伪随机探测再散列
(2)链地址法
(3)公共溢出区法
没有冲突的元素放在基本表,有冲突的元素,将多余的元素放在冲突表。
五、C++ STL 容器:哈希表和哈希集
(1)无序哈希表unordered_map
1. 以键值对(pair类型)的形式存储数据
2. 存储的各个键值对的键互不相同且不允许被修改
3. 不会自行对存储的键值对进行排序
代码如下:
#include <iostream> #include <unordered_map> //哈希表头文件 using namespace std; int main(){ //建立哈希表 unordered_map<Type,Type> hashsmap //第一个Type是键的变量类型,第二个是值得变量类型,hashmap是该哈希表的名称 //插入键值对的两种方法 hashmap.insert(make_pair(key,value)); hashmap[key] = value; //删除键值对 hashset.erase(key) //查询键值 cout<<hashmap[key]<<endl; //搜索键值对 if(hashmap.count(key)>0) cout<<"exist"<<endl; //遍历哈希表 for (auto i = hashmap.begin(); i != hashmap.end(); i++) { cout << "(" << i->first << "," << i->second << ") "<<endl; } /* 其他常用方法 * empty() 若容器为空,返回true; * size() 返回当前容器存有键值对的个数 * at(key) 返回容器中存储的键key对应的值,如果key不存在,则会抛出out_of_range异常 * clear() 清空容器 * find(key) 查找以 key 为键的键值对,如果找到,则返回一个指向该键值对的正向迭代器;反之, * 则返回一个指向容器中最后一个键值对之后位置的迭代器(如果 end() 方法返回的迭代器) */ }
(2)无序哈希集unordered_set
无序哈希集是一种功能简单的哈希结构:
1. 不再以键值对的形式存储数据,而是直接存储数据的值(存储的键值对 键和值相同 所以只存value即可);
2. 容器内部存储的各个元素的值都互不相等,且不能被修改;
3. 不会自行对内部存储的数据进行排序。
代码如下:
#include <iostream> #include <unordered_set> //哈希集头文件 using namespace std; int main(){ //建立哈希集 unordered_set<Type> hashset //Type是哈希集中键的变量类型,hashset是该哈希集的名称 //插入键 hashset.insert(key) //删除键 hashset.erase(key) //搜索键 if(hashset.count(key) > 0) cout<<"exist"<<endl; //遍历哈希集 for (auto i = hashset.begin(); i != hashset.end(); i++) { cout << (*i) << endl; } /* 其他常用方法 * empty() 若容器为空,返回true; * size() 返回当前容器存有元素的个数 * clear() 清空容器 * find(key)查找以值为 key 的元素,如果找到,则返回一个指向该元素的正向迭代器;反之,则返回一 * 个指向容器中最后一个元素之后位置的迭代器(如果 end() 方法返回的迭代器) */ }
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程