查找子串第一次出现
有主串S=“abcacabdc”,模式串T=“abd”,请查找出模式串在主串第一次出现的位置;提示:主串和模式串均为小写字母且都是合法输入。
1、暴力法
1.1 思路
- 遍历主串S
- 在主串遍历中,遍历子串T
- 主串遍历中的字符与子串字符比较,如果子串遍历正常退出,也就是从头到为遍历完成,说明找到了,返回主串的当前遍历到的pos
- 主串遍历完成,说明没找到
1.2 代码
int lookupSubStringInMainStringPosIndex1(char *S, char *T) {
//主串开始字符
int i = 0;
//遍历主串
while (S[i]) {
int j = 0;
//遍历子串
while (T[j]) {
if (S[i + j] == T[j]) {//主串字符和子串字符相等
j++;
} else {//不等跳出子串遍历
break;
}
}
if (!T[j]) {//子串到结尾,说明找到了,直接返回
return i;
}
//当前主串的开始字符不匹配子串,找下一个主串的字符为开始点,再次查找
i++;
}
return -1;
}
1.3 运行
主串:abcacabdc 子串:abd
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, 查找子串!\n");
int pos = lookupSubStringInMainStringPosIndex1("abcacabdc", "abd");
printf("%d\n", pos);
return 0;
}
1.4 可以优化的点
- 主串"abcacabdc"遍历;
- 当匹配到abc的c时,不匹配;
- 主串遍历下次获取的是b,不匹配;
- 再下次是c,不匹配;
- 再到a,匹配上了;
- ......
其实b和c的判断是可以跳过的。接下来我们来研究研究跳跃的方式。
2、跳跃方式
2.1 思路
- 发现不匹配的字符
- 判断是否不匹配的值和子串第一个相等:如果相等,主串的i = i + j,子串的j = 0;
- 不等还是走原来的逻辑。
2.2 代码
int lookupSubStringInMainStringPosIndex2(char *S, char *T) {
int i = 0;
while (S[i]) {
int j = 0;
while (T[j]) {
if (S[i + j] == T[j]) {
j++;
} else {
if (S[i + j] == T[0]) {//跳跃点
i = i + j;
j = 0;
} else {
break;
}
}
}
if (!T[j]) {
return i;
}
i++;
}
return -1;
}
2.3 运行
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, 查找子串!\n");
int pos = lookupSubStringInMainStringPosIndex2("abcacabdc", "abd");
printf("%d\n", pos);
return 0;
}
运行结果和上面的方式相同,大家可以用这个代码调试跟踪一下。
2.4 还可以再优化
用跳跃的方式解决了主串中有重复字符情况。那如果子串中有重复的呢?
例如:主串“abcabdabdabc”,子串“abdabc”。主串中加粗的部分是查找的结果。接下来,讲讲方式3,子串回溯,也是KMP算法。
3、子串回溯
3.1 思路
3.1.1 分析处理子串
当我们主串“abc abdab d abc”匹配到“d”的时候,子串中“abdab c”是“c”。方式2中,我们是将子串从头重新开始匹配。
其实我们用眼睛一看就知道,会做一些没必要的比较。可以直接用子串“ab d abc”中的“d”开始比较。
这是我们人脑的思维方式,但是我们怎么让计算机知道要从哪开始比较呢?或者说,我们怎么来写这个代码呢?
我们用眼睛直接能找到位置,计算机如何找到指定位置呢?答案就是使用数组下标。
3.1.2 下标回溯
我们根据字符下标,通过字符内容,转化成一个回溯的下标数组,先看一下结果:蓝色部分就是回溯下标
3.1.3 下标回溯的创建
我们用图来了解一下如何创建下标回溯
- 定义i和j,初始的时候i=0,指向子串中第一个字符,j=1,指向子串中第二个字符,为了方便,子串我们用string[]代替
- 子串第一个字符a的回溯下标为0
- string[i]与string[j]不相等,而且i==0,那么string[b]的回溯下标也是0
- j向后移动,计算下一个字符d,与string[i]也就是a不相等,也赋值0
- j向后移动到3的位置,对应的字符是a,与i对应的字符相等,此时后面的a的回溯下标=第一个字符的下标+1,也就是0+1,注意是下标的值+1,而不是回溯下标。图片中蓝色的部分值+1
- 遇到字符相等后,i和j同时向后移动:string[i]=b,string[j]=b,字符相等,继续执行i+1的操作
- 字符不相等了,i向前移动找前一个字符,如果找到了=0,说明前面没有重复的字符,就给j的回溯下标设为0
3.1.4 代码实现
int *backtrackingArr(char *string) {
int len = (int)strlen(string);
int *arr = (int*)malloc(sizeof(int) * len);
arr[0] = 0;
int i = 0;
int j = 1;
for (j = 1; j < len;) {
if (string[j] == string[i]) {//字符相等
arr[j] = i + 1;
i++;
j++;
} else {//不等
if (i > 0) {
i = arr[i - 1];
} else {
arr[j] = 0;
j++;
}
}
}
return arr;
}
3.2 主串和子串对比
直接上图
3.2.1 主函数实现
int lookupSubStringInMainStringPosIndex3(char *S, char *T) {
int i = 0;//控制主串
int j = 0;//控制zich
//获取到子串的回溯下标数组
int *arr = backtrackingArr(T);
int len = (int)strlen(T);
//主串不到结尾并且j小于子串的长度
while (S[i] && j < len) {
if (S[i] == T[j]) {//如果相等,主串和子串都往后走
i++;
j++;
} else {//不等
if (j > 0) {
j = arr[j - 1];//从回溯下标数组获取要回溯的位置
} else {
i++;
}
}
}
free(arr);
if (!T[j]) {
//主串中到达的位置减去子串的长度,就是子串在主串中的位置
return i - len;
} else {
return -1;
}
}
3.3 KMP算法
其实这种实现方式就是KMP算法
3.1 KMP简介
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
3.2 参考文档
4、RK算法
4.1 思路
S=“abcacabdc”,模式串T=“abd”
-
哈希算法,将字符转化成数字 字母共26位,那么就将字符转换成一套26进制数。例如模式串“abd”,转换后:('a' - 'a') * 26^2 + ('b'-'a')*26^1 + ('d'-'a')*26^0。 最高位的幂数其实就是模式串的长度-1,通用公式:m = strlen(T), ('a' - 'a') * 26^(m-1) + ('b'-'a')*26^(m-2) + ('d'-'a')*26^(m-3)
-
将主串按照模式串的长度分割,
主串:abcacabdc,模式串:abd
abc bca cac aca cab abd dbc
-
小技巧1:没必要开始时候,把所有的子串截取出来,截取一个比较一个。
-
小技巧2:从第一个abc切换到第二个bca时,abc去掉最高位,其他位置*26,再加上第二个串的最后一位就可以了。
例如:abc:('a' - 'a') * 26^2 + ('b'-'a')*26^1 + ('c'-'a')26^0 去掉第一个字符a 再26 ('b'-'a')*26^1 * 26 + ('c'-'a')*26^0 *26
再加上第二个字符的a ('b'-'a')*26^2 + ('c'-'a')*26^1 + ('a'-'a')*26^0
- 分割后的子串转换为第一步中的哈希值,与模式串的哈希值做对比。
- 如果相等,拿到子串与模式串再比较一次。目的是防止哈希冲突。
4.2 代码
#define CarryOver 26
int lookupSubStringInMainStringPosIndex4(char *S, char *T) {
//获取两个串的长度
int T_len = (int)strlen(T);
int S_len = (int)strlen(S);
//模式串的哈希值
unsigned int numT = 0;
//主串的子串哈希值,子串的长度取决于模式串的长度
unsigned int numS = 0;
//最高进位,26^(T_len - 1)
int high = 0;
for (int i = 0; i< T_len; i ++) {
numT = numT * CarryOver + T[i] - 'a';
numS = numS * CarryOver + S[i] - 'a';
if (i == 0) {
high = 1;
} else {
high = high * CarryOver;
}
}
for (int i = 0; i < S_len - T_len; i ++) {
if (numS == numT) {
//找到哈希相等后,进行字符串比较,防止哈希值冲突
int j = 0;
while (T[j]) {
if (S[i + j] != T[j]) {
break;
}
j++;
}
if (j == T_len) {
return i;
}
} else {
//哈希不等,主串中获取下一个子串,减掉最高位的值,乘以进制,加上下一个字符
numS = (numS - high * (S[i] - 'a'))*CarryOver + S[i + T_len] - 'a';
}
}
return -1;
}
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, 查找子串!\n");
int pos = lookupSubStringInMainStringPosIndex4("abcacabdc", "abd");
printf("%d\n", pos);
return 0;
}