力扣实训 _ [75].颜色分类 _ 杨辉三角

颜色分类

1. 题目回顾

  • 问题描述: 给定一个包含红色、白色和蓝色,一共 nn 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色的顺序排列。
  • 数值映射: 使用整数 012 分别代表红色、白色和蓝色。
  • 约束条件: 必须在不使用代码库自带的排序函数的情况下解决,且要求原地排序。进阶挑战是设计一个仅使用常数空间的一趟扫描算法。
  • 示例:
    • 输入:[2,0,2,1,1,0] →→ 输出:[0,0,1,1,2,2]

2. 核心思路:线性扫描与模拟归并

虽然题目名为"颜色分类",本质上是一个三路划分(3-way Partition) 问题。我们可以借鉴快速排序中"分区"的思想,通过线性扫描将数组划分为三个区域。

  • 区域定义: 我们将数组想象成被两个边界线切分成了三段:
    1. 左区(已处理): 全为 0
    2. 中区(已处理): 全为 1
    3. 右区(已处理): 全为 2
    4. 未处理区: 夹在中区和右区之间,是当前扫描指针正在探索的区域。
  • 策略: 维护三个指针,随着扫描的进行,不断压缩"未处理区",直到其消失,从而实现类似归并排序中将不同元素归类到对应区间的效果。

3. 算法详细步骤

3.1 递归终止条件(边界处理)

虽然本题最优解通常采用迭代(循环)方式,但在算法逻辑上,"终止条件"定义了问题的结束状态和初始边界:

  • 无效输入处理: 如果数组为空或长度小于 2,直接返回,无需处理。
  • 循环终止判定: 我们设定一个当前遍历指针 curr 和一个右边界指针 p2。当 curr > p2 时,意味着"未处理区"已经消失,所有元素都已归位,算法终止。
  • 初始边界设定:
    • p0 = 0:指向下一个 0 应该放置的位置(即左区的右边界)。
    • p2 = n - 1:指向下一个 2 应该放置的位置(即右区的左边界)。
    • curr = 0:当前正在检查的元素索引。
3.2 分解过程:双指针移动

这里实际上是三指针协同工作 的过程。我们需要根据 nums[curr] 的值,决定如何移动指针来分解数组:

  1. 遇到 0(归入左区):

    • nums[curr]nums[p0] 交换。
    • 移动: p0 向右移一位(扩大 0 的区域),curr 向右移一位(继续检查下一个)。
    • 注:交换回来的必然是 1,所以 curr 可以安全前进。
  2. 遇到 2(归入右区):

    • nums[curr]nums[p2] 交换。
    • 移动: p2 向左移一位(扩大 2 的区域)。
    • 关键点: curr 不动!因为从后面换过来的元素可能是 0,需要再次检查。
  3. 遇到 1(归入中区):

    • 移动: 仅需 curr 向右移一位。1 自然留在了中间区域。
3.3 解决过程:值的捕获

这一步描述了算法如何通过"捕获"特定值来推进排序进度:

  • 捕获 0 当我们捕获到 0 时,实际上是将它从"混乱的未处理区"扔到了"有序的左区"。通过与 p0 交换,我们确保了 p0 左侧全是 0。
  • 捕获 2 当我们捕获到 2 时,将其扔到"有序的右区"。通过与 p2 交换,我们确保了 p2 右侧全是 2。
  • 忽略 1 1 是最安全的元素。只要 0 都在左边,2 都在右边,剩下的中间位置自然就是 1。因此,遇到 1 不需要交换操作,直接"放过"即可。
3.4 合并过程:结果计算

这里的"合并"并非指数组拼接,而是指状态的收敛

  • 区间闭合: 随着 curr 的不断右移和 p2 的不断左移,未处理区间 [curr, p2] 逐渐缩小。
  • 最终状态:curr 越过 p2 时,三个区间完美拼接:[0...p0-1] + [p0...p2] (此时为空) + [p2+1...n-1]
  • 结果产出: 此时原数组 nums 已经被修改为有序状态,无需额外的返回值或新数组,原地修改即为最终结果。

4. 代码实现

复制代码
class Solution {
    public void sortColors(int[] nums) {
        // 3.1 边界处理
        if (nums == null || nums.length <= 1) return;

        int p0 = 0;             // 指向0的右边界
        int curr = 0;           // 当前遍历指针
        int p2 = nums.length - 1; // 指向2的左边界

        // 3.2 & 3.4 循环直到未知区域为空
        while (curr <= p2) {
            if (nums[curr] == 0) {
                // 3.3 捕获0:交换到左边
                swap(nums, curr, p0);
                p0++;
                curr++; // 换过来的一定是1,可以直接走
            } else if (nums[curr] == 2) {
                // 3.3 捕获2:交换到右边
                swap(nums, curr, p2);
                p2--;
                // 注意:curr不动,因为换过来的数还需要检查
            } else {
                // 遇到1,直接放过
                curr++;
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

5. 复杂度分析

  • 时间复杂度: O(n) 。
    • 算法只进行了一次线性扫描,curr 指针从 0 移动到 n ,每个元素最多被访问和交换常数次。
  • 空间复杂度: O(1) 。
    • 我们只使用了 p0, curr, p2 三个指针变量,没有使用任何额外的数组或递归栈空间,完全符合原地排序的要求。

杨辉三角

1. 题目回顾

  • 问题描述: 给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

  • 核心规律: 除了每行的首尾元素为 1 之外,其余每个元素都等于它左上方和右上方的元素之和。

  • 示例: 输入 numRows = 5,输出:text

    复制代码
         [1]
        [1,1]
       [1,2,1]
      [1,3,3,1]
     [1,4,6,4,1]

2. 核心思路:动态规划与状态转移

既然杨辉三角的每个元素都依赖于它上一行的相邻元素,这天然就是一个动态规划问题。

  • 状态定义: 我们将整个杨辉三角看作一个二维状态表 resres.get(i).get(j) 表示第 ii 行、第 jj 列的值。
  • 状态转移方程:
    • 当 j=0j=0 或 j=ij=i 时(即行首和行尾):res[i][j] = 1
    • 其他情况:res[i][j] = res[i-1][j-1] + res[i-1][j]

3. 算法详细步骤

3.1 递归终止条件(边界处理)

在动态规划中,边界处理决定了状态转移能否安全进行:

  • 外层循环边界: 如果 numRows <= 0,则无需生成,直接返回空列表。
  • 内层循环边界(行首与行尾): 在计算每一行的元素时,当列索引 j == 0j == i 时,直接赋值为 1。这避免了在计算第一行(i=0)时去访问不存在的上一行(i-1 = -1)而导致的数组越界异常。

3.2 分解过程:逐行生成

我们将生成整个三角的大问题,分解为生成 nn 个独立行的子问题:

  • 外层循环: 遍历行数 i,从 0numRows - 1
  • 行长度确定: 根据杨辉三角的规律,第 i 行恰好有 i + 1 个元素。因此,内层循环的列索引 j0 遍历到 i(包含 i)。

3.3 解决过程:值的捕获(状态转移)

这一步是算法的核心,即如何获取当前元素的值:

  • 捕获首尾值: 如果 j == 0 || j == i,直接 temp.add(1)
  • 捕获中间值: 如果处于中间位置,则严格按照状态转移方程,从结果集 res 中获取上一行(i-1)的两个相邻元素:
    • 左上方元素:res.get(i-1).get(j-1)
    • 右上方元素:res.get(i-1).get(j)
    • 将两者相加后,加入当前行的临时列表 temp 中。

3.4 合并过程:结果计算

这里的"合并"指的是将每一行独立计算出的结果,拼接到最终的全局结果集中:

  • 行入列: 当内层循环结束,当前行 temp 的所有元素都已计算完毕。此时执行 res.add(temp),将这一行追加到结果集中。
  • 最终态: 当外层循环结束时,res 中已经按顺序包含了从第 0 行到第 numRows - 1 行的所有数据,直接返回 res 即可。

4. 代码实现

复制代码
class Solution {
    public List<List<Integer>> generate(int numRows) {
        // 3.1 边界处理:初始化结果集
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        
        // 3.2 分解过程:逐行生成
        for (int i = 0; i < numRows; i++) {
            List<Integer> temp = new ArrayList<Integer>();
            
            // 3.3 解决过程:计算当前行的每个元素
            for (int j = 0; j <= i; j++) {
                if (j == 0 || j == i) {
                    // 边界条件:行首和行尾必定为 1
                    temp.add(1);
                } else {
                    // 状态转移:等于上一行相邻两数之和
                    temp.add(res.get(i - 1).get(j - 1) + res.get(i - 1).get(j));
                }
            }
            
            // 3.4 合并过程:将当前行加入总结果集
            res.add(temp);
        }
        return res;
    }
}

5. 复杂度分析

  • 时间复杂度: O(numRows2)
    • 我们需要生成 numRows 行,第 i 行有 i+1 个元素。总的元素个数是 1+2+⋯+numRows=numRows(numRows+1)2 。每个元素的计算是 O(1)的,因此总时间复杂度为 O(numRows2) 。
  • 空间复杂度: O(numRows2)
    • 我们需要使用一个二维列表 res 来存储整个杨辉三角的所有元素,占用的空间与元素总数成正
相关推荐
jidaowansui2 小时前
P11375 [GESP202412 六级] 树上游走
数据结构·算法
hai3152475433 小时前
FlashAttention C语言(C++)实现(展示版)
c语言·开发语言·c++·人工智能·算法
林爷万福4 小时前
光谱数据预处理:基线校正、平滑去噪实战
人工智能·算法
8Qi84 小时前
LeetCode 1049:最后一块石头的重量 II —— 题解 ✅
算法·leetcode·职场和发展·动态规划·01背包
wubba lubba dub dub7504 小时前
第四十九周学习周报
人工智能·算法·机器学习
Java_2017_csdn5 小时前
ComplexKeysShardingAlgorithm 小结
java·大数据·算法
海梨花5 小时前
快手面试高频算法题
java·算法·面试
lqqjuly5 小时前
超分辨率算法深度解析(Super-Resolution Algorithms)
算法
一切皆是因缘际会5 小时前
AI智能新时代
数据结构·人工智能·ai·架构