技术解析2026年6月25日

文本对比工具是怎么算出差异的?聊聊 diff 算法

diff 不是逐字符比对,而是求最短编辑路径。本文讲清 LCS 与 Myers 算法的核心思路、行级与字符级 diff 的取舍、行内高亮的实现,以及大文本下的性能边界。

文本对比看起来只是"把不一样的地方标红标绿",但真正难的是:插入一行后,后面所有行都往下挪了,怎么知道它们没变、只是位置偏了? diff 算法的核心不是逐字符比,而是求一条"从 A 改到 B 代价最小的编辑路径"。

在编辑图网格上寻找两段文本之间的最短编辑路径

diff 到底在求解什么问题?

diff 的本质是求从文本 A 变成文本 B 的最短编辑序列:用最少的"插入"和"删除"操作把 A 改成 B。为什么不逐字符比?因为逐位比对遇到"在第 3 行插入一行"会彻底错乱——从插入点往后每一行都和原来对不齐,朴素比对会把它们全判成"改了",而人眼一看就知道只是整体下移、内容没变。

把它建模成"最少编辑操作"后,算法就能识别出"这些行其实没变,只是被插入的新行顶下去了"。这与最长公共子序列(LCS) 是同一枚硬币的两面:A 和 B 的 LCS 越长,需要增删的部分就越少,两者一一对应。

最长公共子序列(LCS)是怎么定位"没变的部分"的?

LCS 指两段文本中按原顺序都出现、但不要求连续的最长子序列。它代表 A 和 B 共有、未被改动的内容;剩下的部分,A 里独有的就是"删除",B 里独有的就是"新增"。经典解法是动态规划:

  • 用一个二维表 dp[i][j] 表示 A 前 i 个单位与 B 前 j 个单位的 LCS 长度;
  • 当前两个单位相同则 dp[i][j] = dp[i-1][j-1] + 1,否则取上方与左方的较大值;
  • 回溯这张表即可还原出哪些是公共的、哪些是增删。

它直观、好实现,但时间和空间都是 O(N×M),N、M 是两段文本长度。文本一大,二维表就吃内存,这是它的硬伤。

Myers 算法为什么更快、更常用?

Myers 算法把 diff 看成"在编辑图里找最短路径",在差异较小时远快于朴素 DP,这也是 Git 默认用它的原因。它的关键洞察是:实际场景里两个版本往往只差几处,差异数 D 很小。Myers 的复杂度约 O(ND)(N 为总长度,D 为编辑距离),当 D 远小于 N 时几乎线性。

维度 LCS 动态规划 Myers (O(ND))
思路 填二维表求最长公共子序列 编辑图上找最短编辑路径
时间复杂度 O(N×M) 约 O(ND),D 为差异数
空间 O(N×M)(可优化到 O(N)) 线性空间变体 O(N)
差异很小时 仍要填满整张表 极快(接近线性)
差异很大时 稳定但慢 退化到接近 O(N²)
典型采用 教学、短文本 Git、多数 diff 工具

结论很实际:差异小用 Myers 占尽便宜,差异极大时两者都难免变慢

行级 diff 和字符级 diff 该怎么选?

diff 的"比对单位"可大可小,单位选错会让结果难读。常见三档:

  • 行级:以整行为单位比对,最常用。代码、配置、日志天然按行组织,结果干净;但只能告诉你"这一行变了",不告诉你行里改了哪个字符。
  • 词级:以单词/分词为单位,适合自然语言文本,能标出改了哪个词。
  • 字符级:以单个字符为单位,最细,但对长文本会产生大量琐碎的增删片段,反而看不清。

工程上的常见做法是分层:先按行 diff 抓住宏观结构,再对"看起来是修改"的行对做更细粒度的二次 diff。

"行内高亮"是怎么实现的?

行内高亮(标出某一行里具体改了哪几个字符)通常是两级 diff 的结果。第一级做行级 diff,会得到一串"删除某行 + 新增某行"的操作;当一个删除行和一个新增行在位置上配对、且内容相似度足够高时,就把它们判定为"这一行被修改了"。第二级再对这一对行单独跑一次字符级或词级 diff,把行内真正变化的片段精确标出。

所以行内高亮并不是另一套魔法,而是在更细的粒度上重复同样的算法——这也解释了为什么它比纯行级 diff 更耗算力:每一对修改行都要再算一次。

能力边界与已知限制

diff 算法不是万能的,几个常见边界值得知道:

  • 差异极大时退化:当两段文本几乎完全不同(D 接近 N),Myers 会退化到接近 O(N²),超大文本可能明显卡顿;
  • "语义等价"识别不了:算法只看字符序列,识别不了"变量重命名后逻辑没变"或"代码块整体搬家"这类语义层面的等价,搬家会被记成一删一增;
  • 行尾与空白干扰:CRLF/LF 换行差异、行尾空格、缩进 tab/空格混用,都会被算成差异,对比前常需归一化;
  • 比对单位影响可读性:字符级 diff 在长段落上会碎成大量小片段,可读性反而不如词级或行级。

判断一个 diff 结果好不好用,关键看比对单位是否匹配文本类型、以及是否对换行/空白做了合理归一化,而不只是算法本身快不快。

小结

文本对比的核心不是"逐字比对",而是求最短编辑路径:LCS 找出未变的公共部分,Myers 用编辑图最短路径在差异小时做到接近线性。行级、词级、字符级是不同的比对粒度,行内高亮则是"先行级、再对修改行做字符级"的两级 diff。理解了"最短编辑序列 + 比对粒度 + 归一化"这三件事,就能判断一类 diff 实现适合什么样的文本与规模。

本文用到的工具

常见问题

不是。简单的逐字符比对无法识别'插入一行导致后续整体错位'的情况。主流 diff 把问题建模为'从文本 A 变到 B 的最短编辑序列',通过求最长公共子序列(LCS)或等价的最短编辑脚本来定位真正的增删,而非机械地逐位比较。