zoukankan      html  css  js  c++  java
  • 从C++strStr到字符串匹配算法

    字符串的匹配先定义两个名词:模式串和文本串。我们的任务就是在文本串中找到模式串第一次出现的位置,如果找到就返回位置的下标,如果没有找到返回-1.其实这就是C++语言里面的一个函数:

    extern char *strstr(char *str1, const char *str2);
    

    对于这个函数的解释:

    str1: 被查找目标
    str2: 要查找对象
    返回值:如果str2是str1的子串,则返回str2在str1的首次出现的地址;
    		如果str2不是str1的子串,则返回NULL。
    例如:
    char str[]="1234xyz";
    char *str1=strstr(str,"34");
    cout << str1 << endl;
    显示的是: 34xyz
    

      返回值是一个指针,这个指针指向文本串中第一次出现模式串的位置。


     字符串查找的暴力算法

    先看LeetCode上的一道题目,实现这个函数 int strStr(string haystack, string needle); ,要求返回文本串中出现模式串的下标值。

    1.如果模式串为NULL,那么直接返回0.
    2.如果模式串的长度大于文本串,那么一定查找不到,返回-1.
    3.如果存在的话,查找的范围可以限定在文本串的0~s.size()-p.size();
    

    所以暴力算法的代码实现:

    int strStr(string haystack, string needle) 
    {
    	int i = 0;
    	//模式串为空
    	if(needle.empty())
    	{
    		return 0;
    	}
    	//文本串的大小小于模式串
    	if(haystack.size() < needle.size())
    	{
    		return -1;
    	}
    	//确定查找的范围
    	for(i = 0; i <= haystack.size()-needle.size(); ++i)
    	{
    		int j = 0;
    		for(j = 0; j < needle.size(); ++j)
    		{
    			if(haystack[i+j] != needle[j])
    			{
    				break;
    			}
    		}//for
    		if(j == needle.size())
    		{
    			return i;
    		}
    	}//for
    	if(i == haystack.size()-needle.size() + 1)
    	{
    		return -1;
    	}
    }
    

    字符串查找的KMP算法

    上面的暴力算法,在查找失败以后都要进行回溯,下面再给出一个版本,明显的看到i,j的回溯:

    int strStr(string haystack, string needle) 
    {
    	int sLen = haystack.size();
    	int pLen = needle.size();
    	
    	int i = 0;
    	int j = 0;
    	while(i < sLen && j < pLen)
    	{
    		if(haystack[i] == needle[j])
    		{
    			++i;
    			++j;
    		}
    		else
    		{
    			i = i - j + 1;
    			j = 0;
    		}
    	}//while
    	
    	if(j == pLen)
    	{
    		return i - j;
    	}
    	else
    	{
    		return -1;
    	}
    }
    

    假设我们已经知道了KMP的next数组,所以每次失配以后,i不回溯,j回溯到next[j]指定的位置。也就是 j = next[j]; 。

    int KmpSearch(char *s, char *p)
    {
        int i = 0;
        int j = 0;
        int sLen = strlen(s);
        int pLen = strlen(p);
     
        while(i < sLen && j < pLen)
        {
            //如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
            if(j == -1 || s[i] == p[j])
            {
                i++;
                j++;
            }
            else
            {
                //如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]     
                //next[j]即为j所对应的next值
                j = next[j];
            }
        }//while
     
        if(j == pLen)
        {
            return i - j;
        }
        else
        {
            return -1;
        }
    }
    

    对于上面的j=-1和next[0]=-1作下面的解释:

    说完了KMP的大的框架,下面就得说一下next数组的求解过程了:  

    void GetNext(char *p, int *next)
    {
        int pLen = strlen(p);
        int j = 0;
        int k = -1;
        next[0] = -1;
     
        while(j < pLen - 1)
        {
            //p[k]表示前缀,p[j]表示后缀
            if(k == -1 || p[j] == p[k])
            {
                ++k;
                ++j;
                next[j] = k;//表示在j这个字符之前,能够构成公共前后缀的最大字符数
            }
            else
            {
                k = next[k];//回溯之前已经有过匹配的前缀
            }
        }
    }
    

      

    KMP算法的一个改进  

    上面的KMP算法已经能够很好的跑出结果来了,但是还可以改进,看下面的一个字符串的匹配:

    改进的代码实现:

    void GetNextVal(char *p, int *next)
    {
        int pLen = strlen(p);
        int j = 0;
        int k = -1;
        next[0] = -1;
     
        while(j < pLen - 1)
        {
            //p[k]表示前缀,p[j]表示后缀
            if(k == -1 || p[j] == p[k])
            {
                ++k;
                ++j;
                if(p[j] != p[k])
                {
                    next[j] = k;//表示在j这个字符之前,能够构成公共前后缀的最大字符数
                }
                else
                {
                    next[j] = next[k];//因为不能出现p[j] = p[next[j]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
                }
                     
            }
            else
            {
                k = next[k];//回溯之前已经有过匹配的前缀
            }
        }
    }
    

    再来看一个极端的情况:

      

  • 相关阅读:
    C# LINQ和Lambda表达式详解
    .NET面试题2021.7.13
    linux每日命令(11):cat命令
    linux每日命令(10):touch命令
    linux每日命令(9):cp命令
    linux每日命令(8):mv命令
    linux每日命令(7):rmdir命令
    linux每日命令(5):mkdir命令
    进程和线程的区别?什么时候用进程?什么时候用线程?
    八种方式实现跨域请求
  • 原文地址:https://www.cnblogs.com/stemon/p/4851524.html
Copyright © 2011-2022 走看看