• 浅谈字符串模式匹配


    0 绪论

    字符串模式匹配是计算机科学中最古老、研究最广泛的问题之一(其实也就几十年)。字符串匹配问题就是在一个字符串T中搜索某个字符串P的所有出现位置。其中,T称为文本,P称为模式,T和P都定义在同一个字母表∑上。简单来说,就是在一个目标串T中看有没有子串与另一个较短的字符串相同。

    字符串串匹配算法虽然发展了几十年,然而非常实用的算法是近年才出现。

    传统的串匹配算法可以概括为前缀搜索、后缀搜索、子串搜索。比如以KMP,Shift-And,Shift-Or,BM等子串搜索,以后缀自动机为代表的的后缀搜索等。

    近些年则流行以哈希函数为基础的哈希匹配。

    字符串模式匹配的应用包括生物信息学、信息检索、拼写检查、语言翻译、数据压缩、网络入侵检测等方面。(你见到最多的应用应该是论文查重)

    字符串匹配工作一直是IDS研究领域中的热点之一。在网络速度迅速发展的今天,基于字符匹配技术的网络应用存在着性能瓶颈,提高系统的整体性能是研究和设计者的主要工作。字符匹配是其实现网络入侵检测的主要技术之一,因此,高效的算法将是提高系统总体性能的手段之一。

    这篇博客将简要介绍BF算法(暴力算法),KMP算法和简单的hash匹配。

    1 BF算法

    (1)简介

    字符串的暴力匹配算法也被称为Brute-Force算法(其实就是“暴力”的英语)简称BF算法。

    (2)算法描述

    采用穷举的方法,将文本T字符串的第一个字符与模式P字符串的第一个字符比较,若相等则逐个比较后续字符,否则将文本T字符串的第二个字符与模式P字符串的第一个字符比较。

    (3)算法分析

    假设文本T的字符串长度为n,模式P的字符串长度为m,则BF算法的最好时间复杂度为O(n),最坏时间复杂度为O(n*m),平均时间复杂度O(n*m),空间复杂度为O(n+m)。

    (4)算法实现

    #include<iostream>
    #include<string>
    #include<bits/stdc++.h>
    using namespace std;
    int BF(string t,string p)//返回t与p匹配成功的次数
    {
    	int num=0; 
    	for(int i=0;i<t.length();i++)
    	{
    		int j;
    		for(j=0;j<p.length();j++)
    			if(t[i+j]!=p[j])break;
    		if(j==p.length())
    		{
    			num++;
    			cout<<"匹配成功位置:"<<i<<endl;
    		}
    	}
    	return num;
    }
    int main()
    {
    	string t,p;
    	cin>>t>>p;
    	cout<<BF(t,p)<<endl;
    	return 0;
    }
    

    2 KMP算法

    (1)简介

    KMP算法是由D.E.Kruth,J.h.Morris和V.R.Pratt共同提出,因此人们称它为克努特—莫里斯—普拉特操作,简称KMP算法。该算法是大多数初学者学习字符串匹配的第一个高效算法,也是非暴力算法中比较好理解的一个。

    (2)算法描述

    KMP算法定义了一个next数组,通过next数组在发现匹配中途出现匹配失效时减少回溯长度来提高匹配效率。

    next数组可以理解为:next[j](假设next[j]=k)表示模式p中p[j-k],p[j-k+1]···p[j-1]与p[0],p[1]···p[k-1]相同。在模式匹配的过程中,当匹配在p[j]失效时,指向p的s指针回退到next[j],而非完全回溯。

    KMP还有一种改进算法:由之前的定义可得next[j]=k且p[j]=p[k]时,若t[i] epp[j]就必有t[i] epp[k]。此时没有必要再将t[i]与p[k]比较,可以直接将t[i]与p[next[k]]比较。

    (3)算法分析

    假设文本T的字符串长度为n,模式P的字符串长度为m,则KMP算法的最好时间复杂度为O(n+m),最坏时间复杂度为O(n*m),平均时间复杂度O(n+m),空间复杂度为O(n+m)。

    (4)算法实现

    以下为改进的KMP算法的实现:

    #include<iostream>
    #include<bits/stdc++.h>
    #include<string>
    using namespace std;
    int next[1000];
    void get_next(string p)//计算next数组
    {
    	int len=p.length();
    	int j=0,k=-1;
    	next[0]=-1;
    	while(j<len)
    	{
    		if(k==-1||p[j]==p[k])
    		{
    			j++;k++;
    			if(p[j]==p[k])next[j]=next[k];
    			else next[j]=k;
    		}
    		else k=next[k];
    	}
    }
    int KMP(string t,string p)//进行正式匹配并返回匹配成功的次数
    {
    	int tlen=t.length(),plen=p.length();
    	int i=0,j=0,num=0;
    	while(i<tlen)
    	{
    		if(j==-1||t[i]==p[j])
    		{
    			if(j==plen-1)
    			{
    				num++;
    				cout<<":"<<i-j+1<<'
    ';
    				j=next[j];
    			}
    			else {i++;j++;}
    		}
    		else j=next[j];
    	}
    	return num;
    }
    int main()
    {
    	string t,p;
    	cin>>t>>p;
    	get_next(p); 
    	int num=KMP(t,p);
    	cout<<num<<endl;
    	return 0;
    }
    

    3 hash算法

    (1)简介

    hash匹配算法是将hash算法运用到字符串匹配中的一个例子,并被广泛运用。hash匹配算法本生就具有多种,在此仅介绍简单的hash匹配算法。

    (2)算法描述

    普通hash匹配即每次截取模式串长度的字串计算hash值,然后原模式串的hash值进行比较,两者相同则认为匹配成功。
    hash值的一个简单计算方法为

    [hash(string)=(sum_{i=1}^nk*d^{i-1})mod(p) ]

    其中n表示字符串的长度,k表示字符串的第i个字符在字符集中的序号,d表示hash基数一般取大质数(大于字符集的宽度),p表示模运算底数,一般取232或者264。

    滚动hash匹配是普通hash匹配的优化版,优化之处在于不再频繁计算原串子串的hash值,而是通过前一个子串的hash值得到后一个子串的hash值。

    (3)算法分析

    假设文本T的字符串长度为n,模式P的字符串长度为m,则普通hash算法的最好时间复杂度为O(nm),最坏时间复杂度为O(nm),平均时间复杂度O(n^m),空间复杂度为O(n+m)。滚动hash算法的最好时间复杂度为O(n+m),最坏时间复杂度为O(n+m),平均时间复杂度O(n+m),空间复杂度为O(n+m)。

    (4)算法实现

    以下为滚动hash算法的实现

    #include<iostream>
    #include<string>
    using namespace std;
    const unsigned int size=233;
    int hash(string t,string p)//进行hash匹配
    {
    	int num=0;
    	int tlen=t.length(),plen=p.length();
    	unsigned int k=1,temph=0,ph=0;
    	for(int i=0;i<plen;i++)
    	{
    		ph=ph*size+p[i]; 
    		temph=temph*size+t[i];
    	}
    	for(int i=1;i<plen;i++)k*=size;
    	if(ph==temph)
    	{
    		num++;
    		cout<<"1
    ";
    	}
    	for(int i=plen;i<tlen;i++)
    	{
    		temph-=k*t[i-plen];
    		temph=temph*size+t[i];//滚动计算hash值
    		if(temph==ph)
    		{
    			num++;
    			cout<<i-plen+2<<'
    ';
    		}
    	}
    	return num;
    }
    int main()
    {
    	string t,p;
    	cin>>t>>p;
    	int num=hash(t,p);
    	cout<<num<<endl;
    	return 0;
    }
    

    4 总结

    本文简要介绍了面向初学者的关于字符串匹配的基础知识,希望读者能有所收获。

  • 相关阅读:
    关于在函数中返回动态的内存
    C与C++中的const
    strcat函数的坑点
    面试题30.最小的k个数
    面试题29.数组中出现次数超过一半的数字
    面试题28.字符串的排列
    面试题27.二叉搜索树与双向链表
    C++中构造函数初始化成员列表总结
    Oracle merge into
    检索 COM 类工厂中 CLSID 解决办法
  • 原文地址:https://www.cnblogs.com/WHU-TD/p/14589620.html
Copyright © 2020-2023  润新知