java 使用求字符串相似度算法来实现文本文档差异比较的功能

DocDiffTest.java:


import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;



public class DocDiffTest {

    private static String path = "C:\\xxx\\";

    private static List lines_v1 = readFile2List( path + "doc_v1.txt" );
    private static List lines_v2 = readFile2List( path + "doc_v2.txt" );

    public static void main(String[] args) {
        int[][] dp = calculateShortestEditDistance(lines_v1, lines_v2);

        int index1 = lines_v1.size() - 1;
        int index2 = lines_v2.size() - 1;
        System.out.println("一共需要" + dp[ index1 ][ index2 ] + "步编辑操作");

        List results = new ArrayList<>();
        while ( index1 >= 0 && index2 >= 0 ){
            String line_v1 = lines_v1.get(index1);
            String line_v2 = lines_v2.get(index2);
            if( line_v1.equals( line_v2 ) ){
                // v1:...a
                // v2:...a
                // 原封不动的输出
                results.add( "  " + line_v1 );
                index1--;
                index2--;
            }else {
                // v1:...a
                // v2:...b

                // v1:... a
                // v2:... b
                // 此时,a修改修改为b
                int sed1 = 0;
                if( index1 > 0 && index2 >0 ){
                    sed1 = dp[index1 - 1][index2 - 1];
                }

                // v1:...a
                // v2: ... b
                // 此时,需要插入b
                int sed2 = 0;
                if( index2 >0 ){
                    sed2 = dp[index1][index2 - 1];
                }

                // v1: ... a
                // v2:...b
                // 此时,需要删除a
                int sed3 = 0;
                if( index1 > 0 ){
                    sed3 = dp[index1-1][index2];
                }

                int sed = Math.min( Math.min( sed1,sed2 ),sed3 );
                if( sed == sed1 ){
                    results.add( "+ " + line_v2 + " DIFF:" + StringDiffTest.diff( line_v1,line_v2 ) );
                    results.add( "- " + line_v1 );
                    index1--;
                    index2--;
                }else if( sed == sed2 ){
                    results.add( "+ " + line_v2 );
                    index2--;
                }else if( sed == sed3 ){
                    results.add( "- " + line_v1 );
                    index1--;
                }
            }
        }

        while ( index1 >= 0 ){
            // v1 中多出的 "首行们" 都是需要删除的
            results.add( "- " + lines_v1.get( index1 ) );
            index1--;
        }

        while ( index2 >= 0 ){
            // v2 中多出的 "首行们" 都是需要被插入的
            results.add( "+ " + lines_v2.get( index2 ) );
            index2--;
        }

        for (int i=results.size() -1;i>=0;i--){
            System.out.println( results.get( i ) );
        }
    }

    private static int[][] calculateShortestEditDistance( List lines_v1,List lines_v2 ){
        // dp[i][j] 表示的是将 characters1 的前i个元素变换为 characters2 中的前j个元素需要使用的最优( 即需要转换步骤最少 )的转换方式
        int size_v1 = lines_v1.size();
        int size_v2 = lines_v2.size();
        int[][] dp = new int[ size_v1 ][ size_v2 ];

        for (int index1 = 0; index1 < size_v1; index1++) {
            String line_v1 = lines_v1.get( index1 );
            for (int index2 = 0; index2 < size_v2; index2++) {
                String line_v2 = lines_v2.get( index2 );
                if( index1 == 0 ){
                    if( index2 == 0 ){
                        if( line_v1.equals( line_v2 ) ){
                            // v1:a
                            // v2:a
                            dp[ index1 ][ index2 ] = 0;
                        }else {
                            // v1:a
                            // v2:b
                            dp[ index1 ][ index2 ] = 1;
                        }
                    }else {
                        if( contains( lines_v2,line_v1,0,index2 ) ){
                            // v1:      a
                            // v2:...a...   size =  index2 + 1
                            // v1转换为 v2需要 size - 1步( 也就是 index2步 )插入操作
                            dp[ index1 ][ index2 ] = index2;
                        }else {
                            // v1:      a
                            // v2:...b...   size =  index2 + 1
                            // v1转换为 v2需要 1步编辑操作,size-1= index2 步插入操作,一共index2 + 1步操作
                            dp[ index1 ][ index2 ] = index2 + 1;
                        }
                    }
                }else {
                    if( index2 == 0 ){
                        if( contains(lines_v1, line_v2, 0, index1) ){
                            // v1:....a...  size = index1 + 1
                            // v2:       a
                            // v1转换为 v2需要 size-1=index1步删除操作
                            dp[ index1 ][ index2 ] = index1;
                        }else {
                            // v1:....a...  size = index1 + 1
                            // v2:       b
                            // v1转换为 v2需要 1步编辑操作和size-1=index1步删除操作,一共index1+1步操作
                            dp[ index1 ][ index2 ] = index1 + 1;
                        }
                    }else {
                        if( line_v1.equals( line_v2 ) ){
                            // v1:...a
                            // v2:...a
                            dp[ index1 ][ index2 ] = dp[ index1 - 1 ][ index2 - 1 ];
                        }else {
                            // v1:...a
                            // v2:...b

                            // v1:... a
                            // v2:... b
                            // 此时 v1 的前部分和 v2的前部分做dp运算,a修改为b
                            int sed_prev1 = dp[ index1 - 1 ][ index2 - 1 ];

                            // v1: ... a
                            // v2:...b
                            // 此时v1的前部分和v2做dp运算,删除a
                            int sed_prev2 = dp[ index1 - 1 ][ index2 ];

                            // v1: ...a
                            // v2:  ... b
                            // 此时 v1和v2的前部分做dp运算,插入b
                            int sed_prev3 = dp[ index1 ][ index2 - 1 ];

                            int sed = Math.min( Math.min( sed_prev1,sed_prev2 ),sed_prev3 ) + 1;
                            dp[ index1 ][ index2 ] = sed;
                        }
                    }
                }
            }
        }
        return dp;
    }

    /**
     * @param lines
     * @param targetLine
     * @param beginIndex
     * @param endIndex
     * @return
     */
    private static boolean contains(List lines, String targetLine, int beginIndex, int endIndex) {
        for (int i = beginIndex; i <=endIndex ; i++) {
            // todo 是 == 还是 equals???
            if( targetLine.equals( lines.get( i ) ) ){
                return true;
            }
        }
        return false;
    }

    private static List readFile2List( String filePath ){
        BufferedReader reader = null;
        try {
            List lines = new ArrayList<>();
            reader = new BufferedReader(new FileReader(filePath));
            String line = reader.readLine();
            while (line != null) {
                lines.add( line );
                line = reader.readLine();
            }
            return lines;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
StringDiffTest.java:


import java.util.ArrayList;
import java.util.List;



public class StringDiffTest {
    private static String str1 = "StringEditDistanceTest";
    private static  String str2 = "testStringEditDistance";
    // acc e   ler ate
    // acc u mul   ate

    private static List characters_v1 = string2CharacterList( str1 );
    private static List characters_v2 = string2CharacterList( str2 );


    public static void main(String[] args) {
        String diffInfo = diff("123456789", "4567abcdf");
        System.out.println( diffInfo );
    }

    private static void init( String str1,String str2 ){
        StringDiffTest.str1 = str1;
        StringDiffTest.str2 = str2;

        characters_v1 = string2CharacterList( StringDiffTest.str1 );
        characters_v2 = string2CharacterList( StringDiffTest.str2 );
    }
    public static String diff( String str1,String str2 ) {
        init( str1,str2 );

        int[][] dp = calculateShortestEditDistance(characters_v1, characters_v2);

        int index1 = characters_v1.size() - 1;
        int index2 = characters_v2.size() - 1;
        // System.out.println("一共需要" + dp[ index1 ][ index2 ] + "步编辑操作");

        List operations = new ArrayList<>();
        while ( index1 >= 0 && index2 >= 0 ){
            Character char1 = characters_v1.get(index1);
            Character char2 = characters_v2.get(index2);
            if( char1.equals( char2 ) ){
                // v1:...a
                // v2:...a
                // 将 char1 和char2相同,原封不动的输出
                OperationVO operation = new OperationVO();
                operation.setType( OperationVO.type_read );
                operation.setChar1( char1 );
                operations.add( operation );
                index1--;
                index2--;
            }else {
                // v1:...a
                // v2:...b

                // v1:... a
                // v2:... b
                // 此时,a修改修改为b
                int sed1 = 0;
                if( index1 >0 && index2 > 0 ){
                    sed1 = dp[index1 - 1][index2 - 1];
                }

                // v1:...a
                // v2: ... b
                // 此时,需要插入b
                int sed2 = 0;
                if( index2 > 0 ){
                    sed2 = dp[index1][index2 - 1];
                }

                // v1: ... a
                // v2:...b
                // 此时,需要删除a
                int sed3 = 0;
                if( index1 >0 ){
                    sed3 = dp[index1-1][index2];
                }

                OperationVO operation = new OperationVO();
                int sed = Math.min( Math.min( sed1,sed2 ),sed3 );
                if( sed == sed1 ){
                    operation.setType( OperationVO.type_update );
                    operation.setChar1( char1 );
                    operation.setChar2( char2 );
                    operations.add( operation );
                    index1--;
                    index2--;
                }else if( sed == sed2 ){
                    operation.setType( OperationVO.type_add );
                    operation.setChar2( char2 );
                    operations.add( operation );
                    index2--;
                }else if( sed == sed3 ){
                    operation.setType( OperationVO.type_delete );
                    operation.setChar1( char1 );
                    operations.add( operation );
                    index1--;
                }
            }
        }

        while ( index1 >= 0 ){
            // v1 中多出的 "首行们" 都是需要删除的
            Character char1 = characters_v1.get(index1);
            OperationVO operation = new OperationVO();
            operation.setType( OperationVO.type_delete );
            operation.setChar1( char1 );
            operations.add( operation );
            index1--;
        }

        while ( index2 >= 0 ){
            // v2 中多出的 "首行们" 都是需要被插入的
            Character char2 = characters_v2.get(index2);
            OperationVO operation = new OperationVO();
            operation.setType( OperationVO.type_add );
            operation.setChar2( char2 );
            operations.add( operation );
            index2--;
        }

        List operations_add = new ArrayList<>();
        List operations_delete = new ArrayList<>();
        List operations_update = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        for ( int i = operations.size() - 1;i >= 0;i-- ){
            // abc-a+c-b-d+cddddeee+a
            //  连续的修改、删除、增加要一次输出
            OperationVO operation = operations.get(i);
            int type = operation.getType();
            if( OperationVO.type_read == type ){
                continuePrintAddOperation( sb,operations_add );
                continuePrintDeleteOperation( sb,operations_delete );
                continuePrintUpdateOperation( sb,operations_update );
                sb.append( operation.getChar1() );
            }else if( OperationVO.type_add == type ){
                continuePrintDeleteOperation( sb,operations_delete );
                continuePrintUpdateOperation( sb,operations_update );
                operations_add.add( operation );
            }else if( OperationVO.type_delete == type ){
                continuePrintAddOperation( sb,operations_add );
                continuePrintUpdateOperation( sb,operations_update );
                operations_delete.add( operation );
            }else if( OperationVO.type_update == type ){
                continuePrintAddOperation( sb,operations_add );
                continuePrintDeleteOperation( sb,operations_delete );
                operations_update.add( operation );
            }
        }
        continuePrintAddOperation( sb,operations_add );
        continuePrintDeleteOperation( sb,operations_delete );
        continuePrintUpdateOperation( sb,operations_update );
        return sb.toString();
    }


    private static void continuePrintAddOperation( StringBuilder sb,List operations ){
        // (+abc)
        if( operations.size() == 0 ){
            return;
        }
        String chars_add = "";
        for( OperationVO operation:operations ){
            chars_add += operation.getChar2();
        }
        sb.append( chars_add );
        sb.append( "(" );
        sb.append( "+\"" );
        sb.append( chars_add );
        sb.append( "\"" );
        sb.append( ")" );
        operations.clear();
    }

    private static void continuePrintDeleteOperation( StringBuilder sb,List operations ){
        // (-abc)
        if( operations.size() == 0 ){
            return;
        }
        String chars_old = "";
        for( OperationVO operation:operations ){
            chars_old += operation.getChar1();
        }
        sb.append( "( 删除 \"" );
        sb.append( chars_old );
        sb.append( "\")" );
        operations.clear();
    }

    private static void continuePrintUpdateOperation( StringBuilder sb,List operations ){
        // (abc->def)
        if( operations.size() == 0 ){
            return;
        }
        String chars_old = "";
        String chars_new = "";
        for( OperationVO operation:operations ){
            chars_old += operation.getChar1();
            chars_new += operation.getChar2();
        }
        sb.append( chars_new );
        sb.append( "(原来是 \"" );
        sb.append( chars_old );
        sb.append( "\"" );
        sb.append( ")" );
        operations.clear();
    }

    private static List string2CharacterList(String str) {
        List characters = new ArrayList<>();
        int length = str.length();
        for (int i = 0; i < length; i++) {
            char c = str.charAt(i);
            characters.add( c );
        }
        return characters;
    }

    private static int[][] calculateShortestEditDistance( List characters_v1,List characters_v2 ){
        // dp[i][j] 表示的是将 characters1 的前i个元素变换为 characters2 中的前j个元素需要使用的最优( 即需要转换步骤最少 )的转换方式
        int size_v1 = characters_v1.size();
        int size_v2 = characters_v2.size();
        int[][] dp = new int[ size_v1 ][ size_v2 ];

        for (int index1 = 0; index1 < size_v1; index1++) {
            Character char1 = characters_v1.get( index1 );
            for (int index2 = 0; index2 < size_v2; index2++) {
                Character char2 = characters_v2.get( index2 );
                if( index1 == 0 ){
                    if( index2 == 0 ){
                        if( char1.equals( char2 ) ){
                            // v1:a
                            // v2:a
                            dp[ index1 ][ index2 ] = 0;
                        }else {
                            // v1:a
                            // v2:b
                            dp[ index1 ][ index2 ] = 1;
                        }
                    }else {
                        if( contains( characters_v2,char1,0,index2 ) ){
                            // v1:      a
                            // v2:...a...   size =  index2 + 1
                            // v1转换为 v2需要 size - 1步( 也就是 index2步 )插入操作
                            dp[ index1 ][ index2 ] = index2;
                        }else {
                            // v1:      a
                            // v2:...b...   size =  index2 + 1
                            // v1转换为 v2需要 1步编辑操作,size-1= index2 步插入操作,一共index2 + 1步操作
                            dp[ index1 ][ index2 ] = index2 + 1;
                        }
                    }
                }else {
                    if( index2 == 0 ){
                        if( contains(characters_v1, char2, 0, index1) ){
                            // v1:....a...  size = index1 + 1
                            // v2:       a
                            // v1转换为 v2需要 size-1=index1步删除操作
                            dp[ index1 ][ index2 ] = index1;
                        }else {
                            // v1:....a...  size = index1 + 1
                            // v2:       b
                            // v1转换为 v2需要 1步编辑操作和size-1=index1步删除操作,一共index1+1步操作
                            dp[ index1 ][ index2 ] = index1 + 1;
                        }
                    }else {
                        if( char1.equals( char2 ) ){
                            // v1:...a
                            // v2:...a
                            dp[ index1 ][ index2 ] = dp[ index1 - 1 ][ index2 - 1 ];
                        }else {
                            // v1:...a
                            // v2:...b

                            // v1:... a
                            // v2:... b
                            // 此时 v1 的前部分和 v2的前部分做dp运算,a修改为b
                            int sed_prev1 = dp[ index1 - 1 ][ index2 - 1 ];

                            // v1: ... a
                            // v2:...b
                            // 此时v1的前部分和v2做dp运算,删除a
                            int sed_prev2 = dp[ index1 - 1 ][ index2 ];

                            // v1: ...a
                            // v2:  ... b
                            // 此时 v1和v2的前部分做dp运算,插入b
                            int sed_prev3 = dp[ index1 ][ index2 - 1 ];

                            int sed = Math.min( Math.min( sed_prev1,sed_prev2 ),sed_prev3 ) + 1;
                            dp[ index1 ][ index2 ] = sed;
                        }
                    }
                }
            }
        }
        return dp;
    }

    /**
     * @param characters
     * @param targetChar
     * @param beginIndex
     * @param endIndex
     * @return
     */
    private static boolean contains(List characters, Character targetChar, int beginIndex, int endIndex) {
        for (int i = beginIndex; i <=endIndex ; i++) {
            // todo 是 == 还是 equals???
            if( targetChar.equals( characters.get( i ) ) ){
                return true;
            }
        }
        return false;
    }
}
OperationVO.java:


import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;


@Getter
@Setter
public class OperationVO implements Serializable {

    public static final int type_read = 0;
    public static final int type_delete = 1;
    public static final int type_add = 2;
    public static final int type_update = 3;

    private int type;
    private Character char1;
    private Character char2;
}

doc_v1.txt:

盼望着,盼望着,东风来了,春天的脚步近了。
一切都像刚睡醒的样子,欣欣然张开了眼。
山朗润起来了,水涨起来了,太阳的脸红起来了。
小草偷偷地从土地里钻出来,嫩嫩的,绿绿的。
园子里,田野里,瞧去,一大片一大片满是的。
坐着,躺着,打两个滚,踢几脚球,赛几趟跑,捉几回迷藏。
风轻俏俏的,草软绵绵的。
桃树,杏树,梨树,你不让我,我不让你,都开满了花赶趟儿。
红的像火,粉的像霞,白的像雪。
花里带着甜味;闭了眼,树上仿佛已经满是桃儿,杏儿,梨儿。
花下成千成百的蜜蜂嗡嗡的闹着,大小的蝴蝶飞来飞去。
野花遍地是:杂样儿,有名字的,没名字的,散在草丛里像眼睛像星星,还眨呀眨。
“吹面不寒杨柳风”,不错的,像母亲的手抚摸着你,风里带着些心翻的泥土的气息,混着青草味儿,还有各种花的香,都在微微润湿的空气里酝酿。
鸟儿将巢安在繁花嫩叶当中,高兴起来,呼朋引伴的卖弄清脆的歌喉,唱出婉转的曲子,跟清风流水应和着。
牛背上牧童的短笛,这时候也成天嘹亮的响着。
雨是最寻常的,一下就是三两天。
可别恼。看,像牛牦,像花针,像细丝,密密的斜织着,人家屋顶上全笼着一层薄烟。
树叶却绿得发亮,小草也青得逼你的眼。傍晚时候,上灯了,一点点黄晕的光,烘托出一片安静而和平的夜。
在乡下,小路上,石桥边,有撑着伞慢慢走着的人,地里还有工作的农民,披着所戴着笠。
他们的房屋稀稀疏疏的,在雨里静默着。
天上的风筝渐渐多了,地上的孩子也多了。
城里乡下,家家户户,老老小小,也赶趟似的,一个个都出来了。
舒活舒活筋骨,抖擞抖擞精神,各做各的一份事儿去。
“一年之计在于春”,刚起头儿,有的是功夫,有的是希望 春天像刚落地的娃娃,从头到脚都是新的,它生长着。
春天像小姑娘,花枝招展的笑着走着。 春天像健壮的青年,有铁一般的胳膊和腰脚,领着我们向前去。

doc_v2.txt:

盼望着,盼望着,东风来了,春天的脚步进了。
一切都像刚睡醒的样子,欣欣然张开了眼。
山朗润起来了,水涨起来了,太阳的脸红起来了。
小草偷偷地从土地里钻出来,嫩嫩的,绿绿的。
园子里,田野里,瞧去,一大片一大片满是的。
坐着,躺着,打两个滚、踢几脚球,赛几趟跑、捉几回迷藏。
风轻巧巧的,草软绵绵的。
桃树,杏树,梨树,你不让我,我不让你,都开满了花赶趟儿。
红的像火,粉的像霞,白的像雪。
花里带着甜味;闭了眼,树上仿佛已经满是桃儿,杏儿,梨儿。
花下成千成百的蜜蜂嗡嗡的闹着,大小的蝴蝶非来非去。
野花遍地是:杂样儿,有名字的,没名字的,散在草丛里像眼睛像星星,还眨呀眨。
“吹面不寒杨柳风”,不错的,像母亲的手抚摸着你,风里带着些心翻的泥土的气息,混着青草味儿,还有各种花的香,都在微微润湿的空气里酝酿。
鸟儿将巢安在繁花嫩叶当中,高兴起来,呼朋引伴的卖弄风骚清脆的歌喉,唱出婉转的曲子,跟清风流水应和着。
牛背上牧童的断敌,这时候也成天嘹亮的响着。
雨是最寻常的,一下就是三两天。
可别恼。看,像牛牦,像花针,象细丝,密密的斜织着,人家屋顶上全笼着亿层薄烟。
树叶却绿得发亮,小草也青得逼你的眼。傍晚时候,上灯了,一点点黄晕的光,烘托出一片安静而和平的夜。
在乡下,小路上,石桥边,有撑着伞慢慢走着的人,地里还有工作的农民,披星戴月着所戴着笠。
他们的房屋稀稀疏疏的,在雨里静默着。
天上的风筝渐渐多了,地上的孩子也多了。
城里乡下,家家户户,老老小小,也赶趟似的,一各各都出来了。
舒活舒活筋骨,抖擞抖擞精神,各做各的一份事儿去。
"一年之计在于春",刚起头儿,有的是功夫,有的是希望 春天刚落地的娃娃,从头到脚都是,它生长着。
春天像小菇娘,花枝招展的笑着走着。 春天像键壮的青年,有铁一般的胳膊和腰脚,领着我们向前去。

测试输出:

一共需要11步编辑操作
edit   盼望着,盼望着,东风来了,春天的脚步进(原来是 "近")了。
       一切都像刚睡醒的样子,欣欣然张开了眼。 no change
       山朗润起来了,水涨起来了,太阳的脸红起来了。 no change
       小草偷偷地从土地里钻出来,嫩嫩的,绿绿的。 no change
       园子里,田野里,瞧去,一大片一大片满是的。 no change
edit   坐着,躺着,打两个滚、(原来是 ",")踢几脚球,赛几趟跑、(原来是 ",")捉几回迷藏。
edit   风轻巧巧(原来是 "俏俏")的,草软绵绵的。
       桃树,杏树,梨树,你不让我,我不让你,都开满了花赶趟儿。 no change
       红的像火,粉的像霞,白的像雪。 no change
       花里带着甜味;闭了眼,树上仿佛已经满是桃儿,杏儿,梨儿。 no change
edit   花下成千成百的蜜蜂嗡嗡的闹着,大小的蝴蝶非(原来是 "飞")来非(原来是 "飞")去。
       野花遍地是:杂样儿,有名字的,没名字的,散在草丛里像眼睛像星星,还眨呀眨。 no change
       “吹面不寒杨柳风”,不错的,像母亲的手抚摸着你,风里带着些心翻的泥土的气息,混着青草味儿,还有各种花的香,都在微微润湿的空气里酝酿。 no change
edit   鸟儿将巢安在繁花嫩叶当中,高兴起来,呼朋引伴的卖弄风骚(+"风骚")清脆的歌喉,唱出婉转的曲子,跟清风流水应和着。
edit   牛背上牧童的断敌(原来是 "短笛"),这时候也成天嘹亮的响着。
       雨是最寻常的,一下就是三两天。 no change
edit   可别恼。看,像牛牦,像花针,象(原来是 "像")细丝,密密的斜织着,人家屋顶上全笼着亿(原来是 "一")层薄烟。
       树叶却绿得发亮,小草也青得逼你的眼。傍晚时候,上灯了,一点点黄晕的光,烘托出一片安静而和平的夜。 no change
edit   在乡下,小路上,石桥边,有撑着伞慢慢走着的人,地里还有工作的农民,披星戴月(+"星戴月")着所戴着笠。
       他们的房屋稀稀疏疏的,在雨里静默着。 no change
       天上的风筝渐渐多了,地上的孩子也多了。 no change
edit   城里乡下,家家户户,老老小小,也赶趟似的,一各各(原来是 "个个")都出来了。
       舒活舒活筋骨,抖擞抖擞精神,各做各的一份事儿去。 no change
edit   "(原来是 "“")一年之计在于春"(原来是 "”"),刚起头儿,有的是功夫,有的是希望 春天( 删除 "像")刚落地的娃娃,从头到脚都是( 删除 "新的"),它生长着。
edit   春天像小菇(原来是 "姑")娘,花枝招展的笑着走着。 春天像键(原来是 "健")壮的青年,有铁一般的胳膊和腰脚,领着我们向前去。

你可能感兴趣的:(算法可视化,动态规划,算法,java,算法,动态规划,字符串相似度,diff算法)