王道数据结构(串4)

一、串是什么?

  1. 字符串 string 通常是字符序列,要么是文字常量,要么是某种变量。后者可能允许其元素发生突变,长度改变,或者是固定的(在创建之后)。
  2. 字符串通常被认为是一种数据类型,通常作为字节(或字)的数组数据结构来实现,它存储了一系列元素,通常是使用一些字符编码的字符。
  3. 字符串还可以用来表示更一般的数组或其他序列(或列表)数据类型和结构。
  4. 根据所使用的编程语言和精确的数据类型,声明为字符串的变量可能会导致内存中的存储被静态地分配给预先确定的最大长度,或者可以使用动态分配来容纳可变数量的元素。
  5. 当一个字符串在源代码中出现时,它被称为字符串文字或匿名字符串。
  6. 在数学逻辑和理论计算机科学中使用的正式语言中,string 是由一个叫做字母表的集合所选择的有限的符号序列。
  7. 串的实现形式有两种,堆分配和块链存储,本文讲述块链存储
  8. 串是由零个或多个字符组成的有限序列 一般记为: S = ‘ a 1 a 2 a 3 … a n ’ S=‘a1a2a3…an’ S=a1a2a3an其中S是串名,双引号括起来的就是串值,ai可以是数字字母或者其他字符,字符个数n是串的长度(包括空格),n=0时(长度为0)的串为空串,构成串的所有字符都是空格的串称为空格串,特别的,空串是任意串的子串,任意串是自身的子串。
  9. 串中任意多个连续的字符组成的子序列称为该串的子串,包括子串的串称为主串
  10. 子串在主串的位置就是子串第一个字符在主串中的位置来表示
  11. 当两个串的长度相同、对应字符都相同,那么这两个串相同
  12. 串的位序从1开始
  13. 串常量与整常数、实常数一样,在程序中只能被引用但不能不能改变其值,即只能读不能写,通常串常量是有直接量来表示的,例如语句错误(“溢出”)中“溢出”就是直接量、串变量是可以改变的
  14. 串的逻辑结构与线性表很是相似,区别在于串的数据对象限定为字符集
  15. 线性表的基本操作是以单个元素作为操作对象,而串是以字串作为操作对象

二、基本操作

1. 概述

假设有串 T = ‘’, S = ‘iPhone 11 Pro Max?’, W = ‘Pro’
(1)StrAssign(&T, chars): 赋值操作,把串T赋值为chars;
(2)StrCopy(&T, S): 复制操作,把串S复制得到串T
(3)StrEmpty(S): 判空操作,若S为空串,则返回TRUE,否则返回False;
(4)StrLength(S): 求串长,返回串S的元素个数;
(5)ClearString(&S): 清空操作,将S清为空串;
(6)DestroyString(&S): 销毁串,将串S销毁——回收存储空间;
(7)Concat(&T, S1, S2): 串联联接,用T返回由S1和S2联接而成的新串———可能会导致存储空间的扩展;
(8)SubString(&Sub, S, pos, len): 求子串,用Sub返回串S的第pos个字符起长度为len的子串;
(9)Index(S, T): 定位操作,若主串S中存在与串T值相同的子串,则返回它再主串S中第一次出现的位置,否则函数值为0;
(10)StrCompare(S, T): 串的比较操作,参照英文词典排序方式;若S > T,返回值>0; S = T,返回值=0 (需要两个串完全相同) ; S < T,返回值<0;

2.串的实现

2.1 定长顺序存储表示

(1)代码实现

串定义成字符数组,利用串名可以直接访问串值,用这种表示方式,串的存储空间在编译时确定,其大小不能改变

#define MAXLEN 255   //预定义最大串长为255

typedef struct{
    char ch[MAXLEN];   //静态数组实现(定长顺序存储)
                       //每个分量存储一个字符
                       //每个char字符占1B
    int length;        //串的实际长度
}SString;
(2)串长的表示法

(1)方案一:用一个额外的变量length来存放串的长度(保留ch[0]);
(2)方案二:用ch[0]充当length;
优点:字符的位序和数组下标相同;
(3)方案三:没有length变量,以字符’\0’表示结尾(对应ASCII码的0);
缺点:需要从头到尾遍历;
(4)方案四——最终使用方案:ch[0]废弃不用,声明int型变量length来存放串的长度(方案一与方案二的结合
基本操作实现(基于方案四)

2.2 堆分配存储表示

(1)堆存储结构的特点

仍以一组空间足够大的、地址连续的存储单元依次存放字符序列,但它们的存储空间实在程序执行过程种动态分配的 。
通常,C语言提供的串类型就是以这种存储方式实现的。由动态分配函数malloc()分配一块实际串长所需要的存储空间(“堆”),如果分配成功,则返回此空间的起始地址,作为串的基址。由free()释放串不再需要的空间,

(2)堆存储结构的优点

堆存储结构既有顺序存储结构的特点,处理(随机取子串)方便,操作中对串长又没有任何限制,更显灵活,因此在串处理的应用程序中常被采用。

(3)代码实现
//动态数组实现
typedef struct{
    char *ch;           //按串长分配存储区,ch指向串的基地址
    int length;         //串的长度
}HString;
    HString S;
    S.ch = (char *) malloc(MAXLINE * sizeof(char)); //基地址指针指向连续空间的起始位置
    //malloc()需要手动free()
    S.length;

(4)串的连接操作
bool Hstring *StrConcat(Hstring *T,Hstring *s1,Hstring *s2){
    int k,j,t_len;
    if(T.ch) free(T);//释放旧空间
    t_len=s1->length+s2->length;
    if((p=(char*)malloc(sizeof((char)*t_len))==NULL))
    {
       printf("系统空间不足,申请空间失败")return false;
    }
    for(j=0;j<s1->length;j++){
      T->ch[j]=s1->ch[j];
    }
      for(k=s1->length,j=0;j<s2->length;j++,k++){
      T->ch[j]=s1->ch[j];
    }
    free(s1->ch);
    free(s2->ch);
    return true;
}

2.3 串的链式存储

一种链式储存结构。

(1)基础实现方法
typedef struct StringNode{
    char ch;           //每个结点存1个字符
    struct StringNode *next;
}StringNode, * String;
(2)块链结构

上述问题:存储密度低,每个字符1B,每个指针4B;
解决方案:每一个链表的结点存储多个字符——每个结点称为块——块链结构

  1. 块结点的结构定义
typedef struct StringNode{
    char ch[4];           //每个结点存多个个字符
    struct StringNode *next;
}StringNode, * String;
  1. 块链串的类型定义
typedef struct {
    StringNode head;          //头指针
    int Strlen;
}Blstring;
  • 在这种存储结构下,结点的分配总是完整的结点为单位,因此,为使一个串能UC你党在整数个结点中,在串末尾填上不属于串值的特殊字符,表示串的终结
  • 当一个块内存放多个字符时,往往会使操作过程变得更加复杂,如在串中插入和删除字符操作时通常需要在块间移动字符
(3)结合链表思考优缺点
  • 存储分配角度:链式存储的字符串无需占用连续空间,存储空间分配更灵活;
  • 操作角度:若要在字符串中插入或删除某些字符,则顺序存储方式需要移动大量字符,而链式存储不用;
  • 若要按位序查找字符,则顺序存储支持随机访问,而链式存储只支持顺序访问;

3.串的基本操作

3.1 初始化串

int StrAssign(BBT &T,char *chars){
	int i ;
	char *c;
//	if(T.ch)
//		free(T.ch);
	for(i=0,c=chars;*c;i++,c++);
	if(!i){
		T.ch=NULL;
		T.length=0;
	}//如果所求的串是空串 
	else{
		if(!(T.ch=(char*)malloc(i*sizeof(char)))){
			return -1;
		}
		for(int n=0;n<=i-1;n++)
			T.ch [n]=chars[n];
		T.length =i;
		}
	return 0; 
}

3.2 求子串的功能实现

SubString(&Sub,S,pos,len):求子串,用Sub返回串S的第pos个字符起长度为len的子串

bool SubString(SString &Sub, SString S, int pos, int len){
    //子串范围越界
    if (pos+len-1 > S.length)// 从1开始为下标,注意需要-1
        return false;
    for (int i=pos; i<pos+len; i++)// 注意是<而非≤
        Sub.cn[i-pos+1] = S.ch[i];//储存这个子串,序号从1开始
    Sub.length = len;
    return true;
}

3.3 两串比较的功能实现

StrCompare( S, T):比较操作,若S>T返回值>0;反之,相等返回0,S<T 返回<0

int StrCompare(SString S, SString T){
    for (int i; i<S.length && i<T.length; i++){
        if(S.ch[i] != T.ch[i])
            return S.ch[i] - T.ch[i];
    }
    //扫描过的所有字符都相同,则长度长的串更大
    return S.length - T.length;
}

3.4 定位功能

Index(S, T):定位操作,如果主串s存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0

int Index(SString S, SString T){
    int i=1;
    n = StrLength(S);
    m = StrLength(T);
    SString sub;        //用于暂存子串
    while(i<=n-m+1){ //只需要比较n-m-1次,需要思考
        SubString(Sub,S,i,m); //取出从位置 i开始,长度为m的子串
        if(StrCompare(Sub,T)!=0)
            ++i;
        else 
            return i;    // 返回子串在主串中的位置
    }
    return 0;            //S中不存在与T相等的子串
}

3.5 求串的长度功能

int StrLength(SString S){
	return S.length;
} 

3.6 串的输出功能

void StrPrint(SStringg T)
{
	int i;
	for(i=0;i<T.length;i++)
	{
		printf("%c",T.ch[i]);
	}
} 

3.7 判断串的是否为空功能

bool StrClear(SString &S)
{
	if(S.ch)
	{    free(S.ch);
		S.ch=NULL;
	}
	S.length=0;
	return true;
}

3.8 串的连接操作

bool StrConcat(SString s,SString t){
    int i,j;
    if((s.length+t.length)>MAXLEN)
        return false;
    for(i-0;i<t.length;i++){
        s.ch[s.length+1]=t.ch[i];
        s.length=s.length+t.length;
    }
    return true;
}

三、 模式匹配法

模式匹配:子串的定位操作称为串的模式,它求的是子串(常称模式串)在主串中的位置。

1. 朴素模式匹配算法(Brute-Force)

(1) 思想:找到主串中所有长度为m的子串与模式串进行对比,实现暴力匹配,直到找到一个完全匹配的子串或所有子串都不匹配为止。
① 设置两个指示ij分别指向主串和模式串
② 比较当前指示字符,相同则ij后移,表示目前匹配成功
③如果不相同,则匹配失败,则主串指针i指向下一个子串的第一个位置,模式串指针j回到模式串的第一个位置
④ 如果模式串j>模式串长度,那么说明子串匹配成功,返回当前子串的第一个字符的位置 i - 模式串长度
(2)代码实现

int Index(SString S, SString T){
    int i=1;                //扫描主串S
    int j=1;                //扫描模式串T
    while(i<=S.length && j<=T.length){
        if(S.ch[i] == T.ch[j]){ //如果当前相同,则指示都后移
            ++i;
            ++j;             //继续比较后继字符
        }
        else{       
            i = i-j+2;   //i-j表示指向了当前匹配串的起始位置的前一个,+2表示下一个串的起始位置(+1是当前被匹配串的起始位置)
            j=1;             //指针后退重新开始匹配
        }
    }
    if(j>T.length)
        return i-T.length;
    else
        return 0;
}

(3)总结

  • 最多匹配n-m+1次
  • 其实与上面的定位操作类似
  • 时间复杂度:主串长度n,模式串m【实际操作过程中,时间复杂度接近于O(n+m)】
    最坏时间复杂度 = O(nm)
    每个子串都要对比m个字符(对比到最后一个字符才匹配不上),共要对比n-m+1个子串,复杂度 = O ( ( n − m + 1 ) m ) = O ( n m − m 2 + m ) = O ( n m ) O((n-m+1)m) = O(nm - m^2 + m) = O(nm) O((nm+1)m)=O(nmm2+m)=O(nm)
    PS:大多数时候,n>>m,所以删去的是 m 2 m^2 m2以及m
    最好时间复杂度 = O(n)
    每个子串的第一个字符就匹配失败,共要对比n-m+1个子串,复杂度 = O(n-m+1) = O(n)

2. KMP算法

  1. 改进在于:每当一趟匹配过程出现字符不相等时,主串指示不用回溯,
    而是利用已经得到的“部分匹配”结果,将模式串的指示器向右“滑动”尽可能远的一段距离后,继续进行比较
  2. 总之,在主串s与模式串t的匹配过程中,一旦出现si ≠ tj, 主串s的指针不必回溯,而是直接与模式串的 tk (0≤k<j) 进行比较,而k的取值与主串s无关,只与模式串t本身的构成有关,即从模式串t可求得k值。

(1) 求出next数组

  1. 规律:只看模式串,当前位置前面的子串最大公共位数+1(第一二位除外,第一位next[1]=0,第二位next[2]==1)
  2. 例子 :ababaaababaa
    ① 第一位next[1]=0,第二位next[2]==1
ababaaababaa
01

② 判断第三位前最大公共子串位数+1 ab最大为0 ,+1=1

ababaaababaa
011

③ 判断第四位前最大公共子串位数+1 aba最大为1,+1=2

ababaaababaa
0112

④ 判断第五位前最大公共子串位数+1 abab最大为2,+1=3

ababaaababaa
01123

⑤ 判断第六位前最大公共子串位数+1 ababa最大为3(aba与aba),+1=4

ababaaababaa
011234

⑥ 判断第七位前最大公共子串位数+1 ababaa最大为1(a与a),+1=2

ababaaababaa
0112342

⑥ 最终结果

ababaaababaa
011234223456
  1. 代码:
void next(SString t,int next[]){
    int k=1,j=0;
    next[1]=0;
    while(k<t.length){
        if((j==0)||(t.str[k]==t.str[j])){
            k++;j++;
            if(t.ch[k]==t.ch[j]){
                next[k]=j;
            }
            elsenext[j]=j;
        }
    } 
}

(2)进行KMP算法

1. 思想
① 设目标串(主串)为 s s s,模式串为 t t t,并设i指针和j指针分别指示目标串和模式串中正待比较的字符,设i和j的初值均为1。
② 若有 s = t s=t s=t,则 i i i j j j分别加1。否则, i i i不变,j退回到 j = n e x t [ j ] j=next[j] j=next[j]的位置,
③ 再比较si 和 tj,若相等,则 i i i j j j分别加1。否则, i i i不变, j j j再次退回到 j = n e x t [ j ] j=next[j] j=next[j]的位置,依此类推。
④ 直到下列两种可能:
(1) j j j退回到某个下一个 [ j ] [j] [j]值时字符比较相等,则指针各自加1继续进行匹配。
(2)退回到j=0,将 i i i j j j分 别加1,即从主串的下一个字符 si+1 模式串的 t1 重新开始匹配。
2. 代码

int Index_KMP(SString S, SString T, int next[]){
    int i=1;     //主串
    int j=1;     //模式串
    while(i<S.length && j<=T.length){
        if(j==0 || S.ch[i]==T.ch[j]){      //第一个元素匹配失败时
            ++j;
            ++i;         //继续比较后继字符
        }
        else
            j=next[j]   //模式串向右移动
    }
    if(j>T.length)
        return i-T.length; //匹配成功
}

(3)时间复杂度分析

  • 求next数组时间复杂度 = O(m)
  • 模式匹配过程最坏时间复杂度 = O(n)
  • KMP算法的最坏时间复杂度 = O(m+n)

四、练习题

1.区分概念:空串和空白串、主串和子串、目标串和模式串
2. 若x和y是两个采用顺序结构存储的串,写一算法实现比较两个字符串是否相等
3. 写一算法void StrReplace(char *T, char *P, char *S),将T中首次出现的子串P替换为串S。注意:S和P的长度不一定相等。可以使用已有的串操作。
  由于S和P的长度不一定相等,所以在替换时可能要移动字符元素。我们可以用到前面设计的一系列算法。算法如下:

#include<stdio.h>
#include<stdlib.h>
#define  MaxSize  100   //最大存储量,无分号 
typedef struct
{  char str[MaxSize + 1]; //静态,数组形式
    int length;	//串长度
}  StringType;
void crateStr(StringType* st) { st->length = 0;}
int isEmpty(StringType* st) {
    if (st->length == 0) {return 1; }
    else {  return 0; }
}
void strAssign(StringType* st1, char st2[], int n) {
    int i = 1;
    for (i = 1; i <= n; i++) {
        st1->str[i] = st2[i-1];//从第一个字符开始,着个赋值
        st1->length++;
    }
}
int insertString(StringType* st, int i, StringType* st1) {
    //将str1,插入str的第i个位置
    if (i <= 0 || i > st->length + 1) return 0;
    int k;
    for (k = st->length; k >= i; k--) {
        //将str中的后i个字符后移
        st->str[k + st1->length] = st->str[k]; }
    st->length = st->length + st1->length;
    for (k = 1; k <= st1->length; k++) {
        st->str[k + i - 1] = st1->str[k]; }
    return 1;
}
int delStr(StringType* st, int i, int len) {
    //从第i个位置开始,删除len长度的子串
    if (i <= 0 || i > st->length || len <0 ) { return 0; }
    int k;
    for (k = i + len; k <= st->length; k++) {
        //从str的i+len个位置开始将其后的所有字前移
        st->str[k - len] = st->str[k];  }  
    st->length -= len;
    return 1;
}
void dispStr(StringType* st) {
    int i;
    for (i = 1; i <= st->length; i++) {
        printf("%c\t",st->str[i]);  }
    printf("\n");
}
int BFIndex(StringType* s, StringType* t) {
    {     int i = 1, j = 1;
    while (i <= s->length && j <= t->length)
    {   if (s->str[i] == t->str[j])	//继续匹配下一个字符
        { i++;	 j++; }   //主串和子串依次匹配下一个字符     
        else	//主串、子串指针回溯重新开始下一次匹配
        {i = i - j + 2;	//主串从下一个位置开始匹配
         j = 1;
        } //子串从头开始匹配	
    }
    if (j >= t->length)  return(i - t->length);	//返回匹配的第一个字符的下标
    else    return 0;	//模式匹配不成功}
}
int strLen(StringType* t) {  return t->length;}
//char* StrRelace(char T[], char P[], char S[]) {
//    return 1;
//}
void replase(StringType* t, StringType* p, StringType* s) {
    int i = 0;
    i = BFIndex(t, p);
    delStr(t, i, strLen(p));
    insertString(t, i, s);
    dispStr(t);
}
int main() {
    char T[] = { 'a','b','c','d','e','f','g' };
    char P[] = { 'b','c','d' };
    char S[] = { 'a','b' };
    StringType t, p, s;
    crateStr(&t); crateStr(&p);crateStr(&s);
    strAssign(&t, T, 7);   strAssign(&p, P, 3);strAssign(&s, S, 2);
    dispStr(&t);  dispStr(&p);dispStr(&s);
    replase(&t, &p, &s);
}

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值