【Java】+【LD算法】实现代码相似性比较
一.前言
1.编程题自动判分技术分析
最近在做一个C语言程序设计在线考试平台,要求编程题可自动判分。通过查询资料文献了解到,目前主要有以下三种编程自动判分方法:
①. 根据静态结果文件进行判分[1],判分系统通过比较考生提供的结果文件和预先准备的标准结果文件的内容进行评判;
②通过动态执行程序进行判分,判分系统动态编译考生提交的程序,并按照一定的测试用例动态执行程序,通过比较程序运行返回的结果和标准结果进行评判;
③根据程序源代码进行判分,判分系统按照一定的算法对考生提交的源程序代码和预先准备的标准源程序代码内容进行比较,依据它们的相似性进行评判。
本文即采用第三种方法,利用LD算法通过比较相似性进行评判。
2.LD算法简介
Levenshtein Distance,一般称为编辑距离(Edit Distance,Levenshtein Distance只是编辑距离的其中一种)或者莱文斯坦距离,算法概念是俄罗斯科学家弗拉基米尔·莱文斯坦(Levenshtein · Vladimir I)在1965年提出。此算法的概念很简单:Levenshtein Distance指两个字串之间,由一个转换成另一个所需的最少编辑操作次数,允许的编辑操作包括:
A.将其中一个字符替换成另一个字符(Substitutions)。
B.插入一个字符(Insertions)。
C.删除一个字符(Deletions)。
Levenshtein Distance公式定义如下:
这个数学公式最终得出的数值就是LD的值。举个例子:
将kitten这个单词转成sitting的LD值为3:
kitten → sitten (k→s)
sitten → sittin (e→i)
sittin → sitting (insert a ‘g’)
算法的基本步骤为:
(1)构造行数为m+1 列数为 n+1 的矩阵 , 用来保存完成某个转换需要执行的操作的次数,将串s[1…n] 转换到 串t[1…m] 所需要执行的操作次数为matrix[n][m]的值;
(2)初始化matrix第一行为0到n,第一列为0到m。
Matrix[0][j]表示第1行第j-1列的值,这个值表示将串s[1…0]转换为t[1…j]所需要执行的操作的次数,很显然将一个空串转换为一个长度为j的串,只需要j次的add操作,所以matrix[0][j]的值应该是j,其他值以此类推。
(3)检查每个从1到n的s[i]字符;
(4)检查每个从1到m的s[i]字符;
(5)将串s和串t的每一个字符进行两两比较,如果相等,则让cost为0,如果不等,则让cost为1(这个cost后面会用到);
(6)a、如果我们可以在k个操作里面将s[1…i-1]转换为t[1…j],那么我们就可以将s[i]移除,然后再做这k个操作,所以总共需要k+1个操作。
b、如果我们可以在k个操作内将 s[1…i] 转换为 t[1…j-1] ,也就是说d[i,j-1]=k,那么我们就可以将 t[j] 加上s[1…i],这样总共就需要k+1个操作。
c、如果我们可以在k个步骤里面将 s[1…i-1] 转换为 t [1…j-1],那么我们就可以将s[i]转换为 t[j],使得满足s[1…i] == t[1…j],这样总共也需要k+1个操作。(这里加上cost,是因为如果s[i]刚好等于t[j],那么就不需要再做替换操作,即可满足,如果不等,则需要再做一次替换操作,那么就需要k+1次操作)
因为我们要取得最小操作的个数,所以我们最后还需要将这三种情况的操作个数进行比较,取最小值作为d[i,j]的值;
d、然后重复执行3,4,5,6,最后的结果就在d[n,m]中;
算法的图解过程如下(以kitten与sitting为例):
step 1:初始化如下矩阵
step 2:从源串的第一个字符(“s”)开始,从上至下与目标串进行对比
如果两个字符相等,则在从此位置的左加1,上加1,左上加0三个位置中取出最小的值;若不等,则在从此位置的左,上,左上三个位置中取出最小的值再加上1;
第一次,源串第一个字符“s” 与目标串的“k”对比,左,上,左上三个位置中取出最小的值0,因为两字符不相等,所以加上1;接着,依次对比“s”→“i”,“s”→“t”,“s”→“t”,“s”→“e”,“s”→“n” 到扫描完目标串。
step 3:遍历整个源串与目标串对比:
step 4:扫描完最后一列,则最后一个为最短编辑距离:
求出编辑距离,那么两个字符串的相似度 Similarity = (Max(x,y) - Levenshtein)/Max(x,y),其中 x,y 为源串和目标串的长度。
二. 分析
算法步骤:
S1:剔除程序中所有注释、空行、空格
S2:剔除程序中所有变量、函数名
S3:剩下的部分(实际上主要是有 C++关键词构成的字符串)作为代码特征串
S4:两个特征串之间,使用字符串适量距离(Levenshtein Distance)计算相似度。
三.代码
package compare;
public abstract class Compare {
public abstract String getPreProcessedCode(String filePath);
public abstract double getSimilarity(String code1,String code2);
}
package compare.cplusplus;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.regex.Pattern;
import compare.Compare;
public class CPlusPlusCompare extends Compare{
/*C关键字*/
private String keyWords="and|asm|auto|bad_cast|bad_typeid|bool|break|case|catch|char|class|const|const_cast"+
"|continue|default|delete|do|double|dynamic_cast|else|enum|except|explicit|extern|false|finally|float|for"+
"|friend|goto|if|inline|int|long|mutable|namespace|new|operator|or|private|protected|public|register|reinterpret_cast"+
"|return|short|signed|sizeof|static|static_cast|struct|switch|template|this|throw|true|try|type_info|typedef"+
"|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while|cout";
private HashSet<String>keyWordSet = new HashSet<String>();
private LD ld = new LD();
//将关键字放入keywordset中
public CPlusPlusCompare(){
String list[]=keyWords.split("\\|");
for(String keyword:list){
keyWordSet.add(keyword);
}
}
//删除变量
private String delVariables(String code){
code = " "+code+" ";
//System.out.println("!"+code);
int pos1 = 0,pos2=0;
int len = code.length();
boolean isVariables=false;
StringBuffer ret = new StringBuffer();
while(pos1<len){
pos2++;
if(isVariables){
if(code.substring(pos2,pos2+2).replaceAll("[0-9a-zA-Z_][^a-zA-Z_]", "").equals("")){
isVariables = false;
String vv = code.substring(pos1,pos2+1);
if(this.keyWordSet.contains(vv)){
ret.append(vv);
//System.out.println("vv="+vv);
}
pos1 = pos2+1;
}
}else{
if(code.substring(pos2,pos2+2).replaceAll("[^\\._a-zA-Z][_a-zA-Z]", "").equals("")){
isVariables = true;
ret.append(code.substring(pos1,pos2+1));
//System.out.println(code.substring(pos1,pos2+1));
pos1 = pos2+1;
}
}
if(pos2 == len-2)break;
}
return ret.toString().trim();
//return code.replaceAll("(?<=([^\\._a-zA-Z]))[a-zA-Z_]+[0-9_a-zA-Z]*(?=([^a-zA-Z_]))", "");
}
@Override
public String getPreProcessedCode(String filePath) {
// TODO Auto-generated method stub
String code="";
try{
BufferedReader br = new BufferedReader (new InputStreamReader(new FileInputStream(filePath)));
StringBuffer buf = new StringBuffer();
String line;
while((line=br.readLine())!=null){
buf.append(line+"\n");
}
//删除所有注释
code = DelComments.delComments(buf.toString());
int pos1 = 0,pos2 = 0;
int len = code.length();
boolean isString = false;
StringBuffer ret = new StringBuffer();
while(pos1<len){
pos2++;
if(isString){
if(pos2<len-1){
if(code.substring(pos2, pos2+1).equals("\"") && !code.subSequence(pos2-1, pos2).equals("\\")){
isString = false;
ret.append(delVariables(code.substring(pos1, pos2+1)));
pos1 = pos2+1;
}
}else{
break;
}
}else{
if(pos2<len-1){
if(code.substring(pos2, pos2+1).equals("\"")){
isString = true;
ret.append(delVariables(code.substring(pos1, pos2)));
pos1 = pos2;
}
}else{
ret.append(delVariables(code.substring(pos1, code.length())));
break;
}
}
}
code = ret.toString();
//删除所有空格和换行
code=code.replaceAll("\\s", "");
br.close();
}catch(Exception e){
e.printStackTrace();
}
return code;
}
@Override
public double getSimilarity(String code1, String code2) {
// TODO Auto-generated method stub
return 1-ld.ld(code1, code2)*1.0/Math.max(code1.length(), code2.length());
}
/**********************测试1**********************/
// public static void main(String[]args) throws IOException{
// CPlusPlusCompare cmp = new CPlusPlusCompare();
// File dic= new File("E:\\AllSubmits");
// String names[]={"3568.cpp","3569.cpp","3570.cpp","3571.cpp","3572.cpp"};
//
// for(String name:names){
// BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("E:/"+name)));
// //bw.write("题目:"+name);
// System.out.println("题目:"+name);
// bw.newLine();
// ArrayList<String> idList =new ArrayList<String>();
// ArrayList<String> codeList =new ArrayList<String>();
// for(File f1:dic.listFiles()){
// File f2 = new File(f1.getAbsoluteFile()+"\\"+name);
// //File f2 = new File(f1.getAbsoluteFile()+"");
// if(f2.exists()){
// idList.add(f1.getName());
// codeList.add(cmp.getPreProcessedCode(f2.getAbsolutePath()));
// }
// }
// for(int i=0;i<codeList.size();i++){
// for(int j=i+1;j<codeList.size();j++){
// double s = cmp.getSimilarity(codeList.get(i), codeList.get(j));
//
// if(s>=0.7){
// bw.write(idList.get(i)+"\t"+idList.get(j)+"\t"+s);
// bw.newLine();
// }
// }
// }
// bw.close();
//
// }
//
// }
/**********************测试2**********************/
public static void main(String[] args) {
String code1="void cot<<\"123\"//123 123";
String code2="void cout<<\"\"";
CPlusPlusCompare cPlusPlusCompare = new CPlusPlusCompare();
code1 = cPlusPlusCompare.delVariables(code1);
code2 = cPlusPlusCompare.delVariables(code2);
double similarity = cPlusPlusCompare.getSimilarity(code1, code2);
int score = (int) (similarity*10);
System.out.println(similarity);
System.out.println(score);
}
}
package compare.cplusplus;
import java.io.File;
public class DelComments {
private static final char MARK = '"';
private static final char SLASH = '/';
private static final char BACKSLASH = '\\';
private static final char STAR = '*';
private static final char NEWLINE = '\n';
//引号
private static final int TYPE_MARK = 1;
//斜杠
private static final int TYPE_SLASH = 2;
//反斜杠
private static final int TYPE_BACKSLASH = 3;
//星号
private static final int TYPE_STAR = 4;
// 双斜杠类型的注释
private static final int TYPE_DSLASH = 5;
/**
* 删除char[]数组中_start位置到_end位置的元素
*
* @param _target
* @param _start
* @param _end
* @return
*/
public static char[] del(char[] _target, int _start, int _end) {
char[] tmp = new char[_target.length - (_end - _start + 1)];
System.arraycopy(_target, 0, tmp, 0, _start);
System.arraycopy(_target, _end + 1, tmp, _start, _target.length - _end
- 1);
return tmp;
}
/**
* 删除代码中的注释
*
* @param _target
* @return
*/
public static String delComments(String _target) {
int preType = 0;
int mark = -1, cur = -1, token = -1;
// 输入字符串
char[] input = _target.toCharArray();
for (cur = 0; cur < input.length; cur++) {
if (input[cur] == MARK) {
// 首先判断是否为转义引号
if (preType == TYPE_BACKSLASH)
continue;
// 已经进入引号之内
if (mark > 0) {
// 引号结束
mark = -1;
} else {
mark = cur;
}
preType = TYPE_MARK;
} else if (input[cur] == SLASH) {
// 当前位置处于引号之中
if (mark > 0)
continue;
// 如果前一位是*,则进行删除操作
if (preType == TYPE_STAR) {
input = del(input, token, cur);
// 退回一个位置进行处理
cur = token - 1;
preType = 0;
} else if (preType == TYPE_SLASH) {
token = cur - 1;
preType = TYPE_DSLASH;
} else {
preType = TYPE_SLASH;
}
} else if (input[cur] == BACKSLASH) {
preType = TYPE_BACKSLASH;
} else if (input[cur] == STAR) {
// 当前位置处于引号之中
if (mark > 0)
continue;
// 如果前一个位置是/,则记录注释开始的位置
if (preType == TYPE_SLASH) {
token = cur - 1;
}
preType = TYPE_STAR;
} else if(input[cur] == NEWLINE)
{
if(preType == TYPE_DSLASH)
{
input = del(input, token, cur);
// 退回一个位置进行处理
cur = token - 1;
preType = 0;
}
}
}
return new String(input);
}
}
package compare.cplusplus;
import java.io.File;
public class LD {
/**
* 计算矢量距离
* Levenshtein Distance(LD)
* @param str1 str1
* @param str2 str2
* @return ld
*/
public int ld(String str1, String str2)
{
//Distance
int [][] d;
int n = str1.length();
int m = str2.length();
int i; //iterate str1
int j; //iterate str2
char ch1; //str1
char ch2; //str2
int temp;
if (n == 0)
{
return m;
}
if (m == 0)
{
return n;
}
d = new int[n + 1][m + 1];
for (i = 0; i <= n; i++)
{ d[i][0] = i;
}
for (j = 0; j <= m; j++)
{
d[0][j] = j;
}
for (i = 1; i <= n; i++)
{
ch1 = str1.charAt(i - 1);
//match str2
for (j = 1; j <= m; j++)
{
ch2 = str2.charAt(j - 1);
if (ch1 == ch2)
{
temp = 0;
}
else
{
temp = 1;
}
d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + temp);
}
}
return d[n][m];
}
private int min(int one, int two, int three)
{
int min = one;
if (two < min)
{
min = two;
}
if (three < min)
{
min = three;
}
return min;
}
/**
* 计算相似度
* @param str1 str1
* @param str2 str2
* @return sim
*/
public double sim(String str1, String str2)
{
int ld = ld(str1, str2);
return 1 - (double) ld / Math.max(str1.length(), str2.length());
}
/**
* 测试
* @param args
*/
// public static void main(String[] args)
// {
//
// LD ld = new LD();
// double num = ld.sim("人民", "中国人民是人才");
// System.out.println(num);
// }
}