【数据结构】串——(一)
目录
- 串
- 串的概念
- 串的存储表示及实现
- 定长顺序存储表示 SString(静态存储)
- 堆分配存储表示 HString(动态存储)
- 块链存储表示 LString(链表方式)
- 串赋值(StrAssign)操作
- 串复制(StrCopy)操作
- 判空(StrEmpty)操作
- 求串长(StrLength)操作
- 串比较(StrCompare)操作
- 清空串(ClearString)操作
- 串连接(Concat)操作
- 求子串(SubString)操作
- 定位操作(Index / StrIndex)操作
- 替换操作(Replace)操作
- 插入(StrInsert)操作
- 删除(StrDelete)操作
- 销毁(DestroyString)操作
- 操作集合
串
串的概念
串的基本概念
- 定义:
串(String)是由 零个或多个字符 组成的 有限序列。- 一般记作:
S = "a1 a2 a3 … an"
- 其中:
S
是串的名字a1、a2、…、an
是串的各个字符n
表示串的长度
- 一般记作:
- 注意:
- 串是 特殊的线性表:数据元素限定为“字符”。
- 和普通线性表不同,串主要是用于 处理字符数据,常见操作和应用方向会偏向文本。
串的相关概念
- 空串:长度为 0 的串,记作
""
。 - 空格串:由一个或多个空格字符组成的串,⚠️ 和空串不同。长度为空格的数量。
- 子串:串中任意连续的若干个字符组成的子序列。
- 例:
"abcdef"
的子串有"abc"
、"de"
等。
- 例:
- 主串:包含子串的串。
- 在
"abcdef"
里,"abc"
是子串,而"abcdef"
是主串。
- 在
- 字符位置:字符在串中的序号,一般 从 1 开始(和数组不同)。
串与线性表的区别
- 相同点:串和线性表一样,都是 线性存储结构,有序排列。
- 不同点:
- 数据对象范围不同:
- 线性表元素可以是任意数据对象。
- 串的元素是 字符。
- 基本操作不同:
- 线性表操作侧重于 插入、删除。
- 串操作侧重于 查找、匹配、拼接、截取。
- 数据对象范围不同:
线性表 → 存什么都行,比如学生、商品、成绩
串 → 只能存字符,主要用于处理“文本”类问题,比如:搜索、替换、匹配(比如你常用的字符串操作函数
strlen
、strcmp
、strcat
、strstr
等其实就是“串的操作”)。
串的常见基本操作
-
串赋值(StrAssign)
-
将一个常量字符串赋给串变量。
-
例:
StrAssign(S, "hello")
-
-
串复制(StrCopy)
-
把一个串复制到另一个串。
-
例:
StrCopy(T, S)
-
-
判空(StrEmpty)
-
判断一个串是否为空串(长度 = 0)。
-
例:
StrEmpty(S)
→true
orfalse
-
-
求串长(StrLength)
-
返回串的字符个数。
-
例:
StrLength("abc") = 3
-
-
串比较(StrCompare)
-
按字典顺序比较两个串的大小。
-
例:
"abc" < "abd"
"abc" = "abc"
-
-
清空串(ClearString)
- 把串置为空串。
-
串连接(Concat)
-
生成一个新串,其值是两个串连接而成。
-
例:
Concat("hello", "world") = "helloworld"
-
-
求子串(SubString)
-
截取主串中从第 i 个字符开始、长度为 len 的子串。
-
例:
SubString("abcdef", 2, 3) = "bcd"
-
-
定位操作(Index / StrIndex)
-
在主串中查找某个子串第一次出现的位置。
-
例:
Index("hello world", "world") = 7
- 若不存在则返回 0 或 -1(视实现而定)。
-
-
替换操作(Replace)
-
用新的子串替换主串中所有出现的某个子串。
-
例:
Replace("abcabc", "ab", "xy") = "xycxyc"
-
-
插入(StrInsert)
-
在主串的第 i 个字符前插入一个子串。
-
例:
Insert("abc", 2, "XYZ") = "aXYZbc"
-
-
删除(StrDelete)
- 删除主串中第 i 个字符开始的若干字符。
- 例:
Delete("abcdef", 2, 3) = "aef"
- 例:
- 删除主串中第 i 个字符开始的若干字符。
-
销毁(DestroyString)
- 释放串所占用的存储空间,使之成为一个无效的串变量。
串的存储表示及实现
定长顺序存储表示 SString(静态存储)
定义:用一个 定长数组 存放串中的字符。
特点:
- 数组大小预先固定(比如
MAXSIZE=255
)。 - 实际串长 ≤
MAXSIZE
。
**结构体定义:
#define MAXSIZE 255
typedef struct {char ch[MAXSIZE]; // 存放串的字符int length; // 串的实际长度
} SString;
优点:简单,容易实现。
缺点:空间可能浪费,串长受数组上限限制。
堆分配存储表示 HString(动态存储)
定义:用一段 动态分配的连续存储区 存放串的字符。
特点:
- 串长不受预定义常量限制,只要内存够就行。
- 适合变长串的存储。
结构体定义:
typedef struct {char *ch; // 指向动态分配的字符数组int length; // 串的长度
} HString;
例子:
- 当串长度变化时,可以通过
malloc
/realloc
动态调整。
优点:灵活,适合实际工程。
缺点:需要频繁申请/释放内存,管理成本较高。
块链存储表示 LString(链表方式)
定义:把串的字符分散存放在若干个链表节点中,每个结点可以存放多个字符。
结构体定义:
#define BLOCK_SIZE 4 // 每个结点存4个字符(示例)typedef struct Block {char ch[BLOCK_SIZE];struct Block *next;
} Block;typedef struct {Block *head, *tail; // 串的头结点和尾结点int length; // 串的总长度
} LString;
特点:
- 插入、删除效率高(尤其是大串处理)。
- 适合超长串(比如几 MB 的大文本)。
缺点:存储密度低(指针要额外占空间),查找定位某个字符时效率较低(必须顺链)。
串赋值(StrAssign)操作
SString 的 StrAssign 的逻辑
- 判断源串长度是否超过 SString 最大长度 MAXSIZE
- 遍历源串,将每个字符逐个复制到 S.ch 中
- 设置 S.length 为实际字符数
⚠️ 注意:SString 是定长顺序存储,所以不能超过数组容量。
// 串赋值
int StrAssign(SString *S, const char *chars) {int i = 0;while (chars[i] != '\0') { // 遍历源字符串if (i >= MAXSIZE) // 超过容量报错return 0; // 赋值失败S->ch[i] = chars[i]; // 复制字符i++;}S->length = i; // 设置长度return 1; // 赋值成功
}
StrAssign 是 SString 的管理类操作(和复制、清空类似),主要用于初始化或重新赋值。
如果要赋值的字符串长度超过 MAXSIZE,需要截断或报错,这就是 定长顺序存储的局限。
HString 的 StrAssign 的逻辑
- 如果 H->ch 不为空,先
free(H->ch)
,释放原有空间 - 计算源字符串长度
len
- 分配新空间:
H->ch = (char*)malloc(sizeof(char) * len)
- 将源字符串字符逐一复制到 H->ch
- 设置 H->length = len
- 返回成功标志
⚠️ 注意:
- HString 是动态分配,内存需要手动释放
- 如果 malloc 失败,要返回失败
int StrAssign(HString *H, const char *chars){int len = strlen(chars);if(H->ch) free(H->ch); // 释放原有空间H->ch = (char*) malloc(sizeof(char) * (len + 1)); // +1 给 '\0'if(!H->ch) return 0; // 分配失败strcpy(H->ch, chars); // 复制字符串H->length = len;return 1;
}
HString 与 SString 的区别:
- 动态分配:长度可变,可释放空间
- 赋值操作需 malloc,而 SString 直接写入数组
后续操作(连接、插入、删除等)也都需要注意 空间是否足够
LString 的 StrAssign 的逻辑
- 如果 L->head 不为空,先释放原链表
- 初始化头结点:
L->head = NULL
或创建一个空头结点 - 遍历源字符串 chars:
- 创建新结点
node
node->data = chars[i]
- 将 node 插入链表尾部
- 创建新结点
- 更新 L->length
- 返回成功标志
⚠️ 注意:
- 链表操作中,插入到尾部通常需要维护一个 尾指针,避免每次都从头遍历
- 分配结点失败,需要返回失败
// 释放链表
void FreeLString(LString *L){LNode *p = L->head;while(p){LNode *tmp = p;p = p->next;free(tmp);}L->head = NULL;L->length = 0;
}// 串赋值 StrAssign
int StrAssign(LString *L, const char *chars){FreeLString(L); // 释放原链表L->head = NULL;LNode *tail = NULL;int i=0;while(chars[i]!='\0'){LNode *node = (LNode*) malloc(sizeof(LNode));if(!node) return 0; // 分配失败node->data = chars[i];node->next = NULL;if(L->head == NULL){L->head = node;tail = node;} else {tail->next = node;tail = node;}i++;}L->length = i;return 1;
}
LString 优点:长度不固定,插入删除效率高
LString 缺点:访问单个字符需要遍历,随机访问效率低
与 HString 对比:
- HString 内存连续,随机访问快
- LString 内存链表,动态灵活
串复制(StrCopy)操作
SString 的 StrCopy 的逻辑
- 遍历源串 S.ch,将每个字符复制到目标串 T.ch。
- 设置 T.length = S.length。
⚠️ 注意:SString 是定长顺序存储,目标串容量不能小于源串长度。
// 串复制
int StrCopy(SString *T, const SString *S) {if(S->length > MAXSIZE)return 0; // 复制失败,超出容量for(int i = 0; i < S->length; i++) {T->ch[i] = S->ch[i];}T->length = S->length;return 1; // 复制成功
}
StrCopy 属于 管理类操作(类似赋值、清空)。
和 StrAssign 不同的是:
- StrAssign 是从 字符常量赋值
- StrCopy 是从 另一个 SString 赋值
注意容量限制:SString 的长度不能超过
MAXSIZE
HString 的 StrCopy 的逻辑
- 如果目标串
T->ch
不为空,先free(T->ch)
- 获取源串长度
len = S->length
- 分配新空间:
T->ch = malloc(len + 1)
- 将源串
S->ch
复制到目标串T->ch
- 设置
T->length = S->length
- 返回成功标志
⚠️ 注意:
- 必须处理目标串原有空间释放,否则会内存泄漏
- 动态分配失败需要返回失败
// 串复制 StrCopy
int StrCopy(HString *T, const HString *S){if(T->ch) free(T->ch); // 释放原有空间T->ch = (char*) malloc(sizeof(char) * (S->length + 1));if(!T->ch) return 0; // 分配失败strcpy(T->ch, S->ch);T->length = S->length;return 1;
}
与 SString 对比:
- SString 是静态数组,复制直接逐个赋值
- HString 需要 动态分配内存,保证长度可变
注意内存管理:
- 复制前释放目标串原有空间
- 使用完后要
free()
避免内存泄漏
LString 的 StrCopy 的逻辑
-
释放目标串 T 的旧链表
-
若源串 S 为空串 → 直接返回空串
-
遍历源串 S:
-
为每个字符分配新结点
-
将字符复制到结点
-
按顺序接到目标串链表中
-
-
更新 T->length
-
返回成功标志
// 释放链表
void FreeLString(LString *L){LNode *p = L->head;while(p){LNode *tmp = p;p = p->next;free(tmp);}L->head = NULL;L->length = 0;
}// 串复制 StrCopy
int StrCopy(LString *T, const LString *S){FreeLString(T); // 清空目标串T->head = NULL;if(S->length == 0) { // 源串为空T->length = 0;return 1;}LNode *p = S->head, *tail = NULL;while(p){LNode *node = (LNode*) malloc(sizeof(LNode));if(!node) return 0; // 分配失败node->data = p->data;node->next = NULL;if(T->head == NULL){T->head = node;tail = node;} else {tail->next = node;tail = node;}p = p->next;}T->length = S->length;return 1;
}
- 与 SString / HString 的复制对比:
- SString:直接数组拷贝(静态存储,空间固定)
- HString:
malloc
一次性分配整段空间再strcpy
- LString:逐个结点
malloc
并复制字符👉 所以 LString 的复制效率最低,但灵活性最高(支持无限长串,不受数组限制)。
判空(StrEmpty)操作
SString 的 StrEmpty 的逻辑
- 检查
S.length
是否为 0 - 如果是 0 → 返回真
- 否则 → 返回假
⚠️ 注意:S.ch 中的内容无所谓,只要 length=0 就是空串。
// 判空操作
int StrEmpty(const SString *S) {return (S->length == 0);
}
判空操作非常简单,属于 管理类操作。
在其他串操作(如插入、删除、连接)前,通常先判空,以避免越界。
空串:
length = 0
非空串:
length > 0
HString 的 StrEmpty 的逻辑
- 检查 H->length 是否为 0
- 可选:也可检查 H->ch 是否为 NULL
- 返回布尔值
int StrEmpty(const HString *H){return (H->length == 0) ? 1 : 0;
}
HString 判空 直接判断 length 是否为 0,效率非常高
与 SString 判空类似,只是 HString 的长度是动态维护的
释放内存后也算空串,length = 0
LString 的 StrEmpty 的逻辑
- 检查 L->length 是否为 0
- 或者检查 L->head 是否为 NULL
- 返回布尔值
链式存储的串,空串通常 head = NULL 且 length = 0,两者保持一致
// 判空 StrEmpty
int StrEmpty(const LString *L){return (L->length == 0) ? 1 : 0;
}
判断依据:链表长度
length
或头指针head
特点:效率高(O(1)),不需要遍历链表
与 SString / HString 判空逻辑相似,只是底层存储不同
求串长(StrLength)操作
SString 的 StrLength 的逻辑
- 读取
S.length
- 返回该值
⚠️ 注意:SString 已经存储了长度信息,所以 不需要遍历数组。
- 这是 SString 的优势之一:求长度操作 O(1)
// 求串长操作
int StrLength(const SString *S) {return S->length;
}
对 SString 来说,求长度操作非常高效,只需返回
length
成员。对 HString 或 LString,如果没有缓存长度,求长度可能需要遍历整个字符数组或链表,复杂度 O(n)。
这个操作通常用于:循环、判空、插入、删除、截取等其他操作前的判断。
HString 的 StrLength 的逻辑
- 直接访问
H->length
- 返回值
HString 在赋值、插入、删除等操作中会维护
length
,所以求串长是 O(1) 操作
// 求串长 StrLength
int StrLength(const HString *H){return H->length;
}
HString 的长度
length
是 动态维护的字段,不需要遍历数组与 SString 对比:
- SString 静态数组也可以维护
length
,同样 O(1)- 如果不维护 length,则需要遍历数组统计字符个数(O(n))
求串长操作非常高效,是其他操作(插入、删除、比较)的基础
LString 的 StrLength 的逻辑
- 若 LString 维护
length
,直接返回 L->length - 若没有维护 length,则:
- 遍历链表,从头结点到尾结点
- 每遍历一个结点计数加 1
- 返回计数
通常我们在赋值、插入、删除时维护 length,所以求串长是 O(1) 操作
// 求串长 StrLength
int StrLength(const LString *L){return L->length;
}
维护 length 字段:O(1) 时间求长度
不维护 length:需遍历链表,O(n)
与 HString / SString 对比:
- HString:动态数组,length O(1)
- SString:静态数组,维护 length O(1)
- LString:链表,维护 length O(1),不维护 O(n)
串比较(StrCompare)操作
SString 的 StrCompare 的逻辑
- 从第 1 个字符开始,依次比较 S.ch[i] 和 T.ch[i]
- 遇到不同字符:返回它们 ASCII 值差
S.ch[i] - T.ch[i]
- 遍历完较短串后:
- 如果所有字符相等,则 长度短的串小
⚠️ 注意:
- SString 的下标一般从 0 或 1,教材里常从 1 开始
- 比较的是 字典序(ASCII 顺序)
// 串比较
int StrCompare(const SString *S, const SString *T) {int i = 0;int minLen = (S->length < T->length) ? S->length : T->length;for(i = 0; i < minLen; i++) {if(S->ch[i] != T->ch[i])return S->ch[i] - T->ch[i]; // ASCII差值}// 前 minLen 个字符相同,则长度短的串小return S->length - T->length;
}
StrCompare 属于 管理类操作,常用于:
- 字符串排序
- 查找匹配
- 字典序判断
对 SString:复杂度 O(n),n 为较短串长度。
对 HString / LString:逻辑相同,但 HString 也可以直接遍历数组,LString 需要顺链访问。
HString 的 StrCompare 的逻辑
- 从两个串的第一个字符开始逐个比较
- 当遇到第一个不同的字符时:
- 返回
S->ch[i] - T->ch[i]
- 返回
- 若一串提前结束:
- 返回长度差
S->length - T->length
- 返回长度差
- 若完全相同:
- 返回 0
⚠️ 注意:
- HString 是动态数组,可随机访问,比较效率高
- 可以直接用循环或
strncmp
// 串比较 StrCompare
int StrCompare(const HString *S, const HString *T){int minLen = (S->length < T->length) ? S->length : T->length;for(int i=0; i<minLen; i++){if(S->ch[i] != T->ch[i])return S->ch[i] - T->ch[i];}return S->length - T->length; // 长度不同,较长串大
}
原理:逐字符比较 ASCII 值
效率:O(n),n = 较短串长度
与 SString / LString 对比:
- SString:数组顺序访问,逻辑一致
- LString:链表,需要遍历结点,访问慢一些
LString 的 StrCompare 的逻辑
- 从两个串的头结点开始逐结点比较
data
- 当遇到第一个不同字符时:
- 返回
S->data - T->data
- 返回
- 若一串提前结束:
- 返回长度差
S->length - T->length
- 返回长度差
- 若完全相同:
- 返回 0
⚠️ 注意:
- 链表存储不能随机访问,需要顺序遍历
- 时间复杂度 O(n),n = 较短串长度
// 串比较 StrCompare
int StrCompare(const LString *S, const LString *T){LNode *p = S->head;LNode *q = T->head;while(p && q){if(p->data != q->data)return p->data - q->data;p = p->next;q = q->next;}return S->length - T->length;
}
链表存储串访问较慢,但灵活性高(不受固定数组长度限制)
与 HString / SString 对比:
- SString / HString 可以随机访问数组,效率略高
- LString 逐结点遍历,效率稍低
时间复杂度 O(n),n = 较短串长度
清空串(ClearString)操作
SString 的 ClearString 的逻辑
- 将 S.length 置为 0
- 可选:把 S.ch 数组中的字符清空(不是必须)
⚠️ 注意:
- 清空 ≠ 销毁
- 清空串:空间保留,可继续使用
- 销毁串:释放空间,串不可再用
// 清空串
void ClearString(SString *S) {S->length = 0;// 可选:清空数组内容// for(int i=0;i<MAXSIZE;i++)// S->ch[i] = '\0';
}
ClearString 是 管理类操作
常用于:
- 重新使用串
- 避免旧内容干扰后续操作
对 SString 来说,时间复杂度 O(1)
对 HString / LString,如果要释放原有动态空间或链表节点,逻辑略有不同
HString 的 ClearString 的逻辑
- 将
H->length = 0
- 可选:将
H->ch
内存清零(便于调试) - 不释放
H->ch
,空间仍可使用
与 销毁 DestroyString 的区别:
- ClearString:长度置 0,空间可用
- DestroyString:释放空间,串变量变无效
// 清空串 ClearString
void ClearString(HString *H){H->length = 0;// 可选:清空内存// if(H->ch) memset(H->ch, 0, sizeof(char)*(H->length+1));
}
ClearString 与 DestroyString 的区别:
操作 长度 内存空间 可再次赋值 ClearString 0 保留 可以 DestroyString 0 释放 需重新分配 清空操作 O(1),非常高效
对 HString 的管理操作(判空、长度、清空)都是基于
length
字段
LString 的 ClearString 的逻辑
- 遍历链表释放每个结点
- 将
L->head = NULL
- 将
L->length = 0
- 串变量仍然可用,便于重新赋值
与 销毁 DestroyString 的区别:
- ClearString:释放结点,变量可用
- DestroyString:释放结点,变量变无效(通常还要清掉结构体内容)
// 清空串 ClearString
void ClearString(LString *L){LNode *p = L->head;while(p){LNode *tmp = p;p = p->next;free(tmp);}L->head = NULL;L->length = 0;
}
ClearString 与 DestroyString 的区别:
操作 长度 内存空间 串变量可用 ClearString 0 已释放结点 可以 DestroyString 0 已释放结点 变无效 清空操作的时间复杂度 O(n),n = 结点数
对链式串管理非常重要,避免内存泄漏
串连接(Concat)操作
SString 的 Concat 的逻辑
- 计算 S1.length + S2.length
- 如果总长度 ≤ MAXSIZE → 直接复制
- 先把 S1.ch 复制到 T.ch
- 再把 S2.ch 追加到 T.ch 后面
- 设置 T.length = S1.length + S2.length
- 如果总长度 > MAXSIZE → 截断(或报错)
⚠️ 注意:SString 是 定长顺序存储,不能超过 MAXSIZE
// 串连接
int Concat(SString *T, const SString *S1, const SString *S2) {if(S1->length + S2->length > MAXSIZE)return 0; // 超出容量,连接失败int i;for(i=0;i<S1->length;i++)T->ch[i] = S1->ch[i];for(int j=0;j<S2->length;j++,i++)T->ch[i] = S2->ch[j];T->length = S1->length + S2->length;return 1; // 连接成功
}
Concat 属于 处理类操作(与求子串、查找、插入、删除同类)
对 SString,时间复杂度 O(n + m),n、m 分别为 S1 和 S2 长度
对 HString / LString,可利用动态分配或链表拼接,更灵活
HString 的 Concat 的逻辑
- 分配长度为
S->length + T->length + 1
的新空间 - 将 S->ch 的内容复制到新空间
- 将 T->ch 的内容接到 S 后面
- 更新 H->length
- H->ch 指向新空间
⚠️ 注意:
- HString 是动态数组,连接需要新分配内存
- 原来的 H->ch 如果存在,要先释放
// 串连接 Concat
int Concat(HString *H, const HString *S, const HString *T){if(H->ch) free(H->ch); // 释放原有空间int len = S->length + T->length;H->ch = (char*) malloc(sizeof(char) * (len + 1));if(!H->ch) return 0;strcpy(H->ch, S->ch);strcat(H->ch, T->ch);H->length = len;return 1;
}
HString 连接操作需要 新分配内存,不能原地修改
时间复杂度 O(n + m),n = S.length, m = T.length
使用动态数组的好处是可以随机访问、容易管理
LString 的 Concat 的逻辑
- 先清空
H
,避免内存泄漏 - 遍历
S
,逐个结点复制到H
- 遍历
T
,逐个结点复制到H
- 更新
H->length
⚠️ 注意:
- LString 是 链式存储,所以不能直接拼接指针,而是要 复制新结点
- 否则修改
H
会影响S
或T
// 串连接 Concat
int Concat(LString *H, const LString *S, const LString *T){ClearString(H);LNode *tail = NULL;// 复制 SLNode *p = S->head;while(p){LNode *node = (LNode*) malloc(sizeof(LNode));if(!node) return 0;node->data = p->data;node->next = NULL;if(H->head == NULL){H->head = node;tail = node;} else {tail->next = node;tail = node;}p = p->next;}// 复制 Tp = T->head;while(p){LNode *node = (LNode*) malloc(sizeof(LNode));if(!node) return 0;node->data = p->data;node->next = NULL;if(H->head == NULL){H->head = node;tail = node;} else {tail->next = node;tail = node;}p = p->next;}H->length = S->length + T->length;return 1;
}
LString 连接操作需要 遍历两个链表,复制结点 → 时间复杂度 O(n + m)
与 HString 不同,链式存储不能直接用
strcat
,必须分配新结点这种方式保证 H 不会影响 S 和 T
求子串(SubString)操作
SubString 的逻辑
- 检查
pos
和len
是否有效:pos >= 1
且pos <= S.length
len >= 0
且pos + len - 1 <= S.length
- 遍历源串,从
S.ch[pos-1]
开始,复制len
个字符到Sub.ch
- 设置
Sub.length = len
⚠️ 注意:SString 是定长顺序存储,Sub.length
不能超过 MAXSIZE
// 求子串
int SubString(SString *Sub, const SString *S, int pos, int len) {if(pos < 1 || pos > S->length || len < 0 || len > MAXSIZE || pos + len - 1 > S->length)return 0; // 参数非法for(int i=0;i<len;i++)Sub->ch[i] = S->ch[pos-1 + i]; // 注意数组下标从0开始Sub->length = len;return 1; // 成功
}
SubString 属于 处理类操作,常用于:
- 截取文本
- 查找匹配
- 插入、替换等操作前的分割
时间复杂度 O(len)
对 HString / LString,逻辑类似,但 HString 可动态分配新数组,LString 需顺链复制节点
定位操作(Index / StrIndex)操作
定位操作逻辑(暴力匹配法)
- 检查子串长度是否为 0
- 如果是 → 通常返回 1(空串在任意位置匹配)
- 遍历主串 S,从位置 i = 1 到
S.length - T.length + 1
- 对每个位置 i,比较 S 从 i 开始的连续 T.length 个字符是否等于 T
- 若匹配 → 返回 i
- 遍历完都不匹配 → 返回 0
// 定位操作(暴力匹配法)
int Index(const SString *S, const SString *T) {if(T->length == 0) return 1; // 空串匹配返回1for(int i=0; i <= S->length - T->length; i++) {int j;for(j=0;j<T->length;j++) {if(S->ch[i+j] != T->ch[j])break;}if(j == T->length)return i + 1; // 返回位置从1开始}return 0; // 未找到
}
Index / StrIndex 属于 处理类操作
暴力匹配法时间复杂度 O((n-m+1)*m),n = 主串长度,m = 子串长度
可以使用 KMP 算法 优化到 O(n+m)
对 HString / LString,逻辑类似,但 LString 需要顺链访问节点
替换操作(Replace)操作
Replace 的逻辑
- 检查 T 是否为空串
- 若为空串 → 通常不进行替换
- 在主串 S 中循环查找 T 的位置(可使用
Index
) - 找到后:
- 删除位置 i 的 T 子串
- 在位置 i 插入 V 子串
- 继续查找下一个 T,直到 S 中不再出现 T
- 注意:
- SString 的长度不能超过 MAXSIZE
- 删除 + 插入操作必须保持数组连续性
// 替换操作
void Replace(SString *S, const SString *T, const SString *V){if(T->length==0) return; // 空串不替换int pos=Index(S, T, 1);while(pos){Delete(S, pos, T->length);Insert(S, pos, V);pos=Index(S, T, pos + V->length); // 查找下一个}
}
Replace 属于 处理类操作
内部调用了 Index + Delete + Insert,所以时间复杂度取决于主串长度和被替换次数
对 HString / LString,操作类似,但 HString 可动态分配空间,LString 需要调整链表节点
插入(StrInsert)操作
StrInsert 的逻辑
- 检查插入位置是否合法:
1 <= pos <= S.length + 1
- 检查插入后长度是否超过最大容量 MAXSIZE
- 将主串从
pos
开始的字符向后移动 T.length 个位置,为插入腾出空间 - 将 T.ch 的内容复制到 S.ch[pos-1 … pos-1+T.length-1]
- 更新 S.length = S.length + T.length
⚠️ 注意:SString 是顺序存储,数组必须连续,所以要从后向前移动字符,避免覆盖。
// 插入子串
int StrInsert(SString *S, int pos, const SString *T){if(pos<1 || pos>S->length+1 || S->length+T->length>MAXSIZE)return 0; // 插入位置非法或长度超出// 后移主串for(int i=S->length-1;i>=pos-1;i--){S->ch[i+T->length]=S->ch[i];}// 插入子串for(int i=0;i<T->length;i++){S->ch[pos-1+i]=T->ch[i];}S->length += T->length;return 1;
}
StrInsert 属于 处理类操作
时间复杂度 O(n),n = S.length - pos + 1(需要移动的字符数)
对 HString / LString:
- HString 可动态分配数组空间
- LString 只需调整链表节点,插入更灵活
删除(StrDelete)操作
StrDelete 的逻辑
- 检查删除位置是否合法:
1 <= pos <= S.length
0 <= len <= S.length - pos + 1
- 将从
pos + len
开始的字符向前移动len
个位置,覆盖被删除的部分 - 更新 S.length = S.length - len
⚠️ 注意:SString 是顺序存储,删除后数组仍然连续,不需要额外释放空间
// 删除子串
int StrDelete(SString *S, int pos, int len){if(pos<1 || pos>S->length || len<0 || pos+len-1>S->length)return 0; // 参数非法for(int i=pos-1+len; i<S->length; i++){S->ch[i-len] = S->ch[i]; // 前移覆盖}S->length -= len;return 1;
}
StrDelete 属于 处理类操作
时间复杂度 O(n),n = S.length - pos + 1(需要移动的字符数)
对 HString / LString:
- HString 可动态调整数组
- LString 通过修改链表指针删除节点,操作更灵活
销毁(DestroyString)操作
DestroyString 的逻辑
- 对 SString(静态顺序存储):
- 设置 S.length = 0
- 可选:清空 S.ch 数组
- 标记串不可再使用(逻辑上)
- 对 HString(动态顺序存储):
- free(S.ch)
- S.ch = NULL
- S.length = 0
- 对链式存储(LString):
- 顺序释放所有链表节点
⚠️ 注意:销毁后不能再对该串进行任何操作,否则会访问非法内存
void DestroyString(SString *S){S->length = 0;// 可选:清空数组内容// for(int i=0;i<MAXSIZE;i++) S->ch[i]='\0';
}
销毁串 vs 清空串:
- 清空串(ClearString):长度置 0,但数组/空间仍然可用
- 销毁串(DestroyString):空间不可再用,需要重新分配或赋值
对 链式存储 LString:销毁需遍历链表释放每个节点
⚠️ 注意:
-
SString 的数组
ch[MAXSIZE]
是 固定长度,在栈上或全局分配的,内存大小在编译时就已经确定。 -
ClearString:
void ClearString(SString *S){S->length = 0; }
- 逻辑上:清空串,长度置 0
- 内存仍然存在,可以继续赋值或插入新的内容
-
DestroyString:
void DestroyString(SString *S){S->length = 0; }
- 对静态数组来说,其实无法真正释放数组空间,只能逻辑上标记为不可用
- 所以代码看起来和 ClearString 一样
结论:对于 SString(静态数组),清空和销毁的区别主要在“逻辑含义”
- 清空:可继续使用
- 销毁:逻辑上不再使用,不能再调用操作函数
操作集合
#include <stdio.h>
#define MAXSIZE 255typedef struct {char ch[MAXSIZE];int length;
} SString;// 1. 串赋值 StrAssign
int StrAssign(SString *S, const char *chars){int i=0;while(chars[i]!='\0'){if(i>=MAXSIZE) return 0;S->ch[i]=chars[i];i++;}S->length=i;return 1;
}// 2. 串复制 StrCopy
int StrCopy(SString *T, const SString *S){if(S->length>MAXSIZE) return 0;for(int i=0;i<S->length;i++)T->ch[i]=S->ch[i];T->length=S->length;return 1;
}// 3. 判空 StrEmpty
int StrEmpty(const SString *S){return S->length == 0;
}// 4. 求串长 StrLength
int StrLength(const SString *S){return S->length;
}// 5. 串比较 StrCompare
int StrCompare(const SString *S, const SString *T){int i=0;while(i<S->length && i<T->length){if(S->ch[i] != T->ch[i])return S->ch[i]-T->ch[i];i++;}return S->length - T->length;
}// 6. 清空串 ClearString
void ClearString(SString *S){S->length = 0;
}// 7. 串连接 Concat
int Concat(SString *T, const SString *S1, const SString *S2){if(S1->length + S2->length > MAXSIZE) return 0;for(int i=0;i<S1->length;i++) T->ch[i]=S1->ch[i];for(int i=0;i<S2->length;i++) T->ch[S1->length+i]=S2->ch[i];T->length = S1->length + S2->length;return 1;
}// 8. 求子串 SubString
int SubString(SString *Sub, const SString *S, int pos, int len){if(pos<1 || pos>S->length || len<0 || pos+len-1>S->length) return 0;for(int i=0;i<len;i++)Sub->ch[i] = S->ch[pos-1+i];Sub->length = len;return 1;
}// 9. 定位操作 Index / StrIndex
int Index(const SString *S, const SString *T, int start){if(T->length==0) return start;for(int i=start-1;i<=S->length-T->length;i++){int j;for(j=0;j<T->length;j++)if(S->ch[i+j]!=T->ch[j]) break;if(j==T->length) return i+1;}return 0;
}// 10. 替换操作 Replace
void Replace(SString *S, const SString *T, const SString *V){if(T->length==0) return;int pos = Index(S,T,1);while(pos){Delete(S,pos,T->length);Insert(S,pos,V);pos = Index(S,T,pos+V->length);}
}// 11. 插入操作 StrInsert
int Insert(SString *S, int pos, const SString *T){if(pos<1 || pos>S->length+1 || S->length+T->length>MAXSIZE) return 0;for(int i=S->length-1;i>=pos-1;i--)S->ch[i+T->length] = S->ch[i];for(int i=0;i<T->length;i++)S->ch[pos-1+i]=T->ch[i];S->length += T->length;return 1;
}// 12. 删除操作 StrDelete
int Delete(SString *S, int pos, int len){if(pos<1||pos>S->length||len<0||pos+len-1>S->length) return 0;for(int i=pos-1+len;i<S->length;i++)S->ch[i-len] = S->ch[i];S->length -= len;return 1;
}// 13. 销毁 DestroyString
void DestroyString(SString *S){S->length = 0;// 静态顺序存储数组空间不可释放,只能逻辑上销毁
}int main(){SString S, T, U;StrAssign(&S,"abcabc");StrAssign(&T,"ab");StrAssign(&U,"XY");printf("替换前S=");for(int i=0;i<S.length;i++) printf("%c",S.ch[i]);printf("\n");Replace(&S,&T,&U);printf("替换后S=");for(int i=0;i<S.length;i++) printf("%c",S.ch[i]);printf("\n");return 0;
}
程序运行结果如下: