算法训练第九天 | 第四章 字符串

代码随想录第九天,继续字符串

今日任务

● 28. 实现 strStr()

28. 找出字符串中第一个匹配项的下标

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2
示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1

trivial
库函数
return haystack.indexOf(needle);
String.indexOf() time complexity O(mn)

方法一 暴力解法

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        for (int i = 0; i + m <= n; i++) {
            boolean flag = true;
            for (int j = 0; j < m; j++) {
                if (haystack.charAt(i + j) != needle.charAt(j)) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                return i;
            }
        }
        return -1;
    }
}

时间复杂度均为: O( n * m) n 为haystack 长度,m为needle长度
空间复杂度:O(1)。我们只需要常数的空间保存若干变量

方法二 substring解法 与暴力解法相似

class Solution {
    public int strStr(String haystack, String needle) {
        int needleLen = needle.length();
        char startChar = needle.charAt(0);
        if(haystack.length() < needleLen) return -1;
        
        for(int i = 0; i <= haystack.length() - needleLen; i++){
            if(haystack.charAt(i) == startChar 
               && haystack.substring(i,i + needleLen).equals(needle)){
                return i;
            }
        }
        
        return -1;
    }
}

时间复杂度均为: O( n * m) n 为haystack 长度,m为needle长度
空间复杂度:O(1)。

方法三: KMP算法

kmp 最长相等前后缀. KMP主要应用在字符串匹配上。

主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
所以如何记录已经匹配的文本内容,是KMP的重点,也是prefix/next数组肩负的重任。

前缀表

next / prefix 数组就是一个前缀表(prefix table)
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

例子:
要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。
如果暴力匹配,会发现不匹配,此时就要从头匹配了。
但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。

那么什么是前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

前缀表如何记录

前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。

比如说有一个长度为5字符串 x = "ababc",其中前缀有 ε(空串),a,ab,aba,abab;后缀有 ε(空串),c,bc,abc,babc,这样应该就很好理解了
前缀表就是存储当前字符相同前后缀的长度。

在当前例子中, 模式串为 aabaaf
模式串下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。

所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

如何计算前缀表

在下标0位置,当前子串a,长度为一个字符,没有前缀与后缀,最长相同前后缀的长度为0。
在下标1位置,当前子串aa,长度为2, 最长相同前后缀分别为a,a,最长相同前后缀的长度为1。
在下标2位置,当前子串aab,长度为3,前缀为a,ab; 后缀为b, ab,没有相同前后缀,最长相同前后缀的长度为0。
在下标3位置,当前子串aaba, 长度为4,前缀为a,aa,aab;后缀为a,ba,aba, 最长相同前后缀为a,最长相同前后缀的长度为1。
在下标4位置,当前子串aabaa, 长度为5,前缀为a,aa,aab,aaba; 后缀为a,aa,baa,abaa, 最长相同前后缀为aa,最长相同前后缀的长度为2。
在下标5位置,当前子串aabaaf, 长度为6,前缀为a,aa,aab,aaba,aabaa; 后缀为f,af,aaf,baaf,abaaf, 无相同前后缀,最长相同前后缀的长度为0。

求得的最长相同前后缀的长度就是对应前缀表的元素,如图


可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

复杂度分析

其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。

暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大的提高的搜索的效率。

构造前缀表(next / prefix 数组)

构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:

初始化
处理前后缀不相同的情况
处理前后缀相同的情况

  1. 初始化

定义两个指针i和j,j指向后缀末尾位置,i指向前缀末尾位置。
int j = 0;
next[0] = 0;
next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j), 所以初始化next[0] = j

  1. 处理前后缀不相同的情况
    因为j初始化为0,那么i就从1开始,进行s[i] 与 s[j]的比较。
    所以遍历模式串s的循环下标i 要从 1开始,代码如下:
for(int i = 1, i < pattern.length(); i++)

如果 s[i] 与 s[j]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。

怎么回退呢?

next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。

那么 s[i] 与 s[j] 不相同,就要找 j前一个元素在next数组里的值(就是next[j - 1])。

所以,处理前后缀不相同的情况代码如下:

while(j > 0 && s[i] !=s[j]){
     j = next[j - 1];
}
  1. 处理前后缀相同的情况
    如果 s[i] 与 s[j ] 相同,说明找到了相同的前后缀,j 与i同时向后移动; 同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
  if(s[i] == s[j]){
    j++;
  }
next[i] = j;

整体代码如下:

 public void getPrefix(int[] prefix, String s){
        //1. 初始化
        int j = 0;
        prefix[0] = 0;
        for(int i = 1; i < s.length(); i++){
            //处理前后缀不同情况
            while(j > 0 && s.charAt(j) != s.charAt(i)){
                j = prefix[j - 1];
            }
            //处理前后缀相同情况
            if(s.charAt(j) == s.charAt(i)){
                j++;
            }
            prefix[i] = j;
        }
    }
}

求prefix / next 数组的时候就是相当于在用kmp,把 【0,j】 的前缀看成是模式串,把 【1,i】 的后缀看成是文本串,①文本串一直是每次循环+1,不会回退 ②模式串有时候可以通过next数组跳过前面几个字符直接比较,这两点就是kmp高效的地方

使用next数组来做匹配

在文本串s里 找是否出现过模式串t。

定义两个下标j 指向模式串t起始位置,i指向文本串s起始位置。
j = 0
i就从0开始,遍历文本串,代码如下:

for (int i = 0; i < s.size(); i++) 

如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置

while( j > 0 && s[i] != t[j] )  j = next[j ];

如果 s[i] 与 t[j ] 相同,那么i 和 j 同时向后移动

if(s[i] == t[j] ) j++;

如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。

if(j == (t.length() - 1))    return  i - t.length() +1; 

整体代码:

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length();
        if(n == 0) return 0;
        if(n < needle.length()) return -1;
        
        //construct prefix array of needle
        int[] prefix = new int[needle.length()];
        getPrefix(prefix,needle); 
        
        int j = 0; //j指向 needle 的第一个位置
        for(int i = 0; i < n; i++){
            while(j > 0 && haystack.charAt(i) != needle.charAt(j)){
                j = prefix[j - 1]; //j回到前一位置的prefix值:当前最长相等前后缀
            }
            if(haystack.charAt(i) == needle.charAt(j)){
                j++; //字符相同,j前进一步
            }
            if(j == needle.length()){//j成功走完needle,说明found needle in haystack
                return i - needle.length() + 1;
            }
        }
        
        
        return -1;
    }
    
    
    public void getPrefix(int[] prefix, String s){
        //1. 初始化
        int j = 0;
        prefix[0] = 0;
        for(int i = 1; i < s.length(); i++){
            //处理前后缀不同情况
            while(j > 0 && s.charAt(j) != s.charAt(i)){
                j = prefix[j - 1];
            }
            //处理前后缀相同情况
            if(s.charAt(j) == s.charAt(i)){
                j++;
            }
            prefix[i] = j;
        }
    }
}

KMP 算法的复杂度为 O(m+n)。

KMP 之所以能够在 O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。

空间复杂度部分O(m),我们只用到了长度为 m 的数组保存前缀函数,以及使用了常数的空间保存了若干变量

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容