串结构解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 串结构解析

@[toc]

本篇将讲述串的相关内容。

在前面,我们学习了线性表、栈和队列,接下来我们继续学习串、数组和广义表的相关内容。
对于串,学过字符串的同学应该都不陌生,而C语言中没有字符串类型,是通过字符数组实现的,字符串的一些基本操作,比如分割字符串、求字符串长度等都是通过操作字符数组实现的,C语言之后的一些高级语言都自己封装了字符串类型。

那么首先就来了解一下C语言中的串。

串的定义

串是由零个或多个任意字符组成的有序序列

s = "a1a2...an"(n≥0),其中s为串名,引号部分为串值,n为串长。当n = 0时,称s为空串,通常用符号表示。

串的相关概念

  • 子串:一个串中任意个连续字符组成的子序列(含空串)称为该串的子串。例如,"h","hello","world","w","wor","ld",都是"helloworld"的子串
  • 主串:包含子串的串称为主串
  • 字符位置:字符在字符序列中的序号
  • 子串位置:子串第一个字符在主串中的位置
  • 空格串:由一个或多个空格组成的串,与空串不同
  • 串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,这两个串才是相等的

串的抽象数据类型定义

到现在为止,关于串的抽象数据类型定义相信大家都能够自己写出来,我就直接贴出来了:

ADT String{
   
   
    数据对象:D = {
   
   ai|ai∈CharacterSet,i = 1,2,...,n-1,n,n≥0}
    数据关系:R = {
   
   <ai-1,ai>|ai-1,ai∈D,i = 1,2,...,n-1,n}
    基本操作:
        StrAssign(&T,chars);//串赋值
        StrCompare(S,T);//串比较
        StrLength(S);//求串长
        Concat(&T,S1,S2);//串连接
        SubString(&Sub,S,pos.len);//求子串
        StrCopy(&T,S);//串拷贝
        ......
}ADT String

关于串的操作非常多,我就不全部写出来了。

串的存储结构

串的结构和线性表相同,所以串可以采用与线性表相同的存储结构,我们同样介绍以下两种存储结构:

  1. 顺序存储结构
  2. 链式存储结构

串的顺序存储结构

串的顺序存储结构定义如下:

#define MAXSIZE 10
typedef struct{
   
   
    char ch[MAXSIZE + 1];//字符数组,表示串
    int length;//串长
}String;

在声明字符数组的时候通常会在原数组大小上加1,至于为什么这样做放到后面说。

串的链式存储结构

串的链式存储结构同样通过结点生成一个链表,每个结点存储对应的结点,如下图所示:
在这里插入图片描述
链表的优点显而易见,操作方便,存取快速,但是缺点也很大,存储密度太低,一个char类型的字符就要占用一个结点的内存。

办法总比困难多,为了解决链表存储密度低的问题,可以在一个结点里存储多个字符,充分利用空间,如下图所示:
在这里插入图片描述
所以通常我们使用的都是第二种链式结构,我们称其为块链
串的块链结构定义如下:

#define MAXSIZE 100

typedef struct Chunk{
   
   
    char ch[MAXSIZE];
    struct Chunk *next;
}Chunk;

typedef struct{
   
   
    Chunk *head;    //串的头指针
    Chunk *tail;    //串的尾指针
    int length;        //串的长度
}String;

正常情况下,对串的操作仅限于查找,插入和删除操作很少,所以一般我们采用串的顺序存储结构实现。

串的基本操作

下面介绍串的一些基本操作。

串的模式匹配算法

如何确定子串在主串中的位置,称为串的模式匹配。
下面介绍两种串的模式匹配算法:

  1. BF算法
  2. KMP算法

BF算法

先介绍BF算法,BF是Brute-Force的简称,意思是简单的匹配算法,采用的是暴力比较的办法,算法思想是从主串中的第一个字符开始,依次与子串比较,直至成功匹配。
假设有这样的一个主串:aaaabc,要在该串中查找子串aaab的位置,该如何查找呢?
BF算法是这样实现的:
先定义两个游标i、j,i用于遍历主串,j用于遍历子串,从主串的第一个字符开始,与子串的第一个字符进行比较:
在这里插入图片描述

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20191221223131419.png)
又匹配成功了,让```i+1=3```,```j+1=3```,继续比较:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20191221223246994.png)
还是匹配成功,接着让```i+1=4```,```j+1=4``,继续比较:

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20191221223346901.png)

```a```与```b```不匹配,此时就不用继续比较了,让主串从第二个字符开始重新与子串进行匹配,可是现在i = 4,j = 4,该如何让游标退回去呢?
子串游标很简单,让j = 1,重新开始就可以了,如何让主串游标从下一个字符开始呢?
可以这样做,先让i - j,结果就等于0,然后结果加1,即:```i - j + 1```表示的是上一次开始比较的位置,那我们让结果再加1,即:```i - j + 2```表示的就是下一个开始比较的字符位置。

这样我们又重新开始比较了,主串的第二个字符和子串的第一个字符进行比较:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/2019122122381468.png)
匹配成功,i加1等于3,j加1等于2,继续比较:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20191221223853492.png)
匹配成功,i加1等于4,j加1等于3,继续比较:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20191221223914812.png)
匹配成功,i加1等于5,j加1等于4,继续比较:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20191221223937629.png)
匹配成功,i加1等于6,j加1等于5,此时匹配成功,就返回主串中子串的第一个字符的序列,用i的值减去子串的长度即可得到。

BF代码实现如下:
```c
int Index_BF(String s,String t){
    int i = 1,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;
    }
}

关于在定义时数组大小加1的问题,就是为了方便游标位置的思考,让游标从1开始,符合人的正常思维。

KMP算法

由于BF算法效率低下,克努特、莫里斯和普拉特三人共同提出了KMP算法,该算法较BF算法有了很大的改进,效率显著提高。
KMP算法思想:利用已经部分匹配的结果加快模式串的滑动速度,且主串的游标不必回溯。

假设有主串ababcabcacbab,子串abcac,要想查找子串位置,KMP算法步骤如下:
定义两个游标i和j,i表示主串游标,j表示子串游标,初始让i和j都等于1,第一次是主串和子串的首字符比较:
在这里插入图片描述
匹配成功,i和j均加1,继续比较:
在这里插入图片描述
匹配成功,i和j均加1,继续比较:
在这里插入图片描述
匹配失败,如果是BF算法,此时i和j都得回溯,i回溯到主串的第二个字符位置,j则从子串的第一个字符重新开始。但是KMP算法不这样,为了提高效率,它并不打算回溯,而是让游标i从主串中匹配失败的位置继续匹配,但是得让j回到子串的初始位置,然后开始比较:
在这里插入图片描述
匹配成功,i和j均加1,接着一直进行比较,直到匹配失败:
在这里插入图片描述
这个时候i不必回溯,它仍然从b开始,但是此时的j不再是从子串的初始位置开始比较了,因为串尾字符的前一个字符和串首的字符相同,且已经匹配成功,所以我们只需将j回溯到子串的第二个位置即可,然后进行比较:
在这里插入图片描述
匹配成功,算法就结束了。

通常,我们会定义next[j]数组,表明当子串中第j个字符与主串中相应字符匹配失败时,在子串中需要重新和主串中该字符进行比较的字符的位置。
我们需要在子串找到串尾的前缀和后缀,如果串尾的前缀和j前面的若干字符相等的话,就可以获得k的值,当然k的值可能不止一个,我们就找到其中的最大值即可。如果没有找到相等的情况,且j = 1时,就让k的位置为1,其它情况为0。总结如下:
$$ next[j] = \begin{cases} max\lbrace k_i 1

这样很难理解,我们举个例子,比如下面的一个子串:
在这里插入图片描述
下面开始计算:

  • 先看子串的第一个字符a,此时j = 1,所以next[j]=0
  • 再看子串的第二个字符b,字符b只有一个前缀a,属于其它情况,所以next[j]=1
  • 再看子串的第三个字符c,字符c的前缀b与前面开始的a不相等,属于其它情况,所以next[j]=1
  • 再看子串的第四个字符a,字符a的前缀c与开始位置的a不相等,属于其它情况,所以next[j]=1
  • 再看子串的第五个字符a,字符a的前缀a与开始位置的a相等,当然还要比较组合字符,字符a的前缀ca与开始位置的ab不相等,因为k - 1 = 1,所以next[j]=2
  • 再看子串的第六个字符b,字符b的前缀a与开始位置的a相等,然后比较组合字符,字符b前缀aa与开始位置的ab不相等,字符b的前缀caa与开始位置abc不相等,所以next[j]=2
  • 再看子串的第七个字符b,字符b的前缀b与开始位置的a不相等,继续比较组合字符,字符b的前缀ab与开始位置的ab相等,字符b的前缀aab与开始位置的abc不相等,字符b的前缀caab与开始位置的abca不相等,字符b的前缀bcaab与开始位置的abcaa不相等,所以next[j]=2
  • 后面的就不一一分析了,分析流程相信大家已经清楚了吧

在这里插入图片描述
代码实现如下:

void get_next(String str, int *next){
   
   
    //求模式串str的next数组 
    int i = 1,j = 0;
    next[0] = str.length;
    next[1] = 0;
    while(i < str.length + 1){
   
   
        if(j==0 || str.ch[i] == str.ch[j]){
   
   
            ++i;
            ++j;
            next[i] = j;
        }
        else
            j = next[j];
    }
}



int Index_KMP(String s, String 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;
}

KMP算法的关键就是求出子串的next数组。

源代码

文章中的所有代码:

#include <stdio.h>
#include <malloc.h>
#include <string.h>

#define MAXSIZE 100

typedef struct{
   
   
    char ch[MAXSIZE + 1];//字符数组,表示串
    int length;//串长
}String;

void get_next(String str, int *next){
   
   
    //求模式串str的next数组 
    int i = 1,j = 0;
    next[0] = str.length;
    next[1] = 0;
    while(i < str.length + 1){
   
   
        if(j==0 || str.ch[i] == str.ch[j]){
   
   
            ++i;
            ++j;
            next[i] = j;
        }
        else
            j = next[j];
    }
}



int Index_KMP(String s, String 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;
}

int Index_BF(String s,String t){
   
   
    int i = 1,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;
    }
}
相关文章
|
15天前
|
人工智能
歌词结构的巧妙安排:写歌词的方法与技巧解析,妙笔生词AI智能写歌词软件
歌词创作是一门艺术,关键在于巧妙的结构安排。开头需迅速吸引听众,主体部分要坚实且富有逻辑,结尾则应留下深刻印象。《妙笔生词智能写歌词软件》提供多种 AI 功能,帮助创作者找到灵感,优化歌词结构,写出打动人心的作品。
|
1月前
|
机器学习/深度学习 搜索推荐 大数据
深度解析:如何通过精妙的特征工程与创新模型结构大幅提升推荐系统中的召回率,带你一步步攻克大数据检索难题
【10月更文挑战第2天】在处理大规模数据集的推荐系统项目时,提高检索模型的召回率成为关键挑战。本文分享了通过改进特征工程(如加入用户活跃时段和物品相似度)和优化模型结构(引入注意力机制)来提升召回率的具体策略与实现代码。严格的A/B测试验证了新模型的有效性,为改善用户体验奠定了基础。这次实践加深了对特征工程与模型优化的理解,并为未来的技术探索提供了方向。
82 2
深度解析:如何通过精妙的特征工程与创新模型结构大幅提升推荐系统中的召回率,带你一步步攻克大数据检索难题
|
4月前
|
存储 算法 安全
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
68 0
|
7天前
|
机器学习/深度学习 自然语言处理 数据管理
GraphRAG核心组件解析:图结构与检索增强生成
【10月更文挑战第28天】在当今数据科学领域,自然语言处理(NLP)和图数据管理技术的发展日新月异。GraphRAG(Graph Retrieval-Augmented Generation)作为一种结合了图结构和检索增强生成的创新方法,已经在多个应用场景中展现出巨大的潜力。作为一名数据科学家,我对GraphRAG的核心组件进行了深入研究,并在此分享我的理解和实践经验。
24 0
|
13天前
光纤电缆(FOC)的结构深度解析
【10月更文挑战第21天】
31 0
|
1月前
|
存储 编译器 C++
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
|
2月前
|
数据采集 存储 JavaScript
如何使用Cheerio与jsdom解析复杂的HTML结构进行数据提取
在现代网页开发中,复杂的HTML结构给爬虫技术带来挑战。传统的解析库难以应对,而Cheerio和jsdom在Node.js环境下提供了强大工具。本文探讨如何在复杂HTML结构中精确提取数据,结合代理IP、cookie、user-agent设置及多线程技术,提升数据采集的效率和准确性。通过具体示例代码,展示如何使用Cheerio和jsdom解析HTML,并进行数据归类和统计。这种方法适用于处理大量分类数据的爬虫任务,帮助开发者轻松实现高效的数据提取。
如何使用Cheerio与jsdom解析复杂的HTML结构进行数据提取
【期末不挂科-单片机考前速过系列P6】(第六章:10题速过定时计数器的结构和工作方式例题)经典例题盘点(带图解析)
【期末不挂科-单片机考前速过系列P6】(第六章:10题速过定时计数器的结构和工作方式例题)经典例题盘点(带图解析)
【期末不挂科-单片机考前速过系列P7】(第七章:11题速过串行口基本概念/结构/工作方式/双机通信例题)经典例题盘点(带图解析)
【期末不挂科-单片机考前速过系列P7】(第七章:11题速过串行口基本概念/结构/工作方式/双机通信例题)经典例题盘点(带图解析)
|
3月前
|
存储 缓存 NoSQL
深入解析Memcached:内部机制、存储结构及在大数据中的应用
深入解析Memcached:内部机制、存储结构及在大数据中的应用

热门文章

最新文章

推荐镜像

更多