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

问题本质
我们需要找到所有长度为 n,且相邻两位数字的绝对值差为 k 的非负整数。
- 暴力思路:遍历所有
n位数,逐一校验差值。 - 核心痛点:
n=9时需要遍历 9 亿个数字,时间复杂度为 O(9×10n−1×n),完全超时。
思维跃迁:反向生成
与其 "生成所有数再筛选",不如从第一位开始,按规则生成后续数字------ 这就是 DFS 回溯的核心思想:
- 起点选择:第一位数字从 1-9 选取(避免前导零);
- 递归生成 :基于最后一位数字,生成
last + k和last - k(需在 0-9 范围内); - 终止条件 :当数字长度达到
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 度可以拆解为两次更简单的操作:
- 水平翻转 :矩阵上下翻转(行
i与行n-1-i交换); - 对角线翻转:沿主对角线(左上到右下)翻转。
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()];
}
};
思维跃迁:蓄水池抽样算法
蓄水池抽样可以在未知链表长度、仅遍历一次的情况下,保证每个节点被选中的概率相等:
- 初始化:选择第一个节点作为初始结果;
- 遍历更新 :遍历到第
i个节点时,以1/i的概率选择该节点替换当前结果; - 最终结果:遍历结束后,当前结果即为随机选中的节点。
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,即每个节点被选中的概率相等。
写在最后:算法思维的成长路径
从这三道题可以看出,算法的 "深度" 不在于代码的复杂度,而在于思路的优化方向:
- 连续差相同的数字:从 "生成后校验" 到 "边生成边校验",减少无效计算;
- 旋转图像:从 "借助额外空间" 到 "原地变换拆解",降低空间复杂度;
- 链表随机节点:从 "全量存储" 到 "蓄水池抽样",适应大数据场景。
刷题的最终目的,是训练我们透过问题表象,找到本质规律的能力。当我们不再满足于 "暴力 AC",而是追求 "更优解" 时,算法思维的真正成长才刚刚开始。