算法笔记 散列
本文将介绍散列 (hash) 这一常见的算法思想。
一、用空间换时间
先看一个简单的问题:给出 N 个正整数,再给出 M 个正整数,问这 M 个数中的每个数分别是否在 N 个数中出现过,其中 N, M <= 105 ,且所有正整数均不超过 105 。例如 N=5,M=3,N 个正整数为 {8, 3, 7, 6, 2},欲查询的 M 个正整数为 {7, 4, 2},于是后者中只有 7, 2 在 N 个正整数中出现过。
对于这个问题,最直观的思路是:对每个欲查询的正整数 x,遍历所有 N 个数,看是否由一个数与 x 相等。这种做法的时间复杂度为 o(NM) ,当 N 和 M 都很大时,显然是无法承受的。
还有另一个办法,用空间换时间 。即设定一个 bool 型数组 hashTable[100010] ,并先全部初始化为 false ,再遍历 N 个正整数对 bool 数据进行预处理。其中 hashTable[x] == true 则表示正整数 x 在 N 个数中出现过,反之则表示正整数 x 在 N 个正整数中没有出现过。之后再依次读入 M 个正整数,若 hashTable[x] == true 说明可以查询到,否则则不行。
1 |
|
如果题目要求得到欲查询数在 N 中出现的次数,需要对预处理代码做以下改动。
1 |
|
二、散列
1.散列的概念
上面的做法有一个特点,那就是直接把输入的数作为数组的下标来进行统计运算。这时一个很好的策略,但存在可以优化的地方——如果数字是庞大的天文数字,或者甚至是一个字符串,就不能把它们作为数组下标。
散列 (Hash) 就是用来解决这个问题的,它可以用一句话概括: 将元素通过一个函数转换为整数,使得该整数可以尽量唯一地代表这个元素 。
2.常用 Hash 函数
对于 key 是整数的情况,常用的 hash 函数有直接定址法、数字分析法、平方取中法、除留余数法等。
(1) 直接定址法
1 |
|
(2) 数字分析法
选取关键字中的若干数位组成 Hash 地址,选取的原则是尽量减少冲突。
(3) 平方取中法
将关键字平方,选择平方结果中的部分数位。
(4) 除留余数法
将 key 除以一个数 mod 得到的余数作为 hash 值。
1 |
|
mod 应该小于等于 hash 表的长度 length 。(否则就放不进去)
mod 选取素数,能够更好地防止冲突。
mod 应尽可能大,以避免空间的浪费。
综上,mod 一般选取小于等于表长的最大素数。
3.冲突的处理
在 hash 表中,若 H(key1) == H(key2)
,即不同的数据经计算后得出相同的结果,因此也理应放入同一个地方,便称为发生了冲突。冲突无法完全避免,只能尽量减少冲突的发生,因此在建立 hash 表时就必须考虑冲突处理。
常见的冲突处理方法有:
(1) 线性探查法
依次检查下一个位置,找到空位时就将关键字放入。
(2) 平方探查法
依次检查 H(key)+1 、 H(key)-1 、 H(key)+4 、H(key)-4 、H(key)+9 ······,找到空位后将关键字放入。
(3) 链地址法
将所有同义词用单链表串联起来。 hash 表中不再存放记录本身,而是存放指向相应链表的指针。
三、二维坐标的 hash 函数
如果 key 不是整数,应该如何设计散列函数?
一个例子是:如何将一个二位整点 P 的坐标映射为一个整数,使得整点 P 可以由该整数唯一地代表。假设一个整点的坐标是 (x, y) ,其中 0 <= x, y < Range
,那么可以令 hash 函数为 H(P) = X * Range + y
,这样数据范围内地任意一个整点都一定只有唯一的 hash 函数值。
当
0 <= x, y < Range
时,hash 函数值一定由 n 个 Range 和 一个小于 Range 的数组成,可以很容易地唯一确定一个函数值。
原书:
假设一个整点的坐标是 (x, y) ,其中
0 <= x, y <= Range
,那么可以令 hash 函数为H(P) = X * Range + y
,这样数据范围内地任意一个整点都一定只有唯一的 hash 函数值。这里有一个问题, (n, Range) 的函数值会与 (n + 1, 0) 重合,导致冲突。
四、字符串的 hash 函数
字符串 hash 是指将一个字符串 S 映射为一个整数,使得该整数可以尽可能唯一地代表该字符串。此处只讨论将字符串转换为唯一的整数。为讨论方便,先假设字符串均由大写字母 AZ 构成,并把 AZ 视为 0~25 ,于是字符串就可以转换为二十六进制数,
BZA
01 25 00
接着,按照将二十六进制转换为十进制的思路,将字符串转换为“对应的十进制数”。因为字符串对应的二十六进制数,其数组都是一一对应且不重合的,因此将其转换为十进制后,也是一一对应且不重合的。由此便可实现将字符串映射为整数的需求。代码如下:
1 |
|
如果字符串中出现了小写字母,可以把 az 看作 2651 ,将问题变成五十二进制转化为十进制的问题,代码如下:
1 |
|
如果字符串中出现数字,一般由两种处理方法:
按照小写字母的处理方法,增大进制数至62。
如果保证在字符串的末尾是确定个数的数字,那么就可以把前面英文字母部分先转换为整数,再直接把末尾的数字拼接上去。
例如:
BCD4
① 先将 BCD 转换为整数 731
② 将 4 拼接上去
③ 如上,H(BCD4) = 7314
参考
- 算法笔记