代码随想录

文章详细介绍了KMP算法在字符串匹配中的应用,通过暴力解法引出前缀表和Next数组的概念,解释了如何通过Next数组优化匹配过程。此外,文章还讨论了双指针法在字符串题目中的多种应用场景,如替换空格、反转字符串等,并总结了双指针法的两类主要操作模式:快慢指针和首尾指针。
摘要由CSDN通过智能技术生成

代码随想录第9天 | LeetCode 28. strStr LeetCode 459.重复的子字符串 字符串题目总结 双指针法的总结


前言

本章节我一刷直接费了好哒的功夫。给完成了。
KMP算法理论:
kmp算法解决的是字符串匹配的问题。给一个文本串,咱给一个模式串,探索文本串中是否出现模式串。暴力解法:两侧for循环挨个去匹配。在这里我们需要用到一个很重要的表:前缀表,虽然后缀我们也同样会用到

接下来我说一下自己的重磅思路
首先,我们来看一下暴力算法。

for (int i = 1; i <= n; i ++ )
{
    bool flag = true;
    for (int j = 1; j <= m; j ++ )
    {
        if (s[i + j - 1] != p[j])
        {
            flag=false;
            break;
        }
    }
}

(加粗只是为了分隔行,看的更清晰一点)
再暴力算法中,第一层循环遍历文本,从第一个开始,然后作为开头一次和模板的开头进行比较(这个过程是第二层循环:遍历模板)。
如果我们发现有元素不同,匹配失败。那就要接着从文本的下一个元素开始重复操作。这样一看确实是没有任何的毛病,可是有一种直觉告诉我们,你这样做好像真的没有必要啊,你完全可以跳一步进行啊。这样说可能有些抽象,那我们就可以就举个例子,加入我们的模板是aabaaf。文本是abbaabaafa。我们进行判断时,发现在第二步的aa和ab就发生了不一样。那就有问题了,我们就下一步呗,但是很明显,第一个元素肯定要是a才可以,你这个是肯定就不对了呀,因此这种情况是完全可以跳过的,那么我们就要思考,类似的情况我们都要跳过,都要避免这些不必要的浪费。
那么我们应该跳多少呢?我们跳的越多,肯定步数越少,越好去提高查询的效率。那就从最后开始查呗,从最后开始查可以从最远的步数直接来看。
这么以来思路就更加清晰了,找到和开头一样的元素,可是显然我们想要的并不只是开头的一个元素而已,更可以是整体的开头的一对元素,你有一个a可以,我可以接着往下找,但是你直接和我开头一样有2个a,那不直接起飞吗。这证明在这两个a之前的所有情况我都可以跳过。
但是跳过的途中你不可以浪费自己的元素,发生浪费就是不可以跳过的。那么如何进行避免掉浪费呢,求出包含的相等元素最多的公共区间,把这些可能利用到的元素的情况留下来,其他那些百分百可以跳过的元素就要舍弃掉了。
因此就有了前缀和(包含头部的子区间)和后缀和(包含尾部的子区间)的最长公共区间。
那现在就是文本的遍历问题喽,他们怎么才可以跳转到下一个节点呢,如何控制长度呢,我们就需要设置一个Next数组(来表示前缀表)
实现Next数组的方法有很多,这几种方式虽然不同,但是他们有一个共同点,就是他们都用来处理冲突后需要移动到的那个值。使用Next数组时,我们的后缀的后面发生了矛盾,那么我们去寻找矛盾发生处的前面的子区间的最长相等前后缀,之后再找到前缀的后面进行的那个元素进行匹配(其实也可以看做是这个后缀的坑位让给了前缀去坐,那么整体也跟着前缀去移动。)那就是文本是不变的,模板是不断进行移动去匹配的。nexti = j(前缀的末尾。也可以表示长度)=>p[1---- j ] = p[i - j + 1 ---- i](相等,此处可以理解成一个相同区间,因为(j - 1)可以看成一个长度。)(注意,这里的i值是做一个和j的一个区分,他并不是指向的文本的那个i,此处它指向的还是模式串的)


// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
//指针j是指向前缀的末尾位置,他还代表i之前的包括i的子串的包括最长相等前后串的长度。中i是指向后缀的末尾位置。
//求模式串的Next数组(前缀表)
//此模板是将所有的模式串下标加一
for (int i = 2, j = 0; i <= m; i ++ )//i=2的原因是next[1]肯定更是0;
{
    while (j && p[i] != p[j + 1]) j = ne[j];//不断的退步。j代表长度,对应那个点的长度
    if (p[i] == p[j + 1]) j ++ ;//相等就是要加一个,不相等就回退,但是怎么想到前后缀了不太明白
    ne[i] = j;//(一个i对应一个J)//ne[]直接就代表着最长相等前后缀。他为零还不相等,那那一段的最长相等前后缀可不就是零吗。(j 代表的是长度)。然后忽然遇到一个一样的,那就是和第一个是一样的。然后接着看一样不一样,有几个一样就是长度j然后记录下来。直到又不一样后,然后几乎回溯到0,或者类似于零的地方(用模式串abababcdefgabc可以类比。)


// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];//因为发生矛盾的点是j+1,则我们需要找到J点,获得next[J],获得新J后重新进行判断。
    //两种情况终止判断,退到最后了,成功匹配了。
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

一、LeetCode28. 实现 strStr()

题目解析:其实就和上面的模板一模一样。两个部分。直接写就好。

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        if (s.equals("")) return false;

        int len = s.length();
        // 原串加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了
        s = " " + s;
        char[] chars = s.toCharArray();
        int[] next = new int[len + 1];

        // 构造 next 数组过程,j从0开始(空格),i从2开始
        for (int i = 2, j = 0; i <= len; i++) {
            // 匹配不成功,j回到前一位置 next 数组所对应的值
            while (j > 0 && chars[i] != chars[j + 1]) j = next[j];
            // 匹配成功,j往后移
            if (chars[i] == chars[j + 1]) j++;
            // 更新 next 数组的值
            next[i] = j;
        }

        // 最后判断是否是重复的子字符串,这里 next[len] 即代表next数组末尾的值
        if (next[len] > 0 && len % (len - next[len]) == 0) {
            return true;
        }
        return false;
    }
}

C++代码如下:

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) {
            return 0;
        }
        int next[needle.size()];
        getNext(next, needle);
        int j = 0;
        for (int i = 0; i < haystack.size(); i++) {
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            if (haystack[i] == needle[j]) {
                j++;
            }
            if (j == needle.size() ) {
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};

二、LeetCode 459.重复的子字符串

前缀表(不减一)的C++代码实现

这道题目不只是可以用kmp来写,我在抄一下上一次作业写出来的一个可以用的答案,
今天要学习的内容是字符串哈希:求哈希时:将每一个前缀的哈希值记录下来。1. 把字符串看成P进制的数。2.进制的数转换成十进制。3.模Q。。。注意。不能映射成零,且可能冲突,但是这里我们假定Rp足够好,不需要考虑冲突的情况。P=131或者1331.Q=2的64次方。字符串哈希不允许冲突的存在,而前面的哈希就允许。我们可以利用前缀的哈希求得所有需要的字段的哈希。

接下来是一个模板思路
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
//h[]是表示的是哈希数组 p[]表示的是进制
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

在此处我就先不给代码了,有点累了。之后再补一补吧。

三、字符串题目总结

1.首先要提到的是这一章节中我们用到过的双指针法
在字符串这一章,首先我们在344.反转链表中学习了反转字符串的操作,运用的是双指针法,双指针法的两个指针就是首尾两个指针,分别同时往中间移动。JAVA的一个交换方法也比较特殊,也可以记一下。
接着在替换空格的的题目中我们也用到了双指针法,此题目用双指针法在空间复杂度O(1),时间复杂度O(n)的情况下完成,其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。靠后的节点指示的是获取元素的位置,靠前的节点指示的是将要获取的元素的值。
下一个是反转字符串里的单词,这里双指针的一个用处是用来删除空格,删除字符串中没有意义的一些空格。

2.翻转系列:
先整体反转再局部反转,实现了反转字符串里的单词…通过先局部反转再整体反转达到了左旋的效果。总而言之呢就是进行两次反转从而达到局部为一个整体的反转。

四、双指针法的总结

其实双指针法无非就两种:快慢指针,同向移动。一个指向目标地址,一个指向目标元素。要么赋值,要么删除,结束的标志是移动到了对吼,*。第二种是:首尾指针,相向移动, 可以赋值,可以删除,可以旋转。结束的标志是两指针相遇。需要注意的是双指针问题中进行 i++。j++。j–的操作的要求。

1.移除元素,且引入了头指针。快慢指针,同向移动:通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
也有 相向双指针方法,基于元素顺序可以改变的题目描述改变了元素相对位置,确保了移动最少元素:1 找左边等于val的元素。2.找右边不等于val的元素。3.将右边不等于val的元素覆盖左边等于val的元素。

2.反转字符串:使用相向双指针法:即首尾指针,不断向中间移动
直到相遇。遍历过程中交换元素。

  1. 替换空格:快慢指针法。且是从后往前。其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。好处:不用申请新数组。从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。因为是要添加一些元素,所以从后往前。

4.反转字符串里的单词:需要用到快慢指针来删除空格,但是之后也有用到思想:慢指针指示的是位置,快指针指示的是元素。

5.反转单链表:这里也是用到快慢指针法,链表的删除元素也要用到快慢指针吗~我们需要用快慢指针来方便我们获取前后左右的节点,方便我们遍历。(链表出添加虚拟头节点来方便我们遍历,链表处注意末尾的结束情况。)

6.删除链表的倒数第N个节点。删除链表节点,用到快慢指针法。fast提前走一步,因为需要让slow指向删除节点的上一个节点。

7.二数之和,三数之和,四数值和:其实耳熟之和就是首尾指针,找到A和0-A;三数之和是一层用到for循环,for循环的循环变量s[i]也可以代表一个值,四数之和的话就是套用两层for循环,方法是一样的。

总结

今天非常的充实,非常的有干活,框架结构和思维都更加清晰了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值