美文网首页
《大话数据结构》之串

《大话数据结构》之串

作者: 我才是臭吉吉 | 来源:发表于2019-03-17 18:05 被阅读0次

    1. 朴素的字符串匹配方法

    1.1 比较过程

    以主串T为基础,根据索引i对每个字符进行遍历,依次与目标串P的每个索引j的字符进行匹配。若字符相同,则两索引均后移,继续匹配;若不相同,则j恢复为0,从头开始,i回溯到开始成功匹配时索引的下一位。

    1.2 比较结果

    遍历索引i结束之后,若此时j已经达到目标串P的最后,则证明目标串的所有字符均匹配成功,目标串在主串中的位置即length(T) - j;否则匹配失败,P不是T的子串。

    1.3 主要实现

    NSInteger findIndex(const char *sourceStr, const char *targetStr) {
        // 获取二者长度
        NSInteger sourceLength = strlen(sourceStr);
        NSInteger targetLength = strlen(targetStr);
        
        NSInteger i = 0; // 主串索引
        NSInteger j = 0; // 目标串索引
        
        while ((i < sourceLength) && (j < targetLength)) {
            if (sourceStr[i] == targetStr[j]) {
                // 字符相同,继续向后
                i += 1;
                j += 1;
            } else {
                // 字符不同
                // 主串索引恢复到开始成功匹配时的下一位
                i = i - j + 1;
                // 目标串索引恢复到头
                j = 0;
            }
        }
        
        // 注:这里的大于只是防止最后一位匹配成功后的+1操作
        if (j >= targetLength) {
            // 主串的最终遍历索引 前移 目标串长度,即为子串的起始位置
            return i - targetLength;
        }
        
        return NSNotFound;
    }
    

    2. KMP的字符串匹配方法

    KMP方法的作用主要是去掉了一些可避免的比较,提高了匹配效率。

    2.1 匹配过程

    KMP的主要匹配过程与朴素一致,都是字符依次比较。优化点是在字符匹配失败时,尽量让主串T的索引i不回溯,目标串的索引j回溯到需要的位置(而不是直接到0)。从而达到减少比较次数的目的。

    如上所述,KMP的核心就是在任意位置匹配失败时,目标串的索引j回溯到的位置

    我们将所有回溯位置的信息保存到数组next中,故最终目的就是计算出next数组的完整信息。

    2.1.1 匹配表(Partial Match Table)

    要说next数组,就先要了解匹配表PMT的含义。以字符串“abababca”为例,其匹配表如下:

    image

    其中,value行即为PMT的值。其意义是什么呢?

    value的值是子串中前缀与后缀集合中,最长的公共子串的长度。例如,下表分别为当index为2和5时,value的得出过程:

    索引 子串 前缀集合 后缀集合 交集 最长子串长度
    2 “aba” {“a”,“ab”} {“ba”,“a”} {“a”} 1
    5 “ababab” {“a”,“ab”,“aba”,“abab”,“ababa”} {“b”,“ab”,“bab”,“abab”,“babab”} {“ab”,“abab”} 4

    故目标字符串有几位,就会有几个PMT中对应的值。

    2.1.2 next表(PMT表的修改形式)

    先抛图(主串“ababababca”和子串“abababca”进行匹配的过程之一)。

    image

    如图所示,当在主串的i处匹配失败,其之前长度为6的字符串“ababab”均已匹配成功。此时,由PMT表我们可知,子串“ababab”的value为4,即其四位前缀与四位后缀是一致的。又因为在主串中,i之前的四位与j之前的四位字符均一致。故,目标串的四位前缀与主串中i之前的四位字符一致。我们可以省略对该子串的比较,直接在目标串的j=4的位置与主串的当前位置i继续比较。

    在上面是叙述中,我们发现,匹配失败,并不会导致主串索引i改变,只会影响目标串索引j出现变化。并且,有趣的是,以图中的过程为例,当j=6时,匹配失败,即最后一个成功匹配的j是5。之后j回溯的位置4正好是PMT表中index为5时的value值。根据这个原理,我们就可以跳过不需要的比较,只通过修改目标串索引的位置即可进行快速匹配。

    image

    那么,我们引入next表的目的是什么呢?由于在某一位置匹配失败时,都要查看其前一个位置对应的PMT值,故我们将PMT数据均偏移一位,直接对应于当前位置,以便在快速查找下一步时,可以直接得到目标串索引j的位置。因此,我们把PMT值偏移后的数据叫做next数据,为了补位,next的首位赋值为-1。

    因此,有了next的方式,我们就可以对朴素匹配法进行优化了。

    2.2 匹配过程

    NSInteger KMPFindIndex(const char *sourceStr, const char *targetStr) {
        // 获取目标串的next信息,以备匹配时进行回溯(KMP算法核心)
        NSArray *nextInfo = getNextInfo(targetStr);
        NSLog(@"next - %@", nextInfo);
        
        // 获取两串长度
        NSInteger sourceLength = strlen(sourceStr);
        NSInteger targetLength = strlen(targetStr);
        
        // 主串索引
        NSInteger i = 0;
        // 目标串索引
        NSInteger j = 0;
        
        // 均在范围内查找
        while ((i < sourceLength)
               && (j < targetLength)) {
            if ((j == -1) 
                || (sourceStr[i] == targetStr[j])) {
                    // 字符匹配时,索引均后移,准备下一次匹配(j为-1即为了防止出现next[0] = -1的情况出现)
                    i += 1;
                    j += 1;
                } else {
                    // 字符不匹配,i不变,j回溯为next中对应的位置,作为下次匹配的起点
                    j = [nextInfo[j] integerValue];
                }
        }
        
        if (j == tragetLength) {
            return i - j;
        }
        
        return NSNotFound;
    }
    

    2.3 生成Next数组的过程

    我们知道,生成Next数据的过程,即在每一个位置上,获取前面子串里,前后缀集合中最长的公共子串的长度

    如目标字符串“ABCDABDE”,我们以k作为前缀索引,j为后缀索引。如下图,当在字符索引在“D”时(即j为6时),其子串“ABCDAB”的最长前后缀公共子串长度为2(子串“AB”的长度),故next[6] = 2。我们还可知,在j为5时,next[5] = 1,即在j = 5时,公共子串最大长度为1(子串“A”的长度)。故我们可以得到:在当前字符存在有效next值时(Pk == Pj时),下一个字符的next即为之前next位置的下一个,即next[j + 1] = next[j] + 1 = k + 1

    image

    若Pk与Pj不相等,如图中k为2,j为6时。按照KMP的思想,后缀j不变,需要更新前缀k,以便找到另一个字符与Pj相同。故将前缀索引k修改为next[k]以继续进行比较(缩小前缀查找范围),直到找到相同的字符“D”或没有相同字符为止。

    按照这个思想,生成next数据的实现为:

    NSArray *getNextInfo(const char *targetStr) {
        // 创建返回的next数组
        NSMutableArray *nextInfo = [[NSMutableArray alloc] initWithCapacity:10];
        nextInfo[0] = @(-1); // 人为指定首位的next值为-1(首位无法回溯,故此值只是占位用)
        
        // 获取字符串长度
        NSInteger strLength = strLen(targetStr);
        
        NSInteger k = -1; // 前缀索引初始值
        NSInteger j = 0; // 后缀索引初始值(后缀必须大于前缀才有效)
        
        // 以后缀索引作为遍历范围(-1为了防止最后无效的自加1)
        while (j < strLength - 1) {
            if ((k == -1)
                || (targetStr[k] == targetStr[j])) {
                    // 前缀为初值索引时,无条件后移并记录(即没有有效前后缀公共串)
                    // 当前位置的字符相同,即找到了公共前后缀字符串,记录下一个位置的next值
                    
                    // 下一位置的next值即为当前next的索引值k再加1
                    k += 1;
                    j += 1;
                    
                    nextInfo[j] = @(k);
                } else {
                    // 当前位置字符不同
                    // 按照KMP思想,j不回溯,k回溯为下一次的查找索引
                    k = [next[k] integerValue];
                }
        }
        
        return [nextInfo copy];
    } 
    

    Objective-C实现的代码地址:点击查看

    2.4 Next数组的优化

    我们设想一下这种情况:目标串为“abcabc”,主串为“abacababc”,其中一种比较情况如下图:

    img

    即此时,第四位比较失败(“c”与“b”不同)。我们知道,此时目标串下一次比较的索引j为next[j],即第二位。也就是目标串右移j-next[j]位(2位)。

    img

    如图所示,由于移动后,第二位的字符仍然为“b”,显然比较失败。在这种情况下,本次比较实际上是没有意义的。那么应该如何避免这种比较呢?

    其实,在正常情况下,当匹配失败时,我们还会进一步缩小匹配范围(j = next[j]),故在这种情况下(next[j]指向的字符与当前字符相同时),我们只需再次递归next[j],跳过相同字符即可

    因此,解决办法就是,在生成next数组时,在当前Pk和Pj相同的情况下,若下一位的j与k指向的字符相同,直接设置该位置的next为next[k]即可

    NSArray *getNextInfo(const char *targetStr) {
        NSMutableArray *nextInfo = [[NSMutableArray alloc] initWithCapacity:10];
        nextInfo[0] = @(-1); 
        
        NSInteger strLength = strLen(targetStr);
        
        NSInteger k = -1; // 前缀索引初始值
        NSInteger j = 0; // 后缀索引初始值(后缀必须大于前缀才有效)
        
        // 以后缀索引作为遍历范围
        while (j < strLength - 1) {
            if ((k == -1)
                || (targetStr[k] == targetStr[j])) {
                    // 前缀为初值索引时,无条件后移并记录(即没有有效前后缀公共串)
                    // 当前位置的字符相同,即找到了公共前后缀字符串,记录下一个位置的next值
                    
                    k += 1;
                    j += 1;
                    
                    // nextInfo[j] = @(k); // 修改
                    
                    if (targetStr[k] == targetStr[j]) {
                        // 下一位的前后缀字符相同
                        nextInfo[j] = @(nextInfo[k]); // next跳过当前的前缀位置,直接指向前缀位置的next位置
                    } else {
                        // 下一位的前后缀字符不同(正常情况)
                        nextInfo[j] = @(k); // 原始匹配,next指向前缀后面的位置
                    }
                    
                    
                } else {
                    // 当前位置字符不同
                    k = [next[k] integerValue];
                }
        }
        
        return [nextInfo copy];
    } 
    

    最终,优化后的next数组信息如下表所示(以目标串“abab”为例):

    img

    使用优化后的next数组,可以再次减少一些不必要的比较。

    2.5 时间复杂度

    KMP方式的运算次数为计算next数组的次数匹配主串的次数之和,即O(m + n)。

    参考引用:

    相关文章

      网友评论

          本文标题:《大话数据结构》之串

          本文链接:https://www.haomeiwen.com/subject/dsnumqtx.html