【LeetCode Hot100 刷题日记 (98/100)】75. 颜色分类 —— 数组、双指针、原地操作、荷兰国旗问题🧠

2 阅读5分钟

📌 题目链接:75. 颜色分类 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:数组、双指针、原地操作、荷兰国旗问题

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)


🧠 题目分析

给定一个包含 0(红)1(白)2(蓝) 的整数数组 nums,要求原地排序,使得所有 0 在前,1 居中,2 在后。
⚠️ 不能使用库函数如 sort(),且需满足 一趟扫描 + 常数空间 的进阶要求。

这道题是经典的 「荷兰国旗问题」(Dutch National Flag Problem),由图灵奖得主 Edsger W. Dijkstra 提出,用于演示如何用三路划分(Three-way Partitioning)高效处理含三种取值的数组。

在面试中,此题常被用来考察:

  • 双指针 / 三指针 技巧的掌握;
  • 原地交换边界控制 的理解;
  • 是否能从暴力解法 → 优化到最优解。

🔑 核心算法及代码讲解

本题有 三种主流解法,我们重点讲解 方法三:双指针(p0 与 p2),因其最符合“一趟扫描 + O(1) 空间”的进阶要求,也是面试官最期待的答案。

✅ 方法三:双指针(左指针 p0,右指针 p2)

🎯 核心思想

  • 使用两个指针:
    • p0:指向 下一个 0 应该放置的位置(从左往右);
    • p2:指向 下一个 2 应该放置的位置(从右往左)。
  • 遍历指针 i 从左向右移动,但 i > p2 时停止(因为 p2 右侧已全是 2,无需再处理)。
  • 关键细节:遇到 2 时,不能直接 i++,因为从 p2 交换过来的元素可能是 0 或 1,需再次检查!

📜 C++ 代码(带详细行注释)

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int n = nums.size();
        int p0 = 0;          // 指向下一个 0 应放的位置(左边界)
        int p2 = n - 1;      // 指向下一个 2 应放的位置(右边界)

        // 注意:i <= p2!因为 p2 右侧已全是 2,无需再处理
        for (int i = 0; i <= p2; ++i) {
            // 当前元素是 2:不断与 p2 交换,直到 nums[i] != 2
            while (i <= p2 && nums[i] == 2) {
                swap(nums[i], nums[p2]);
                --p2;        // p2 左移,缩小未处理区域
                // 注意:此处不 i++!因为交换来的 nums[i] 可能是 0/1,需继续处理
            }
            // 此时 nums[i] 只可能是 0 或 1
            if (nums[i] == 0) {
                swap(nums[i], nums[p0]);
                ++p0;        // p0 右移
            }
            // 如果是 1,直接跳过(留在中间区域)
        }
    }
};

💡 为什么需要 while 循环处理 2?

假设 nums = [2, 0, 2, 1, 1, 0],初始 p2 = 5

  • 第一次 i=0nums[0]=2,与 nums[5]=0 交换 → [0, 0, 2, 1, 1, 2]p2=4
  • 此时 nums[0]=0,若直接 i++,会错过这个 0!
  • 但我们的代码在交换后 不立即 i++,而是留在原地检查新值(0),再进入 if (nums[i]==0) 分支处理。

这就是 while 循环的关键作用:确保从右侧交换过来的元素也被正确归位。


🧩 解题思路(分步拆解)

  1. 初始化指针

    • p0 = 0:0 区域的右边界(exclusive);
    • p2 = n-1:2 区域的左边界(exclusive);
    • i = 0:当前遍历位置。
  2. 遍历数组(i <= p2

    • 情况一:nums[i] == 2
      • nums[p2] 交换;
      • p2--
      • 不移动 i,继续检查新 nums[i](可能为 0/1/2)。
    • 情况二:nums[i] == 0
      • nums[p0] 交换;
      • p0++
      • i++(因为从左侧交换来的只能是 1 或已处理过的 0,安全)。
    • 情况三:nums[i] == 1
      • 直接 i++(1 应留在中间,无需移动)。
  3. 终止条件:当 i > p2,说明中间区域已全部为 1,左右分别为 0 和 2,排序完成。


📊 算法分析

方法时间复杂度空间复杂度扫描次数面试推荐度
计数重写O(n)O(1)2 次⭐⭐
双指针(p0/p1)O(n)O(1)1 次⭐⭐⭐
双指针(p0/p2)✅O(n)O(1)1 次⭐⭐⭐⭐⭐
  • 时间复杂度:每个元素最多被访问两次(一次由 i,一次由 p2 交换),仍为 O(n)
  • 空间复杂度:仅用几个指针变量,O(1)
  • 稳定性:本题不要求稳定排序(相同颜色无区别),故交换合法。

💬 面试加分点

  • 能说出这是 Dijkstra 提出的荷兰国旗问题
  • 能解释 为何处理 2 时要用 while 而不是 if
  • 能对比三种方法的优劣,并指出方法三是最优解。

💻 代码

✅ C++ 完整代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int n = nums.size();
        int p0 = 0, p2 = n - 1;
        for (int i = 0; i <= p2; ++i) {
            while (i <= p2 && nums[i] == 2) {
                swap(nums[i], nums[p2]);
                --p2;
            }
            if (nums[i] == 0) {
                swap(nums[i], nums[p0]);
                ++p0;
            }
        }
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    vector<int> nums1 = {2,0,2,1,1,0};
    sol.sortColors(nums1);
    for (int x : nums1) cout << x << " "; // 输出: 0 0 1 1 2 2
    cout << "\n";

    vector<int> nums2 = {2,0,1};
    sol.sortColors(nums2);
    for (int x : nums2) cout << x << " "; // 输出: 0 1 2
    cout << "\n";

    return 0;
}

✅ JavaScript 完整代码

/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var sortColors = function(nums) {
    const n = nums.length;
    let p0 = 0;          // next position for 0
    let p2 = n - 1;      // next position for 2

    for (let i = 0; i <= p2; i++) {
        // Keep swapping 2 to the end until nums[i] is not 2
        while (i <= p2 && nums[i] === 2) {
            [nums[i], nums[p2]] = [nums[p2], nums[i]]; // ES6 swap
            p2--;
        }
        if (nums[i] === 0) {
            [nums[i], nums[p0]] = [nums[p0], nums[i]];
            p0++;
        }
    }
};

// Test
console.log(sortColors([2,0,2,1,1,0])); // [0,0,1,1,2,2]
console.log(sortColors([2,0,1]));       // [0,1,2]

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!