第 5 章 串 .. 5.1 串的定义 5.1.1 串的基本概念 串,即字符串(String),是由n(n≥0)个字符组成的有限序列。串由双引号进 行界定,引号之内的字符序列是串的值,可以是字母、数字或其他字符。长度为0 的串称为空串,用. 表示。 串的相关概念如下。 (1)子串:串中连续的m (0≤m ≤n)个字符组成的子序列。 (2)主串:包含子串的串。 (3)字符在主串中的位置:字符在串中的序号。 (4)子串在主串中的位置:子串的第一个字符在主串中的位置。 串中数据之间呈线性关系,每个字符有前驱和后继。串与线性表之间的主要 区别在于串的数据对象限定为字符集,其基本操作通常以子串为操作对象。 5.1.2 抽象数据类型定义 串的类主要描述数据和操作,其中,数据是一个字符数组,基本操作包括串的 拼接、子串查找、串的长度、子串位置、子串匹配等,具体的抽象数据类型如下。 ADT String{ 数据对象: D ={ai |ai ∈CharacterSet,i =1,2,…,n (n ≥0)} 数据关系: S ={<ai -1,ai > |ai -1,ai ∈D (i =2,…,n )} 基本操作: concat(String ss)———串的拼接。 substring(int pos,int len)———子串查找。 strcpy(String dest)———串的复制。 strins(int pos,String t)———串的插入。 getlength()———获取串的长度。 clearstr()———清空串。 compare(String t)———串的比较。 index(String t)———子串的位置。 } 根据串的抽象数据类型可以得到如下串的类。 76 数据结构与问题求解(C++版·微课版) #define MAXLENGTH 255 //串允许的最大长度为255 class String{ private: char s[MAXLENGTH]; int length; void init(const char* ss); public: String(){} String(const char* ss); String(char* ss, int start =0, int len =0); void operator =(const char* ss); String& concat(String ss); String substring(int pos,int len); String strcpy(int start, int len); String strcpy(int start); String strcpy(); bool strins(int pos,String t); int getlength(); int compare(String t); int index(String t); char getchar(int i); char* toarray(); }; .. 5.2 串的实现 5.2.1 串的构造 String类中提供了三个构造函数,包括无参构造函数、有参构造函数、带默认值的有参 构造函数,这些不同的构造函数为后续操作的实现提供了便利。其中,无参构造函数已经实 现,其他两个有参字符串的实现代码如下。 //初始化操作,该函数将会在多处被调用,但不需要提供给外界使用,所以被private 修饰 void String::init(const char* ss){ int len =strlen(ss); for(int i =0; i <len; i ++){ this->s[i]=ss[i]; } this->s[len]='\0'; this->length =len; } //用字符串常量构造String 对象 //ss: 常量字符串 String::String(const char* ss){ init(ss); 第5章 串77 } //用字符串构造String 对象 //ss: 字符串 //start: 字符串中的起始位置,默认值为0 //len: 连续字符个数,默认值为0 String::String(char* ss,int start, int len){ int l =strlen(ss); if(len ==0) len =l; //如果是默认值,则用整个字符串构造String 对象 else len =min(l,len); //否则,取字符串长度和参数len 的最小值来构造对象 for(int i =0; i <len; i ++){ this->s[i]=ss[start +i]; } this->s[len]='\0'; this->length =len; } 5.2.2 串的赋值 例5.1 串的赋值。 串的赋值可以通过重载赋值运算符加以实现,代码如下。 //赋值符号后面是字符串常量,所以参数类型为const char* //具体赋值过程与构造函数String(const char* ss)一致 void String::operator =(const char* ss){ init(ss); } //返回String 类中的成员变量,方便输出操作 char* String::toarray(){ return this->s; } //返回串的长度 int String::getlength(){ return this->length; } //测试对象构造和赋值 int main(){ String s,ss(" C++"); s ="hello world"; //使用赋值语句构造串的对象 cout<<"s:"<<s.toarray()<<"(length:"<<s.getlength()<<")"<<endl; cout<<"ss:"<<ss.toarray()<<"(length:"<<ss.getlength()<<")"<<endl; return 0; } 运行结果: 78 数据结构与问题求解(C++版·微课版) s:hello world(length:11) ss: C++(length:4) 5.2.3 子串截取 例5.2 子串截取。 截取串中指定位置为pos、指定长度为len的子串,如果从串的pos到达串的末尾的字 符数量大于len,则直接截取;否则,根据实际情况截取。 //截取子串 //pos: 子串开始的位置 //len: 要截取子串的长度 String String::substring(int pos,int len){ char t[len +1]; //计算从pos 开始串中剩余的字符数,并确定实际可截取子串的长度 len =min(len,this->getlength() -pos +1); for(int i =0; i <len; i ++){ //形成子串字符数组 t[i]=this->s[pos +i]; } t[len]='\0'; String ss(t); //利用子串字符数组构造String 对象 return ss; } int main(){ String s; s ="substring"; String sub =s.substring(2,5); cout<<"test1:"<<sub.toarray()<<endl; String sub2 =s.substring(2,15); cout<<"test2:"<<sub2.toarray()<<endl; return 0; } 运行结果: test1: bstri test2: bstring 第一次测试,从第2个字符开始截取长度为5的子串,此时,串可以满足截取长度为5 的子串;第二次测试,从第2个字符开始截取长度为15的字符串,显然,串的长度不够,只能 根据实际情况进行截取。 5.2.4 子串插入 例5.3 子串的插入。 在串指定的位置i 处插入子串t,需要从串的最末尾处将字符向后移动L 个单元(L 为 第5章 串79 串t 的长度),然后再将t 插入对应位置,如图5.1所示。 图5.1 子串插入 完整代码如下。 //在串的指定位置插入子串 //pos: 插入子串的位置 //t: 要插入的子串 bool String::strins(int pos,String t){ int off =t.getlength(); //串和子串的长度大于串的最大存储空间,插入失败,返回 if(this->getlength() +off >=MAXLENGTH) return false; //从串的最后字符开始,将每个字符移动到off 个存储单元之后 for(int i =this->length -1; i >=pos; i --){ this->s[i +off]=this->s[i]; } char* st =t.toarray(); //pos 至pos+off 的位置已空,可以将t 的内容逐一复制过来 for(int i =0; i <off; i ++){ this->s[i +pos]=st[i]; } this->length +=off; this->s[this->length]='\0'; return true; } int main(){ String s; s ="substring"; String t(" of a "); cout<<"source:"<<s.toarray()<<endl; cout<<"insert a string at the 3rd char:"<<t.toarray()<<endl; if(s.strins(3,t)){ cout<<s.toarray()<<endl; } return 0; } 80 数据结构与问题求解(C++版·微课版) 运行结果: source:substring insert a string at the 3rd char: of a after inserted:sub of a string 5.2.5 串的复制 例5.4 串的复制。 将一个串复制,构成另外一个串。通过指定复制子串开始的位置start以及子串长度 len,构造新的串对象返回。为了方便操作,提供了三个重载函数,包括指定开始start和长 度len的函数,只有开始位置start(长度为串长L-start),没有参数(复制整个串)。 完整代码如下。 //复制指定子串,从start 开始,长度为len String String::strcpy(int start, int len){ char* chs =this->toarray(); String ss(chs,start,len); //根据串的内容,以及start、len 的限定构造String 对象 return ss; } //重载strcpy,只提供start,意味着从start 开始直到串的结尾 String String::strcpy(int start){ return strcpy(start,this->getlength() -start); //构造完整的参数列表,调用第一个函数 } //重载strcpy,无参数,意味着完整复制 String String::strcpy(){ return strcpy(0,this->getlength()); //构造完整的参数列表,调用第一个函数 } int main(){ String s; s ="substring"; String cp1 =s.strcpy(2,5); String cp2 =s.strcpy(5); String cp3 =s.strcpy(); cout<<cp1.toarray()<<endl; cout<<cp2.toarray()<<endl; cout<<cp3.toarray()<<endl; return 0; } 运行结果: bstri ring 第5章 串81 substring 5.2.6 串的比较 例5.5 串的比较。 比较两个串的大小,直接调用strcmp()实现: int String::compare(String t){ return strcmp(this->s,t.toarray()); } 5.2.7 串的拼接 例5.6 串的拼接。 将一个串拼接在另一个串的后面形成一个更长的串,代码如下。 String& String::concat(String ss){ int base =this->getlength(); int len =ss.getlength(); if(base +len >=MAXLENGTH) throw "overflow"; for(int i =0; i <ss.getlength(); i ++){ this->s[base +i]=ss.getchar(i); } length +=ss.getlength(); this->s[length]='\0'; return*this; } int main(){ String s,ss(" C++"); s ="hello world!"; try{ cout<<"s1:"<<s.toarray()<<endl; cout<<"s2:"<<ss.toarray()<<endl; s =s.concat(ss); cout<<"after concat:"<<endl<<s.toarray()<<endl; }catch(const char* err){ } return 0; } 运行结果: s1:hello world! s2: C++ after concat: hello world! C++ 82数据结构与问题求解(C++版·微课版) ..5.3串的模式匹配算法 5.3.1暴力匹配 例5.7串的暴力匹配。 串的匹配是串的重要操作,主要用于从主串中查找到模式串的位置,其中,主串一般指 长的串,用于检测是否包含较短的模式串。 暴力匹配的过程如图5.2所示。 图5.暴力匹配的过程 2 从图5.主串和模式串都从头开始逐一比对, 则主串 2可知, 如果对应位置的字符相同, 和模式串都向后移动一个单位,继续比对。当对应位置的字符不相同时,称为失配,此时主 串回溯至上次匹配开始的后一个位置,而模式串则回溯至第一个位置,如图5.f)所示。 代码如下。 2( 第5章 串83 int String::index(String t){ int n =this->getlength(); int m =t.getlength(); bool matched =true; char* st =t.toarray(); int i,j; for(i =0; i <n -m +1; i ++){ matched =true; for(j =0; j <m; j ++){ if(s[i+j]!=st[j]){ matched =false; break; } } if(matched){ return i; } } return -1; } 暴力匹配的思路简单、操作易行,但其中进行了大量的回溯,特别是主串上回溯的次数 过多,导致算法效率低下。假定主串的长度为n,模式串的长度为m ,则算法的复杂度为O (nm )。 5.3.2 KMP匹配算法 KMP匹配 算法 根据暴力匹配过程,当主串从i+1的位置开始与模式串中的字符逐个匹配,但在i+k 的位置失配,即s[i+k]≠t[k],如图5.3所示。此时,主串需要从i+k 的位置回溯到i+2 的位置,模式串需要从k 回溯到开头位置,讨论如下两种情况。 图5.3 匹配失败主串不需要回溯 (1)如果从i+2开始,主串与模式串能够进行完全匹配,则字符串s[i+2.. i+k-1]= t[1..k-2],此时需要逐一判断从s[i+k]开始的主串字符是否与t[k-1]开始的模式串字 符相同,因此在i+k 匹配失败后,如果主串与模式串能够匹配,则只需要判断从i+k 位置 开始的后续字符是否相同,因此,主串不用回溯。 (2)如果从i+2开始主串与模式串不能完全匹配,则主串需要从i+3的位置开始判 断主串与模式串是否相同,如果从i+3的位置开始主串与模式串匹配,则情况等同于情况 (1),主串仍然不用回溯。如果不能完全匹配,持续试探i+4,…,i+k,其推理过程与情况 (1)(2)相同。 84数据结构与问题求解(C++版·微课版) 综合以上两点可知,在匹配过程中如果匹配失败,主串不用回溯。 由于主串不回溯,则只需回溯模式串,因此在主串i+k处存在如图5.4所示的部分匹 配关系。 图5.4模式串回溯存在的部分匹配 图5.4中模式串回溯至前缀P3 后面的位置j时,P3 可以与主串i+k处的后缀P1 匹 配,则必然存在后缀P1、P2 与前缀P3 相等,即在模式串k处失配时,模式串中存在相同的 前后缀P3、P2(公共前后缀)。实际中可能有长度不同的多组前后缀,为了不丢失可能的匹 配,需要将模式串回溯至最长前缀之后的位置重新开始匹配。假设前缀P3、后缀P2 为最 长公共前后缀,当模式串在位置k处失配时,需要回溯到最长公共前缀后面的一个位置j, 记为next[k]=j,并从位置j开始与主串进行匹配。 下面以图5.5为例说明模式串的回溯过程。当主串和模式串在位置k处失配,主串仍 然保持在 k 处,模式串回溯。由于模式串在位置 k 失配时的最长公共前后缀为“AB”,长度 为2,所以子串回溯的位置为2,并从位置2处继续向后匹配。 图5.模式串回溯示例 5 由于模式串的任意位置都可能失配,所以需要计算模式串中每个位置失配时的最长公 共前后缀。图5.即n1]如果t[t[1], 6中存在最长公共前后缀A=B, t[k, k] 则next[= k +1 。 exj-==j 如果 jt] [j-1],则不能在A、B的基础上继续生成在 j 处失配时的最长公共前后 k]≠t[ 缀,需搜索新前缀A1(i],j-1]),并存在后缀B1 与A1 相同,如 图5. nexAj1- =t[1. i<k=next[ A2, 7所示。 记在位置k=t[1]处失配时的最长公共前后缀为A1、因为A=B,所以A2= B1,联合A1=A2 可知A1=B。因此,在1~k-1的范围内存在前缀A1,并在位置j-1左 侧存在后缀B1 与前缀A1 相同。(1)