散列 --- 解题必备良品

散列


导言

  • 一定要学会 散列
  • 散列 也是利用了 空间换时间 的思想。
  • 下文会提及: 散列、哈希、哈希函数、哈希冲突。

问题

有两个数组 AB,它们的长度都在 [ 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; 
}

的确,上面的代码可以得出正确答案。但是,假如此时数组 AB 的长度都为 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

于是,借助于 hashTable0.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]=true5 就是偏移量,这样就能避免负数越界了。

那这时候又有小伙伴会说,计算机的内存是有限的,假如数组最大值为 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]=2106+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 (xy 可以相等)。
    其中,这些运算操作的集合叫 哈希函数
  • 哈希冲突 是什么?如何解决?
    哈希冲突 指:两个不同的数 xy,通过 哈希函数 后,得到了一个相同的数值 n
    解决 哈希冲突 的方法有: 开放地址法、拉链法 …

拓展

  • 如果我想求数组中只出现 1 次的元素,那应该如何构建散列呢?该散列的类型应该是什么?如何解决这个问题? 题目链接
  • 以上的哈希专指数值类型的哈希。假如元素是一个字符串,那我们应该怎样 哈希 呢?

最后

以上如果有任何错误,欢迎大家指出哈。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值