Java 前缀和 及其优化技巧Ⅱ

 

一. 前缀和的简单介绍

1.前缀和的数组型简单定义

        对于数组nums来说,前缀和pre[i] = pre[i-1] + nums[i],即每个位置上存储的是前i个数组元素的和,用数学公式表示为:

pre[i] = \sum_{j=0}^{j=i}nums[j]

数组的典型案例:

其他类型的前缀和多是最终能变成和数量相关,即转换成int[] 数组类型。

那么,对于后缀和是一样的道理。

2.前缀和的典型使用场景/触发关键词

        在一些题目或者场景中,出现了子数组*连续一段区间,并且和求数量或者判断数量相关,很大程度上可以向前缀和的方去思考。

二. 使用前缀和的经典案例 

1.leetcode1413 逐步求和得到正数的最小值

前缀和 及其优化技巧

2.leetcode2383 赢得比赛需要的最少训练时长

前缀和 及其优化技巧

3.leetcode2389 和有限的最长子序列

给你一个长度为 n 的整数数组 nums ,和一个长度为 m 的整数数组 queries 。

返回一个长度为 m 的数组 answer ,其中 answer[i] 是 nums 中 元素之和小于等于 queries[i] 的 子序列 的 最大 长度  。

子序列 是由一个数组删除某些元素(也可以不删除)但不改变剩余元素顺序得到的一个数组。

输入:nums = [4,5,2,1], queries = [3,10,21]
输出:[2,3,4]
解释:queries 对应的 answer 如下:
- 子序列 [2,1] 的和小于或等于 3 。可以证明满足题目要求的子序列的最大长度是 2 ,所以 answer[0] = 2 。
- 子序列 [4,5,1] 的和小于或等于 10 。可以证明满足题目要求的子序列的最大长度是 3 ,所以 answer[1] = 3 。
- 子序列 [4,5,2,1] 的和小于或等于 21 。可以证明满足题目要求的子序列的最大长度是 4 ,所以 answer[2] = 4 。
class Solution {
    public int[] answerQueries(int[] nums, int[] queries) {
        int[] pre = new int[nums.length];
        Arrays.sort(nums);
        pre[0] = nums[0];
        for(int i = 1; i < nums.length; i++){
            pre[i] = pre[i-1] + nums[i];
        }
        int[] ans = new int[queries.length];
        for(int i = 0; i < queries.length; i++){
            int len = find(pre,queries[i]);
            ans[i] = len;
        }
        return ans;
    }
    public int find(int[] pre, int target){
        return erfen(pre,target,0,pre.length-1);
    }
    public int erfen(int[] pre, int target, int left, int right){
        if(left <= right){
            int mid = left + ((right-left)>>1);
            if(pre[mid] > target){
                return erfen(pre,target,left,mid-1);
            }
            else if(pre[mid] < target){
                return erfen(pre,target,mid+1,right);
            }
            else{
                return mid+1;
            }
        }
        return right+1;
    }
}

本题小结:(1)此题运用基础前缀和,不需要做其他变换

                  (2)在查找方面需要用二分,否则时间复杂度很高,若有较长判例会超过时间复杂度
 

4.leetcode560 和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的连续子数组的个数 。

输入:nums = [1,1,1], k = 2
输出:2
class Solution {
    public int subarraySum(int[] nums, int k) {
        int len = nums.length;
        int count = 0;
        int[] presum = new int[len+1];
        presum[0] = 0;
        for(int i = 0; i < len; i++){
            presum[i+1] += presum[i]+nums[i]; 
        }
        for(int left = 0; left < len; left++){
            for(int right = left+1; right <= len; right++){
                if(presum[right] - presum[left] == k){
                    count++;
                }
            }
        }
        return count;
    }
}

本题小结:(1)此题运用基础前缀和

                  (2)此题也可以进行优化,即先得到前缀和,然后用HashMap存储,再次遍历找HashMap中是否有k-pre[i],思想类似于两数之和,在前篇前缀和 及其优化技巧 已经见的非常多了

三. 前缀和优化

1.leetcode1871 跳跃游戏 VII(前缀和+动态规划)

2.leetcode1590 使数组和能被 P 整除(前缀和+HashMap)

3.面试题 17.05.  字母与数字(前缀和+HashMap)

1~3 见 前缀和 及其优化技巧

4.leetcode209 长度最小的子数组(前缀和+二分/滑动窗口)

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

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

(1)前缀和

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int[] pre = new int[nums.length+1];
        pre[1] = nums[0];
        int min = nums.length;
        if(nums[0] >= target) return 1;
        for(int i = 2; i < nums.length+1; i++){
            pre[i] = pre[i-1] + nums[i-1];
        }
        if(pre[nums.length] < target) return 0;
        for(int i = 1; i < nums.length+1; i++){
            for(int j = 0; j < i; j++){
                if(pre[i] - pre[j] >= target){
                    min = Math.min(min,i-j);
                }
            }
        }
        return min;
    }
}

最基础前缀和,不再赘述,此解法不能AC,时间复杂度O(n^2)

本题小结:(1) int[] pre = new int[nums.length+1]前缀和的长度比数组长一些利于后续操作,是前缀和中经常使用的操作。

(2)前缀和剪枝

在最基础的前缀和上,可以进行剪枝

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int[] pre = new int[nums.length+1];
        pre[1] = nums[0];
        int min = nums.length;
        if(nums[0] >= target) return 1;
        for(int i = 2; i < nums.length+1; i++){
            pre[i] = pre[i-1] + nums[i-1];
        }
        if(pre[nums.length] < target) return 0;
        for(int i = 1; i < nums.length+1; i++){
            for(int j = i-1; j >= 0; j--){
                if(pre[i] < target) break;
                if(pre[i] - pre[j] >= target){
                    min = Math.min(min,i-j);
                    break;
                }
            }
        }
        return min;
    }
}

 可以通过,时间较长

剪枝(1)当在i处前缀和<target,证明在当前长度的子数组之和<target,可直接跳过

       (2)当在j处有一个结果使得pre[i] - pre[j] >= target,直接break,因为再往前长度更长,结果不可能比当前j处小

(3)前缀和+二分

前缀和天然具有二分性质,在leetcode2389 和有限的最长子序列中利用的也是这一点

对于本题可以在构建好前缀和后,使用二分【1】

class Solution {
    public int minSubArrayLen(int t, int[] nums) {
        int n = nums.length, ans = n + 10;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        for (int i = 1; i <= n; i++) {
            int s = sum[i], d = s - t;
            int l = 0, r = i;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (sum[mid] <= d) l = mid;
                else r = mid - 1;
            }
            if (sum[r] <= d) ans = Math.min(ans, i - r);
        }
        return ans == n + 10 ? 0 : ans;
    }
}

(4)滑动窗口【3】

对于子数组的题目来说,滑动窗口也是经常使用的方法,在前缀和的题目中可以多考虑滑动窗口,实际上,此题在时间和空间复杂度上更适合用滑动窗来解决。

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int ans = Integer.MAX_VALUE;
        int start = 0, end = 0;
        int sum = 0;
        while (end < n) {
            sum += nums[end];
            while (sum >= s) {
                ans = Math.min(ans, end - start + 1);
                sum -= nums[start];
                start++;
            }
            end++;
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }
}

5.2488 统计中位数为 K 的子数组(前缀和+HashMap/数组)

给你一个长度为 n 的数组 nums ,该数组由从 1 到 n 的 不同 整数组成。另给你一个正整数 k 。

统计并返回 nums 中的 中位数 等于 k 的非空子数组的数目。

注意:

数组的中位数是按 递增 顺序排列后位于 中间 的那个元素,如果数组长度为偶数,则中位数是位于中间靠 左 的那个元素。
例如,[2,3,1,4] 的中位数是 2 ,[8,4,3,5,1] 的中位数是 4 。
子数组是数组中的一个连续部分。

输入:nums = [3,2,1,4,5], k = 4
输出:3
解释:中位数等于 4 的子数组有:[4]、[4,5] 和 [1,4,5] 。

(1)前缀和+HashMap

老朋友,直接上图理解

以nums = [3,2,1,4,5], k = 4为例: 

class Solution {
    public int countSubarrays(int[] nums, int k) {
        HashMap<Integer,Integer> map = new HashMap<>();
        map.put(0,1);
        int index = 0;
        int pre_m_g = 0;
        int pre_m_l = 0;
        int ans = 0;
        for(int i = 0; i < nums.length; i++){
            if(nums[i] == k){
                index = i;
                break;
            }
            if(nums[i] > k) pre_m_g++;
            if(nums[i] < k) pre_m_l++;
            int diff = pre_m_g - pre_m_l;
            if(map.containsKey(diff)){
                map.put(diff,map.get(diff)+1);
            }
            else{
                map.put(diff,1);
            }
        }
        for(int i = index; i < nums.length; i++){
            if(nums[i] > k) pre_m_g++;
            if(nums[i] < k) pre_m_l++;
            int diff = pre_m_g-pre_m_l;
            if(map.containsKey(diff)){
                ans += map.get(diff);
            }
            if(map.containsKey(diff-1)){
                ans += map.get(diff-1);
            }
        }
        return ans;
    }
}

(2)类前缀和遍历+数组

上方用HashMap的方法比较好理解,用类似的思想,采用数组映射的方式【2】

class Solution {
    public int countSubarrays(int[] nums, int k) {
        int n = nums.length;
        int i = 0;
        for (; nums[i] != k; ++i) {}
        int[] cnt = new int[n << 1 | 1];
        int ans = 1;
        int x = 0;
        for (int j = i + 1; j < n; ++j) {
            x += nums[j] > k ? 1 : -1;
            if (x >= 0 && x <= 1) {
                ++ans;
            }
            ++cnt[x + n];
        }
        x = 0;
        for (int j = i - 1; j >= 0; --j) {
            x += nums[j] > k ? 1 : -1;
            if (x >= 0 && x <= 1) {
                ++ans;
            }
            ans += cnt[-x + n] + cnt[-x + 1 + n];
        }
        return ans;
    }
}

【2】:“在编码上,我们可以直接开一个长度为2*n+1的数组,用于统计当前数组中,比k大的个数和比k小的个数的差值,加上n即可把范围从[-n,n]传换成[0,2n]” 

不好理解,上图:

(1)###

(2) ###

 

(3)###

(4)###

(5)###

(6)###

 

(7)###

(8)###

参考来源

【1】leetcode 宫水三叶 前缀和 + 二分 运用题

【2】leetcode ylb [Python3/Java/C++/Go/TypeScript] 一题一解:遍历 + 计数(详细题解)

【3】leetcode 官方 长度最小的子数组 

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Java中的for循环是一种常见的迭代结构,用于循环执行一段代码块。下面介绍一些优化for循环的方法: 1. 使用增强for循环 增强for循环也称为for-each循环,它可以很方便地遍历数组和集合等数据结构,语法简单,易于理解。与传统的for循环相比,增强for循环可以减少变量声明、循环变量的初始值、循环条件等语句,从而使代码更简洁。 例如,使用增强for循环遍历数组: ``` int[] arr = {1, 2, 3, 4, 5}; for (int i : arr) { System.out.println(i); } ``` 2. 避免重复计算 在for循环中,有些表达式可能需要多次计算,这样会降低程序的性能。为了避免重复计算,可以将表达式的结果存储在局部变量中,并在循环中重复使用。 例如,以下代码中的表达式arr.length会在每次循环中计算一次: ``` for (int i = 0; i < arr.length; i++) { // ... } ``` 可以将arr.length存储在一个局部变量中,以避免重复计算: ``` int len = arr.length; for (int i = 0; i < len; i++) { // ... } ``` 3. 使用前缀递增运算符 在for循环中,使用前缀递增运算符(++i)比后缀递增运算符(i++)更高效,因为前缀递增运算符不需要创建一个临时变量来存储递增前的值。 例如,以下代码使用后缀递增运算符: ``` for (int i = 0; i < arr.length; i++) { // ... } ``` 可以改为使用前缀递增运算符: ``` for (int i = 0; i < arr.length; ++i) { // ... } ``` 4. 使用位运算代替乘除运算 在for循环中,乘法和除法运算可能会比较耗时,可以使用位运算代替乘法和除法运算,从而提高程序的性能。 例如,以下代码使用乘法运算: ``` for (int i = 0; i < arr.length; i++) { int j = i * 2; // ... } ``` 可以改为使用位运算: ``` for (int i = 0; i < arr.length; i++) { int j = i << 1; // ... } ``` 5. 避免使用过多的方法调用 在for循环中,如果调用了过多的方法,会降低程序的性能。为了避免这种情况,可以将方法调用的结果存储在局部变量中,并在循环中重复使用。 例如,以下代码中的方法调用Math.sin()会在每次循环中执行一次: ``` for (double i = 0; i < Math.PI; i += 0.01) { double y = Math.sin(i); // ... } ``` 可以将Math.sin()的结果存储在一个局部变量中,以避免重复调用: ``` for (double i = 0, sinI = Math.sin(i); i < Math.PI; i += 0.01, sinI = Math.sin(i)) { double y = sinI; // ... } ``` 以上就是优化Java for循环的一些方法,可以根据具体情况选择适合的优化方法来提高程序性能。 ### 回答2: Java的for循环是一种常用的循环结构,用于重复执行特定的代码块。一般来说,for循环用于执行已知次数的迭代操作。为了优化for循环的性能,我们可以通过以下几种方式: 1. 减少迭代次数:如果我们已知循环的次数,可以使用确定次数的for循环,避免不必要的迭代。比如,如果我们想要迭代5次,可以使用for(int i=0; i<5; i++)的方式来执行循环。 2. 避免重复计算:在循环中,如果有一些表达式的值在每次迭代时都是不变的,可以将其计算结果保存在一个临时变量中,避免重复计算。这样可以提高循环的执行效率。 3. 使用增强的for循环:Java还提供了增强的for循环,也称为foreach循环,适用于遍历数组、集合或其他可迭代对象。与传统的for循环相比,增强的for循环更简洁、易读,并且在性能上并没有明显的劣势。 4. 使用并行循环:如果循环体中的迭代操作是相互独立的,可以考虑将循环改为并行循环。Java 8引入了并行流(Parallel Streams)的概念,可以通过使用parallel()方法将顺序流转换为并行流,从而实现循环的并行执行。这种方式适用于具有大量迭代操作和较复杂计算的情况,可以提高程序的性能。 总的来说,优化Java的for循环可以通过减少迭代次数、避免重复计算、使用增强的for循环或并行循环来提高性能。根据具体的应用场景和需求,选择适合的优化策略可以使程序更加高效。 ### 回答3: 在Java中,for循环是一种常见的循环结构,用于重复执行一段代码。为了提高for循环的性能和效率,可以采取以下几种优化方法: 1. 减少循环次数:在设计for循环时,可以通过适当选择循环条件来减少循环次数。例如,如果循环次数已知,可以直接在循环条件中指定固定的值,而不是使用动态计算的方法来判断循环的结束条件。 2. 缓存循环变量:在进行多次循环时,可以通过将循环变量的值缓存到一个局部变量中,以减少对循环变量的重复访问。这样可以减少内存访问的开销,并提高循环的执行速度。 3. 使用增强for循环:增强for循环是Java中引入的一种简化版的for循环,适用于遍历数组或集合。与传统的for循环相比,增强for循环具有更简洁的语法和更高的执行效率,可以减少循环的代码量和错误的可能性。 4. 避免在循环中进行复杂的操作:在for循环中应尽量避免进行复杂的操作,如方法调用、对象创建等。这些操作会增加循环的执行时间,降低循环的性能。如果需要进行复杂的操作,可以考虑将其提取到循环外部,并使用局部变量来保存计算结果。 5. 合理使用循环变量:在for循环中,可以合理利用循环变量的值来简化逻辑、减少计算量。例如,可以使用循环变量来作为数组或集合的索引,减少对索引的计算;或者使用循环变量来作为某种状态的标志,减少对状态的判断。 总之,优化Java中的for循环需要结合具体的业务场景和性能需求,通过减少循环次数、缓存循环变量、使用增强for循环、避免复杂操作以及合理利用循环变量等方法,可以最大程度地提高for循环的性能和执行效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值