跳转至

Myers 差异算法

算法简介

Myers 差异算法是一种高效的序列比较算法,由 Eugene W. Myers 在1986年提出。该算法用于找出将一个序列转换为另一个序列所需的最小编辑操作(插入和删除)。它在版本控制系统(如Git)、文本比较工具和生物信息学中被广泛应用。

算法原理

Myers 算法基于编辑图(Edit Graph)的概念,通过寻找从图的左上角到右下角的最短路径来确定最小编辑操作序列。让我们详细解释其核心概念:

编辑图(Edit Graph)

给定两个序列 A[1...N] 和 B[1...M],编辑图是一个 (N+1) × (M+1) 的网格:

  • 每个点 (x,y) 表示将 A 的前 x 个字符与 B 的前 y 个字符进行比较的状态
  • 从 (0,0) 开始(空序列比较)
  • 目标是到达 (N,M)(完整序列比较)

在这个图中: - 水平移动(从 (x,y) 到 (x+1,y))表示删除 A[x+1] - 垂直移动(从 (x,y) 到 (x,y+1))表示插入 B[y+1] - 对角线移动(从 (x,y) 到 (x+1,y+1))表示 A[x+1] 和 B[y+1] 匹配(或替换,但 Myers 算法只考虑匹配)

编辑距离(d)与 k 线

Myers 算法引入了两个关键概念:

  1. 编辑距离 d:从起点到某点所需的最小编辑操作数(插入和删除的总数)
  2. d = 0 表示不需要编辑操作(只有对角线移动)
  3. d = 1 表示需要一次编辑操作(一次插入或一次删除)
  4. 依此类推

  5. k 线:满足 x - y = k 的所有点的集合

  6. k = 0 表示主对角线(x = y)
  7. k > 0 表示在主对角线上方的对角线(x > y)
  8. k < 0 表示在主对角线下方的对角线(x < y)

k 和 d 之间的关系

k 和 d 之间存在重要的数学关系:

  1. k 的范围:对于给定的编辑距离 d,k 的可能值范围是 -d ≤ k ≤ d
  2. 这意味着编辑距离为 d 时,我们只需考虑 2d+1 条 k 线

  3. k 的奇偶性:对于给定的 d,k 的奇偶性与 d 相同

  4. 如果 d 是偶数,k 也必须是偶数
  5. 如果 d 是奇数,k 也必须是奇数

  6. 递进关系

  7. 当我们从编辑距离 d-1 移动到 d 时,我们只需要计算新的 k 值
  8. 每个新的 k 值都基于编辑距离 d-1 时的 k-1 或 k+1 的结果

算法核心思想

Myers 算法的核心思想是:

  1. 对于每个编辑距离 d(从 0 开始递增)
  2. 计算每条 k 线上能到达的最远点(最大的 x 值)
  3. 如果在编辑距离 d 时能到达终点 (N,M),则找到了最短编辑路径

这种方法保证了找到的路径具有最小的编辑操作数。

算法步骤详解

  1. 初始化
  2. 设置起点 (0,0)
  3. 初始化 V 数组,用于存储每条 k 线上能到达的最远 x 坐标

  4. 迭代搜索

  5. 对于每个编辑距离 d(从 0 递增)
  6. 对于每个 k(从 -d 到 d,步长为 2)

    • 确定是向下移动(垂直,插入)还是向右移动(水平,删除)
    • 计算在 k 线上能到达的最远点 (x,y)
    • 尝试沿对角线前进(匹配)
    • 检查是否到达终点 (N,M)
  7. 回溯路径

  8. 从终点回溯到起点
  9. 记录每一步的编辑操作(插入或删除)

图解示例

让我们通过一个具体的例子来理解 Myers 差异算法。假设我们有两个字符串: - A = "ABCABBA" - B = "CBABAC"

编辑图表示

我们可以将这两个字符串表示为一个编辑图:

    |   | C | B | A | B | A | C
----+---+---+---+---+---+---+---
    | 0 | 1 | 2 | 3 | 4 | 5 | 6
----+---+---+---+---+---+---+---
  A | 1 |   |   | \ |   | \ |  
----+---+---+---+---+---+---+---
  B | 2 |   | \ |   | \ |   |  
----+---+---+---+---+---+---+---
  C | 3 | \ |   |   |   |   | \
----+---+---+---+---+---+---+---
  A | 4 |   |   | \ |   | \ |  
----+---+---+---+---+---+---+---
  B | 5 |   | \ |   | \ |   |  
----+---+---+---+---+---+---+---
  B | 6 |   | \ |   | \ |   |  
----+---+---+---+---+---+---+---
  A | 7 |   |   | \ |   | \ |  

其中 "\" 表示对角线匹配(A[i] = B[j])。

k 线示意

在这个编辑图中,不同的 k 线如下: - k = 0: 主对角线,x = y - k = 1: x - y = 1 的对角线 - k = -1: x - y = -1 的对角线 - 依此类推

编辑距离 d 的迭代

  1. d = 0:只考虑 k = 0
  2. 从 (0,0) 开始,沿对角线移动
  3. 无法到达终点

  4. d = 1:考虑 k = -1 和 k = 1

  5. k = -1:先向下移动到 (0,1),然后尝试对角线
  6. k = 1:先向右移动到 (1,0),然后尝试对角线
  7. 仍无法到达终点

  8. d = 2, 3, ...

  9. 继续迭代,直到找到到达终点的路径

最终路径

最终,算法会找到一条从 (0,0) 到 (7,6) 的最短路径,对应的编辑操作序列就是最小编辑脚本。

代码实现

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;
    }
}

复杂度分析

Myers 差异算法的复杂度与编辑距离和序列长度相关:

  • 时间复杂度:O(ND)
  • N 是两个序列的总长度(N = len(A) + len(B))
  • D 是编辑距离(最小编辑操作数)
  • 在最坏情况下(两个完全不同的序列),D 可能接近 N,导致时间复杂度接近 O(N²)
  • 但在实际应用中,通常 D << N,使得算法非常高效

  • 空间复杂度:O(ND)

  • 主要用于存储每个编辑距离 d 的路径信息
  • 可以通过只保存必要的信息来优化空间使用

详细算法示例

让我们通过一个详细的例子来理解 Myers 差异算法的执行过程。假设我们有两个字符串: - A = "ABCABBA" - B = "CBABAC"

初始状态

  • 起点:(0,0)
  • 终点:(7,6)
  • 初始化 V 数组:V[1] = 0

编辑距离 d = 0

  • 只考虑 k = 0(主对角线)
  • 从 (0,0) 开始,无法沿对角线移动(A[0] ≠ B[0])
  • V[0] = 0

编辑距离 d = 1

  • 考虑 k = -1 和 k = 1
  • k = -1:
  • 向下移动到 (0,1)
  • 无法沿对角线移动(A[0] ≠ B[1])
  • V[-1] = 0
  • k = 1:
  • 向右移动到 (1,0)
  • 无法沿对角线移动(A[1] ≠ B[0])
  • V[1] = 1

编辑距离 d = 2

  • 考虑 k = -2, k = 0, k = 2
  • k = -2:
  • 向下移动到 (0,2)
  • 无法沿对角线移动
  • V[-2] = 0
  • k = 0:
  • 选择从 k = -1 向右移动(因为 V[-1] = 0 < V[1] = 1)
  • 到达 (1,1)
  • 无法沿对角线移动(A[1] ≠ B[1])
  • V[0] = 1
  • k = 2:
  • 向右移动到 (2,0)
  • 沿对角线移动到 (3,1)(因为 A[2] = B[0] = 'C')
  • V[2] = 3

继续迭代

  • 继续计算更高的编辑距离,直到找到到达终点的路径
  • 每次迭代都会更新 V 数组,记录每条 k 线上能到达的最远点

最终路径

最终,算法会找到一条从 (0,0) 到 (7,6) 的最短路径。通过回溯,我们可以构建出编辑操作序列:

  1. 删除 A[0]:'A'
  2. 匹配 C
  3. 匹配 B
  4. 匹配 A
  5. 匹配 B
  6. 匹配 B
  7. 匹配 A
  8. 插入 C

这表示将 "ABCABBA" 转换为 "CBABAC" 的最小编辑操作序列。

动画演示

应用场景

Myers 差异算法在多个领域有广泛应用:

  1. 版本控制系统
  2. Git 等版本控制系统使用类似 Myers 算法的差异算法来比较文件版本
  3. 帮助识别代码变更并生成补丁

  4. 文本比较工具

  5. 如 diff、Beyond Compare 等工具
  6. 高亮显示文本文件之间的差异

  7. 协同编辑

  8. 实时协作编辑工具中的冲突解决
  9. 合并来自多个用户的更改

  10. 生物信息学

  11. DNA 序列比对
  12. 识别基因突变和变异

  13. 拼写检查和自动更正

  14. 计算用户输入与字典单词之间的编辑距离
  15. 提供拼写建议

算法优化

Myers 算法有几种常见的优化方式:

  1. 空间优化
  2. 只存储当前和前一个编辑距离的 V 数组,而不是所有历史记录
  3. 将空间复杂度从 O(ND) 降低到 O(N)

  4. 线性空间变体

  5. Myers 提出了一种线性空间变体,使用分治策略
  6. 空间复杂度为 O(N),但时间复杂度仍为 O(ND)

  7. 启发式优化

  8. 使用启发式方法预估编辑距离的上下界
  9. 减少需要探索的 k 线数量

练习题

  1. 基础实现:实现 Myers 差异算法,计算两个字符串之间的编辑操作。

  2. 可视化:创建一个简单的可视化工具,展示 Myers 算法的执行过程和编辑图,特别关注 k 线和编辑距离 d 的关系。

  3. 空间优化:实现 Myers 算法的空间优化版本,只存储必要的信息,将空间复杂度从 O(ND) 降低到 O(N)。

  4. 应用:使用 Myers 差异算法实现一个简单的文本比较工具,能够高亮显示两个文本文件之间的差异。

  5. 扩展:修改 Myers 算法,使其能够处理替换操作(将一个元素替换为另一个元素),而不仅仅是插入和删除操作。

  6. 挑战:实现 Myers 算法的线性空间变体,该变体使用 O(N) 空间而不是 O(ND) 空间,其中 N 是序列的总长度,D 是编辑距离。

  7. 高级:实现一个带有启发式优化的 Myers 算法,使用 A* 搜索策略来减少需要探索的状态数量。

参考资料

  1. Myers, E. W. (1986). "An O(ND) Difference Algorithm and Its Variations". Algorithmica. 1 (1): 251–266. [原始论文,详细介绍了算法的理论基础和变体]

  2. Miller, W.; Myers, E. W. (1985). "A File Comparison Program". Software: Practice and Experience. 15 (11): 1025–1040. [介绍了算法的实际应用]

  3. Hunt, J. W.; McIlroy, M. D. (1976). "An Algorithm for Differential File Comparison". Computing Science Technical Report, Bell Laboratories 41. [早期的差异算法,为 Myers 算法奠定了基础]

  4. Ukkonen, E. (1985). "Algorithms for approximate string matching". Information and Control. 64 (1–3): 100–118. [介绍了与 Myers 算法相关的近似字符串匹配算法]

  5. Git 源代码 - xdiff/xdiffi.c: Git 中实现的差异算法,基于 Myers 算法的变体



📅 Created 0 days ago ✏️ Updated 0 days ago