hash算法小见解
作者:互联网
hash算法的作用一是满足快速存取,并根据关键字key快速查找元素;二是用于加密明文,因为其有着良好的不可逆性,在密码学中有着十分充分的应用,网上有很多关于hash算法这方面的说法,这里就不一一列举了,本文着重讲hash算法的简单应用,即如何实现快速存取并查找,实现时需要注意什么。
首先想要满足快速存取,我们得先理解检索这个概念。这里用我上学时书上有关检索的概念:
在一组记录集合中找到关键码值等于给定值的某个记录,或者找到关键码值符合特定条件的某些记录的过程。
所以对记录集合进行检索这个操作,一般也分为两种种常见的方法:对于线性表的检索以及散列技术。
线性表的检索其实就是基于某种线性关系而排列成的记录集合的查找,我们最常见的就是对数组的顺序检索,即从头到尾的查看;也有二分查找,即对从小到大或从大到小的线性表进行折半查找;还有分块检索,是线性检索和二分检索的折衷,将表按块分好,其实是缩小检索区间的过程,如何分类这里不再赘述。
散列技术则是本文的重点。
说到散列技术,就不得不说散列表,前面的线性表不知道你们有咩有发现,线性表的关键值和数据在表中的位置,没有一个确定的对应关系,也就是下标就是下标,它和关键字没有关系,下标只是代表他在线性表中的位置,0代表线性表中的第一个位置,1代表线性表中的第二个位置……
而散列技术则把关键值和数据在表中的位置通过一个散列函数对应起来,因此给你一个关键字,你可以通过散列函数算出散列值,然后散列值就是这个关键字在散列表中的位置所在。
数学好的同学可能发现,这有点类似于数学的f(x)映射关系,事实也的确是这样的。
一个优秀的散列函数,一定有以下几个特点:
1、正向快速:给定关键字和散列函数,有限时间和资源内能计算出散列(hash)值
2、逆向困难:只给定hash值,你很难逆向算出关键字。
3、输入敏感:原始输入信息修改一点点,产生的hash值也能有较大的不同。
4、冲突避免:你很难找到两个不同的关键字算出的散列值是相同的(发生冲突)。
目前有有很多的著名的散列算法,例如SHA系列函数,网上很多,这里也不说了(没他们说的好哈哈)。
常见的构造散列函数的办法有:1. 除余法 2. 乘余取整法 3. 平方取中法4.数字分析法 5. 基数转换法 6. 折叠法 7. 随机数法
1、除余法
设定散列函数为: hash(key) = key MOD p 其中, p≤m (表长) 并且 p 应为不大于 m 的素数 或是 不含 20 以下的质因子
2、乘余取整法
先让关键码key乘上一个常数A,提取乘积的小数部分再乘以n并向下取整,结果作为散列的地址。
hash(key)=n*(A*key%1)=n*[(A*key)-int(A*key)]
Knuth认为,A可以为任意值,与待排序的数据特征有关。A一般取黄金分割比数0.6180339巴拉巴拉为最理想。
若地址空间为P位,则取n=2p
3、平方取中法
以关键码的平方值的中间几位作为存储地址。求key值的平方是扩大差别,同时平方值的中间各位又能受到整个key值各位的影响。此方法适用于关键码每一位数字都有某些数字出现频率很高的现象。
4、数字分析法
假设关键码集合中的每个关键码都是 由 s 位数字组成 (u1 , u2 , …, us ),分析关键字集中的全体, 并从中提取分布均匀 的若干位或它们的组合作为地址。
此方法适合能预先估计出全体关键码的每一位上 各种数字出现的频度。
5、基数转换法
把关键码看成是另一进制上的数 后 再把它转换成原来进制上的数
取其中若干位作为散列地址
一般取大于原来基数的数作为转换的基数,并且两个基数要互素
6、折叠法
将关键码分割成若干部分,然后取它 们的叠加和为散列地址。有两种叠加处 理的方法:移位叠加和分界叠加。
此方法适合于关键码数字位数特别多的。
• 移位叠加—把各部分的最后一位对齐相 加
• 分界叠加—各部分不折断,沿各部分的 分界来回折叠,然后对齐相加,将相加 的结果当做散列地址
7、随机数法
设定散列函数为: hash(key) = Random(key) 其中,Random 为伪随机函数 通常,此方法用于对长度不等的关键 码构造散列函数。
如果你放入的X值,求出的f(x)值相同怎么办?你可能会说你设计的散列函数根本不会产生重复的值,但事实往往事与愿违,现实生活中应用于散列技术时往往要考虑很多因素,这也就造成了你设计的散列函数一般会算出重复的值,而算出重复的值有个专门的说法,叫做冲突。因此,处理冲突,也是散列技术的重要部分。
为产生冲突的地址寻找下一个散列地址
1. 开散列法(拉链法):把发生 冲突的关键码存储在散列表主表 之外,将所有散列地址相同的记录 (同义词 )都链接在同一链表 中。
• 动态申请同义词的空间,适合于 内存操 作
• 如果整个散列表存储在内存中,开散列 方法比较容易实现
• 如果散列表存储在磁盘中,用开散列不 太合适: 一个同义词表中的元素可能 存储在不同的磁盘页中 这就会导致在 检索一个特定关键码值时引起多次磁盘 访问,从而增加了检索时间
2. 闭散列法(开地址法) :把 发生冲突的关键码存储在表中 另一 个槽内
为产生冲突的地址 H(key) 求得一个 地址序列:H0 , H1 , H2 , …, Hs 1≤ s≤m-1 其中:H0 = H(key)(基位置 ) Hi = ( H(key) + di ) MOD m i=1, 2, …, s
对增量 di 有四种取法:
• 1) 线性探查法(线性探测再散列)di = c*i 探查函数是p( K, i) = i*c 最简单的情况 c=1 其他情况则必须使常数c与m互素
特点:
1、产生“聚集”(clustering,或称为“堆 积”或基本聚集) :散列地址不同的结 点,争夺同一后继散列地址
2、 小的聚集可能汇合成大的聚集
3、 导致很长的探查序列
4、在理想情况下,表中的每个空槽都应该 有相同的机会接收下一个要插入的记 录。
• 2)二次探查法(平方探测再散列) di = 12 , -12 , 22 , -22 , …, p( K,2 i-1) = i*i p( K,2 i) = - i*i
• 3)随机探查法(随机探测再散列) di 是一组伪随机数列 p(K,i) = perm[i - 1]
两个方法的特点:
1、 能消除基本聚集(基本聚集 :基地址不同 的关键码,其探查序列的某些段重叠在 一起)
2、产生二级聚集:如果两个关键码散列到同 一个基地址,还是得到同样的探查序列, 所产生的聚集.
原因是探查序列只是基地址的函数,而不 是原来关键码值的函数
• 4)双散列探查法(双散列函数探测) di=i×H2 (key) p(K, i) = i * H2 (key)
1、避免二级聚集
2、探查序列是原来关键码值的函数
3、而不仅仅是基位置的函数 • h2 (key) 必须与m互素, 使发生冲突的同义词地址均匀地分布 在整个表中,否则可能造成同义词地址的循环计算
• 双散列的优点: 不易产生“聚集” • 缺点:计算量增大
注意:增量 di应具有“完备性”
产生的Hi均不相同,且产生的s(m-1)Hi值能覆盖哈希表中所有地址。则必须要求:1、二次探查时的表长m必为形如4j+3的素数。2、随即探查时的m和di没用公因子。
也就是总结以上内容:
散列技术包含设计良好的散列表(散列表的大小,使用什么数据结构),设计巧妙的散列函数,冲突的处理办法。
而散列技术经过多年的发展以及各个领域的应用的逐渐分化,真的不是一两句话就能概括完的,所以这里回到我写这篇博客的初衷——解决一道十分简单的算法题
leetcode第一题:
1. 两数之和
给定一个整数数组nums
和一个目标值 target
,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
这道题有两个解决方案:1、暴力法:穷举所有可能,复杂度为O(n2)
2、hash算法,遍历数组一次,时间复杂度为O(n)
这里只讲hash算法。如果用hash算法,思路就是遍历数组,对每个数进行如下步骤:
1、ex=target-当前数。
2、查看hash集合中是否包含ex值,如果包含则当前数和ex就为答案,输出他们的下标;如果不包含,则把当前数放入到hash集合中。
思路并不难,如果用C++自带的map集合,很快就算出了答案,但是作为新手且好学的我,想自己尝试一下自己模拟该过程
我的思路是这样的,并不完美:
因为这个题目是给整数,而整数并无规律且是随机,也就是说面向的是整个整数范围,但是正常的数组无法开出这么大空间(leetcode上开1000000数组就报错了),所以冲突无法避免,而且整数中包含负数,负数经过散列函数的计算后要变为正数,因此我的散列函数设计如下:
int hascode(int key) { key=really(key); float result=0.618*key-int(key*0.618); return int(result*100); }
really函数是取反函数,保证每个值都是正的。即-3和+3的hash值是相等的,是同义词,必然会发生冲突。
我这里冲突的解决办法是采用开散列的办法,即散列表每一项对应一个链表,链表后顺序查找,虽然效率随着链表的长度变长而下降,但是易实现。
最终代码如下:
class key_value{ public: int data,index; key_value(int data,int index):data(data),index(index){} }; class hashtable{ public: vector<key_value> open; bool flag; hashtable():flag(false){} int look(int target) { for(int a=0;a<open.size();a++) if(open[a].data==target) return a; return -1; } }; class Solution { public: int really(int x) { if(x<0) return x*-1; return x; } int hascode(int key) { key=really(key); float result=0.618*key-int(key*0.618); return int(result*100); } vector<int> twoSum(vector<int>& nums, int target) { vector<int> result; hashtable hash[100]; for(int a=0;a<nums.size();a++) { int cal=hascode(target-nums[a]); if(hash[cal].flag==false)//说明散列值对应元素为空,执行步骤2 { hash[hascode(nums[a])].flag=true; key_value ex(nums[a],a); hash[hascode(nums[a])].open.push_back(ex); } else { int ex=hash[cal].look(target-nums[a]); if(ex!=-1)//说明找到符合条件的两个值,输出答案。 { result.push_back(hash[cal].open[ex].index); result.push_back(a); break; } else//说明链表中无对应的值,执行步骤2 { hash[hascode(nums[a])].flag=true; key_value ex(nums[a],a); hash[hascode(nums[a])].open.push_back(ex); } } } return result; } };
标签:见解,hash,函数,关键码,地址,算法,key,散列 来源: https://www.cnblogs.com/tanyui/p/14191064.html