散列
导言
- 一定要学会 散列!
- 散列 也是利用了 空间换时间 的思想。
- 下文会提及: 散列、哈希、哈希函数、哈希冲突。
问题
有两个数组 A
、B
,它们的长度都在
[
0
,
1
0
6
]
[0, 10^6]
[0,106] 之间,数值范围都在
[
0
,
1
0
4
]
[0, 10^4]
[0,104]。现在我想知道,数组 B
中的每个元素是否存在于数组 A
。请把对应的结果存放在另外一个数组 C
中。
例子: A = [1 2 3 4 5],B = [1 3 10 5],那么答案 C = [true true false true],因为 B 数组的 1、3、5 都在 A 数组中,而 10 不在。
最直观的就是暴力法了: 把数组 B
的每个元素和数组 A
的所有元素进行比较,就能判断该元素是否存在于数组 A
中了。代码如下:
// 函数作用: 判断 B 中的元素是否存在于 A,并返回结果。
bool* isLiving(int* A,int* B,int lengthA,int lengthB){
bool* C = new bool[lengthB];
for (int i=0;i<lengthB;i++){
// 判断 B[i] 是否存在于数组A
C[i] = false; // 默认不存在
for (int t=0;t<lengthA;t++){
// 存在
if (A[t]==B[i]) C[i] = true;
}
}
return C;
}
的确,上面的代码可以得出正确答案。但是,假如此时数组 A
、B
的长度都为
1
0
6
10^6
106 呢?按照这个代码的计算量,计算机需要花费 27.7 小时才能得出结果!这太久了!
为什么是 27.7 小时,前缀和 这篇博客有计算过 ,这里不再赘述。
那有没有更快的方案呢? 答案是肯定的!我们可以使用 散列,只需 0.1 秒就能解决这个问题。
进入正题
接下来,我们先来构建一个长度为
1
0
4
+
1
10^4 + 1
104+1 的布尔数组 hashTable
,定义如下。
h
a
s
h
T
a
b
l
e
[
n
u
m
b
e
r
]
=
{
t
r
u
e
,
如果 数组 A 中有 number
f
a
l
s
e
,
如果 数组 A 中没 number
hashTable[number] = \begin{cases} true, & \text{如果 数组 A 中有 number} \\ false, & \text{如果 数组 A 中没 number} \end{cases}
hashTable[number]={true,false,如果 数组 A 中有 number如果 数组 A 中没 number
由定义可见,数组 hashTable
的下标就是数组 A
元素的取值。而 hashTable
就叫做 散列。
为什么叫 散列 呢?这是因为它很"零散",看下面的例子。
例子: A = [0 1 5 10000],那么构建出来的 hashTable 如下:
hashTable = [true true false false false true false … true(下标为10000) … false]。
可见,hashTable 的第 3 个 true 和第 4 个 true 隔了很远。现在可以理解"零散"的意思了吧?
以下就是构建 hashTable
的代码。
// A: 源数组
// lengthA: 源数组长度
// maxValue: 源数组的最大取值
// 函数作用: 以数组 A 为基础构建散列数组 hashTable,并返回。
bool* constructHashTale(int* A,int lengthA,int maxValue){
bool* hashTable = new bool[maxValue+1]; // 堆中初始化时,数组元素都为 false
for (int i=0;i<lengthA;i++){
hashTable[A[i]] = true;
}
return hashTable;
}
通过上面的定义及例子,我们可以知道:
假如数组 A
有某个元素 number
,那么一定有
h
a
s
h
T
a
b
l
e
[
n
u
m
e
r
]
=
t
r
u
e
hashTable[numer] = true
hashTable[numer]=true。
于是,借助于 hashTable
,0.1 秒的解题代码如下:
// 函数作用: 判断 B 中的元素是否存在于 A,并返回结果。
bool* isLiving(int* A,int* B,int lengthA,int lengthB){
bool* hashTable = constructHashTale(A,lengthA,10000); // 以 A 为基础构建 hashTable。
bool* C = new bool[lengthB];
for (int i=0;i<lengthB;i++){
// hashTable[B[i]] == true时: 表示 hashTable 中存在元素 B[i],即 数组 A 中存在 B[i]。
// hashTable[B[i]] == false时: 表示 hashTable 中不存在元素 B[i],即 数组 A 中不存在 B[i]。
C[i] = hashTable[B[i]];
}
return C;
}
// A: 源数组
// lengthA: 源数组长度
// maxValue: 源数组的最大取值
// 函数作用: 以数组 A 为基础构建散列数组 hashTable,并返回。
bool* constructHashTale(int* A,int lengthA,int maxValue){
bool* hashTable = new bool[maxValue+1]; // 堆中初始化时,数组元素都为 false
for (int i=0;i<lengthA;i++){
hashTable[A[i]] = true;
}
return hashTable;
}
接下来,我们来说说散列长度的取值问题。
散列长度取值问题
小伙伴可能会疑惑:为什么上面那道题,hashTable
长度取值为
1
0
4
+
1
10^4 + 1
104+1 呢?
这是因为 hashTable
的最大下标 就是 数组 A
的最大取值。而数组 A
的元素取值范围为
[
0
,
1
0
4
]
[0,10^4]
[0,104]。所以,为了不产生越界,我们至少给 hashTable
开辟
1
0
4
+
1
10^4 + 1
104+1 个空间。
那么现在有小伙伴会说,假如数组 A
的取值存在负数呢?
这个问题也很简单,我们只需要在存放时加个 偏移量 就可以了。
例子: 假如数组 A 取值为 [ − 5 , 10 ] [-5, 10] [−5,10],那么在构建 hashTable 时,我们可以让 h a s h T a b l e [ A [ i ] + 5 ] = t r u e hashTable[A[i] + 5] = true hashTable[A[i]+5]=true,5 就是偏移量,这样就能避免负数越界了。
那这时候又有小伙伴会说,计算机的内存是有限的,假如数组最大值为
1
0
10
10^{10}
1010,难道我们还是硬着头皮开
1
0
10
10^{10}
1010 个空间吗?
显然,我们不能开辟
1
0
10
10^{10}
1010 个。但是开辟
1
0
6
10^6
106 个还是没问题的。那我们怎么存储呢?
我们可以通过一个 函数,对数组元素进行求余,求余后再存放,这样就能存储得下了。
其中,上面的 函数 官方术语叫 哈希函数,而 求余 是 哈希 的一种方式。
哈希 是 hash 的中文译名,hash 的本意是 “映射”。
所以 哈希 就是 “映射” 的意思。
接下来看个哈希例子。
例子: 假如我们开辟了一个 1 0 6 10^6 106 空间的 hashTable。此时 A[i] = 1 0 10 10^{10} 1010,那么我们可以让 number = A[i] % 1 0 6 10^6 106,用 number 来代替 A[i],再让 hashTable[number] = true。
(number = A[i] % 1 0 6 10^6 106 就是一个哈希操作。该操作保证了 number 在 [ 0 , 1 0 6 ) [0, 10^6) [0,106) 范围内)
上面的方法的确可以解决数值过大的问题,但这也带来了新的问题,如下:
假如
A
[
x
]
=
1
0
6
+
1
A[x] = 10^6+1
A[x]=106+1,
A
[
y
]
=
2
∗
1
0
6
+
1
A[y] = 2*10^6+1
A[y]=2∗106+1,此时
A
[
x
]
%
1
0
6
=
=
A
[
y
]
%
1
0
6
A[x] \% 10^6 == A[y] \% 10^6
A[x]%106==A[y]%106。
显然,此时产生冲突了,因为两个不同的元素居然哈希到了同一个数值!这个冲突就叫做 哈希冲突。
那我们如何解决 哈希冲突 呢?
我们可以采用 开放地址法、拉链法… 去解决。(有兴趣的小伙伴可以去了解一下)
总结
- 散列 是什么?
散列是一个数组,因为这个数组内部比较 “零散”,所以称这个数组为 散列。 - 哈希、哈希函数 是什么?
哈希 本意就是 映射,而 映射 指的是:将一个数值x
,通过一些运算,转变为另一个数值y
(x
、y
可以相等)。
其中,这些运算操作的集合叫 哈希函数。 - 哈希冲突 是什么?如何解决?
哈希冲突 指:两个不同的数x
、y
,通过 哈希函数 后,得到了一个相同的数值n
。
解决 哈希冲突 的方法有: 开放地址法、拉链法 …
拓展
- 如果我想求数组中只出现 1 次的元素,那应该如何构建散列呢?该散列的类型应该是什么?如何解决这个问题? 题目链接
- 以上的哈希专指数值类型的哈希。假如元素是一个字符串,那我们应该怎样 哈希 呢?
最后
以上如果有任何错误,欢迎大家指出哈。