数组累加和问题三连

数组累加和问题三连

第一题

题目:

给定一个全是正数的数组arr,一个目标数字target,求数组中满足和为target的最长子数组的长度

思路:

这是很简单的题目,用双指针和窗口就可以解决。具体见代码

代码:

	public static int getMaxLength(int[] arr, int K) {
		if (arr == null || arr.length == 0 || K <= 0) {
			return 0;
		}
		int left = 0;
		int right = 0;
		int sum = arr[0];
		int len = 0;
		while (right < arr.length) {
			if (sum == K) {
				len = Math.max(len, right - left + 1);
				sum -= arr[left++];
			} else if (sum < K) {
				right++;
				if (right == arr.length) {
					break;
				}
				sum += arr[right];
			} else {
				sum -= arr[left++];
			}
		}
		return len;
	}

第二题

题目:

给定一个数组arr,数组中整数,负数和0都有,一个目标数字target,求数组中满足和为target的最长子数组的长度

思路

对于这种数组问题,要求某个S问题的解,一般来说有两种解决思路

  1. 以数组元素i作为起始位置时,S问题的解。遍历所有的解,能得到最终答案
  2. 以数组元素i作为终止位置时,S问题的解。遍历所有的解,能得到最终答案

对于这个题,采用的第二种思路。
对于数组元素a[j]来说,要求其作为子数组最后一个元素时,满足条件的子数组的最大长度,对于这个问题来说,那么需要找到一个a[i],使得sum(a[i],…,a[j])=target,同时,这个i要尽可能的小,这样才能满足子数组最长。对于每一个a[j],都这样做,最后遍历结果,就可以找到最终的结果。

按照上面的思路,感觉时间复杂度是O(N^2)的。这里可以继续优化,我们可以在O(n)的时间内求出数组的前缀和S,如果想要得到一个最小的i,使得sum(a[i],…,a[j])=target,那么就相当于找到一个i,使得S[i] = S[j] - target。当然,如果找不到满足条件的i,说明以a[j]结尾的子数组没有满足条件的,直接遍历下一个就可以了。

时间复杂度为O(N),空间复杂度也是O(N)

代码

    public int maxSubArrayLen(int[] nums, int k) {
        // Write your code here
		if (nums == null || nums.length == 0) {
			return 0;
		}
		//用Map存储出现过的前缀和及其对应的索引
		HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
		map.put(0, -1); // 需要提前压入一个键值对<0,-1>
		//如果不压入的话,当从0到j正好满足条件时,Map中找不到前缀为0对应的索引,
		//所以会导致答案不对
		int len = 0;
		int sum = 0;
		for (int i = 0; i < nums.length; i++) {
			sum += nums[i];
			if (map.containsKey(sum - k)) {
				len = Math.max(i - map.get(sum - k), len);
			}
			if (!map.containsKey(sum)) { //对于同样的前缀和,只保存第一次出现的位置,因为要保		    证子数组最长
				map.put(sum, i);
			}
		}
		return len;

    }

变体

题目:给定一个数组arr,称1和2的个数相同的子数组为合格的子数组,求arr中合格的子数组的最大长度

思路:
首先对数组进行一次遍历,对于每个元素,如果为1,则不做处理;如果为2,则变为-1;如果为其他数,则设置为0。那么问题就转化为,求arr中,满足目标和为0的子数组的最大长度。

第三题

题目

给定一个数组arr,数组中整数,负数和0都有,一个目标数字target,求数组中满足和小于等于target的最长子数组的长度

思路

首先定义概念,定义两个数组,分别是

  • MinSum[]:MinSum[i]代表着子数组以i位置元素开头,能取得的最小累加和
  • MinSumEnd[]:MinSumEnd[i]代表着子数组以i位置元素开头,能取得最小累加和的子数组的结束位置

MinSum[i]表示从i开始往后,所有子数组的最小的累加和,MinSumEnd[i]代表着对应的结束位置,这两个数据是非常有用的。

下面开始主流程:从数组的index位置开始,设index = 0 ,利用上面两个数组,求出0位置作为子数组开头所能达到的满足条件的子数组的最大长度。很简单,如果MinSum[0]<=target,那么可以得到末尾索引j(j=MinSumEnd[i]),此时sum = MinSum[0],接下来计算sum +MinSum[j+1],如果sum<=target,则sum = sum +MinSum[j+1],并且得到新的末尾位置k(k=MinSumEnd[j+1]);继续执行上面的操作直到 sum +MinSum[k+1] >target 时,停止,此时记录0位置作为子数组的最大长度(满足条件的子数组),记此时的子数组末尾位置为p;
然后,sum = sum - arr[0],也就是将0位置元素从sum中减去,此时sum为从1到p位置所有元素的和,如果sum + MinSum[p+1] <=target,则sum = sum + MinSum[p+1],p = MinSumEnd[p+1],继续循环;如果sum + MinSum[p+1] > target,说明以1为开头的,能达到的满足条件的子数组,最大长度已经找到了,尝试更新这个全局的最大长度,然后将index++,重复上述过程。

这个算法的精髓在于,在找到0位置开头的满足条件的最长子数组之后,将0位置删除以后,以1位置开始找满足长度的子数组时,不需要从头开始找。假设从0开始的满足条件的最大子数组的长度是p+1,也就是到索引p位置终止(这就是说0到p的sum值,加上MinSum[p+1]的值,大于target),那么当1为子数组开头元素时,如果1到p的sum值,加上MinSum[p+1]的值还是大于target的,说明1作为开始位置的,能达到满足条件的子数组中,长度不会超过p,这是比从0开始的满足条件的最大子数组的长度要小的,所以1开头的子数组就没有继续讨论的意义了。

这个算法的关键在于,除了第一次遍历,后面的遍历,都不需要从头开始找,删除了对那些不可能成为答案的结果的遍历,所以可以达到O(N)

代码

	public static int maxLengthAwesome(int[] arr, int k) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		int[] minSums = new int[arr.length];
		int[] minSumEnds = new int[arr.length];
		//初始化两个关键数组
		minSums[arr.length - 1] = arr[arr.length - 1];
		minSumEnds[arr.length - 1] = arr.length - 1;
		for (int i = arr.length - 2; i >= 0; i--) {
			if (minSums[i + 1] < 0) {
				minSums[i] = arr[i] + minSums[i + 1];
				minSumEnds[i] = minSumEnds[i + 1];
			} else {
				minSums[i] = arr[i];
				minSumEnds[i] = i;
			}
		}
		
		int end = 0;
		int sum = 0;
		int res = 0;
		// i是窗口的最左的位置,end扩出来的最右有效块儿的最后一个位置的,再下一个位置
		// end也是下一块儿的开始位置
		// 窗口:[i~end)
		for (int i = 0; i < arr.length; i++) {
			// while循环结束之后:
			// 1) 如果以i开头的情况下,累加和<=k的最长子数组是arr[i..end-1],看看这个子数组长度能不能更新res;
			// 2) 如果以i开头的情况下,累加和<=k的最长子数组比arr[i..end-1]短,更新还是不更新res都不会影响最终结果;
			while (end < arr.length && sum + minSums[end] <= k) {
				sum += minSums[end];
				end = minSumEnds[end]+1;
			}
			res = Math.max(res, end - i);
			if (end > i) { // 窗口内还有数 [i~end) [4,4)
				sum -= arr[i];
			} else { // 这是需要特殊考虑的一点,窗口内已经没有数了,
			//说明从i开头的所有子数组累加和都不可能<=k,这时end=i了,让end=  i+1,下一轮开始时
			//i也会加1,就自动跳到下一个数去遍历了
				end = i + 1;
			}
		}
		return res;
	}

代码的循环中,两个遍历,i和end,都是从0跑到n,所以时间复杂度是O(N).

对于代码中end=i的情况,可以看下面这个例子
[-7,3,3,3,3,3,…],target=0
从0位置开始,可以最多到3位置,此时end = 3,i=0,sum = sum - (-7),i++
当i=1时,sum = 6 > target ,sum = sum-3,i++,
i=2时,sum = 3 >target,sum = sum-3,i++ ,
i=3时,sum = 0 =target(此时没有元素在窗口中了,但是end仍然扩不动,此时的end==i,所以让end = i+1,end变成4,下一轮i++也会变成4,就会跳过3位置的元素,继续遍历了)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值