【算法】算法题核心类别与通用解题思路

180 阅读7分钟

以下系统地汇总了算法题的主要类别,并为每一类提供了通用的解题思路和技巧。


算法题核心类别与通用解题思路

算法题虽然千变万化,但绝大多数都可以归入以下几类。掌握每一类的“解题模板”和核心思想,就能做到举一反三。

1. 数据结构类 (Data Structures)

这类问题直接考察对基础数据结构的应用和理解。

  • 数组 & 字符串 (Array & String)

    • 核心思想: 在连续内存空间上进行操作。常用技巧是使用指针(下标)来遍历或标记位置。
    • 通用思路
      • 双指针 (Two Pointers): 解决有序数组、去重、合并、滑动窗口等问题。
        • 对撞指针: 一前一后向中间遍历,用于“两数之和”、“反转字符串”等。
        • 快慢指针: 一快一慢,用于“判断循环”、“找中点”、“删除元素”等。
      • 滑动窗口 (Sliding Window): 解决子串/子数组问题(如“找和为K的最短子数组”、“最长无重复字符子串”)。
        • 模板: 用 leftright 指针维护一个窗口,right 向右扩张,left 根据条件向右收缩,并在此过程中更新答案。
      • 前缀和 (Prefix Sum): 快速求解任意区间和。
        • 模板: 预处理一个 prefix[i] 数组,存储 nums[0]nums[i-1] 的和,则区间 [i, j] 的和为 prefix[j+1] - prefix[i]
  • 链表 (Linked List)

    • 核心思想: 指针操作是核心。注意处理头节点和边界条件(空链表、单节点链表)。
    • 通用思路
      • 虚拟头节点 (Dummy Node): 在任何可能修改头节点的操作前,创建一个 dummy 节点指向 head,可以简化代码,避免空指针和复杂判断。
      • 双指针
        • 快慢指针: 找链表中点、判断环路、找倒数第K个节点。
        • 多指针操作: 在“反转链表”、“K个一组反转链表”中,需要精确控制 prev, curr, next 等指针的指向。
  • 栈 & 队列 (Stack & Queue)

    • 核心思想: 栈(LIFO)用于处理对称性、递归性问题;队列(FIFO)用于处理顺序性、BFS问题。
    • 通用思路
      • 单调栈 (Monotonic Stack): 解决“下一个更大元素”、“柱状图中最大矩形”等问题。
        • 模板: 遍历数组,维护一个栈(从栈底到栈顶单调递增或递减)。当新元素不满足单调性时,弹出栈顶元素并进行计算,直到满足条件再入栈。
      • 优先队列 (堆, Priority Queue): 解决“Top K”、“中位数”、“调度”问题。C++中可用 std::priority_queue
  • 哈希表 (Hash Table)

    • 核心思想: 用空间换时间,实现O(1)时间的查找和插入。C++中可用 std::unordered_map / std::unordered_set
    • 通用思路
      • 快速查找: 用于“两数之和”、“判断重复”等需要快速查询元素是否存在的场景。
      • 频率统计: 用于“统计字母频率”、“找出现次数最多的元素”等。
  • 树 (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皇后”等问题。
        1. 做出选择 (Make choice)
        2. 递归 (Recurse)
        3. 撤销选择 (Undo choice) // 回溯的核心
      • 剪枝 (Pruning): 在递归过程中提前判断当前路径不可能得到正确结果,直接返回,极大提升效率。
  • 分治 (Divide and Conquer)

    • 核心思想: 将大问题分解成小问题,解决小问题,再合并结果。“归并排序”和“快速排序”是经典例子。
    • 通用思路
      • 模板
        1. 分解 (Divide): 将原问题分解成子问题。
        2. 解决 (Conquer): 递归地解决子问题。
        3. 合并 (Combine): 将子问题的结果合并成原问题的解。
      • 应用: “合并K个排序链表”、“计算逆序对”。
  • 动态规划 (Dynamic Programming)

    • 核心思想: 用空间存储子问题的解,避免重复计算。解决具有重叠子问题最优子结构的问题。
    • 通用思路 (四步法)
      1. 定义状态 (dp数组的含义): 比如 dp[i]dp[i][j] 代表什么。
      2. 确定状态转移方程: 这是最关键的一步,找出 dp[i] 和之前状态(如 dp[i-1], dp[i-2])的关系。
      3. 初始化: 初始化 dp 数组的初始值,作为递推的基础。
      4. 确定遍历顺序: 按什么顺序填充 dp 数组(自顶向下记忆化搜索 or 自底向上迭代)。
    • 常见类型: 背包问题、序列问题(最长递增子序列LIS)、字符串编辑距离、股票买卖问题。
  • 贪心算法 (Greedy)

    • 核心思想: 每一步都做出当前看来最优的选择,希望导致全局最优。不像DP那样有固定的公式,需要证明其正确性
    • 通用思路: 通常需要对数据进行排序预处理。
    • 应用: “区间调度”、“分发饼干”、“跳跃游戏”。
  • 二分查找 (Binary Search)

    • 核心思想: 在有序集合中,通过比较中间元素快速将搜索范围减半。
    • 通用思路
      • 模板: 记住一个清晰的循环不变量(例如,leftright 定义的搜索区间是 [left, right]),就能避免边界错误。
      • 应用: 不仅用于找值,更用于“寻找旋转排序数组中的最小值”、“在排序数组中查找元素的第一个和最后一个位置”等边界问题

学习建议 (以下部分是基于C++开发栈)

  1. 从核心数据结构开始: 即使很熟悉,也请用刷题的视角重新审视它们。用C++的STL(vector, list, unordered_map, priority_queue等)去实现,这能极大提升编码效率。
  2. 按类别刷题: 不要随机刷题。在一到两周内,集中刷某一类别(如“动态规划”),总结这类问题的共通点和模板。
  3. 先思考,再编码: 看到题目,先花5-10分钟分析:
    • 属于哪一类问题?
    • 有哪些可能的解法?时间/空间复杂度各是多少?
    • 哪种是最优解?
  4. 重视代码质量
    • 边界条件: 空输入、单个元素、负数等。
    • 代码风格: 变量名清晰、函数功能单一、添加关键注释。
    • 复杂度分析: 能清晰地说出自己算法的时间和空间复杂度。
  5. 使用C++的优势
    • STL是利器: 熟练使用 algorithm 头文件中的 sort, lower_bound, max 等函数。
    • 智能指针/引用: 在复杂数据结构问题中,如果可以,提及如何使用它们来管理内存或避免拷贝,体现您的工程能力。
  6. 经典题库LeetCode 是首选。可以从它的“学习计划”(如“算法面试题汇总”)或者“热门100题”开始,这些都是经过市场检验的高频考题。

最后,也是最重要的:总结和复盘。 准备一个笔记本(或电子文档),为每一类算法画出思维导图,记录经典例题和你的解题模板。定期回顾,内化成自己的知识体系。