数据结构:串、数组与广义表
📌目录
- 🔤 一,串的定义
- 🌰 二,案例引入
- 场景1:文本编辑器中的查找替换
- 场景2:用户手机号验证
- 📚 三,串的类型定义、存储结构及其运算
- (一)串的抽象类型定义
- (二)串的存储结构
- 1. 顺序存储(定长顺序串)
- 2. 堆分配存储(动态顺序串)
- 3. 链式存储(串的链表表示)
- (三)串的模式匹配算法
- 1. 朴素模式匹配算法(BF算法)
- 2. KMP算法
- 🔢 四,数组
- (一)数组的类型定义
- (二)数组的顺序存储
- 1. 一维数组
- 2. 二维数组
- (三)特殊矩阵的压缩存储
- 1. 对称矩阵
- 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语言):
#define MAXLEN 255 // 最大长度 typedef struct {char ch[MAXLEN + 1]; // 存储字符(+1预留结束符位置)int length; // 实际长度 } SString;
-
优势:随机访问效率高(通过下标直接获取字符);
-
劣势:长度固定,超过MAXLEN时会截断(如存储长文本可能溢出)。
2. 堆分配存储(动态顺序串)
用动态数组存储串,长度可根据需要动态调整(通过malloc和realloc分配内存)。
-
存储表示(C语言):
typedef struct {char *ch; // 指向动态分配的字符数组int length; // 实际长度 } HString;
-
初始化示例:
void 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语言):
#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。
代码实现:
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。
(二)广义表的存储结构
由于元素可能是原子或子表,需用链式存储,每个节点包含标志位(区分原子或子表):
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模式匹配算法,遍历主文本定位所有模式串的起始位置;
- 替换逻辑:对每个匹配位置,先删除原模式串,再插入替换串,更新主文本长度和内容。
完整代码实现
#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解析等场景中广泛应用。
从本质上看,这些结构都是对“数据关系”的抽象:串强调字符的顺序关系,数组强调多维索引关系,广义表强调嵌套关系。理解它们的存储规律和核心算法,不仅能解决具体问题,更能培养“根据数据特性选择结构”的思维——这正是数据结构的核心素养。