工作中遇到一个需求,需要对一个词典列表(本质是一个字符串列表)进行去除冗余处理。对于没有使用到的词典进行去除。
判断是否使用的规则是,在一个大型数据结构中词典名是否出现。这个数据结构有许多字段,有的字段可能还是嵌套的数据结构,每一层中都有词典出现的可能。
对数据结构进行循环遍历非常麻烦,时间复杂度也很高。想到可以把数据结构进行序列化,转换成判断短字符串是否在长字符串中出现的问题。
搜索了网络,发现有两种高效的字符串匹配算法:KMP算法与Trie字典树。
KMP算法
KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法。它通过构建一个部分匹配表,避免了不必要的回溯,从而提高了匹配的效率。KMP算法的主要思想是利用已匹配的信息,通过移动模式串的方式来避免重新匹配已经匹配过的字符。
KMP算法的优点在于适用于单模式串匹配,对于处理大文本串和多次匹配的情况下具有较好的性能。它的时间复杂度为O(n+m),其中n是长字符串的长度,m是短字符串的长度。KMP算法在实际应用中被广泛使用,例如在文本编辑器中的搜索功能和字符串匹配问题中。
Trie字典树
Trie字典树是一种多模式匹配的数据结构,用于高效地处理多个模式串的匹配问题。它将模式串逐个字符地插入到树中,形成一棵以字符为边的树形结构。通过遍历树,可以判断某个字符串是否存在于字典树中。
Trie字典树的优点在于可以在较短的时间内处理多个模式串的匹配,并且可以方便地进行前缀匹配和模式串的查找。它的时间复杂度为O(m),其中m是待匹配字符串的长度。Trie字典树在搜索引擎、拼写检查和自动补全等领域得到广泛应用。
然而,Trie字典树的缺点是占用较多的内存空间。每个字符都需要一个指针来指向下一个节点,当模式串较长时,字典树的节点数量会急剧增加,从而占用大量的空间。此外,在构建字典树时,需要遍历所有的模式串,构建过程的时间复杂度较高。
总结与应用场景
如果只需要进行单字符串匹配,并且处理大文本串和多次匹配,那么KMP算法是一个不错的选择。它能够快速定位匹配位置,提高匹配效率。
如果需要同时匹配多个不同的字符串,并且希望能够高效地处理多个字符串的匹配,那么Trie字典树是更合适的算法。它能够方便地进行前缀匹配和模式串的查找,适用于搜索引擎、拼写检查和自动补全等领域。
在实际应用中,KMP算法通常比字典树更快速,尤其是当短字符串的数量较少且长度较短时。字典树的优势在于它可以高效地处理大量的短字符串查找,而不需要重复构建前缀函数表。因此,如果需要频繁地对多个短字符串进行查找操作,且这些短字符串会被多次使用,那么字典树可能会更适合。
那我们的场景是不会对字符串进行重复匹配,那么使用 KMP 可能更合适。
使用 KMP 算法在长字符串中查询短字符串的代码:
/**
* 查看长字符串是否包含短字符串
*/
public static boolean containsSubstring(String longString, String shortString) {
int n = longString.length();
int m = shortString.length();
int[] lps = computeLPSArray(shortString);
int indexForLong = 0;
int indexForShort = 0;
while (indexForLong < n) {
if (longString.charAt(indexForLong) == shortString.charAt(indexForShort)) {
indexForLong++;
indexForShort++;
if (indexForShort == m) {
return true;
}
} else {
if (indexForShort != 0) {
indexForShort = lps[indexForShort - 1];
} else {
indexForLong++;
}
}
}
return false;
}
/**
* 计算字符串的前缀函数表
*/
private static int[] computeLPSArray(String pattern) {
int m = pattern.length();
int[] lps = new int[m];
int len = 0;
int i = 1;
while (i < m) {
if (pattern.charAt(i) == pattern.charAt(len)) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
KMP 的前缀函数表
KMP算法的核心思想是利用已经匹配过的信息,避免在模式串与文本串进行匹配时出现回溯。来记录模式串中前缀和后缀的最长公共部分长度的部分匹配表(Partial Match Table)就是核心。 可以举个例子来说说明这个前缀函数表(部分匹配表)如何生成:
初始化next[0] = 0。
从位置i=1开始计算next[i]的值。
a. 对于i=1,P[1] = 'B'。由于前缀和后缀都只有一个字符,且不相等,所以next[1] = 0。
b. 对于i=2,P[2] = 'C'。当前字符与前缀的下一个字符不相等,需要回溯。回溯到next[i-1] = next[1] = 0。P[0] = 'A'与P[2] = 'C'不相等,回溯到next[0] = 0。此时无法找到更短的相同前缀后缀,所以next[2] = 0。
c. 对于i=3,P[3] = 'D'。当前字符与前缀的下一个字符不相等,需要回溯。回溯到next[i-1] = next[2] = 0。P[0] = 'A'与P[3] = 'D'不相等,回溯到next[0] = 0。此时无法找到更短的相同前缀后缀,所以next[3] = 0。
d. 对于i=4,P[4] = 'A'。当前字符与前缀的下一个字符相等,可以在已经计算出的部分匹配表的基础上得到next[4]。由于next[3] = 0,所以next[4] = next[3] + 1 = 0 + 1 = 1。
e. 对于i=5,P[5] = 'B'。当前字符与前缀的下一个字符不相等,需要回溯。回溯到next[i-1] = next[4] = 1。P[1] = 'B'与P[5] = 'B'相等,找到了一个更短的相同前缀后缀,所以next[5] = 1 + 1 = 2。
f. 对于i=6,P[6] = 'D'。当前字符与前缀的下一个字符不相等,需要回溯。回溯到next[i-1] = next[5] = 2。P[2] = 'C'与P[6] = 'D'不相等,回溯到next[2] = 0。此时无法找到更短的相同前缀后缀,所以next[6] = 0。
g. 对于i=7,P[7] = 'B'。当前字符与前缀的下一个字符不相等,需要回溯。回溯到next[i-1] = next[6] = 0。P[0] = 'A'与P[7] = 'B'不相等,回溯到next[0] = 0。此时无法找到更短的相同前缀后缀,所以next[7] = 0。
计算完所有的next[i],得到部分匹配表为:next = [0, 0, 0, 0, 1, 2, 0, 0]。
通过以上的计算过程,我们得到了模式串"ABCDABD"的部分匹配表(next数组)为[0, 0, 0, 0, 1, 2, 0, 0]。这个部分匹配表可以避免不必要的比较和回溯操作,提高匹配效率。