📌目录
- [🔤 一,串的定义](#🔤 一,串的定义)
- [🌰 二,案例引入](#🌰 二,案例引入)
- [📚 三,串的类型定义、存储结构及其运算](#📚 三,串的类型定义、存储结构及其运算)
-
- (一)串的抽象类型定义
- (二)串的存储结构
-
- [1. 顺序存储(定长顺序串)](#1. 顺序存储(定长顺序串))
- [2. 堆分配存储(动态顺序串)](#2. 堆分配存储(动态顺序串))
- [3. 链式存储(串的链表表示)](#3. 链式存储(串的链表表示))
- (三)串的模式匹配算法
-
- [1. 朴素模式匹配算法(BF算法)](#1. 朴素模式匹配算法(BF算法))
- [2. KMP算法](#2. KMP算法)
- [🔢 四,数组](#🔢 四,数组)
-
- (一)数组的类型定义
- (二)数组的顺序存储
-
- [1. 一维数组](#1. 一维数组)
- [2. 二维数组](#2. 二维数组)
- (三)特殊矩阵的压缩存储
-
- [1. 对称矩阵](#1. 对称矩阵)
- [2. 稀疏矩阵](#2. 稀疏矩阵)
- [🌐 五,广义表](#🌐 五,广义表)
- [🛠️ 案例分析与实现](#🛠️ 案例分析与实现)
- [📝 章结](#📝 章结)
🔤 一,串的定义
串(String),又称字符串,是由零个或多个字符组成的有限序列。通常记为 S = "a₁a₂...aₙ"
(n≥0),其中:
- S 是串名;
- 双引号(或单引号)是串的定界符,不属于串的内容;
- aᵢ(1≤i≤n)是单个字符,称为串的元素;
- n 是串的长度,当 n=0 时称为空串(记为 "")。
串的核心特点是元素的同质性------所有元素都是字符,且元素间存在明确的顺序关系。例如,"Hello" 是长度为5的串,由字符 'H'、'e'、'l'、'l'、'o' 组成。
需要注意的是:
- 空串("")与空格串(" ")不同,空格串的长度为空格的个数(如 " " 长度为2);
- 串中任意连续的字符组成的子序列称为该串的子串 ,包含子串的串称为主串。例如,"abc" 是 "abcdef" 的子串,起始位置为1(通常从1开始计数)。
🌰 二,案例引入
场景1:文本编辑器中的查找替换
在 Word 或记事本中,当你使用"查找替换"功能,将文档中所有"数据结构"替换为"Data Structure"时:
- 程序需要先在主串(文档内容)中定位子串"数据结构"的所有位置(模式匹配);
- 再将这些位置的子串替换为新内容。
这一过程的效率直接取决于串的模式匹配算法性能。
场景2:用户手机号验证
注册账号时,系统需验证输入的手机号是否为11位数字:
- 本质是检查串的长度是否为11,且每个字符是否属于 '0'~'9'。
这一过程依赖串的基本操作(长度判断、字符遍历)。
这些案例表明,串是处理文本数据的基础结构,其存储方式和算法设计直接影响文本处理的效率。
📚 三,串的类型定义、存储结构及其运算
(一)串的抽象类型定义
串的抽象数据类型(ADT)定义如下,包含数据集合及核心操作:
ADT String {
数据:
由n(n≥0)个字符组成的有限序列S = "a₁a₂...aₙ",字符具有相同类型。
操作:
1. StrAssign(&T, chars):将字符串常量chars赋值给T。
2. StrCopy(&T, S):将串S复制到串T。
3. StrEmpty(S):判断串S是否为空,空则返回TRUE,否则返回FALSE。
4. StrLength(S):返回串S的长度n。
5. StrCompare(S, T):比较S和T,若S>T返回正数,S=T返回0,S<T返回负数。
6. StrConcat(&T, S1, S2):将S1和S2拼接为新串T(T = S1 + S2)。
7. SubString(&Sub, S, pos, len):从S的第pos个字符开始,截取长度为len的子串Sub。
8. StrIndex(S, T, pos):从S的第pos个字符开始,查找T首次出现的位置,未找到返回0。
9. StrReplace(&S, T, V):将S中所有与T相等的非重叠子串替换为V。
10. StrDestroy(&S):销毁串S,释放内存。
}
(二)串的存储结构
1. 顺序存储(定长顺序串)
用固定长度的字符数组存储串,数组下标表示字符位置,另设变量记录串的实际长度(避免依赖 '\0' 等结束符)。
-
存储表示(C语言):
c#define MAXLEN 255 // 最大长度 typedef struct { char ch[MAXLEN + 1]; // 存储字符(+1预留结束符位置) int length; // 实际长度 } SString;
-
优势:随机访问效率高(通过下标直接获取字符);
-
劣势:长度固定,超过MAXLEN时会截断(如存储长文本可能溢出)。
2. 堆分配存储(动态顺序串)
用动态数组存储串,长度可根据需要动态调整(通过malloc和realloc分配内存)。
-
存储表示(C语言):
ctypedef struct { char *ch; // 指向动态分配的字符数组 int length; // 实际长度 } HString;
-
初始化示例:
cvoid InitString(HString *S) { S->ch = NULL; S->length = 0; } void StrAssign(HString *T, char *chars) { if (T->ch) free(T->ch); // 释放原有空间 int len = strlen(chars); if (len == 0) { T->ch = NULL; T->length = 0; } else { T->ch = (char*)malloc((len + 1) * sizeof(char)); strcpy(T->ch, chars); // 复制字符 T->length = len; } }
-
优势:长度灵活,适合存储不确定长度的串;
-
劣势:动态分配可能产生内存碎片。
3. 链式存储(串的链表表示)
用单链表存储串,每个节点存储一个或多个字符(通常存储多个以提高效率,称为"块链")。
-
存储表示(每个节点存4个字符,C语言):
c#define CHUNKSIZE 4 // 每个节点存储的字符数 typedef struct Chunk { char ch[CHUNKSIZE]; struct Chunk *next; } Chunk; typedef struct { Chunk *head, *tail; // 头指针和尾指针 int length; // 串的总长度 } LString;
-
优势:插入删除方便,适合频繁修改的场景;
-
劣势:存储密度低(需额外存储指针),随机访问效率差。
(三)串的模式匹配算法
模式匹配是指在主串 S 中查找子串 T(模式串)首次出现的位置,是串的核心操作。
1. 朴素模式匹配算法(BF算法)
思路:从主串 S 的第 pos 个字符开始,逐个与模式串 T 的字符比较:
- 若匹配成功,继续比较下一个字符;
- 若匹配失败,主串回溯到上一次开始位置的下一个字符,模式串回溯到第一个字符,重新比较。
示例:在 S="ababcabcacbab" 中查找 T="abcac"
- 初始从 pos=1 开始,S[1]='a' 与 T[1]='a' 匹配,继续比较;
- 直到 S[4]='b' 与 T[4]='c' 不匹配,主串回溯到 S[2],模式串回溯到 T[1];
- 重复过程,最终在 S[6] 处匹配成功,返回位置6。
代码实现:
c
int Index_BF(SString S, SString T, int pos) {
int i = pos; // 主串当前位置(从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; // 匹配失败
}
时间复杂度:最坏情况 O(n×m)(n为主串长度,m为模式串长度),适合短模式串场景。
2. KMP算法
思路:通过预处理模式串 T,得到一个"部分匹配表"(next数组),避免主串回溯,仅移动模式串:
- next[j] 表示 T 中前 j-1 个字符的最长相等前后缀长度;
- 匹配失败时,模式串直接跳到 next[j] 位置,主串不回溯。
优势:时间复杂度优化为 O(n + m),适合长文本匹配(如论文查重、DNA序列比对)。
🔢 四,数组
(一)数组的类型定义
数组是由相同类型的数据元素组成的有序集合,每个元素由唯一的下标(或索引)标识。
- 一维数组:元素按线性顺序排列(如
int a[5]
); - 二维数组:元素按行和列排列(如
int matrix[3][4]
,3行4列); - 多维数组:更高维度的扩展(如三维数组可表示立方体)。
数组的抽象数据类型定义核心操作包括:初始化、取元素(根据下标访问)、修改元素等。
(二)数组的顺序存储
数组在内存中采用顺序存储,即元素按一定次序存放在连续的内存空间中,通过下标计算元素地址。
1. 一维数组
设数组 a[n]
的基地址为 LOC(a[0])
,每个元素占用 size
字节,则 a[i]
的地址为:
LOC(a[i]) = LOC(a[0]) + i × size
2. 二维数组
有两种存储方式:
- 行优先顺序 (C语言采用):先存第0行,再存第1行......第i行第j列元素
a[i][j]
的地址为:
LOC(a[i][j]) = LOC(a[0][0]) + (i × n + j) × size
(n为列数)。 - 列优先顺序 (Fortran语言采用):先存第0列,再存第1列......地址公式为:
LOC(a[i][j]) = LOC(a[0][0]) + (j × m + i) × size
(m为行数)。
(三)特殊矩阵的压缩存储
特殊矩阵(如对称矩阵、三角矩阵、对角矩阵)中存在大量重复元素或零元素,可通过压缩存储减少空间浪费。
1. 对称矩阵
若 n 阶矩阵 A 满足 A[i][j] = A[j][i]
(i,j=0,1,...,n-1),则只需存储下三角(含对角线)元素。
- 元素总数为
n(n+1)/2
,按行优先顺序存入一维数组sa
中,A[i][j]
(i≥j)在sa
中的下标为:
k = i(i+1)/2 + j
。
2. 稀疏矩阵
非零元素极少且分布无规律的矩阵(如多数元素为0的系数矩阵),采用三元组表存储:
- 每个非零元素用 (行标, 列标, 值) 表示;
- 再存储矩阵的行数、列数和非零元素个数。
示例 :矩阵 [[1,0,0],[0,2,0],[0,0,3]]
的三元组表为:
((0,0,1), (1,1,2), (2,2,3))
,行数3,列数3,非零元素数3。
🌐 五,广义表
(一)广义表的定义
广义表(Lists)是线性表的扩展,允许元素既可以是单个数据(原子),也可以是另一个广义表(子表)。
- 记为
LS = (a₁, a₂, ..., aₙ)
,n为长度,n=0时称为空表。 - 示例:
A = ()
:空表,长度0;B = (e)
:长度1,元素为原子e;C = (a, (b, c, d))
:长度2,第一个元素为原子a,第二个元素为子表(b,c,d)
;D = (A, B, C)
:长度3,元素均为子表。
广义表的深度是指嵌套的最大层数(空表深度为1),例如C的深度为2,D的深度为3。
(二)广义表的存储结构
由于元素可能是原子或子表,需用链式存储,每个节点包含标志位(区分原子或子表):
c
typedef enum {ATOM, LIST} ElemTag; // ATOM=0表示原子,LIST=1表示子表
typedef struct GLNode {
ElemTag tag; // 标志位
union { // 共用体,原子或子表二选一
char atom; // 原子值(若tag=ATOM)
struct GLNode *hp; // 子表头指针(若tag=LIST)
};
struct GLNode *tp; // 指向下一个元素(同层下一个节点)
} GLNode, *GList;
示例 :广义表 C = (a, (b, c))
的存储结构:
- 头节点 tag=LIST,hp 指向第一个元素(原子a);
- 原子a的节点 tag=ATOM,atom='a',tp 指向第二个元素(子表);
- 子表节点 tag=LIST,hp 指向子表
(b,c)
的头节点,tp=NULL(无下一个元素)。
🛠️ 案例分析与实现
案例:文本查找与替换工具
功能需求:实现一个简单的文本处理工具,支持在长文本中查找指定单词,并将所有匹配的单词替换为新单词(如将"数据结构"替换为"Data Structure")。
核心思路
- 数据存储:使用堆分配串存储主文本(文章)、模式串(待查找单词)和替换串(新单词),支持动态调整长度;
- 查找逻辑:基于BF模式匹配算法,遍历主文本定位所有模式串的起始位置;
- 替换逻辑:对每个匹配位置,先删除原模式串,再插入替换串,更新主文本长度和内容。
完整代码实现
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 堆分配串定义
typedef struct {
char *ch; // 动态字符数组
int length; // 串实际长度
} HString;
// 初始化串
void InitString(HString *s) {
s->ch = NULL;
s->length = 0;
}
// 串赋值(将字符串常量复制到串中)
void StrAssign(HString *s, const char *chars) {
if (s->ch) free(s->ch); // 释放原有空间
int len = strlen(chars);
if (len == 0) {
s->ch = NULL;
s->length = 0;
} else {
s->ch = (char*)malloc((len + 1) * sizeof(char)); // +1预留结束符
strcpy(s->ch, chars);
s->length = len;
}
}
// BF模式匹配算法(返回模式串在主串中从pos开始的首次位置,1-based)
int IndexBF(HString S, HString T, int pos) {
if (pos < 1 || pos > S.length || T.length == 0) return 0; // 参数合法性检查
int i = pos - 1; // 主串下标(0-based)
int j = 0; // 模式串下标(0-based)
while (i < S.length && j < T.length) {
if (S.ch[i] == T.ch[j]) {
i++; // 匹配成功,继续比较下一个字符
j++;
} else {
i = i - j + 1; // 主串回溯到上一次匹配的下一位
j = 0; // 模式串重置到起点
}
}
if (j == T.length) return i - T.length + 1; // 匹配成功,返回1-based起始位置
else return 0; // 匹配失败
}
// 替换主串中所有非重叠的模式串为替换串
void StrReplace(HString *S, HString T, HString V) {
if (T.length == 0) return; // 模式串为空,无需替换
int pos = 1; // 从主串第1个字符开始查找
while (pos <= S->length - T.length + 1) {
int i = IndexBF(*S, T, pos); // 查找模式串位置
if (i == 0) break; // 未找到更多匹配,退出循环
// 步骤1:删除主串中从i开始的模式串
int delete_len = T.length;
for (int j = i + delete_len - 1; j < S->length; j++) {
S->ch[j - delete_len] = S->ch[j]; // 元素左移覆盖
}
S->length -= delete_len; // 更新主串长度
// 步骤2:在删除位置插入替换串
int insert_len = V.length;
if (insert_len > 0) {
// 重新分配内存以容纳插入的字符
S->ch = (char*)realloc(S->ch, (S->length + insert_len + 1) * sizeof(char));
// 元素右移腾出插入空间
for (int j = S->length - 1; j >= i - 1; j--) {
S->ch[j + insert_len] = S->ch[j];
}
// 插入替换串内容
for (int j = 0; j < insert_len; j++) {
S->ch[i - 1 + j] = V.ch[j];
}
S->length += insert_len; // 更新主串长度
}
// 下一次查找从替换后的下一个位置开始
pos = i + insert_len;
}
}
int main() {
HString text, oldWord, newWord;
InitString(&text);
InitString(&oldWord);
InitString(&newWord);
// 初始化文本、待替换单词和新单词
StrAssign(&text, "数据结构是计算机科学的核心,学好数据结构很重要!");
StrAssign(&oldWord, "数据结构");
StrAssign(&newWord, "Data Structure");
// 执行替换操作
StrReplace(&text, oldWord, newWord);
// 输出结果
printf("替换后的文本:%s\n", text.ch);
// 预期输出:"Data Structure是计算机科学的核心,学好Data Structure很重要!"
// 释放内存
free(text.ch);
free(oldWord.ch);
free(newWord.ch);
return 0;
}
代码说明
- 存储选择:堆分配串解决了定长串的长度限制问题,适合处理未知长度的文本;
- 效率权衡:BF算法实现简单,适合短文本或模式串场景;若处理长篇小说等大文本,可优化为KMP算法提升效率;
- 替换逻辑:通过"先删除后插入"的方式实现替换,需注意内存重分配和字符移位的边界处理。
📝 章结
串、数组和广义表是线性表的扩展与变形,在数据处理中承担着不同角色:
-
串作为字符的有序序列,是文本处理的基础。其核心价值在于模式匹配算法------从朴素的BF算法到高效的KMP算法,优化的不仅是时间复杂度,更是对"避免重复比较"这一思想的体现。存储结构的选择(定长、堆分配、链式)需结合文本长度和操作频率,例如堆分配串兼顾灵活性与访问效率,适合多数文本场景。
-
数组通过多维下标实现结构化数据的存储,其顺序存储特性确保了高效的随机访问。特殊矩阵的压缩存储(如对称矩阵、稀疏矩阵)则展示了"按需存储"的优化思想------通过减少冗余数据,显著降低空间开销,这在科学计算、图像处理等领域至关重要。
-
广义表打破了线性表的同质性限制,允许元素嵌套,是处理复杂层次数据的工具。其链式存储结构灵活支持原子与子表的混合存储,在Lisp等函数式语言、XML/JSON解析等场景中广泛应用。
从本质上看,这些结构都是对"数据关系"的抽象:串强调字符的顺序关系,数组强调多维索引关系,广义表强调嵌套关系。理解它们的存储规律和核心算法,不仅能解决具体问题,更能培养"根据数据特性选择结构"的思维------这正是数据结构的核心素养。