Myers差分算法

本文参考了文章: 简析Myers

在参考文章中我对Myers差分算法有了一定的理解,但是在代码实现的过程中还是会遇到很多问题,而作者对源码的解释比较少,因此我着重于对实现过程做一个分析。

概述

我把代码的实现分为了三个部分:

绘制编辑图

生成snake

回溯snake

绘制编辑图

下图是原论文里面的编辑图,我们需要吧圈起来的点对应的保存下来,保存的方式类似下面的表格

image.png

在这张表格里面表示了k线和步数d之间的关系
image.png

具体的代码实现可以参考论文里面的伪代码

 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
            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);
                    break dloop;
                }
            }
            dkPositions.add(kPositionsTemp);
        }
          //到这里就已经绘制完了整个编辑图,然后要利用这个编辑图生成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
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值