【数据结构】串——(一)

目录

串的概念

串的基本概念

  • 定义
    串(String)是由 零个或多个字符 组成的 有限序列
    • 一般记作:S = "a1 a2 a3 ... an"
    • 其中:
      • S 是串的名字
      • a1、a2、...、an 是串的各个字符
      • n 表示串的长度
  • 注意
    • 串是 特殊的线性表:数据元素限定为"字符"。
    • 和普通线性表不同,串主要是用于 处理字符数据,常见操作和应用方向会偏向文本。

串的相关概念

  1. 空串 :长度为 0 的串,记作 ""
  2. 空格串:由一个或多个空格字符组成的串,⚠️ 和空串不同。长度为空格的数量。
  3. 子串 :串中任意连续的若干个字符组成的子序列。
    • 例:"abcdef" 的子串有 "abc""de" 等。
  4. 主串 :包含子串的串。
    • "abcdef" 里,"abc" 是子串,而 "abcdef" 是主串。
  5. 字符位置 :字符在串中的序号,一般 从 1 开始(和数组不同)。

串与线性表的区别

  • 相同点 :串和线性表一样,都是 线性存储结构,有序排列。
  • 不同点
    1. 数据对象范围不同
      • 线性表元素可以是任意数据对象。
      • 串的元素是 字符
    2. 基本操作不同
      • 线性表操作侧重于 插入、删除
      • 串操作侧重于 查找、匹配、拼接、截取

线性表 → 存什么都行,比如学生、商品、成绩

→ 只能存字符,主要用于处理"文本"类问题,比如:搜索、替换、匹配(比如你常用的字符串操作函数 strlenstrcmpstrcatstrstr 等其实就是"串的操作")。

串的常见基本操作

  1. 串赋值(StrAssign)

    • 将一个常量字符串赋给串变量。

    • 例:StrAssign(S, "hello")

  2. 串复制(StrCopy)

    • 把一个串复制到另一个串。

    • 例:StrCopy(T, S)

  3. 判空(StrEmpty)

    • 判断一个串是否为空串(长度 = 0)。

    • 例:StrEmpty(S)true or false

  4. 求串长(StrLength)

    • 返回串的字符个数。

    • 例:StrLength("abc") = 3

  5. 串比较(StrCompare)

    • 按字典顺序比较两个串的大小。

    • 例:

      • "abc" < "abd"
      • "abc" = "abc"
  6. 清空串(ClearString)

    • 把串置为空串。
  7. 串连接(Concat)

    • 生成一个新串,其值是两个串连接而成。

    • 例:Concat("hello", "world") = "helloworld"

  8. 求子串(SubString)

    • 截取主串中从第 i 个字符开始、长度为 len 的子串。

    • 例:SubString("abcdef", 2, 3) = "bcd"

  9. 定位操作(Index / StrIndex)

    • 在主串中查找某个子串第一次出现的位置。

    • 例:

      • Index("hello world", "world") = 7
      • 若不存在则返回 0 或 -1(视实现而定)。
  10. 替换操作(Replace)

    • 用新的子串替换主串中所有出现的某个子串。

    • 例:Replace("abcabc", "ab", "xy") = "xycxyc"

  11. 插入(StrInsert)

    • 在主串的第 i 个字符前插入一个子串。

    • 例:Insert("abc", 2, "XYZ") = "aXYZbc"

  12. 删除(StrDelete)

    • 删除主串中第 i 个字符开始的若干字符。
      • 例:Delete("abcdef", 2, 3) = "aef"
  13. 销毁(DestroyString)

    • 释放串所占用的存储空间,使之成为一个无效的串变量。

串的存储表示及实现

定长顺序存储表示 SString(静态存储)

定义 :用一个 定长数组 存放串中的字符。

特点

  • 数组大小预先固定(比如 MAXSIZE=255)。
  • 实际串长 ≤ MAXSIZE

**结构体定义:

c 复制代码
#define MAXSIZE 255
typedef struct {
    char ch[MAXSIZE]; // 存放串的字符
    int length;       // 串的实际长度
} SString;

优点:简单,容易实现。

缺点:空间可能浪费,串长受数组上限限制。

堆分配存储表示 HString(动态存储)

定义 :用一段 动态分配的连续存储区 存放串的字符。

特点

  • 串长不受预定义常量限制,只要内存够就行。
  • 适合变长串的存储。

结构体定义

c 复制代码
typedef struct {
    char *ch;   // 指向动态分配的字符数组
    int length; // 串的长度
} HString;

例子

  • 当串长度变化时,可以通过 malloc / realloc 动态调整。

优点:灵活,适合实际工程。

缺点:需要频繁申请/释放内存,管理成本较高。

块链存储表示 LString(链表方式)

定义:把串的字符分散存放在若干个链表节点中,每个结点可以存放多个字符。

结构体定义

c 复制代码
#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 的逻辑

  1. 判断源串长度是否超过 SString 最大长度 MAXSIZE
  2. 遍历源串,将每个字符逐个复制到 S.ch
  3. 设置 S.length 为实际字符数

⚠️ 注意:SString 是定长顺序存储,所以不能超过数组容量。

c 复制代码
// 串赋值
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 的逻辑

  1. 如果 H->ch 不为空,先 free(H->ch),释放原有空间
  2. 计算源字符串长度 len
  3. 分配新空间:H->ch = (char*)malloc(sizeof(char) * len)
  4. 将源字符串字符逐一复制到 H->ch
  5. 设置 H->length = len
  6. 返回成功标志

⚠️ 注意:

  • HString 是动态分配,内存需要手动释放
  • 如果 malloc 失败,要返回失败
c 复制代码
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 的区别:

  1. 动态分配:长度可变,可释放空间
  2. 赋值操作需 malloc,而 SString 直接写入数组

后续操作(连接、插入、删除等)也都需要注意 空间是否足够

LString 的 StrAssign 的逻辑

  1. 如果 L->head 不为空,先释放原链表
  2. 初始化头结点:L->head = NULL 或创建一个空头结点
  3. 遍历源字符串 chars:
    • 创建新结点 node
    • node->data = chars[i]
    • 将 node 插入链表尾部
  4. 更新 L->length
  5. 返回成功标志

⚠️ 注意:

  • 链表操作中,插入到尾部通常需要维护一个 尾指针,避免每次都从头遍历
  • 分配结点失败,需要返回失败
c 复制代码
// 释放链表
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 的逻辑

  1. 遍历源串 S.ch,将每个字符复制到目标串 T.ch
  2. 设置 T.length = S.length。

⚠️ 注意:SString 是定长顺序存储,目标串容量不能小于源串长度

c 复制代码
// 串复制
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 的逻辑

  1. 如果目标串 T->ch 不为空,先 free(T->ch)
  2. 获取源串长度 len = S->length
  3. 分配新空间:T->ch = malloc(len + 1)
  4. 将源串 S->ch 复制到目标串 T->ch
  5. 设置 T->length = S->length
  6. 返回成功标志

⚠️ 注意:

  • 必须处理目标串原有空间释放,否则会内存泄漏
  • 动态分配失败需要返回失败
c 复制代码
// 串复制 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 的逻辑

  1. 释放目标串 T 的旧链表

  2. 若源串 S 为空串 → 直接返回空串

  3. 遍历源串 S:

    • 为每个字符分配新结点

    • 将字符复制到结点

    • 按顺序接到目标串链表中

  4. 更新 T->length

  5. 返回成功标志

c 复制代码
// 释放链表
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:直接数组拷贝(静态存储,空间固定)
    • HStringmalloc 一次性分配整段空间再 strcpy
    • LString :逐个结点 malloc 并复制字符

👉 所以 LString 的复制效率最低,但灵活性最高(支持无限长串,不受数组限制)。

判空(StrEmpty)操作

SString 的 StrEmpty 的逻辑

  1. 检查 S.length 是否为 0
  2. 如果是 0 → 返回真
  3. 否则 → 返回假

⚠️ 注意:S.ch 中的内容无所谓,只要 length=0 就是空串。

c 复制代码
// 判空操作
int StrEmpty(const SString *S) {
    return (S->length == 0);
}

判空操作非常简单,属于 管理类操作

在其他串操作(如插入、删除、连接)前,通常先判空,以避免越界。

空串:length = 0

非空串:length > 0

HString 的 StrEmpty 的逻辑

  1. 检查 H->length 是否为 0
  2. 可选:也可检查 H->ch 是否为 NULL
  3. 返回布尔值
c 复制代码
int StrEmpty(const HString *H){
    return (H->length == 0) ? 1 : 0;
}

HString 判空 直接判断 length 是否为 0,效率非常高

SString 判空类似,只是 HString 的长度是动态维护的

释放内存后也算空串,length = 0

LString 的 StrEmpty 的逻辑

  1. 检查 L->length 是否为 0
  2. 或者检查 L->head 是否为 NULL
  3. 返回布尔值

链式存储的串,空串通常 head = NULL 且 length = 0,两者保持一致

c 复制代码
// 判空 StrEmpty
int StrEmpty(const LString *L){
    return (L->length == 0) ? 1 : 0;
}

判断依据 :链表长度 length 或头指针 head

特点:效率高(O(1)),不需要遍历链表

SString / HString 判空逻辑相似,只是底层存储不同

求串长(StrLength)操作

SString 的 StrLength 的逻辑

  1. 读取 S.length
  2. 返回该值

⚠️ 注意:SString 已经存储了长度信息,所以 不需要遍历数组

  • 这是 SString 的优势之一:求长度操作 O(1)
c 复制代码
// 求串长操作
int StrLength(const SString *S) {
    return S->length;
}

SString 来说,求长度操作非常高效,只需返回 length 成员。

HString 或 LString,如果没有缓存长度,求长度可能需要遍历整个字符数组或链表,复杂度 O(n)。

这个操作通常用于:循环、判空、插入、删除、截取等其他操作前的判断。

HString 的 StrLength 的逻辑

  1. 直接访问 H->length
  2. 返回值

HString 在赋值、插入、删除等操作中会维护 length,所以求串长是 O(1) 操作

c 复制代码
// 求串长 StrLength
int StrLength(const HString *H){
    return H->length;
}

HString 的长度 length动态维护的字段,不需要遍历数组

SString 对比:

  • SString 静态数组也可以维护 length,同样 O(1)
  • 如果不维护 length,则需要遍历数组统计字符个数(O(n))

求串长操作非常高效,是其他操作(插入、删除、比较)的基础

LString 的 StrLength 的逻辑

  1. 若 LString 维护 length,直接返回 L->length
  2. 若没有维护 length,则:
    • 遍历链表,从头结点到尾结点
    • 每遍历一个结点计数加 1
    • 返回计数

通常我们在赋值、插入、删除时维护 length,所以求串长是 O(1) 操作

c 复制代码
// 求串长 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. 从第 1 个字符开始,依次比较 S.ch[i] 和 T.ch[i]
  2. 遇到不同字符:返回它们 ASCII 值差 S.ch[i] - T.ch[i]
  3. 遍历完较短串后:
    • 如果所有字符相等,则 长度短的串小

⚠️ 注意:

  • SString 的下标一般从 0 或 1,教材里常从 1 开始
  • 比较的是 字典序(ASCII 顺序)
c 复制代码
// 串比较
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 的逻辑

  1. 从两个串的第一个字符开始逐个比较
  2. 当遇到第一个不同的字符时:
    • 返回 S->ch[i] - T->ch[i]
  3. 若一串提前结束:
    • 返回长度差 S->length - T->length
  4. 若完全相同:
    • 返回 0

⚠️ 注意:

  • HString 是动态数组,可随机访问,比较效率高
  • 可以直接用循环或 strncmp
c 复制代码
// 串比较 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 的逻辑

  1. 从两个串的头结点开始逐结点比较 data
  2. 当遇到第一个不同字符时:
    • 返回 S->data - T->data
  3. 若一串提前结束:
    • 返回长度差 S->length - T->length
  4. 若完全相同:
    • 返回 0

⚠️ 注意:

  • 链表存储不能随机访问,需要顺序遍历
  • 时间复杂度 O(n),n = 较短串长度
c 复制代码
// 串比较 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 的逻辑

  1. 将 S.length 置为 0
  2. 可选:把 S.ch 数组中的字符清空(不是必须)

⚠️ 注意:

  • 清空 ≠ 销毁
    • 清空串:空间保留,可继续使用
    • 销毁串:释放空间,串不可再用
c 复制代码
// 清空串
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 的逻辑

  1. H->length = 0
  2. 可选:将 H->ch 内存清零(便于调试)
  3. 不释放 H->ch,空间仍可使用

销毁 DestroyString 的区别:

  • ClearString:长度置 0,空间可用
  • DestroyString:释放空间,串变量变无效
c 复制代码
// 清空串 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 的逻辑

  1. 遍历链表释放每个结点
  2. L->head = NULL
  3. L->length = 0
  4. 串变量仍然可用,便于重新赋值

销毁 DestroyString 的区别:

  • ClearString:释放结点,变量可用
  • DestroyString:释放结点,变量变无效(通常还要清掉结构体内容)
c 复制代码
// 清空串 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 的逻辑

  1. 计算 S1.length + S2.length
  2. 如果总长度 ≤ MAXSIZE → 直接复制
    • 先把 S1.ch 复制到 T.ch
    • 再把 S2.ch 追加到 T.ch 后面
    • 设置 T.length = S1.length + S2.length
  3. 如果总长度 > MAXSIZE → 截断(或报错)

⚠️ 注意:SString 是 定长顺序存储,不能超过 MAXSIZE

c 复制代码
// 串连接
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 的逻辑

  1. 分配长度为 S->length + T->length + 1 的新空间
  2. 将 S->ch 的内容复制到新空间
  3. 将 T->ch 的内容接到 S 后面
  4. 更新 H->length
  5. H->ch 指向新空间

⚠️ 注意:

  • HString 是动态数组,连接需要新分配内存
  • 原来的 H->ch 如果存在,要先释放
c 复制代码
// 串连接 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 的逻辑

  1. 先清空 H,避免内存泄漏
  2. 遍历 S,逐个结点复制到 H
  3. 遍历 T,逐个结点复制到 H
  4. 更新 H->length

⚠️ 注意:

  • LString 是 链式存储 ,所以不能直接拼接指针,而是要 复制新结点
  • 否则修改 H 会影响 ST
c 复制代码
// 串连接 Concat
int Concat(LString *H, const LString *S, const LString *T){
    ClearString(H);
    LNode *tail = NULL;

    // 复制 S
    LNode *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;
    }

    // 复制 T
    p = 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 的逻辑

  1. 检查 poslen 是否有效:
    • pos >= 1pos <= S.length
    • len >= 0pos + len - 1 <= S.length
  2. 遍历源串,从 S.ch[pos-1] 开始,复制 len 个字符到 Sub.ch
  3. 设置 Sub.length = len

⚠️ 注意:SString 是定长顺序存储,Sub.length 不能超过 MAXSIZE

c 复制代码
// 求子串
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)操作

定位操作逻辑(暴力匹配法)

  1. 检查子串长度是否为 0
    • 如果是 → 通常返回 1(空串在任意位置匹配)
  2. 遍历主串 S,从位置 i = 1 到 S.length - T.length + 1
  3. 对每个位置 i,比较 S 从 i 开始的连续 T.length 个字符是否等于 T
  4. 若匹配 → 返回 i
  5. 遍历完都不匹配 → 返回 0
c 复制代码
// 定位操作(暴力匹配法)
int Index(const SString *S, const SString *T) {
    if(T->length == 0) return 1; // 空串匹配返回1
    for(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 的逻辑

  1. 检查 T 是否为空串
    • 若为空串 → 通常不进行替换
  2. 在主串 S 中循环查找 T 的位置(可使用 Index
  3. 找到后:
    • 删除位置 i 的 T 子串
    • 在位置 i 插入 V 子串
  4. 继续查找下一个 T,直到 S 中不再出现 T
  5. 注意:
    • SString 的长度不能超过 MAXSIZE
    • 删除 + 插入操作必须保持数组连续性
c 复制代码
// 替换操作
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. 检查插入位置是否合法:
    • 1 <= pos <= S.length + 1
  2. 检查插入后长度是否超过最大容量 MAXSIZE
  3. 将主串从 pos 开始的字符向后移动 T.length 个位置,为插入腾出空间
  4. T.ch 的内容复制到 S.ch[pos-1 ... pos-1+T.length-1]
  5. 更新 S.length = S.length + T.length

⚠️ 注意:SString 是顺序存储,数组必须连续,所以要从后向前移动字符,避免覆盖。

c 复制代码
// 插入子串
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. 检查删除位置是否合法:
    • 1 <= pos <= S.length
    • 0 <= len <= S.length - pos + 1
  2. 将从 pos + len 开始的字符向前移动 len 个位置,覆盖被删除的部分
  3. 更新 S.length = S.length - len

⚠️ 注意:SString 是顺序存储,删除后数组仍然连续,不需要额外释放空间

c 复制代码
// 删除子串
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 的逻辑

  1. 对 SString(静态顺序存储):
    • 设置 S.length = 0
    • 可选:清空 S.ch 数组
    • 标记串不可再使用(逻辑上)
  2. 对 HString(动态顺序存储):
  3. 对链式存储(LString):
    • 顺序释放所有链表节点

⚠️ 注意:销毁后不能再对该串进行任何操作,否则会访问非法内存

c 复制代码
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(静态数组),清空和销毁的区别主要在"逻辑含义"

  • 清空:可继续使用
  • 销毁:逻辑上不再使用,不能再调用操作函数

操作集合

c 复制代码
#include <stdio.h>
#define MAXSIZE 255

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

程序运行结果如下: