哈希表: 数组 + 链表
作用:能快速判断一个元素是否存在一个集合中。 时间复杂度是: O(1)。
原理:因为使用了哈希算法,可以计算key
的哈希值,然后映射到链表中,通过数组下标快速的访问到这个元素。 最简单的映射关系: 下标 = hash值 % tableSize。
问题:会出现Hash冲突,就是计算不同的key得到同一个值,放到数组的同一个下标,即发生了Hash冲突。
解决方案:
(1)拉链法:冲突的元素直接变量链表。这个要防止链表的长度太长了,影响查询的效率。
(2)线性探测法: 冲突的时候,将计算的Hash值继续往下排列,移到数组的下一个下标位置,不形成链表。 这个要要数组的大小大于总元素的个数。
第1题 : 242. 有效的字母异位词
题目描述: 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
题目翻译: 这个题翻译说来就是看这两个字符串的字符是不是全部都相同,个数也都必须全部相同。
思路: (1)直接使用HashMap是最简单的想法。
(2)数组其实也是特殊的 哈希表。因为只有26个小写字符,数组的大小就定义为26,一个很关键的问题是怎么把 字符 映射到对应的数组下标,比如:a 就是 0。
题解
暴力使用HashMap:
class Solution {
public boolean isAnagram(String s, String t) {
//这里就是判断t和s的字符是不是完全相等
//HashMap把t和s的字符统计
HashMap<Character,Integer> sMap = new HashMap<>();
HashMap<Character,Integer> tMap = new HashMap<>();
for(int i = 0; i < s.length(); ++i){
sMap.put(s.charAt(i), sMap.getOrDefault(s.charAt(i), 0) + 1);
}
for(int i = 0; i < t.length(); ++i){
tMap.put(t.charAt(i), tMap.getOrDefault(t.charAt(i), 0) + 1);
}
//遍历tMap,看是否所有字符都和s的相同
for(Character cur : tMap.keySet()){
if(!tMap.get(cur).equals(sMap.get(cur))){
return false;
}else{
sMap.remove(cur);
}
}
if(sMap.size() != 0){
return false;
}
return true;
}
}
数组形式:
class Solution {
public boolean isAnagram(String s, String t) {
//数组其实也是 哈希表, 小写字母只有26个
//s字符的时候++,t字符的时候--,最后判断是不是所有位置都为0即可
int[] arr = new int[26];
//处理s字符串 --> 注意字符和数组下标的映射,使用 s.charAt(i) - 'a'
for(int i = 0; i < s.length(); ++i){
arr[s.charAt(i) - 'a']++;
}
//处理t字符串
for(int i = 0; i < t.length(); ++i){
arr[t.charAt(i) - 'a']--;
}
//看数组是不是都为0
for(int i = 0; i < arr.length; ++i){
if(arr[i] != 0){
return false;
}
}
return true;
}
}
第2题: 383. 赎金信
题目描述:
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
题目翻译: 与上一题不同,这个题翻译说来就是看 magazine 的字符是不是能囊括 ransomNote 的所有字符。
思路: 直接使用数组 来代替 HashMap 完成。
题解
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
//这个就是看 magazine 的字符是不是包含 ransomNote 所有的字符
//数组也是特殊的哈希表
int[] arr = new int[26];
//处理字符串magazine
for(int i = 0; i < magazine.length(); ++i){
arr[magazine.charAt(i) - 'a']++;
}
//处理字符串ransomNote
for(int i = 0; i < ransomNote.length(); ++i){
arr[ransomNote.charAt(i) - 'a']--;
}
//看数组里面所有的是不是都 > 0
for(int i = 0; i < arr.length; ++i){
if(arr[i] < 0){
return false;
}
}
return true;
}
}
上面的最后两个for循环可以放在一起进行处理:
//处理字符串ransomNote
for(int i = 0; i < ransomNote.length(); ++i){
if(arr[ransomNote.charAt(i) - 'a'] > 0){
arr[ransomNote.charAt(i) - 'a']--;
}else{
return false;
}
}
第3题: 49. 字母异位词分组
题目描述:
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
分析: 这题是难题较大的一题,我认为关键的问题是 : 有这么多的字符串,要怎么确定这些字符串会是字母异位词,若直接使用HashMap
,显然不现实,因为这么多字符串,每一个都要进行处理,你会得到很多的HashMap
,你要对不同的map
去判断他们的字符数是不是相同,感觉起来就是非常不靠谱的事。
字母异位词,我们要深究他的概念,如果是字母异位词,那么所有字符数都是相等的。这样我们是不是可以使用对字符串进行排序来判断,不需要字符的个数。 将排序后的字符串作为Map
的key
,而将结果List
作为value
。
思路: 使用HashMap。
题解
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
//使用HashMap,关键问题是怎么确定 这个字符串和其他的字符串是字母异位词
//如果HashMap的key使用Character,这样肯定会特别麻烦
//另外,我们应该考虑到对字符串进行排序,这样如果是字母异位词,排序后肯定是相同的。
//将HashMap设计成HashMap>
//总结果
List<List<String>> res = new ArrayList<>();
//创建HashMap
HashMap<String, List<String>> map = new HashMap<>();
//遍历字符数组
for(String str : strs){
//字符串转成字符数组,方便排序
char[] strChar = str.toCharArray();
//排序
Arrays.sort(strChar);
//得到排序后字符串 --> 这里要使用new String(),不能使用toString(),因为没有重写
String newStr = new String(strChar);
//拿排序后的字符串去map中查看--> 如果这个字符串是第一次,就创建出一个
List<String> sList = map.getOrDefault(newStr, new ArrayList<String>());
//然后把 未排序的字符串加入,并放入map中
sList.add(str);
map.put(newStr, sList);
}
//处理map,变成我们的结果
for(Map.Entry<String, List<String>> entry : map.entrySet()){
res.add(entry.getValue());
}
return res;
}
}
上面存在一些巧妙处理的地方,可以多消化消化。
还有遍历map
的几种方式也要掌握。除了上面这样方式,还有一种较为常见的方式,使用map.keySet()
,得到所有的key集合。
第4题: 438. 找到字符串中所有字母异位词
题目描述:
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)
分析: 这题和上面一题非常像。这是这里变成了字符串,并且要返回的是满足条件的下标开始位置。
思路: (1)通过截取字符串暴力。
(2)可以使用数组模拟哈希表,然后采用滑动窗口的方式求解。
题解
暴力方式
class Solution {
public List<Integer> findAnagrams(String s, String p) {
//这题和 49.字母异位词分组 非常像
//可以对s字符串截取p长度
//结果
List<Integer> res = new ArrayList<>();
//先对p进行排序
char[] cp = p.toCharArray();
Arrays.sort(cp);
String newP = new String(cp);
//字符串p的长度
int pLen = p.length();
//遍历字符串s,截取字符串进行判断
for(int i = 0; i <= s.length() - pLen; ++i){
//从s中截取子串
String curS = s.substring(i, i + pLen);
//排序
char[] sp = curS.toCharArray();
Arrays.sort(sp);
if(newP.equals(new String(sp))){
res.add(i);
}
}
return res;
}
}
使用数组的方式:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
//滑动窗口方法
int slen = s.length();
int plen = p.length();
List<Integer> result = new ArrayList<>();
if(slen < plen) return result;
//数组记录s和p字符出现的个数
int[] sarr = new int[26];
int[] parr = new int[26];
//遍历p,把p加入到parr中,顺便处理s的前p个字符
for(int i = 0; i < p.length(); ++i){
parr[p.charAt(i) - 'a'] += 1;
sarr[s.charAt(i) - 'a'] += 1;
}
//判断p的第一个长度是不是
if(Arrays.equals(sarr, parr)){
result.add(0);
}
//判断从0下标开始之后的,使用滑动窗口方法。i为右指针,i-plen为窗口的左指针
//对p后面的每个字符进行遍历,每次循环的时候,将右指针的字符加上,同时左指针的字符减去,就相当于是一个滑动窗口
for(int i = plen; i < slen; ++i){
sarr[s.charAt(i) - 'a'] += 1; //加上前一个字符
sarr[s.charAt(i-plen) - 'a'] -= 1; //减去最后一个字符
if(Arrays.equals(sarr,parr)){
result.add(i - plen + 1);
}
}
return result;
}
}
第5题 : 349. 两个数组的交集
题目描述:
给定两个数组,编写一个函数来计算它们的交集。
题目翻译: 找到他们公有的元素,并且不重复。
思路: 使用HashSet
就没错。
题解
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
//要去重,判断一个元素是不是再两个数组里面都有
//使用HashSet最为合适
HashSet<Integer> set1 = new HashSet<>();
for(int num : nums1){
set1.add(num);
}
//存储结果
HashSet<Integer> res = new HashSet<>();
for(int num : nums2){
if(set1.contains(num)){
res.add(num);
}
}
int[] ans = new int[res.size()];
int count = 0;
for(Integer num : res){
ans[count++] = num;
}
return ans;
}
}
第6题 : 350. 两个数组的交集 II
题目描述:
给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
题目翻译: 和上题的区别是: 这里的结果不能去重。
思路: 因为不能去重,就不能使用HashSet了,要使用HashMap。
题解
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
//与349题非常类似,但是这里的结果是不能去重的,那就使用HashMap
HashMap<Integer,Integer> map1 = new HashMap<>();
for(int num : nums1){
map1.put(num, map1.getOrDefault(num, 0) + 1);
}
//结果集
List<Integer> res = new ArrayList<>();
//遍历第二个数组,在map1存在就加入res,map1--。
for(int num : nums2){
//存在才需要处理,不存在直接跳过
if(map1.containsKey(num)){
//放入
res.add(num);
//map1--
map1.put(num, map1.get(num) - 1);
//已经没了
if(map1.get(num) == 0){
map1.remove(num);
}
}
}
//遍历res
int[] ans = new int[res.size()];
for(int i = 0; i < res.size(); ++i){
ans[i] = res.get(i);
}
return ans;
}
}
此外,还可以使用排序 + 双指针的方式。
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
//使用双指针法, 但是要提前进行排序
Arrays.sort(nums1);
Arrays.sort(nums2);
int len1 = nums1.length; int len2 = nums2.length;
//遍历下标
int index1 = 0; int index2 = 0; int index = 0;
//创建结果数据,以小的为大小
int[] res = new int[len1 > len2 ? len2 : len1];
while(index1 < len1 && index2 < len2){
if(nums1[index1] < nums2[index2]){
index1++;
}else if(nums1[index1] > nums2[index2]){
index2++;
}else{
//相等的情况
res[index++] = nums1[index1];
index1++;
index2++;
}
}
//从res中处理得到最终结果
return Arrays.copyOfRange(res, 0, index);
}
}
第7题 : 202. 快乐数
题目描述:
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:
如果 n 是快乐数就返回 true ;不是,则返回 false 。
思路: 要理解快乐数, 里面有一个点,会出现无限循环,无限循环就是会出现重复,重复就使用HashSet
,可以这样: 只要 n != 1
,就一直循环,直到找到1
为止。 然后在循环里面去看这个n
是否重复,不重复,就加入并更新n
的值,重复那就是不限循环,return false
。
还有一个关键点是: 给你一个数,你怎么样能拿到这个数的所有位置上的数,这样要掌握。
题解
class Solution {
public boolean isHappy(int n) {
//如果出现无限循环,那就是结果会重复,重复就用HashSet
//还有一个关键是: 给你一个数,你怎么获得这个数每个位置上的数值分别是什么
HashSet<Integer> set = new HashSet<>();
//只要不是1就一直找
while(n != 1){
//看是否n重复了 --> 无限循环
if(set.contains(n)){
return false;
}
//加入
set.add(n);
//没有重复,要处理这个n,拿到所有位置的数
//记录n每个位置数平方和后的结果
int temp = 0;
while(n > 0){
//循环取每一位数值, 从 个位 -> 十位 -> 百位
int num = n % 10;
temp += num * num;
//更新n,才能取到下一位的
n = n / 10;
}
//进行下一轮循环
n = temp;
}
//n == 1
return true;
}
}
关键:给你一个数,怎么获取 个位、十位、百位。。。上的每一个数值大小。
/*
* 这个num就是不断更新,从 个位 -> 十位 -> 百位。。一直取,直至取完为止
*/
while(n > 0){
//循环取每一位数值, 从 个位 -> 十位 -> 百位
int num = n % 10;
//更新n,才能取到下一位的
n = n / 10;
}
第8题 : 1. 两数之和
题目描述:
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:
(1)两层for
暴力。
(2)使用HashMap
,可以记录元素的下标位置。
题解
class Solution {
public int[] twoSum(int[] nums, int target) {
//使用HashMap,因为要返回的是数组的下标位置
HashMap<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; ++i){
int cur = nums[i];
//找到
if(map.containsKey(target - cur)){
return new int[]{i, map.get(target - cur)};
}else{
map.put(cur, i);
}
}
//没有
return new int[0];
}
}
第9题 : 454. 四数相加 II
题目描述:
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
思路:
使用组合的方式,有4个不同的数组,将两两数组进行组合,1和2的sum组合放到map中,然后去3和4的sum中找是否存在让四个数的结果为0的。
题解
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
//使用HashMap对四个数组进行两两分组。 要记录次数
HashMap<Integer,Integer> map = new HashMap<>();
//1和2的结果分为一组,放到map中,然后查看3和4中是不是能找到map中对应的数
for(int num1 : nums1){
for(int num2 : nums2){
//和加入map
int sum = num1 + num2;
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
}
//记录结果
int res = 0;
//3和4分成一组
for(int num3 : nums3){
for(int num4 : nums4){
int sum = num3 + num4;
if(map.containsKey(0 - sum)){
res += map.get(0 - sum);
}
}
}
return res;
}
}
第10题 : 15. 三数之和
题目描述:
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组
思路:
使用三指针法。 一个指针下标i
去遍历数组,这是第一个数。 下标i+1
作为左边界,下标length-1
作为右边界,他们分别是第二个和第三个数。 然后根据sum
的值不断去更新这些下标。这就是三指针法。
一个关键的问题是: 要去重!!!
题解
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
//三指针法。 外层for从前向后遍历,然后下标从前往后,另外一个下标从后往前,要考虑去重问题
List<List<Integer>> res = new ArrayList<>();
//先排序 --> 排序是为了去重做准备
Arrays.sort(nums);
//遍历
for(int i = 0; i < nums.length - 2; ++i){
//sum肯定 > 0
if(nums[i] > 0){
return res;
}
//去重
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
//另外两个下标
int left = i + 1, right = nums.length - 1;
while(left < right){
//三数之和
int sum = nums[i] + nums[left] + nums[right];
if(sum < 0){
left++;
}else if(sum > 0){
right--;
}else{
//和为0,那么第一次出现的就加入,在下面进行去重操作
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
//left下标的去重
while(left < right && nums[left] == nums[left+1]){
left++;
}
//right下标的去重
while(left < right && nums[right] == nums[right-1]){
right--;
}
//更新left和right的下标
left++;
right--;
}
}
}
return res;
}
}
第11题 : 18. 四数之和
题目描述:
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
你可以按 任意顺序 返回答案
思路:
这个和上一题基本一样,就是多了一个数,所以将问题变成和上面一样就解决了。这样,我们就可以使用四指针法,然后和上面一样的方式解决问题。
题解
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
//可以这题处理成 第15题. 三数之和 类似的问题。 就是要先处理好两个数的和,这样就变成了15题。
List<List<Integer>> res = new ArrayList<>();
//排序,方便去重
Arrays.sort(nums);
for(int i = 0; i <= nums.length - 4; ++i){
//去重
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
for(int j = i + 1; j <= nums.length - 3; ++j){
//去重
if(j > i + 1 && nums[j] == nums[j-1]){
continue;
}
//计算出3和4的和应该是多少
int sum = target - nums[i] - nums[j];
//另外两个下标
int left = j + 1, right = nums.length - 1;
while(left < right){
int sumlr = nums[left] + nums[right];
if(sum > sumlr){
//和太小了
left++;
}else if(sum < sumlr){
right--;
}else{
//相等,加入,然后去重
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
//left去重
while(left < right && nums[left] == nums[left+1]){
left++;
}
//right去重
while(left < right && nums[right] == nums[right-1]){
right--;
}
//更新坐标
left++;
right--;
}
}
}
}
return res;
}
}
(1)数组是特殊的哈希表,如果个数确定,就可以使用数组代替哈希表。
(2)如果有去重操作,那就使用HashSet
; 如果不需要去重,并且还要将所有的结果都进行展示,那就使用HashMap
。