目录

题目描述

485. 最大连续 1 的个数

image-20230307212841379

题意分析

给一个只含 0 和 1 的数组,求其中连续的 1 最长能有多长,返回这个长度。

关键词是「连续」,也就是要求这些 1 在下标上首尾相接、中间不能夹任何 0。这与「1 的总个数」完全不同,[1,0,1,1] 的答案是 2 而不是 3。读题时要先把这一点锁死。

数组只有 0 和 1 两种取值,这个约束非常强:0 是唯一的分隔符。整个数组被 0 天然切成若干段全 1 的区间,问题等价于求这些区间里最长的那段。识别出「元素只有两类、其中一类做分隔符」这个结构,解法方向就基本定了。

题目不允许翻转或删除任何元素,这也是它比 487、1004、1493 简单的根本原因——那几道题允许把有限个 0 当成 1 用,必须维护一个可伸缩的窗口;本题不允许,窗口一旦遇到 0 就整段作废,无需回退。

数据规模是长度不超过 $10^5$,线性扫描绰绰有余,不需要任何预处理或额外结构。

边界上,题目保证数组非空,但要考虑全 0(答案 0)、全 1(答案是数组长度)、以及最长的一段恰好紧贴数组末尾这三种情况——最后一种是本题最容易漏掉的。

解法:线性扫描

核心思路

暴力做法是枚举所有子数组,检查每个子数组是否全为 1,取最长的。这是 $O(n^2)$ 甚至 $O(n^3)$ 的量级,在 $10^5$ 规模下不可行。

瓶颈在于反复重新检查了同一段数据。而观察到一个可以直接消掉这个开销的性质:从左往右扫描时,「以当前位置结尾的连续 1 的长度」只取决于前一个位置的同类信息。如果当前是 1,它等于前一个位置的值加一;如果当前是 0,它等于 0。这是一条极简的递推关系,只需要保存前一个值,不需要保存整个历史。

于是用一个变量 cur 承担这个递推,另一个变量 best 记录扫描过程中 cur 达到过的最大值。

循环不变量是:每处理完下标 icur 恰好等于以 nums[i] 结尾的连续 1 的长度,best 恰好等于前缀 nums[0..i] 中所有连续 1 段的最大长度。初始时前缀为空,两者都是 0;每一步按上面的递推更新 cur,再用 cur 去刷新 best,不变量得以维持。扫描结束时前缀就是整个数组,best 就是答案。

之所以「遇到 0 直接把 cur 清零」是正确的,是因为任何跨越这个 0 的区间都不可能全为 1,前面积累的长度对后面的段没有任何参考价值,可以安全丢弃。这正是本题不需要滑动窗口的原因——窗口左边界永远直接跳到 0 的后面,不存在渐进收缩。

best 的更新放在「遇到 1」的分支里即可,因为 cur 只可能在这个分支变大;放在循环末尾无条件更新同样正确,只是多做几次无用比较。这个选择也解释了为什么循环结束后不需要再补一次比较——最长段即使紧贴数组末尾,它的最后一个 1 也已经在循环内部刷新过 best 了。

解题步骤

  • 初始化 cur = 0best = 0cur 是「以当前位置结尾的连续 1 长度」,扫描开始前没有任何元素,所以是 0;best 初值取 0 而不是 Integer.MIN_VALUE,因为答案下界本来就是 0(全 0 数组的正确答案),这样全 0 用例无需特判。
  • 用增强 for 顺序遍历数组:本题只需要元素值、不需要下标,用 for (int num : nums) 更简洁;顺序必须从左到右,因为递推方向是「依赖前一个位置」。
  • 遇到 1 时 cur++:把当前元素接到正在积累的这段后面,长度加一。
  • 紧接着 best = Math.max(best, cur):必须在 cur++ 之后立刻更新,这样每一段的峰值都能被捕获。放在这里而不是循环末尾,是因为 cur 只在这一支变大,遇到 0 那支只会变小,无需比较。
  • 遇到 0 时 cur = 0:直接清零而不是递减,因为这个 0 把前后彻底隔断,之前积累的长度对后续毫无用处。注意这里不能顺手更新 best,也不需要——峰值早在上一个 1 处就记录过了。
  • 返回 best:循环内已经处理完所有位置,末尾无需补任何逻辑。

nums = [1,1,0,1,1,1] 走一遍。

初始:cur = 0best = 0

下标 0(值 1):cur = 1best = max(0,1) = 1。以下标 0 结尾的连续 1 长度确实是 1。

下标 1(值 1):cur = 2best = max(1,2) = 2

下标 2(值 0):cur = 0best 保持 2。此处清零切断了前后两段。

下标 3(值 1):cur = 1best = max(2,1) = 2。新段刚开始,还没超过历史最大值。

下标 4(值 1):cur = 2best = 2

下标 5(值 1):cur = 3best = max(2,3) = 3

循环结束,返回 3。注意最长的这一段紧贴数组末尾,但因为 best 是在每个 1 处即时更新的,末尾不需要补充任何比较就已经拿到了正确答案。

再看两个极端用例。nums = [0,0]:两轮都走 cur = 0 分支,best 始终是初值 0,返回 0,正确。nums = [1]:一轮后 cur = 1best = 1,返回 1,正确。

代码实现

class Solution {
    // 遇到 1 则计数加一,遇到 0 则重置为 0。
    public int findMaxConsecutiveOnes(int[] nums) {
        int cur = 0;
        int best = 0;

        for (int num : nums) {
            if (num == 1) {
                cur++;
                best = Math.max(best, cur);
            } else {
                cur = 0;
            }
        }

        return best;
    }
}
func findMaxConsecutiveOnes(nums []int) int {
    // 遇到 1 则计数加一,遇到 0 则重置为 0。
    cur := 0
    best := 0

    for _, v := range nums {
        if v == 1 {
            cur++
            if cur > best {
                best = cur
            }
        } else {
            cur = 0
        }
    }

    return best
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组长度。只扫描一趟,每个元素做一次判断和至多一次比较,全是常数操作,没有任何回退。
  • 空间复杂度:$O(1)$,只用了 curbest 两个整型变量,与数组长度无关。不需要前缀和数组,也不需要记录每段的起止位置。

关键点总结

  • 「最长连续满足某性质的子数组」这类问题,先问一句违反性质的元素能否被容忍:不能容忍就是本题的清零式扫描 $O(1)$ 空间;能容忍 $k$ 个就升级成滑动窗口。这个判断能瞬间把 485、487、1004 三题区分开。
  • 状态定义写成「以当前位置结尾的最优值」,往往能把二维的子数组枚举压成一维递推;最大子数组和、最长递增子序列的 $O(n^2)$ 解法都是同一个思路。
  • 遇到分隔符直接清零而非递减,本质是因为跨越分隔符的答案一定不合法,历史积累毫无参考价值——能论证「可以安全丢弃」是这类贪心式扫描的正确性来源。
  • 答案更新的位置要与「什么时候可能变大」对齐:cur 只在遇到 1 时增大,所以只在那一支刷新 best 即可,循环结束后无需补刀。想不清楚时,把更新无条件放在循环末尾是最保险的写法。
  • best 初值取答案的自然下界(这里是 0),能让全 0 这种极端输入自然落进主逻辑,比事后特判更干净。

易错点总结

  • cur = 0 写成 cur--[1,1,0,1] 时下标 2 处 cur 从 2 减到 1,下标 3 又加回 2,返回 2 虽然碰巧对,但换成 [1,1,1,0,1] 就会得到 3 之后再涨到 3,把被 0 隔开的两段错误地部分拼接了。
  • 在遇到 0 的分支里忘记重置[1,0,1] 会一路累加成 cur = 2,返回 2,而正确答案是 1——这等于把题目从「最长连续 1」做成了「1 的总数」。
  • 只在遇到 0 时才更新 best[1,1,1] 全程不进 0 分支,best 停留在初值 0,返回 0 而不是 3;最长段紧贴数组末尾时必然翻车。
  • best 初值设成 nums[0] 或 1[0,0,0] 会返回 1 或 0 之外的错误值,全 0 数组的正确答案必须是 0。
  • best = Math.max(best, cur) 写在 cur++ 之前[1] 会用旧的 cur = 0 去比较,返回 0 而不是 1,整体结果永远比正确答案少 1。
  • 判断写成 if (num != 0)(或 num > 0:本题恰好只有 0 和 1 所以看不出问题,但一旦被面试官改成含有 2 的数组,[1,2,1] 会返回 3,而 2 本不该被计入。
  • 误以为要统计 1 的总个数[1,0,1,1] 会返回 3,正确答案是 2;这是读题层面的错误,比代码错误更致命。
  • 用双重循环枚举所有子数组再检查:逻辑虽对,但 $10^5$ 长度下 $O(n^2)$ 是 $10^{10}$ 次操作,必然超时。
  • 借助滑动窗口并允许窗口内含 0:本题不允许任何翻转,窗口一旦含 0 就非法,写成 1004 的模板并把 k 设成 0 虽然能过,但多出无谓的左指针收缩逻辑,面试中会被认为没看清题目的简化条件。
  • 返回 cur 而不是 best[1,1,0] 结束时 cur 已被清零,返回 0 而不是 2;只有当最长段恰好在末尾时才碰巧正确。

相似题目

题目 难度 考察点
487. 最大连续1的个数 II 中等 允许翻转一个 0,清零式扫描升级为记录「上一个 0 位置」的双变量法
1004. 最大连续1的个数 III 中等 允许翻转 k 个 0,必须用滑动窗口按 0 的个数收缩左边界
1493. 删掉一个元素以后全为 1 的最长子数组 中等 必须删掉恰好一个元素,答案要在窗口长度上再减一且需处理全 1 特例
面试题 05.03. 翻转数位 简单 与 487 同构,但输入是一个整数,需先按位取出再扫描
1446. 连续字符 简单 分隔符从「0」变成「与前一个字符不同」,递推条件改成比较相邻元素
674. 最长连续递增序列 简单 分隔条件是 nums[i] <= nums[i-1],同样的清零式扫描骨架
53. 最大子数组和 中等 同为「以当前位置结尾」的递推,但重置条件从遇 0 变成前缀和为负
424. 替换后的最长重复字符 中等 字符不止两类,需在窗口内维护各字符计数并用最大频次判断可行性
3. 无重复字符的最长子串 中等 违规元素的位置不固定,左指针需跳到重复字符上次出现处的下一位
128. 最长连续序列 中等 「连续」指数值连续而非下标连续,需借哈希集合从每段起点向后延伸