数据结构学习笔记(第四章:串)
第四章:串
- 4.1 串的定义和实现
- 串的定义
- 串的实现
- 1.定长顺序存储实现
- 2.堆分配存储表示
- 3.块链存储表示
- 4.2 串的模式匹配
- 简单的模式匹配算法
- 改进模式匹配算法(KMP算法)
- KMP算法的进一步优化
4.1 串的定义和实现
串的定义
串(String)是由零个或多个字符组成的有限序列。一般记为
S=‘a1a2...an’S=‘a_1a_2...a_n’S=‘a1a2...an’
串中任意多个连续的字符组成的子序列称为该串的子串。当两个串的长度相等且每个对应位置的字符相等时,称这两个串是相等的。
需要注意的是,由一个或多个空格组成的串称为空格串(空格串不是空串)
串的逻辑结构和线性表类似,区别仅在于串的数据类型限定为字符集。但在基本操作上,串和线性表有很大的区别。
串的实现
1.定长顺序存储实现
类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。在串的定长存储序列结构中,每个串变量都只分配了一个固定长度的存储空间,也就是一个定长的数组。
#define MAXSTRLEN 255
typedef unsigned char SString[MAXSTRLEN+1];//0号单元存放串的长度
或者
typedef struct{char ch[MAXSTRLEN]; //用来申请串空间的指针,按串长动态分配int length;//串长度
}SString;
串的实际长度只能小于给定的MAXSTRLEN,超过部分只能舍去,称为截断。
要克服这个弊端,只能采用动态分配的方法,不限定串长的最大长度。
2.堆分配存储表示
其实笔者在实现线性表的顺序存储结构时,应用的就是堆分配存储表示。
堆分配存储表示仍然以一组地址连续的春初单元存放串值的字符序列,但是它和定长顺序存储不同,它的存储空间是在程序执行过程中动态分配得到的。
#include <iostream> //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;
#include <string.h>
#include <cstring>
#include <string>typedef int Status;
#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1#define MAXSTRLEN 255
typedef struct{char *ch; //用来申请串空间的指针,按串长动态分配int length;//串长度
}HString;Status InitString(HString &T){T.ch=NULL;T.length=0;return TRUE;
}
Status StrAssign(HString &T,char* chars){//生成一个其值等于串常量chars的串Tif(T.ch) free(T.ch);//如果T里原来存有值,c则释放T原有空间int i;char* c;for(i=0,c=chars;*c!='\0';++i,++c);//获得chars的长度iif(!i){//如果给的串常量为空串T.ch=NULL;T.length=0;}else{T.ch=(char *)malloc((i+1)*sizeof(char));//需要多出一个空间存放结束控制符if(!T.ch) return FALSE;for(int j=0;j<i;j++){T.ch[j]=*chars;chars++;}T.ch[i]='\0';//字符串的结束控制符T.length=i;}return TRUE;
}
int StrLength(HString S){//返回S的元素个数,称为串的长度return S.length;
}
int StrCompare(HString S,HString T){//如果S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0for(int i=0;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;
}
Status Concat(HString &T,HString S1,HString S2){//用T返回由S1和S2联接而成的新串if(T.ch){//如果原来的T有数据,释放T原有的旧空间free(T.ch);}T.ch=(char *)malloc((S1.length+S2.length+1)*sizeof(char));//多一个空间存放结束控制符if(!T.ch)return error;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;T.ch[T.length]='\0';//字符串最后存放结束控制符return TRUE;
}
Status SubString(HString &Sub,HString S,int pos,int len){//用Sub返回串S的第pos个字符起长度为len的子串if(pos<1||pos>S.length||len<1||len>S.length-pos+1){//判断起始位置和取串的长度是否合法return error;}if(Sub.ch){free(Sub.ch);//释放旧空间}if(!len){//如果要取的是空串Sub.ch=NULL;Sub.length=0;}else{Sub.ch=(char *)malloc((len+1)*sizeof(char));for (int i=0; i<len; i++) {Sub.ch[i]=S.ch[pos-1+i];}Sub.ch[len]='\0';Sub.length=len;}return TRUE;
}int main() {char c[100];gets(c);HString T1;HString T2;HString T3;InitString(T1);InitString(T2);InitString(T3);StrAssign(T1, c);StrAssign(T2, c);Concat(T3,T1,T2);SubString(T1, T3, 2, 2);return 0;
}
该分配方式可以通过笔者以前写的顺序线性表的实现来类推。
3.块链存储表示
类似于线性表的链式存储结构,在串中,我们也可以采用链表的方式存储串值。由于串的特殊性(每个元素都是一个字符),在具体实现的时候,每个结点既可以只放一个字符,也可以存放多个字符。每个结点称为”块“,整个链表称为”块链结构“。
#define CHUNKSIZE 80 //由用户定义的块大小
typedef struct Chunk{char ch[CHUNKSIZE];struct Chunk *next;
}Chunk; //块结点
typedef struct{Chunk *head,*tail; //串的头尾指针int curlen; //串当前的长度
}LString;
4.2 串的模式匹配
子串的定位操作通常称为串的模式匹配,它求的是子串在主串中的位置。
简单的模式匹配算法
最简单,最直接就是暴力搜索,算法思想如下所示:
假如我们要找到子串 a b c a c 在主串 a b a b c a b c a c b a b 中第一次出现的位置
简单模式匹配算法,最坏情况下,时间复杂度会达到 O(nm) ,n 为主串的长度,m 为模式串的长度。因为最坏的情况就是每趟匹配都是比较到模式串的最后一位才发现不同,然后指针 i 需要回溯到最初的起点的下一位。
具体代码如下:(串采用的是定长顺序存储结构来做示范)
int Index(HString S,HString T){//返回子串T在主串S中的位置。若不存在,则函数值为0int i,j;if(T.length==0)//T非空return error;for(i=0,j=0;i<S.length&&j!=T.length;){if(S.ch[i]==T.ch[j]){j++;i++;}else{//回溯匹配i=i-j+1;j=0;}}if(j==T.length){//匹配成功return i-T.length+1;}//匹配失败return 0;
}
改进模式匹配算法(KMP算法)
如何改进模式匹配算法呢,首先,我们在暴力算法中可以注意到,我们每次在每趟的比较完毕后,总是回到最开始匹配的下一个位置,但是在该趟的比较中我们可能已经读过下面的字符了,我们回溯到最初的那个位置的下一位重新比较就有点浪费时间了,有什么办法可以将回溯的位置向后挪一挪呢?
我们可以从模式串的结构入手,因为在每趟匹配中,我们已经匹配的匹配串的子串中,前缀序列中的某个序列刚刚好是模式串的前缀,那么就可以将模式向后滑动到与这些相等字符对其的位置上。下面我们解释一下这个思路。
1、原理:
1、字符串的前缀、后缀和部分匹配值
前缀:除最后一个字符以外,字符串的所有头部子串;
后缀:除第一个字符外,字符串的所有尾部子串;
部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。
例如:“ababa”
‘a’的前缀和后缀都是空值,最长相等前后缀长度为0
'ab’的前缀为{a},后缀为{b},{a}∩{b}=∅\{a\}\cap\{b\}= \varnothing{a}∩{b}=∅,最长相等前后缀长度为0
‘aba’的前缀为{a,ab},后缀为{a,ba},{a,ab}∩{a,ba}={a}\{a,ab\}\cap\{a,ba\}= \{a\}{a,ab}∩{a,ba}={a},最长相等前后缀长度为1
'abab’的前缀为{a,ab,aba},后缀为{b,ab,bab},{a,ab,aba}∩{b,ab,bab}={ab}\{a,ab,aba\}\cap\{b,ab,bab\}= \{ab\}{a,ab,aba}∩{b,ab,bab}={ab},最长相等前后缀长度为2
'ababa’的前缀为{a,ab,aba,abab},后缀为{a,ba,aba,baba},{a,ab,aba,abab}∩{b,ba,aba,baba}={a,aba}\{a,ab,aba,abab\}\cap\{b,ba,aba,baba\}= \{a,aba\}{a,ab,aba,abab}∩{b,ba,aba,baba}={a,aba},最长相等前后缀长度为3
所以我们可以得到“ababa”的部分匹配值为00123
我们用模式串匹配主串,匹配失败的时候,子串向后移动的位置应该是为
移动位数=已匹配的字符数−失配元素的前一个部分匹配值移动位数=已匹配的字符数-失配元素的前一个部分匹配值移动位数=已匹配的字符数−失配元素的前一个部分匹配值
例如上面简单的模式匹配的例题:
我们要找到子串 a b c a c 在主串 a b a b c a b c a c b a b 中第一次出现的位置
我们利用上述的方法很容易获得子串“abcac”的部分匹配值为00010,我们由此可以得到部分匹配值表(PM表):
字符串 | a | b | c | a | c |
---|---|---|---|---|---|
PM | 0 | 0 | 0 | 1 | 0 |
在第一趟的匹配中:
发现a与c不一样,前面的两个字符’ab’是已经匹配过了的,查部分匹配值可以知道,最后一个匹配字符’b’对应的部分匹配值为0,因此我们的子串需要向后移动的位置为2-0=2,所以我们直接将子串向后移动两格
继续进行第二趟匹配:
同理,abca已经匹配成功的了,但是断在了最后一个字母a与c匹配失败,查部分匹配值可以知道,最后一个匹配字符’a’对应的部分匹配值为1,所以我们的子串需要向后移动的位置为4-1=3,所以我们将子串向后移动三格
继续进行第三趟匹配:
匹配成功!
由上面的例子可以知道,如果对应的部分匹配值为0,那么表示已匹配相等序列中没有与之相等的前缀和后缀,此时移动的位数就达到最大
如果已匹配的相等序列中存在了相同的前缀和后缀(首尾重合)我们要注意将,当前的后缀在下一趟匹配中需要承担前缀的角色,后缀的长度为部分匹配值。类似上面的第二趟匹配中’abca’,前缀’a’与后缀’a‘重合了,所以后缀的’a’是不能忽略的,他必须要变成一次前缀的‘a’来再进行一次匹配串,所以子串只向后移动了三格。
2、进一步改进:
使用部分匹配值的时候,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来有点不太方便,所以将部分匹配值得表向右移动一个位子,这样在哪里元素失配了就直接看它自己的部分匹配值即可,这个向右移动了一个位子的PM表称为next数组。
例如上面的PM表
字符串 | a | b | c | a | c |
---|---|---|---|---|---|
PM | 0 | 0 | 0 | 1 | 0 |
原来的公式为:
move=(j−1)−PM[j−1]move=(j-1)-PM[j-1]move=(j−1)−PM[j−1]
改成next数组
字符串 | a | b | c | a | c |
---|---|---|---|---|---|
next | -1 | 0 | 0 | 0 | 1 |
我们应该注意到
1)第一个元素向右移动后的空缺用了-1来填充,因为若是第一个元素就匹配失败,则只需要向右移动一位,不用计算子串移动的位数
2)我们忽略了最后一个元素在向右移动的溢出,因为在PM表中,不可能查表查到最后一个元素的,因为在匹配过程中若匹配的子串已经通过了最后一个元素,他就已经完成了我们的目标,不会再去寻找回溯值了。
这样我们的公式应该就改成了
move=(j−1)−PM[j]move=(j-1)-PM[j]move=(j−1)−PM[j]
相当于我们子串的比较指针 jjj 就回退到
j=j−move=j−{(j−1)−PM[j]}=next[j]+1j=j-move=j-\{(j-1)-PM[j]\}=next[j]+1j=j−move=j−{(j−1)−PM[j]}=next[j]+1
所以我们还可以为了公式简洁,将next数组整体+1
字符串 | a | b | c | a | c |
---|---|---|---|---|---|
next | 0 | 1 | 1 | 1 | 2 |
next[j]next[j]next[j]具体含义是:在子串第jjj个字符与主串发生失配的时候,跳到子串的next[j]位置重新与主串当前位置进行比较。
其实说白了就是把已经匹配的后缀当下次匹配的前缀使用,next[j]就是下次匹配时已经匹配过的前缀的长度。所以下一轮的匹配可以忽略掉这个前缀部分。
next [ 1 ]=0:代表当模式串的第一个字符与主串的当前字符比较不相等时,主串当前指针应后移一位,然后重新与模式串的第一个字符比较。
next [ j ]=1:代表失配时,主串指针 i 保持不变,模式串需要从新回到第一个字符和主串的第 i 个字符比较
3、next数组如何代码实现:
思想:首先我们也可以把求next数组的问题视为模式匹配问题:
整个模式串即是主串又是模式串
具体做法:用下面做例子
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
字符串 | a | b | a | a | b | c | a | b | a |
next | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 | 3 |
next[1]:首位肯定是0无疑
next[2]:看前一位next[1]=0,所以直接令next[2]=1;
next[3]:看前一位next[2]=1,因为next[2]=1,但p2p_2p2!=p1p_1p1,并且进入next递归,next[ next[2] ]=next[1]=0,递归结束,next[3]=1;
next[4]:看前一位,因为next[3]=1,且p3p_3p3=p1p_1p1,next[4]=next[3]+1=2
next[5]:看前一位,因为next[4]=2,但p4p_4p4!=p2p_2p2,进入next递归,next[next[4]]=next[2]=1,且p4p_4p4=p1p_1p1,next[5]=next[2]+1=2
next[6]:看前一位,因为next[5]=2,且p5p_5p5=p2p_2p2,next[6]=next[2]+1=3
next[7]:看前一位,因为next[6]=3,但p6p_6p6!=p3p_3p3,进入next递归,next[next[6]]=next[3]=1,但p6p_6p6!=p1p_1p1,进入下一次递归,next[next[3]]=next[1]=0,递归结束,next[3]=1;
next[8]:看前一位,因为next[7]=1,且p7p_7p7=p1p_1p1,next[8]=next[7]+1=2;
next[9]:看前一位,因为next[8]=2,且p8p_8p8=p2p_2p2,next[9]=next[8]+1=3;
代码:
void get_next(HString s,int next[]){int i=1;int j=0;next[1]=0;while (i<s.length) {if(j==0||s.ch[i]==s.ch[j]){++i;++j;next[i]=j;}else{j=next[j];}}
}
相比next数组的求解法来说,KMP的匹配算法就非常简单了,仅仅只是将暴力法中的回溯的代码修改成next数组所记录的回溯点即可
int Index_KMP(HString S,HString T,int next[]){//返回子串T在主串S中的位置。若不存在,则函数值为0int i,j;if(T.length==0)//T非空return error;for(i=0,j=0;i<S.length&&j!=T.length;){if(j!=0||S.ch[i]==T.ch[j]){j++;i++;}else{//回溯j=next[j];}}if(j==T.length){//匹配成功return i-T.length+1;}//匹配失败return 0;
}
暴力模式匹配的时间复杂度为O(mn),KMP算法的时间复杂度为O(m+n)。
KMP算法仅在主串和子串中存在大量"部分匹配"时才比普通算法快很多,主要优点是主串不会回溯。所以在一般的情况下,普通模式匹配实际的执行时间也能近视为O(m+n),因此也采用至今。
KMP算法的进一步优化
例如
模式串为m=“aaaab”,主串为s="aaabaaaaab"比较:
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
字符串 | a | a | a | a | b |
next | 0 | 1 | 2 | 3 | 4 |
第一轮匹配中,当m4m_4m4!=s4s_4s4时匹配失败
如果仍使用上面的next数组,我们还需要继续比较m3m_3m3!=s4s_4s4,m2m_2m2!=s4s_4s4,m1m_1m1!=s4s_4s4
但我们知道m4m_4m4=m3m_3m3=m2m_2m2=m1m_1m1=‘a’,已经知道了m4m_4m4!=s4s_4s4的情况下,我们再进行那三次比较是非常傻的行为。
那么我们如何改进我们的next数组呢?
思路:不应该出现mjm_jmj=mnext[j]m_{next[j]}mnext[j]。当出现mjm_jmj=mnext[j]m_{next[j]}mnext[j]时,如果当前匹配mjm_jmj!=sis_isi,那么下次比较mnext[j]m_{next[j]}mnext[j]!=sis_isi是没有比较的必要的,这必然会继续失配。
所以我们出现mjm_jmj=mnext[j]m_{next[j]}mnext[j]时,则需要进行递归,令next[j]=next[next[j]]next[j]=next[next[j]]next[j]=next[next[j]],在继续比较,直到mjm_jmj!=mnext[j]m_{next[j]}mnext[j]为止
改进后的next数组称为nextval数组
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
字符串 | a | a | a | a | b |
nextval | 0 | 0 | 0 | 0 | 4 |
实现代码如下:
void get_nextval(HString s,int nextval[]){int i=1;int j=0;nextval[1]=0;while (i<s.length) {if(j==0||s.ch[i]==s.ch[j]){++i;++j;if(s.ch[i]!=s.ch[j]){nextval[i]=j;}else{nextval[i]=nextval[j];}}else{j=nextval[j];}}
}
数据结构学习笔记(第四章:串)相关推荐
- 《Go语言圣经》学习笔记 第四章 复合数据类型
<Go语言圣经>学习笔记 第四章 复合数据类型 目录 数组 Slice Map 结构体 JSON 文本和HTML模板 注:学习<Go语言圣经>笔记,PDF点击下载,建议看书. ...
- 计算机网络(第7版)谢希仁著 学习笔记 第四章网络层
计算机网络(第7版)谢希仁著 学习笔记 第四章网络层 第四章 网络层 4.3划分子网和构造超网 p134 4.3.1划分子网 4.3.2使用子网时分组的转发 4.3.3无分类编址CIDR(构建超网) ...
- Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25
Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25 目录 Rule20 接口优于抽象类 Rule21 为后代设计接口 Rule22 接口只用于定义类型 ...
- 机器人导论(第四版)学习笔记——第四章
机器人导论(第四版)学习笔记--第四章 4.1 引言 4.2 解的存在性 4.3 当n<6时操作臂子空间的描述 4.4 代数解法和几何解法 4.5 简化成多项式的代数解法 4.6 三轴相交的Pi ...
- 数据结构学习笔记(四):重识数组(Array)
目录 1 数组通过索引访问元素的原理 1.1 内存空间的连续性 1.2 数据类型的同一性 2 数组与链表增删查操作特性的对比 2.1 数组与链表的共性与差异 2.2 数组与链表增删查特性差异的原理 3 ...
- 线性代数学习笔记——第四章学习指南——n维向量空间
一.学习内容及要求 1. 内容: §4.1. n维向量空间的概念 线性代数学习笔记--第四十讲--n维向量空间的概念 线性代数学习笔记--第四十一讲--n维向量空间的子空间 §4.2. 向量组的线性相 ...
- 深入理解Linux网路技术内幕学习笔记第四章:通知链
第四章:通知链 内核很多子系统之间具有很强的依赖性,其中一个子系统侦测到的或者产生的事件,其他子系统可能都感兴趣,为了实现这种需求,Linux使用了通知链.通知链只在内核子系统之间使用. 通知链就是一 ...
- Lan Goodfellow 《DEEP LEARNING》学习笔记 --第四章
https://app.yinxiang.com/shard/s64/nl/22173113/a89ab8f8-3937-419c-8b81-cc913abaa35a/ 为了方便起见,我用的可手写的a ...
- python实验题第四章_「Python」2020.03.16学习笔记 | 第四章列表、元组、字典-习题(11-13)...
学习测试开发的Day74,真棒! 学习时间为1H 第四章列表.元组.字典-习题(11-13) 11.求两个集合的交集和并集 代码 list1=[1,2,3,4] list2=[2,3,5,5] def ...
- java email bean_JavaWeb学习笔记-第四章JavaBean技术
第四章 JavaBean技术 4.2.2 使用JavaBean的意义 如果使HTML代码与Java代码相分离,将Java代码单独封装成为一个处理某种业务逻辑的类,然后在JSP页面中调用此类,就可以降低 ...
最新文章
- python下的橡皮线_python线性代数常用操作
- python matlab大数据,Python第八课:Python数据分析基础
- Linux文件分割命令split笔记
- JVM—引用计数和可达性分析算法(存活性判断)
- ArcGIS AddIN异常:无法注册程序集 未能加载文件或程序集ESRI.ArcGIS.Desktop.Addins
- 产能过剩时代,为什么说“送比卖更赚钱”
- pythonrequests查询_Python Requests实例,查询成绩
- 什么技术才值得你长期投入? | 凌云时刻
- Oauth三种认证方式
- R中输出常见位图和矢量图格式总结
- Linux基本操作---实践+理解--CentOS 7
- python ppt教程_python pptx复制指定页的ppt教程
- 用arcgis批量裁剪栅格(tiff)数据的矩形区域
- 北京企业平均薪酬达16.68万元;小米 11 内核已开源;阿里达摩院 2021 十大科技趋势 | EA周报...
- Python3批量爬取指定微博中的图片
- 云盼智能快递柜提供第三方便民服务平台,解决快递业终端服务困境
- JAVA自定义信件消息模板内容
- 柏西机器人_《勿忘我》孔木猴 ^第15章^ 最新更新:2020-08-03 17:37:51 晋江文学城_手机版...
- 设计模式之中介者模式---Mediator Pattern
- 华为ensp防火墙web登陆配置