本文参考了文章: 简析Myers
在参考文章中我对Myers差分算法有了一定的理解,但是在代码实现的过程中还是会遇到很多问题,而作者对源码的解释比较少,因此我着重于对实现过程做一个分析。
概述
我把代码的实现分为了三个部分:
绘制编辑图
生成snake
回溯snake
绘制编辑图
下图是原论文里面的编辑图,我们需要吧圈起来的点对应的保存下来,保存的方式类似下面的表格
在这张表格里面表示了k线和步数d之间的关系
具体的代码实现可以参考论文里面的伪代码
private static void myers(String str1, String str2) {
char[] arr1 = str1.toCharArray();
char[] arr2 = str2.toCharArray();
int m = arr1.length;
int n = arr2.length;
//我们需要得到一张图
//横坐标标识步数,纵坐标代表k的取值,横纵坐标对应的值代表在步数d下能够到达的点
//1、定义一个map,存储我们图里面横向的数据,key为k的值,而value为x坐标,其实也就是步数d
//2、这个kposition里面的每一个k的取值有上一个k-1或者k+1得来
Map<Integer,Integer> kPositions = new HashMap<>();
//2、定义一个集合存储每一步的kPositions
List<Map<Integer,Integer>> dkPositions = new ArrayList<>();
// 最大的一个可以走的步数
int maxd = n + m;
//一步一步的走
kPositions.put(1,0);
dloop: for(int d=0;d<=maxd;d++){
Map<Integer,Integer> kPositionsTemp = new HashMap<>();
//每走的一步都是会落在k线上面,而k的取值范围是 -d,d
boolean breakFlag = false;
for(int k = -d;k<=d;k+=2){
int x;
//k==-d 意味着往下走,也就是全部新增,那么x坐标是不变的,也就是上一个的x坐标
if(k==-d){
x = kPositions.get(k+1);
//k==d 意味着 往右边走,也就是代表这一步删除了一个字符,那么x坐标是前一个的x+1
}else if(k==d){
x= kPositions.get(k-1)+1;
//上面这是两个边界情况,
//下面的情况,当前步的前一步取值有两种,k-1和k+1,我们遵循先删除后添加的规则
}else{
//k = x-y , x = k+y , y = x- k
//我们在两个可选项里面判断
//怎么求的x?
//我们需要判断我们当前位置和上一个位置之间的走向
//怎么判断?
//用k来判断,从k-1到k 意味着向右边走,那么x需要+1,k+1 到k 意味着向下走,那么x不变,那我们到底是要从k-1过来呢还是从k+1过来呢??
//我们需要选取一个走得最远的点,我认为我是从远点过来的,判断远点的条件就是谁的x大,谁就走得远。
// 如果kPositions.get(k-1) 大,那么意味着是从k-1走到k,也就是意味着x需要+1,反之意味着从kPositions.get(k+1) 走到k,意味着 x不变
x= kPositions.get(k-1)<kPositions.get(k+1)?kPositions.get(k+1):kPositions.get(k-1)+1;
}
//现在我们已经走了一步了,可以计算出这一步的y坐标
int y = x-k;
//这时候我们还需要判断是否会有斜线,也就是相等的情况,如果相等意味着走斜线,不计入步数,可以继续走,直到不相等
while( y<n&&x<m && arr1[x]==arr2[y]){
x=x+1;
y=y+1;
}
kPositions.put(k,x);
kPositionsTemp.put(k,x);
//如果出现了 x,y大于等于 m,n的情况意味着已经找到了终点,没必要再走了
if (x>=m&&y>=n){
dkPositions.add(kPositionsTemp);
breakFlag = true;
break dloop;
}
}
dkPositions.add(kPositionsTemp);
if (breakFlag) break ;
}
//到这里就已经绘制完了整个编辑图,然后要利用这个编辑图生成snakes
//生成snakes
Stack<Snake> snakes = generateSnake(dkPositions, arr1, arr2);
//回溯snake
printSnake(snakes,arr1,arr2);
}
复制代码
生成snakes
在上面绘制完编辑图之后需要生成snakes,我这里对snake的定义是一个包含了当前步所在的点的x,y坐标以及下一步的走向的对象
@Data
@ToString
@AllArgsConstructor
static class Snake{
private int x;
private int y;
private Boolean right;
}
复制代码
有了这个对象的定义之后要生成所有的snake
private static Stack<Snake> generateSnake(List<Map<Integer, Integer>> dkPositions, char[] arr1, char[] arr2) {
Stack<Snake> snakes = new Stack<>();
//定义起始位置为终点,因为我们要从终点开始回溯
int currentX = arr1.length;
int currentY = arr2.length;
snakes.push(new Snake(currentX,currentY,null));
//从终点出发,往前回溯
for(int d=dkPositions.size()-1;d>0;d--){
//这里获取上一步的可能的点
Map<Integer, Integer> preKPositions = dkPositions.get(d-1);
//计算当前点对应的k线
int k=currentX-currentY;
//前一个点只能是k+1或者k-1
//向下走一步得到当前点的前一个点(从k+1到k k变小)
//这里要注意(是从前一个点向下走一步得到当前的点,这是个反推的过程,下面同理)
Integer prex1 = preKPositions.getOrDefault(k + 1,-1);
//向右走一步得到当前点的前一个点
Integer prex2 = preKPositions.getOrDefault(k - 1,-1);
if (prex1==null&&prex2==null) {
return snakes;
}
//需要判断是从哪一个点过来的,先判断是不是从前一个点向右走一步得到的当前点
boolean right = prex2!=null;
int y1 ;
int y2 ;
//计算前一个点的x坐标和y坐标
int prey =right? prex2-k+1: prex1-k-1;
int prex = right?prex2:prex1;
//上面得到的x坐标和y坐标是基于prex1和prex2 其中有一个为空得来的
//如果两者不为空我们要重新计算
if(prex1!=null&&prex2!=null){
//计算两者的y坐标
y1 = prex1-k-1;
y2 = prex2-k+1;
//计算当前x坐标和上一个点的x坐标的差值
int diff1 = currentX-prex1;
int diff2 = currentX-prex2;
//我们倾向于从离当前点最近的地方过来的
//如果离prex2近,也就是diff2小,那么就是上一个点向右走一步得来的,反之就是向下
right = diff2<diff1;
//那么如果这两个x相等呢?那就继续比较y
if(diff1==diff2){
// 如果和prex2 的y(y2)差值更小,说明也是向右,反之向下
right = Math.abs(y2-currentY)<Math.abs(y1-currentY);
}
//最终我们得到了前一个点的x坐标和y坐标
prey = right?y2:y1;
prex = right?prex2:prex1;
}
//把我们得到的前一个点构造成一个snake对象,然后压栈
snakes.push(new Snake(prex,prey,right));
//当前坐标变成了前一个坐标
currentX = prex;
currentY = prey;
}
return snakes;
}
复制代码
回溯输出差异
当有了snake之后在想要得到差异信息就很简单了,只需要循环出栈就可以了
private static void printSnack(Stack<Snake> snakes, char[] arr1, char[] arr2) {
int x = 0;
int y = 0;
//从原点开始(这里要注意,不能忽略前n个相同的字符)
while(x<arr1.length&&y<arr2.length&&arr1[x]==arr2[y]){
System.out.println(arr2[y]);
x++;y++;
}
//整个逻辑很简单,根据right标识判断,如果是right,那么就向右走一步,也就是x+1(删除一个元素)
//然后再寻找后面有没有斜线(也就是相等的元素)
//那么如果right为FALSE就是向下走了,其余的代码也是同理
//right 为空就代表着结束了
while(!snakes.isEmpty()){
Snake snack = snakes.pop();
Boolean right = snack.right;
x = snack.x;
y = snack.y;
//right 为空代表是最后一个点了,直接退出
if (right==null) return;
if (right){
System.out.println("-"+arr1[x]);
x++;
while(x<arr1.length&&y<arr2.length&&arr1[x]==arr2[y]){
System.out.println(arr1[x]);
x++;y++;
}
}else{
System.out.println("+"+arr2[y]);
y++;
while(x<arr1.length&&y<arr2.length&&arr1[x]==arr2[y]){
System.out.println(arr2[y]);
x++;y++;
}
}
}
}
复制代码
最终的输出结果:
A
-B
-C
-A
+S
D
F
+C
+V
+F
E
D
-S
-F
G
+D
+C
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END