字符串匹配算法和应用

KMP算法

KMP: KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。模式串就是关键字(接下来称它为P),如果它在一个主串(接下来称为T)中出现,就返回它的具体位置,否则返回-1(常用手段)。

1、常规匹配和kmp匹配

常规匹配: 从左到右一个个匹配,如果这个过程中有某个字符不匹配,就跳回去,将模式串向右移动一位。

kmp匹配: 既然不匹配位置前面的字符(区域S1)已经比较过了,那么就不应该再比较一次。我们已经知道S1部分就是 abab,如果后移动一个字符,a与b肯定不能匹配。后移2位是可以的,因为模版串的前两个字符 ab 正好对齐S1的后两个字符 ab ,我们可以发现,这样的移动跟待匹配串是没有任何关系的,只要模版串中的失配点确定,那么对应后移的位数也随之确定。

kmp算法: 利用已经部分匹配这个有效信息,保持主串中的i指针不回溯,通过修改模式串中的j指针,让模式串尽量地移动到有效的位置**。当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪?计算每一个模式串位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j](主串第i个位置和模式串第j个位置不相等)时,模式串j指针需要移动的下一个位置。**模式串中最前面的k个字符和j之前的最后k个字符是一样的。即P[0 ~ k-1] == P[j-k ~ j-1]。

next数组推导过程: next[j]=k, 表示最前面的k个字符和j之前的最后k个字符是pi。

  • 当j为0时,如果这时候不匹配,j已经在最左边了,不可能再移动了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。
  • 当j为1时,j指针一定是后移到0位置的。因为它前面也就只有这一个位置了。
  • 当P[k] == P[j]时,有next[j+1] == next[j] + 1。

    推导:因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)。这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

  • 当P[k] != P[j]时,有k = next[k]。 对于下图的示例:已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程像不像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]啦。 若P[k] != P[j],但P[0 ~ k-1] == p[j-k ~ j-1],所以对于next[j+1]来说,需要从k位置之前找到和j位置匹配的串。j位置和k位置的字符不相同,若要找k位置之前和j位置为右端点的相同子串的话相当于next[k]的值。

当next[j]==0时说明j指针需要移动到字符串的头部,j之前的串没有首尾位置相等的子串。

next数组求解算法

int GetNext(string str,int next[]) { 
    int length=str.size();
    next[0] = -1;
    next[1] = 0;
    int j=0;
    int k=-1;
    while(j<length){
        if (k == -1 || p[j] == p[k]) {     //next[j]的值为k,所以next[++j]的值为next[j]+1即k+1
            next[++j] = ++k;               //当k==-1时说明下一步j位置的指针需要移动到起始位置0.
        } else {
            k = next[k];
        }
    }
}

kmp利用next数组的字符串匹配KMP算法:

int KMP(String s,String t)
{
   int tlength=t.size();
   int next[tlength],i=0,j=0;
   Getnext(t,next);
   while(i<s.size()&&j<t.size())
   {
      if(j==-1 || s[i]==t[j])  //j等于-1时,需要移动i指针(i++),并把j置为初始位置0(j++)。
      {
         i++;
         j++;
      }
      else j=next[j];               //j回退
   }
   if(j>=t.length)
       return (i-tlength);         //匹配成功,返回子串的位置
   else
      return (-1);                  //没找到
}


马拉车(manacher)算法

马拉车(manacher): Manacher算法是一个用来查找一个字符串中的最长回文子串(不是最长回文序列)的线性算法。它的优点就是把时间复杂度为O(n2)的暴力算法优化到了O(n)。

s="abcd",最长回文长度为 1;
s="ababa",最长回文长度为 5;
s="abccb",最长回文长度为 4,即 bccb。

由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,具体做法是:

  1. 在字符串首尾及每个字符间都插入一个 “#”,这样可以使得原先的奇偶回文都变为奇回文;在字符串的首尾、相邻的字符中插入分隔符,例如 "babad" 添加分隔符 "#" 以后得到 "#b#a#b#a#d#"新字符串的回文子串的长度一定是奇数.
  1. 接着再在首尾两端各插入 “$” 和 “^”,这样中心扩展寻找回文的时候会自动退出循环,不需每次判断是否越界.

  2. 引入p数组, mx, id. 预处理后的新串表示为 Str,定义数组p[],p[i]表示 Str 中以下标i为回文中心的最大回文半径。mx 代表以 id 为中心的最长回文的右边界,也就是 mx = id + p[id]。

  3. 根据回文的性质,p[i] 的值基于以下三种情况得出:

    (1):j 的回文串有一部分在 id 的之外,如下图:

上图中,黑线为 id 的回文,i 与 j 关于 id 对称,红线为 j 的回文。那么根据代码此时 p[i] = mx - i,即紫线。那么 p[i] 还可以更大么?答案是不可能!见下图:

假设右侧新增的紫色部分是 p[i] 可以增加的部分,那么根据回文的性质,a 等于 d ,也就是说 id 的回文不仅仅是黑线,而是黑线+两条紫线,矛盾,所以假设不成立,故 p[i] = mx - i,不可以再增加一分。

(2):j 回文串全部在 id 的内部,如下图:

根据代码,此时 p[i] = p[j],那么 p[i] 还可以更大么?答案亦是不可能!见下图:

​ 假设右侧新增的红色部分是 p[i] 可以增加的部分,那么根据回文的性质,a 等于 b ,也就是说 j 的回文应该再加上 a 和 b ,矛盾,所以假设不成立,故 p[i] = p[j],也不可以再增加一分。

(3):j 回文串左端正好与 id 的回文串左端重合,见下图:

根据代码,此时 p[i] = p[j]p[i] = mx - i,并且 p[i] 还可以继续增加,所以需要

while (s_new[i - p[i]] == s_new[i + p[i]]) 
    p[i]++;

(4):当id >= mx时,此时,没有已知信息可以辅助计算了,令p[i] = 1然后暴力扩展。

if (i >= mx) p[i] = 1;
while (s_new[i - p[i]] == s_new[i + p[i]]) 
    p[i]++;

(5) : 求解出p[i]以后,需要更新id和mx,代码如下:


if (i + p[i] > mx) { /* 如果以i为中心的最大回文串能更新rt */
    mx = i + p[i];
    id = i; /* 更新mx对应的id */
}

马拉车完整算法:

string InitStr(string s)
{
    int len = strlen(s);
    string s_new(2*len," ");
    s_new[0] = '$';
    s_new[1] = '#';
    int j = 2;

    for (int i = 0; i < len; i++)
    {
        s_new[j++] = s[i];
        s_new[j++] = '#';
    }

    s_new[j++] = '^';  // 别忘了哦
    s_new[j] = '\0';   // 这是一个好习惯
    
    return s_new;  // 返回 s_new 的长度
}

int Manacher(string s)
{
    string s_new = InitStr(s);  // 取得新字符串长度并完成向 s_new 的转换
    int len=s_new.size();
    int max_len = -1;  // 最长回文长度
	int * p=(int*)malloc(sizeof(int)*len); //p[i] 表示以 i 为中心的最长回文的半径
    int id;     						   //字符串下标
    int mx = 0;	//mx 代表以 id 为中心的最长回文的右边界,也就是 mx = id + p[id]。

    for (int i = 1; i < len; i++)
    {
        if (i < mx)
            p[i] = min(p[2 * id - i], mx - i);  
        else
            p[i] = 1;

        while (s_new[i - p[i]] == s_new[i + p[i]])  // 不需边界判断,因为左有 $,右有 ^
            p[i]++;

        // 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
        // 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
        if (mx < i + p[i])
        {
            id = i;
            mx = i + p[i];
        }

        max_len = max(max_len, p[i] - 1);
    }
    return max_len;
}

示例题

1、leetcode kmp next数组相关:

给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。

分析:采用字符串匹配KMP算法及求next值算法。先求s字符串首字符开始的最大回文串的长度length,拼接s串length下标后的字符串的翻转就是最短回文串。先创建临时字符串temp(s+rev(s)),再求出temp字符串的next数组,next[temp.size]就是s字符串首字符开始的最大回文串的长度。

示例 :

输入:s = "aacecaaa"
输出:"aaacecaaa"
/*思路  如对于串 abcd 想要将其变为回文串
      那么先把它逆序 然后放在前面 自然是回文了 
                                   abcd
                                   dcba
                               dcbaabcd ->是回文
      但是我们发现根本没必要放这么多在前面 因为abcd的前缀和dcab的后缀有重合(如a) 所以为了只添加最少的字符,我们在前方只需要添加不重复的即可
                                    abcd
                                 dcba
                                 dcbabcd ->依然是回文
     //为了添加的最少 我们就需要找到dcba的后缀和abcd的前缀重合的部分,且让重合部分最大即可
     //故而联想到kmp算法,它的next数组就是用来求一个串的前缀和后缀相同的长度的最大值
     //所以拼接起字符串 abcddcba 但是我们所求的前缀是不能超过中点的,因此用一个特殊字符隔开
     //           即为 abcd#dcba 这样在匹配前后缀时,相同长度就一定不会超过#号了
     //           这样问题就转化为了 求abcd#dcba的next数组 易知该串的前后缀相同时的最大长度为1
                此时的最长相同前后缀即为a 和 a  
                                     所以把后半部分除去重叠的部分拼接到前半部分即可
                            答案就是  dcbabcd
                                     大功告成!
                  
  */
    string shortestPalindrome(string s) {
        string revs = s;//存s的逆序
        int tn = s.size();//中点处,#前面的位置
        reverse(revs.begin(),revs.end());
        s = ' '+ s + '#' + revs;//让下标从1开始
        int n = s.size()-1;//实际长度
        vector<int> ne(n+1);//next数组
        for(int i = 2, j = 0; i <= n; i++){//求next数组 
            while(j&&s[i]!=s[j+1]) j = ne[j];
            if(s[i]==s[j+1]) j++;
            ne[i] = j;
        }
        return s.substr(tn+2,tn-ne[n])+s.substr(1,tn);//后半部分除去重叠后缀+前半部分
    }


2、leetcode 马拉车相关:

给你一个字符串 s,找到 s 中最长的回文子串。

解法1:动态规划

class Solution {
public:
	string longestPalindrome(string s) {
		int length = s.size();
		if (length == 0){
			return s;
		}
		int maxCount = 0;
		int left = 0;
		int right = 0;
		vector<vector<bool>> dp(length, vector<bool>(length,false));
		for (int i = 0; i < length; i++)
		{
			for (int j = i; j >= 0; j--){
				dp[i][j] = (s[i] == s[j]) && ((i - j < 3) || dp[i-1][j+1]);
				if (dp[i][j] && (i - j + 1>maxCount))
				{
					maxCount = i - j + 1;
					left = j;
					right = i;
				}
			}
		}
		return s.substr(left, right-left+1);
	}
};

解法2:马拉车算法

class Solution {
public:
    string longestPalindrome(string s) {
        int length=s.size();
        string ss="!#";
        for(int i=0;i<length;i++)
        {
            ss+=s[i];
            ss+='#';
        }
        ss+='^';
        int *p=new int[2*length+3];
        int start=1,maxn=0;
        int mx=1,id=1;
        for(int i=1;i<ss.size()-1;i++)
        {
             if (i < mx)
            	p[i] = min(p[2 * id - i], mx - i);  
        	 else
          	    p[i] = 1;
             while(ss[i+len[i]]==ss[i-p[i]])
                p[i]++;
             if(mx<p[i]+i)
             {
                mx=p[i]+i;
                id=i;
             }
             if(maxn<p[i])
             {
                maxn=p[i];
                start=i;
             }
        }
        string res="";
        for(int i=start-maxn+1;i<start+maxn;i++)
        {
            if(ss[i]!='#')
                res+=ss[i];
        }
        return res;
    }
};

trie树字符串匹配算法

在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。Trie又经常叫前缀树,字典树等等。Trie树是一种非常重要的数据结构,它在信息检索,字符串匹配等领域有广泛的应用,同时,它也是很多算法和复杂数据结构的基础,如后缀树,AC自动机等。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计、字符串检索,搜索引擎的热门查询

  • 它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。利用字符串的公共前缀来减少查询时间,最大限度的减少无谓的字符串比较,查询效率比哈希树高。
  • Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
  • Trie树也有它的缺点,Trie树的内存消耗非常大.当然,或许用左儿子右兄弟的方法建树的话,可能会好点.

leetcode1: 实现Tire树的插入,搜索以及查找是否存在以某个Prefix开头的单词。

class TrieNode{
public:
    TrieNode* next[26];  //字母数
     //bool变量isword,如果为true,表示该节点表示的字符串(准确地说,是从根节点一直next到此节点表示的字符串)在Trie树中存在,否则不存在。
    bool isword;        
    TrieNode(){
        memset(next,NULL,sizeof(next));
        isword=false;
    }
    ~TrieNode(){
        for(int i=0;i<26;i++)if(next[i])delete next[i];
    }
};

class Trie {
    TrieNode* root;
public:
    /** Initialize your data structure here. */
    Trie() {
        root=new TrieNode();
    }
    
   //遍历需要插入的string,同时指针p从root一直往下next,如果对应字符的next为NULL,就创建一个新的TrieNode,
   //遍历完后,在最终那个TireNode标记为True,表示这个TrieNode对应的词在这课Trie树中存在。 
    /** Inserts a word into the Trie. */
    void insert(string word) {
        TrieNode*p=root;
        for(int i=0;i<(int)word.size();i++){
            if(p->next[word[i]-'a']==NULL)
                p->next[word[i]-'a']=new TrieNode();
            p=p->next[word[i]-'a'];
        }
        p->isword=true;
    }
    
    //和插入的思路类似,遍历string,同时指针p从root节点一直往下next,如果碰到对应字符的next[]为NULL或者string已经遍历完成,
    // 则退出循环。最后检查p是否为不为NULL以及isword是否为true,两者都满足则说明这个词存在于Trie树。
    /** Returns if the word is in the Trie. */
    bool search(string word) {
        TrieNode *p=root;
        for(int i=0;i<(int)word.size()&&p;i++){
            p=p->next[word[i]-'a'];
        }
        return p&&p->isword;
    }
    
    //实现上基本同查找,唯一的区别在于,无需检查isword是否为true。
    /** Returns if there is any word in the Trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        TrieNode*p=root;
        for(int i=0;i<(int)prefix.size()&&p;i++){
            p=p->next[prefix[i]-'a'];
        }
        return p;
    }
    ~Trie(){
        delete root;
    }
};

leetcode2:实现trie树的模糊查找:(回溯法)

示例:

search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true
class TrieNode{
public:
    TrieNode* next[26];  //字母数
     //bool变量isword,如果为true,表示该节点表示的字符串(准确地说,是从根节点一直next到此节点表示的字符串)在Trie树中存在,否则不存在。
    bool isword;        
    TrieNode(){
        memset(next,NULL,sizeof(next));
        isword=false;
    }
    ~TrieNode(){
        for(int i=0;i<26;i++)if(next[i])delete next[i];
    }
};

bool search(Trienode* root, string word) {
        Trienode*p=find(root,word);
        return p&&p->isword;//如果p存在且isword为true,则模糊查找到匹配的字符串
}

Trienode* find(Trienode*p,string word){
        for(int i=0;i<(int)word.size()&&p;i++){
            if(word[i]=='.'){
                for(int j=0;j<26;j++){
                    if(p->next[j]){//用任何一个字符来匹配‘.’
                        int len=(int)word.size()-i-1;
                        if(len>=1){
                            Trienode*res=find(p->next[j],word.substr(i+1,len));
                            if(res&&res->isword)
                              return res;
                        }else if(p->next[j]->isword)
                             return p->next[j];        
                    }//如果找到了匹配的字符,直接返回,若没有,则继续寻找。
                }
                return nullptr;//找遍所有的树,也没找到模糊匹配的,返回NULL
            }else{
                p=p->next[word[i]-'a'];
            }
        }
     return p;
}

leetcode3: 单词搜索:给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

输入:board = [[“o”,“a”,“a”,“n”],[“e”,“t”,“a”,“e”],[“i”,“h”,“k”,“r”],[“i”,“f”,“l”,“v”]], words = [“oath”,“pea”,“eat”,“rain”]
输出:[“eat”,“oath”]

class trienode{//树节点结构
public:
    trienode*next[26];
    int isexist;//-1表示节点表示的单词不存在,0及以上的数表示此单词存在且与vector<string>>words的序号对应
    trienode(){
        memset(next,NULL,sizeof(next));
        isexist=-1;
    }
    ~trienode(){
        for(int i=0;i<26;i++){
            if(next[i])delete next[i];
        }
    }
};

class Solution {
public:
    int arr[4][2]={{1,0},{-1,0},{0,-1},{0,1}};//搜索的四个方向
    void insert(trienode*root,string& s,int&index){
        for(int i=0;i<(int)s.size();i++){
            if(!root->next[s[i]-'a']){
                root->next[s[i]-'a']=new trienode();
            }
            root=root->next[s[i]-'a'];
        }
        root->isexist=index; //index表示vector<string>>words的序号
    }
    void dfs(int r,int c,trienode*root,unordered_set<int>&res,vector<vector<char>>b){
        b[r][c]='0';//标记已经搜索过得地方为‘0’
        if(root&&root->isexist>=0){//如果isexist>=0表示word[isexist]存在于此节点。
            res.insert(root->isexist);
        }
        for(int i=0;i<4;i++){//向上下左右四个方向搜索
            int newr=r+arr[i][0],newc=c+arr[i][1];
            if(newr>=0&&newc>=0&&newr<b.size()&&newc<b[0].size()&&b[newr][newc]!='0'&&root->next[b[newr][newc]-'a']){
                dfs(newr,newc,root->next[b[newr][newc]-'a'],res,b);
            }
        }
    }
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        trienode* root=new trienode();
        for(int i=0;i<(int)words.size();i++){
            insert(root,words[i],i);//将所有的单词插入Trie树
        }
        unordered_set<int>res;//用于保存在board中查找到的words的序号
        for(int r=0;r<(int)board.size();r++){
            for(int c=0;c<(int)board[r].size();c++){
                if(root->next[board[r][c]-'a'])
                    dfs(r,c,root->next[board[r][c]-'a'],res,board);
            }
        }
        vector<string>vres;//根据res中的序号制作string数组返回
        for(auto it:res){
            vres.push_back(words[it]);
        }
        return vres;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值