串的存储结构
与线性表类似,串也有两种基本存储结构:顺序存储和链式存储。但考虑到存储效率和算法的方便性,串多采用顺序存储结构。
1 串的顺序存储
typedef struct { //0号单元存放串的长度
char ch[MAXSTRLEN+1];
int length;
}SString;复制代码
这种定义方式是静态的,在编译时就确定了串空间的大小,而多数情况下,串的操作是以串的整体形式参与的,串变量之间的长度相差较大,在操作中串值长度的变化也较大,这时就需要动态地分配和释放字符数组空间。
在C语言中,存在这一个称之为“堆(Heap)”的自由存储区,可以为每个新产生的串动态分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址,这种字符串的存储方式也成为串的堆式顺序存储结构,定义如下:
typedef struct{
char *ch;
int length;
}HString;复制代码
串的模式匹配算法
子串的定位运算通常称为串的模式匹配或串匹配。此运算应用非常广泛,比如在搜索引擎,拼写检查,语言翻译,数据压缩等应用中,都需要进行串匹配。
串的模式匹配设有两个字符串S和T,设S为子串,也成为正文串;设T为子串,也成为模式,在主串S中查找与模式T相匹配的子串,如果匹配成功,确定相匹配的子串中第一个字符在主串S中的位置。
著名的模式匹配算法有BF算法和KMP算法
BF算法
C++语言
#include<cstring>
#include<iostream>
using namespace std;
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
#define MAXSTRLEN 255 //用户可在255以内定义最长串长
typedef struct { //0号单元存放串的长度
char ch[MAXSTRLEN+1];
int length;
}SString;
Status StrAssign(SString &T, char *chars) { //生成一个其值等于chars的串T
int i;
if (strlen(chars) > MAXSTRLEN)
return ERROR;
else {
T.length = strlen(chars);
for (i = 1; i <= T.length; i++)
T.ch[i] = *(chars + i - 1);
return OK;
}
}
int Index(SString S, SString T, int pos)
{
int i,j;
i=pos;
j=1;
while(i<=S.length && j<=T.length){
if(S.ch[i]==T.ch[j]){
++i;
++j;
}
else{
i=i-j+2;
j=1;
}
}
if(j>T.length){
return i-T.length;
}
else return 0;
}
int main()
{
SString S;
StrAssign(S,"ababcabcacbab");
SString T;
StrAssign(T,"abcac");
cout<<"主串和子串在第"<<Index(S,T,1)<<"个字符处首次匹配\n";
return 0;
}复制代码
java语言
package one;
public class BF {
public static int find(String str1,String str2){
char[] c1=str1.toCharArray();
char[] c2=str2.toCharArray();
int i=0;
int j=0;
while(i<str1.length() && j<str2.length()){
if(c1[i]==c2[j]){
i++;
j++;
}
else{
i=i-j+1;
j=0;
}
}
if(j>=str2.length()) return i-str2.length();
else return -1;
}
public static void main(String[] args) {
String str1="ababcabcacbab";
String str2="abcac";
int n=find(str1,str2);
System.out.println(str2+"在"+str1+"中出现的位置是:"+n);
}
}复制代码
javascript语言
function find(str1,str2){
var c1=str1.split("");
var c2=str2.split("");
var i=0;j=0;
while(i<str1.length&&j<str2.length){
if(c1[i]==c2[j]){
i++;
j++;
}
else{
i=i-j+1;
j=0;
}
}
if(j>=str2.length){
return i-str2.length;
}
else return -1;
}
var str1="ababcabcacbab";
var str2="abcac";
console.log(find(str1,str2));复制代码
此处可以想到javascript里面的string的indexOf()方法
function find2(str1,str2){
return str1.indexOf(str2);
}
var str1="ababcabcacbab";
var str2="abcac";
console.log(find2(str1,str2));复制代码
这样是不是就简单多了呢?
下面我们来分析一下BF算法的时间复杂度
(1)最好情况下,每趟不成功的匹配都发生在模式串中的第一个字符与主串中相对应字符的比较
例如:
S="aaaaaba"
T="ba"
设主串长度为n,子串长度为m
若第i趟成功的字符比较的次数为m,则总比较次数为i-1+m,对于成功匹配的主串,其起始位置由1到n-m+1,假定这n-m+1个起始位置上的匹配成功概率相等,可计算出这种情况下匹配成功的平均比较次数为
即最好情况下的平均时间复杂度是
O(n+m)
(2)最坏情况,每趟不成功的匹配都发生在模式串最后一个字符与主串中相对应字符的比较。
例如:
S="aaaaaab"
T="aab"
假设从主串的第i个位置与模式串匹配成功,则在前i-1趟匹配中字符一共比较了
(i-1)*m
次,若第i趟成功的字符比较次数为m,则总比较次数为
i*m
。因此最坏情况下匹配成功的比较次数为
即最好情况下的平均时间复杂度是
O(n*m)
BF算法思路直观简明,但算法时间复杂度高,下面将介绍另外一种改进的模式匹配算法。
KMP算法
在开始这个算法之前,先介绍两个概念:前缀和后缀
由上图所得, "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀" 指除了第一个字符以外,一个字符串的全部尾部组合。
为了更好地说明这个算法,先来看个例子吧:
主串S="abcdefgab"
模式串T="abcdex"
串T首字母"a"与后面的串"bcdex"中的任意一个字符都不相等,也就意味着模式串T的首字符"a"不可能与S串的第2到5位字符相等,也就没必要像BF算法那样i回溯到i-j+2处了,直接让i=6。可是如果T串后面含有首字符"a"呢?
再看一个例子:
S="abcabcabc"
T="abcabx"
前5个字符相等,第6个字符不等,根据刚才的经验,T的首字符"a"与T的第二个字符"b",第三个字符"c"均不等,所以不需要做判断。因为T的首位"a"与T的第四位"a"相等,第二位的"b"与第五位的"b"相等,因此,T的首字符"a",第二位的字符"b"与S的第四位字符和第五位字符也不需要再比较了,肯定也是相等的。我们只需要直接比较i=6,与j=3处的值是否相等就可以了。i值不需要发生回溯,j值的变化跟主串没什么关系,关键就在于T串的结构中的是否有重复的子串。
由于T="abcdex"当中没有重复的字符,所以j值就由6变成了1,而在T="abcabx",前缀的"ab"与最后的"x"之前串的后缀"ab"是相等的,因此j就由6变成了3。因此我们可以得出规律:j值得多少取决于当前字符之前的串的前后缀的相似度。我们把T串中各个位置的j值得变化定义为一个数组next。
next数组值推导
例子:
j | 123456789 |
---|---|
模式串T | ababaaaba |
next[j] | 011234223 |
j | 123456789 |
---|---|
模式串T | aaaaaaaab |
next[j] | 012345678 |
j | 123456 |
---|---|
模式串T | abcabx |
next[j] | 011123 |
C++代码实现
#include<cstring>
#include<iostream>
using namespace std;
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
#define MAXSTRLEN 255 //用户可在255以内定义最长串长
typedef char SString[MAXSTRLEN+1]; //0号单元存放串的长度
Status StrAssign(SString T,char *chars){
int i;
if(strlen(chars)>MAXSTRLEN)
return ERROR;
else{
T[0]=strlen(chars);
for(i=1;i<=T[0];i++){
T[i]=*(chars+i-1);
}
return OK;
}
}
void get_next(SString T,int next[]){
int i=1,j=0;
next[1]=0;
while(i<T[0]){
if(j==0||T[i]==T[j]){//T[i]表示后缀的单个字符
++i; //T[j]表示前缀的单个字符
++j;
next[i]=j;
}
else j=next[j];//若字符不相等,则j值回溯
}
}
int Index_KMP(SString S,SString T,int pos,int next[]) {
int i=pos;//i用于主串当前位置下标值,若pos不为1,则从pos位置开始匹配
int j=1;
while(i<=S[0]&&j<=T[0]){
if(j==0||S[i]==T[j]){
++i;
++j;
}
else j=next[j];
}
if(j>T[0])
return i-T[0];
else return 0;
}
int main(){
SString S;
StrAssign(S,"aaabaaaab");
SString T;
StrAssign(T,"aaaab");
int *p=new int[T[0]+1];
get_next(T,p);
cout<<"主串和子串在第"<<Index_KMP(S,T,1,p)<<"个字符处首次匹配\n";
return 0;
}复制代码
KMP模式匹配算法改进
如果主串是S="aaaabcde"
T="aaaaax"
其next数组值为01234,在开始时i=5,j=5,b与z不相等,此时j=next[5]=4,此时"b"与第四位置的"a"依然不等,j=next[4]=3;...知道j=next[1]=0
,根据算法,++i,++j,得到i=6,j=1。那么在这个过程中有4步是多余的。由于T串的第2,3,4,5都与收首位的a相等,那么我们可以用首位next[1]的值去取代与它相等的字符后续next[j]的值,改良后的部分代码:
void get_next(SString T, int next[])
{ //求模式串T的next函数值并存入数组next
int i = 1, j = 0;
next[1] = 0;
while (i < T[0]){
if (j == 0 || T[i] == T[j])
{
++i;
++j;
if(T[i]!=T[j])
next[i]=j;
else next[i]=next[j];
}
else
j = next[j];
}
}复制代码
改良后的next1数组值推导
例子:
j | 123456789 |
---|---|
模式串T | ababaaaba |
next[j] | 011234223 |
next1[j] | 010104210 |
j | 123456789 |
---|---|
模式串T | aaaaaaaab |
next[j] | 012345678 |
next1[j] | 000000008 |
java版KMP算法
package one;
public class KMP {
public static int[] getNext(String b)
{
int len=b.length();
int j=0;
int next[]=new int[len+1];//next表示长度为i的字符串前缀和后缀的最长公共部分,从1开始
next[0]=next[1]=0;
for(int i=1;i<len;i++)//i表示字符串的下标,从1开始
{//j在每次循环开始都表示next[i]的值,同时也表示需要比较的下一个位置
while(j>0&&b.charAt(i)!=b.charAt(j))
j=next[j];
if(b.charAt(i)==b.charAt(j))
j++;
next[i+1]=j;
}
return next;
}
public static void search(String original, String find, int next[]) {
int j = 0;
for (int i = 0; i < original.length(); i++) {
while (j > 0 && original.charAt(i) != find.charAt(j))
j = next[j];
if (original.charAt(i) == find.charAt(j))
j++;
if (j == find.length()) {
System.out.println("find at position " + (i - j+1));
System.out.println(original.subSequence(i - j + 1, i + 1));
j = next[j];
}
}
}
public static void main(String[] args){
String s1="abcaababcabacc";
String s2="ababcab";
int[] next=getNext(s2);
search(s1,s2,next);
}
}复制代码
javascript版的有兴趣的童鞋可以自己编写一下,此处就不在编写了。