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

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

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'];//窗口内该字符数量+1
                if(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中相同
                    //那么其移出之后,符合条件的字符数量-1
                    if(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字符减完还有剩,说明其无法覆盖ransomNote
            if(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-1000
        for(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,可能为空,为空就新创一个List
            List<String> list= hashmap.getOrDefault(key,new ArrayList());
            list.add(str);//这个str对应的是当前的key值,吧str加入key值对应的list
            hashmap.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>(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'];
            }
            //接下来需要拼接字符串,为了效率,使用StringBuilder
            StringBuilder 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,可能为空,为空就新创一个List
            List<String> list= hashmap.getOrDefault(key,new ArrayList<String>());
            list.add(str);//这个str对应的是当前的key值,吧str加入key值对应的list
            hashmap.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>(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)){
                //就要把窗口内对应的字符数量+1
                window.put(c,window.getOrDefault(c,0)+1);
                //加完后需要看这个字符的数量达到要求了没,达到了说明满足要求的字符数量+1
                if(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-1
                    if(window.get(l).equals(need.get(l)))valid--;
                    //要去掉的l字符是t需要的,就要在window中将该字符的value-1
                    window.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){//先遍历更短的数组,存入hashmap
            hashmap.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%

官方题解:
力扣刷题记录--哈希表相关题目_第1张图片
代码如下:

//前缀和+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+1
            if(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
                    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亿相加会爆int
                    long 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开始的序列长度为1
                int curNum=num;
                //从num开始,每次+1,判断后面的数在不在,在的话就将序列长度+1
                while(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==slow
        return 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-1000
        for(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,散列表,数据结构)