文本对比的核心问题可以这样描述:给定两个序列 A 和 B,找出把 A 变成 B 所需的最少增删操作。这个"最少"是判断哪种对比结果更"好看"的依据。
最长公共子序列
所有 diff 算法几乎都建立在一个概念上:最长公共子序列(Longest Common Subsequence,LCS)。
找出 A 和 B 共有的、顺序一致但不要求连续的那部分,剩下的就是需要改动的地方。举个小例子:
A: A B C D E
B: A C D F E
公共子序列是 A C D E(长度 4)。那么 B 里多出的 F 就是新增,A 里多出的 B 就是删除。
直接求 LCS 用动态规划,时间复杂度 O(n×m)。对两段各一万行的代码来说就是上亿次操作,浏览器里会明显卡顿。
所以真正的实现都做了优化
实用的 diff 算法会在几个方向上省力:
先剥掉公共前后缀
最简单的优化往往最有效。如果两段文本开头有 5000 行完全相同,结尾又有 3000 行相同,那就直接跳过,只对中间真正不同的部分跑算法。日常场景里这一招能砍掉绝大部分工作量。
按"编辑图"找最短路径
把对比过程想象成一张网格:横向走代表删除,纵向走代表插入,斜着走代表内容相同。目标是从左上角走到右下角,且尽量多走对角线。
Myers 在 1986 年提出的算法就建立在这个图上,它利用"到达某个编辑距离所需的步数是有限的"这一点剪枝,把平均复杂度压到接近 O(nd),其中 d 是实际差异的数量。差异越少,跑得越快 —— 这正好符合实际使用场景。
两个层次的差异
好的对比工具会分两层处理:
- 行级:先确定哪些行被增删了。这决定了整体的对齐关系。
- 行内:对改动的行再跑一次对比,这次以词或字符为单位,把真正变化的部分高亮出来。
少了第二层,整行都会被标成"删除 + 新增",改一个标点符号看起来也像重写了一句。这也是为什么行内对比的粒度通常选在词级别 —— 按字符切太碎,按行切又太粗。
为什么有时结果看着"不对"
回到开头那个问题。diff 算法给出的是最少编辑数的答案,但"最少编辑"和"人认为最合理"并不总是一回事。
举个典型例子:把一段代码整个缩进增加了两个空格。算法视角下,每一行都变了,最省的表达方式就是"全部删除再全部新增"。而人希望看到的是"整体缩进变化"。这也是为什么成熟的对比工具都提供"忽略首尾空白"选项 —— 它把这类噪声从算法输入里剔除掉,而不是让算法去猜意图。
同样的道理,重排过的代码块也是难以处理的情形。算法只认顺序,它无法判断你是"移动了一段"还是"删掉了旧的又加了新的" —— 结果就是把移动显示成一删一增。
想自己感受一下,可以到 文本对比 里试试改缩进、换行、改标点这几种情况,再勾上忽略选项对比差异。