原文链接:模糊匹配算法
最长公共子序列的暴力实现:
import java.util.*;
public class StringCompareUtil {
public static int longestCommonSubsequence(String text1, String text2) {
return searchSame(text1, text2, 0, 0, new int[text1.length()][text2.length()]);
}
/**
* 最长公共子序列实现中,每个节点将面临四种不同的选择情况,设 N=Max(str1.length,str2.length),时间复杂度为 3 的 N 次幂
* 函数 searchSame( point1,point2 ) 表示 在 point1,point2 指向的字节前的字符串最大的重合长度
* 则可以发现,递归过程中,每对 point 指针指向的结果是可以被复用的
* 建立缓存避免重复计算
*/
private static int searchSame(String str1, String str2, int point1, int point2, int[][] cache) {
if (point1 == str1.length() || point2 == str2.length()) return 0;
if (cache[point1][point2] != 0) return cache[point1][point2];
int re = 0;
if (str1.charAt(point1) == str2.charAt(point2)) re = searchSame(str1, str2, point1 + 1, point2 + 1, cache) + 1;
re = Math.max(re, searchSame(str1, str2, point1 + 1, point2, cache));
re = Math.max(re, searchSame(str1, str2, point1, point2 + 1, cache));
re = Math.max(re, searchSame(str1, str2, point1 + 1, point2 + 1, cache));
System.out.println("point 1:" + point1 + " ,point2:" + point2);
return cache[point1][point2] = re;
}
public static void main(String[] args) {
String text1 = "abcde";
String text2 = "ace";
int l = longestCommonSubsequence(text1,text2);
System.out.println(l);
}
}
2.递归优化为递推,在缓存表上进行二维 DP :
public class test1 {
public static int longestCommonSubsequenceDP(String str1, String str2) {
if (str1 == null || str2 == null) return 0;
int m = str1.length(), n = str2.length();
int[][] cache = new int[m + 1][n + 1];
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (str1.charAt(i) == str2.charAt(j)) cache[i][j] = cache[i + 1][j + 1] + 1;
else cache[i][j] = Math.max(cache[i][j + 1], cache[i + 1][j]);
}
}
return cache[0][0];
}
public static void main(String[] args) {
String text1 = "abcde";
String text2 = "ace";
int l = longestCommonSubsequenceDP(text1,text2);
System.out.println(l);
}
}
时间复杂度又 3 的 max(n,m) 次幂 优化为 n*m (n,m 分别为两个入参字符串的长度)。至此,求取最长公共子序列的方法便确定下来了。
因为缓存使用的是二维数组,需要连续的存储空间,在待比较字符串长度较长时所需连续空间较大。空间较为紧张时可使用 Map 来替代数组:
import java.util.HashMap;
import java.util.Map;
public class test2 {
public static int longestCommonSubsequenceDP(String str1, String str2) {
if (str1 == null || str2 == null) return 0;
int m = str1.length(), n = str2.length();
Map<Long, Integer> cache = new HashMap<Long, Integer>();
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
long key = getKey(i, j);
if (str1.charAt(i) == str2.charAt(j)) cache.put(key, cache.getOrDefault(getKey(i + 1, j + 1),0)+1);
else cache.put(key, Math.max(cache.getOrDefault(getKey(i, j + 1),0), cache.getOrDefault(getKey(i + 1, j),0)));
}
}
return cache.get(getKey(0, 0));
}
private static long getKey(int i, int j) {
return ((long)i<<32)|j;
}
public static void main(String[] args) {
String text1 = "abcde";
String text2 = "ace";
int l = longestCommonSubsequenceDP(text1,text2);
System.out.println(l);
}
}
在 key 值的计算上使用了 i,j 分别表示 long 高低位字节的方式来避免 key 的冲突。
最长公共子序列的长度 % min(n,m) 便可表示重合率:
//计算重合率
private static double coincidenceRate(String str1, String str2, int length) {
int coincidenc = longestCommonSubsequence(str1, str2);
return MathUtils.txfloat(coincidenc, length);
}
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
public class test3 {
/**
* TODO 除法运算,保留小数
* @author 袁忠明
* @date 2018-4-17下午2:24:48
* @param a 被除数
* @param b 除数
* @return 商
*/
public static double txfloat(int a,int b) {
// TODO 自动生成的方法存根
DecimalFormat df=new DecimalFormat("0.00");//设置保留位数
return (float)a/b;
}
//计算重合率
private static double coincidenceRate(String str1, String str2, int length) {
int coincidenc = longestCommonSubsequence(str1, str2);
System.out.println("coincidenc"+coincidenc);
System.out.println("length"+length);
return txfloat(coincidenc, length);
}
public static int longestCommonSubsequence(String text1, String text2) {
return searchSame(text1, text2, 0, 0, new int[text1.length()][text2.length()]);
}
/**
* @Author Niuxy
* @Date 2020/10/2 7:12 下午
* @Description
* 最长公共子序列实现中,每个节点将面临四种不同的选择情况,设 N=Max(str1.length,str2.length),时间复杂度为 3 的 N 次幂
* 函数 searchSame( point1,point2 ) 表示 在 point1,point2 指向的字节前的字符串最大的重合长度
* 则可以发现,递归过程中,每对 point 指针指向的结果是可以被复用的
* 建立缓存避免重复计算
*/
private static int searchSame(String str1, String str2, int point1, int point2, int[][] cache) {
if (point1 == str1.length() || point2 == str2.length()) return 0;
if (cache[point1][point2] != 0) return cache[point1][point2];
int re = 0;
if (str1.charAt(point1) == str2.charAt(point2)) re = searchSame(str1, str2, point1 + 1, point2 + 1, cache) + 1;
re = Math.max(re, searchSame(str1, str2, point1 + 1, point2, cache));
re = Math.max(re, searchSame(str1, str2, point1, point2 + 1, cache));
re = Math.max(re, searchSame(str1, str2, point1 + 1, point2 + 1, cache));
System.out.println("point 1:" + point1 + " ,point2:" + point2);
return cache[point1][point2] = re;
}
public static void main(String[] args) {
String text1 = "abcde";
String text2 = "ace";
double l = coincidenceRate(text1,text2,5);
System.out.println(l);
}
}
去除冗余信息
在计算匹配度前,应当去除字符串中的冗余信息,进一步提高比对的精度。
比如 “中华人民共和国结婚证”、“中国结婚证书” 中,“中华人民共和国” 与 “中国” 对于比对来说,实际上是冗余信息。比对这两个字符串是否是同一个证照,只需要比对 “结婚证” 三个字即可。一股脑的对所有信息进行比对,会造成较大的误差。
因此在比对前,对于一些可以提前确定的常用的冗余信息,应当提前去除掉。比如对于证照名称比对的场景来说:“中华人民共和国”、“中国”、“山东省”、“XX市”、“书” 等都是应当提前去除的冗余信息。“中华人民共和国结婚证” 与 “山东省结婚证” 实际上都是同一个证照。
类test4:
public class test4 {
/**
* @Author Niuxy
* @Date 2020/9/30 1:12 下午
* @Description 字符串模糊匹配
*/
/**
* @Author Niuxy
* @Date 2020/9/30 2:08 下午
* @Description str1, str2 待比较字符串,threshold: 比较阈值,redundances: 冗余信息项
*/
public static boolean isSame(String str1, String str2, double threshold, String[] redundances) {
if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
throw new NullPointerException("str1 or str2 is null");
str1 = deleteRedundances(str1, redundances);
str2 = deleteRedundances(str2, redundances);
int length = Math.max(str1.length(), str2.length());
return isSame(str1, str2, length, threshold);
}
//比较重合率与阈值
public static boolean isSame(String str1, String str2, int length, double threshold) {
double re = coincidenceRate(str1, str2, length);
return re >= threshold;
}
//计算重合率
private static double coincidenceRate(String str1, String str2, int length) {
int coincidenc = longestCommonSubsequence(str1, str2);
return (float)coincidenc/length;
}
//去处冗余
private static String deleteRedundances(String str, String[] redundances) {
StringBuilder stringBuilder = new StringBuilder(str);
for (String redundance : redundances) {
int index = stringBuilder.indexOf(redundance);
if (index != -1) stringBuilder.replace(index, index + redundance.length(), "");
}
return stringBuilder.toString();
}
//计算最长公共子序列
public static int longestCommonSubsequence(String str1, String str2) {
if (str1 == null || str2 == null) return 0;
int m = str1.length(), n = str2.length();
int[][] cache = new int[m + 1][n + 1];
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (str1.charAt(i) == str2.charAt(j)) cache[i][j] = cache[i + 1][j + 1] + 1;
else cache[i][j] = Math.max(cache[i][j + 1], cache[i + 1][j]);
}
}
return cache[0][0];
}
// public static void main(String[] args) {
// String text1 = "abcde";
// String text2 = "ace";
// double l = coincidenceRate(text1,text2,5);
// System.out.println(l);
// }
}
类LicenseStringUtils:
public class LicenseStringUtils {
static final String[] redundances = new String[]{"中华人民共和国", "中国", "证书", "证照", "书", "审批表",
"申请表", "表","(",")","(",")","批准","登记","书","经营","许可证"};
static public boolean isSame(String str0, String str1) {
test4 test4 = new test4();
return test4.isSame(str0, str1, 0.6, redundances);
}
public static void main(String[] args) {
String text1 = "abcde";
String text2 = "ace";
boolean l = isSame(text1,text2);
System.out.println(l);
}
}
方法简单,但可以满足我目前的需求。
只是对于一些及其相似的证照,比如 “中华人民共和国残疾人证”、“中华人民共和国残疾军人证”,在阈值为 0.75 时无法分辨。
阈值设置太大又会造成一些证照的漏配,像这种极为相似的证照,就需要在阈值的选择上和冗余信息的选择上进行更贴合使用场景的优化了。
因为我的需求场景要求没有这么细致,且该类情况较少。类似的情况我直接在后期进行了人工干预(按名称排序人工检查一下即可)、
欢迎指出更好的方案或优化建议。