一、理论基础
1.哈希表是根据键直接访问值的数据结构。
2.哈希函数:通过hashCode把其他数据格式转化成不同的数值。
3.不同的键经过哈希函数映射到同一个数值上,称为哈希碰撞,哈希碰撞的解决方法:
(1)拉链法
(2)线性探测法
4.常见的三种哈希结构
(1)数组
(2)集合set
(3)映射map
总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
二、题目
关注其中的哈希思想,判断一个元素是否出现过,出现过次数的场景,第一时间想到哈希法。
class Solution {
public boolean isAnagram(String s, String t) {
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 i=0; i<26; i++){
if(record[i]!=0)
return false;
}
return true;
}
}
在这道题中,选用数组作为哈希表,是因为限制了数值的大小,空间复杂度可以达到O(1)。
如果题目没有限制数值的大小,就无法使用数组来做哈希表了。
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
例如题目:
此时就要使用另一种结构体了,set
std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
在Java中,类似于C++中 set
、multiset
和 unordered_set
的数据结构是:
-
HashSet
: 对应于 C++ 的unordered_set
,底层实现是哈希表。HashSet
不保证元素的顺序,并且不允许重复元素。 -
TreeSet
: 对应于 C++ 的set
,底层实现是红黑树。TreeSet
中的元素是有序的(根据自然顺序或提供的比较器),不允许重复元素。 -
LinkedHashSet
: 这是一个介于HashSet
和TreeSet
之间的数据结构,底层也是哈希表,但它维护了一个双向链表来记录插入顺序,所以迭代顺序是插入顺序。
那遇到哈希问题我直接都用set不就得了,用什么数组啊。
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> reset = new HashSet<>();
for ( int i : nums1){
set1.add(i);
}
for ( int i : nums2){
if(set1.contains(i)){
reset.add(i);
}
}
int[] resArr = new int[reset.size()];
int j = 0;
for(int i : reset){
resArr[j++]=i;
}
return resArr;
}
}
3.202.快乐数
题目中提到很关键的特征:无限循环
这使得我们可以不用数学的方法来解,而是使用模拟的方法,并且需要使用Set来记录是否有循环出现的结果。
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while(n != 1 && !record.contains(n)){
record.add(n);
n = getNextNum(n);
}
return n==1;
}
public int getNextNum(int n){
int res=0;
while(n != 0){
int tmp = n%10;
res = res + tmp*tmp;
n/=10;
}
return res;
}
}
当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法.
本题中需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,该元素对应的答案是否遍历过,也就是 是否出现在这个集合。
那么我们就应该想到使用哈希法了。
因为本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
再来看一下使用数组和set来做哈希法的局限。
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。
C++中map,有三种类型:
class Solution {
public int[] twoSum(int[] nums, int target) {
int len = nums.length;
int[] res = new int[2];
Map<Integer,Integer> map = new HashMap<>(len-1);
for(int i=0; i<len ; i++){
int match = target - nums[i];
if(map.containsKey(match)){
res[0]=map.get(match);
res[1]=i;
}
map.put(nums[i],i);
}
return res;
}
}
4.四数相加II
本题是四个独立的数组,不涉及重复判断,可以两两分组,使用哈希思想。
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int res=0;
Map<Integer,Integer> map = new HashMap<>();
for(int i :nums1){
for(int j :nums2){
int sum1 = i+j;
map.put(sum1,map.getOrDefault(sum1,0)+1);
}
}
for(int i : nums3){
for(int j : nums4){
res+=map.getOrDefault(0-i-j,0);
}
}
return res;
}
}
5.383赎金信
本题使用数组的空间代价更小
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
boolean res = true;
int[] record = new int[26];
for(char c : magazine.toCharArray()){
record[c-'a']++;
}
for(char c : ransomNote.toCharArray()){
if(record[c-'a']==0){
res = false;
}
record[c-'a']--;
}
return res;
}
}
6.三数之和
本题要求不包含重复的三元组,和 4.四数相加II 不同,如果用哈希表的话,需要处理复杂的判断重复的逻辑。
本题建议用双指针。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i = 0; i < nums.length-2 ; i++){
int left = i+1;
int right = nums.length - 1;
if(nums[i]>0) return res;
if(i>0 && nums[i]==nums[i-1]) continue;
while(left < right){
if(nums[i]+nums[left]+nums[right]<0){
left++;
}else if(nums[i]+nums[left]+nums[right]>0){
right--;
}else{
res.add(Arrays.asList(nums[i],nums[left],nums[right]));
while(left < right && nums[left]==nums[left+1]) left++;
while(left < right && nums[right]==nums[right-1]) right--;
left++;
right--;
}
}
}
return res;
}
}
7.18.四数之和
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。