赞
踩
举一个最常见的例子,我们使用git进行提交时,通常会使用git diff --cached
来查看这次提交做了哪些改动,这里我们先简单定义一下什么是diff:diff就是目标文本和源文本之间的区别,也就是将源文本变成目标文本所需要的操作。Myers算法由Eugene W.Myers在1986年发表的一篇论文中提出,是一个能在大部分情况产生”最短的直观的“diff的一个算法。
”寻找最短的直观的diff”是一个非常模糊的问题,首先,我们需要把这个问题抽象为一个具体的数学问题,然后再来寻找算法解决。抽象的过程交给算法科学家了,抽象的结果是:寻找diff的过程可以被表示为图搜索
什么意思呢?还是以两个字符串,src=ABCABBA,dst=CBABAC为例,根据这两个字符串我们可以构造下面一张图,横轴是src内容,纵轴是dst内容,那么图中每一条从左上角到右下角的路径,都表示一个diff。向右表示删除,向下表示新增,对角线则表示原内容保持不动
根据图中形成的线路,我们可以选择一条路径看看它的效果
现在,“寻找diff”这件事,被抽象成了“寻找图的路径”了。那么,“最短的直观的”diff对应的路径有什么特点呢?
路径长度最短(对角线不算长度)
先向右,再向下(先删除,后新增)
根据 Myers 的论文,他提出了三个概念:
snake: 一条snake代表走一步。例如从(0,0)->(0,1) / (0,0)->(1,0) / (0,1)->(0,2)->(2,4) 这分别为三条snake,走对角线不计入步数。
k line: 定义 k = x - y (我们可以写成 y = x - k,是相同斜率的平行斜线组成的一个集合)
d contour: 走一步算一个d
根据路径图与提出的三个概念,我们可以画出如下一张图,横坐标代表d(步数),纵坐标代表k(偏移),图中每一点代表最优坐标
现在我们可以知道,其实Myers算法是一个典型的动态规划算法,也就是说,父问题的求解归结为子问题的求解。要知道d=5时所有k对应的最优坐标,必须先要知道d=4时所有k对应的最优坐标,要知道d=4时的答案,必须先求解d=3,以此类推
根据上图,我们也能直观的看到到达(7,6)的最短路径,见下图
外层循环步数d,内层循环偏移k,两个for循环找出最短diff路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public abstract class PathNode { public final int i; public final int j; public final PathNode prev; public PathNode(int i, int j, PathNode prev) { this.i = i; this.j = j; this.prev = prev; } public abstract Boolean isSnake(); @Override public String toString() { StringBuffer buf = new StringBuffer("["); PathNode node = this; while (node != null) { buf.append("("); buf.append(Integer.toString(node.i)); buf.append(","); buf.append(Integer.toString(node.j)); buf.append(")"); node = node.prev; } buf.append("]"); return buf.toString(); } } public final class Snake extends PathNode { public Snake(int i, int j, PathNode prev) { super(i, j, prev); } @Override public Boolean isSnake() { return true; } } public final class DiffNode extends PathNode { public DiffNode(int i, int j, PathNode prev) { super(i, j, prev); } @Override public Boolean isSnake() { return false; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | public class MyersDiff<T> { /** * 默认相等规则 */ private final Equalizer<T> DEFAULT_EQUALIZER = (original, revised) -> original.equals(revised); private final Equalizer<T> equalizer; public MyersDiff() { equalizer = DEFAULT_EQUALIZER; } public MyersDiff(Equalizer<T> equalizer) { this.equalizer = equalizer; } /** * 寻找最优路径 */ public PathNode buildPath(List<T> orig, List<T> rev) throws Exception { if (orig == null) throw new IllegalArgumentException("original sequence is null"); if (rev == null) throw new IllegalArgumentException("revised sequence is null"); final int N = orig.size(); final int M = rev.size(); //最大步数(先全减后全加) final int MAX = N + M + 1; final int size = 1 + 2 * MAX; final int middle = size / 2; //构建纵坐标数组(用于存储每一步的最优路径位置) final PathNode diagonal[] = new PathNode[size]; //用于获取初试位置的辅助节点 diagonal[middle + 1] = new Snake(0, -1, null); //外层循环步数 for (int d = 0; d < MAX; d++) { //内层循环所处偏移量,以2为步长,因为从所在位置走一步,偏移量只会相差2 for (int k = -d; k <= d; k += 2) { //找出对应偏移量所在的位置,以及它上一步的位置(高位与低位) final int kmiddle = middle + k; final int kplus = kmiddle + 1; final int kminus = kmiddle - 1; //若k为-d,则一定是从上往下走,即i相同 //若diagonal[kminus].i < diagonal[kplus].i,则最优路径一定是从上往下走,即i相同 int i; PathNode prev; if ((k == -d) || (k != d && diagonal[kminus].i < diagonal[kplus].i)) { i = diagonal[kplus].i; prev = diagonal[kplus]; } else { //若k为d,则一定是从左往右走,即i+1 //若diagonal[kminus].i = diagonal[kplus].i,则最优路径一定是从左往右走,即i+1 i = diagonal[kminus].i + 1; prev = diagonal[kminus]; } //根据i与k,计算出j int j = i - k; //上一步的低位数据不再存储在数组中(每个k只清空低位即可全部清空) diagonal[kminus] = null; //当前是diff节点 PathNode node = new DiffNode(i, j, prev); //判断被比较的两个数组中,当前位置的数据是否相同,相同,则去到对角线位置 while (i < N && j < M && equals(orig.get(i), rev.get(j))) { i++; j++; } //判断是否去到对角线位置,若是,则生成snack节点,前节点为diff节点 if (i > node.i) node = new Snake(i, j, node); //设置当前位置到数组中 diagonal[kmiddle] = node; //达到目标位置,返回当前node if (i >= N && j >= M) { return diagonal[kmiddle]; } } } throw new Exception("could not find a diff path"); } private boolean equals(T orig, T rev) { return equalizer.equals(orig, rev); } /** * 打印diff */ public void buildDiff(PathNode path, List<T> orig, List<T> rev) { List<String> result = new ArrayList<>(); if (path == null) throw new IllegalArgumentException("path is null"); if (orig == null) throw new IllegalArgumentException("original sequence is null"); if (rev == null) throw new IllegalArgumentException("revised sequence is null"); while (path != null && path.prev != null && path.prev.j >= 0) { if (path.isSnake()) { int endi = path.i; int begini = path.prev.i; for (int i = endi - 1; i >= begini; i--) { result.add(" " + orig.get(i)); } } else { int i = path.i; int j = path.j; int prei = path.prev.i; if (prei < i) { result.add("- " + orig.get(i - 1)); } else { result.add("+ " + rev.get(j - 1)); } } path = path.prev; } Collections.reverse(result); for (String line : result) { System.out.println(line); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public static void main(String[] args) { String oldText = "A\nB\nC\nA\nB\nB\nA"; String newText = "C\nB\nA\nB\nA\nC"; List<String> oldList = Arrays.asList(oldText.split("\\n")); List<String> newList = Arrays.asList(newText.split("\\n")); MyersDiff<String> myersDiff = new MyersDiff<>(); try { PathNode pathNode = myersDiff.buildPath(oldList, newList); System.out.println(pathNode); myersDiff.buildDiff(pathNode, oldList, newList); } catch (Exception e) { e.printStackTrace(); } } |
1 2 3 4 5 6 7 8 9 10 | [(7,6)(7,5)(6,4)(5,4)(3,2)(3,1)(2,0)(1,0)(0,0)(0,-1)] - A - B C + B A B - B A + C |
注意
在git中不只是有增加和删除,还有修改,修改的原理是同时出现增加和删除,即为修改
传统diff使用的是最naive的LSC计算算法(LCS:最长公共子序列),时间复杂度是O(M*N)
(M,N分别为src与target的序列长度)
Myers的diff算法更好,时间复杂度为O((M+N)*D)
(D为src与target的距离长度)尤其是在src与target相近时时间复杂度接近O(M+N)
差分算法是典型的动态规划算法,动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决
1 | 动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解 |
我们有个题目:有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。
根据题目我们可以确定F(10)=F(8)+F(9),即:从0到10级台阶的走法数量=从0到8级台阶的走法数量+从0到9级台阶的走法数量
,则可推出如下公式:
1 2 3 | F(1) = 1 F(2) = 2 F(n) = F(n-1)+F(n-2)(n>=3) |
最优子结构:F(8)和F(9)是F(10)的最优子结构
边界:F(1)和F(2)是问题的【边界】,如果一个问题没有边界将永远无法得到有限的结果
状态转移方程:F(n)=F(n-1)+F(n-2)是阶段与阶段之间的【状态转移方程】,这是动态规划的核心,决定了问题的每一个阶段和下一阶段的关系
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。