算法训练第七天 | 第三章 哈希表 454,383,15,18

代码随想录算法训练第七天,继续哈希表

今日任务
● 454.四数相加II
● 383. 赎金信
● 15. 三数之和
● 18. 四数之和
● 总结

454. 四数相加II

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

思路:
将四个数组 简化成two sum 问题, 具体步骤:

  1. 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
  2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
  4. 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
  5. 最后返回统计值 count 就可以了
class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        Map<Integer, Integer> adSum = new HashMap<Integer, Integer>();
        int resCount = 0;  // result
        //save sum of A, B, and occurrence
        for(int i = 0; i < nums1.length; i++){
            for(int j = 0; j < nums2.length; j++){
                int sum = nums1[i] + nums2[j];
                if(adSum.containsKey(sum)){
                    //put() will override old value
                    adSum.put(sum, adSum.get(sum) + 1);
                }else{
                    adSum.put(sum, 1);
                }
            }
        }
        //iterate C and D, find if 0-(c+d) exist in map 
        for(int i = 0; i < nums3.length; i++){
            for(int j = 0; j < nums4.length; j++){
                int sum = nums3[i] + nums4[j];
                int target = 0 - sum;
                if(adSum.containsKey(target)){
                    resCount += adSum.get(target);
                }
                
            }
        }
    return resCount;
            
    }
}

时间复杂度: O(nn)
空间复杂度: O(n
n) 哈希映射需要的空间, 最差情况下A[i] + B[j]值均不相同,需要n*n

  1. 赎金信
    给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。

(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)

你可以假设两个字符串均只含有小写字母。

本题 和 242.有效的字母异位词 是一个思路
因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        if(ransomNote.length() > magazine.length()) return false;
        
        int[] avaliableLetters = new int[26];

        //for(char c : magazine.toCharArray())
        for(int i = 0; i < magazine.length(); i++){
            
            avaliableLetters[magazine.charAt(i) - 'a']++;
        }
        
        for(int i = 0; i < ransomNote.length(); i++){
            int index = ransomNote.charAt(i) - 'a';
            avaliableLetters[index]--;
            if(avaliableLetters[index] < 0) return false;
            
        }
        return true;
    }
}

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

双指针算法:
对数组进行排序
遍历排序后数组:

  • 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回结果。
  • 对于重复元素:跳过,避免出现重复解
  • 令左指针 L=i+1,右指针 R=n−1,当 L<R 时,执行循环:
    -当 nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,R 移到下一位置,寻找新的解
    - 若和大于 0,说明 nums[R] 太大,R 左移
    - 若和小于 0,说明 nums[L] 太小,L 右移
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> resList = new ArrayList<>();
        //original order doesnt matter for return result 
        //sort the array, use left,right pointer to get sum of triplets
        Arrays.sort(nums);
        int n = nums.length;
        
        //if smallest number > 0 or largest number < 0, wont find any qualifying triplets
        if(nums[0] > 0 || nums[n - 1] < 0) return resList;
        
        for(int i = 0; i < n - 2; i++){
            //skip duplicates for first number
            if(i > 0 && nums[i] == nums[i - 1]) continue;
            //left points to the smallest number besides i
            //right points to the largest number besides i
            int left = i + 1, right = n - 1;
            
            while(left < right){
                
                int sum = nums[i] + nums[left] + nums[right];
                if(sum > 0){
                    //nums[left] + nums[right] needs to be smaller to get sum to 0
                    right--;
                }else if(sum < 0){
                    //nums[left] + nums[right] needs to be bigger to get a 0 sum
                    left++;
                }else{
                    //triplet found
                    resList.add(Arrays.asList(nums[i],nums[left],nums[right]));
                    //skip duplicates
                    while(left < right && nums[left] == nums[left + 1]) left++;
                    while(left < right && nums[right] == nums[right - 1]) right--;
                    //increment to process next combination
                    left++;
                    right--;
                }
            }
            
            
        } 
        return resList;
    }
}

时间复杂度 O(n * n) : 数组排序O(nlogn); 固定指针i 循环复杂度 O(N), 双指针遍历O(N)
空间复杂度O(1)

18. 四数之和

对于15.三数之和 (opens new window)双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,四数之和的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法。

哈希表的经典题目:454.四数相加II (opens new window),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。而454是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况。所以可以利用hashmap存储A[i] + B[j]之和,将之变形为O(n*n) 解法

思路:

四数之和与前面三数之和的思路几乎是一样的,
其实就是在三数之和的基础上多添加一个遍历的指针而已。

  • 使用四个指针(i<j<left<right)。固定最小的a和b在左边,c=b+1,d=_size-1 移动两个指针包夹求解。
    保存使得nums[i]+nums[j]+nums[left]+nums[right]==target的解。偏大时d左移,偏小时c右移。
    left和right相遇时,表示以当前的i和j为最小值的解已经全部求得。j++,进入下一轮循环j循环,当j循环结束后。 i++,进入下一轮i循环。 即(i在最外层循环,里面嵌套j循环,再嵌套双指针left,right包夹求解)。

a遍历O(N)里嵌套b遍历O(N)再嵌套c,d双指针O(N)--> O(N^3)。 比暴力法O(N^4)好些吧。

但是有一些细节需要注意,例如: 不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> resList = new ArrayList<>();
        Arrays.sort(nums);
        int n = nums.length;
        if(n < 4) return resList;
        
        int left,right;
        long sum;
        for(int i = 0; i < n - 3; i++){
            //remove duplicate
            if(i > 0 && nums[i] == nums[i - 1]) continue;
            
            for(int j = i + 1; j < n - 2; j++){
                //remove duplicate, notice j lower bound value
                if( j > i + 1 && nums[j] == nums[j - 1]) continue;
                left = j + 1;
                right = n - 1;
                
                while(left < right){
                    sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
                    if(sum < target){
                        left++;
                    }else if(sum > target){
                        right--;
                    }else{
                        //quadruplets found
                        resList.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--;
                        left++;
                        right--;
                    }
                }
            
            }
        }
        return resList;
    }
}

总结:
注意三种哈希结构的不同应用场景

  • 数组:
    242.有效的字母异位词 (opens new window)是求 字符串a 和 字符串b 是否可以相互组成,在383.赎金信 (opens new window)中是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。
    使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效!

  • set(集合)
    数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
    如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。更适合set

  • map(映射) store <key, value> pair
    使用用例: 1.两数之和, 454.四数相加
    使用数组和set来做哈希法的局限:
    数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
    set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
    map是一种<key, value>的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容