数据结构(串、数组和广义表)


1、串

①.串的定义

串(String)----零个或多个字符组成的有限序列,是一种特殊的线性表,其数据元素为一个字符,即内容受限的线性表。

在这里插入图片描述

子串:

串中任意个连续的字符组成的子序列

主串:

包含子串的串

字符位置:

字符在序列中的序号

子串位置:

以子串的第一个字符在主串中的位置来表示

空格串:

一个或多个空格组成的串

在这里插入图片描述

两个串是相等的,当且仅当这两个串的值相等。也就是说,只有当两个串的长度相等,并且各个对应位置的字符都相等时才相等。例如,上例中的串a、b、c和d彼此都不相等。

串的抽象数据类型的定义:

在这里插入图片描述
在这里插入图片描述

与线性表类似,串也有两种基本存储结构:顺序存储和链式存储。但考虑到存储效率和算法的方便性,串多采用顺序存储结构。

②.串的顺序存储

类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区,则可用定长数组如下描述:

typedef struct
{
    char ch[MAXLEN + 1]; // MAXLEN为串的最大长度
                         //存储串的一维数组
    int length; //串长度
} SString;

而多数情况下,串的操作是以串的整体形式参与的,串变量之间的长度相差较大,在操作中串值长度的变化也较大,这样为串变量设定固定大小的空间不尽合理。因此最好是根据实际需要,在程序执行过程中动态地分配和释放字符数组空间。在C语言中,存在一个称之为“堆”(Heap)的自由存储区,可以为每个新产生的串动态分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址,同时为了以后处理方便,约定串长也作为存储结构的一部分。

typedef struct
{
    char *ch;   //若串非空,则按串长分配存储区,否则ch为NULL
    int length; //串长度
} HString;

③.串的链式存储

顺序串的插人和删除操作不方便,需要移动大量的字符。因此,可采用单链表方式存储串。

#define CHUNKSIZE 80 //可由用户定义的块大小
typedef struct Chunk
{
    char ch[CHUNKSIZE]; //存储串的一维
    struct Chunk *next;
} Chunk;

typedef struct
{
    Chunk *head, *tail; //串的头指针和尾指针
    int curlen;         //串的当前长度
} LString;

串值的链式存储结构对某些串操作,如联接操作等,有一定方便之处,但总地说来,不如顺序存储结构灵活,它占用存储量大且操作复杂。

④.串的模式匹配算法

子串的定位运算通常称为串的模式匹配或串匹配,主串称为正文串,子串称为模式串,该算法目的是确定主串中缩含子串第一次出现的位置(定位)。此运算的应用非常广泛,比如在搜索引擎、拼写检查、语言翻译、数据压缩等应用中,都需要进行串匹配。

BF(Brute-Force)算法

模式匹配不一定是从主串的第一个位置开始,可以指定主串中查找的起始位置pos。如果采用字符串顺序存储结构,可以写出不依赖于其他串操作的匹配算法。

  1. 分别利用计数指针i和j指示主串S和模式T中当前正待比较的字符位置,i初值为pos,j初值为1。

  2. 如果两个串均未比较到串尾,即i和j均分别小于等于S和T的长度时,则循环执行以下操作:

    - S[i].ch和T[j].ch 比较,若相等,则i和j分别指示串中下个位置,继续比较后续字符;
    
    - 若不等,指针后退重新开始匹配,从主串的下一个字符( i=i-j+2)起再重新和模式的第一个字符(j=1 )比较。
    
  3. ③如果j>T.length,说明模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则匹配成功,返回和模式T中第一个字符相等的字符在主串S中的序号( i-T.length );否则称匹配不成功,返回0。

在这里插入图片描述

int Index(SString S, SString T, int pos)
{
    i = pos;
    j = 1;                              //初始化
    while (i <= S[0] && j <= T[0])		//两个串均未比较到串尾
    {
        if (S[i] = T[j])
        {
            ++i;
            ++j;
        } //继续比较后继字符
        else
        {
            i = i - j + 2;
            j = 1;
        } //指针后退重新开始匹配
    }
    if (j > T[0])
        return i-T[0]; //匹配成功
    else
        return 0; //匹配失败
}

BF算法实例:

/***字符串匹配算法***/
#include <cstring>
#include <iostream>
using namespace std;

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
#define MAXSTRLEN 255                //用户可在255以内定义最长串长
typedef char SString[MAXSTRLEN + 1]; // 0号单元存放串的长度

Status StrAssign(SString T, char *chars)
{ //生成一个其值等于chars的串T
    int i;
    if (strlen(chars) > MAXSTRLEN)
        return ERROR;
    else
    {
        T[0] = strlen(chars);
        for (i = 1; i <= T[0]; i++)
            T[i] = *(chars + i - 1);
        return OK;
    }
}

// BF算法
int Index(SString S, SString T, int pos)
{
    //返回模式T在主串S中第pos个字符之后第s一次出现的位置。若不存在,则返回值为0
    //其中,T非空,1≤pos≤StrLength(S)
    int i = pos;
    int j = 1;
    while (i <= S[0] && j <= T[0])
    {
        if (S[i] == T[j])
        {
            ++i;
            ++j;
        } //继续比较后继字符
        else
        {
            i = i - j + 2;
            j = 1;
        } //指针后退重新开始匹配
    }
    if (j > T[0])
        return i - T[0];
    else
        return 0;
    return 0;
} // Index

int main()
{
    SString S;
    StrAssign(S, "bbaaabbaba");
    SString T;
    StrAssign(T, "abb");
    cout << "主串和子串在第" << Index(S, T, 1) << "个字符处首次匹配\n";
    return 0;
}

该算法时间复杂度为:O(n*m)

KMP算法

这种改进算法是由Knuth、Morris和 Pratt同时设计实现的,因此简称KMP算法。此算法可以在O(n + m)的时间数量级上完成串的模式匹配操作。其改进在于:每当一趟匹配过程中出现字符比较不等时,不需回溯i指针,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较。

  1. 分别利用计数指针i和j指示主串S和模式T中当前正待比较的字符位置,i初值为pos,j初值为1。

  2. 如果两个串均未比较到串尾,即i和j均分别小于等于S和T的长度时,则循环执行以下操作:

    - S[i].ch 和T[i].ch 比较,若相等,则i和j分别指示串中下个位置,继续比较后续字符;
    - 若不等,指针后退重新开始匹配,从主串的下一个字符(i=i-j+2)起再重新和模式的第一个字符(j=1)比较。
    
  3. 如果j>T.length,说明模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则匹配成功,返回和模式T中第一个字符相等的字符在主串S中的序号(i-T.length);否则称匹配不成功,返回0。

该算法思想感兴趣可观看天勤公开课讲解视频,个人认为讲解比较透彻:

「天勤公开课」KMP算法易懂版

以下对其进行简单阐述:

kmp算法作用是快速的从一个主串中找到想要的子串,kmp算法可以做到仅仅后移模式串,比较指针不回溯。

在这里插入图片描述
在这里插入图片描述

找出模式串不匹配处最长但不长于不匹配处左端的公共前后缀,直接移动模式串,使得前缀直接来到后缀的位置。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

int Index_KMP(SString S, SString T, int pos)
{
    i = pos;
    j = 1;                                 //初始化
    while (i <= S.length && j <= S.length) //两个串均未比较到串尾
    {
        if (j == 0 || S[i] == T[j])
        {
            ++i;
            ++j;
        } //继续比较后继字符
        else
        {
            j = next[j];
        } //模式串向右移动
    }
    if (j > T[0])
        return i - T[0]; //匹配成功
    else
        return 0; //匹配失败
}

void get_next(SString T, int next[])
{ //求模式串T的next函数值并存入数组next
    i = 1;
    next[1] = 0;
    j = 0;
    while (i < T[0])
    {
        if (j == 0 || T[i])
        {
            ++i;
            ++j;
            next[i] = j;
        }
        else
        {
            j = next[j];
        }
    }
}

KMP算法实例:

/***字符串匹配算法***/
#include <cstring>
#include <iostream>
using namespace std;

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
#define MAXSTRLEN 255                //用户可在255以内定义最长串长
typedef char SString[MAXSTRLEN + 1]; // 0号单元存放串的长度

Status StrAssign(SString T, char *chars)
{ //生成一个其值等于chars的串T
    int i;
    if (strlen(chars) > MAXSTRLEN)
        return ERROR;
    else
    {
        T[0] = strlen(chars);
        for (i = 1; i <= T[0]; i++)
            T[i] = *(chars + i - 1);
        return OK;
    }
}
//算法4.3 计算next函数值
void get_next(SString T, int next[])
{ //求模式串T的next函数值并存入数组next
    int i = 1, j = 0;
    next[1] = 0;
    while (i < T[0])
        if (j == 0 || T[i] == T[j])
        {
            ++i;
            ++j;
            next[i] = j;
        }
        else
            j = next[j];
} // get_next

// KMP算法
int Index_KMP(SString S, SString T, int pos, int next[])
{ // 利用模式串T的next函数求T在主串S中第pos个字符之后的位置的KMP算法
    //其中,T非空,1≤pos≤StrLength(S)
    int i = pos, j = 1;
    while (i <= S[0] && j <= T[0])
        if (j == 0 || S[i] == T[j]) // 继续比较后继字
        {
            ++i;
            ++j;
        }
        else
            j = next[j]; // 模式串向右移动
    if (j > T[0])        // 匹配成功
        return i - T[0];
    else
        return 0;
} // Index_KMP

int main()
{
    SString S;
    StrAssign(S, "aaabbaba");
    SString T;
    StrAssign(T, "abb");
    int *p = new int[T[0] + 1]; // 生成T的next数组
    get_next(T, p);
    cout << "主串和子串在第" << Index_KMP(S, T, 1, p) << "个字符处首次匹配\n";
    return 0;
}

时间复杂度:O(n+m)


2、数组

①.数组的定义

数组是由类型相同的数据元素构成的有序集合,每个元素称为数组元素,每个元素受n(n≥1 )个线性关系的约束,每个元素在n个线性关系中的序号i1,i2,…,in称为该元素的下标,可以通过下标访问该数据元素。因为数组中每个元素处于n (n≥1)个关系中,故称该数组为n维数组。数组可以看成是线性表的推广,其特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型。

在这里插入图片描述

②.数组的顺序存储

由于数组一般不做插人或删除操作,也就是说;一旦建立了数组,则结构中的数据元素个数和元素之间的关系就不再发生变动。因此,采用顺序存储结构表示数组比较合适。
二维数组可有两种存储方式:一种是以列序为主序的存储方式;一种是以行序为主序的存储方式。

在这里插入图片描述

插

三维数组的顺序存储:

在这里插入图片描述
在这里插入图片描述

③.特殊矩阵的压缩存储

特殊矩阵: 值相同的元素或0元素在矩阵中的分布有一定的规律。如:对称矩阵、三角矩阵、对角矩阵等。

压缩存储: 压缩存储是指为多个值相同的元只分配一个存储空间,且对零元不分配存储空间。目的是节省大量存储空间。

什么样的矩阵能够压缩?
特殊矩阵、稀疏矩阵等。

对称矩阵

若n阶矩阵A中的元满足aij = aji 1≤i,j≤ n 则称为n阶对称矩阵。对于对称矩阵,可以为每一对对称元分配一个存储空间,则可将n2个元压缩存储到n(n+1)/2个元的空间中,不失一般性,可以行序为主序存储其下三角(包括对角线)中的元。
假设以一维数组sa[r(n+1)/2]作为n阶对称矩阵A的存储结构,则 sa[k]和矩阵元aj之间存在着一一对应的关系:

在这里插入图片描述

三角矩阵

以主对角线划分,三角矩阵有上三角矩阵和下三角矩阵两种。上三角矩阵是指矩阵下三角(不包括对角线)中的元均为常数c或零的n阶矩阵,下三角矩阵与之相反。对三角矩阵进行压缩存储时,除了和对称矩阵一样,只存储其上(下)三角中的元素之外,再加一个存储常数c的存储空间即可。

上三角矩阵
sa[k]和矩阵元aij之间的对应关系为:
在这里插入图片描述

下三角矩阵
sa[k]和矩阵元aij之间的对应关系为:
在这里插入图片描述

对角矩阵

对角矩阵所有的非零元都集中在以主对角线为中心的带状区域中,即除了主对角线上和直接在对角线上、下方若干条对角线上的元之外,所有其他的元皆为零。
在这里插入图片描述


3、广义表

①.广义表的定义

广义表:n ( ≥0)个表元素组成的有限序列,记作LS=(a1,a2,…,an)。LS是表名,ai是表元素,它可以是表(称为子表),可以是数据元素(称为原子)。n为表的长度,n=0的广义表称为空表。

求表头GetHead(L):非空广义表的第一个元素,可以是一个原子,也可以是一个子表。
求表尾GetTail(L):非空广义表除去表头元素以外其它所有元素所构成的表。表尾一定是一个表。

在这里插入图片描述

②.广义表的存储结构

由于广义表中的数据元素可以有不同的结构(或是原子,或是列表),因此难以用顺序存储结构表示,通常采用链式存储结构。

头尾链表的存储结构

由于广义表中的数据元素可能为原子或广义表,由此需要两种结构的结点:一种是表结点,用以表示广义表;一种是原子结点,用以表示原子。从上节得知:若广义表不空,则可分解成表头和表尾,因此,一对确定的表头和表尾可唯一确定广义表。一个表结点可由3个域组成:标志域、指示表头的指针域和指示表尾的指针域。而原子结点只需两个域:标志域和值域。

在这里插入图片描述

typedef enum
{
    ATOM,
    LIST
} ElemTag;
// ATOM==0:原子,LIST==1:子表
typedef struct GLNode
{
    ElemTag tag; //公共部分,用于区分原子结点和表结点
    union        //原子结点和表结点的联合部分
    {
        AtomType atom; // atom是原子结点的值域,AtomType由用户定义
        struct
        {
            struct GLNode *hp, *tp;
        } ptr;
        // ptr是表结点的指针域,prt.hp和ptr.tp分别指向表头和表尾
    };
} * GList, GLNode; /* 广义表类型 */

扩展线性链表的存储结构

在这种结构中,无论是原子结点还是表结点均由三个域组成

在这里插入图片描述

typedef struct glnode
{
    int tag; // 0 原子结点;1 子表结点
    union
    {
        atomtype atom;     //原子结点的值域
        struct glnode *hp; //子表表头指针
    } struct glnode *tp;   //下一元素指针
} * glist;


4、总结

  1. 串是内容受限的线性表,它限定了表中的元素为字符。串有两种基本存储结构:顺序存储和链式存储,但多采用顺序存储结构。串的常用算法是模式匹配算法,主要有BF算法和KMP算法。BF算法实现简单,但存在回溯,效率低,时间复杂度为O(m ×n)。KMP算法对BF算法进行改进,消除回溯,提高了效率,时间复杂度为O(m+n)。

  2. 多维数组可以看成是线性表的推广,其特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型。一个n维数组实质上是n个线性表的组合,其每一维都是一个线性表。数组一般采用顺序存储结构,故存储多维数组时,应先将其确定转换为一维结构,有按“行”转换和按“列”转换两种。科学与工程计算中的矩阵通常用二维数组来表示,为了节省存储空间,对于几种常见形式的特殊矩阵,比如对称矩阵、三角矩阵和对角矩阵,在存储时可进行压缩存储,即为多个值相同的元只分配一个存储空间,对零元不分配空间。

  3. 广义表是另外一种线性表的推广形式,表中的元素可以是称为原子的单个元素,也可以是一个子表,所以线性表可以看成广义表的特例。广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构。广义表的常用操作有取表头和取表尾。广义表通常采用链式存储结构:头尾链表的存储结构和扩展线性链表的存储结构。


5、例题与应用

在这里插入图片描述
在这里插入图片描述

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会思想的苇草i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值