在软件工程中,我们用到字符串匹配的地方非常多,比如:文本编辑软件中的查找功能,判断两个字符串是否相等。字符串匹配分为两种情况:(1)字符串一对一的匹配,(2)在一个字符串中同时查找多个子串。
1.对于一对一的匹配,有经典的BF算法(Brute Force)暴力匹配算法:
核心思想:字符串匹配算法中有两个核心词:(1)基础字符串(主串)(2)模式串
(例如:在字符串A中查找字符串B,那么A就是主串,B就是模式串)
假如主串长度为m,模式串长度为n,根据模式串的长度,可以将主串分解成m-n+1个子串。然后拿着模式串与主串逐一进行匹配,时间复杂度为O(m*n)
主串:abcbdef 子串:bcd
可以将主串分解为:abc bcd cbd bde def
public class StringMatch {
public static void main(String[] args) {
String basic = "zhangsanlisi";
String pattern = "lisi";
StringMatch match = new StringMatch();
int i = match.bf(basic,pattern);
System.out.println("在基础字符串中第一次出现的位置:"+i);
}
/*暴力匹配算法:
* 将基础字符串按照模式串分解成a-b+1个,然后逐个与模式串进行匹配
* 时间复杂度为O(n*m) n为基础字符串的长度,m为模式串的长度*/
public int bf(String basic,String pattern) {
int a = basic.length();
int b = pattern.length();
int k;
char[] bas = basic.toCharArray();
char[] pat = pattern.toCharArray();
/*判断参数是否有效*/
if(a == 0 || b == 0 || a-b<0) return -1;
/*当前for循环的意思是:基础串与模式串比较的次数,一个基础串可以分解成a-b+1个模式串*/
for(int i=0;i<=a-b;i++) {
k = 0;
/*拿模式串与当前分解的基础串进行逐个字符的匹配,参数k用来记录匹配到的字符串的个数*/
for(int j=0;j<b;j++) {
if(bas[i+j] == pat[j]) {
k++;
}else {
break;
}
}
/*如果k与模式串的长度b相等的话,那么证明两个字符串就是相等的*/
if(k == b) return i;
}
return -2;
}
}
在实际的软件开发中,主串和模式串的长度可能相对比较短,并且在匹配的过程中,如果遇到不相等的字符,就会停止当前的匹配操作,最坏的情况下时间复杂度为O(m*n),但是实际情况都比这个值低,并且暴力匹配算法思想简单,容易实现,所以在条件允许的情况下是首选。
2.Trie树(字典树):
像是Google,百度这样的搜索引擎,当我们在搜索栏中键入某一些文字的时候,它就会在下拉栏中自动提示出一些相应的关键词。这种搜索引擎的自动提示功能就可以使用Trie树来实现。
Trie数的本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起,构成一个(字符串集合)一颗字典树。
Trie树的结构大致如下:
在Trie数中,根节点不包含任何信息,其它每一个节点都包含某一字符串中的一个字符。在Trie中有两个重要的操作:
(1)通过字符串集合来构建一颗Trie树:因为构建Trie树需要遍历所有的字符串,所以时间复杂度为O(n)
(2)在Trie中查询某一个字符串:假设需要查询的字符串长度为k,那么我们只需比对大约k个节点,所以查找的时间复杂度为 O(k)。
3.Trie树和散列表,红黑树的比较:
字符串的匹配问题,归根结底就是数据的查找问题,对于动态高效数据操作的数据结构,其实也可以实现字符串的匹配功能。例如,散列表,红黑树,跳表等。他们各自的优缺点如下:
利用Trie树进行字符串匹配,其实对数据的要求比较苛刻:
(1)首先字符集不能太大,因为Trie树在存储数据时,不仅要存储数据本身,还要存储指针的引用,所以会浪费更多的空间。
(2)同时需要字符串前缀重合的比较多,不然同样会消耗更多的空间。
(3)Tried树中的数据是通过指针串起来的,所以在内存中的分布是不连续的,对CPU缓存不友好。
(4)如果要使用Trie树来实现字符串匹配,需要从0到1来构建,增加了工程的复杂度。
实际上,Tried树只是不适合精确查找,如果进行精确查找,那么可以使用其它动态操作数据的容器。Trie树适合匹配查找这种应用场景