主串S:"abcacabdc",模式串T:"abd",请找出模式串在主串中第一次出现的位置。
提示:主串和模式串均为小写字母且都是合法输入。
1.1 思路1
- 匹配肯定头部要相等才开始比较后面的
- 如果开始匹配,每一个字符都应该相等,且不为结束符
\0
或0
或NULL
。- 如果匹配结束时,子串已经到结束符,那说明和子串完全匹配
过程:
- abcacabdc 和 abd 匹配不上,移动到下一个
a
开头的子串 - abcacabdc 和 abd 匹配不上,移动到下一个
a
开头的子串 - abcacabdc 和 abd 匹配上了,返回
abd
开头的索引
#define NOT_FOUND -1
int findStringInString(char *a, char *b) {
if (!*a || !*b) return NOT_FOUND;
int i = 0, j;
char *p;
while (a[i]) {
if (a[i] == b[0]) {
p = a + i; // 当前字符串的开头
j = 1; // 头已经比较过了,从下一个位置开始比较
while (p[j] && b[j] && p[j] == b[j]) // 一次比较
j++;
if (!b[j]) // 子串完全匹配
return i;
if (!p[j]) // 主串已经到末尾了,之后字符串长度都不够,就不需要再比较了
return NOT_FOUND;
}
i++; // 移动字符开始的索引
}
return NOT_FOUND;
}
1.2 思路2
假设主串:"abxabcabcaby",子串:"abcaby"。
思路1中,a指针都是依次增加的,所以a需要移动次,为主串长度,为子串长度。
参考每日气温 栈练习题 中题目2的思路2,跳跃对比的想法,在比较过程中如果出现了a
开头,我们就可以直接跳过中间不是a
开头的字符。
例如,abxabcabcaby中,abcabc不匹配之后,我们可以直接跳过bc
来到下一个a
。
int findStringInString2(char *a, char *b) {
if (!*a || !*b) return NOT_FOUND;
int i = 0, j, next = NOT_FOUND;
char *p;
while (a[i]) {
if (a[i] == b[0]) {
p = a + i; // 当前字符串的开头
j = 1; // 头已经比较过了,从下一个位置开始比较
while (p[j] && b[j]) { // 一次比较
if (next == NOT_FOUND && p[j] == b[0]) { // 匹配过程中找到了下一个开头
next = i + j;
}
if (p[j] != b[j]) break;
j++;
}
if (!b[j]) // 子串完全匹配
return i;
if (!p[j]) // 主串已经到末尾了,之后字符串长度都不够,就不需要再比较了
return NOT_FOUND;
}
if (next != NOT_FOUND) {
i = next;
next = NOT_FOUND;
} else {
i++; // 移动字符开始的索引
}
}
return NOT_FOUND;
}
1.3 思路3
假设主串:"abxabcabcaby",子串:"abcaby"。
思路2 abxabcabcaby 中,abcabc不匹配之后,我们可以直接跳过bc
来到下一个a
。
在匹配 abxabcabcaby 中,其实我们发现最后的c
和子串的y
不匹配,但是c
之前的ab
和aby
中的ab
是匹配过的,且ab
在abcaby
的前面也出现过,能否用这个思路来进行跳跃对比呢?
显然我们需要对我们的子串进行处理,当发生不匹配时,用来进行跳跃。要怎么构建这个跳跃信息呢?
以"abcaby"为例,重复的子串是ab
。abcaby匹配abcabc时:
- 当前面字符均匹配,来到最后的
y
和c
时发生不匹配。 - 我们可以认为
y
之前的ab
已经匹配过了,我们从abcaby跳过最前面的ab
从c
开始继续进行匹配。
也就是说,当模式串存在重复子串时,我们可以回溯到重复子串之后一个位置继续进行匹配。
0 1 2 3 4 5 // 索引
a b c a b y // 模式串
0 0 0 1 2 0 // 回溯索引
第二次出现ab
时,前一次他们出现的下一个位置是c
。当y
不匹配时,我们看到前一个位置索引为2
,正是我们子串c
的索引值,然后继续进行比较。
1.3.1 构建回溯数组
那么构建这个数组的思路就明了了,即求出子串中重复子串的下一个位置。
我们用aabaabaaa
进行一次构建来说明:
int *GetNext(char *pattern){
size_t length = strlen(pattern); // 获取数组长度
int *res = (int *)malloc(sizeof(length) * length);
res[0] = 0; // 第一个回溯索引为0
for (int i = 1, j = 0; i < length;) {
if (pattern[i] == pattern[j]) { // 匹配位置i和j的值相同,则res[i]等于j+1,i和j后移
res[i] = j + 1;
j++;
i++;
} else { // 匹配位置i和j的值不同
if (j != 0) { // 还j不等于0,j等于前一个字符的回溯索引,重新比较
j = res[j-1];
} else {
res[i] = 0; // j等于0,则res[i]填入0,i后移
i++;
}
}
}
return res;
}
1.3.2 匹配过程
下面我们来说说比较问题:
这里我们用abxabcabcaby
和abcaby
进行匹配。
int FindStringInString3(char *a, char *b){
if (!*a || !*b) return NOT_FOUND;
int length = (int)strlen(b); // 获取子串长度
int *arr = GetNext(b, length); // 构建回溯索引
int i = 0, j = 0;
while (a[i] && j < length) {
if (a[i] == b[j]) { // 匹配位置i和j的值相同,i和j后移
i++;
j++;
} else { // 匹配位置i和j的值不同
if (j != 0) { // j不等于0,j等于前一个字符的回溯索引,重新比较
j = arr[j-1];
} else { // j等于0,i后移
i++;
}
}
}
free(arr);
if (j == length) { // 如果循环结束时是因为子串到头了,说明匹配上了
return i - length; // 子串的开始位置为i减去匹配字符串的长度
}
return NOT_FOUND;
}
1.3.3 总结
这个算法其实就是著名的KMP算法。
核心思想:
若当前位置不匹配,当前位置前存在重复子串,则通过回溯可以跳过重复的子串继续匹配,而不是从头开始。
- 这里相比于思路2,同时跳过了主串和模式串成功匹配的部分。
- 1.3.2 匹配过程中的图示8~10很好地体现了这个优势。
不管是构建模式串的回溯数组还是匹配算法,其实两个函数的处理是非常相似的。
int *GetNext(char *pattern, size_t length) {
int *res = (int *)malloc(sizeof(int) * length);
res[0] = 0; // 第一个回溯索引为0
for (int i = 1, j = 0; i < length;) {
if (pattern[i] == pattern[j]) { // 匹配位置i和j的值相同,则res[i]等于j+1,i和j后移
res[i] = j + 1;
j++;
i++;
} else { // 匹配位置i和j的值不同
if (j != 0) { // j还不等于0,j等于前一个字符的回溯索引,重新比较
j = res[j-1];
} else {
res[i] = 0; // j等于0,则res[i]填入0,i后移
i++;
}
}
}
return res;
}
int FindStringInString3(char *a, char *b){
if (!*a || !*b) return NOT_FOUND;
int length = (int)strlen(b); // 获取子串长度
int *arr = GetNext(b, length); // 构建回溯索引
int i = 0, j = 0;
while (a[i] && j < length) {
if (a[i] == b[j]) { // 匹配位置i和j的值相同,i和j后移
i++;
j++;
} else { // 匹配位置i和j的值不同
if (j != 0) { // j不等于0,j等于前一个字符的回溯索引,重新比较
j = arr[j-1];
} else { // j等于0,i后移
i++;
}
}
}
free(arr);
if (j == length) { // 如果循环结束时是因为子串到头了,说明匹配上了
return i - length; // 子串的开始位置为i减去匹配字符串的长度
}
return NOT_FOUND;
}
int main(int argc, const char * argv[]) {
char *a = "abxabcabcaby";
char *b = "abcaby";
int idx = findStringInString3(a, b);
printf("%d\n", idx);
return 0;
}
2. RK算法
该算法有几点值得学习的地方:
- 把字符串比较问题,转换为了Hash值比较问题。
- 利用前一个Hash值计算结果,辅助计算下一个Hash值。(可以算是动态规划)
- 在比较的过程中,进行判断,同时解决Hash冲突问题。
2.1 字符串转Hash值
在十进制中一个数可以被表示为:
这里只考虑小写字母,我们有26个字母,所以字母的进制是二十六进制,'a'
的ASC码为97,如果直接用,很容易造成溢出问题,所以在计算Hash值时会减去'a'
。比如"abc"
就可以被表示为:
这里我们可以发现一个Hash冲突问题,比如"abc"
和"bc"
的Hash值是一样的,因为最高位是0
。
这里要解决Hash冲突有两个办法:
使用更优的Hash算法,比如我们不使用减去
'a'
,而是减去'a'-1
。(a
在最高位时不会为0
)在发生冲突的时候,我们根据字符的起点,比较一下两个字符串。
注意:Hash算法再牛逼,还是存在冲突的可能性。所以我们就算用了优化的算法,最好也使用方法2再比较一次。
2.2 利用前一个结果计算下一个Hash值
我们还是以十进制为例,主串6512
,第一项为651
,第二项为512
:
我们假设d
为使用的进制,m
表示匹配串的长度,那么字符串的Hash值我们可以通过下面的公式进行计算:
当然我们使用取余也是可以的:
RK算法实现:
#define NOT_FOUND -1
bool isMatch(char *mainStr, int i, char *matchStr, int m) {
for (int j = 0; j < m; j++) {
if (mainStr[i+j] != matchStr[j])
return false;
}
return true;
}
int RK(char *mainStr, char *matchStr) {
int d = 26; // 表示进制
int n = (int)strlen(mainStr); // 主串长度
int m = (int)strlen(matchStr); // 子串长度
unsigned int mainHash = 0; // 主串分解子串的哈希值
unsigned int matchHash = 0; // 模式串的哈希值
// 1.求得子串与主串中0~m字符串的哈希值[计算子串与主串0-m的哈希值]
// 循环[0,m)获取模式串A的HashValue以及主串第一个[0,m)的HashValue
int offset = 'a' - 1; // a为最小值,保证a是最高位时,不会为0
// 2.计算哈希值同时获取d^m-1值(因为经常要用d^m-1进制值)
int dm1 = 0;
for (int i = 0; i < m; i++) { // 小于m,不计算\0的值,数字会小一点
matchHash = (d * matchHash + (matchStr[i] - offset));
mainHash = (d * mainHash + (mainStr[i] - offset));
dm1 = dm1 > 0 ? dm1 * d : 1;
}
// 3.遍历比较哈希值
for (int i = 0; i <= n-m; i++) {
if (matchHash == mainHash // 判断模式串哈希值是否和其他子串的哈希值一致
&& isMatch(mainStr, i, matchStr, m)) // 哈希值相等后,再次用字符串进行比较,防止哈希值冲突
return i;
// 计算下一个子串的哈希值
mainHash = (mainHash - (mainStr[i] - offset) * dm1) * d + mainStr[i+m] - offset;
// mainHash = (mainHash % dm1) * d + mainStr[i+m] - offset; // 或者取余
}
return NOT_FOUND;
}
3. KMP改进
对于aaaaab这个匹配串,next数组为0 1 2 3 4 0,但实际如果发生不匹配,不需要回溯前一个a
,因为还是不匹配,应该直接回溯到头部。
这里修改一下KMP的实现,即现在next[j]对应的是当前不匹配时应该回溯的索引,而不是通过next[j-1]获取。同时头部回溯索引使用-1
,可以更方便判断是否回到了头部。
#define NOT_FOUND -1
int *GetNext(char *pattern, int length) {
int *next = (int *)malloc(sizeof(int) * length);
int i, j;
j = next[0] = -1;
i = 0;
while (i < length-1) {
// 当因为回溯j<0时,说明一直不匹配,回溯到了头部,当前字符没法匹配,所以i和j都进行后移
// j<0时,i++是为了开始下一个字符的比较(因为当前没法再回溯了,说明这个字符没法匹配)
// j<0时,j++是为了使得j=0来到匹配串的开头,开始下一轮的比较
if (j < 0 || pattern[j] == pattern[i]) {
i++;
j++;
// next[i] = j; // 未优化
// 如果当前匹配,i和j后移之后还匹配,可以使用之前j的回溯值(更快回溯到头部)
// 为什么要使用next[j],因为重复串回溯回去还是会不匹配,不如直接通过next[j]回溯到更前面
// 如果当前匹配,i和j后移之后不匹配,优化前的策略得到回溯值
next[i] = pattern[j] == pattern[i] ? next[j] : j;
} else {
// 当前不匹配时,通过next[j]进行回溯
j = next[j];
}
}
return next;
}
int FindStringInString(char *a, char *b) {
if (!*a || !*b) return NOT_FOUND;
int length = (int)strlen(b);
int *next = GetNext(b, length); // 获取next数组
int i, j;
i = j = 0;
while (a[i] && j < length) {
// 开始j = 0,所以直接比较字符是否相等
// 当因为回溯j<0时,说明一直不匹配,回溯到了头部,当前字符没法匹配,所以i和j都进行后移
// j<0时,i++是为了开始下一个字符的比较(因为当前没法再回溯了,说明这个字符没法匹配)
// j<0时,j++是为了使得j=0来到匹配串的开头,开始下一轮的比较
if (j < 0 || a[i] == b[j]) {
i++;
j++;
} else {
j = next[j];
}
}
free(next);
if (j == length) {
return i - length;
}
return NOT_FOUND;
}