• 数据结构与算法——串


    🚀前言


    😊各位小伙伴久等了,本专栏新文章出炉了!!!

    在这里插入图片描述

    计算机上的非数值处理的对象基本上是字符串数据,字符串一般简称为是计算机中一种常见且重要的数据结构。

    在不同数据类型的应用中,所处理的字符串具有不同的特点,要有效的实现字符串的处理,就必须根据具体情况使用合适的存储结构。


    🚀串类型的定义


    串(String)(或字符串)是由零个或多个字符组成的有限序列,一般记为 S = ′ a 1 , a 2 , a 3 , . . . , a n ′ ( n > = 0 ) S='a1, a2, a3,...,an'(n>=0) S=a1,a2,a3,...,an(n>=0),其中,s是串的名,用单引号括起来的字符序列是串的值; a i ( 1 < = i < = n ) ai(1<=i<=n) ai1<=i<=n可以是字母,数字或其他字符;串中字符的数目n称为串的长度,零个字符的串称为空串(null String),表示为 t = “” t=“ ” t=“”,它的长度为零,串的逻辑结构属于线性结构。

    在这里插入图片描述

    📌例如:S=“holleworld!”

    由一个或多个空格字符组成的串,称为空格串(blank string,请注意,此处不是空串),它的长度为串中空格字符的个数

    串中任意个连续的字符组成的子序列称为该串的子串,包含子串的串相应地称为主串,空串是任意非空字符串的子串,通常称字符在序列中的序号为该字符在串中的位置,子串在主串中的位置则以子串的第一个字符在主串中的位置来表示

    想要称两个串是相等的,只有当且仅当这两个串的值相等,那么这两个串才是相等的,也就是说,只有当两个串的长度相等,并且各个对应位置的字符都相等时才相等

    值得一提的是,串值必须用一对单引号(双引号)括起来,但单引号本身不属于串,它的作用是为了避免与变量名或数的常量混淆而已

    串的逻辑结构和线性表极为相似,区别仅在于串的数据对象约束为字符集,然而,串的基本操作和线性表由很大差别。在线性表的基本操作中,大多以”单个元素“作为操作对象,例如在线性表中查找某个元素,求取某个元素,在某个位置上插入一个元素和删除一个元素等,而在串的基本操作中,通常以”串的整体“作为操作对象,例如在串中查找某一个子串,求取一个子串,在串中的某个位置上插入一个子串以及删除一个子串


    串与线性表的区别:

    • 📌串是限定了元素为字符的线性表
    • 📌线性表的操作主要针对表内的某一个元素,而串操作主要针对串内的一个子串(整体)
    • 📌在串里面,一个数据元素是由一个字符组成

    🚀串的存储结构以及实现


    串有3中机内表示方法,分别介绍如下:


    🚢定长顺序存储表示

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

    #define MAXLEN 255      //预定义最大串长为255
    typedef struct{
        char ch[MAXLEN];        //每个分量存储一个字符
        int length;             //串的长度
    }SString;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    串的实际长度可在这预定义长度的范围内随意,超过预定义长度的串值则被舍去,称之为“截断”。对串长有两种方法表示:一种是如上定义描述的那样,以下标为0的数组分量存放串的实际长度,另一种是在串值后面加一个不计入串长的结束标记字符,此时的串长为隐含值,显然不便于进行某些串的操作。


    🌈串联接Concat(&T, S1, S2)

    假设 S 1 , S 2 S1,S2 S1S2 T T T都是String型的串变量,且串T是由串S1联杰结串S2得到的,即串T的值的前一段和串S1的值相等,串T的值的后一段和串S2的值相等,则只要进行相应的“串值复制”操作即可,只是需前述约定,对超长部分实施“截断”操作


    🌈求子串SubString(& Sub, S, pos, len)

    求子串的过程即为复制字符序列的过程,将串S中从第pos个字符开始长度为len的字符序列赋值到串Sub中。显然,本操作不会有需截断的情况,但有可能产生用户给出的参数不符合操作的初始条件,返回false

    bool SubString(SString &Sub, SString S, int pos, int len){
        //子串范围越界
        if(pos+len-1 > S.length){
            return false;
        }
        for(int i = pos; i<pos+len; i++){
            Sub.ch[i-pos+1] = S.ch[i];
        }
        Sub.length = len;
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在顺序存储结构中,实现串操作的原操作为“字符序列的复制”,操作的时间复杂度基于复制的字符序列的长度,另一操作的特点是:如果在操作中出现串值序列的长度超过上界MAXSTRLEN时,约定用截尾法处理。这种情况不仅在求联串时可能发生,在串的其他操作中,如插入、置换等也可能发生。客服这个弊病唯有不限定串长的最大长度,即动态分配串值的存储空间。


    🌈比较操作

    若S>T,则返回值>0;若S=T,则返回值=0;若S

    //比较操作,若S>T,则返回值>0;若S=T,则返回值=0;若S
    int StrCompare(SString S, SString T){
        for(int i=1; i<S.length && i<T.length; i++){
            if(S.ch[i] != T.ch[i]){
                return S.ch[i] - T.ch[i];
            }
        }
        //扫描过的错有字符都相同,则长度长的串更大
        return S.length - T.length;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    🌈定位操作

    若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0。

    int Index(SString S, SString T){
        int i = 1, n = StrLength(s), m = StrLength(T);
        SString sub;        //用于暂存子串
        while(i<=n-m+1){
            SubStriing(sub, S, i, m);
            if(StrCompare(sub, T) != 0){
                i++;
            }else{
                return i;       //返回子串在主串中的位置
            }
        }
        return 0;       //S中不存在与T相等的子串
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    🚢堆分配存储表示

    这种存储表示的特点是,仍以一组地址连续的存储单元存放串值字符序列,但他们的存储空间是在程序执行过程中动态分配而得。利用函数malloc()为每个新产生的串分配一块实际串长所需要的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址,同时,为了以后处理方便,约定串长作为存储结构的一部分

    #define MAXLEN 255      //预定义最大串长为255
    typedef struct{
        char *ch;           //按串长分配存储区,ch指向串的基地址
        int length;         //串的长度
    }HString;
    
    HString S;
    S.ch = (char *)malloc(MAXLEN * sizeof(char));
    S.length = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这种存储结构表示时的串操作仍是基于“字符序列的复制”进行的。

    由于堆分配存储结构的串既有顺序存储结构的特点,处理方便,操作中对串长又没有任何限制,更显灵活,因此在串处理的应用程序中也常被选用


    🚢链式存储表示

    和线性表的链式存储结构相类似,可采用链表方式存储串值,由于串结构的特殊性——结构中的每个数据是一个字符,则用链表存储串值时,存在一个“结点大小”的问题,即每个结点可以存放一个字符,也可以存放多个字符。当结点大小大于1时,由于串长不一定是结点大小的整数倍,则链表中的最后一个结点不一定全被串值占满,此时通常补上“#”或者其他非串值字符(通常“#”不属于串的字符集,是一个特殊的符号

    第一种表示:

    在这里插入图片描述

    typedef struct StringNode{
        char ch;        //每个节点存一个字符
        struct StringNode * next;
    }StringNode, * String;
    
    • 1
    • 2
    • 3
    • 4

    为了便于进行串的操作,当以链表存储串值时,除头指针外还可附设一个尾指针表示链表中的最后一个结点,并给出当前串的长度。称如此定义的串存储结构为块链结构

    第二种表示:

    在这里插入图片描述

    typedef struct StringNode{
        char ch[4];        //每个节点存多个字符
        struct StringNode * next;
    }StringNode, * String;
    
    • 1
    • 2
    • 3
    • 4

    由于在一般情况下,对串进行操作时,只需要从头向尾扫描即可,则对串值不必建立双向链表。设尾指针的目的是为了便于进行联结操作,但应注意联结时需要处理第一个串尾的无效字符

    在链式存储方式中,结点大小的选择和顺序存储方式的格式选择一样都很重要,它直接影响着串处理的效率

    存储密度可定义为:串值所占的存储位/实际分配的存储位

    存储密度小(如结点大小为1时),运算处理方便,然而,存储占用最大,如果在串处理过程中需要进行内、外存交换的话,则会因为内外存交换操作过多而影响处理的总效率。一般地,字符集小,则字符的机内编码就短,这也影响串值的存储方式的选取。

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


    🚀拓展


    🚢朴素模式匹配算法

    串的模式匹配:在主串中找到与匹配模式相同的子串,并返回其所在位置。

    朴素模式匹配算法:
    将主串中与模式串长度相同的子串提出来,一个一个与模式串进行比较,当子串与模式串某个对应字符不匹配时,就立即放弃当前子串,转而检索下一个子串。

    int Index(SString S, SString T){
        int k=1;
        int i=k, j=1;
        while(i<=S.length && j<=T.length){
            if(S.ch[i] == T.ch[i]){
                ++i;
                ++j;        //继续比较后继字符
            }else{
                k++;        //检查下一个子串
                i=k;
                j=1;
            }
        }
        if(j>T.length){
            return k;
        }else{
            return 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    若模式串长度为m,主串长度为n,则匹配成功的最好时间复杂度为:O(m)匹配失败的最好时间复杂度为:O(n)

    若模式串长度为m,主串长度为n,则直到匹配成功/匹配失败最多需要 ( n − m + 1 ) ∗ m (n-m+1)*m nm+1m次比较。最坏时间复杂度就可表示为:O(nm)


    🚢KMP算法

    KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。

    朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描指针i会经常回溯,导致时间开销增加。KMP算法的出现就是为了解决主串指针回溯问题。

    int Index_KMP(SString T, int next[]){
        int i=1, j=1;
        while(i<=S.length && j<=T.length){
            if(j==0 || S.ch[i] == T.ch[j]){
                ++i;
                ++j;        //继续比较后继字符
            }else{
                j=next[j];      //模式串向右移动
            }
        }
        if(j>T.length){
            return i-T.length;      //匹配成功
        }else{
            return 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这里只是对KMP算法的 一个简单介绍,并没有过多深入,但是KMP算法是串这一章节中比较重要的一个算法,后面还会涉及到next数组的推算等等,所以将会在以后单独出一篇文章讲解KMP算法。


    💻总结


    在经过将近一个多月的停更后,本专栏的文章再次开始更新,之前因为个人原因停更,感谢各位小伙伴们的支持与等待,同样,在新文章更新的同时,我也会不断提高自己的创作水平,争取自己的文章能给小伙伴们带来更多的帮助。

    本篇文章比较全面的讲解了数据结构中串这一节的详细内容,因其与线性表类似的结构,所以有些点并没有重复提起,但是关于串的知识点,小伙伴们要多花时间去细读文章,串是编程中处理字符串比较重要的数据结构。一如既往希望我的文章能给各位小伙伴们带来帮助,数据结构与算法专栏也在持续更细中!!!


    🎨觉得不错的话记得点赞收藏呀!!🎨

    😀别忘了给我关注~~😀

  • 相关阅读:
    洛谷 P1909 [NOIP2016 普及组] 买铅笔
    Scrum敏捷开发实施步骤和注意事项
    ATT&CK初步了解
    神经网络 深度神经网络,深度神经网络的深度是?
    戴口罩 目标检测数据集-12000张
    飞书公式总结
    面试必问:Redis 如何实现库存扣减操作?
    Final Cut Pro 10.6.10中文用法儿
    Paillier算法简介
    (零代) MDD 开创低代码领行设计模式
  • 原文地址:https://blog.csdn.net/weixin_53614367/article/details/126139418