第4章:串

40 阅读5分钟

1、串的定义和实现

1.1 串的定义

串(String)是由零个或多个字符组成的有限序列,一般记为:S=a1a2anS='a_1a_2…a_n'。其中 SS 是串名,单引号括起来的字符序列是串的值;串中字符的个数 nn 称为串的长度。

空串:n=0n=0 时串称为空串

子串、主串:串中任意多个连续的字符组成的子序列称为该串的子串,包含子串的串称为主串

某个字符在串中的序号称为该字符串在串中的位置。子串在主串中的位置以子串的第一个字符在子串中的位置来表示

空格串:由一个或多个空格组成的串

1.2、串的基本操作

  • StrAssign(&T, chars):赋值操作。把串 T 赋值为 chars
  • StrCopy(&T, S):复制操作。由串 S 复制得到串 T
  • StrEmpty(S):判空操作。若 S 为空串,则返回 True,否则返回 False
  • StrCompare(S, T):比较操作。若 S>T,则返回值>0;若 S=T,则返回值=0;若 S<T,则返回值<0
  • StrLength(S):求串长。返回串 S 的元素个数
  • SubString(&Sub, S, pos, len):求子串。用 Sub 返回串 S 第 pos 个字符起长度为 len 的子串
  • Concat(&T, S1, S2):串连接。用 T 返回由 S1 和 S2 连接而成的新串
  • Index(S, T):定位操作。若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0
  • ClearString(&S):清空操作。将 S 清为空串
  • DestoryString(&S):销毁串。将串 S 销毁

1.3、串的存储结构

定长顺序存储表示

类似于线性表的顺序存储结构,用一组地址连续的存储单元来存储串值的字符序列,在串的定长顺序存储结构中,为每个串变量分配一个固定长度的存储区,即定长数组

#define MAXLEN 255		//预定义最大串长为255
typedef struct {
    char ch[MAXLEN];	//每个分量存储一个字符
    int length;			//串的实际长度
}SString;

堆分配存储表示

堆分配存储表示仍然以一组地址连续的存储单元存放串值的字符序列,但他们的存储空间是在程序执行过程中动态分配得到的

在 C 语言中,存在一个称为堆的自由存储区,并用 malloc() 和 free() 函数来完成动态存储管理

typedef struct{
    char *ch;		//按串长分配存储区,ch指向串的基地址
    int length;		//串的长度
}HString;

块链存储表示

类似于线性表的链式存储结构,也可采用链表方式存储串值。在具体实现时每个结点既可以存放一个字符,也可以存放多个字符(不足用#补充)。每个结点称为块,整个链表称为块链结构

2、串的匹配模式

2.1、简单的匹配模式算法(BF)

模式匹配是指在主串中找到与模式串(想要搜索的某个字符串)相同的子串,并返回其所在的位置

算法思想:从主串 S 的第一个字符起,与模式串 T 的第一个字符比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符起,再重新和模式串 T 的字符比较;以此类推,直至模式串 T 中的每个字符依次和主串 S 中的一个连续的字符序列相等,则称匹配成功,函数返回值为与模式串 T 中第一个字符相等的字符在主串 S 中的序号,否则称匹配不成功,函数返回值为 0。

在简单模式匹配中,设主串和模式串长度分别为 n 和 m(n>>m),则最多需要进行 n-m+1 趟比较,每趟最多需要进行 m 次比较,最坏时间复杂度为 O(nm)

int Index(String S, SString T){
    int i=1;	//主串中当前待比较字符位置
    int 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
}

2.2、串的模式匹配算法(KMP)

算法描述

KMP 算法每当一趟匹配过程中出现字符比较不相等时,主串 S 的 i 指针不需要回溯,而是利用已经得到的“部分匹配值(PM)表”结果将模式串向右滑动一段距离后,继续进行比较即可

右滑位数 = 已匹配的字符数 - 对应的部分匹配值

时间复杂度:O(n+m)

最长公共前后缀

前缀:除最后一个字符外,字符串的所有头部子串

后缀:除第一个字符外,字符串的所有尾部子串

最长公共前后缀:字符串的长度最大的前缀后缀相等的子串

部分匹配值:指字符串的前缀和后缀的最长相等前后缀长度

void get_next(SString T, int next[]){
    int i=1, j=0;
    next[1]=0;
    while(i<T.length){
        if(j==0 || T.ch[i]==T.ch[j]){
            ++i;++j;
            next[i]=j;
        }else{	
            j=next[j];	
        }
    }
}
int Index_KMP(SString S, SString T, int next[]){
    int i=1; j=1;
    while(i<=L.length && j<=T.length){
        if(j==0 || S.ch[i]==T.ch[j]){
            ++i;++j;
        }else{
            j=next[j];	//模式串向右滑动
        }
    }
    if(j>T.length)
        return i-T.length;
    else 
        return 0;
}

理论上普通模式匹配时间复杂度为 O(mn),KMP 算法时间复杂度为 O(m+n),但在一般情况下,普通模式匹配的实际时间复杂度近似为 O(m+n),因此至今仍被采用。KMP 算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快,其主要优点是主串不回溯

2.3、KMP算法优化

优化 next 数组,因为在某些情况下 next 数组仍然有缺陷,即当模式串出现多个相同字符的序列。这时候就需要对 next 数组进行修正,修正后的数组称为 nextval

右滑位数 = j - nextval[j]

void get_nextval(SString T, int nextval[]){
    int i=1, j=0;
    nextval[1]=0;
    while(i<T.length){
        if(j==0 || T.ch[i]==T.ch[j]){
            ++i;++j;
            if(T.ch[i]!=T.ch[j])
                nextval[i] = j;
            else
                nextval[i] = nextval[j]
        }else
            j=nextval[j];
    }
}