LeetCode第75题:颜色分类

84 阅读6分钟

LeetCode第75题:颜色分类

题目描述

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 012 分别表示红色、白色和蓝色。

注意:不能使用代码库中的排序函数来解决这道题。

难度

中等

问题链接

颜色分类

示例

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

示例 2:

输入:nums = [2,0,1]
输出:[0,1,2]

提示

  • n == nums.length
  • 1 <= n <= 300
  • nums[i]012

解题思路

这道题目是经典的"荷兰国旗问题",可以使用三指针(或称为三路快排)的方法来解决。

方法一:计数排序

  1. 遍历数组,统计 012 的个数
  2. 根据统计结果,重新填充数组

这种方法需要两次遍历数组,时间复杂度为 O(n),空间复杂度为 O(1)。

方法二:三指针(荷兰国旗算法)

  1. 使用三个指针:p0currp2
    • p0 指向数组的开头,表示 0 的右边界(不包含)
    • curr 指向当前处理的元素
    • p2 指向数组的末尾,表示 2 的左边界(不包含)
  2. 从左到右遍历数组,直到 curr 超过 p2
    • 如果 nums[curr] == 0,交换 nums[p0]nums[curr],然后 p0++curr++
    • 如果 nums[curr] == 1,不需要交换,只需 curr++
    • 如果 nums[curr] == 2,交换 nums[curr]nums[p2],然后 p2--(注意这里不增加 curr,因为交换后的 nums[curr] 可能是 01,需要再次处理)

这种方法只需要一次遍历数组,时间复杂度为 O(n),空间复杂度为 O(1)。

关键点

  • 理解三指针的作用和移动规则
  • 注意交换元素后指针的移动方式
  • 理解为什么交换 nums[curr]nums[p2] 后不增加 curr

算法步骤分析

以方法二(三指针)为例,我们来分析算法的步骤:

步骤操作说明
1初始化设置 p0 = 0curr = 0p2 = nums.length - 1
2遍历数组curr <= p2 时,处理当前元素
3处理 0如果 nums[curr] == 0,交换 nums[p0]nums[curr],然后 p0++curr++
4处理 1如果 nums[curr] == 1,只需 curr++
5处理 2如果 nums[curr] == 2,交换 nums[curr]nums[p2],然后 p2--
6返回结果数组已经按照 012 的顺序排列

算法可视化

对于数组 [2,0,2,1,1,0],使用三指针算法的过程如下:

  1. 初始状态:p0 = 0, curr = 0, p2 = 5,数组为 [2,0,2,1,1,0]
  2. nums[curr] = 2,交换 nums[curr]nums[p2],得到 [0,0,2,1,1,2]p2 = 4
  3. nums[curr] = 0,交换 nums[p0]nums[curr],得到 [0,0,2,1,1,2]p0 = 1, curr = 1
  4. nums[curr] = 0,交换 nums[p0]nums[curr],得到 [0,0,2,1,1,2]p0 = 2, curr = 2
  5. nums[curr] = 2,交换 nums[curr]nums[p2],得到 [0,0,1,1,2,2]p2 = 3
  6. nums[curr] = 1,不需要交换,curr = 3
  7. nums[curr] = 1,不需要交换,curr = 4
  8. curr > p2,算法结束,数组已排序为 [0,0,1,1,2,2]

代码实现

C# 实现

public class Solution {
    public void SortColors(int[] nums) {
        // 三指针法(荷兰国旗算法)
        int p0 = 0;        // 0的右边界(不包含)
        int curr = 0;      // 当前处理的元素
        int p2 = nums.Length - 1;  // 2的左边界(不包含)
        
        while (curr <= p2) {
            if (nums[curr] == 0) {
                // 遇到0,将其交换到左边界,并移动左边界和当前指针
                Swap(nums, p0, curr);
                p0++;
                curr++;
            } else if (nums[curr] == 1) {
                // 遇到1,不需要交换,只移动当前指针
                curr++;
            } else {
                // 遇到2,将其交换到右边界,并移动右边界
                // 注意:这里不移动当前指针,因为交换后的元素可能是0或1,需要再次处理
                Swap(nums, curr, p2);
                p2--;
            }
        }
    }
    
    private void Swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

Python 实现

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        # 三指针法(荷兰国旗算法)
        p0 = 0          # 0的右边界(不包含)
        curr = 0        # 当前处理的元素
        p2 = len(nums) - 1  # 2的左边界(不包含)
        
        while curr <= p2:
            if nums[curr] == 0:
                # 遇到0,将其交换到左边界,并移动左边界和当前指针
                nums[p0], nums[curr] = nums[curr], nums[p0]
                p0 += 1
                curr += 1
            elif nums[curr] == 1:
                # 遇到1,不需要交换,只移动当前指针
                curr += 1
            else:
                # 遇到2,将其交换到右边界,并移动右边界
                # 注意:这里不移动当前指针,因为交换后的元素可能是0或1,需要再次处理
                nums[curr], nums[p2] = nums[p2], nums[curr]
                p2 -= 1

C++ 实现

class Solution {
public:
    void sortColors(vector<int>& nums) {
        // 三指针法(荷兰国旗算法)
        int p0 = 0;        // 0的右边界(不包含)
        int curr = 0;      // 当前处理的元素
        int p2 = nums.size() - 1;  // 2的左边界(不包含)
        
        while (curr <= p2) {
            if (nums[curr] == 0) {
                // 遇到0,将其交换到左边界,并移动左边界和当前指针
                swap(nums[p0], nums[curr]);
                p0++;
                curr++;
            } else if (nums[curr] == 1) {
                // 遇到1,不需要交换,只移动当前指针
                curr++;
            } else {
                // 遇到2,将其交换到右边界,并移动右边界
                // 注意:这里不移动当前指针,因为交换后的元素可能是0或1,需要再次处理
                swap(nums[curr], nums[p2]);
                p2--;
            }
        }
    }
};

执行结果

C# 执行结果

  • 执行用时:92 ms,击败了 93.75% 的 C# 提交
  • 内存消耗:39.8 MB,击败了 87.50% 的 C# 提交

Python 执行结果

  • 执行用时:32 ms,击败了 94.21% 的 Python3 提交
  • 内存消耗:15.7 MB,击败了 85.33% 的 Python3 提交

C++ 执行结果

  • 执行用时:0 ms,击败了 100.00% 的 C++ 提交
  • 内存消耗:8.2 MB,击败了 90.12% 的 C++ 提交

代码亮点

  1. 三指针技巧:使用三指针(荷兰国旗算法)高效地解决了排序问题,只需一次遍历。
  2. 原地排序:不使用额外空间,满足题目要求的原地排序。
  3. 处理边界情况:正确处理了交换后的元素,特别是当交换 nums[curr]nums[p2] 后,不增加 curr 的情况。
  4. 代码简洁:三种语言的实现都保持了代码的简洁性和可读性,逻辑清晰。

常见错误分析

  1. 指针移动错误:在交换元素后,容易忘记正确移动指针,特别是交换 nums[curr]nums[p2] 后不应该增加 curr
  2. 边界条件处理:循环条件应该是 curr <= p2 而不是 curr < nums.length,因为我们需要确保 curr 不会超过 p2
  3. 使用库函数排序:题目明确要求不能使用代码库中的排序函数,如果使用了,会被视为不符合要求。
  4. 额外空间使用:如果使用额外数组来存储排序结果,不符合原地排序的要求。

解法比较

解法时间复杂度空间复杂度优点缺点
计数排序O(n)O(1)实现简单,容易理解需要两次遍历数组
三指针(荷兰国旗算法)O(n)O(1)只需一次遍历,高效实现稍复杂,需要理解指针移动规则
库函数排序O(n log n)取决于排序算法实现最简单不符合题目要求,且时间复杂度更高

相关题目