【数据结构】哈希表&前缀和/滑动窗口双指针 和为k的子数组/两数三数四数之和-LeetCode题型记录(Java)

目录

    • 560. 和为K的子数组
      • 滑动窗口
        • 面试题57. 和为s的两个数字
        • 面试题57 - II. 和为s的连续正数序列
      • 前缀和 + 哈希表
    • 1248. 统计「优美子数组」
      • 前缀和+哈希表
      • 滑动窗口
    • 1371. 每个元音包含偶数次的最长子字符串
    • 1.两数之和
    • 454. 四数相加 II
    • 15. 三数之和
    • 18. 四数之和
    • 209. 长度最小的子数组

560. 和为K的子数组

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

看到这道题首先想到的是滑动窗口(双指针)感觉似曾相识,然后失败了。因为会出现负数的情况。
下面和可以用之前双指针思路的题目对比一下

滑动窗口

面试题57. 和为s的两个数字

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

划重点:递增排序
思路是双指针 i , j 分别指向数组 nums的左右两端,然后往里面移动

代码

class Solution {
     
    public int[] twoSum(int[] nums, int target) {
     
        if(nums == null){
     
            return new int[0];
        }
        int i = 0;
        int j = nums.length-1;
        while(i < j){
     
            int sum = nums[i] + nums[j];
            if(sum == target){
     
                return new int[]{
     nums[i], nums[j]};
            }else if(sum < target){
     
                i++;
            }else{
     
                j--;
            }
        }
        return new int[0];
    }
}

面试题57 - II. 和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

划重点:正整数序列 从小到大排列
思路是用i和j两个指针,然后分别往右移动,如果和小于target,则j++,如果和大于target,则i++

代码

class Solution {
     
    public int[][] findContinuousSequence(int target) {
     
        int i = 1;
        int j = 2;
        int sum = 3;
        List<int[]> res = new ArrayList<>();
        while(i <= target / 2){
     
            if(sum == target){
     
                int[] ans = new int[j-i+1];
                int k = i;
                for(int m = 0; m < j-i+1; m++){
     
                    ans[m] = k;
                    k++;
                }
                res.add(ans);
                sum -= i;
                i++;
                j++;
                sum += j;
            }else if(sum < target){
     
                j++;
                sum += j;
            }else{
     
                sum -= i;
                i++;
            }
        }
        return res.toArray(new int[res.size()][]);
    }
}

前缀和 + 哈希表

然后回到560. 和为K的子数组这道题,没有说从小到大,也不是正整数。不能用滑动窗口的思路,需要用到前缀和的思想。

  • 前缀和:当前项之前的所有项的和,则可以将当前项用两个相邻的前缀和的差来表示
    preSum[x] = nums[0]+nums[1]+…+nums[x]
  • nums 数组从i到 j项的和:
    nums[i] +…+nums[j]=preSum[j] - preSum[i-1] =k
    即preSum[j] =preSum[i-1] -k
  • 所以转化成 找前缀和数组中两个数的差==k的情况
  • 因为会有正负数,就会抵消,存在前缀和相等的情况(前缀和数组中会有重复),所以用哈希表来存这个前缀和数组。

代码

class Solution {
     
    public int subarraySum(int[] nums, int k) {
     
        // key:前缀和,value:key 对应的前缀和的个数
        Map<Integer, Integer> preSumFreq = new HashMap<>();
        int count = 0;
        int preSum = 0; //前缀和
        preSumFreq.put(0, 1);
        for(int num : nums){
     
            preSum += num;
            // 先获得前缀和为 preSum - k 的个数,加到计数变量里
            if(preSumFreq.containsKey(preSum - k)){
     
                count += preSumFreq.get(preSum - k);
            }
            // 维护 preSumFreq
            preSumFreq.put(preSum, preSumFreq.getOrDefault(preSum, 0) + 1);
        }
        return count;
    }
}

参考题解

  • HashMap中的key:当前前缀和;value:当前前缀和出现的次数
  • 每次遍历一个数字,计算前缀和,放入map中,如果之前没出现过这个前缀和,则value为1,如果出现过,则value为之前的次数+1
  • 如果发现map 中存在 key 为 当前前缀和 - k,说明存在 【之前的某个前缀和】,满足 【当前前缀和】 - 【之前的某个前缀和】 === k,把对应的次数累加到count中。看【之前的某个前缀和】出现几次,count就加几次。
  • 其中map.getOrDefault的用法:在这题中,当Map中有这个key时,就使用这个key对应的value值,如果没有就使用默认值0。

1248. 统计「优美子数组」

给你一个整数数组 nums 和一个整数 k。
如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。
请返回这个数组中「优美子数组」的数目。

前缀和+哈希表

和前面NO.560的思路相同,不同的是这里的前缀和不再是真正的前i个数字的和,而是要把前缀和想象成当前的奇数个数。

class Solution {
     
    public int numberOfSubarrays(int[] nums, int k) {
     
        Map<Integer, Integer> preFixCnt = new HashMap<>();
        //key是前缀和(当前奇数的个数),value是前缀和的个数
        preFixCnt.put(0, 1);
        int count = 0;
        int sum = 0;
        for(int num : nums){
     
            sum += num & 1;//当前奇数的个数
            preFixCnt.put(sum, preFixCnt.getOrDefault(sum, 0) + 1);
            if(preFixCnt.containsKey(sum - k)){
     
                count += preFixCnt.get(sum - k);
            }
        }
        return count;
    }
}

滑动窗口

参考题解
用滑动窗口,先找第k个奇数(右边界),然后看到下一个奇数之间的偶数个数,这些偶数都可以作为终点。再找第一个奇数前面的偶数个数,这些偶数都可以作为起点,这样从起点到终点计算子数组个数。然后left++,再往后更新一个奇数,继续计算。

class Solution {
     
    public int numberOfSubarrays(int[] nums, int k) {
     
        int left = 0;
        int right = 0;
        int oddCnt = 0;
        int res = 0;
        while(right < nums.length){
     
            //右指针先往右走,直到奇数个数为k
            if((nums[right++] & 1) == 1){
     
                oddCnt++;
            }
            if(oddCnt == k){
     
                //找第k+1个奇数
                // tmp是第k个奇数右边的偶数,right指向第k+1个奇数
                int tmp = right;
                while(right < nums.length && (nums[right] & 1) == 0){
     
                    right++;
                }
                int rightCount = right - tmp;//第k个奇数到k+1个奇数中间的偶数个数,都可以作为终点
                //找第一个奇数前面偶数的个数,都可以作为起点
                //left指向第一个奇数
                int leftCount = 0;
                while((nums[left] & 1) == 0){
     
                    leftCount++;
                    left++;
                }
                res += (leftCount + 1) * (rightCount + 1);
                left++;
                oddCnt--;
            }
        }
        return res;
    }
}

1371. 每个元音包含偶数次的最长子字符串

给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’ ,在子字符串中都恰好出现了偶数次。

前缀和+哈希表+状态压缩
首先根据题目中“恰好出现偶数次”,想到可以用异或来表示。

  • 状态压缩:每一位分别表示每一个元音字母出现的次数,1表示出现次数为奇数,0表示出现次数为偶数。例如:假如到第 i 个位置,u o i e a 出现的奇偶性分别为 1 1 0 0 1,那么我们就可以将其压成一个二进制数 (11001)_2=(25) 作为它的状态,则可以用2^5=32位的数组来表示
  • 32位的数组中存的是什么:存的是首次出现这个status的位置
  • 前缀和的理解:当32位数组中下一次再出现这个status时,二者的位置差的这部分子串就是要求的子字符串(偶数个元音异或后那一位还是0)
    比如现在我们找到了一个符合要求的子串[L,R],同时有另一个未知符合要求与否的子串[0,L],那么元音字符次数在[0,L]+[L,R]与[0,R]的奇偶性是相同的
  • map[0] = 0:如果第一个是辅音,如果map[0]=-1的话就没办法更新ans了
  • map[status] = i+1i+1-map[status]为什么是i+1而不是i:
    1、如果子字符串出现在原字符串的中间,那么新的i+1减去原来的i+1和 新的i减去原来的i效果是一样的,也就是用i+1和i没什么影响
    2、子字符串从头截取,也就是子字符串的第一位和原字符串第一位重合的情况,由于前面map[0]=0,所以这边i-(-1)=i+1-0。
class Solution {
     
    public int findTheLongestSubstring(String s) {
     
        int[] map = new int[32];
        Arrays.fill(map, -1);
        int ans = 0;
        int status = 0;
        map[0] = 0;
        for(int i = 0; i < s.length(); i++){
     
            if(s.charAt(i) == 'a'){
     
                status ^= 1<<0;
            }
            if(s.charAt(i) == 'e'){
     
                status ^= 1<<1;
            }
            if(s.charAt(i) == 'i'){
     
                status ^= 1<<2;
            }
            if(s.charAt(i) == 'o'){
     
                status ^= 1<<3;
            }
            if(s.charAt(i) == 'u'){
     
                status ^= 1<<4;
            }
            if(map[status] < 0){
     //没有出现过,更新map
                map[status] = i+1;
            }else{
       //求最大区间
                ans = Math.max(ans, i+1-map[status]);
            }
        }
        return ans;
    }
}

1.两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

哈希表

class Solution {
     
    public int[] twoSum(int[] nums, int target) {
     
        HashMap<Integer, Integer> map = new HashMap<>();
        int[] res = new int[2];
        for(int i = 0; i < nums.length; i++){
     
            int dif = target - nums[i];
            if(map.get(dif) != null){
     
                res[0] = map.get(dif);
                res[1] = i;
            }
            map.put(nums[i],i);
        }
        return res;
    }
}

454. 四数相加 II

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。
题目直达链接

为两数之和的扩展,分组。AB为一组,CD为一组。
用哈希表存放AB的和。key为AB的和,value为和出现的次数。
然后算CD的和,如果map中存在key为CD和的相反数,则count计数

代码

class Solution {
     
    public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
     
        int count = 0;
        Map<Integer, Integer> map = new HashMap<>();
        //计算AB数组中的和
        for(int a : A){
     
            for(int b : B){
     
                int sumAB = a + b;
                map.put(sumAB, map.getOrDefault(sumAB, 0) + 1);
            }
        }

        //算CD数组的和,去map里找,找到是CD和的相反数,就是要找的数
        for(int c : C){
     
            for(int d : D){
     
                int sumCD = c + d;
                if(map.containsKey(-sumCD)){
     
                    count += map.get(-sumCD);
                }
            }
        }
        return count;
    }
}

15. 三数之和

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

双指针
首先对数组从小到大进行排序,排序后固定一个数 nums[i],然后用i和j两个指针,一个从nums[i+1]往后移动,一个从末尾往前移动。
如果和刚好为0,则添加答案
如果和<0,i++
如果和>0,,j–
注意去重

代码

class Solution {
     
    public List<List<Integer>> threeSum(int[] nums) {
     
        List<List<Integer>> ans = new ArrayList();
        int len = nums.length;
        if(nums == null || len < 3){
     
            return ans;
        }
        Arrays.sort(nums);
        for(int i = 0 ; i < len-2 ; i++){
     
            if(nums[i] > 0){
     
                break;
            }
            if(i > 0 && nums[i] == nums[i-1]){
     //去重
                continue;
            }
            int L = i+1;
            int R = len-1;
            while(L < R){
     
                int sum = nums[i] + nums[L] + nums[R];
                if(sum == 0){
     
                    ans.add(Arrays.asList(nums[i], nums[L], nums[R]));
                    while(L < R && nums[L] == nums[L+1]){
     
                        L++;
                    }
                    while(L < R && nums[R] == nums[R-1]){
     
                        R--;
                    }
                    L++;
                    R--;
                }else if(sum < 0){
     
                    L++;
                }else{
     
                    R--;
                }
            }
        }
        return ans;
    }
}

18. 四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

在三数之和外面多嵌套一层循环,就变成了四数字之和。
同样用双指针

代码

class Solution {
     
    public List<List<Integer>> fourSum(int[] nums, int target) {
     
        List<List<Integer>> ans = new ArrayList<>();
        int len = nums.length;
        if(nums == null || len < 4){
     
            return ans;
        }
        Arrays.sort(nums);
        for(int i = 0; i < len-3; i++){
     
            if(i > 0 && nums[i] == nums[i-1]) continue;//去重
            for(int j = i+1; j < len-2; j++){
     
                if(j > i+1 && nums[j] == nums[j-1]) continue;//去重
                int L = j + 1;
                int R = len - 1;
                while(L < R){
     
                    int sum = nums[i] + nums[j] + nums[L] + nums[R];
                    if(sum == target){
     
                        ans.add(Arrays.asList(nums[i], nums[j], nums[L], nums[R]));
                        while(L < R && nums[L] == nums[L+1]) L++;//去重
                        while(L < R && nums[R] == nums[R-1]) R--;//去重
                        L++;
                        R--;
                    }else if(sum < target){
     
                        L++;
                    }else if(sum > target){
     
                        R--;
                    }
                }
            }
        }
        return ans;
    }
}

这样写时间效果不是很好
可以加上加上「值域判断」,进行最大最小值剪枝:

  • 计算当前最小值,如果最小值比target大,忽略
  • 计算当前最大值,如果最大值比target小,忽略

耗时从22ms降到了3ms

class Solution {
     
    public List<List<Integer>> fourSum(int[] nums, int target) {
     
        List<List<Integer>> ans = new ArrayList<>();
        int len = nums.length;
        if(nums == null || len < 4){
     
            return ans;
        }
        Arrays.sort(nums);
        for(int i = 0; i < len-3; i++){
     
            if(i > 0 && nums[i] == nums[i-1]) continue;//去重
            //获取当前最小值,如果最小值比target大,说明后面越来越大的也不可能,直接退出
            int min1 = nums[i] + nums[i+1] + nums[i+2] + nums[i+3];
            if(min1 > target) break;
            //获取当前最小值,如果最大值比target小,说明后面越来越小的也不可能,直接进行下一个i
            int max1 = nums[i] + nums[len-1] + nums[len-2] + nums[len-3];
            if(max1 < target) continue;
            for(int j = i+1; j < len-2; j++){
     
                if(j > i+1 && nums[j] == nums[j-1]) continue;//去重
                int L = j + 1;
                int R = len - 1;
                //当前最小值,如果最小值比target大,忽略
                int min2 = nums[i] + nums[j] + nums[L] + nums[L+1];
                if(min2 > target) continue;
                //当前最大值,如果最大值比target小,忽略
                int max2 = nums[i] + nums[j] + nums[R] + nums[R-1];
                if(max2 < target) continue;
                while(L < R){
     
                    int sum = nums[i] + nums[j] + nums[L] + nums[R];
                    if(sum == target){
     
                        ans.add(Arrays.asList(nums[i], nums[j], nums[L], nums[R]));
                        while(L < R && nums[L] == nums[L+1]) L++;//去重
                        while(L < R && nums[R] == nums[R-1]) R--;//去重
                        L++;
                        R--;
                    }else if(sum < target){
     
                        L++;
                    }else if(sum > target){
     
                        R--;
                    }
                }
            }
        }
        return ans;
    }
}

209. 长度最小的子数组

20200628

难度:中等

题目描述

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0。

示例:

输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。

进阶:

如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-size-subarray-sum

Solution

  1. 双指针
class Solution {
     
    public int minSubArrayLen(int s, int[] nums) {
     
        if(nums == null || nums.length == 0) return 0;
        int i = 0;
        int j = 0;
        int sum = nums[0];
        int ans = Integer.MAX_VALUE;
        while(i <= j && j < nums.length){
     
            if(sum < s){
     
                j++;
                if(j < nums.length) sum += nums[j];
            }else{
     
                ans = Math.min(ans, j-i+1);
                sum -= nums[i];
                i++;
            }
        }
        if(i == 0 && sum < s) return 0;
        return ans;
    }
}

​ 复杂度分析

  • 时间复杂度:O(n)。

  • 空间复杂度:O(1)。

  1. 前缀和+二分
class Solution {
     
    public int minSubArrayLen(int s, int[] nums) {
     
        int n = nums.length;
        if (n == 0) {
     
            return 0;
        }
        if(n == 1 && nums[0] >= s) return 1;
        int ans = Integer.MAX_VALUE;
        int[] sums = new int[n + 1];//存前缀和
        for(int i = 1; i < n+1; i++){
     
            sums[i] = sums[i-1] + nums[i-1];
        }
        //找sums[bound]- sums[i-1] >= s
        for(int i = 1; i <= n; i++){
     
            int target = s + sums[i-1];
            //二分在sums中找第一个>=target的元素
            int l = i;
            int r  = n;
            while(l < r){
     
                int mid = l + (r - l) / 2;
                if(sums[mid] < target){
     
                    l = mid + 1;
                }else{
     
                    r = mid;
                }
            }
            //判断是否找到了
            if(sums[l] >= target){
     
                ans = Math.min(ans, l-i+1);
            }
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }
}

复杂度分析

  • 时间复杂度:O(nlogn)。
  • 空间复杂度:O(n)。

Tips:

Java中有现成的库和函数来实现这里二分查找:大于等于target的第一个位置。

int bound = Arrays.binarySearch(sums, target);

你可能感兴趣的:(Java,数据结构与算法,数据结构,java,算法,哈希表,leetcode)