1、什么是哈希表?
哈希表(Hash Table,也叫散列表),是根据键(Key)而直接访问在内存存储位置的数据结构。哈希表通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这个映射函数称做哈希函数,常见的哈希函数有平方取中、除数取余等,存放记录的数组就叫做哈希表。有时,不同的键值有可能映射到相同的位置,即发生了哈希冲突,解决哈希冲突的方案有:开放定址法、再哈希法、链地址法、建立公共溢出区等。
本文的重点不在于哈希表的概念,对哈希表的基本知识不再过多赘述,对于还不了解哈希表的读者建议先去学习完哈希表后再尝试阅读本文。
2、哈希表在算法中的应用
哈希表在算法中常用于统计频率、快速检验某个元素是否出现过等,我们以下面几道例题体会下:
例1:两数之和
给出一个整型数组 nums 和一个目标值 target,请在数组中找出两个加起来等于目标值的数的下标,返回的下标按升序排列。
本题的朴素做法是双重循环遍历数组,求和与目标值比较。但本题的要求是在O(nlogn)的时间复杂度下解决,因此我们需要对朴素算法做出优化。我们要寻找的是数组中的两个数,设为a,b,要求为a+b=target,注意一个小技巧,这种类型问题都可以转化为判断问题,将原式移项得b=target-a,即问题变为数组中是否存在target-a这个数,到这里应该不难想到可以用哈希表实现了,key存放数组中的元素,value存放其下标位置,循环a,判断target-a是否在哈希表中出现即可。
public int[] twoSum(int[] nums, int target) {
int[] ans = new int[2];
HashMap<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;++i){
int key = target - nums[i];
if(map.containsKey(key)){
ans[0]=map.get(key);
ans[1]=i;
return ans;
}
map.put(nums[i],i);
}
return ans;
}
例2:三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
这题与上题类似,同样朴素算法需要三重循环,将其转化为判定问题c=-(a+b)后,利用哈希表可以优化到,本题还需要一个判重操作,即(1,2,3)与(2,1,3)、(3,1,2)等只算做一种情况,这里将其按从小到大转化为字符串如“123”存入Set处理。但这不是本题的最优解,利用双指针可以做到时间内解决该题,读者可以自行思考或者去题目页面查看题解。
public ArrayList<ArrayList<Integer>> threeSum(int[] num) {
ArrayList<ArrayList<Integer>> ans = new ArrayList<>();
HashMap<Integer,Integer> map = new HashMap<>();
for(int i=0;i<num.length;++i)
map.put(num[i],i);
HashSet<String> vis = new HashSet();
for(int i=0;i<num.length;++i){
for(int j=i+1;j<num.length;++j){
int k = -(num[i]+num[j]);
if(!map.containsKey(k)) continue;
int index = map.get(k);
if(index<=j) continue;
int a = num[i], b = num[j];
if(a>b) {int t=a; a=b; b=t;}
if(b>k) {int t=b; b=k; k=t;}
if(a>b) {int t=a; a=b; b=t;}
ArrayList<Integer> tmp = new ArrayList<>(Arrays.asList(a,b,k));
StringBuilder s = new StringBuilder();
s.append(a); s.append(b); s.append(k);
if(vis.contains(s.toString())) continue;
vis.add(s.toString());
ans.add(tmp);
}
}
return ans;
}
华为机试题中也有一道类似的题,感兴趣的读者可以自行尝试,思路与本题一致:输入一组数字,数字之间用空格隔开,判断输入的数字是否可以满足A=B+2C,每个元素最多只可用一次。若有满足的数字组合,依次输出A、B、C三个数字,之间用空格隔开;若无满足条件的组合,输出0。
例3:第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
第一次循环用哈希表记录每个元素的出现次数,第二次循环第一次发现出现次数为1的字符直接返回即可。或者是用哈希表记录每个元素的出现位置,当其再次出现时,将其出现位置置为-1,最后在所有的不是-1的value中搜索最小的那个。
public char firstUniqChar(String s) {
HashMap<Character,Integer> map = new HashMap<>();
for(int i=0;i<s.length();++i){
char a = s.charAt(i);
if(map.containsKey(a))
map.put(a,-1);
else map.put(a,i);
}
int min=Integer.MAX_VALUE;
boolean flag = false;
for(int i : map.values()){
if(i != -1){
flag=true;
min=Math.min(min,i);
}
}
return flag ? s.charAt(min) : ' ';
}
例4: 给定一个字符串S,求出该字符串里满足各个字符最多出现两次的最长子串的长度。
没有找到原题链接,这里给出一个类似题目的链接,区别在这题要求各字符最多出现一次,即子串中不能有重复字符:无重复的最长子串。
对于字符出现的次数,可以使用哈希表统计,最终实现上需要使用双指针,右指针不断向右滑动,每滑动到一个新的位置就将该位置对应的元素出现次数+1,若发现该位置对应的元素出现次数+1后大于了2,则停止滑动右指针,改为滑动左指针,同时更新此时的最长子串长度,左指针每滑动一次就要将原位置对应元素出现次数-1,到左指针滑动完整个字符串后结束。
public lengthOfLongestSubstring(String s) {
int r = -1, ans = 0;
HashMap<Character,Integer> map = new HashMap<>();
int n = s.length();
for(int i=0;i<n;++i) {
if(i!=0) {
char key = s.charAt(i-1);
map.replace(key, map.get(key)-1);
}
while(r+1<n) {
char key = s.charAt(r+1);
if(!map.containsKey(key)) map.put(key, 1);
else {
if(map.get(key)==2) break;
map.replace(key, map.get(key)+1);
}
r++;
}
ans = Math.max(ans, r+1-i);
}
return ans;
}