4.1 串类型的定义
串的实例
™ 文字处理,编程语言,信息检索等实际应用中,经常需要处理各种字符串
™ 如“I love China.”,“if(c<0) return false”,“data structure”,等等都是字符串的实例。
串是在计算机应用中使用最广泛的一种数据结构。
一、串的基本概念
v串:是零个或多个字符组成的有限序列。一般记作S= ‘a1a2a3…an’,其中:
™S 是串名,单引号括起来的字符序列是串值;
™ai(1≤i≤n)可以是字母、数字或其它字符;
™串中所包含的字符个数称为该串的长度。
™长度为零的串称为空串(Empty String),它不包含任何字符。
™仅由一个或多个空格组成的串称为空白串(Blank String)
注意:空串和空白串的不同,例如‘ ’ 和‘’分别表示长度为2的空白串和长度为0的空串。
串相对线性表的特殊性:数据元素类型必须是字符型
™子串与主串:串中任意连续的字符组成的子序列称为该串的子串。包含子串的串相应地称为主串。
™子串的位置:子串的第一个字符在主串中的序号。
™串相等:两个串的长度相等且对应字符都相等。
例如 A=‘This is a string’ B=‘is’ 则:
vB是A的子串,A为主串。
vB在A中出现两次,首次出现所对应的主串位置是3。则B在A中的序号(或位置)为3。
v空串是任意串的子串,任意串S是其自身的子串。除S本身外,S的其他子串称为S的真子串
例1 现有以下4个字符串:Sa =‘BEI’ Sb =‘JING’
Sc = ‘BEIJING’ Sd = ‘BEI JING’
问:① 他们各自的长度?
② Sa是哪个串的子串?在主串中的位置是多少?
③ 空串是哪个串的子串? Sa是不是自己的子串?
答:
①Sa =3,Sb =4,Sc = 7,Sd=8
②Sa是Sc和Sd的子串,在Sc和Sd中的位置都是1
③ 空串是任意串的子串; Sa是其本身的子串。
二、串的基本操作
1.求串长strlength(s)
2.串赋值strassign(s1,s2)
3.连接操作strconcat(s1,s2,s)或strconcat(s1,s2)
\4. 求子串substr (s,i,len)
5.串比较 strcmp(s1,s2)
6.子串定位 strindex(s,t)
7.串插入 strinsert(s,i,t)
8.串删除 strdelete(s,i,len)
9.串替换strrep(s,t,r)
1.求串长strlength(s)
Ø操作条件:串s存在
Ø操作结果:求出串s的长度。
2.串赋值strassign(s1,s2)
Ø操作条件: s1是一个串变量,s2是一个串常量串变量(通常,s2 是串常量时称为串赋值,是串变量称为串拷贝)。
Ø操作结果:将s2的串值赋值给s1, s1原来的值被覆盖掉。
3.连接操作 strconcat (s1,s2,s) 或strconcat (s1,s2)
Ø操作条件:串s1,s2存在。
Ø操作结果:两个串的联接就是将一个串的串值紧接着放在另一个串的后面,连接成一个串。前者是产生新串s,s1和s2不改变; 后者是在s1的后面联接s2的串值,s1改变, s2不改变。
如:s1=“he”,s2=“ bei”, strconcat (s1,s2,s)操作结果是s=“he bei”; strconcat (s1,s2)操作结果是s1=“he bei”。
4.求子串substr (s,i,len)
Ø操作条件:串s存在在,1≤i≤strlength(s),0≤len≤strlength(s)-i+1。
Ø操作结果:返回从串s的第i个字符开始的长度为 len 的子串。len=0得到的是空串。
Ø例如:substr(“abcdefghi”,3,4)= “cdef”
5.串比较 strcmp(s1,s2)
Ø操作条件:串s1,s2存在。
Ø操作结果:若s1==s2,操作返回值为0;若s1<s2, 返回值<0;若s1>s2, 返回值>0。
6.子串定位 strindex(s,t):子串t在主串s中首次出现的位置
Ø操作条件:串s,t存在。
Ø操作结果:若t∈s,则返回t在s中首次出现的位置,否则返回值为-1。
如:strindex(“abcdebda”, “bc”)=2
7.串插入 strinsert(s,i,t)
Ø操作条件:串s,t存在, 1≤i≤strlength(s)+1。
Ø操作结果:将串t插入到串s的第i个字符位置上,s的串值发生改变。
8.串删除 strdelete(s,i,len)
Ø操作条件:串s存在,1≤i≤strlength(s),0≤len≤strlength(s)-i+1。
Ø操作结果:删除s中从第i个字符开始的长度为len的子串,s的串值改变。
9.串替换strrep(s,t,r)
Ø操作条件:串s,t,r存在,t不为空。
Ø操作结果:用串r替换串s中出现的所有与串t相等的不重叠的子串,s的串值改变。
以上是串的几个基本操作。其中前5个操作是最为基本的,它们不能用其他的操作来合成,因此通常将这5个基本操作称为最小操作集。
例2 设 s =’I AM A STUDENT’, t=’GOOD’,
求:StrConcat(SubStr(s,6,2),
StrConcat(t,SubStr(s,7,8)))=?
解:SubStr(s,6,2)=‘A ’;
SubStr(s,7,8)=‘ STUDENT’
StrConcat(t,SubStr(s,7,8))=’GOOD STUDENT’
所以:
StrConcat(SubStr(s,6,2),
StrConcat(t,SubStr(s,7,8)))=‘A GOOD STUDENT’
4.2 串的顺序表示和 实现
因为串是数据元素类型为字符型的线性表,所以线性表的存储方式仍适用于串,也因为字符的特殊性和字符串经常作为一个整体来处理的特点,串在存储时还有一些与一般线性表不同之处。
v串有三种存储方法:
™定长顺序存储表示
™堆分配存储表示
块链存储表示
4.2.1 串的定长顺序存储表示
v用一组地址连续的存储单元存储串值中的字符序列
v所谓定长是指按预定义的大小,为每一个串变量分配一个固定长度的存储区,如:
#define MAXSIZE 256
char s[MAXSIZE];
则串的最大长度不能超过256。
v问题:如何标识实际长度?
方法1:用一个指针来指向最后一个字符,描述如下:
typedef struct
{char data[MAXSIZE];
int curlen; } seqstring;
该存储方式可直接得到串的长度:s.curlen+1。
如图所示。
方法2:在串尾存储一个不会在串中出现的特殊字符作为串的终结符,表示串的结尾。例如,C语言中采用’\0’ 表示串的结束。如下图所示:
v这种存储方法不能直接得到串的长度,只能用判断当前字符是否是’\0’来确定串是否结束,从而求得串的长度。
方法3:设定长串存储空间:char s[MAXSIZE+1]; 串值存放在s[1]~s[MAXSIZE],s[0]存放串实际长度。
v优点:字符的序号和存储位置一致,应用更为方便。
v定长顺序存储特点
-
串中字符的存储空间是连续的
-
必须预先给出串长的上界
-
静态分配存储空间
串的定长顺序存储:
# define MAXSTRLEN 255//串空间的初始分配量
typedef unsigned char String[MAXSTRLEN+1]
p0号单元存放串的长度;
p串的实际长度可在预定义长度范围内随意,超过预定义长度的串值则被舍去,称之为“截断”。
顺序串的基本操作实现
1、串联接concat(t,s1,s2)
v 假设s1、s2和t都是String型的串变量;
v串t是由串s1联结串s2得到的,即串t的值的前一段,和串s1的值相等,串t的值的后一段和串s2的值相等。
v则只要进行相应的“串值复制”操作即可;
只是需按前述约定,对超长部分实施“截断”操作
基于串s1和s2的长度不同,串t的值可能有三种情况:
vs1[0]+s2[0]<= MAXSTRLEN,得到的串t是正确的结果。
vs1[0]<MAXSTRLEN,s1[0]+s2[0]>MAXSTRLEN,则将串s2的一部分截断,得到的串t只能包含串s2的一个子串。
vs1[0]=MAXSTRLEN,得到的串t并非联接结果,而和串S1相等。
算法描述如下。
Status Concat(String &T, String S1, String S2) {
// 用T返回由S1和S2联接而成的新串。
//若未截断,则返回TRUE,否则FALSE。
int i;
Status uncut;
if (S1[0]+S2[0] <= MAXSTRLEN) { // 未截断
for (i=1; i<=S1[0]; i++) T[i] = S1[i];
for (i=1; i<=S2[0]; i++) T[i+S1[0]] = S2[i];
T[0] = S1[0]+S2[0];
uncut = TRUE;
}
else
if (S1[0] < MAXSTRLEN) { // 截断
for (i=1; i<=S1[0]; i++) T[i] = S1[i];
for (i=S1[0]+1; i<=MAXSTRLEN; i++) T[i] = S2[i-S1[0]];
T[0] = MAXSTRLEN;
uncut = FALSE;
}
else { // 截断(仅取S1)
for (i=0; i<=MAXSTRLEN; i++) T[i] = S1[i];
uncut = FALSE;
}
return uncut;
} // Concat
2、求子串substr(&Sub,S,pos,len)
™求子串的过程即为复制字符序列的过程,将串S中从第pos个字符开始长度为len的字符序列复制到串Sub中。其中:
v1≤pos≤StrLength(S)且
v0≤len≤StrLength(S)-pos+1。
算法描述:
Status SubString(String &Sub, String S, int pos, int len)
{ int i;
if (pos <1||pos>S[0]|| len<0 || len > S[0]-pos+1)
return ERROR;
for(i=1; i<=len; i++)
Sub[i] = S[pos+i-1];
Sub[0] = len;
return OK;
} // SubString
串与线性表在操作上的异同 :
串的逻辑结构和线性表极为相似,区别仅在于串的数据对象约束为字符集。
串的基本操作和线性表有很大差别:
Ø线性表基本操作中大多以“单个元素”作为操作对象。
Ø串的基本操作通常以“串的整体”作为操作对象,如:在串中查找某个子串、求取一个子串、在串的某个位置上插入一个子串以及删除一个子串等。
4.2.2堆分配存储表示
Ø串的堆存储: 以一组地址连续的存储单元存储串值 的字符序列,但它们的存储空间是在程序执行过程中由动态分配而得到。也称为动态存储分配的顺序表。
Ø在C语言中,利用函数malloc( )和free( )进行串值所需空间的动态管理。 malloc()为每一个新产生的串分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址。
优点:
不限定串长最大长度,动态分配串
串的堆分配存储表示 :
typedef struct {
char *ch;//若非空串则按串长分配存储区,否则为NULL int length;// 串长度
} HString;
这种存储结构表示的串操作仍基于“字符序列的复制”进行。 ,
v例1:串插入操作。
Status StrInsert(HString &S,int pos,HString T)
{int i;
if (pos<1||pos>S.length+1) // pos不合法
return ERROR;
if (T.length) {// T非空,则重新分配空间,插入T
if (!(S.ch=(char*)
realloc(S.ch,(S.length+T.length)*sizeof(char))))
return ERROR;
for(i=S.length-1;i>=pos-1;--i)//为插入T而腾出位置
S.ch[i+T.length] = S.ch[i];
for (i=0; i<T.length; i++) // 插入T
S.ch[pos-1+i] = T.ch[i];
S.length+=T.length;
} return OK;
} // StrInsert
例2 堆分配存储方式下的串赋值函数
Status StrAssign(HString &T, char *chars)
{//生成一个串T, T值←串常量chars
if (T.ch) free(T.ch); //释放T原有空间
for (i=0, c=chars; *c; ++i, ++c); //求chars的串长度i
if ( !i ){T.ch = NULL; T.length = 0;}
else{
if (!(T.ch = (char*)malloc( i *sizeof(char))))
exit(OVERFLOW);
T.ch[0..i-1] = chars[0..i-1];
T.length =i;
} return OK;
} //StrAssign
下面两个算法参见教材P77
例3,串连接
int Concat(STRING *s1,STRING s2)
例4,求子串
int SubStr(STRING *s1,STRING s2,int start,int len)
串的定位
int Index(STRING s1,STRING s2)
{ len1=Length(s1); len2=Length(s2);
//计算s1和s2的长度
i=0; j=0; //设置两个扫描指针
while (i<len1&&j<len2) {
if (s1.str[i]==s2.str[j]) { i++; j++; }
else {i=i-j+1; j=0;} //对应字符不相等时,重新比较
}
if (j==len2) return i-len2+1;
else return 0;}
4.2.3 串的链式存储结构
顺序串上的插入和删除操作不方便,需要移动大量的字符。因此,可用单链表方式来存储串值,串的链式存储结构简称为链串。
typedef struct node // 结点结构
{ char data;
struct node *next;
} Chunk;
为了便于进行串的操作,当以链表存储串值时,除头指针外还可附设一个尾指针指示链表中的最后一个结点,并给出当前串的长度。
typedef struct { // 串的链表结构
Chunk *head, *tail; // 串的头和尾指针
int curlen; // 串的当前长度
} LString;
这种结构便于进行插入和删除运算,但如果串结构的每个数据元素是一个字符,则存储空间利用率太低。
为了提高存储密度,可使每个结点存放多个字符,称其为块链结构。
通常将结点数据域存放的字符个数定义为结点的大小。
串的链式存储结构在连接等操作中有一定的方便之处,但总的来说不如另两种存储结构灵活,占用存储空间大且操作复杂。
当结点大小大于 1时,串的长度不一定正好是结点的整数倍,因此要用特殊字符(如符号“#”)来填充最后一个结点,以表示串的终结。
对于结点大小不为1的链串,其类型定义为只需对上述的结点类型做简单的修改即可。
#define NODESIZE 80
typedef struct node
{ char data[NODESIZE];
struct node *next;
}Chunk;
4.3 串的模式匹配
Ø首先,回顾子串定位操作 int index(S, T, pos)
Ø功能:返回子串T在主串S中第pos个字符之后的位置,若不存在,则函数值为0。
Ø操作条件:串s,t存在, 1≤pos≤StrLength(S)
Ø操作结果:若t∈s,则操作返回t在s中首次出现的位置,否则返回值为0。
Ø子串的定位操作是串处理中最重要的运算之一。
顺序存储结构的实现程序:
int S_index(SString t,SString p,int pos)
{
int n,m,i,j;
m=strlen(t);n=strlen(p);
for(i=pos-1;i<=m-n;i++)
{
for(j=0;j<n&&t[i+j]==p[j];j++);
if(j==n) return(i+1);
}
return(0);
}
v设串: s=“a1a2…an”, T=“b1b2…bm”(m≤n)
v子串定位:是指在主串S中找出一个与子串T相同的子串。
™通常把主串S称为目标串,
™把子串T称为模式串
v从主串S中查找模式串T的过程称为“模式匹配”。
匹配结果:成功或失败两种
ØS中有模式为T的子串,就返回该子串在S中的位置,当S中有多个模式为T的子串,通常只找出第一个子串—匹配成功;
ØS中无模式为T的子串,返回值为零—匹配失败。
以定长顺序结构表示串时,常有两种算法: BF算法和 KMP算法。
一、 BF算法
又称古典或经典的、朴素的、穷举的算法
ØBF算法设计思想:
u将主串的第pos个字符和模式的第1个字符比较:
u相等,继续逐个比较后续字符;
u不等,从主串的下一字符(pos+1)起,重新与第一个字符比较。
u直到主串的一个连续子串字符序列与模式相等,匹配成功,返回S中与T匹配的子序列第一个字符的序号。
否则,匹配失败,返回值 0 .
2、 BF算法的实现—Index()操作的实现
int Index(SString S, SString T, int pos) {// 返回子串T在主串S中第pos个字符之后的位置。
// T非空,1≤pos≤StrLength(S)。 i = pos; j = 1; while (i <= S[0] && j <= T[0]) { if (S[i] == T[j]) { ++i; ++j; } // 继续比较后继字符 else { i = i-j+2; j = 1; } // 指针后退重新开始匹配 } if (j > T[0]) return i-T[0]; else return 0;} // Index
二、KMP算法
KMP克努特-莫里斯-普拉特)算法思想:每当一趟匹配过程中出现字符比较不等时,不需i指针回溯,而是利用得到的部分匹配的结果,将模式向右滑动尽可能远的一段距离后,继续进行比较。
Ø需要讨论的两个问题: (1)如何由当前部分匹配结果确定模式向右滑动的新比较起点k? (2)模式应该向右滑多远才是高效率的? 算法描述:自学
vKMP算法的时间复杂度
由于指针i无须回溯,比较次数仅为n,即使加上计算next[j]时所用的比较次数m,比较总次数也仅为:
n+m=O(n+m) 。
注意:由于BF算法在一般情况下的时间复杂度也近似于O(n+m),所以至今仍被广泛采用。
4.4 串操作应用举例
文本编辑:实质是修改字符数据的形式或格式,包括串的查找、插入、删除等基本操作。
例如: Microsoft word ,其工作的基础原理都是文本编辑。虽然各种文本编辑程序的功能强弱不同,但是其基本操作是一致的,一般都包括串的查找,插入和删除等基本操作。
思路:
Ø用换页符将文本划分成若干页,用换行符将每页划分成若干行。页是文本串的子串,行是页的子串。
Ø在编辑程序中,先为文本串建立相应的页表和行表,在程序中设立页指针、行指针和字符指针,分别指向当前操作的页、行和字符。
Ø进行文本编辑的过程,就是一个对行表、页表进行查找、插入或删除的过程。
假设有下列一段C的源程序 s
main(){
float a,b,max;
scanf(“%f,%f”,&a,&b);
if a>b max=a;
else max=b;
};
小结
v串的概念(定义)
v串的存储(实现)
v串的操作
v串的匹配
串操作的应用
典型题例
例:用带头结点的单链表存储串(结点大小为1),编写算法,实现串的模式匹配算法。
【问题分析】
该算法类同顺序串的简单模式匹配,实现匹配过程需考虑链表的特征(从头比较的技术,指针保留的技术)
【算法思想】
从主串s的第一个字符和模式串t的第一个字符开始比较
u相等,继续比较后续字符。
u不等,则从主串s的下一个字符开始重新和模式串t比较。
u一直到模式串t中的每一个字符依次和主串s中的对应字符相等,则匹配成功,返回主串的当前起始位置指针。
u如果主串中没有和模式串相同的子串,则称匹配不成功,返回空指针NULL。
【算法描述】
Link *StrIndex(LKString *s, LKString *t)
/* 求模式串t在主串s中第一次出现的位置指针 */
{ Link *sp, *tp, *start;
if (t->len == 0) return s->head->next;
/* 空串是任意串的子串 */
start = s->head->next;
/* 记录主串的起始比较位置 */
sp = start;
/* 主串从start开始 */
tp = t->head->next;
/* 模式串从第一个结点开始 */
while(sp != NULL && tp != NULL)
{
if (sp->ch == tp->ch) /* 若当前对应字符相同,则继续比较 */
{
sp = sp->next;
tp = tp->next;
}
else /* 发现失配字符,则返回到主串当前起始位置的下一个结点继续比较*/
{
start = start->next; /* 更新主串的起始位置 */
sp = start;
tp = t->head->next; /* 模式串从第一个结点重新开始 */
}
}
if ( tp == NULL) return start; /* 匹配成功,返回主串当前起始位置指针 */
else return NULL; /* 匹配不成功,返回空指针 */
}
补充:KMP(D.E.Knuth, V.R.Pratt,J.H.Morris) 算法
StrIndex(SString s,int pos, SString t)
当 S[i] <> T[j] 时,
已经得到的结果:
S[i-j+1…i-1] == T[1…j-1]
若已知 T[1…k-1] == T[j-k+1…j-1]
则有 S[i-k+1…i-1] == T[1…k-1]
定义:模式串的next函数
int Index_KMP(SString S, SString T, int pos) {
// 1≤pos≤StrLength(S)
i = pos; 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;
} // Index_KMP
求next函数值的过程是一个递推过程,分析如下:
已知:next[1] = 0;
假设:next[j] = k;又 T[j] = T[k]
则: next[j+1] = k+1
若: T[j] !=T[k]
则需往前回朔,检查 T[j] = T[ ?]
这实际上也是一个匹配的过程,
不同在于:主串和模式串是同一个串
void get_next(SString &T, int &next[] ) {
// 求模式串T的next函数值并存入数组next
i = 1; next[1] = 0; j = 0;
while (i < T[0]) {
if (j = 0 || T[i] == T[j])
{++i; ++j; next[i] = j; }
else j = next[j];
}
} // get_next
还有一种特殊情况需要考虑:
例如:
S = ¢aaabaaabaaabaaabaaab¢
T = ¢aaaab¢
next[j]=01234
nextval[j]=00004
void get_nextval(SString &T, int &nextval[]) {
i = 1; nextval[1] = 0; j = 0;
while (i < T[0]) {
if (j = 0 || T[i] == T[j]) {
++i; ++j;
if (T[i] != T[j]) next[i] = j;
else nextval[i] = nextval[j];
}
else j = nextval[j];
}
} // get_nextval