当遇到需要快速判断一个元素是否出现在集合里面的时候,可以考虑哈希法,牺牲一定的空间换取查找的时间。

java常用的哈希表有HashMap、HashSet以及用数组去模拟哈希,这几种方法各有优劣。

  • 数组模拟哈希
    数组模拟需要在一开始就确定大小,如果key的值域范围很大,但是key的数量很少,就要开辟很大的数组空间存很少的key,造成空间的浪费。不过数组相比HashMap的优势在于map可能需要维护红黑树或者链表,而且还需要计算哈希函数,当数据量比较大的时候数组更能节省时间。

  • HashSet
    基于 HashMap 来实现、无序(不会记录插入的顺序,可以用LinkedHashMap实现有序插入,也可以用TreeSet实现插入后元素的排序)、允许有 null 值、不允许有重复元素、不是线程安全的(用ConcurrentHashMap实现线程安全)。

  • HashMap
    存储的内容是键值对(key-value)映射、最多允许一条记录的键为 null、无序(即不会记录插入的顺序)、不支持线程同步

目录

  • 数组模拟hash
    • LeetCode 242. 有效的字母异位词(数组模拟hash)
    • LeetCode 438. 找到字符串中所有字母异位词(滑动窗口+数组模拟hash)
    • LeetCode 383. 赎金信(数组模拟hash)
    • LeetCode 1002. 查找共用字符(数组模拟hash)
    • LeetCode 349. 两个数组的交集(数组模拟/HashSet)
    • LeetCode 3. 无重复字符的最长子串(同剑指 Offer 48. 最长不含重复字符的子字符串 与 剑指 Offer II 016. 不含重复字符的最长子字符串)
  • HashMap
    • LeetCode 49. 字母异位词分组(HashMap)
    • LeetCode 76. 最小覆盖子串(HashMap/数组模拟hash)
    • LeetCode 76. 最小覆盖子串(同剑指 Offer II 017. 含有所有字符的最短字符串)
    • LeetCode 350. 两个数组的交集 II(HashMap/先排序+双指针)
    • LeetCode 1. 两数之和(HashMap)
    • LeetCode 560. 和为 K 的子数组
    • LeetCode 454. 四数相加 II(分组+hashMap)
    • LeetCode 15. 三数之和(双指针+剪枝+去重)
    • LeetCode 18. 四数之和(双指针降复杂度+去重+剪枝)
  • HashSet
    • LeetCode 128. 最长连续序列
    • LeetCode 202. 快乐数(HashSet/快慢指针判断是否成环)
    • LeetCode 349. 两个数组的交集(数组模拟/HashSet)

数组模拟hash


LeetCode 242. 有效的字母异位词(数组模拟hash)

原题链接

题目数据规定为小写字母,因此可以开辟一个26长度的数组模拟哈希。统计s每个字母出现的次数,再遍历t字符串,出现对应字母就将模拟的哈希数组对应数值-1。

代码如下:

class Solution {public boolean isAnagram(String s, String t) {int[] hash=new int[26];for(int i=0;i<s.length();i++){++hash[s.charAt(i)-'a'];}for(int i=0;i<t.length();i++){--hash[t.charAt(i)-'a'];}for(int i=0;i<26;i++){if(hash[i]!=0)return false;}return true;}
}

LeetCode 438. 找到字符串中所有字母异位词(滑动窗口+数组模拟hash)

原题链接

这题具体解题思路在力扣刷题记录-双指针解决数组问题中的滑动窗口部分。

代码如下:

class Solution {public List<Integer> findAnagrams(String s, String p) {int sLen=s.length(),pLen=p.length();List<Integer> res=new ArrayList<>();int[] need=new int[26];int[] window=new int[26];int count=0;for(int i=0;i<pLen;i++)++need[p.charAt(i)-'a'];for(int i=0;i<26;i++){if(need[i]!=0)count++;}int wStart=0,wEnd=0;int valid=0;//window中能与need数量一样的字母数while(wEnd<sLen){char c=s.charAt(wEnd++);//首先确定加入窗口的这个字符有没有在p中if(need[c-'a']!=0){//如果该字符在p中++window[c-'a'];//窗口内该字符数量+1if(window[c-'a']==need[c-'a'])++valid;//当数量加到与p中相同}//符合题目要求的窗口长度一定和p长度相同,当长度超过时,需要从左边缩小if(wEnd-wStart>pLen){char l=s.charAt(wStart++);//左边界字符要在p中,才需要修改窗口中有关异位词的数据if(need[l-'a']!=0){//如果移出窗口的字符在窗口中时,该字符数量和p中相同//那么其移出之后,符合条件的字符数量-1if(window[l-'a']==need[l-'a'])--valid;--window[l-'a'];//窗口内该字符数量-1}}//只有长度相同,并且符合要求的字符数量相同时,窗口内才是异位词if(wEnd-wStart==pLen&&valid==count)res.add(wStart);}return res;}
}

LeetCode 383. 赎金信(数组模拟hash)

原题链接

代码如下:

/**
这题考察的还是异位词的概念,ransomNote能由magazine里面的字符构成,一定要满足magazine中拥有ransomNote中的所有字符(magazine只会更多,而且可能有别的字符,即ransomNote中没有的字符),因此只需要判断magazine中,同ransomNote一样的字符的数量是否超过ransomNote即可。*/class Solution {public boolean canConstruct(String ransomNote, String magazine) {int[] hash=new int[26];for(char c:ransomNote.toCharArray()){++hash[c-'a'];}for(char c:magazine.toCharArray()){--hash[c-'a'];}for(int i=0;i<26;i++){//如果magazine字符减完还有剩,说明其无法覆盖ransomNoteif(hash[i]>0)return false;}return true;}
}

LeetCode 1002. 查找共用字符(数组模拟hash)

原题链接

查找所有单词的共用字符,其实就是将所有当次拆分成一个个字符,统计单词中所有字符的出现次数,找到所有单词中各个字符出现的最小次数。

代码如下:

class Solution {public List<String> commonChars(String[] words) {int[] hash=new int[26];//遍历第一个单词,统计字母出现次数作为初始标准for(char c:words[0].toCharArray()){++hash[c-'a'];}for(int i=1;i<words.length;i++){//统计其它单词字母出现次数int[] otherWordHash=new int[26];for(char c:words[i].toCharArray()){++otherWordHash[c-'a'];}for(int j=0;j<26;j++){//对所有单词,取各个字母出现的最小次数hash[j]=Math.min(hash[j],otherWordHash[j]);}}List<String> res=new ArrayList<>();for(int i=0;i<26;i++){//注意这里是while,字母出现次数可能会大于1,只要有几次就要返回几个while(hash[i]!=0){res.add(String.valueOf((char)(i+'a')));--hash[i];}}return res;}
}

LeetCode 349. 两个数组的交集(数组模拟/HashSet)

原题链接

可以使用数组模拟,也可以利用HashSet的不允许有重复元素的特点。

解法一(数组模拟):

//用两个数组模拟哈希,时间O(m+n+1001),空间O(1001)
//如果数组中数字值非常大,就不适合用这个,可以使用HashSet
class Solution {public int[] intersection(int[] nums1, int[] nums2) {int[] hash1=new int[1001];//数字范围0-1000for(int num:nums1){++hash1[num];}int[] hash2=new int[1001];for(int num:nums2){++hash2[num];}List<Integer> list=new ArrayList<>();for(int i=0;i<1001;i++){//当两个数组都有这个数字时if(hash1[i]!=0&&hash2[i]!=0){list.add(i);}}int n=list.size();int[] res=new int[n];for(int i=0;i<n;i++){res[i]=list.get(i);}return res;}
}

解法二:(HashSet)

//用两个HashSet,因为HashSet基于 HashMap 来实现的,是一个不允许有重复元素的集合
//时间O(m+n),其中 m和n分别是两个数组的长度。使用两个集合分别存储两个数组中的元素需要
//O(m+n) 的时间,遍历较小的集合并判断元素是否在另一个集合中需要 O(min(m,n)) 的时间
//因此总时间复杂度是 O(m+n)
//空间空间复杂度:O(logm+logn),其中m和n分别是两个数组的长度。
//空间复杂度主要取决于排序使用的额外空间。
class Solution {public int[] intersection(int[] nums1, int[] nums2) {Set<Integer> set1 = new HashSet<>();Set<Integer> set2 = new HashSet<>();      for(int num:nums1){set1.add(num);}//set1中无重复元素for(int num:nums2){if(set1.contains(num)){//set1中有nums2的数字,就是交集元素set2.add(num);//set2存储交集元素}}int[] res = new int[set2.size()];int index=0;for(int num:set2){res[index++] = num;}return res;}
}

LeetCode 3. 无重复字符的最长子串(同剑指 Offer 48. 最长不含重复字符的子字符串 与 剑指 Offer II 016. 不含重复字符的最长子字符串)

原题链接

2023.05.30 三刷

思路:
1.题目只要求长度,可以用maxLen来记录遍历过程中的最大长度;
2.利用滑动窗口,窗口内的字符不重复,那么窗口大小就是不含有重复字符的最大长度
3.如何保证窗口内字符不重复?–可以用数组模拟hash,用来记录窗口内各种字符的数量
4.窗口扩张–模拟hash数组对应+1,窗口长度+1,窗口右边界+1
5.窗口收缩–当前面扩张时进入窗口的字符数量大于1收缩,窗口长度-1,模拟hash数组对应-1,左边界+1.

代码如下:

//时间O(n),空间O(字符集大小,一般为128)
class Solution {public int lengthOfLongestSubstring(String s) {int maxLen=0;//最大长度int[] hash=new int[128];int l=0,r=0;int sLen=s.length();while(r<sLen){char c=s.charAt(r++);//窗口左闭右开(一开始r++)++hash[c];while(hash[c]>1){char cc =s.charAt(l++);--hash[cc];}maxLen=maxLen>r-l ? maxLen:r-l;//收缩之后窗口才符合没有重复字符的要求}return maxLen;}
}

HashMap

LeetCode 49. 字母异位词分组(HashMap)

原题链接

这题主要考察对HashMap的使用,有两种方法。

第一种:对每个单词转成字符数组,再利用Arrays.sort()方法进行排序,再将排序后的字符数组转回字符串,用排序后的字符串作为hashmap的key值进行映射,value值为最初的单词字符串。

时间复杂度:O(nklogk),n为字符串数量,k为字符串最大长度,需要遍历n个字符串,对于每个字符串需要klogk的时间进行排序,哈希表更新时间复杂度为O(1),总时间复杂度为O(nklogk);

空间复杂度:O(nk),需要用哈希表存下所有的字符串。

第二种:统计单词中每个字母出现次数,并且把字母从小到大,每个字母后面跟着这个字母在这个单词中出现的次数,拼接成字符串,如acbccb–>a1b2c3。这样拼接后的字符串作为hashmap的key值,去存储value

解法一代码如下:

//解法1
class Solution {public List<List<String>> groupAnagrams(String[] strs) {//key存储HashMap<String,List<String>> hashmap =new HashMap<>();for(String str:strs){char[] c=str.toCharArray();Arrays.sort(c);//将字符数组按字母顺序重新排序//重新排序后的字符数组转换为字符串,作为key值String key=new String(c);//需要获取这个key值对应的list,可能为空,为空就新创一个ListList<String> list= hashmap.getOrDefault(key,new ArrayList());list.add(str);//这个str对应的是当前的key值,吧str加入key值对应的listhashmap.put(key,list);//再把list放回hashmap}List<List<String>> res=new ArrayList<List<String>>();for(List value:hashmap.values()){res.add(value);}return res;//上面的操作等价于:return new ArrayList<List<String>>(hashmap.values());}
}

解法二代码如下:

 class Solution {public List<List<String>> groupAnagrams(String[] strs) {//key为单词中字母及其出现的次数拼接而成的字符串HashMap<String,List<String>> hashmap =new HashMap<>();for(String str:strs){int n=str.length();int[] count=new int[26];//存储当前字符串中各字母出现次数for(int i=0;i<n;i++){++count[str.charAt(i)-'a'];}//接下来需要拼接字符串,为了效率,使用StringBuilderStringBuilder sb=new StringBuilder();for(int i=0;i<26;i++){if(count[i]!=0){sb.append((char)(i+'a'));sb.append(count[i]);}}String key=sb.toString();//需要获取这个key值对应的list,可能为空,为空就新创一个ListList<String> list= hashmap.getOrDefault(key,new ArrayList<String>());list.add(str);//这个str对应的是当前的key值,吧str加入key值对应的listhashmap.put(key,list);//再把list放回hashmap}List<List<String>> res=new ArrayList<List<String>>();for(List value:hashmap.values()){res.add(value);}return res;//上面的操作等价于:return new ArrayList<List<String>>(hashmap.values());}
}

LeetCode 76. 最小覆盖子串(HashMap/数组模拟hash)

原题链接

2023/06/02 三刷

LeetCode 76. 最小覆盖子串(同剑指 Offer II 017. 含有所有字符的最短字符串)

原题链接

2023/06/02 三刷

这题是相对复杂的滑动窗口题,掌握之后,套用模板再做后面的题就会比较容易了。

主要需要解决的问题:
1、什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?

2、什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?

3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

关于窗口边界左闭右开的选择理由:

理论上可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。

代码如下:

//1.HashMap写法
class Solution {public String minWindow(String s, String t) {/*注意:里面value存储的类型是包装类,比较value时不能用==,而是要用equals*/Map<Character,Integer> need=new HashMap<>();//存储t串字符,及对应字符出现次数Map<Character,Integer> window=new HashMap<>();//存储窗口内字符,及对应出现次数/** 最开始要先遍历t串,统计每个字符出现的次数(以键值对形式存储)*/for(char c:t.toCharArray()){//getOrDefault(c,0)作用:如果c这个键上有值,则获取其value;若无值,则赋0//后面补上的+1则表示,遍历到c这个字符,就将c对应的value+1;need.put(c,need.getOrDefault(c,0)+1);}int valid=0;//记录窗口中,符合需求的字符的个数int wStart=0,wEnd=0;//窗口边界,左闭右开[start,end),初始[0,0)为空int wLen=100001;//记录窗口大小int subStart=0;//最后返回的子串的边界(substring,左闭右开)/**开始滑动窗口代码 */while(wEnd<s.length()){/**①窗口扩张,右边界移动 */char c=s.charAt(wEnd);//记录新加入窗口内的字符wEnd++;///**②接下来更新窗口内数据*///判断当前字符c是不是t串中需要的,如果字符c是t串中需要的if(need.containsKey(c)){//就要把窗口内对应的字符数量+1window.put(c,window.getOrDefault(c,0)+1);//加完后需要看这个字符的数量达到要求了没,达到了说明满足要求的字符数量+1if(window.get(c).equals(need.get(c)))valid++;}/**③然后就需要考虑窗口缩小问题了,即当窗口内元素符合要求时,左边界前进 *///当valid值达到need中包含的字符数量时,说明窗口已经涵盖了t所有字符了while(valid==need.size()){//记录下当前窗口大小,如果更小//本来wEnd-wStart+1才是窗口长度,但是在最前面wEnd已经向前了(左闭右开)//所以这里不用+1就是真实窗口长度if(wEnd-wStart<wLen){wLen=wEnd-wStart;//就记录更小的窗口长度subStart=wStart;//并且记录更小字串的起始位置(方便最后返回字串)}char l=s.charAt(wStart++);//记录下当前窗口左边界字符,然后窗口左边界缩小/**④开始更新缩小后的窗口内数据 *///窗口内字符可能不是t中需要的,就不用对window特别处理,是t需要的才进行处理if(need.containsKey(l)){//只有当window中字符l与need中字符l个数相同,去掉l才会导致valid-1if(window.get(l).equals(need.get(l)))valid--;//要去掉的l字符是t需要的,就要在window中将该字符的value-1window.put(l,window.get(l)-1);}}}return wLen == 100001 ? "" : s.substring(subStart,subStart+wLen);}
}

此外可以用数组模拟hash方法,思路与HashMap一样。代码如下:

// 2.数组模拟hash,字符集大小为k,时间O(tLen+sLen+k),空间O(k)
class Solution {public String minWindow(String s, String t) {int sLen=s.length(),tLen=t.length();int[] need=new int[128];for(int i=0;i<tLen;i++){char c=t.charAt(i);++need[c];}int tCount=0;for(int i=0;i<128;i++){if(need[i]!=0)++tCount;}int[] window=new int[128];int wStart=0,wEnd=0,wCount=0;int subLen=100001,subStart=0;while(wEnd<sLen){char r=s.charAt(wEnd++);if(need[r]!=0){++window[r];if(window[r]==need[r])wCount++;}while(wCount==tCount){if(wEnd-wStart<subLen){subStart=wStart;subLen=wEnd-wStart;}char l=s.charAt(wStart++);if(need[l]!=0){if(window[l]==need[l]){--wCount;}--window[l];}}}return subLen==100001 ? "" : s.substring(subStart,subStart+subLen);}
}

LeetCode 350. 两个数组的交集 II(HashMap/先排序+双指针)

原题链接

这题对空间的要求会更严格一些,所以无法用数组模拟hash的方法解题。

解法一:

//解法一:HashMap方法
//时间:O(m+n),m、n分别为nums1和nums2长度(需要对nums1和nums2分别遍历一次,对nums2遍历中查询hashmap操作时间复杂度为O(1))
//空间:O(min(m,n)),只需要长度最小的数组的空间的hashmap
class Solution {public int[] intersect(int[] nums1, int[] nums2) {//始终让nums1位更短的数组if(nums1.length>nums2.length){return intersect(nums2,nums1);}Map<Integer,Integer> hashmap =new HashMap<>();for(int num:nums1){//先遍历更短的数组,存入hashmaphashmap.put(num,hashmap.getOrDefault(num,0)+1);}//res长度不能定义为hashmap.size(),有可能发生只有一个键,但是值很大的情况//res长度定义为nums1长度,nums1为更短的数组,结果数组长度不会超过nums1的长度int[] res=new int[nums1.length];int index=0;for(int num:nums2){//只有当nums2元素在hashmap(nums1)中,且对应个数超过0,才记录这个元素if(hashmap.getOrDefault(num,0)>0){res[index++]=num;hashmap.put(num,hashmap.get(num)-1);}}//res为nums1长度,实际交集元素个数可能达不到,只要截取实际存入的那部分即可return Arrays.copyOfRange(res,0,index);}
}

解法二:

//解法二:先排序+双指针
//时间O(mlogm+nlogn):对1和2数组排序O(mlogm+nlogn),再双指针遍历O(min(m,n))
//空间O(min(m,n))
class Solution {public int[] intersect(int[] nums1, int[] nums2) {int m=nums1.length,n=nums2.length;Arrays.sort(nums1);Arrays.sort(nums2);int index=0,index1=0,index2=0;int[] res=new int[Math.min(m,n)];//不相等时,小的前进一步,相等时,添加进结果数组,并且所有指针前进//只要有一个数组遍历完就要结束 while(index1<m&&index2<n){if(nums1[index1]==nums2[index2]){res[index++]=nums1[index1];++index1;++index2;}else if(nums1[index1]<nums2[index2]){++index1;}else if(nums1[index1]>nums2[index2]){++index2;}}return Arrays.copyOfRange(res,0,index);}
}
  • 如果给定的数组已经排好序呢?你将如何优化你的算法?
    ---- 双指针法

  • 如果 nums1的大小比 nums2 小,哪种方法更优?
    ---- HashMap法,无序排序,只要分别遍历

  • 如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
    ---- 如果nums2的元素存储在磁盘上,磁盘内存是有限的,并且不能一次加载所有的元素到内存中。那么就无法高效地对nums2进行排序,因此推荐使用方法一而不是方法二。在方法一中,nums2只关系到查询操作,因此每次读取 nums2中的一部分数据,并进行处理即可。


LeetCode 1. 两数之和(HashMap)

原题链接

用key存元素的大小,value存元素下标
在遍历数组过程中,去hashmap中查询有没有和当前元素匹配的key,如果有,就可以配对,把各自下标存入res。

代码如下:

//时间O(n),空间O(n)
class Solution {public int[] twoSum(int[] nums, int target) {int[] res=new int[2];Map<Integer,Integer> hashmap=new HashMap<>();for(int i=0;i<nums.length;i++){int tmp=target-nums[i];if(hashmap.containsKey(tmp)){res[0]=hashmap.get(tmp);res[1]=i;}hashmap.put(nums[i],i);}return res;}
}

LeetCode 560. 和为 K 的子数组

原题链接

2023.05.31 一刷

前缀和+HashMap
  遍历数组nums,计算从第0个元素到当前元素nums[i]的和,用哈希表保存出现过的累积和preSum的次数。如果preSum - k在哈希表中出现过,则代表从当前下标i往前有连续的子数组的和为k。

时间:只需要遍历nums一次–O(n)–用时22ms,击败89.7%
空间:需要用hashmap存储前缀和–O(n)–内存消耗44.9MB,击败48.71%

官方题解:

代码如下:

//前缀和+HashMap
class Solution {public int subarraySum(int[] nums, int k) {// key存前缀和,value存对应前缀合出现的次数HashMap<Integer,Integer> hashmap=new HashMap<>();int preSum=0;int count=0;hashmap.put(0,1);//这句很重要,原因看下面注释for(int i=0;i<nums.length;i++){// preSum记录nums[0~i]之间的和preSum+=nums[i];// preSum[i]-preSum[j-1]=k,包含preSum-k键值对说明nums[j~i]的区间和为k,此时需要看0~i之间有多少次前缀和为preSum[j-1],count加上对应次数即可// put(0,1)补上了nums[0~i]区间和为k的情况(preSum=k),此时count+1if(hashmap.containsKey(preSum-k)){count+=hashmap.get(preSum-k);}hashmap.put(preSum,hashmap.getOrDefault(preSum,0)+1);}return count;}
}

LeetCode 454. 四数相加 II(分组+hashMap)

原题链接

注意这题的四个数来自四个独立的数组,和15.三数之和以及18.四数之和不一样

如果暴力4层for循环遍历四个数组会超时间,可以把ABCD分成两组,AB一组,CD一组

用两层for循环遍历AB数组,求A[i]+B[j],将A[i]+B[j]作为key值,它们出现的次数作为value值,每出现一次key值对应的value值就+1;

再用另外两层for循环遍历CD数组,求C[k]+D[l],在hashmap中寻找有没有【0-(C[k]+D[l])】这样的键,如果hashmap中存在这样的键,说明这它们组合起来相加为0,这时只要把A[i]+B[j]键对应的value值(A[i]+B[j]这样的值出现的次数)加入结果即可。

代码如下:

//分组+哈希:时间O(n^2),空间O(n^2)
class Solution {public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {Map<Integer,Integer> hashmap=new HashMap<>();for(int num1:nums1){for(int num2:nums2){int tmp=num1+num2;hashmap.put(tmp,hashmap.getOrDefault(tmp,0)+1);}}int res=0;for(int num3:nums3){for(int num4:nums4){int tmp=num3+num4;if(hashmap.containsKey(0-tmp)){res+=hashmap.get(0-tmp);}}}return res;}
}

LeetCode 15. 三数之和(双指针+剪枝+去重)

原题链接

题目中要求不能包含重复的三元组,所以就不能简单照搬454.四数之和Ⅱ的分组哈希做法

先将数组排序,用i作为索引遍历nums数组,对每一个i,left=i+1,right=nums.length-1;

left和right向中间收缩,当sum<0,说明当前三个数太小,nums[i]固定,只能增大left;同理sum>0,减小right。

另外在遍历的时候需要注意三元组的去重。

class Solution {public List<List<Integer>> threeSum(int[] nums) {Arrays.sort(nums);//先排序才能用双指针List<List<Integer>> res=new ArrayList<>();for(int i=0;i<nums.length-2;i++){//三元组第一个数都比0大,后面加上后两个数不可能等于0,所有后面的都不用考虑if(nums[i]>0)break;//当前数和前一个一样,那么得到的三元组也会和前一个数得到的三元组一样,直接跳过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){//符合条件,加入res,索引向中间移动res.add(Arrays.asList(nums[i],nums[left++],nums[right--]));//如果nums[left]和nums[left-1]一样,得到的三元组也会一样//为了去重,直接跳过当前这个数。但是要在left<right范围内进行while(left<right&&nums[left]==nums[left-1])left++;while(left<right&&nums[right]==nums[right+1])right--;}else if(sum<0){left++;}else if(sum>0){right--;}}}return res;}
}

LeetCode 18. 四数之和(双指针降复杂度+去重+剪枝)

原题链接

其实就是在三数之和的基础上,再多一个指针j,三数之和中是nums[i]为确定值,这题里面就用nums[i]+nums[j]作为确定值,然后再利用首尾两个指针left和right向中间收缩。

中间有一些剪枝以及去重操作是需要注意的,可以很好提高代码效率

代码如下:

class Solution {public List<List<Integer>> fourSum(int[] nums, int target) {List<List<Integer>> res=new ArrayList<>();Arrays.sort(nums);int n=nums.length;for(int i=0;i<n-3;i++){//去重if(i>0&&nums[i]==nums[i-1])continue;//剪枝,当最小的4个数相加都超过,后面肯定找不到符合条件的if((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target)break;//剪枝,当当前最大的4个数都小于,当前nums[i]肯定不够,直接用下一个if((long)nums[i]+nums[n-3]+nums[n-2]+nums[n-1]<target)continue;for(int j=i+1;j<n-2;j++){//与i同样道理,去重if(j>i+1&&nums[j]==nums[j-1])continue;//与i一样的道理,剪枝if((long)nums[i]+nums[j]+nums[j+1]+nums[j+2]>target)break;if((long)nums[i]+nums[j]+nums[n-2]+nums[n-1]<target)continue;int left=j+1,right=n-1;while(left<right){//四个10亿相加会爆intlong sum=(long)nums[i]+nums[j]+nums[left]+nums[right];if(sum==target){res.add(Arrays.asList(nums[i],nums[j],nums[left++],nums[right--]));//去重while(left<right&&nums[left]==nums[left-1])left++;while(left<right&&nums[right]==nums[right+1])right--;}else if(sum<target){left++;}else if(sum>target){right--;}}}}return res;}
}

HashSet

LeetCode 128. 最长连续序列

原题链接

2023.05.28 一刷

思路:
  想要找到以当前num为开始的最长序列,就判断从num开始,每次+1的数在不在set中,利用set.contains(curNum)方法可以在O(1)时间判断,直到curNum不在set为止,记录每个num对应的序列长度,每个num的序列长度与maxLen进行比较,取较大值。但是这样每个num都需要暴力遍历,时间复杂度高。

优化:
可以在遍历到每个num的时候,判断num-1在不在set中:

  • 如果num-1在set中,说明以num开始的序列不可能是最长的(从num-1开始的更长),可以直接跳过当前num,遍历下一个;
  • 如果num-1不在set中,则需要按前面提到的方法计算从num开始的序列的长度。

  时间复杂度:O(n)。外层循环需要 O(n) 的时间复杂度,只有当一个数是连续序列的第一个数的情况下才会进入内层循环,然后在内层循环中匹配连续序列中的数,因此数组中的每个数只会进入内层循环一次。
  空间复杂度:O(n)。HashSet存储数组中所有元素。

代码如下:

class Solution {public int longestConsecutive(int[] nums) {Set<Integer> set=new HashSet<>();//利用HashSet进行去重for(int num:nums){set.add(num);}//记录最大长度int maxLen=0;for(Integer num:set){//如果num-1在set中,说明以num开始的序列不可能是最长的(从num-1开始的更长)//如果num-1不在set中,则需要计算从num开始的序列的长度if(!set.contains(num-1)){int curLen=1;//目前以num开始的序列长度为1int curNum=num;//从num开始,每次+1,判断后面的数在不在,在的话就将序列长度+1while(set.contains(curNum+1)){++curLen;++curNum;}maxLen=Math.max(curLen,maxLen);}}return maxLen;}
}

LeetCode 202. 快乐数(HashSet/快慢指针判断是否成环)

原题链接

法一:

//法一:HashSet,只要下一个数字在set中存在,说明有循环
//时间O(logn),空间O(logn)
class Solution {public boolean isHappy(int n) {Set<Integer> set=new HashSet<>();//n=1就要返回true,set包含n说明无限循环了while(n!=1&&!set.contains(n)){set.add(n);n=getNext(n);}//退出循环有两种条件:n=1或set.contains(n)return n==1;//如果n!=1,说明set.contains(n)}//获取n的下一个数字(逐位的平方和)public int getNext(int n){int sum=0;while(n>0){int mod=n%10;sum+=mod*mod;n/=10;}return sum;}
}

法二:

//法二:快慢指针(判断有无环)
//时间O(logn),空间O(1)
class Solution {public boolean isHappy(int n) {int slow=n;int fast=getNext(n);//fast=1就要返回true,fast=slow说明存在逻辑上的环,会死循环while(fast!=1&&fast!=slow){slow=getNext(slow);fast=getNext(getNext(fast));}//退出循环有两种条件:fast=1或fast==slowreturn fast==1;//如果fast!=1,说明fast==slow,有环}//获取n的下一个数字(逐位的平方和)public int getNext(int n){int sum=0;while(n>0){int mod=n%10;sum+=mod*mod;n/=10;}return sum;}
}

LeetCode 349. 两个数组的交集(数组模拟/HashSet)

原题链接

可以使用数组模拟,也可以利用HashSet的不允许有重复元素的特点。

解法一(数组模拟):

//用两个数组模拟哈希,时间O(m+n+1001),空间O(1001)
//如果数组中数字值非常大,就不适合用这个,可以使用HashSet
class Solution {public int[] intersection(int[] nums1, int[] nums2) {int[] hash1=new int[1001];//数字范围0-1000for(int num:nums1){++hash1[num];}int[] hash2=new int[1001];for(int num:nums2){++hash2[num];}List<Integer> list=new ArrayList<>();for(int i=0;i<1001;i++){//当两个数组都有这个数字时if(hash1[i]!=0&&hash2[i]!=0){list.add(i);}}int n=list.size();int[] res=new int[n];for(int i=0;i<n;i++){res[i]=list.get(i);}return res;}
}

解法二:(HashSet)

//用两个HashSet,因为HashSet基于 HashMap 来实现的,是一个不允许有重复元素的集合
//时间O(m+n),其中 m和n分别是两个数组的长度。使用两个集合分别存储两个数组中的元素需要
//O(m+n) 的时间,遍历较小的集合并判断元素是否在另一个集合中需要 O(min(m,n)) 的时间
//因此总时间复杂度是 O(m+n)
//空间空间复杂度:O(logm+logn),其中m和n分别是两个数组的长度。
//空间复杂度主要取决于排序使用的额外空间。
class Solution {public int[] intersection(int[] nums1, int[] nums2) {Set<Integer> set1 = new HashSet<>();Set<Integer> set2 = new HashSet<>();      for(int num:nums1){set1.add(num);}//set1中无重复元素for(int num:nums2){if(set1.contains(num)){//set1中有nums2的数字,就是交集元素set2.add(num);//set2存储交集元素}}int[] res = new int[set2.size()];int index=0;for(int num:set2){res[index++] = num;}return res;}
}

力扣刷题记录--哈希表相关题目相关推荐

  1. 力扣刷题记录-动态规划问题总结

    百度百科里对于动态规划问题是这样解释的: 在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果.因此各个阶段 ...

  2. 力扣刷题记录-回溯算法相关题目

    首先介绍一下回溯算法 回溯通常在递归函数中体现,本质也是一种暴力的搜索方法,但可以解决一些用for循环暴力解决不了的问题,其应用有: 1.组合问题: 例:1 2 3 4这些数中找出组合为2的组合,有1 ...

  3. 力扣刷题记录--位运算问题

    这里写目录标题 一.n&(n-1) 1. 求一个数的二进制表示中的1的个数 力扣 191. 位1的个数 AcWing 801. 二进制中1的个数 2. 判断一个数是否是2的方幂 二.n& ...

  4. 力扣刷题记录-单调栈相关题目

    单调栈是指栈里的元素保持升序或者降序. 判别是否需要使用单调栈:通常是一维数组里面,需要寻找一个元素左边或者右边第一个比自己大或者小的元素的位置,则可以考虑使用单调栈:这样的时间复杂度一般为O(n). ...

  5. 力扣刷题记录_字符串(自学)

    字符串 一.字符串 1.反转字符串(力扣344) 2.反转字符串 II(力扣541) 3.替换空格(剑指 Offer 05) 4.翻转字符串里的单词(力扣151) 5.左旋转字符串(剑指 Offer ...

  6. python力扣刷题记录——204. 计数质数

    题目: 统计所有小于非负整数 n 的质数的数量. 方法一: 暴力法 class Solution:def countPrimes(self, n: int) -> int:count = 0if ...

  7. 力扣刷题记录---二分法

    一般的二分查找应用的地方都是在一个单调有序序列里面进行值的搜索,,用中间点进行区域划分,当中间值大于目标值target,说明目标值在左区域,反之则在右区域.这样不断缩小区域,每次搜索区域都只要当前范围 ...

  8. 力扣刷题记录---快排算法

    AcWing 785. 快速排序 对快排算法思想就不描述了,针对快排递归过程中边界的取值做了总结: x为每次递归中,选取的基准数(枢轴) 如果x = q[i]或者x = q[l + r >> ...

  9. 力扣刷题记录---归并排序

    AcWing 787. 归并排序 归并排序代码模板如下: /* 归并排序 时间O(nlogn),空间O(n) */import java.util.*; public class Main{publi ...

最新文章

  1. R语定义函数对宽分布(wide distribution )、有偏分布(skew distribution)的数据进行对数变换(符号对数变换函数、signed log transformation)
  2. 些许注意事项(初学)
  3. 微信小程序网络请求代码片段
  4. 冒泡排序python实现
  5. 【git】.gitignore文件
  6. uvm 形式验证_UVM基础
  7. 开始好好学习了,生活得有追求~~~
  8. python的作者叫什么_作者的来历是什么?
  9. 【Flink】Could not connect to BlobServer at address
  10. border_mode
  11. 本周小结!(二叉树系列三)
  12. linux定时器的实现方法
  13. 计算机毕设分词,基于词表的中文分词算法
  14. Efficient Diffusion Models for Vision: A Survey
  15. CAD 关于打断和合并对象
  16. springboot自动装配原理
  17. 如何使用计算机计算平方面积,平方怎么算面积公式-3种方法详解须知
  18. Java日志门面担当-SLF4J
  19. 耀月家族公会部门规定职责等2011-11-26
  20. unity 打砖块—休闲小游戏,摸鱼必备(完整代码)

热门文章

  1. 阿里云「海外战事」再升级:从出海到本土化,再到国际化
  2. 关于页码的几种设置方法
  3. 微信小程序用picker-view 实现时间的选择 及加减
  4. 正则表达式:pattern=[^\\w]---------总结------------
  5. pandas第四章——变形
  6. 如何产生满足高斯分布的随机数据
  7. vivo怎么调时间_还原专业声音,vivo影音耳机让你化身quot;K歌达人quot;
  8. 上位机工业协议-S7COMM
  9. 晶振、时钟信号、锁相环、分频器
  10. EVO工具的安装和使用(window10)