一、 哈希函数
out = f(in)
1. 哈希函数的特点
(1)输入域是无穷的,输出域是相对有限的
举例:
输入域无穷:可以接收任意长度的字符串
输出域有限:比如一个S域 (输出域很大但一定是有穷尽的)
经典算法:
MD5算法 返回值为 0~2^64-1
SHa1算法 返回值 0~2^128-1
种子码: ea089d31f 这是一个16进制的数
每一位的范围是 09+af 16个状态
MD5算法和SHA1算法返回的是某一个字符串,字符串的每一位字符有16种情况, 代表一个16进制的数,这个16进制数的范围,如果是MD5,那么这个16进制数的范围在 0~2^64-1, 如果是SHa1,那么这个16进制数的范围在 0~2^128-1 , 对于md5码来说他会返回一个长度为16的字符串,一个位表示16种状态,正好是264(=1616);如果是SHA1返回的字符串,字符串长度就是32,每一位有16种状态,一共是2128(=1632)种情况
(2) 一个哈希函数,如果相同的输入参数,一定会返回相同的输出值
- same in ==> same out
- 哈希函数内部没有任何随机的成分
(3)由于输入是无限的,输出是有限的,则会有不同的输入对应相同的输出。
- (dif in ==> same out) 哈希碰撞 产生的原因
(4) 离散性、均匀性
2. 实例应用上基本原理
in1 --f--> out1 --%m--> m1
in2 --f--> out2 --%m--> m2
in3 --f--> out3 --%m--> m3
in4 --f--> out4 --%m--> m4
m的范围是0~m-1之间
out在S范围内,S范围太大了,模m可以将范围缩小
3.哈希函数的使用示例
题目描述:
有一个大文件,这个文件里面都是无符号的整数,每个整数的范围0~2^32-1 (== 0~42亿);
这样的数在这个大文件中有40亿个
如果只给你1G的内存,返回出现次数最多的数是哪一个?
传统使用哈希表解决该问题:
设计一个哈希表,key(int)是大文件中的某一个数,value(int)表示这个数出现的次数
用1G内存设计一个哈希表,统计每个数出现的次数,最后返回
特殊情况预测:
假设大文件中40亿个数都不一样,那么哈希表需要存储40亿条记录,每条记录(两个int,(key,value))是8个字节
最差情况需要 320亿个字节的存储空间, 即32GB >> 1GB
所以该种设计存在内存爆掉的情况
特点分析:
- 我们发现哈希表的使用只和输入的数的种类有关,相同的数多次出现,不会产生很大的空间占用,属于一条记录,只需要修改这条记录的value值即可;
- 当不同的数出现次数多时,内存空间会被占用
使用哈希函数的解法:
有40亿个数,a1,a2,a3,a4……an
然后给每一个数调用一下哈希函数,产生一个输出, 即每个数的哈希值 为 b1,b2,b3……bn
然后给每一个数模100,得到 m1,m2,m3……mn
通过此过程可以知道:取模后的数m在0~99之间
这个过程可以看做将一个大文件中的内容,分配到一些小文件里面去
假设a1这个数调用哈希函数,得到的函数值模上100,得到m1=17,那么我们将a1分配在17号文件上
同理
假设a2这个数调用哈希函数,得到的函数值模上100,得到m2=3,那么我们将a2分配在3号文件上
…… …… ……
根据刚在的分析,假设不同的数有40亿种,依据哈希函数的性质,我们可以认为0~99号文件中,每个文件含有不同种类的原始的数。
每个小文件中,原始数的种类数是差不多的(由哈希函数的离散性和均分性可得),可能不是正好均分,但是也差不多。
根据哈希函数的特性:
相同的数一定会发送到相同的小文件中,此过程将不同种类的数几乎均匀分配到100个小文件中去
接下来,对于每一个小文件,使用哈希表进行统计
此时,每一个小文件最多使用的存储空间是 32GB/100, 这样内存是不会爆的
这样下来,每一个小文件中会出现一个次数出现最多的数, (相同的数肯定只出现在1个文件中)
对每个小文件出现次数最多的数进行统计,然后将这个小文件的内存空间释放掉,统计下一个小文件的情况。
我们将统计出的每个小文件中出现次数最多的数再进行比较,最终会得到一个出现次数最多的数。
总结:
利用哈希函数,让我们在数的种类上做到均分,然后我们再对每一个小文件进行分别的统计,统计完一个小文件,我们将小文件的内存释放掉,去统计下一个小文件,周而复始,我们将会统计完,最后通过比较,输出出现次数最多的数
二、哈希表的实现
具体的实例
题目
设计一种结构,在该结构中有如下三个功能:
insert(key):将某个key加入到该结构,做到不重复加入
delete(key):将原本结构中的某个key移除
getRndom():等概率随机返回结构中的任何一个key
要求:insert\delete\getRandom方法的时间复杂度都是O(1)
分析与实现
map1 字符串到index对应的一张表
map2 是字符串到index对应的一张表
正着查找和反着查找都可以
int size=0 代表表的长度
- insert方法的思想
假设现在有一个字符串‘A’
在map1中存放 “A” - 0
在map2中存放 0 - “A”
size = 1
还有一个字符串"B"
map1 “B” - 1
map2 1 - “B”
size = 2
- delete方法思想
关键:为了保证index区域额连续性,还需要用最后一条记录去填补删除后的这条记录留下的“漏洞”
假设这时候要删除一个字符串str1,要将它进行删除
(1)对map1的操作
step1:在map1中我们可以找到 str1对应的index1,
step2:删除这条记录
step3:根据当前的size,将最后一条记录,也就是最后一个字符串对应的index值变为index1
step4:返回index1
(2)对map2的操作
step1:根据(1)返回的index1,在map2中找到一条记录进行删除;
step2:根据size找到最后一条记录,将其对应的字符串同index1组成一条新的记录放置map2表中
step3:删除map2表中的最后一条记录
(3)size-1
!!!保证了index区间的连续性,在查找和随机返回时提供便利
- getRandom方法思想
随机等概率返回一个key
(1)没有进行删除操作的情况下
根据size可以知道map中现在存在多少个字符串,而且在没有进行删除操作的情况下,
这些字符串对应的下标index是连续的,所以size为多少,就有几个index
根据随机数生成函数,等概率生成0~size-1之间的一个数
假设随机生成的数是17(<size),那么就将map2中key=17位置上的value返回
这样以来,只要用系统函数生成的这些数是等概率随机的,用户返回的字符串就是随机的
时间复杂度为O(1)
(2)有删除行为的情况下
删除后0~size-1这个区间上会少很多的数(出现很多的漏洞),就是一个不连续的区间了,
假设在这种情况下进行等概率数的返回
随机数生成函数生成一个17,我们拿着17在map2中寻找,发现没有,
就说明这个位置上的字符串已经被删除了
那么我们又需要继续生成一个随机数,继续查找……直到找到,我们需要尝试很多次
这样的一个过程是不符合题目中“随机等概率”返回一个数,这样一个要求的
所以我们必须保证删除的时候,别让index区域上出现“漏洞”(不连续)
保证index区域永远是连续的
java语言实现过程
import java.util.HashMap;
public class class02 {
public static class Pool<K>{
private HashMap<K, Integer> keyIndexMap;
private HashMap<Integer, K> indexKeyMap;
private int size;
public Pool(){
this.keyIndexMap = new HashMap<K, Integer>();
this.indexKeyMap = new HashMap<Integer, K>();
this.size = 0;
}
public void insert(K key) {
if(!this.keyIndexMap.containsKey(key)){ //先要判断一下有没有加过,加过了就不加了
this.keyIndexMap.put(key,this.size);
this.indexKeyMap.put(this.size++, key); //size++是后++!!!
}
}
public void delete(K key) {
if(this.keyIndexMap.containsKey(key)){
// key是要删除的字符串
int deleteIndex = this.keyIndexMap.get(key); //保存当前要删除的index值
int lastIndex = --this.size; //最后一条记录的index值
K lastKey = this.indexKeyMap.get(lastIndex); //最后一条记录的key指
this.keyIndexMap.put(lastKey, deleteIndex); //将map1最后一条记录的索引值变为要删除的记录的索引值
this.indexKeyMap.put(deleteIndex, lastKey); //将map2中要删除的那一条记录所对应的字符串变成最后一条记录对应的字符串
this.keyIndexMap.remove(key); //在map1中删除key所对应的索引值
this.indexKeyMap.remove(lastIndex); //在map2中删除最后一条记录
}
}
public K getRandom(){
if(this.size==0){
return null;
}
int randomIndex = (int)(Math.random()*this.size); //做到0~size-1上的等概率
return this.indexKeyMap.get(randomIndex);
}
}
public static void main(String[] args) {
Pool<String> pool = new Pool<String>();
pool.insert("zuo");
pool.insert("cheng");
pool.insert("yun");
System.out.println(pool.getRandom());
pool.delete("zuo");
System.out.println(pool.getRandom());
}
}