数据结构:串、数组与广义表

📌目录


🔤 一,串的定义

串(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语言):

    c 复制代码
    typedef struct {
        char *ch;  // 指向动态分配的字符数组
        int length; // 实际长度
    } HString;
  • 初始化示例

    c 复制代码
    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语言):

    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")。

核心思路

  1. 数据存储:使用堆分配串存储主文本(文章)、模式串(待查找单词)和替换串(新单词),支持动态调整长度;
  2. 查找逻辑:基于BF模式匹配算法,遍历主文本定位所有模式串的起始位置;
  3. 替换逻辑:对每个匹配位置,先删除原模式串,再插入替换串,更新主文本长度和内容。

完整代码实现

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解析等场景中广泛应用。

从本质上看,这些结构都是对"数据关系"的抽象:串强调字符的顺序关系,数组强调多维索引关系,广义表强调嵌套关系。理解它们的存储规律和核心算法,不仅能解决具体问题,更能培养"根据数据特性选择结构"的思维------这正是数据结构的核心素养。

相关推荐
柯南二号37 分钟前
MacOS 系统计算机专业好用工具安装
开发语言·lua
神洛华1 小时前
Lua语言程序设计2:函数、输入输出、控制结构
开发语言·lua
java1234_小锋2 小时前
一周学会Matplotlib3 Python 数据可视化-绘制热力图(Heatmap)
开发语言·python·信息可视化·matplotlib·matplotlib3
三体世界3 小时前
Mysql基本使用语句(一)
linux·开发语言·数据库·c++·sql·mysql·主键
etcix3 小时前
wrap cpp variant as dll for c to use
java·c语言·开发语言
Websites4 小时前
Hyperf 百度翻译接口实现方案
开发语言·自然语言处理·php·自动翻译
月殇_木言5 小时前
算法基础 第3章 数据结构
数据结构·算法
亮亮爱刷题5 小时前
算法提升之树上问题-(LCA)
数据结构·算法·leetcode·深度优先
papership5 小时前
【入门级-C++程序设计:11、指针与引用-引 用】
c语言·开发语言·c++·青少年编程