代码随想录第七天|哈希表part02--454.四数相加II、383. 赎金信、15. 三数之和、18. 四数之和

资源引用:

leetcode题目:

454.四数相加Ⅱ(454. 四数相加 II - 力扣(LeetCode))

383.赎金信(383. 赎金信 - 力扣(LeetCode))

15.三数之和(15. 三数之和 - 力扣(LeetCode))

18.四数之和(18. 四数之和 - 力扣(LeetCode))

例行碎碎念:

今天也追赶上了一些进度,虽然生病感冒,但今天很好的坚持了从早到晚的复习,秉承开源的精神我也将自己的复习资料整理出来分享给其他同学,这也给了我一些成就感!在晚上虽然很疲惫,但还是坚持完成了一天的代码随想录,哈希表的利弊变得更加清晰,数组的小而美也令我认知刷新,更重要的是自己能够很好的复用先前学到的技巧(尤其是双指针),继续加油!

454.四数相加Ⅱ(454. 四数相加 II - 力扣(LeetCode))

题目分析:

四个长度为n的整数数组,分别从中取元素组成四项元组,问有多少元组的项之和为0,最后返回符合条件的元组数,这是讨论是否存在的问题,并返回存在数量的问题,需要存储key为元素值,value为元素个数,考虑使用HashMap。

解题思路:

四个数组的长度均为n,则元组最多有n^4个。边界条件是n为零,即四个数组均为空时,直接返回0。

关键在于是四个map,还是value是四个下标。

若采用四个下标的形式进行存储,则时间复杂度极高,与暴力解法没有差别,且key值不能重复,该解法无法实现。

考虑使用类似二分法的思想,将num1和num2作为一个组合,将num3和num4作为一个组合,每个组合分别具有n^2个元组,时间复杂度从n^4缩减到n^2,第一个组合用map1存储,key为num1和num2中元素和的不同情况,value为不同情况存在的组合数;第二个组合用map2存储同理。对map1中的每个key值,从map2中检查是否存在其相反数,若存在,则将二者的value值相乘并加到返回值res上。

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        if(nums1.length==0 || nums2.length==0 || nums3.length==0 || nums4.length==0){
            return 0;
        }
        Map map1 = new HashMap<>();
        Map map2 = new HashMap<>();
        int res=0;
        for(int i : nums1){
            for(int j : nums2){
                int sum1 = i+j;
                int new_value=0;
                if(map1.containsKey(sum1)){
                    new_value = map1.get(sum1)+1;
                }else{
                    new_value = 1;
                }
                map1.put(sum1,new_value);
            }
        }
        for(int k : nums3){
            for(int l : nums4){
                int sum2 = k+l;
                int new_value=0;
                if(map2.containsKey(sum2)){
                    new_value = map2.get(sum2)+1;
                }else{
                    new_value = 1;
                }
                map2.put(sum2,new_value);
            }
        }
        for (Map.Entry entry : map1.entrySet()) {
            int sum1 = entry.getKey();
            if(map2.containsKey(-sum1)){
                res+=(entry.getValue()*map2.get(-sum1));
            }
        }
        return res;
    }
}

进一步简化:

c和d遍历时无需放入哈希表中,为减少开销,若存在(c,d)组合的两数之和的相反数存在于map1中,直接取map1对应的value加到计数res上即可。

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        if(nums1.length==0 || nums2.length==0 || nums3.length==0 || nums4.length==0){
            return 0;
        }
        Map map1 = new HashMap<>();
        int res=0;
        for(int i : nums1){
            for(int j : nums2){
                int sum1 = i+j;
                int new_value=0;
                if(map1.containsKey(sum1)){
                    new_value = map1.get(sum1)+1;
                }else{
                    new_value = 1;
                }
                map1.put(sum1,new_value);
            }
        }
        for(int k : nums3){
            for(int l : nums4){
                int sum2 = k+l;
                if(map1.containsKey(-sum2)){
                    res+=map1.get(-sum2);
                }
            }
        }
        return res;
    }
}

再进一步简化:

map.getOrDefault(sum, 0) 的使用:如果存在key为sum的值则返回其value,否则返回默认值(此处参数将其规定为0)

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        int res = 0;
        Map map = new HashMap();
        //统计两个数组中的元素之和,同时统计出现的次数,放入map
        for (int i : nums1) {
            for (int j : nums2) {
                int sum = i + j;
                map.put(sum, map.getOrDefault(sum, 0) + 1);
            }
        }
        //统计剩余的两个元素的和,在map中找是否存在相加为0的情况,同时记录次数
        for (int i : nums3) {
            for (int j : nums4) {
                res += map.getOrDefault(0 - i - j, 0);
            }
        }
        return res;
    }
}

383.赎金信(383. 赎金信 - 力扣(LeetCode))

题目分析:

两个字符串,判断ransomNote中的字符是否全部包含在magazine中,且每个字符是否有其一一对应,即magazine中的每个字符只能在ransomNote,由于是存在问题且不仅需要匹配元素值还要匹配个数,考虑用HashMap解决。

解题思路:

map的键值对存储magazine中的元素值及其出现次数,遍历ransomNote中的每个字符,如果该字符在map中存在,若其value值不为0则将其对应的value值减一,若其value值为0则直接返回false;若该字符不存在,也直接返回false。最终返回true。

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        Map map = new HashMap<>();
        for(char i : magazine.toCharArray()){
            map.put(i,map.getOrDefault(i,0)+1);
        }
        for(char j : ransomNote.toCharArray()){
            if(map.getOrDefault(j,0) == 0){
                return false;
            }else{
                map.put(j,map.getOrDefault(j,0)-1);
            }
        }
        return true;
    }
}

反思总结:

灵活运用了for-each循环和map.getOrDefault()方法

然而,map的空间消耗和哈希函数的调用消耗导致:不论是时间层面还是空间层面,该题使用map都不如使用数组,使用数组更加简单有效!

也不要忘了控制边界条件!

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        // 边界条件
        if (ransomNote.length() > magazine.length()) {
            return false;
        }
        // 定义一个哈希映射数组
        int[] record = new int[26];
        // 遍历
        for(char c : magazine.toCharArray()){
            record[c - 'a'] += 1;
        }
        for(char c : ransomNote.toCharArray()){
            record[c - 'a'] -= 1;
        }
        // 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符
        for(int i : record){
            if(i < 0){
                return false;
            }
        }
        return true;
    }
}

15.三数之和(15. 三数之和 - 力扣(LeetCode))

题目分析:

由于这题要返回的是元组本身,故使用哈希表很不方便,carl哥提示用双指针法。

该题仅有一个数组nums,若长度小于3则直接返回空的List> res。

关键在于怎样在遍历过程中保证i,j,k两两不相等,且返回元组不可重复。

妙解:

为了保证元组不可重复,需要在遍历过程中避免重复使用值相同的元素,所以将数组排序!

初始化:假设已经从小到大排序,下标i从0开始遍历,left=i+1,right=nums.length-1(1.注意针对i的“去重” )

寻址:若三数之和大于0,说明偏大,right左移1位;若三数之和小于0,说明偏小,left右移1位。重复“寻址”过程,直到left=right为止,在这过程中遇到三数和为0的添入List(全程注意去重!)。

关于去重:
1.i的去重:

由于已经经过排序,如果可以减少对nums[i]相同情况的遍历,但应当注意判断条件应为nums[i] != nums[i-1],而非nums[i] != nums[i+1],因为元组不能重复,但元组内的元素值可以重复!

2.三数之和为0时,left和right相遇前的去重:

参考i的去重,可知应为nums[left]!=nums[left-1],nums[right]!=nums[right+1]吗?注意处理下标防止越界——right+1存在越界风险,且由于left和right不是锚定数所以前后去重无本质区别,考虑方便实现即可。

总结反思:

1.双指针法一定要排序

2.边界条件处理:①数组长度检查②排序后,nums[i]为正数时,则三数之和不可能为0,直接返回当前result。

class Solution {
    public List> threeSum(int[] nums) {
        List> result = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
      	    // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) { 
                return result;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {  // 去重a
                continue;
            }
            int left = i + 1;
            int right = nums.length - 1;
            while (right > left) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum > 0) {
                    right--;
                } else if (sum < 0) {
                    left++;
                } else {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
            		    // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;
                    
                    right--; 
                    left++;
                }
            }
        }
        return result;
    }
}

18.四数之和(18. 四数之和 - 力扣(LeetCode))

题目分析:

/*与454.四数相加Ⅱ不同之处在于:①在同一个数组中取数,由于下标不可重复,因此相较于对4个数组操作的难度增大②返回值是满足条件的四元组,而非元组数*/

在15.三数之和的基础上仍然使用双指针。

解题思路:

先将数组排序,a从0下标开始遍历,注意剪枝和去重

嵌套循环b从a+1开始遍历,注意剪枝和去重

初始化left=b+1,right=nums.length-1,

循环不变量是right>left。

b在nums.length-3处为最后一次,a在nums.lengh-4处为最后一次

其他步骤参考15.三数之和。

tips:同样注意检查剪枝条件:①nums数组长度不小于4②此题target并非确定的值,需要进一步约束剪枝条件为nums[i] > target && (nums[i] >=0 || target >= 0)

总结反思:

1.双指针法就是将类似N数之和的问题的时间复杂度从O(n^N)降为O(n^(N-1))

2.注意数值大小,为防止溢出,需要使用长整型long记录sum

import java.util.*;
public class Solution {
    public List> fourSum(int[] nums, int target) {
        Arrays.sort(nums);  // 排序数组
        List> result = new ArrayList<>();  // 结果集
        for (int k = 0; k < nums.length-3; k++) {
            // 剪枝处理
            if (nums[k] > target && nums[k] >= 0) {
                break;
            }
            // 对nums[k]去重
            if (k > 0 && nums[k] == nums[k - 1]) { //注意当k为0时k-1越界,故做此处理
                continue;
            }
            for (int i = k + 1; i < nums.length-2; i++) {
                // 第二级剪枝
                if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
                    break;
                }
                // 对nums[i]去重
                if (i > k + 1 && nums[i] == nums[i - 1]) { //与k为0时的处理类似,但重点是i为k+1时不必去重
                    continue;
                }
                int left = i + 1;
                int right = nums.length - 1;
                while (right > left) {
                    long sum = (long) nums[k] + nums[i] + nums[left] + nums[right];
                    if (sum > target) {
                        right--;
                    } else if (sum < target) {
                        left++;
                    } else {
                        result.add(Arrays.asList(nums[k], nums[i], nums[left], nums[right]));
                        // 对nums[left]和nums[right]去重
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] == nums[left + 1]) left++;
                        right--;
                        left++;
                    }
                }
            }
        }
        return result;
    }
}

你可能感兴趣的:(代码随想录算法训练营一刷,散列表,java,算法,数据结构)