算法解题思路指南

算法解题思路指南

看到题目如何思考?用什么算法?如何分析?


目录

  1. 解题思维框架
  2. 题目特征识别速查表
  3. 数据结构选择策略
  4. 算法选择决策树
  5. 各类问题解题套路
  6. 复杂度分析与优化方向
  7. 常见错误与避坑指南

1. 解题思维框架

1.1 看到题目的第一反应(5秒判断)

复制代码
题目输入 → 观察特征 → 初步判断 → 确定方向

第一步:看数据规模

  • n ≤ 20:可能用暴力、回溯、DFS
  • n ≤ 100:O(n²) 可接受,双指针、模拟
  • n ≤ 1000:O(n²) 勉强,考虑优化到 O(n log n)
  • n ≤ 10⁵:必须 O(n) 或 O(n log n),考虑双指针、滑动窗口、二分、贪心
  • n ≤ 10⁶:必须 O(n),不能用嵌套循环
  • n ≤ 10⁸:必须 O(log n) 或 O(1),二分、数学推导

第二步:看数据类型

  • 数组 → 连续存储 → 索引访问快 → 双指针/滑动窗口/二分
  • 链表 → 离散存储 → 只能顺序访问 → 快慢指针/虚拟头节点
  • 二叉树 → 层次结构 → 递归天然适合 → DFS/BFS
  • 图 → 网络结构 → DFS/BFS/最短路径
  • 字符串 → 特殊数组 → 双指针/KMP

第三步:看问题类型

  • 查找问题 → 有序?二分;无序?哈希表
  • 最值问题 → 动态规划/贪心/单调栈
  • 计数问题 → 哈希表/前缀和
  • 排列组合 → 回溯
  • 区间问题 → 滑动窗口/前缀和/贪心
  • 路径问题 → DFS/BFS/动态规划

1.2 五步解题法

复制代码
第一步:理解题目
第二步:分析数据特征
第三步:确定算法类型
第四步:写出代码框架
第五步:完善细节、边界处理

详细展开

第一步:理解题目

  • 明确输入输出
  • 找出约束条件
  • 理解题目本质要求
  • 画图举例验证理解

第二步:分析数据特征

  • 数据有序吗?(有序→二分)
  • 数据有重复吗?(无重复→哈希表、二分更简单)
  • 数据范围大吗?(大→需要优化)
  • 数据是连续还是离散?(连续→数组技巧;离散→链表技巧)
  • 数据之间有联系吗?(有→图/树结构)

第三步:确定算法类型

  • 查找类:二分、哈希、双指针
  • 遍历类:DFS、BFS、迭代、递归
  • 计算类:动态规划、贪心、数学
  • 构造类:回溯、模拟
  • 组合类:分治、滑动窗口

第四步:写出代码框架

  • 根据算法类型写出基本结构
  • 不必先考虑细节,先写出骨架

第五步:完善细节

  • 边界条件处理
  • 特殊情况判断
  • 优化常数时间
  • 检查常见错误

1.3 通用解题模板

模板一:双指针

cpp 复制代码
// 适用场景:数组、字符串、链表
// 核心思想:两个指针协同工作

// 1. 快慢指针(链表找中点、判断环)
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
    slow = slow->next;
    fast = fast->next->next;
}

// 2. 左右指针(数组反转、二分查找)
int left = 0, right = n - 1;
while (left < right) {
    // 处理逻辑
    left++;
    right--;
}

// 3. 滑动窗口(子数组问题)
int left = 0;
for (int right = 0; right < n; right++) {
    // 扩展窗口
    while (满足收缩条件) {
        // 收缩窗口
        left++;
    }
    // 更新结果
}

模板二:递归

cpp 复制代码
// 适用场景:树、链表、分治问题
// 核心思想:大问题分解为小问题

// 递归三部曲
// 1. 确定参数和返回值
// 2. 确定终止条件
// 3. 确定单层递归逻辑

TreeNode* func(TreeNode* root) {
    // 终止条件
    if (root == nullptr) return nullptr;
    
    // 单层逻辑
    TreeNode* left = func(root->left);   // 递归左子树
    TreeNode* right = func(root->right); // 递归右子树
    // 处理当前节点
    return root;
}

模板三:回溯

cpp 复制代码
// 适用场景:组合、排列、子集、切割问题
// 核心思想:穷举+剪枝

// 回溯三部曲
// 1. 确定递归函数参数
// 2. 确定终止条件
// 3. 确定单层搜索逻辑

void backtrack(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择 : 本层所有选择) {
        处理节点;
        backtrack(下一层参数);  // 递归
        回溯,撤销处理结果;     // 恢复状态
    }
}

模板四:动态规划

cpp 复制代码
// 适用场景:最值、计数、可行性问题
// 核心思想:状态转移

// 动规五步曲
// 1. 确定dp数组及下标含义
// 2. 确定递推公式
// 3. 确定dp数组初始化
// 4. 确定遍历顺序
// 5. 举例推导dp数组

// 01背包模板
vector<int> dp(容量 + 1, 0);
for (int i = 0; i < 物品数量; i++) {        // 遍历物品
    for (int j = 容量; j >= weight[i]; j--) { // 遍历容量(逆序)
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

2. 题目特征识别速查表

2.1 看到这些关键词 → 想到这些算法

关键词 可能算法 原理说明
有序数组 二分查找 利用有序性,每次排除一半
子数组/子串 滑动窗口、前缀和 连续区间,动态调整边界
两个数组对比 双指针、哈希表 避免嵌套循环
链表 快慢指针、虚拟头节点 无法索引访问
环形/循环 快慢指针 快指针追慢指针
DFS、BFS、递归 层次结构天然递归
DFS、BFS、最短路径 网络遍历
排列/组合 回溯 穷举所有可能
最值问题 动态规划、贪心 状态转移或局部最优
重复元素 哈希表 快速查找是否出现过
括号匹配 后进先出特性
单调性 单调栈/队列 维护单调序列
区间问题 贪心、前缀和 排序后处理或预处理
最近/相邻 单调栈 维护候选元素
最长/最短 动态规划、二分 状态转移或搜索

2.2 数据结构特征识别

数组特征

  • 有序 → 二分查找双指针
  • 无序但需查找 → 哈希表排序后二分
  • 连续子数组 → 滑动窗口前缀和
  • 原地修改 → 双指针(避免额外空间)

链表特征

  • 找中点 → 快慢指针
  • 判断环 → 快慢指针
  • 反转 → 迭代/递归
  • 删除/插入 → 虚拟头节点
  • 合并 → 双指针

字符串特征

  • 反转 → 双指针
  • 子串 → 滑动窗口
  • 匹配 → KMP算法
  • 括号 →

二叉树特征

  • 深度/路径 → DFS
  • 层次遍历 → BFS
  • 判断性质 → 递归
  • 构造树 → 递归
  • BST操作 → 利用有序性

2.3 问题类型识别

查找类问题

复制代码
有序数组 → 二分查找(O(log n))
无序数组 → 哈希表(O(n))
链表 → 顺序查找或快慢指针
树 → DFS/BFS

最值类问题

复制代码
单序列最值 → 动态规划
多阶段决策 → 动态规划
局部最优推全局 → 贪心
区间最值 → 单调栈

计数类问题

复制代码
统计出现次数 → 哈希表
统计区间和 → 前缀和
统计路径数 → 动态规划

排列组合类问题

复制代码
所有排列 → 回溯
所有组合 → 回溯
所有子集 → 回溯
特定排列 → 数学/动态规划

3. 数据结构选择策略

3.1 查找场景选择

需求 时间复杂度要求 推荐数据结构
频繁查找单个元素 O(1) 哈希表(unordered_map/set)
范围查找、有序遍历 O(log n) 有序数组+二分 / map/set
查找最值 O(1) 堆(priority_queue)
查找相邻元素 O(1) 单调栈/双指针
查找前驱后继 O(log n) 平衡树(map/set)

3.2 存储场景选择

需求 推荐数据结构 原因
顺序访问 数组 缓存友好
频繁插入删除 链表 O(1)操作
后进先出 递归模拟、括号匹配
先进先出 队列 BFS、滑动窗口
唯一元素 set 自动去重
键值映射 map 快速查找

3.3 优先级比较

复制代码
查找频率高 → 哈希表优先
需要有序 → map/set优先
需要最值 → 堆优先
需要遍历 → 数组优先
内存受限 → 数组优先(比哈希表省空间)

4. 算法选择决策树

4.1 查找问题决策

复制代码
查找问题
├── 数据有序?
│   ├── 是 → 二分查找(O(log n))
│   └── 否
│       ├── 查找频率高?
│       │   ├── 是 → 哈希表预处理(O(n)预处理,O(1)查找)
│       │   └── 否 → 排序后二分(O(n log n)排序 + O(log n)查找)
│       └── 单次查找 → 顺序查找(O(n))

4.2 数组问题决策

复制代码
数组问题
├── 有序?
│   ├── 是 → 二分查找
│   └── 否
│       ├── 子数组问题?
│       │   ├── 是
│       │   │   ├── 固定长度 → 滑动窗口
│       │   │   ├── 可变长度 → 滑动窗口 + 条件判断
│       │   │   └── 需要前缀和 → 前缀和数组
│       │   └── 否
│       │       ├── 双指针可解? → 双指针(O(n))
│       │       └── 否 → 暴力(O(n²))或哈希表优化
├── 需要原地操作?
│   ├── 是 → 双指针(快慢指针)
│   └── 否 → 创建新数组

4.3 链表问题决策

复制代码
链表问题
├── 找位置?
│   ├── 找中点 → 快慢指针(fast走两步,slow走一步)
│   ├── 找倒数第k → 快慢指针(fast先走k步)
│   └── 找特定节点 → 顺序遍历
├── 判断环? → 快慢指针(相遇则有环)
├── 删除/插入?
│   ├── 头节点 → 虚拟头节点
│   └── 其他位置 → 双指针(pre和cur)
├── 反转?
│   ├── 整体反转 → 迭代(pre、cur、next三指针)
│   └── 局部反转 → 递归或迭代
├── 合并?
│   ├── 两个有序 → 双指针比较
│   └── k个 → 分治或堆

4.4 树问题决策

复制代码
树问题
├── 需要深度?
│   ├── 是 → DFS(递归或栈)
│   └── 否
│       ├── 需要层次信息? → BFS(队列)
│       └── 否 → 根据问题选择
├── 需要路径?
│   ├── 所有路径 → DFS
│   ├── 最短路径 → BFS
│   └── 特定路径 → DFS + 条件
├── 需要遍历?
│   ├── 前序/中序/后序 → 递归或迭代(栈)
│   ├── 层序 → BFS(队列)
│   └── 所有节点 → DFS或BFS均可
├── 构造树?
│   ├── 从数组 → 递归(找根节点,划分左右)
│   └── 从遍历序列 → 递归(前序+中序,后序+中序)

4.5 最值问题决策

复制代码
最值问题
├── 问题可分解?
│   ├── 是
│   │   ├── 子问题独立? → 动态规划
│   │   ├── 子问题重叠? → 动态规划
│   │   └── 局部最优可推全局? → 贪心
│   └── 否 → 暴力或数学推导
├── 有约束条件?
│   ├── 是 → 动态规划(背包问题)
│   └── 否 → 贪心或数学
├── 多阶段决策?
│   ├── 是 → 动态规划
│   └── 否 → 根据问题选择

4.6 组合问题决策

复制代码
组合问题
├── 求所有可能?
│   ├── 是 → 回溯(穷举)
│   └── 否
│       ├── 求数量? → 动态规划或数学公式
│       ├── 求最优解? → 动态规划
│       └── 求是否存在? → DFS或动态规划
├── 有剪枝条件?
│   ├── 是 → 回溯+剪枝
│   └── 否 → 回溯

5. 各类问题解题套路

5.1 二分查找类问题

判断是否可用二分

复制代码
1. 数据是否有序?(必须条件)
2. 是否需要查找特定值或边界?
3. 是否可以通过比较排除一半?

解题套路

cpp 复制代码
// 第一步:确定区间定义(左闭右闭 or 左闭右开)
// 推荐:左闭右闭,更容易理解

// 第二步:确定循环条件
// 左闭右闭:while (left <= right)
// 左闭右开:while (left < right)

// 第三步:确定mid计算(防止溢出)
int mid = left + (right - left) / 2;

// 第四步:确定边界更新
// 左闭右闭:right = mid - 1 或 left = mid + 1
// 左闭右开:right = mid 或 left = mid + 1

// 第五步:确定返回值
// 找到:return mid
// 未找到:return -1 或 left(插入位置)

常见变体

  • 查找第一个等于target的位置(左边界)
  • 查找最后一个等于target的位置(右边界)
  • 查找第一个大于target的位置
  • 查找最后一个小于target的位置

5.2 双指针类问题

判断是否可用双指针

复制代码
1. 数组/字符串连续存储?
2. 两个方向协同工作能简化问题?
3. 能避免嵌套循环?

解题套路

cpp 复制代码
// 类型一:快慢指针(原地修改)
int slow = 0;
for (int fast = 0; fast < n; fast++) {
    if (满足条件) {
        nums[slow++] = nums[fast];  // 保留
    }
}
return slow;  // 新长度

// 类型二:左右指针(两端向中间)
int left = 0, right = n - 1;
while (left < right) {
    if (nums[left] + nums[right] == target) {
        // 找到
    } else if (nums[left] + nums[right] < target) {
        left++;
    } else {
        right--;
    }
}

// 类型三:快慢指针(链表)
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
    slow = slow->next;
    fast = fast->next->next;
}
return slow;  // 中点

5.3 滑动窗口类问题

判断是否可用滑动窗口

复制代码
1. 是否是子数组/子串问题?
2. 是否求连续区间?
3. 窗口是否可以动态调整?

解题套路

cpp 复制代码
// 模板框架
int left = 0;
int result = 初始值;

for (int right = 0; right < n; right++) {
    // 1. 扩展窗口:加入right元素
    更新窗口状态;
    
    // 2. 判断是否需要收缩
    while (窗口不满足条件) {
        // 收缩窗口:移除left元素
        更新窗口状态;
        left++;
    }
    
    // 3. 更新结果
    if (满足条件) {
        result = 更新结果;
    }
}

return result;

关键点

  • 窗口内维护什么状态?(计数、和、最大值等)
  • 什么时候扩展?什么时候收缩?
  • 如何判断窗口满足条件?
  • 结果在什么时机更新?

5.4 回溯类问题

判断是否可用回溯

复制代码
1. 需要穷举所有可能?
2. 问题可以抽象为树形结构?
3. 可以通过剪枝减少搜索?

解题套路

cpp 复制代码
// 回溯模板
void backtrack(参数) {
    // 终止条件
    if (满足终止条件) {
        result.push_back(path);  // 收集结果
        return;
    }
    
    // 单层搜索
    for (int i = startIndex; i < n; i++) {
        // 处理当前节点
        path.push_back(元素);
        
        // 递归下一层
        backtrack(新参数);  // startIndex+1 或 i+1
        
        // 回溯:撤销处理
        path.pop_back();
    }
}

剪枝技巧

  • 剩余元素不足以填满结果 → 提前终止
  • 已有元素不符合条件 → 跳过
  • 排序后避免重复 → if (i > startIndex && nums[i] == nums[i-1]) continue

5.5 动态规划类问题

判断是否可用动规

复制代码
1. 是否求最值、计数、可行性?
2. 问题可分解为重叠子问题?
3. 最优子结构?(子问题的最优解能推出原问题最优解)
4. 无后效性?(当前状态确定后,之前的状态不影响后续)

解题套路(五步曲)

cpp 复制代码
// 第一步:确定dp数组含义
// dp[i] 表示什么?dp[i][j] 表示什么?

// 第二步:确定递推公式
// dp[i] = f(dp[i-1], dp[i-2], ...)

// 第三步:确定初始化
// dp[0] = ?, dp[1] = ?

// 第四步:确定遍历顺序
// 正序遍历?逆序遍历?(01背包逆序)

// 第五步:举例推导验证
// 手动计算几个例子,验证公式正确

// 实现示例(斐波那契)
vector<int> dp(n + 1);
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];

背包问题模板

cpp 复制代码
// 01背包(每件物品只能用一次)
vector<int> dp(V + 1, 0);
for (int i = 0; i < n; i++) {
    for (int j = V; j >= weight[i]; j--) {  // 逆序遍历容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

// 完全背包(每件物品可用多次)
vector<int> dp(V + 1, 0);
for (int i = 0; i < n; i++) {
    for (int j = weight[i]; j <= V; j++) {  // 正序遍历容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

5.6 贪心类问题

判断是否可用贪心

复制代码
1. 局部最优能否推出全局最优?
2. 没有明显重叠子问题?
3. 问题可以分解为一系列选择?

解题套路

cpp 复制代码
// 贪心模板
int result = 0;
int 当前状态 = 初始值;

while (还有选择) {
    // 选择局部最优
    int 选择 = 选择局部最优解;
    
    // 更新状态
    result += 选择的影响;
    当前状态 = 新状态;
}

return result;

常见贪心场景

  • 区间调度:按结束时间排序,选最早结束的
  • 区间覆盖:按开始时间排序
  • 跳跃游戏:维护能到达的最远位置
  • 分发糖果:两个方向分别考虑

6. 复杂度分析与优化方向

6.1 复杂度判断

复杂度 数据规模 常见算法
O(1) 任意 直接计算、数学公式
O(log n) ≤ 10⁸ 二分查找、树的查找
O(n) ≤ 10⁶ 双指针、滑动窗口、线性遍历
O(n log n) ≤ 10⁵ 排序、堆、二分优化
O(n²) ≤ 1000 双重循环、DP二维
O(n³) ≤ 100 三重循环
O(2ⁿ) ≤ 20 回溯、DFS穷举
O(n!) ≤ 10 全排列

6.2 优化思路

从 O(n²) 到 O(n)

复制代码
1. 查找优化:哈希表代替内层循环查找
2. 双指针:利用有序性或单调性
3. 滑动窗口:连续子数组问题
4. 前缀和:区间求和问题

从 O(n log n) 到 O(n)

复制代码
1. 滑动窗口替代排序+二分
2. 哈希表替代排序+查找
3. 单调栈替代排序+比较

从 O(n) 到 O(log n)

复制代码
1. 二分查找替代线性查找(有序数据)
2. 利用数学公式直接计算

6.3 空间优化

复制代码
1. 滚动数组:dp[i]只依赖dp[i-1],不用保存整个数组
2. 原地修改:双指针在原数组操作
3. 状态压缩:多维dp压缩为一维
4. 位运算:用int存储多个状态

7. 常见错误与避坑指南

7.1 二分查找常见错误

复制代码
❌ 错误:mid = (left + right) / 2  // 可能溢出
✅ 正确:mid = left + (right - left) / 2

❌ 错误:区间定义不一致(左闭右闭用left < right)
✅ 正确:明确区间定义,坚持循环不变量原则

❌ 错误:边界更新不一致(左闭右闭用right = mid)
✅ 正确:根据区间定义更新边界

7.2 双指针常见错误

复制代码
❌ 错误:忘记更新指针位置
✅ 正确:每次循环都要移动指针

❌ 错误:快指针和慢指针步数设置错误
✅ 正确:找中点fast走两步,找倒数k个fast先走k步

❌ 错误:循环条件错误
✅ 正确:链表遍历要检查fast && fast->next

7.3 滑动窗口常见错误

复制代码
❌ 错误:用if代替while收缩窗口
✅ 正确:窗口需要持续收缩时用while

❌ 错误:窗口状态更新时机错误
✅ 正确:扩展时更新,收缩时也要更新

❌ 错误:忘记处理剩余元素
✅ 正确:循环结束后检查是否满足条件

7.4 回溯常见错误

复制代码
❌ 错误:忘记回溯步骤(撤销处理)
✅ 正确:path.pop_back()恢复状态

❌ 错误:参数传递错误
✅ 正确:startIndex控制下一层起始位置

❌ 错误:剪枝条件判断错误
✅ 正确:理解剪枝的数学条件

7.5 动态规划常见错误

复制代码
❌ 错误:dp数组含义定义不清
✅ 正确:明确dp[i]或dp[i][j]表示什么

❌ 错误:初始化错误
✅ 正确:根据递推公式确定初始化值

❌ 错误:遍历顺序错误
✅ 正确:01背包容量逆序,完全背包容量正序

❌ 错误:没有推导验证
✅ 正确:手动计算几个例子验证公式

8. 实战解题流程

8.1 看到题目后的思考步骤

复制代码
Step 1: 读题(30秒)
- 输入是什么?输出是什么?
- 数据规模?
- 有什么约束?

Step 2: 分类(30秒)
- 这是什么类型的问题?(查找/遍历/计算/构造)
- 数据有什么特征?(有序/连续/离散/层次)

Step 3: 选择算法(60秒)
- 根据分类和数据特征选择算法
- 估算时间复杂度是否满足要求

Step 4: 写代码框架(5分钟)
- 不考虑细节,写出基本结构
- 明确关键变量和循环结构

Step 5: 完善细节(5分钟)
- 边界条件处理
- 特殊情况判断
- 添加必要注释

Step 6: 验证(2分钟)
- 手动跑几个例子
- 检查边界情况
- 检查常见错误点

8.2 卡住时的应对策略

卡在思路

复制代码
1. 重新理解题目(画图举例)
2. 从暴力解法开始
3. 思考暴力解法的优化方向
4. 回顾类似题目
5. 查看题目分类提示

卡在实现

复制代码
1. 写出伪代码
2. 分模块实现
3. 先写主逻辑,再处理边界
4. 简化问题(去掉约束,先解简单版)

卡在边界

复制代码
1. 列举所有边界情况
2. 逐一处理
3. 用assert验证假设
4. 看题解的边界处理方式

总结

解题的核心是:观察特征 → 选择算法 → 遵循模板 → 完善细节

记住:

  • 数据规模决定复杂度要求
  • 数据特征决定数据结构选择
  • 问题类型决定算法方向
  • 循环不变量保证边界正确

最后更新:2024 年

相关推荐
地平线开发者1 小时前
Conv+BN+Add+ReLU 融合机制简介
算法·自动驾驶
yuanyuan2o22 小时前
模型预训练:Hugging Face Transformers 基础
算法·ai·语言模型·自然语言处理·nlp·深度优先
杨充2 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
妄想出头的工业炼药师2 小时前
GS slam mono
算法·开源
_日拱一卒3 小时前
LeetCode:207课程表
java·数据结构·算法·leetcode·职场和发展
用户987409238875 小时前
llamafactory 0.6.3 没有 llamafactory-cli
算法
计算机安禾5 小时前
【算法分析与设计】第26篇:参数化算法与固定参数可解性理论
大数据·人工智能·算法·机器学习·剪枝
AI科技星6 小时前
基于**v=c(空间光速螺旋运动)唯一第一性原理**重新完整求导证明
人工智能·线性代数·算法·机器学习·架构·概率论·学习方法