数据结构与算法——字符串匹配算法

432 阅读5分钟

题目: 有一个主串S = {a, b, c, a, c, a, b, d, c}, 模式串T = { a, b, d } ; 请找到模式串在主串中第一次出现的位置;
提示: 不需要考虑字符串大小写问题, 字符均为小写字母;

1.BF算法

BF算法其实就是暴力解法
1.从头开始遍历主串 取出主串的第i个字符
2.如果S[i]!=T[0], 则继续取i+1 进行比较
3.如果S[i]==T[0], 则继续比较后面的字符S[i+j]和T[j]
4.如果直到j==strlen(T), 字符都相等 则说明匹配上了 返回位置i
5.如果直到i小于strlen(S)-strlen(T)+1,也就是S后续的字符长度比T的小了, 都没匹配上, 则返回-1;

int Index_BF(char *S, char *T,int pos){
    if (strlen(T)==0) {
        return -1;
    }
    
    for (int i=pos; i<strlen(S)-strlen(T)+1; i++) {
        if (S[i]==T[0]) {
            int flag = 1;
            for (int j = 1; j<strlen(T); j++) {
                if (S[i+j] != T[j]) {
                    flag = 0;
                    break;
                }
            }
            if (flag == 1) {
                return i;
            }
        }
    }
    return -1;
}

2.RK算法

如果两个字符串hash后的值不相同,则它们肯定不相同;如果它们hash后的值相同,它们不一定相同。
RK算法的基本思想就是:将模式串T的hash值跟主串S中的每一个长度为|T|的子串的hash值比较。如果不同,则它们肯定不相等;如果相同,为了避免冲突,再将子串与模式串的字符逐个比较,最终确认是否相同。

2.1 hash值的转换

数字的比较是单个值的比较,而字符的比较则需要逐个字符比较。如果我们能设计一个一个转换hash公式,将不同的字符组合,映射成不同的数字,这样字符串的比较就会简单很多。
我们知道 657 = 6 *10 * 10 + 5 * 10 + 7 * 1 数字657我们可以根据位数和进制进行拆解, 变成各个位数的累加值。进制为10。
那么我们也可以这样转换字符串abc: abc = a * 26 * 26 + b * 26 + c。进制为26。但是上面的a仍然是用字符表示的。
如何将a也转换成数字呢?用ascii码表示吗? 不可以。 因为如果使用ASC码时,计算数字过大。容易导致类型溢出。所以。我们要设计一个规则. 将字母-'a'的值作为该字母所对应的数字。 也就是:
a对应的值为a-a=0
b对应的值为b-a=1
c对应的值为c-a=2
... 这样一来,abc = 0 * 26 * 26 + 1 * 26 + 2 = 28

2.2 字符串的拆解求值

我们可以在比较之前, 做一次for循环,计算出主串的每个子串的hash值, 然后存入数组,在比较式取出。 这显然不是个好办法,如果中途就匹配到了,那后面的就白计算了。

我们还有另一种方法:

从上图我们可以看出,假设当已知dbc的值M时,再向后移一位求bce的的值时,可以利用M - d*26^(m-1) + e, 这个公式求得,m为子串的长度,这里是2,程序员应该都懂。

2.3 冲突解决

只要是hash,就有可能造成冲突问题。要想解决冲突可以设计更复杂的哈希公式。也可以再进行一次字符的挨个比较,进行最终确认;

3.代码实现


//d 表示进制
#define d 26

//4.为了杜绝哈希冲突. 当前发现模式串和子串的HashValue 是一样的时候.还是需要二次确认2个字符串是否相等.
int isMatch(char *S, int i, char *P, int m)
{
    int is, ip;
    for(is=i, ip=0; is != m && ip != m; is++, ip++)
        if(S[is] != P[ip])
            return 0;
    return 1;
}

//3.算出最d进制下的最高位
//d^(m-1)位的值;
int getMaxValue(int m){
    int h = 1;
    for(int i = 0;i < m - 1;i++){
        h = (h*d);
    }
    
    return h;
}

/*
 * 字符串匹配的RK算法
 * Author:Rabin & Karp
 * 若成功匹配返回主串中的偏移,否则返回-1
 */
int RK(char *S, char *P)
{
    //1. n:主串长度, m:子串长度
    int m  = (int) strlen(P);
    int n  = (int) strlen(S);
    printf("主串长度为:%d,子串长度为:%d\n",n,m);
    
    //A.模式串的哈希值; St.主串分解子串的哈希值;
    unsigned int A   = 0;
    unsigned int St  = 0;
    
    //2.求得子串与主串中0~m字符串的哈希值[计算子串与主串0-m的哈希值]
    //循环[0,m)获取模式串A的HashValue以及主串第一个[0,m)的HashValue
    for(int i = 0; i != m; i++){
        //第一次 A = 0*26+2;
        //第二次 A = 2*26+2;
        A = (d*A + (P[i] - 'a'));
        
        //第一次 st = 0*26+0
        //第二次 st = 0*26+1
        St = (d*St + (S[i] - 'a'));
    }
    
    //3. 获取d^m-1值(因为经常要用d^m-1进制值)
    int hValue = getMaxValue(m);
    
    //4.遍历[0,n-m], 判断模式串HashValue A是否和其他子串的HashValue 一致.
    //不一致则继续求得下一个HashValue
    //如果一致则进行二次确认判断,2个字符串是否真正相等.反正哈希值冲突导致错误
    for(int i = 0; i <= n-m; i++){
        if(A == St)
            if(isMatch(S,i,P,m))
                //加1原因,从1开始数
                return i+1;
        St = ((St - hValue*(S[i]-'a'))*d + (S[i+m]-'a'));
        
    }
    
    return -1;
}