zoukankan      html  css  js  c++  java
  • Leetcode-28. 实现strStr()

    28. 实现 strStr()

    实现 strStr() 函数。

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

    示例 1:

    输入: haystack = "hello", needle = "ll"
    输出: 2
    

    示例 2:

    输入: haystack = "aaaaa", needle = "bba"
    输出: -1
    

    说明:

    needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

    对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。

    KMP

    str=haystack, pat=needle

    这道题一开始我是奔着KMP算法去了,因为很明显的字符串匹配问题嘛,参照KMP 算法详解这片博文,用C++实现了一次KMP算法。主要要点是,主串的指针i不回退,根据模式pat计算状态转移矩阵next(状态就是指当前匹配了pat里的几个字符了,转移动作是指当前主串中要遍历到的字符),遍历主串时,根据这个状态转移矩阵转移状态,直到转移到最后一个状态则匹配成功,否则匹配失败。难点就在于这个next矩阵是怎么得到的。

    加入pat字符串的长度为M,里面的字符有N个,则状态范围是[0, M], 转移操作的范围是那N个字符加上一个"other", next的维度是(M+1,N+1)。在构造next矩阵时,遇到的转移操作正好时pat的下一个字符时,很简单next相应位置为下一个状态即可。例如:

    pat = ABABC

    状态0遇到A字符直接跳转到状态1即可,遇到其他字符还是状态0.

    那状态4遇到A要怎么办呢,可以跳到状态3,因为遇到A后,当前最后匹配的ABA和前缀ABA匹配,所以跳到状态3最省时间。那为啥跳状态3,代码里是怎么知道要跳这里的。那片博文介绍使用影子状态来记录这个位置,影子状态和当前状态有相同的前缀(应该是慢一步的相同前缀,因为如果说是相同前缀,那影子状态应该就是当前状态了)。

    首先初始化next矩阵为:

    x=0 A B C other
    0 1 0 0 0
    1 0 0 0 0
    2 0 0 0 0
    3 0 0 0 0
    4 0 0 0 0
    5 0 0 0 0

    影子状态x初始为0,构造next矩阵从状态1开始,如果遇到字符char为pat[当前状态1],则next[当前状态1] [char] = 下一个状态2。否则状态更新为影子状态遇到char后的状态。

    在每次更新完一个状态的next矩阵后,需要对影子状态进行更新,更新公式为x = next[x] [pat[当前状态]],即当影子状态和当前状态下的字符一样时,影子状态才向前更新(在实际手动计算一次next矩阵后才更好理解)。这样由于当前状态是从1开始,所以影子状态永远比当前状态慢一步。更新完状态1后的next矩阵为:

    x=0 A B C other
    0 1 0 0 0
    1 1 2 0 0
    2 0 0 0 0
    3 0 0 0 0
    4 0 0 0 0
    5 0 0 0 0

    接下来更新状态2:

    x=1 A B C other
    0 1 0 0 0
    1 1 2 0 0
    2 3 0 0 0
    3 0 0 0 0
    4 0 0 0 0
    5 0 0 0 0

    此时隐藏状态x有了变化,因为状态2的pat字符A和隐藏状态x的pat字符A一样,所以x = next[x] [pat[当前状态2]] = next[0] [A] =1。

    更新状态3:

    x=2 A B C other
    0 1 0 0 0
    1 1 2 0 0
    2 3 0 0 0
    3 1 4 0 0
    4 0 0 0 0
    5 0 0 0 0

    隐藏状态x依然有了变化,因为pat[3] = B, next[1] [B] = 2, 所以x=2. 之后更新状态4:

    x=2 A B C other
    0 1 0 0 0
    1 1 2 0 0
    2 3 0 0 0
    3 1 4 0 0
    4 3 0 5 0
    5 0 0 0 0

    完成了next矩阵构造,之后就可以拿这个矩阵去匹配字符串了。

    注:在写C++源码时,可以用vector<map<string, int>>来表示这个二维矩阵。从char转换为string时需要利用(n, val)的初始化方式,即 char c; string cc(1, c);

    class Solution {
    public:
        int strStr(string haystack, string needle) {
            string &str = haystack;
            string &pat = needle;
            if (pat.size() == 0) {
                return 0; // 空模式,返回0.
            }
            // pat出现的所有字符,因为要包含'other'所以需要是string类型
            set<string> pat_char;
            for (char c : pat) {
                string cc(1, c);
                pat_char.insert(cc);
            }
            pat_char.insert("other");
            // 构造next表
            vector<map<string, int>> next = this->construct_next(pat, pat_char);
            // 根据next表匹配
            int state = 0;
            for (int i = 0; i < str.size(); ++i) {
                string cc(1, str[i]);
                if (pat_char.find(cc) == pat_char.end()) {
                    cc = "other";
                }
                state = next[state][cc];
                if (state == pat.size()) {
                    return i - pat.size() + 1;
                }
            }
            return -1;
        }
    
        vector<map<string, int>> construct_next(string& pat, set<string> &pat_char) {
            vector<map<string, int>> next(pat.size() + 1);
            // 初始化都是0,0状态pat[0]处状态转移为1.
            for (int s = 0; s <= pat.size(); ++s) {
                for (string cc : pat_char) {
                    next[s][cc] = 0;
                }
            }
            string cc0(1, pat[0]);
            next[0][cc0] = 1;
            int x = 0; // 影子状态
            for (int s = 1; s < pat.size(); ++s) {
                string current_char(1, pat[s]);
                for (string cc : pat_char) {
                    if (cc == current_char) {
                        next[s][cc] = s + 1;
                    } else {
                        next[s][cc] = next[x][cc];
                    }
                }
                // 更新影子状态
                x = next[x][current_char];
            }
    
            return next;
        }
    };
    

    带预处理的暴力求解BS

    使用KMP提交后,发现并没有那么好的效果,翻题解后发现只用暴力求解就可以很快了,因为KMP需要构造next矩阵,如果面对比较小型的字符串可能效果还不如暴力求解,当然暴力求解也要考虑先排除一些情况才会很快:

    1. pat为空时返回0.
    2. str为空时返回-1.
    3. pat长度小于str时返回-1.

    最终代码如下:

    class Solution {
    public:
        int strStr(string haystack, string needle) {
            if (needle == "") return 0;
            if (haystack == "") return -1;
            int l1 = haystack.size();  // size方法返回一个unsigned的数值,所以直接相减会返回一个很大的数。
            int l2 = needle.size();
            for (int i = 0; i <= l1 - l2; ++i) {
                int j = 0;
                if (haystack[i] == needle[j]) {
                    while (j < needle.size()) {
                        if (haystack[i + j] != needle[j]) {
                            j = 0;
                            break;
                        } else {
                            ++j;
                        }
                    }
                    if (j == needle.size()) {
                        return i;
                    } else {
                        ;
                    }
                }
            }
            return -1;
        }
    };
    

    两者的通过情况对比:

    KMP:

    BS:

    看来这道题的测试样例没有特别大特别复杂的测试样例,特别大的测试样例都可以被BS的预处理部分很快的处理掉。

  • 相关阅读:
    FreeIPA部署及基本使用
    第7天:Django模板使用与表单
    第6天:数据库配置与模型
    第5天:基于类的视图与中间件
    linux下修改MySQL表名与表的字符编码
    webService理解(一)
    查询MySQL库下所有表名,数据为空的表名
    linux中MySQL连接找不到mysql.sock文件
    初步书写Webservice项目,运行后报错:Cannot assign requested address: bind
    linux配置jdk,MySQL环境
  • 原文地址:https://www.cnblogs.com/eggplant-is-me/p/13904450.html
Copyright © 2011-2022 走看看