算法题-查找子串第一次出现和KMP算法学习

732 阅读7分钟

查找子串第一次出现

有主串S=“abcacabdc”,模式串T=“abd”,请查找出模式串在主串第一次出现的位置;提示:主串和模式串均为小写字母且都是合法输入。

1、暴力法

1.1 思路

  1. 遍历主串S
  2. 在主串遍历中,遍历子串T
  3. 主串遍历中的字符与子串字符比较,如果子串遍历正常退出,也就是从头到为遍历完成,说明找到了,返回主串的当前遍历到的pos
  4. 主串遍历完成,说明没找到

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 下标回溯

我们根据字符下标,通过字符内容,转化成一个回溯的下标数组,先看一下结果:蓝色部分就是回溯下标

我们找到子串c和主串d不匹配,找到子串c前一个字符b的回溯下标2,图片中的红色部分。
回溯下标2对应的下标元素d,图片中蓝色部分。
其实找的就是重复元素第一次出现的下一个字符。

3.1.3 下标回溯的创建

我们用图来了解一下如何创建下标回溯

  1. 定义i和j,初始的时候i=0,指向子串中第一个字符,j=1,指向子串中第二个字符,为了方便,子串我们用string[]代替
  2. 子串第一个字符a的回溯下标为0
  3. string[i]与string[j]不相等,而且i==0,那么string[b]的回溯下标也是0
  4. j向后移动,计算下一个字符d,与string[i]也就是a不相等,也赋值0
  5. j向后移动到3的位置,对应的字符是a,与i对应的字符相等,此时后面的a的回溯下标=第一个字符的下标+1,也就是0+1,注意是下标的值+1,而不是回溯下标。图片中蓝色的部分值+1
  6. 遇到字符相等后,i和j同时向后移动:string[i]=b,string[j]=b,字符相等,继续执行i+1的操作
  7. 字符不相等了,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 参考文档

KMP算法—终于全部弄懂了

kmp算法

4、RK算法

4.1 思路

S=“abcacabdc”,模式串T=“abd”

  1. 哈希算法,将字符转化成数字 字母共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)

  2. 将主串按照模式串的长度分割,

    主串: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

  1. 分割后的子串转换为第一步中的哈希值,与模式串的哈希值做对比。
  2. 如果相等,拿到子串与模式串再比较一次。目的是防止哈希冲突。

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;
}