LeetCode 904:水果成篮(滑动窗口) —— 题解

👋 欢迎阅读

一.题目

904. 水果成篮 - 力扣(LeetCode)

🎯 欢迎来到「水果成篮」题解之旅! 本文将带你从"用两个篮子采摘最多水果"这一趣味场景出发,深入理解滑动窗口(双指针) 的灵活运用,并掌握如何通过维护窗口内水果种类不超过 2 种来高效求解最长连续子数组长度。

在开始之前,建议你先:

  • 了解题目背景 :这是 LeetCode 904 题,给定一个整数数组 fruits,每个数字代表一种水果类型。你只能从某棵树开始,连续向右采摘 ,且全程只能使用两个篮子 (即最多包含两种不同的水果),一旦遇到第三种水果就必须停止。目标是求最多能采摘的果树数量 。本质上,我们要找最长的连续子数组 ,使得其中不同元素的个数 ≤ 2

  • 明确学习目标 :掌握滑动窗口核心流程 ------右指针不断扩展,将新水果加入窗口并统计其出现次数;若窗口内水果种类超过 2,则移动左指针缩小窗口,直至种类数恢复为 2;在每次调整后更新窗口长度的最大值。理解如何用哈希表(数组模拟)种类计数器 来高效判断窗口是否合法,并熟练实现"入窗口 → 判断 → 收缩 → 更新"的标准模板。

  • 准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 fruits = [0,1,2,2] 输出 3fruits = [1,2,3,2,2] 输出 4)。

本文将从问题转化、滑动窗口策略设计(种类计数约束)、窗口收缩条件 到代码实现,层层递进。即使你对滑动窗口还不熟悉,我们也会从"维护一个最多包含两种水果的窗口,并尽量拉长它"这一直觉出发,让你轻松抓住核心思想 ------窗口内水果种类是唯一限制条件,用双指针动态调整窗口,在满足条件时记录最大长度。现在,让我们一起在果林中滑动窗口,摘取最多的果实吧! 🍎🍌

二.做题思路

一、问题分析(前置分析)

给定一个整数数组 fruits,每个元素代表一棵树上的水果类型。你只有 两个篮子 ,每个篮子只能装一种类型的水果,但数量不限。你必须从某棵树开始,连续采摘 ,直到遇到第三种水果类型为止。目标是 收集尽可能多的水果 (即最长连续子数组,其中最多包含两种不同的水果类型)。

核心观察 :等价于 寻找一个最长的连续子数组,其中不同元素的种类数 ≤ 2 。这正好适合 滑动窗口 来解决。


二、算法策略(滑动窗口 + 哈希计数)

  • 使用 数组 nums[100001](或哈希表)记录当前窗口内每种水果出现的次数。

  • 使用变量 basket 记录当前窗口内 不同水果的种类数

  • 右指针 right 从 0 到 n-1 依次遍历:

    • 入窗口nums[fruits[right]]++;若该水果之前次数为 0(即新类型),则 basket++

    • 判断条件 :若 basket > 2,则需 收缩左指针 left,直到窗口内种类数 ≤ 2。

    • 收缩时,将 fruits[left] 移出窗口,若其计数变为 0,则 basket--left++

    • 更新结果 :每次调整后,计算当前窗口长度 right - left + 1,并更新最大值 len

示例执行过程fruits = [1, 2, 3, 2, 2]):

步骤 right fruits[right] 入窗后 basket 是否需收缩 操作 当前窗口 [left, right] 窗口长度 len 更新
初始 - - 0 - - - - 0
1 0 1 1 - 0,0 1 1
2 1 2 2 - 0,1 2 2
3 2 3 3 左移:移除 fruits[0]=1nums[1] 变为0,basket=2left=1 1,2 2 2(保持)
4 3 2 2 - 1,3 3 3
5 4 2 2 - 1,4 4 4

最终 len = 4,对应子数组 [2, 3, 2, 2],与示例一致。


三、正确性说明(简单版本)

滑动窗口始终维护 最多包含两种水果类型的连续子数组 。当窗口内种类数超过 2 时,必须移动左指针直到种类数 ≤ 2,因为继续扩展右指针不会减少种类数。这种 "不满足就收缩" 的策略,保证了在右指针固定的情况下,当前窗口是 以该右端点为结尾的最长有效子数组。遍历所有右端点,记录最大值,即可得到全局最优解。由于每个元素最多入窗出窗一次,算法正确且高效。


四、实现细节(边界防护)

  • 使用 int nums[100001] = {0} 统计水果出现次数(根据题目提示,水果类型 ≤ 100000)。

  • 变量 basket 记录当前窗口内不同水果的种类数。

  • for (int right = 0; right < n; ++right) 遍历:

    • 入窗口:nums[fruits[right]]++;若该类型首次出现(nums[fruits[right]] == 1),则 basket++

    • basket > 2while (left < right && basket > 2) 循环收缩:

      • nums[fruits[left]]--;若变为 0,则 basket--left++
    • 更新 len = max(len, right - left + 1)

  • 时间复杂度 O(n),空间复杂度 O(100001)(常数空间)。


五、返回值(目标映射)

返回 len,即 能收集到的最大水果数量

三.代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class Solution 
{
public:
    int totalFruit(vector<int>& fruits) 
    {
        // 算法思路:滑动窗口(双指针)
        // 题目本质是求最长连续子数组,使得子数组中不同元素的种类数不超过2。
        // 使用哈希表(数组模拟)记录窗口内每种水果的出现次数,用 basket 记录窗口内不同水果的种类数。
        // 右指针不断扩展,增加水果计数;如果新增水果是第一次出现,则 basket 增加。
        // 如果 basket > 2,则左指针右移,移出水果,如果该水果计数变为0,则 basket 减少。
        // 在满足条件时更新窗口最大长度。

        // 注意:这里局部数组命名为 count 以避免与参数 fruits 重名(原代码使用 nums 会导致隐藏参数,编译警告/错误)
        int count[100001] = {0};   // 哈希表,记录每种水果在窗口内的出现次数(水果类型范围假设为 0~100000)
        int basket = 0;            // 当前窗口中不同水果的种类数
        int n = fruits.size();
        int len = 0;               // 记录满足条件的最长窗口长度

        // 双指针 left 和 right 定义窗口 [left, right]
        for (int left = 0, right = 0; right < n; right++) 
        {
            // 入窗口:将 fruits[right] 加入窗口,增加其计数
            count[fruits[right]]++;

            // 如果加入后该水果是第一次出现(计数变为1),则不同水果种类数加1
            if (count[fruits[right]] == 1) 
            {
                basket++;
            }

            // 判断条件:如果窗口内不同水果种类超过2,则需要收缩左边界
            while (left < right && basket > 2) 
            {
                // 出窗口:将 fruits[left] 移出窗口,减少其计数
                count[fruits[left]]--;

                // 如果移出后该水果计数变为0,则不同水果种类数减1
                if (count[fruits[left]] == 0) 
                {
                    basket--;
                }
                left++;   // 左指针右移,缩小窗口
            }

            // 更新结果:当前窗口满足条件(不同水果种类 <= 2),计算窗口长度
            len = max(len, right - left + 1);
        }

        // 返回最长连续子数组的长度(即可采摘的最大水果数量)
        return len;
    }
};

int main() {
    // 测试用例:水果序列 [1,2,1],最多两种不同水果,整个数组都可以采摘,长度为 3
    vector<int> fruits = {1, 2, 1};
    Solution sol;
    int result = sol.totalFruit(fruits);
    cout << result << endl;  // 输出 3
    return 0;
}

四、易错点分析

难点一:basket 与哈希表计数的联动关系

cpp

复制代码
count[fruits[right]]++;
if (count[fruits[right]] == 1) { basket++; }

为什么容易混淆?

basket 记录的是窗口中不同水果的种类数 ,而 count 记录的是每种水果出现的次数。当右指针扩展时,只有该水果第一次出现(计数从 0 变 1)时,basket 才增加 ;如果该水果之前已经在窗口中,则 basket 不变。

这里的难点在于理解"新增元素不一定增加种类数",需要同时关注计数变化和种类变化的区别,否则容易错误地认为每次入窗口 basket 都加 1。


难点二:收缩窗口时,left 的移动与 basket 减少的条件

复制代码
while (left < right && basket > 2)
{
    count[fruits[left]]--;
    if (count[fruits[left]] == 0) { basket--; }
    left++;
}

核心难点:

左指针右移时,只有当移出的水果计数变为 0(即该种类在窗口中完全消失)时,basket 才减 1 ;否则种类数不变。

这要求理解"种类数"是独立于数量的概念,即使某个水果还有剩余,种类数也不会减少。初学者可能误以为每移出一个元素都要 basket--,导致计数错误。


难点三:while 循环条件 left < right 的必要性

复制代码
while (left < right && basket > 2)

为什么需要 left < right

当窗口长度为 1 时,left == right,此时若 basket > 2 不可能成立(因为只有一个元素),但为了安全防止 left 超过 right,加入 left < right 作为保护条件。

难点在于:即使不加这个条件,本算法在 left 越过 right 后也能结束(因为 basket 会降为 0 或 1),但加上后逻辑更清晰,理解上需要区分"收缩窗口"和"窗口空"的边界情况。


难点四:更新长度的时机与位置

复制代码
len = max(len, right - left + 1);

难点在于为什么放在 while 之后?

因为经过 while 收缩后,窗口一定满足 basket ≤ 2,此时窗口是合法的,所以可以用当前 rightleft 计算长度。

如果放在 while 之前,可能窗口非法(basket > 2)时也更新长度,导致错误。理解"先处理非法状态,再记录合法状态"的顺序是掌握本算法的关键。

五、流程图

🎯 闭幕

🎉 恭喜你完成了「水果成篮」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践

在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题本质是求 最长连续子数组 ,使得子数组中 不同元素的种类数不超过 2 。代码中使用 basket 记录窗口内不同水果的种类数,count 数组记录每种水果的出现次数。请问 为什么当新加入的水果第一次出现时(count == 1),就增加 basket,而移出水果时若 count == 0 就减少 basket 这种计数方式的依据是什么?

  • 滑动窗口在 basket > 2 时收缩左指针,每次只移动 一步 并更新计数。如果窗口内包含多种水果且某些水果计数很大,为什么 一次只移动一步 仍能保证 O(n) 复杂度且不会遗漏最优解?

📚 延伸挑战

  • 如果问题改为 最多可以采摘 k 种不同的水果(即篮子数量为 k,而不是固定 2 种),代码应如何通用化?

  • 如果要求采摘的 水果种类必须恰好为 2 种(而非不超过 2 种),滑动窗口的条件应如何修改?

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏

👤 关注作者 ,获取更多题解

💬 留言交流你的疑问或优化思路


📌 深入思考答案

  • 计数方式依据basket 表示当前窗口内 有效不同值 的个数,当某个水果从 0 变为 1 时,说明窗口新增了一个种类,因此 basket++;当某个水果计数从 1 变为 0 时,说明该种类在窗口中已消失,basket--。这种"进出"计数完全反映了窗口内实际存在的不同种类数。

  • 一次移动一步 是滑动窗口的标准操作,因为右指针每次只扩展一个元素,左指针也至多移动一步就能使窗口重新合法,且每个位置作为窗口左端点只会被移出一次,总移动次数 O(n),不会遗漏任何长度更短的合法窗口,因为所有可能的右端点都被遍历过。

🔍 延伸挑战答案

  • 挑战1 :将硬编码的 2 改为参数 kbasket > k 时收缩窗口,即可支持任意篮子数量,代码其他逻辑不变。

  • 挑战2 :若要求 恰好 2 种 ,则需同时满足 basket == 2 才更新答案,窗口内不能少于 2 种。但若整个数组只有一种水果,则无法满足,此时应返回 0;需额外处理:只有在 basket == 2 时才更新长度,否则不更新(或单独记录单种的情况)。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

相关推荐
怪奇云呼军1 小时前
G.711、Opus 和重采样会拖慢识别吗?闪电智能VoiceAgent 的音频入口怎么选
java·人工智能·python·算法·云计算·音视频
ZC跨境爬虫1 小时前
LeetCode 27. 移除元素(双指针详解 + Java Python 多解法对比)
java·python·leetcode
乐观勇敢坚强的老彭2 小时前
C++ 竞赛常用算法模板速查表
开发语言·c++·算法
高洁013 小时前
工信部教考中心证书
人工智能·深度学习·算法·机器学习·知识图谱
我变成萤火虫3 小时前
河南萌新联赛2026第(三)场:郑州轻工业大学
数据结构·c++·算法·贪心算法·stl·深度优先·哈希算法
Dr.kangder3 小时前
嵌入式面试总结(十九)——内存泄露
单片机·算法·面试·职场和发展·架构·硬件架构
白狐_7983 小时前
408数据结构第7章:B树①——基础概念与结构
数据结构·b树
Herbert_hwt3 小时前
【C语言基础】常量、选择结构与运算符全解析
c语言·开发语言·算法
buhuizhiyuci3 小时前
【Linux 篇】数字世界的水流蓄水池 —— IO 缓冲区机制深度实战解析
linux·运维·服务器·c++