跳转至

Myers 差异算法

算法简介

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

算法原理

Myers 算法基于以下关键概念:

  1. 编辑图(Edit Graph):将两个序列 A 和 B 表示为一个网格,其中水平移动表示删除操作,垂直移动表示插入操作,对角线移动表示匹配(无操作)。
  2. 最短编辑脚本(SES):从图的左上角到右下角的最短路径,代表最少的编辑操作。
  3. k线(k-line):表示编辑图中满足 x - y = k 的所有点的集合。

算法步骤

  1. 构建编辑图,其中 x 轴表示序列 A,y 轴表示序列 B
  2. 从原点 (0,0) 开始,计算到达终点 (N,M) 的最短路径
  3. 对于每个 d(编辑距离),计算可以到达的最远点
  4. 当路径到达终点时,回溯路径以构建差异结果

代码实现

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])

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

计算过程

  1. 从 (0,0) 开始,我们尝试找到到达 (7,6) 的最短路径
  2. 对于每个编辑距离 d,我们计算可以到达的最远点
  3. 当我们找到一条到达终点的路径时,回溯以构建差异结果

最终差异结果

对于我们的例子,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" 的最小编辑操作。

动画演示

应用场景

  1. 版本控制系统:Git 等版本控制系统使用类似 Myers 算法的差异算法来比较文件版本
  2. 文本比较工具:如 diff、Beyond Compare 等工具
  3. 协同编辑:实时协作编辑工具中的冲突解决
  4. 生物信息学:DNA 序列比对

练习题

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

  2. 可视化:创建一个简单的可视化工具,展示 Myers 算法的执行过程和编辑图。

  3. 优化:Myers 算法在处理大型序列时可能会消耗大量内存。尝试实现一个空间优化版本,只存储必要的信息。

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

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

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

参考资料

  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.


📅 Created 0 days ago ✏️ Updated 0 days ago