Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。
百度百科给出的散列算法的解释如上,个人理解散列算法就是给个输入通过散列算法后输出得道固定长度的输出。
输入—>散列算法—>输出(固定长度)
第一个接触的散列算法应该是MD5吧…学JavaWeb的时候做密码加密。虽然现在已经不推荐使用了。
0.散列算法的特点
a.固定长度
b.不可逆
c.高速存储“空间换时间”
…先这些想到再加
1.常用散列算法
MD5,SHA这种的就不列了,我们说点基础的…
a.除余法
除余法就是用关键码a % M,并取余数作为散列地址。除余法几乎是最简单的散列方法,散列函数为: h(x) = x % M。
//取余法
public int remainderHash(int a)
{
int b = 0;
b = a % 10;
return b;
}
b.乘法
这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效果并不好)
static int bernsteinHash(String key)
{
int hash = 0;
int i;
for (i=0; i<key.length(); ++i) {
hash = 33 * hash + key.charAt(i);
}
return hash;
}
jdk5.0里面的String类的hashCode()方法也使用乘法Hash。不过,它使用的乘数是31。推荐的乘数还有:131, 1313, 13131, 131313等等。(上面这个不是很典型…返回值长度可能不一致,理解意思)写个一样长的。
static int RSHash(String str)
{
int b = 378551;
int a = 63689;
int hash = 0;
for(int i = 0; i < str.length(); i++)
{
hash = hash * a + str.charAt(i);
a = a * b;
}
return (hash & 0x7FFFFFFF);
}
c.平方取中法
由于整数相除的运行速度通常比相乘要慢,所以有意识地避免使用除余法运算可以提高散列算法的运行时间。平方取中法的具体实现是:先通过求关键码的平方值,从而扩大相近数的差别,然后根据表长度取中间的几位数(往往取二进制的比特位)作为散列函数值。因为一个乘积的中间几位数与乘数的每一数位都相关,所以由此产生的散列地址较为均匀。
d. 直接寻址法
取keyword或keyword的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,当中a和b为常数(这样的散列函数叫做自身函数)。
e. 数字分析法
分析一组数据,比方一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体同样,这种话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
f.折叠法
将keyword切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加和(去除进位)作为散列地址。
2.hashCode与equals
对hashCode重写就必须重写equals,这事必须的…
我们看看String类中
String中hashCode与equals
hashCode
以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模,达到了目的——只要字符串的内容相同,返回的哈希码也相同。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];//看到了上面提到了31
}
hash = h;
}
return h;
}
equals
equals方法包含了"==",双等号比较的是地址,存储地址相同,内容则相同。当地址不同的时候,先验证了比较对象是否为String,接着比较了两个字符串的长度,最后才循环比较每个字符是否相等。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
不得不说人这JDK写的是好…
3.Hash冲突
比如用余数法上面我给出的A%10的例子中,1,11,21…的hash值都一样,这就造成了hash冲突,在算法上要尽量避免hash冲突,但是毕竟固定长度…出现冲突在所难免,所以需要对此解决。
a.开放地址
b.再哈希
c.链地址
d.建立公共溢出区
a.开放地址:开放地址法处理冲突的基本原则就是出现冲突后按照一定算法查找一个空位置存放。
1)线性探测再散列,即依次向后查找。
2)二次探测再散列,即依次向前后查找,增量为1、2、3的二次方。
3)伪随机探测再散列,伪随机,顾名思义就是随机产生一个增量位移。
b.再哈希:出现冲突后采用其他的哈希函数计算,直到不再冲突为止。
c.链地址:链接地址法不同与前两种方法,他是在出现冲突的地方存储一个链表,所有的同义词记录都存在其中。形象点说就行像是在出现冲突的地方直接把后续的值摞上去。
这也是HashMap中的解决办法(JDK8中当链表过长(>8)就会变成红黑树)
d.建立公共溢出区
设哈希函数的值域是[1,m-1],则设向量HashTable[0…m-1]为基本表,每个分量存放一个记录,另外设向量OverTable[0…v]为溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。
参考:https://www.jianshu.com/p/f9239c9377c5
https://segmentfault.com/a/1190000012201011