Myers 差异算法¶
算法简介¶
Myers 差异算法是一种高效的序列比较算法,由 Eugene W. Myers 在1986年提出。该算法用于找出将一个序列转换为另一个序列所需的最小编辑操作(插入和删除)。它在版本控制系统(如Git)、文本比较工具和生物信息学中被广泛应用。
算法原理¶
Myers 算法基于以下关键概念:
- 编辑图(Edit Graph):将两个序列 A 和 B 表示为一个网格,其中水平移动表示删除操作,垂直移动表示插入操作,对角线移动表示匹配(无操作)。
- 最短编辑脚本(SES):从图的左上角到右下角的最短路径,代表最少的编辑操作。
- k线(k-line):表示编辑图中满足 x - y = k 的所有点的集合。
算法步骤¶
- 构建编辑图,其中 x 轴表示序列 A,y 轴表示序列 B
- 从原点 (0,0) 开始,计算到达终点 (N,M) 的最短路径
- 对于每个 d(编辑距离),计算可以到达的最远点
- 当路径到达终点时,回溯路径以构建差异结果
代码实现¶
def myers_diff(a, b):
"""
使用 Myers 差异算法计算两个序列之间的差异
参数:
a: 第一个序列
b: 第二个序列
返回:
编辑操作列表,其中:
- ('+', i, x) 表示在位置 i 插入元素 x
- ('-', i, x) 表示在位置 i 删除元素 x
"""
# 创建编辑图
n = len(a)
m = len(b)
max_edit = n + m
# 存储每个 k 线上能到达的最远 x 坐标
v = {1: 0}
# 存储路径
trace = []
# 计算最短编辑路径
for d in range(max_edit + 1):
# 保存当前 d 的路径信息
trace.append(v.copy())
for k in range(-d, d + 1, 2):
# 确定是向下还是向右移动
if k == -d or (k != d and v.get(k - 1, -1) < v.get(k + 1, -1)):
x = v.get(k + 1, -1) # 向下移动
else:
x = v.get(k - 1, -1) + 1 # 向右移动
# 计算 y 坐标
y = x - k
# 沿对角线移动(匹配)
while x < n and y < m and a[x] == b[y]:
x += 1
y += 1
# 更新 v
v[k] = x
# 如果到达终点,回溯并返回编辑脚本
if x >= n and y >= m:
return _backtrack(a, b, trace, n, m)
# 如果没有找到路径(不应该发生)
return []
def _backtrack(a, b, trace, x, y):
"""回溯并构建编辑脚本"""
result = []
d = len(trace) - 1
while d > 0:
v = trace[d]
k = x - y
# 确定前一个 k 值
if k == -d or (k != d and v.get(k - 1, -1) < v.get(k + 1, -1)):
prev_k = k + 1
else:
prev_k = k - 1
# 计算前一个位置
prev_x = v.get(prev_k, 0)
prev_y = prev_x - prev_k
# 处理对角线移动(匹配)
while x > prev_x and y > prev_y:
x -= 1
y -= 1
# 记录编辑操作
if x == prev_x:
# 插入操作
result.insert(0, ('+', x, b[prev_y]))
else:
# 删除操作
result.insert(0, ('-', prev_x, a[prev_x]))
# 更新位置
x = prev_x
y = prev_y
d -= 1
return result
# 测试
a = "ABCABBA"
b = "CBABAC"
diff = myers_diff(a, b)
print("差异结果:", diff)
import java.util.*;
public class MyersDiff {
public static void main(String[] args) {
String a = "ABCABBA";
String b = "CBABAC";
List<EditOperation> diff = myersDiff(a, b);
System.out.println("差异结果:");
for (EditOperation op : diff) {
System.out.println(op);
}
}
// 编辑操作类
static class EditOperation {
char type; // '+' 表示插入, '-' 表示删除
int position;
char element;
public EditOperation(char type, int position, char element) {
this.type = type;
this.position = position;
this.element = element;
}
@Override
public String toString() {
return "(" + type + ", " + position + ", " + element + ")";
}
}
public static List<EditOperation> myersDiff(String a, String b) {
char[] aChars = a.toCharArray();
char[] bChars = b.toCharArray();
int n = aChars.length;
int m = bChars.length;
int maxEdit = n + m;
// 存储每个 k 线上能到达的最远 x 坐标
Map<Integer, Integer> v = new HashMap<>();
v.put(1, 0);
// 存储路径
List<Map<Integer, Integer>> trace = new ArrayList<>();
// 计算最短编辑路径
for (int d = 0; d <= maxEdit; d++) {
// 保存当前 d 的路径信息
trace.add(new HashMap<>(v));
for (int k = -d; k <= d; k += 2) {
// 确定是向下还是向右移动
int x;
if (k == -d || (k != d && v.getOrDefault(k - 1, -1) < v.getOrDefault(k + 1, -1))) {
x = v.getOrDefault(k + 1, -1); // 向下移动
} else {
x = v.getOrDefault(k - 1, -1) + 1; // 向右移动
}
// 计算 y 坐标
int y = x - k;
// 沿对角线移动(匹配)
while (x < n && y < m && aChars[x] == bChars[y]) {
x++;
y++;
}
// 更新 v
v.put(k, x);
// 如果到达终点,回溯并返回编辑脚本
if (x >= n && y >= m) {
return backtrack(aChars, bChars, trace, n, m);
}
}
}
// 如果没有找到路径(不应该发生)
return new ArrayList<>();
}
private static List<EditOperation> backtrack(char[] a, char[] b, List<Map<Integer, Integer>> trace, int x, int y) {
List<EditOperation> result = new ArrayList<>();
int d = trace.size() - 1;
while (d > 0) {
Map<Integer, Integer> v = trace.get(d);
int k = x - y;
// 确定前一个 k 值
int prevK;
if (k == -d || (k != d && v.getOrDefault(k - 1, -1) < v.getOrDefault(k + 1, -1))) {
prevK = k + 1;
} else {
prevK = k - 1;
}
// 计算前一个位置
int prevX = v.getOrDefault(prevK, 0);
int prevY = prevX - prevK;
// 处理对角线移动(匹配)
while (x > prevX && y > prevY) {
x--;
y--;
}
// 记录编辑操作
if (x == prevX) {
// 插入操作
result.add(0, new EditOperation('+', x, b[prevY]));
} else {
// 删除操作
result.add(0, new EditOperation('-', prevX, a[prevX]));
}
// 更新位置
x = prevX;
y = prevY;
d--;
}
return result;
}
}
复杂度分析¶
- 时间复杂度:O(ND),其中 N 是两个序列的总长度,D 是编辑距离(最小编辑操作数)
- 空间复杂度:O(ND),用于存储路径信息
算法示例¶
让我们通过一个具体的例子来理解 Myers 差异算法的工作原理。假设我们有两个字符串: - A = "ABCABBA" - B = "CBABAC"
编辑图表示¶
我们可以将这两个字符串表示为一个编辑图,其中: - x 轴表示字符串 A - y 轴表示字符串 B - 对角线表示匹配(当 A[i] = B[j])
计算过程¶
- 从 (0,0) 开始,我们尝试找到到达 (7,6) 的最短路径
- 对于每个编辑距离 d,我们计算可以到达的最远点
- 当我们找到一条到达终点的路径时,回溯以构建差异结果
最终差异结果¶
对于我们的例子,Myers 算法会产生类似这样的差异结果: - 删除 A[0]:A - 保留 B[0]:C - 保留 A[1]:B - 保留 A[2]:C - 保留 A[3]:A - 保留 A[4]:B - 保留 A[5]:B - 保留 A[6]:A - 插入 B[5]:C
这表示将 "ABCABBA" 转换为 "CBABAC" 的最小编辑操作。
动画演示¶
应用场景¶
- 版本控制系统:Git 等版本控制系统使用类似 Myers 算法的差异算法来比较文件版本
- 文本比较工具:如 diff、Beyond Compare 等工具
- 协同编辑:实时协作编辑工具中的冲突解决
- 生物信息学:DNA 序列比对
练习题¶
-
基础实现:实现 Myers 差异算法,计算两个字符串之间的编辑操作。
-
可视化:创建一个简单的可视化工具,展示 Myers 算法的执行过程和编辑图。
-
优化:Myers 算法在处理大型序列时可能会消耗大量内存。尝试实现一个空间优化版本,只存储必要的信息。
-
应用:使用 Myers 差异算法实现一个简单的文本比较工具,能够高亮显示两个文本文件之间的差异。
-
扩展:修改 Myers 算法,使其能够处理替换操作(将一个元素替换为另一个元素),而不仅仅是插入和删除操作。
-
挑战:实现 Myers 算法的线性空间变体,该变体使用 O(N) 空间而不是 O(ND) 空间,其中 N 是序列的总长度,D 是编辑距离。
参考资料¶
- Myers, E. W. (1986). "An O(ND) Difference Algorithm and Its Variations". Algorithmica. 1 (1): 251–266.
- Miller, W.; Myers, E. W. (1985). "A File Comparison Program". Software: Practice and Experience. 15 (11): 1025–1040.
- Hunt, J. W.; McIlroy, M. D. (1976). "An Algorithm for Differential File Comparison". Computing Science Technical Report, Bell Laboratories 41.