Hash
Hash又称为散列,是常用的算法之一。
引例
下面让我们来看一个简单的引例。
给你 N N N个数字,再给出 M M M个数字( N , M < 1 0 5 N,M<10^5 N,M<105)。判断这 M M M个数字中有哪些数字在 N N N个数字中出现过。
看到这个问题我起初的思路比较简单直观,直接对这N个数字进行M次循环不就好了吗,但是很显然,如果这么做的话,时间复杂度将会是 O ( N M ) O(NM) O(NM)。那还有什么好点的办法吗?
不如换个思路想一下,既然时间复杂度这么大,我们可不可以牺牲空间来换时间呢?既然
N
N
N和
M
M
M都小于
1
0
5
10^5
105,我们可以构造一个判断数组:Judge[100001]
,当Judge[x] = 1
时表示这个数字出现过,若Judge[x] = 0
时表示没有出现过。
根据这个思路,我们就可以准确的写出代码:
#include<stdio.h>
int Judge[100001] = { 0 };
int main()
{
int m, n;
scanf_s("%d %d", &n, &m);
//输入N个数字
for (int i = 0; i < n; i++)
{
int num;
scanf_s("%d", &num);
Judge[num] = 1;
}
//输入M个数字并作出判断
for (int j = 0; j < m; j++)
{
int num;
scanf_s("%d", &num);
if (Judge[num])
printf("%d is exist\n", num);
else
printf("%d is not exist\n", num);
}
}
直接将输入的数字作为数组的下标来使用可以大大降低时间的复杂度。但是如果我们输入的并不是一个可取的整数(如超出了int的范围),或者是一段字符串呢?这时候,我们就要来正式介绍一下散列了:
定义
有人将散列归纳为了一句话:将元素通过一个函数转换为整数(又称hash值),使得该整数可以尽量唯一的代表这个元素。这个转换函数就称为散列函数(H)。
说白了就是通过一个函数转化一下,将每个元素都有一个对应的数字标号。
那么现在重点来了:我们要如何定义这个散列函数H,做到每个元素都能有一个独特的编号呢?
散列函数
我们用element
来代表这个元素。
如果element
为整数。常用的散列函数为以下内容:
1)直接定址法。
恒等变换H(element) = element
或者进行线性变换H(element) = a*element + b
2)平方取中法。
取element
的平方的中间若干位作为hash值。
3)除留余数法。
是指将element
除以一个数得到的余数作为hash值,即为H(element) = element % num
看完上述的三种方法,你会不会有一点小疑问?前两种方法还好,但是第三种,会不会有两个整数对应的余数是一样的呢?如果有的话,那我们要怎么处理呢?
其实,这种情况被称为hash冲突,那我们要如何解决这种冲突呢?下面是一些常见的方法:
1)线性探查法。
如果得到的
H
(
e
l
e
m
e
n
t
)
H(element)
H(element)对应的下标已经被其他元素占用了,那么就检查一下
H
(
e
l
e
m
e
n
t
)
+
1
H(element) + 1
H(element)+1是否被占用,没有的话就使用这个位置,如果有,那么就继续往后找直到找到一个空位置。或者进行线性变换
H
(
e
l
e
m
e
n
t
)
=
a
∗
e
l
e
m
e
n
t
+
b
H(element) = a*element + b
H(element)=a∗element+b
2)平方探查法。
如果得到的
H
(
e
l
e
m
e
n
t
)
H(element)
H(element)对应的下标已经被其他元素占用了,那么就依次检查
H
(
e
l
e
m
e
n
t
)
+
1
2
H(element) + 1^2
H(element)+12、
H
(
e
l
e
m
e
n
t
)
−
1
2
H(element) - 1^2
H(element)−12、
H
(
e
l
e
m
e
n
t
)
+
2
2
H(element) + 2^2
H(element)+22、
H
(
e
l
e
m
e
n
t
)
−
2
2
H(element) - 2^2
H(element)−22、
H
(
e
l
e
m
e
n
t
)
+
3
2
H(element) + 3^2
H(element)+32、
H
(
e
l
e
m
e
n
t
)
−
3
2
H(element) - 3^2
H(element)−32是否被占用,没有的话就使用这个位置即可。若超出最大下标可以将得到的下标值对总长度进行取模处理。
3)链地址法。
该方法不需要重新获取hash值,而是将所有hash值相同的
e
l
e
m
e
n
t
element
element链接成一个单链表即可。
如果element
不再是整数。下面我们将着重讨论一下element
为字符串时的情况。
我们先假设字符串中只含有大小写字母。即为a-z和A-Z的内容。
那么我们可以将这52个字母对应到进制当中去,即对应着五十二进制,那么我们就可以将五十二进制转换为十进制。那么求出的十进制也一定是唯一的。代码可以写成下面这种:
int func(char str[], int len)
{
int id = 0;
for (int i = 0; i < len; i++)
{
if (str[i] >= 'A' && str[i] <= 'Z')
id = id * 52 + (str[i] - 'A');
else if (str[i] >= 'a' && str[i] <= 'z')
id = id * 52 + (str[i] - 'a') + 26;
}
return id;
}
上面只考虑了字母的情况,那如果字符串里面也出现了数字怎么办呢?
简单的一个方法就是再加上十个数字,变成六十二进制,再用上面的方法把六十二进制转换为十进制即可。
应用
hash算法的应用也十分的广泛,比如进行安全加密,数据校验等等。有兴趣的话可以多查一些资料,网上有很多相关文献。