哈希表理论基础
哈希表是根据关键码的值而直接进行访问的数据结构。直白来说其实数组就是一张哈希表。哈希表能解决什么问题呢?一般哈希表都是用来快速判断一个元素是否出现在集合里。
哈希函数
哈希函数,把数组元素的值直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道某个元素是否在集合中了。
index = hashFunction(name)
hashFunction = hashCode(name) % tableSize
为了保证映射出来的索引数值都落在哈希表上,会再次对数值做一个取模的操作。如果元素数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几个元素同时映射到哈希表同一个索引下标的位置。
哈希碰撞
一般哈希碰撞有两种解决方法,拉链法和线性探测法。
其实拉链法就是要选择适当的哈希表大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
使用线性探测法,一定要保证tableSize大于dataSize,需要依靠哈希表中的空位来解决碰撞问题。
常见的三种哈希结构
当我们想使用哈希法来解决问题时,一般会选择如下三种数据结构:
数组、set(集合)、map(映射)
总结
当我们遇到要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但哈希法也是牺牲了空间换取时间,因为需要额外的数组、set或map来存放数据,才能实现快速的查找。
LeetCode 242.有效的字母异位词
本题作为哈希表的第一题,是一道简单题。
想法:将字符串s和字符串t先转换为字符串数组存起来,然后遍历两个数组统计每个字符出现的次数,若符合条件则为字母异位词,属于暴力解法,代码如下:
import java.util.Map;
import java.util.HashMap;
class Solution {
public boolean isAnagram(String s, String t) {
String[] ss = s.split("");
String[] ts = t.split("");
if(ss.length != ts.length) return false;
Map<String, Integer> smap = new HashMap<>();
Map<String, Integer> tmap = new HashMap<>();
for(int i=0; i<ss.length; i++){
if(smap.get(ss[i]) == null){
smap.put(ss[i], 1);
}else{
smap.put(ss[i], smap.get(ss[i])+1);
}
if(tmap.get(ts[i]) == null){
tmap.put(ts[i], 1);
}else{
tmap.put(ts[i], tmap.get(ts[i])+1);
}
}
for(String key : smap.keySet()){
if(!smap.get(key).equals(tmap.get(key))) return false;
}
return true;
}
}
看完讲解的文章后,发现我想的方法过于复杂了,可以用一个数组搞定的。
思路:先将字符串s和字符串t转化为字符数组,用一个长度为26的数组record记录s和t中出现的字符数量,其中对s来说,统计s[i]出现的次数并存储在record数组中下标为s[i]-'a'的位置,每次++;对t来说,t[i]对应的record[t[i]-'a']--,最后,若数组中存在非0元素,则说明不是字母异位词。代码如下:
class Solution {
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
int[] record = new int[26];
for(int i=0; i< s.length(); i++){
record[s.charAt(i) - 'a']++;
}
for(int i=0; i< t.length(); i++){
record[t.charAt(i) - 'a']--;
}
for(int count: record){
if(count != 0) return false;
}
return true;
}
}
LeetCode 349.两个数组的交集
想法:将nums1数组中的元素统计并保存至map中,在用一个map用来保存既在nums1数组中又在nums2数组中的元素,最后将其转换为int数组返回。由于对map比较熟悉,所以第一反应是用map来写,但知道这并不是最优解法,代码如下:
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Map<Integer, Integer> numsItem = new HashMap<>();
Map<Integer, Integer> sameItem = new HashMap<>();
for(int i=0;i<nums1.length; i++){
if(numsItem.get(nums1[i]) == null){
numsItem.put(nums1[i], 1);
}else{
numsItem.put(nums1[i], numsItem.get(nums1[i]) + 1);
}
}
for(int i=0; i<nums2.length; i++){
if(numsItem.get(nums2[i]) != null){
sameItem.put(nums2[i], 0);
}
}
return sameItem.keySet().stream().mapToInt(x -> x).toArray();
}
}
看了讲解文章中提到,在C++中可以用unordered_set,对应于Java中的HashSet,于是再写了一遍:
import java.util.HashSet;
import java.util.Set;
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> numsSet = new HashSet<>();
Set<Integer> result = new HashSet<>();
for(int i: nums1){
numsSet.add(i);
}
for(int i: nums2){
if(numsSet.contains(i)){
result.add(i);
}
}
return result.stream().mapToInt(x -> x).toArray();
}
}
LeetCode 202.快乐数
看到本题的时候毫无思绪,实在想到怎么做,就去看了讲解文章,看明白之后才发现这确实是一个简单题!
思路:题目中提到可能会无限循环,也就是说在求和过程中,sum可能会重复出现,导致无限循环。前面有提到哈希表的使用场景就是需要快速判断一个元素是否在集合中出现,所以可以用哈希表保存sum,只要遇到之前出现过的sum,就返回false,否则一直找,直到sum=1,代码如下:
import java.util.Set;
import java.util.HashSet;
class Solution {
public boolean isHappy(int n) {
int temp = n;
int sum = 0;
Set<Integer> record = new HashSet<>();
while(sum != 1){
sum = 0;
while(temp != 0){
int single = temp % 10;
sum += single * single;
temp /= 10;
}
if(record.contains(sum)) return false;
record.add(sum);
temp = sum;
}
return true;
}
}
LeetCode 1.两数之和
想法:用Map对象来存储nums数组和对应的下标,但过不了有两个相同的数存在的案例情况,错误代码如下:
import java.util.Map;
import java.util.IdentityHashMap;
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> record = new IdentityHashMap<>();
int one = 0;
int two = 0;
for(int i=0; i<nums.length; i++){
record.put(nums[i], i);
}
for(int key: record.keySet()){
if(record.keySet().contains(target - key)){
one = record.get(key);
two = record.get(target - key);
break;
}
}
return new int[]{one, two};
}
}
看了讲解文章后,才意识到我的代码的问题是用Map对象存储了所有数,这样会覆盖掉key相同但下标不同的情况,正确的做法是用Map对象存储遍历过的元素,这样即使存在相同的key,加起来不等于target的话,覆盖掉也没有影响,如果恰好等于target则为要找的数,返回对应下标即可。代码如下:
import java.util.Map;
import java.util.HashMap;
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> record = new HashMap<>();
int one = 0;
int two = 0;
for(int i=0; i<nums.length; i++){
if(record.keySet().contains(target - nums[i])){
one = record.get(target - nums[i]);
two = i;
break;
}
record.put(nums[i], i);
}
return new int[]{one, two};
}
}
总结
- 为什么会想到用哈希表? 答:当需要快速知道一个元素是否在集合中出现时。
- 哈希表为什么用map? 答:数组的大小是受限制的,如果元素很少,但哈希值太大,会造成内存空间的浪费;set中只能存放一个元素,当需要存放两个元素时,应使用Map对象。