Day6 力扣哈希 : 454. 四数相加 II | 383. 赎金信 | 15. 三数之和 | 18.四数之和

Day6 力扣哈希 : 454. 四数相加 II | 383. 赎金信 |

  • 454. 四数相加 II
  • 383. 赎金信
  • 15. 三数之和
  • 18.四数之和

454. 四数相加 II

本题关键 :

  1. 为什么用哈希的思路? 我感觉挺难的
  2. 用哪种哈希?

思路 :

  • 为什么用哈希的思路?
    一共四个数组, 从每个数组各取一个元素之和为0就可以. 而且不需要去掉重复元素
    做过有效异位词和两数之和之后, 我觉得所谓的当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法 是对两个东西进行操作, 将A放在哈希表里, 再去看B需要什么, 对应A里有没有, 这样就是判断A中的元素是否出现过, 而不是用两个甚至更多的for循环去一个一个的看.

    但是这道题一共有四个数组, 并不是两个东西. 所以要考虑将他们合并.
    具体操作就是遍历nums1和nums2的元素和sum1存放在map里, 再去遍历num3和num4的元素和sum2, 同时计算出其中每个元素和如果想满足sum2 + sum1 = 0的话, 需要的sum1是多少. 去map里寻找有没有这个sum1. (形成一种2 + 2 = 4的感觉)

    那可能有人会问, 为什么不是num1自己放在map里, 遍历num2 num3 num4的元素和, 再去看map里有没有呢? (形成一种 1 + 3 = 4的感觉). 如果遍历num2 num3 num4, 就需要三个for循环, 时间复杂度O(n3).而刚才的那种都是O(n2). 所以要用2+2=4.

  • 用哪种哈希?
    因为两数和的范围不确定, 所以不可能用数组. 考虑到只需要返回有几个, 而不是具体的下标, 所以一定要记录出现的次数, 所以就要用map 以两数和为Key, 出现次数为Value.

具体代码如下 :

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        HashMap<Integer, Integer> map = new HashMap<>();
        int count = 0;
        for (int a : nums1) {
            for (int b : nums2) {
                if (map.containsKey(a+b)) {
                    map.put(a+b, map.get(a+b) + 1);
                } else {
                    map.put(a+b, 1);
                }
            }
        }

        for (int c : nums3) {
            for (int d : nums4) {
                int temp = 0 - ( c + d );
                if (map.containsKey(temp)) {
                    count += map.get(temp);
                }
            }
        }
        return count;
    }
}

代码可以用getOrDefault()函数,这样更加简洁

map.getOrDefault(从哪个key取,取不到的话默认多少?)

简洁代码如下:

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        Map<Integer, Integer> hash = new HashMap<>();
        int result = 0;

        for (int a : nums1) {
            for (int b : nums2) {
                int sum = a + b;
                hash.put(sum, hash.getOrDefault(sum, 0) + 1);
            }
        }

        for (int c : nums3) {
            for (int d : nums4) {
                result += hash.getOrDefault(0 - c - d, 0);
            }
        }
        return result;
    }
}

383. 赎金信

思路 :

  • 为什么用哈希?
    类比一下 242. 有效异位词, 其实是一样的, 因为只有26个小写字母,所以都是用数组下标作为索引来使用哈希.

  • 为什么不用map呢?
    这里摘抄代码随想录中的解析 : 使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!

    还是要了解不同哈希表的底层结构, 这里再摘抄代码随想录中哈希表的基础知识.

Day6 力扣哈希 : 454. 四数相加 II | 383. 赎金信 | 15. 三数之和 | 18.四数之和_第1张图片
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

Day6 力扣哈希 : 454. 四数相加 II | 383. 赎金信 | 15. 三数之和 | 18.四数之和_第2张图片
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。

那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

其他语言例如:java里的HashMap (我上网查了java里超过8长度的就从链表变成红黑树, 那就当作java里是红黑树吧),TreeMap 都是一样的原理。可以灵活贯通。

虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。

最后给出本题代码 :
注释的两部分二选一即可, 第一部分是我第一次做的时候自己的想法, 第二部分单独写可能更清晰.

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
            int[] hash = new int[26];

            for (int i = 0; i < magazine.length(); i++) {
                hash[magazine.charAt(i) - 'a']++;
            }

            for (int i = 0; i < ransomNote.length(); i++) {
                hash[ransomNote.charAt(i) - 'a']--;
                // if (hash[ransomNote.charAt(i) - 'a'] < 0) {
                //     return false;
                // }
            }

            // for (int i = 0; i < hash.length; i++) {
            //     if (hash[i] < 0) {
            //         return false;
            //     }
            // }
            return true;
    }
}

15. 三数之和

本题关键

  1. 为什么不用哈希?
  2. 该如何去重?

思路

  • 为什么不用哈希?
    其实到最后我也不是很理解。题解里说不用哈希是因为不能对结果去重。这道题应该使用双指针。

  • 该如何去重?
    双指针还是个比较熟悉的方法,这里需要注意的是使用双指针一定要排序。但这道题该如何使用双指针的时候去重呢?
    在排序之后,数组从小到大依次排列,从第一个元素 i 开始,后面的区域左侧是left,右侧是right,如果 i 本身就大于0了,也就是最小的元素是大于0的,那么就不可能有三个元素的和是0了,可以直接返回了。
    a+b+c=0 这里先当作a是i,b是left,c是right。

    1. 对a去重:
    首先,对每个a来说,它们是不能重复的。所以在循环的时候,如果发现这个a和邻近的元素一样,就可以让i++,去找下一个a了 (这里需要注意数组排序了,如果有一样的就一定在邻近元素)。那么就一定需要判断 nums[i] == nums[i++] 或者nums[i] == nums[i--]
    不妨举特例[-1,-1,2],如果是nums[i] == nums[i++],i先指向第一个-1,然后判断出和第二个-1一样,i就指向了第二个-1,这样left和right就只剩下2了,其实也就是没有三个数了,那就丢掉了-1 -1 2这个三元组。所以这个逻辑是不行的,它其实做的是让 i 和left不会一样了。所以应该nums[i] == nums[i--],当 i 和之前的元素一样时再跳过它。

    2.对b,c去重:
    当a确定了,不断调整 left 和 right 确定b,c的数值。当获得满足条件的就加入result里,但是如果数组是[ -3, 1, 1, 1, 2, 2, 2]这样的,left指向最左侧的1,right指向最右侧的2,满足了条件,但是当left和right再次移动时,就会遇到重复的结果集,所以要对left和right也去重,去重的代码比较简单直接看就行,但是要注意在这个过程要判断left < right , 我用的是left != right,都是可以的,要不然就会有下标越界了。

    同时引用代码随想录里的一个思考题 :

既然三数之和可以使用双指针法,我们之前讲过的1.两数之和,可不可以使用双指针法呢?既然三数之和可以使用双指针法,我们之前讲过的两数之和,可不可以使用双指针法呢?
如果不能,题意如何更改就可以使用双指针法呢?
两数之和就不能使用双指针法,因为两数之和要求返回的是索引下标,
而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。 如果两数之和要求返回的是数值的话,就可以使用双指针法了。

这道题的result是一个 ArrayList 里面的元素类型是装着Integer的列表(三元组):
List> result = new ArrayList<>();
数组排序:Arrays.sort(nums);
ArrayList的添加:result.add();
将数组元素变成列表 :Arrays.asList(nums[i], nums[left], nums[right])

代码如下:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> 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]) {
                continue; //一定是continue而不是i++,如果约到-2 -2 -2 的情况,判断第二个-2之后i++,到第三个-2 ,但是仍然会执行下面的操作,就会重复了。continue的话第二个-2跳过,第三个-2跳过,到之后的数字。
            }

            int left = i + 1;
            int right = nums.length - 1;
            while (left < right) {
                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]));
                    while (left != right && nums[left] == nums[left + 1]) {
                        left++;
                    }

                    while (left != right && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    left++;
                    right--;
                }
            }
        }

        return result;

    }
}

18.四数之和

思路:
就是在三数之和外面套上一层k,注意剪枝条件
这道题我没自己写,粘贴一些代码随想录的

但是有一些细节需要注意,例如: 不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0
就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2,
-1],target是-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。

15.三数之和 (opens new window)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] +
nums[left] + nums[right] == 0。

四数之和的双指针解法是两层for循环nums[k] +
nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left]

  • nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。

那么一样的道理,五数之和、六数之和等等都采用这种解法。

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

之前我们讲过哈希表的经典题目:454.四数相加II (opens new
window),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。

而454.四数相加II (opens new window)是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] =
0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少!

我们来回顾一下,几道题目使用了双指针法。

双指针法将时间复杂度:O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下:

27.移除元素(opens new window)
15.三数之和(opens new window)
18.四数之和(opens new window) 链表相关双指针题目:

206.反转链表(opens new window)
19.删除链表的倒数第N个节点(opens new window) 面试题 02.07. 链表相交(opens new window) 142题.环形链表II(opens new window) 双指针法在字符串题目中还有很多应用,后面还会介绍到。

你可能感兴趣的:(leetcode,哈希算法,算法)