字串查找算法总结及MS的strstr源码

来源:互联网 发布:java.util.map 编辑:程序博客网 时间:2024/05/25 13:34
首先来说说字串的查找,即就是在一个指定的字串A中查找一个指定字串B出现的位置或者统计其他B在A中出现的次数等等相关查找。
  MS自己提供了一个strstr函数原型:extern char *strstr(char *str1, char *str2);头文件<string.h>。但也可不包含头文件直接使用下面代码: 
View Code
复制代码
char * __cdecl strstr (        const char * str1,        const char * str2        ){        char *cp = (char *) str1;        char *s1, *s2;        if ( !*str2 )            return((char *)str1);        while (*cp)        {                s1 = cp;                s2 = (char *) str2;                while ( *s1 && *s2 && !(*s1-*s2) )                        s1++, s2++;                if (!*s2)                        return(cp);                cp++;        }        return(NULL);}
复制代码

  可直接将char 替换为 wchar_t用于宽字符。不过在字串的查找中效率是最低。
  KMP算法 详细介绍可参考下面:1、KMP算法   2、KMP算法详解
  其实KMP算法可对里面的数组然后会更优化一些。
  Sunday 是BM查找算法的变种,但是效率更好。下面摘自博客园园友Aga.J的文章:
  Sunday算法是一种比KMP和BM更加高效的匹配算法,它的思想跟BM算法相似,在匹配进行时,Sunday算法失败时关注的是源字符串中当前参加匹配的长度为M(模式串的长度)的最后一位字符的下一个字符。如果该字符不在模式串中出现,那么就将直接跳过,即移动步长=模式串的长度+1,否则,则移动步长=模式串中最右端的该字符到末尾的距离加1。
  
假设我们要匹配”HEREISASIMPLEEXAMPLE”和”EXAMPLE”
      Text:HERE  IS  A  SIMPLE EXAMPLE(此处中间是一个空格,用两个空格是为了对齐.下同)
  Pattern:EXAMPLE

  我们从源串的初始位置开始和模式串比较,发现第一个就不匹配了,这时候如果使用朴素算法,那么我们就只是将模式串右移一位,而使用KMP算法的话,则根据自身的模式数组来确定移动的步长,使用Sunday算法时,我们会比较源串对齐后的下一个字符,也就是text中IS后面的空格,因为无论我们用什么方法去移动模式串,这个字符总是要参与下一次匹配(假设我们只移动1位或者小于模式串长度位,这个空格所在的位一定要参与匹配,不然我们就可能遗漏掉可能的匹配,而假设在小于模式串长度的位的移动中没有匹配,那么下一个起始匹配点就是空格的所在位)。
  既然知道该为一定要匹配,那么就和模式串进行比较,如果模式串中不存在该字符(这里是空格),那么就直接跳到空格字符的下一个字符进行匹配(这时候从E和A开始匹配)
      Text:HERE  IS  A  SIMPLE  EXAMPLE
  Pattern:EXAMPLE
  这时候再次进行匹配的判断,发现第一个字符又不匹配,所以和上面的方法一样,我们直接看对齐后的下一个字符E,这时候拿E从右往左和模式串比较,如果模式串中存在E,那么就移动模式串知道两个E对齐,这样得到(这种从后往前,从右往左的匹配思想是从BM算法借鉴过来的,从后往前比较的优点是一旦遇到不匹配的时候,可以跳跃的距离更大,因为后面都不匹配了,前面再匹配都没有用了,所以这里拿E从模式串中从右往左匹配,找到第一个和E匹配的位置—详细看BM算法的分析!)
      Text:HERE  IS  A  SIMPLE  EXAMPLE
  Pattern:EXAMPLE
  接下来还是从模式串的首位和源串的新起始位开始比较,发现,又无法匹配,而再判断对齐的后一位,这次还是将整个模式串移动到新的不匹配位空格之后,最后完成匹配。
上述例子是从网上摘录下来的,不是很典型,因为在第二次匹配的时候,刚好源串对齐后的下一位就和模式串的最后一位匹配,所以体现不出Sunday算法的模式串移动的过程。
 

View Code
复制代码
/** 算法分析:* 1 从第一个字符开始,对pattern和source逐个字符比较* 2 如果出现匹配失败,则检查source的在pattern末尾位置的后一个字符是否和pattenr的某个一个字符相等*    如果是,则对其到那个相等字符(从右往左对齐),重新执行*    如果不是,则将pattern和source的比较初始位设置在source的后后个字符,再执行* 3 如果匹配,则成功*/#include<iostream>using namespace std;// 调用前先检测源串是否超过长度,函数返回 以源串的sourceStartPos为比较的起始点,第一个和模式串不匹配的字符的位置,int compare(char* source, char* pattern, int sourceStartPos, int patternLength){    int i = 0;    for(; ((i < patternLength) && (source[sourceStartPos+i] == pattern[i])); i++)    {        ;    }    return i;    // 返回pattern中无法匹配的元素的位置i,pattern[i]}bool sundayMatch(char* source, char* pattern, int sourceLength, int patternLength){    int startPos = 0;            // 源串的起始比较点    int failMatchPos = 0;        // 失效点    int j = 0;    while( (startPos+patternLength -1) <= sourceLength) // 源串中剩下的子串的长度比模式串短,即还可以继续比较    {        failMatchPos=compare(source,pattern,startPos,patternLength);    // 获得首次失配的字符在源串中的位置        cout<<"failMatchPos:"<<failMatchPos<<" ";        if( failMatchPos == patternLength)                // 如果刚好和模式串一样长,即匹配成功            return true;        else        {            for( j = patternLength-1; j >= 0; j--)        // 查看源串当前匹配子串的下一个字符是否和模式串中的任意一个字符匹配,注意是从右往左查找                if( source[startPos+patternLength] == pattern[j])                {                    startPos += patternLength - j;                    break;        // 一旦存在,则初始化下一个起始匹配点,即上述所谓的对齐                }                if(j < 0)        // 如果不存在,则跳过                    startPos = startPos+patternLength + 1;        }        cout<<"newStartPos"<<startPos<<endl;    }    return false;}void main(){    char *s1 = "THIS IS A EXAMPLE";        // HERE_IS_A_SIMPLE_EXAMPLE 24    char *s2 = "EXAMPLE";    bool result = sundayMatch(s1, s2, 17, 7);    if( result )        cout<<"yes";    int i = 0;    cin>>i;}
复制代码

  网络上的实现方法是这样的,先做一个预处理,针对子串中每个出现的字符,保存源串对齐后的下一位一旦出现匹配或者不匹配所需要移动的距离,然后在匹配过程中就可以直接使用,不需要像我写的方法那样重复的判断某个位的字符是否和模式串中出现及其出现位置是哪里。(这种方法花费了空间但赢得了时间)
  

View Code
复制代码
/* 采用BM/KMP的预处理的做法,事先计算好移动步长,等到遇到不匹配的值直接使用 */#include <iostream>#include <string.h>using namespace std;#define MAX_CHAR_SIZE 256    //一个字符8位 最大256种/** 设定每个字符最右移动步长,保存每个字符的移动步长* 如果大串中匹配字符的右侧一个字符没在子串中,大串移动步长=整个串的距离+1* 如果大串中匹配范围内的右侧一个字符在子串中,大串移动距离=子串长度-这个字符在子串中的位置*/int *setCharStep(char *subStr){    int *charStep = new int[MAX_CHAR_SIZE];        // 代码没有注意安全:)    int subStrLen = strlen(subStr);    for(int i = 0; i < MAX_CHAR_SIZE; i++)        charStep[i] = subStrLen + 1;    // 如果大串中匹配字符的右侧一个字符没在子串中,大串移动步长=整个串的距离+1    // 从左向右扫描一遍 保存子串中每个字符所需移动步长    for(int j = 0; j < subStrLen; j++)    {        charStep[(unsigned char)subStr[i]] = subStrLen - i;        // 如果大串中匹配范围内的右侧一个字符在子串中,大串移动距离=子串长度-这个字符在子串中的位置    }    return charStep;}/** 算法核心思想,从左向右匹配,遇到不匹配的看大串中匹配范围之外的右侧第一个字符在小串中的最右位置* 根据事先计算好的移动步长移动大串指针,直到匹配*/int sundaySearch(char* mainStr,char* subStr,int* charStep){    int mainStrLen = strlen(mainStr);    int subStrLen = strlen(subStr);    int main_i = 0;    int sub_j = 0;    while (main_i < mainStrLen)    {        int tem = main_i;    // 保存大串每次开始匹配的起始位置,便于移动指针        while(sub_j < subStrLen)        {            if(mainStr[main_i] == subStr[sub_j])            {                main_i++;                sub_j++;                continue;            }            else{    //  如果匹配范围外已经找不到右侧第一个字符,则匹配失败                if( (tem + subStrLen) > mainStrLen)                    return -1;                // 否则,移动步长,重新匹配                char firstRightChar = mainStr[tem+subStrLen];                main_i = tem+charStep[(unsigned char)firstRightChar];                sub_j = 0;                break; // 退出本次失败匹配 重新一轮匹配            }        }        if(sub_j == subStrLen)            return (main_i - subStrLen);    }    return -1;}int main(){    char* mainStr = "absaddsasfasdfasdf";    char* subStr = "dd";    int* charStep = setCharStep(subStr);    cout<<"位置:"<<sundaySearch(mainStr,subStr,charStep)<<endl;    system("pause");    return 0;}
复制代码

【参考资料 感谢作者】
以上部分摘自: 字符串匹配算法之Sunday算法的学习笔记  

另外例举几篇关于字串查找的文章:
1、KMP算法 
2、KMP 算法并非字符串查找的优化 [转]
3、精确字符串匹配(BM算法) [转] 
4、sunday算法简介 

原创粉丝点击