实现strStr()的三种方法

strStr()函数的定义:

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

现在我们来介绍一下实现这个函数的三种方法(采用的语言为C++)

对不齐可不能怨我哦,写的时候是对齐的。。。

文章目录

暴力解法

暴力解法也就是所说的移动窗口,将needle的0下标与haystack的0下标对齐,然后一一对比每一个字符,如果有一个字符不匹配,就把needle的0下标与haystack的1下标对齐,接着进行一一对比。。。。。。以此类推(注意不要越界),直到needle的所有字符都与haystack里的字符对应,就返回此时needle的0下标对应的haystack下标;如果,到最后也没有匹配成功,就返回-1

    int strStr(string haystack, string needle) 
    {
        if (needle.empty())return 0;
        for (int i = 0; i <= (int)(haystack.size()-needle.size()); i++)
        {   
            bool flag = true;
            for (size_t j = 0; j < needle.size(); j++)
                if (haystack[i + j] != needle[j])
                {
                    flag = false;
                    break;
                }
            if (flag)return i;
        }
        return -1;
    } 

由于c++的string类都以’\0’结尾,恰好符合这个算法,所以

for (int i = 0; i <= (int)(haystack.size()-needle.size()); i++)

可以改写为

for (size_t i = 0; i <= haystack.size(); i++)

不理解的可以自行推导一下

时间复杂度O(mn),m是haystack的长度,n是needle的长度

空间复杂度O(1),没有使用额外的空间

KMP算法

看完暴力解法,不知道有没有发现一个很有意思的点,如果

haystack是"aaaaaaaaaaaaaab"

needle是"aaaab"

这时候我们再用暴力解法就显得很傻*,因为进行了很多没有意义的操作,KMP算法就是来解决这个问题的,对于needle字符串,如果存在多个重复的字符或者字符组也就是所谓的公共前后缀

公共前后缀

“abca” --> “a”

“abhab” --> “ab”

对于一个字符串"aaaab",这里看的并不单单是整个字符串的公共前后缀,而是前string.size()个字符的公共前后缀

公共前后缀是不包括自身全部的。不要因为这个看不懂下面的

“a” --> “” 前一个

“aa” --> “a” 前两个

“aaa” --> “aa” 前三个

“aaaa” --> “aaa” 前四个

”aaaab“ --> “” 前五个

是不是不知道这个有什么用,别急,这个就是KMP算法的核心,***通过构造回溯数组***来优化暴力解法

现在来构造回溯数组

    vector<int> makeNext(string needle)
    {
        int pre = -1,
            rev = 0;
        vector<int> arrayNext(1, -1);
        while (rev < (int)needle.size())
        {
            if (0 > pre || needle[pre] == needle[rev])
            {
                pre++;
                rev++;
                arrayNext.push_back(pre);
            }
            else
                pre = arrayNext[pre];
        }
        return arrayNext;
    }
字符串			"a  a  a  a  b"
回溯数组    -1  0  1  2  3  0			(这里的-1是为了运算而添加的)

回溯数组的下标对应的就是前n个字符的公共前后缀,有兴趣的话可以看一下回溯数组的构造代码,自己推敲一遍

现在,让我们回到最初的问题

haystack是 “aaaaaaaaaaaaaab”

needle是 “aaaab”

现在开始一一匹配


aaaaaaaaaaaaaab

aaaab

匹配到这里的时候出现字符不匹配的情况,此时b的下标是4,于是在回溯数组里找到对应下标的数据为"3",转换到第四个字符

aaaaaaaaaaaaaab

aaaab

此时匹配,进行下一个字符比较

aaaaaaaaaaaaaab

aaaab

此时又出现不匹配情况,接着跳转

aaaaaaaaaaaaaab

​ aaaab

。。。。。。

以此类推到最后

aaaaaaaaaaaaaab

​ aaaab

全部匹配,结束

时间复杂度为O(n)

空间复杂度为O(1)


现在是不是还没有明白,下面再举一个例子

haystack ”abcabcabcp“

needle “abcabcp”

回溯数组为[-1, 0, 0, 0, 1, 2, 3, 0]

可以很容易的看出,正确的匹配在最后,现在开始一一匹配


abcabcabcp

abcabcp

匹配到这里出现不匹配情况,但是,”p“之前的”abcabc“都是匹配的且公共前后缀是3,那就可以把第二个”abc“当作开头,直接跳到

abcabcabcp

​ abcabcp

然后再一路匹配下去


    int strStr(string haystack, string needle)
    {
        if (needle.empty())return 0;
        vector<int> arrayNext = makeNext(needle);
        int i = 0,
            j = 0;
        while (i < (int)haystack.size() && j < (int)needle.size())
        {
            if (j < 0 || haystack[i] == needle[j])
            {
                i++;
                j++;
            }
            else
                j = arrayNext[j];
        }
        return j == needle.size() ? i - needle.size() : -1;
    }

时间复杂度O(n)(最好情况),O(mn)(最坏情况)

空间复杂度O(n)构造了一个和needle一样大小的回溯数组

Rabin Karp算法

其实kmp算法在乱序情况下和暴力解法相差不大,局限性很大,但是Rabin Karp算法(以下称为R算法),是真正的封神算法

haystack ”abcabcabcp“

needle ”abcp“

因为我们的搜索基本上包含的字符都属于ASCII表里的内容,而大多数情况下是a~z的26个小写字母,这里为了简单,就假定字符串只包含26个小写英文字母(如果有兴趣的话可以我的基础上进行拓展)。

对于needle字符串,”abcp“,转化为[0, 1, 2, 15],转换原理就是把每个字符对应的ASCII值减去”a“的ASCII值也就是97。

long long a = 0*pow(26,3) + 1*pow(26,2) + 2*pow(26,1) + 15*pow(26,0)

此时a的值就是needle这个字符串的自定哈希值

使用这个步骤来计算haystack前4个字符的哈希值,与a进行对比,如果相等则返回下标,如果不相等,则计算第二个字符到第5个字符这四个字符的哈希值,可能会有人会问,那这个时间复杂度岂不是还是很高,对,如果这样的话,和暴力解法就一样了,但是,如果细心一点就可以发现,第一个4字符哈希值和第二个4字符哈希值之间存在联系。

设第一个4字符哈希值 a = 10

设第二个4字符哈希值 b

b = a * 26 - haystack[0] * pow(26,4) + haystack[4]

通过这样就可以进行复杂度为1的下一个哈希值运算

但是又会有一个新的问题,就是数据太大,long long已经装不下,这个在python里面还好不需要考虑,但是在c,java里是必须考虑的,那就对哈希值取模(余)呗,理论上,除数越大,越不容易出现两个字符串哈希值相同的情况,这里取2的63次方

    int strStr(string haystack, string needle)
    {
        if (needle.empty())return 0;
        if (needle.size() > haystack.size())return -1;
        long long modulus = (long long)pow(2, 63);
        int capacity = 26;
        long long hayHashnum = 0, needHashnum = 0, aL = 1;
        for (size_t i = 0; i < needle.size(); i++)
        {
            hayHashnum = (hayHashnum * capacity + haystack[i] - 'a') % modulus;
            needHashnum = (needHashnum * capacity + needle[i] - 'a') % modulus;
            aL = (aL * 26) % modulus;
        }
        for (size_t i = 0; i <= haystack.size() - needle.size(); i++)
        {
            if (hayHashnum == needHashnum)return i;
            hayHashnum = (hayHashnum * capacity - ((long long)haystack[i] - 'a') * aL + haystack[i + needle.size()] - 'a') % modulus;
        }
        return - 1;
    }

时间复杂度O(n),只遍历了一遍

空间复杂度O(1),没有使用额外空间


= needHashnum)return i;
hayHashnum = (hayHashnum * capacity - ((long long)haystack[i] - ‘a’) * aL + haystack[i + needle.size()] - ‘a’) % modulus;
}
return - 1;
}


***时间复杂度O(n),只遍历了一遍***

***空间复杂度O(1),没有使用额外空间***

------

好了,这就是strStr()的三种实现方式,如果还有更好的想法,在评论区里留言哦~~
上一篇:28. 实现 strStr()『简单』


下一篇:k8s源码分析之kubelet