以下系统地汇总了算法题的主要类别,并为每一类提供了通用的解题思路和技巧。
算法题核心类别与通用解题思路
算法题虽然千变万化,但绝大多数都可以归入以下几类。掌握每一类的“解题模板”和核心思想,就能做到举一反三。
1. 数据结构类 (Data Structures)
这类问题直接考察对基础数据结构的应用和理解。
-
数组 & 字符串 (Array & String)
- 核心思想: 在连续内存空间上进行操作。常用技巧是使用指针(下标)来遍历或标记位置。
- 通用思路:
- 双指针 (Two Pointers): 解决有序数组、去重、合并、滑动窗口等问题。
- 对撞指针: 一前一后向中间遍历,用于“两数之和”、“反转字符串”等。
- 快慢指针: 一快一慢,用于“判断循环”、“找中点”、“删除元素”等。
- 滑动窗口 (Sliding Window): 解决子串/子数组问题(如“找和为K的最短子数组”、“最长无重复字符子串”)。
- 模板: 用
left和right指针维护一个窗口,right向右扩张,left根据条件向右收缩,并在此过程中更新答案。
- 模板: 用
- 前缀和 (Prefix Sum): 快速求解任意区间和。
- 模板: 预处理一个
prefix[i]数组,存储nums[0]到nums[i-1]的和,则区间[i, j]的和为prefix[j+1] - prefix[i]。
- 模板: 预处理一个
- 双指针 (Two Pointers): 解决有序数组、去重、合并、滑动窗口等问题。
-
链表 (Linked List)
- 核心思想: 指针操作是核心。注意处理头节点和边界条件(空链表、单节点链表)。
- 通用思路:
- 虚拟头节点 (Dummy Node): 在任何可能修改头节点的操作前,创建一个
dummy节点指向head,可以简化代码,避免空指针和复杂判断。 - 双指针:
- 快慢指针: 找链表中点、判断环路、找倒数第K个节点。
- 多指针操作: 在“反转链表”、“K个一组反转链表”中,需要精确控制
prev,curr,next等指针的指向。
- 虚拟头节点 (Dummy Node): 在任何可能修改头节点的操作前,创建一个
-
栈 & 队列 (Stack & Queue)
- 核心思想: 栈(LIFO)用于处理对称性、递归性问题;队列(FIFO)用于处理顺序性、BFS问题。
- 通用思路:
- 单调栈 (Monotonic Stack): 解决“下一个更大元素”、“柱状图中最大矩形”等问题。
- 模板: 遍历数组,维护一个栈(从栈底到栈顶单调递增或递减)。当新元素不满足单调性时,弹出栈顶元素并进行计算,直到满足条件再入栈。
- 优先队列 (堆, Priority Queue): 解决“Top K”、“中位数”、“调度”问题。C++中可用
std::priority_queue。
- 单调栈 (Monotonic Stack): 解决“下一个更大元素”、“柱状图中最大矩形”等问题。
-
哈希表 (Hash Table)
- 核心思想: 用空间换时间,实现O(1)时间的查找和插入。C++中可用
std::unordered_map/std::unordered_set。 - 通用思路:
- 快速查找: 用于“两数之和”、“判断重复”等需要快速查询元素是否存在的场景。
- 频率统计: 用于“统计字母频率”、“找出现次数最多的元素”等。
- 核心思想: 用空间换时间,实现O(1)时间的查找和插入。C++中可用
-
树 (Tree)
- 核心思想: 递归是处理树问题最自然的方式。几乎所有问题都可以用DFS(深度优先搜索)解决。
- 通用思路:
- DFS (递归): 前序、中序、后序遍历,解决“路径和”、“深度”、“属性判断”等问题。
- BFS (层序遍历): 使用队列,解决“层序遍历”、“找最短路径(在树中)”等问题。
- 二叉搜索树 (BST): 利用中序遍历有序和左<根<右的性质进行高效查找、验证和范围查询。
-
图 (Graph)
- 核心思想: 图是树的泛化,算法也更复杂。核心是遍历(DFS/BFS)和拓扑排序。
- 通用思路:
- 表示方法: 邻接表(常用,
vector<vector>或unordered_map<int, vector>)或邻接矩阵。 - DFS/BFS遍历: 是解决几乎所有图问题的基础(如“连通块”、“最短路径”)。
- 拓扑排序 (Topological Sort): 用于有向无环图(DAG)的任务调度、依赖分析。可用BFS(Kahn算法,计算入度)或DFS实现。
- 并查集 (Union-Find): 高效解决“动态连通性”问题(如“朋友圈”、“岛屿数量”的变种)。
- 表示方法: 邻接表(常用,
2. 算法策略类 (Algorithmic Paradigms)
这类是解决问题的宏观方法和思想,是面试中的重中之重。
-
递归 & 回溯 (Recursion & Backtracking)
- 核心思想: 通过函数自我调用,尝试所有可能的解,如果当前路径不行就“撤销选择,回溯到上一步”。
- 通用思路:
- 模板: 解决“全排列”、“组合”、“N皇后”等问题。
- 做出选择 (Make choice)
- 递归 (Recurse)
- 撤销选择 (Undo choice) // 回溯的核心
- 剪枝 (Pruning): 在递归过程中提前判断当前路径不可能得到正确结果,直接返回,极大提升效率。
- 模板: 解决“全排列”、“组合”、“N皇后”等问题。
-
分治 (Divide and Conquer)
- 核心思想: 将大问题分解成小问题,解决小问题,再合并结果。“归并排序”和“快速排序”是经典例子。
- 通用思路:
- 模板:
- 分解 (Divide): 将原问题分解成子问题。
- 解决 (Conquer): 递归地解决子问题。
- 合并 (Combine): 将子问题的结果合并成原问题的解。
- 应用: “合并K个排序链表”、“计算逆序对”。
- 模板:
-
动态规划 (Dynamic Programming)
- 核心思想: 用空间存储子问题的解,避免重复计算。解决具有重叠子问题和最优子结构的问题。
- 通用思路 (四步法):
- 定义状态 (dp数组的含义): 比如
dp[i]或dp[i][j]代表什么。 - 确定状态转移方程: 这是最关键的一步,找出
dp[i]和之前状态(如dp[i-1],dp[i-2])的关系。 - 初始化: 初始化
dp数组的初始值,作为递推的基础。 - 确定遍历顺序: 按什么顺序填充
dp数组(自顶向下记忆化搜索 or 自底向上迭代)。
- 定义状态 (dp数组的含义): 比如
- 常见类型: 背包问题、序列问题(最长递增子序列LIS)、字符串编辑距离、股票买卖问题。
-
贪心算法 (Greedy)
- 核心思想: 每一步都做出当前看来最优的选择,希望导致全局最优。不像DP那样有固定的公式,需要证明其正确性。
- 通用思路: 通常需要对数据进行排序预处理。
- 应用: “区间调度”、“分发饼干”、“跳跃游戏”。
-
二分查找 (Binary Search)
- 核心思想: 在有序集合中,通过比较中间元素快速将搜索范围减半。
- 通用思路:
- 模板: 记住一个清晰的循环不变量(例如,
left和right定义的搜索区间是[left, right]),就能避免边界错误。 - 应用: 不仅用于找值,更用于“寻找旋转排序数组中的最小值”、“在排序数组中查找元素的第一个和最后一个位置”等边界问题。
- 模板: 记住一个清晰的循环不变量(例如,
学习建议 (以下部分是基于C++开发栈)
- 从核心数据结构开始: 即使很熟悉,也请用刷题的视角重新审视它们。用C++的STL(
vector,list,unordered_map,priority_queue等)去实现,这能极大提升编码效率。 - 按类别刷题: 不要随机刷题。在一到两周内,集中刷某一类别(如“动态规划”),总结这类问题的共通点和模板。
- 先思考,再编码: 看到题目,先花5-10分钟分析:
- 属于哪一类问题?
- 有哪些可能的解法?时间/空间复杂度各是多少?
- 哪种是最优解?
- 重视代码质量:
- 边界条件: 空输入、单个元素、负数等。
- 代码风格: 变量名清晰、函数功能单一、添加关键注释。
- 复杂度分析: 能清晰地说出自己算法的时间和空间复杂度。
- 使用C++的优势:
- STL是利器: 熟练使用
algorithm头文件中的sort,lower_bound,max等函数。 - 智能指针/引用: 在复杂数据结构问题中,如果可以,提及如何使用它们来管理内存或避免拷贝,体现您的工程能力。
- STL是利器: 熟练使用
- 经典题库: LeetCode 是首选。可以从它的“学习计划”(如“算法面试题汇总”)或者“热门100题”开始,这些都是经过市场检验的高频考题。
最后,也是最重要的:总结和复盘。 准备一个笔记本(或电子文档),为每一类算法画出思维导图,记录经典例题和你的解题模板。定期回顾,内化成自己的知识体系。