【第四章】串

本章是统考大纲第六章内容,单独作为一章,大纲只要求掌握字符串模式匹配,重点掌握KMP匹配算法的原理及next数组的推理过程,手工求next数组可以先计算出部分匹配值表然后变形,或根据公式来求解。了解nextval数组的求解办法。

*4.1 串的定义和实现

字符串简称,计算机上非数值处理的对象基本都是字符串数据。我们常见的信息检索系统(如搜索引擎)、文本编辑程序(如Word)、问答系统、自然语言翻译系统等,都是以字符串数据作为处理对象的。

本章详细介绍字符串的存储结构及相应的操作。

4.1.1 串的定义

(string)是由零个或多个字符组成的有限序列。一般记为:

\[S='a_{1}a_{2}\dots a_{n}'(n\geq0) \]

其中,

  • \(S\)串名
  • 单引号 'xxxx' 括起来的字符序列是串的值
  • \(a_{i}\) 可以是字母、数字或其他字符
  • 串中字符的个数 \(n\) 成为串的长度
  • \(n=0\) 时的串成为空串。(用 \(\emptyset\) 表示)
  • 子串:串中任意多个连续的字符组成的子序列称为该串的子串。
  • 串中的位置:某个字符在串中的序号称为该字符在串中的位置。
  • 子串在主串中的位置:以子串的第1个字符在主串中的位置来表示。
  • 当两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等的。

例如,有串 A='China BeijingB=BeijingC=China,则它们的长度分别是13、7和5。B和C是A的子串,B在A中的位置是7,C在A中的位置是1.

[!Caution] 注意
由一个或多个空格(空格是特殊字符)组成的串称为空格串(空格串不是空串),其长度为串中空格字符的个数。

串的逻辑与线性表极为相似,区别仅在于串的数据对象限定为字符集

在基本操作上,串和线性表有很大差别。

  • 线性表的基本操作主要以单个元素为操作对象,如查找、插入或删除某个元素等;
  • 而串的基本操作通常以子串为操作对象,如查找、插入或删除一个子串等。

4.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清空为空串。
  • DestroyString(&S):销毁串。
    • 将串S销毁。

不同的高级语言对串的基本操作集可以有不同的定义方法。

在上述定义的操作中,

  • 串赋值StrAssign
  • 串比较StrCompare
  • 求串长StrLength
  • 串联结Concat
  • 求子串SubString

这五种操作构成串类型的最小操作子集,即这些操作不可能利用其他串操作来实现;反之,其他串操作(除串清除ClearString和串销毁DestroyStrings外)均可在该最小操作子集上实现。

一共有十种操作,其中有一半是最小操作子集。

4.1.3 串的存储结构

1. 定长顺序存储表示

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

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

串的实际长度只能小于或等于 MAXLEN,超过预定义长度的串值会被舍去,称为

串长的两种表示方法:

  • 一是如上述定义描述的那样,用一个额外的变量 len存放串的长度
  • 二是在串值后面加一个不计入串长的结束标记字符\0,此时的串长为隐含值

在一些串的操作(如插入、联接等)中,若串值序列的长度超过上界 MAXLEN,约定用“截断”法处理,要克服这种弊端,只能不限定串长的最大长度,即采用动态分配的方式。

2. 堆分配存储表示

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

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

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

利用 malloc() 为每个新产生的串分配一块实际串长所需的存储空间,

  • 分配成功,则返回一个指向起始地址的指针,作为串的基地址,这个串由 ch 指针来指示;
  • 分配失败,则返回 NULL。已分配的空间可用 free() 释放掉。

上述两种存储表示通常为高级程序设计语言所采用。块链存储表示仅作简单介绍。

3. 块链存储表示

  • 类似于线性表的链式存储结构,也可采用链表方式存储串值。
  • 由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点既可以存放一个字符,又可以存放多个字符。
  • 每个结点称为,整个链表称为块链结构

最后一个结点占不满时通常用“#”补上。

4.2 串的模式匹配

4.2.1 简单的模式匹配算法

模式匹配是指在主串中找到与模式串(想要搜索的某个字符串)相同的子串,并返回其所在的位置。这里采用定长顺序存储结构,给出一种不依赖于其他串操作的暴力匹配算法。

int Index(SString S, SString T) {
    int i=1,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;
}

在上述算法中,分别用计数指针i和j指示主串S和模式串T中当前待比较多字符位置。

算法思想是:

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

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

例如,当模式串为 '0000001' 而主串为 '000000000000000000000000000000000000000000000 0000001' 时,由于模式串中前6个字符均为 '0',主串中的前45个字符均为 '0',每趟匹配都是比较到模式串中的最后一个字符时才发现不等,整个匹配过程中指针 i 需要回溯39次,总比较次数为 \(4\times7=280\) 次。

4.2.2 串的模式匹配算法——KMP算法

关于KMP算法,看视频的动画很容易就理解了,但在课本上却有较大的篇幅,这里省略大段的描述,仅将代码搬运下来。

视频地址

求next值的程序如下:

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];    // 否则令j=next[j],循环继续
    }
}

框架代码:

int Index_KMP(SString S,SString T,int next[]) {
    int i=1,j=1;
    while (i<=S.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;
}

进一步优化后的nextval求解代码:

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];
    }
}

个人认为KMP算法其实不算很难,多看几遍自然就懂了。代码实现也相对简单。

posted @ 2026-05-16 00:52  syn_tax  阅读(6)  评论(0)    收藏  举报