找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3779

积分

0

好友

503

主题
发表于 昨天 20:04 | 查看: 11| 回复: 0

开篇

面试官问你“两数之和”,你写了双重循环,他追一句“能更快吗?”你用哈希表做到 O(n),他点头放过。

但你有没有想过:为什么哈希表就能更快? 为什么有些问题暴力只能 O(n²) 却存在 O(n log n) 的解法?为什么有些 O(n log n) 已经是极限,再怎么努力都不可能更快?

这篇文章不讲具体题目。我们退一步,从更高的视角回答一个根本问题:

算法优劣的本质差异是什么?

理解了这件事,后面每一篇具体的算法文章你都会知道——它在做什么、为什么能做到、以及什么时候该用它。


内功心法

一、算法的本质是搜索

所有算法都在做同一件事:在解空间中找到满足条件的解。

排序问题的解空间是 n 个元素的全排列——共 n! 种可能。一个正确的排序算法,就是从这 n! 种排列中找到唯一有序的那一个。

暴力法逐一检查每种可能。而好算法之所以快,是因为它每一步操作都能排除大量不可能的候选

比较排序每次比较两个元素,结果只有“大于”或“小于”两种——相当于一个 yes/no 问题。每个 yes/no 最多把剩余可能性砍掉一半。n! 种可能需要砍多少次?

这就是比较排序 O(n log n) 下界的由来。不是某个算法恰好做到这么快,而是信息论决定了你至少需要这么多次比较才能确定答案

核心命题:优化 = 利用更多信息来缩小搜索空间。

暴力法遍历完整空间 vs 优化算法利用信息跳过区域

暴力法遍历全部空间(左)vs 好算法利用信息跳过大片区域(右)

暴力法不利用任何信息,所以它遍历全部空间。每一种“更好的算法”,本质上都是发现并利用了问题中的某种结构性信息,从而跳过大片不可能的区域。


二、五大策略:利用不同类型的信息

所有算法策略,归根结底都在回答同一个问题:“我能利用什么信息来缩小搜索空间?”只是它们利用的信息类型不同。

分治——利用“问题的递归结构”

如果一个大问题可以拆成若干互不影响的子问题,分别解决后合并,那搜索空间就从一整块变成了几小块之和。

归并排序把 n 个元素拆成两半分别排序再合并——每层做 O(n) 工作,共 log n 层,总计 O(n log n)。快速排序选一个 pivot 把数组分成“比它小”和“比它大”两部分,每个子问题的搜索空间直接减半。

典型应用:归并排序、快速排序、快速幂、二维矩阵搜索。

贪心——利用“局部最优 = 全局最优”的结构

有些问题存在一个强性质:每一步选当前最好的,最终结果就是全局最好的。这意味着你根本不需要考虑未来的所有可能,只需要看眼前。

贪心之所以快,是因为它把指数级的搜索空间压缩成了线性的“逐步选择”。但它的前提很严格——必须满足“贪心选择性质”和“无后效性”。一旦不满足,贪心就会给出错误答案。

典型应用:区间调度、Huffman 编码、股票买卖(单次/无限次)、分糖果、排序不等式。

动态规划——利用“重叠子问题 + 最优子结构”

如果问题可以拆成子问题,但子问题之间不是独立的(同一个子问题会被反复用到),那就把每个子问题的答案记下来,下次直接查表。

暴力递归可能是指数级的——因为同一个子问题被算了无数遍。动态规划通过“记忆化”把这些重复全部消除,时间复杂度降到“状态数 × 每个状态的转移代价”。

典型应用:背包问题(01/完全/多重/分组)、最长递增子序列(LIS)、最长公共子序列(LCS)、编辑距离、股票买卖 K 次、最长回文串、正则匹配、扔鸡蛋、打家劫舍。

回溯——利用“约束条件来剪枝”

有些问题没有贪心性质,子问题也没有重叠,只能老老实实穷举。但即便如此,也不必把所有可能都走完——一旦发现当前路径违反约束,立即回头。

回溯的本质是“带剪枝的深度优先搜索”。剪枝越狠,实际遍历的空间越小。好的剪枝策略可以把指数级的理论复杂度压缩到可接受的范围。

典型应用:组合/排列生成、N 皇后、矩阵路径搜索、切割问题、机器人计数。

二分搜索——利用“单调性/有序性”

如果解空间是有序的,或者目标函数具有单调性,那每次比较都能排除一半——搜索空间以指数速度收缩。

二分不仅仅用在“有序数组中找元素”。只要你能定义一个判定函数,它在某个分界点左边返回 true、右边返回 false(或反过来),就可以二分。

典型应用:旋转数组最小值、缺失数字、出现次数统计、值等于下标的元素。

缩小搜索空间的五大算法策略:分治、贪心、动态规划、回溯、二分搜索

五大策略的统一视角:利用不同类型的信息


三、数据结构:预处理的信息容器

算法策略解决的是“用什么思路搜索”。数据结构解决的是另一个问题:如何提前把信息组织好,让后续查询更快?

每种数据结构都是一个“信息容器”——它预先花一些代价把数据整理成特定形式,换取后续操作的加速。选择数据结构,本质上是在问:“我后续最频繁的操作是什么?有没有一种结构能让这个操作接近 O(1) 或 O(log n)?”

数据结构 预存的信息 加速什么操作
哈希表 键→值的直接映射 O(1) 查找、判重(布隆过滤器是概率版本)
动态维护的极值 O(1) 取最大/最小,O(log n) 插入删除
单调栈 每个元素左/右第一个更大(更小)值 O(n) 完成所有元素的“最近更大值”查询
单调队列 滑动窗口内的极值 O(1) 获取窗口最值
前缀和 / 差分 区间的累加信息 O(1) 区间和查询 / O(1) 区间批量修改
Trie 字符串的公共前缀 O(L) 前缀匹配,多模式串搜索(AC 自动机)
并查集 元素间的连通关系 近 O(1) 的合并与查询
跳表 多级索引的有序结构 O(log n) 有序查找(Redis 的选择)
B+ 树 / BST / Splay 有序平衡结构 O(log n) 范围查询、第 K 大、中序后继
Merkle 树 层层哈希摘要 O(log n) 数据完整性校验
链表 节点间的指针连接 O(1) 插入删除(反转、LRU、快慢指针)

四、高级技巧:信息的巧妙编码

除了五大策略和数据结构选择,还有一类优化手段:用巧妙的编码方式把信息压缩到更紧凑的表示中,从而减少搜索代价。

双指针 ——用两个指针编码当前搜索状态。对于有序数组中的“两数之和”,左指针向右移动意味着“当前和太小”,右指针向左意味着“当前和太大”。每次移动都利用了有序性排除一整行/列的候选,把 O(n²) 降到 O(n)。盛水容器、接雨水、荷兰国旗分区都是同一思想的变体。

滑动窗口 ——维护一个满足条件的连续子段,右端扩张、左端收缩,指针只进不退。本质是利用了“子段的单调性”:如果当前窗口已经不满足条件,继续扩张右端不会让它更好;如果已经满足,收缩左端去寻找更优解。最长不重复子串、最短覆盖子串都靠这个。

位运算 ——把信息压缩到二进制的每一位上,一条指令并行处理 32 或 64 个 bool 值。n & (n-1) 消除最低位的 1,lowbit(n) = n & (-n) 取出最低位,XOR 的“相同为 0 不同为 1”能找出唯一出现一次的数。状态压缩 DP 用一个整数表示一个集合的选取状态,把集合枚举变成位操作。

扫描线 ——想象一条竖直线从左向右扫过平面,遇到“事件”(线段端点、矩形边界)时更新状态。它把二维问题降维成一维的事件序列处理。

摩尔投票 ——候选-抵消机制。用 O(1) 空间、O(n) 时间找到出现超过半数的元素。本质是利用了“多数元素数量 > 所有其他元素之和”这个信息。

字符串匹配(KMP / BM / Rabin-Karp) ——暴力匹配在失败时把主串指针回退,浪费了“已经匹配成功的前缀”这个信息。KMP 利用模式串自身的“部分匹配表”,失败时只回退模式串指针,主串指针永远不回头。BM 更激进,利用“坏字符”和“好后缀”两条规则一次跳过更多位置。

排序的利用 ——很多问题在排序后性质会变得明显。归并排序过程中可以顺带求逆序数(因为合并时左右两半的相对顺序就是逆序信息)。桶排序利用值域有限这个信息绕过比较模型。

概率与随机化 ——随机性打破最坏情况。随机快排的期望复杂度是 O(n log n),不存在能让它退化到 O(n²) 的“恶意输入”。HyperLogLog 用概率换空间,12KB 内存估算上亿基数。

数学公式 ——有些问题存在封闭解或递推公式,直接计算代替搜索。约瑟夫环 f(n,k) = (f(n-1,k) + k) % n 一行代码解决;第 N 位数字通过数位规律直接定位。


五、如何更轻松地拿下算法题

拿下算法题靠两条腿走路:想到更好的解法 + 肌肉记忆秒写代码。缺一不可。

5.1 想到更好的解法

面对一道新题,写出暴力解之后,问自己三个问题:

1. 我在重复计算什么?

如果暴力解中有大量重复的子计算,那答案往往是 DP 或预处理。斐波那契的递归树里 f(3) 被算了无数遍——加个备忘录就变成 O(n)。区间和被反复求——预计算前缀和就变成 O(1) 查询。

2. 我在遍历什么不可能的区域?

如果暴力解的内层循环中,很多迭代其实是“不可能产生更优解”的,那就找一个条件把它们跳过。有序性→二分;单调性→双指针/滑动窗口不回退;约束条件→回溯剪枝。

3. 有没有结构可以帮我预存信息?

如果每次迭代都需要查询某种信息(最大值?是否存在?前一个更大的?),考虑用一个数据结构提前维护好这个信息。需要 O(1) 判存→哈希表;需要动态最值→堆;需要最近更大→单调栈。

这三个问题不是万能的,但它们覆盖了面试中 90% 以上的优化思路。

从暴力解法到优化解法的决策流程:重复计算、不可能区域、数据结构选择

写出暴力解后,用这三个问题找优化方向

除了三个自问,还有两张“作弊表”帮你快速锁定方向:

Pattern 速查清单 ——遇到新题时,对照这张清单做匹配:

算法策略类

Pattern 识别信号
二分搜索 有序 / 单调判定函数 / 答案有范围
分治 子问题独立、合并代价可控
贪心 局部最优=全局最优(排序不等式/区间/Huffman)
动态规划 重叠子问题 + 最优子结构(背包/LIS/LCS/区间DP/状态机DP/树形DP)
回溯 求所有方案 / 组合排列 / 棋盘类
DFS / BFS 图/树遍历、连通性、最短路

数据结构类

Pattern 识别信号
哈希表 O(1) 判存 / 计数 / 去重
动态 Top-K / 第 K 大 / 数据流
单调栈 “下一个更大/更小元素”
单调队列 “滑动窗口内的最值”
前缀和 / 差分 频繁区间和查询 / 频繁区间修改
并查集 动态连通性 / 集合合并
Trie 前缀匹配 / 多模式串
二叉树 / BST 递归结构 / 有序查找 / 第K大
链表 原地操作 / 快慢指针 / 反转

编码技巧类

Pattern 识别信号
双指针 有序数组 / 对撞方向 / 奇偶分区
滑动窗口 连续子段 + 满足/不满足条件
位运算 集合状态 / 唯一出现 / 并行判断
扫描线 区间/矩形覆盖 / 事件排序
字符串匹配 模式串搜索(KMP/BM/Rabin-Karp/AC自动机)
排序 + 贪心/二分 排序后性质变明显 / 归并求逆序
数学公式 规律递推 / 封闭解(约瑟夫/数位/丑数)
概率随机化 打破最坏情况 / 近似计数(HyperLogLog)
摩尔投票 找出现超过半数/三分之一的元素

从 n 的范围倒推算法选择 ——这张表最初是 ACM/OI 竞赛选手的“作弊表”,比赛中看到数据范围就能秒判该用什么复杂度的算法。面试同样适用:面试官给你一个 10⁵ 量级的输入,如果你写了 O(n²),他一定会追问“能更快吗”。

一般在线评测系统 1 秒能执行约 10⁸ 次简单操作。根据 n 的范围,你能接受的复杂度上限是:

n 的范围 可接受的时间复杂度 通常对应的策略
n ≤ 20 O(2ⁿ) 或 O(n!) 回溯穷举、状态压缩 DP
n ≤ 100 O(n³) 区间 DP、Floyd 最短路
n ≤ 5,000 O(n²) 朴素 DP、双重循环
n ≤ 10⁵ O(n log n) 排序 + 二分、分治、堆
n ≤ 10⁶ O(n) 双指针、前缀和、单调栈/队列、线性 DP
n ≤ 10⁹ O(log n) 或 O(√n) 二分答案、数学公式

5.2 模板题的肌肉记忆

光“想到”还不够。面试是限时赛,你需要对每种 pattern 的模板题烂熟于心——看到题目 3 秒内识别类型,30 秒内写出骨架代码。

这就像练武:招式(pattern)要理解原理,但出招(写代码)必须靠肌肉记忆。

每种 pattern 都有 1~2 道“祖师爷题”——后续每篇文章都会标注哪道是模板题,建议反复写到闭眼能写。这里先给出方法论:

  1. 先理解,再背 ——先搞懂这个 pattern 利用了什么信息、为什么能加速,然后再去记模板代码。顺序反了就是死记硬背,遇到变体就不会了。
  2. 每个 pattern 只精练 1 道模板题 ——贪多嚼不烂。二分就练“旋转数组找最小”,滑动窗口就练“最长不重复子串”,回溯就练“全排列”。
  3. 间隔重复 ——今天写完,明天再写一遍,三天后再写一遍。三遍之后就是肌肉记忆了。
  4. 变体 = 模板 + 微调 ——面试中的题几乎都是模板题的变体。能识别出“这是哪道模板题的变形”,就等于解了 80%。

后续每篇文章会明确标注:本篇模板题(必须达到肌肉记忆级别)和 变体题(在模板基础上微调)。


六、下界、上界与普通人的定位

比较排序的 O(n log n) 是少数“理论下界 = 已知最优算法”的完美案例。但大多数问题没这么幸运——理论下界和已知最优算法之间往往存在一道 gap

矩阵乘法的理论下界是 Ω(n²)(你至少得读完所有输入),但目前最好的算法是 O(n^{2.37}),没人知道能不能做到 O(n²)。整数排序的下界是 Ω(n),而最好的确定性算法是 O(n√(log log n))——离下界只差一个几乎看不见的因子,但几十年来没人消除它。

缩小这道 gap 需要的是:发明全新的组合结构、改变计算模型、引入随机性或近似容忍。这是顶级计算机科学家做的事情。

这不是面试考你的东西。

面试考的是:给定一个已知有高效解法的问题,你能不能——

  1. 识别它的结构性质(有序?重叠子问题?局部最优=全局最优?)
  2. 匹配到对应的 pattern(二分?DP?贪心?)
  3. 正确地实现出来

换句话说,你不需要发明新算法,你需要的是记住有限的几种 pattern,识别问题属于哪种,然后套用

这正是本系列后续文章要做的事。


结尾

这篇文章没有一道具体的题目,但它是后面所有文章的“操作系统”。

记住三件事:

  1. 算法优化的本质是信息利用 ——每种策略利用不同类型的信息来缩小搜索空间。
  2. 数据结构是预处理的信息容器 ——选结构就是选“我最常做的查询是什么”。
  3. 面试不考你发明算法,考你识别 pattern ——记住有限的几种,匹配问题,正确实现。

从下一篇开始,我们逐个击破每种数据结构和算法策略。第一站:1.4.1 数组与矩阵 ——看似最简单的结构,却藏着原地操作、螺旋遍历、前缀和等大量面试高频考点。




上一篇:大模型数学与Infra优化:从RMSNorm到Softmax的深层拆解
下一篇:RAG 落地最难在哪里?阿里 Agent 岗三面深度答疑
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-11 10:47 , Processed in 0.631892 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表