力扣刷题之路

在算法刷题的过程中,最有价值的收获往往不是 "AC" 的瞬间,而是思路从 "蛮力堆砌" 到 "精准巧解" 的跃迁。今天我以连续差相同的数字、旋转图像、链表随机节点这三道题为例,聊聊如何透过问题表象,找到更本质的解题路径。


967. 连续差相同的数字:从 "大海捞针" 到 "定向生成"

问题本质

我们需要找到所有长度为 n,且相邻两位数字的绝对值差为 k 的非负整数。

  • 暴力思路:遍历所有 n 位数,逐一校验差值。
  • 核心痛点:n=9 时需要遍历 9 亿个数字,时间复杂度为 O(9×10n−1×n),完全超时。

思维跃迁:反向生成

与其 "生成所有数再筛选",不如从第一位开始,按规则生成后续数字------ 这就是 DFS 回溯的核心思想:

  1. 起点选择:第一位数字从 1-9 选取(避免前导零);
  2. 递归生成 :基于最后一位数字,生成 last + klast - k(需在 0-9 范围内);
  3. 终止条件 :当数字长度达到 n 时,加入结果集。
cpp 复制代码
void dfs(int num, int len) {
    if (len == target_len) {
        ret.push_back(num);
        return;
    }
    int last = num % 10;
    if (last + diff <= 9) dfs(num * 10 + (last + diff), len + 1);
    if (diff != 0 && last - diff >= 0) dfs(num * 10 + (last - diff), len + 1);
}

深度思考

这道题的优化本质是减少无效计算。暴力解法的无效计算量是 "所有不符合条件的数字",而 DFS 只生成符合条件的数字,时间复杂度直接降到 O(2n)。当问题要求 "生成满足特定规则的序列" 时,反向生成往往是更高效的选择。


48. 旋转图像:从 "借助外力" 到 "原地起舞"

问题本质

n×n 矩阵顺时针旋转 90 度,要求原地修改,不能使用额外矩阵。

  • 暴力思路:复制一个临时矩阵,按旋转规则填充后再赋值回原矩阵。
  • 核心痛点:空间复杂度为 O(n2),违反了 "原地修改" 的要求。
cpp 复制代码
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        auto tmp = matrix;
        for (int i = 0; i < n; ++i) 
            for (int j = 0; j < n; ++j) 
                tmp[j][n - i - 1] = matrix[i][j];
        matrix = tmp;
    }
};

思维跃迁:两次翻转替代旋转

顺时针旋转 90 度可以拆解为两次更简单的操作:

  1. 水平翻转 :矩阵上下翻转(行 i 与行 n-1-i 交换);
  2. 对角线翻转:沿主对角线(左上到右下)翻转。
cpp 复制代码
void rotate(vector<vector<int>>& matrix) {
    int n = matrix.size();
    // 水平翻转
    for (int i = 0; i < n / 2; ++i) {
        swap(matrix[i], matrix[n-1-i]);
    }
    // 主对角线翻转
    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) {
            swap(matrix[i][j], matrix[j][i]);
        }
    }
}

深度思考

这道题的关键是将复杂操作拆解为基础变换。旋转是一个复杂的几何变换,但翻转是更基础、更易实现的操作。这种 "拆解" 思维在矩阵类问题中非常常见 ------ 比如逆时针旋转 90 度可以拆解为 "垂直翻转 + 对角线翻转"。


382. 链表随机节点:从 "全量存储" 到 "蓄水池抽样"

问题本质

在单链表中随机选择一个节点,要求每个节点被选中的概率相等。

  • 暴力思路:遍历链表,将所有节点值存入数组,再随机返回数组中的一个元素。
  • 核心痛点:空间复杂度为 O(n),当链表长度极大时(如 100 万节点),内存开销不可忽视。
cpp 复制代码
class Solution {
    vector<int> arr;

public:
    Solution(ListNode *head) 
    {
        while (head) 
        {
            arr.push_back(head->val);
            head = head->next;
        }
    }

    int getRandom() 
    {
        return arr[rand() % arr.size()];
    }
};

思维跃迁:蓄水池抽样算法

蓄水池抽样可以在未知链表长度、仅遍历一次的情况下,保证每个节点被选中的概率相等:

  1. 初始化:选择第一个节点作为初始结果;
  2. 遍历更新 :遍历到第 i 个节点时,以 1/i 的概率选择该节点替换当前结果;
  3. 最终结果:遍历结束后,当前结果即为随机选中的节点。
cpp 复制代码
int getRandom() {
    ListNode* curr = head;
    int res = curr->val;
    int i = 2;
    while (curr->next) {
        curr = curr->next;
        if (rand() % i == 0) res = curr->val;
        i++;
    }
    return res;
}

深度思考

这道题的优化本质是用时间换空间 。当数据量极大或无法一次性加载时,蓄水池抽样是处理 "随机选择" 问题的利器。它的核心数学保证是:对于第 i 个节点,最终被保留的概率为 1/i × i/(i+1) × ... × (n-1)/n = 1/n,即每个节点被选中的概率相等。


写在最后:算法思维的成长路径

从这三道题可以看出,算法的 "深度" 不在于代码的复杂度,而在于思路的优化方向:

  1. 连续差相同的数字:从 "生成后校验" 到 "边生成边校验",减少无效计算;
  2. 旋转图像:从 "借助额外空间" 到 "原地变换拆解",降低空间复杂度;
  3. 链表随机节点:从 "全量存储" 到 "蓄水池抽样",适应大数据场景。

刷题的最终目的,是训练我们透过问题表象,找到本质规律的能力。当我们不再满足于 "暴力 AC",而是追求 "更优解" 时,算法思维的真正成长才刚刚开始。

相关推荐
不能隔夜的咖喱10 分钟前
牛客网刷题(2)
java·开发语言·算法
VT.馒头11 分钟前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript
进击的小头28 分钟前
实战案例:51单片机低功耗场景下的简易滤波实现
c语言·单片机·算法·51单片机
咖丨喱2 小时前
IP校验和算法解析与实现
网络·tcp/ip·算法
罗湖老棍子2 小时前
括号配对(信息学奥赛一本通- P1572)
算法·动态规划·区间dp·字符串匹配·区间动态规划
fengfuyao9853 小时前
基于MATLAB的表面织构油润滑轴承故障频率提取(改进VMD算法)
人工智能·算法·matlab
机器学习之心3 小时前
基于随机森林模型的轴承剩余寿命预测MATLAB实现!
算法·随机森林·matlab
一只小小的芙厨3 小时前
寒假集训笔记·树上背包
c++·笔记·算法·动态规划
庄周迷蝴蝶3 小时前
四、CUDA排序算法实现
算法·排序算法
以卿a3 小时前
C++(继承)
开发语言·c++·算法