04-串

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。

如图所示。

04-方法1

方法2:在串尾存储一个不会在串中出现的特殊字符作为串的终结符,表示串的结尾。例如,C语言中采用’\0’ 表示串的结束。如下图所示:

1557808551638

v这种存储方法不能直接得到串的长度,只能用判断当前字符是否是’\0’来确定串是否结束,从而求得串的长度。

方法3:设定长串存储空间:char s[MAXSIZE+1]; 串值存放在s[1]~s[MAXSIZE],s[0]存放串实际长度。

1557808603209

v优点:字符的序号和存储位置一致,应用更为方便。

v定长顺序存储特点

  1. 串中字符的存储空间是连续的

  2. 必须预先给出串长的上界

  3. 静态分配存储空间

串的定长顺序存储:

# 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;

1557808774765

这种存储结构表示的串操作仍基于“字符序列的复制”进行。 ,

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; 

为了便于进行串的操作,当以链表存储串值时,除头指针外还可附设一个尾指针指示链表中的最后一个结点,并给出当前串的长度。

1557808905314

typedef struct { // 串的链表结构           
Chunk *head, *tail; // 串的头和尾指针
int curlen; // 串的当前长度
} LString;    

这种结构便于进行插入和删除运算,但如果串结构的每个数据元素是一个字符,则存储空间利用率太低。

1557808961498

为了提高存储密度,可使每个结点存放多个字符,称其为块链结构。

通常将结点数据域存放的字符个数定义为结点的大小。

串的链式存储结构在连接等操作中有一定的方便之处,但总的来说不如另两种存储结构灵活,占用存储空间大且操作复杂。

当结点大小大于 1时,串的长度不一定正好是结点的整数倍,因此要用特殊字符(如符号“#”)来填充最后一个结点,以表示串的终结。

1557808991118

对于结点大小不为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函数

1557809374199

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

猜你喜欢

转载自blog.csdn.net/qq_44621510/article/details/90204083