【算法对比】连续子区间的两种控制逻辑:Kadane和滑动窗口

0. 前言

在做数组相关的算法题时,连续子区间 是出现频率最高,也是花样最多的一个考点。

本文将通过两道算法题,深度对比处理连续子区间的两大方法:**针对最大和的 Kadane 算法 ** 与 针对最小区间长度的滑动窗口

1. 问题描述

1.1 岁晚可可塔

这道题其实就是求最大连续总和,数组元素有正有负。

1.2 内存块的最短分配

这道题其实就是求符合要求的最短连续长度。

现在有一排连续的内存块,每个内存块的大小不一,由一个正整数数组 nums 表示。现在需要分配一个 连续 的内存区域,使得该区域内所有内存块的大小总和 大于等于 给定的目标值 target

请找出满足条件的 最短连续子数组 的长度。如果不存在符合条件的子数组,返回 0。

输入:

  • 第一行:一个整数 target,目标内存大小。
  • 第二行:一个整数 n,代表内存块的总数量。
  • 第三行:n 个由空格分隔的正整数,每个正整数代表对应的每个内存块的大小。

输出:

  • 一个整数,代表满足条件的 最短连续子数组的长度

2. 算法逻辑对比

我用一个表格总结了一下 Kadane 算法和滑动窗口的算法逻辑,方便大家根据合适的场景进行选择:

3. 代码实现

3.1 岁晚可可塔代码

cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

int main() {
    int n;
    cin >> n;

    string dummy;
    getline(cin, dummy);//拿掉n后面的换行符

    string line;
    getline(cin, line);
    stringstream ss(line);//将第二行转换成流,然后一个一个往出拿

    long max_sum = -2e7;//最终最大甜度,初始赋个极小值,防止第二行全为负数
    long curr_sum = 0;//当前最大甜度
    for(int i=0; i<n; i++)
    {
        long curr;
        ss >> curr;//从流里面拿出来一个

        //kadane算法核心
        //如果前面的加上当前的,和还不如当前的自身大,那么前面全是累赘,直接抛弃,以当前为新的开头
       curr_sum = max(curr, curr+curr_sum);

        //更新max
       max_sum = max(curr_sum , max_sum);
    }
    cout << max_sum;
    return 0;
}

有个需要注意的点,如果甜度全是负数,最大值应该也是负数。

这时,如果 max_sum 的初始值为 0,后面取 max 是就会得到 0,最终结果也就是 0,从而导致错误。

还有,在开头,读取 n 的值之后,用 getline 拿掉 n 后面的换行符很关键,否则就会导致后面本该读取第二行内容的 getline 读到了第一行的换行符,也会导致错误。

然后就是 for 循环中的 Kadane 算法,其大致思路如下:

对于数组中的每个元素 x,我们有两种选择:第一,如果前面和为正的话,把 x 加入前面的子数组。第二,如果前面和为负,就抛弃前面的子数组,从 x 本身重新开始一个新的子数组。

3.2 内存块的最短分配代码

cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>
#include <vector>

using namespace std;

int main()
{
	int target, n;
	string dummy;

	cin >> target;
	getline(cin,dummy);//依旧拿掉换行符
	cin >> n;
	getline(cin, dummy);

	string line;
	getline(cin, line);
	stringstream ss(line);

	int left = 0;
	int min_len = n+1;//将最小长度初始化为一个较大的值
	int sum = 0;

	vector<int> nums;//动态数组,存放每个内存块的大小
	for(int i=0; i<n; i++)
	{
        //从流中逐个获取每个内存块大小,并存入数组
		int temp;
		ss >> temp;
		nums.push_back(temp);
	}

	for(int right = 0; right < n; right++)//right负责向右扩展
	{
		sum += nums[right];//每遍历到一个元素,就把它加到sum

		while(sum >= target)//每当sum已经达到target要求时,开始考虑收缩left,直到sum刚好再次小于target
		{
			min_len = min(min_len, right-left+1);//进入while循环,一定要先记录最小长度,否则会出错

			sum -= nums[left];
			left++;//左边界收缩
		}
	}
	if(min_len == n+1) cout << 0;
	else cout << min_len;
    return 0;
}

这段代码中唯一需要拆解的就是滑动窗口的运作逻辑,如下:

cpp 复制代码
while(sum >= target)//先检查现在的sum够不够
{
    //记录,此时sum确实是>=target的,这一刻的长度是满足要求的
	min_len = min(min_len, right-left+1);

    //尝试缩小,把左边的数减掉
	sum -= nums[left];
	//left向右挪
    left++;
    
    //回到最上面重新检查,不满足就跳出循环
}

关键点就在于我们在执行第 3 步减掉数字之前,已经先在第 2 步把当前的 min_len 存起来了,如果减掉 nums[left] 之后,sum 变得小于 target 了,下一次 while 循环的条件就会判定为 false,循环直接结束,而我们最后一次记录 min_len 的那个时刻,sum 依然是满足条件的。

4. 避坑指南

  • 为什么内存块那道题不能用 Kadane?
    • Kadane 是单指针,只能一值向前走,它只在乎 有没有变大,不会在满足 target 后,回过头去尝试缩短左边界。
  • 为什么可可塔不能用滑动窗口?
    • 滑动窗口依赖 单调性 ,也就是说右移 right 必然会让 sum 变大。如果数组里有负数,这个方法就没用了,窗口的伸缩会变得乱七八糟。

5. 算法模型选择

看到下面的场景时,可以根据上文讲的 Kadane 和滑动窗口的特征选择合适的模型:

  • 求连续一段的最大和?

    • 优先考虑 Kadane。
  • 求满足条件的连续最短或最长长度?

    • 优先考虑滑动窗口。
  • 数组里全是正数?

    • 滑动窗口。
  • 数组里正负数都有?

    • 必须用 Kadane。
相关推荐
水蓝烟雨10 分钟前
2359. 找到离给定两个节点最近的节点
算法·leetcode
ComputerInBook10 分钟前
C++ 17 相比 C++ 14 新增之特征
开发语言·c++·c++ 17
澈20723 分钟前
哈希表:O(1)查找的终极指南
算法·哈希算法·散列表
Peter·Pan爱编程24 分钟前
引用:比指针更安全的别名
c++·指针·引用·c++基础
m0_5027249525 分钟前
golang 、java、c++、javascript 语言switch case异同
java·javascript·c++·golang
我命由我1234525 分钟前
Android Framework P1 - 低配学习 Framework 方案、开机启动 Init 进程
android·c语言·c++·学习·android jetpack·android-studio·android runtime
许长安25 分钟前
互斥锁、自旋锁、读写锁使用场景以及底层实现
c++·经验分享·笔记
幻奏岚音37 分钟前
AI模型用户画像分析_new
人工智能·算法·计算机视觉·数据挖掘
Season45041 分钟前
C++11并发支持库(condition_variable | future全家桶)
java·jvm·c++
阿Y加油吧1 小时前
二刷 LeetCode:爬楼梯与杨辉三角,Java 实现复盘
java·算法·leetcode