一、哈希表(散列表)
查找领域有三种主要的查找方式,线性表查找,树形查找(类似BST),还有一种就是散列表查找,散列表又叫哈希表。
- 哈希函数
- 哈希表
- 数据记录
哈希表就是我们的函数,对于每一个输入的键值,唯一映射一个存储地址,地址终究存放着我们的数据。
- 哈希函数是一个映射,设计核心是设计完美的哈希函数尽量减少冲突。
- 哈希函数:对任意的键值返回出相应的唯一的内存地址以供我们对数据的插入删除和存储
- 冲突:就像函数中会出现周期函数一样,我们完全也会碰见一种情况就是,针对不同的关键字我们可能会获得相同的内存地址,这就是冲突,相应的这两个关键字我们称作同义词。
- 哈希表:哈希表就是根据我们设定的哈希函数和我们设定的解决冲突的方法将一组关键字映射到一个连续的地址集(区间)上,并以关键字在地址中的像作为记录在表中的存储位置,这就叫做哈希表, 这一映射过程我们也叫做建立哈希表或者散列,我们得到的存储位置叫做哈希地址或者散列地址。
综上我们会发现哈希表的两个核心:
1、建立哈希:(构建哈希函数)根据哈希函数,建立映射。
2、处理冲突:我们很难设计出完全没有冲突的哈希函数,所以对冲突的处理是非常有必要的。
二、散列表的构造方法
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,每个关键字key对应一个存储位置f(key),查找时,根据这个对应的关系找到给定值key的映射f(key)若查找集合中存在这个记录,则必定在f(key)的位置上。我们把这种对应关系f称为散列函数,又称为哈希(Hash)函数。采用散列技术将记录存储在一块存储空间中,这块连续空间称为散列表或哈希表(Hash-Table)。
2.1 直接定址法
直接定址法使用下面的公式
f(key)= a*key+b,a,b为常数
比如统计出生年月,那么就可以使用f(key)= key-1990来计算散列地址。
地址h(key) | 出生年份(key) | 人数(attribute) |
0 | 1990 | 1285万 |
1 | 1991 | 1281万 |
2 | 1992 | 1280万 |
10 | 2000 | 1250万 |
21 | 2011 | 1180万 |
2.2 除留取余法
这种方法是最常用的散列函数构造方法,对于表长为m的散列公式为:
f(key)= key mod p (p<=m)
地址 h(key) |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
关键词 key |
34 | 18 | 2 | 20 | 23 | 7 | 42 | 27 | 11 | 30 | 15 |
- 这里:p=Tablesize =17
- 一般,p取素数
2.3 数字分析法
分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址。这里使用一个手机号码作为例子,手机号码是一个字符串,一般地说,后面4位是真正的用户号。
比如:取11位手机号码key的后4位作为地址:
散列函数为:h(key)=atoi(key+7) (char*key)
2.4 折叠法
把关键词分割成位数相同的几个部分,然后叠加:
如:56793542
2.5 平方取中法
如:56793542
三、冲突解决方法
常用处理冲突的思路:
1、换个位置:开放地址法
2、同一个位置的冲突对象组织在一起:链地址法
3.1 开放地址法
一旦产生了冲突(该地址已有其他元素),就按某种规则取去寻找另一空地址,若发生了第i次冲突,试探的下一个地址将增加,基本公式是:
mod TableSize (1<=i<TableSize)
这里面决定了不同的解决冲突方案:线性探测、平方探测、双散列。下面依次介绍各种方法:
3.1.1 线性探测法
线性探测法以增量序列1,2,...,(TableSize-1)循环试探下一个存储地址。
【 例1】设关键词序列为47,7,29,11,9,84,54,20,30,散列表表长
TableSize=11(装填因子),散列函数为:h(key)=key mod 11。用线性探测法处理冲突,列出依次插入后的散列表,并估算查找性能。
【解】初步的散列地址如下表所示:
关键词(key) 47 7 29 11 9 84 54 20 30
散列地址h(key) 3 7 7 0 9 7 10 9 8
可以看出有多个关键词的散列地址发生了冲突,具体见下表
关键词(key) 47 7 29 11 9 84 54 20 30
散列地址h(key) 3 7 7 0 9 7 10 9 8
冲突次数 0 0 1 0 0 3 1 3 6
具体的哈希表构建过程可以用下面的图表来表示
操作 | 地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 说明 |
插入47 | 47 | 无冲突 | |||||||||||||
插入7 | 47 | 7 | 无冲突 | ||||||||||||
插入29 | 47 | 7 | 29 | ||||||||||||
插入11 | 11 | 47 | 7 | 29 | 无冲突 | ||||||||||
插入9 | 11 | 47 | 7 | 29 | 9 | 无冲突 | |||||||||
插入84 | 11 | 47 | 7 | 29 | 9 | 84 | |||||||||
插入54 | 11 | 47 | 7 | 29 | 9 | 84 | 54 | ||||||||
插入20 | 11 | 47 | 7 | 29 | 9 | 84 | 54 | 20 | |||||||
插入30 | 11 | 30 | 47 | 7 | 29 | 9 | 84 | 54 | 20 |
这里引出一下散列表的查找性能分析,散列表的查找性能,一般有两种方法
1、成功平均查找长度(ASLs)
2、不成功平均查找长度(ASLu)
对于上面一题的散列地址冲突次数为
关键词(key) 0 1 2 3 4 5 6 7 8 9 10 11 12
散列地址h(key) 11 30 47 7 28 9 84 54 20
冲突次数 0 6 0 0 1 0 3 1 3
ASLs:查找表中关键词的平均查找比较次数(其冲突次数加1)
ASLs=(1+7+1+1+2+1+4+2+4)/9=23/92.56
ASLu: 不在散列表中的关键词的平均查找次数(不成功)
一般方法:将不在散列表中的关键词分若干类。
如:根据h(key)值分类
ASLu=(3+2+1+2+1+1+1+9+8+7+6)/11=41/113.73
3.1.2 平方探测法
平方探测法以增量序列,且循环试探下一个存储地址。还是使用【例1】,得到的冲突如下表
关键词key | 47 | 7 | 29 | 11 | 9 | 84 | 54 | 20 | 30 |
散列地址h(key) | 3 | 7 | 7 | 0 | 9 | 7 | 10 | 9 | 8 |
冲突次数 | 0 | 0 | 1 | 0 | 0 | 2 | 0 | 3 | 3 |
ASLs =(1+1+2+1+1+3+1+4+4)/9=18/9=2
操作 | 地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 说明 |
插入47 | 47 | 无冲突 | |||||||||||
插入7 | 47 | 7 | 无冲突 | ||||||||||
插入29 | 47 | 7 | 29 | ||||||||||
插入11 | 11 | 47 | 7 | 29 | 无冲突 | ||||||||
插入9 | 11 | 47 | 7 | 29 | 9 | 无冲突 | |||||||
插入84 | 11 | 47 | 84 | 7 | 29 | 9 | |||||||
插入54 | 11 | 47 | 84 | 7 | 29 | 9 | 54 | 无冲突 | |||||
插入20 | 11 | 20 | 47 | 84 | 7 | 29 | 9 | 54 | |||||
插入30 | 11 | 30 | 20 | 47 | 84 | 7 | 29 | 9 | 54 |
3.2 链地址法
链地址法就是将相应位置上冲突的所有关键词存储在同一个单链表中。
【例2】设关键字序列为47,7,29,11,16,92,22,8,3,50,37,89,94,21,散列函数取为h(key)=key mod 11,用分离链接法处理冲突。
【每日一题】
两数之和
给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2,7,11,15],target = 9
因为 nums[0] + nums[1] = 2 + 7 =9
所以返回[0,1]
方法一:暴力枚举
【题解】
思路与方法:
最容易想到的方法是枚举数组中的每一个数x,寻找数组中是否存在target-x。
当我们使用遍历整个数组的方式寻找target-x时,需要注意到每一个位于x之前的元素都已经和x匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在x后面的元素中寻找target-x。
代码:
class Solution {
public int[] twoSum(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[0];
}
}
方法二:哈希表
思路与算法:
注意到方法一的时间复杂度较高的原因是寻找target-x的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。
使用哈希表,可以将寻找target-x的时间复杂度降低到O(N)降低到O(1)。
这样我们创建一个哈希表, 对于每一个x,我们首先查询哈希表中是否存在target-x,然后将x插入到哈希表中,即可保证不会让x和自己匹配。
代码:
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (hashtable.containsKey(target - nums[i])) {
return new int[]{hashtable.get(target - nums[i]), i};
}
hashtable.put(nums[i], i);
}
return new int[0];
}
}