4.1 串的定义和实现
4.1.1 串的定义
-
串: 即字符串,零个或多个字符组成的有限序列,如
S = 'iPhone 11 Pro Max?'; -
串名:S是串名,单引号括起来的字符序列是串的值(有些地方用双引号)
-
串的长度:串中字符的个数 , 时的串称为空串
-
子串:串中任意多个连续的字符组成的子序列称为该串的子串,空串也是该串的子串
主串:包含子串的串;
-
字符在主串中的位置:某个字符在串中的序号(从1开始);
子串在主串中的位置:子串的第一个字符在主串中的位置;
-
空串 V.S 空格串:
-
M = ''是空串; -
N = ' '是空格串;
-
-
串 V.S 线性表:
-
串是特殊的线性表,数据元素之间呈线性关系(逻辑结构相似);
-
串的数据对象限定为字符集:中文字符、英文字符、数字字符、标点字符…
-
串的基本操作,如增删改除通常以子串为操作对象
-
4.1.2 串的基本操作
假设有串 T = '',S = 'iPhone 11 Pro Max?',W = 'Pro'
-
StrAssign(&T, chars):赋值操作,把串T赋值为chars; -
StrCopy(&T, S):复制操作,把串S复制得到串T; -
StrEmpty(S):判空操作,若S为空串,则返回TRUE,否则返回False; -
StrLength(S):求串长,返回串S的元素个数;即:返回length值 -
ClearString(&S):清空操作,将S清为空串;即:将length = 0,逻辑上清空,但是内存中还有 -
DestroyString(&S):销毁串,将串S销毁——回收存储空间; -
Concat(&T, S1, S2):串联联接,用T返回由S1和S2联接而成的新串———可能会导致存储空间的扩展。如:执行Concat(&T, S, W)后,T = ‘iPhone 11 Pro Max?Pro’ -
SubString(&Sub, S, pos, len):求子串,用Sub返回串S的第pos个字符起长度为len的子串;如:执行SubString(&T, S, 4, 6)后,T = ‘one 11’
-
Index(S, T):定位操作,若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则返回0。如:执行Index(S, T)后,返回11 -
StrCompare(S, T):串的比较操作,参照英文词典排序方式,从第一个字符开始往后依次对比,先出现更大字符的串就更大;长串前缀与短串相同时,长串更大。 若S > T,返回值>0;S = T,返回值=0 (需要两个串完全相同) ;S < T,返回值<0;
拓展:字符集编码
-
字符集:英文字符——ASCII字符集,中英文——Unicode字符集。基于同一个字符集,可以有多种编码方案,如:UTF-8,UTF-16,不同字符所占空间不同
-
编码方案:ASCII编码
-
乱码问题->解码方式出现错误
4.1.3 串的存储结构
串的顺序存储
#define MAXLEN 255 //预定义最大串长为255
typedef struct{
char ch[MAXLEN]; //静态数组实现(定长顺序存储)
//每个分量存储一个字符
//每个char字符占1B
int length; //串的实际长度
}SString;串长的两种表示法:
-
方案一:用一个额外的变量
length来存放串的长度(保留ch[0]); -
方案二:用
ch[0]充当length;-
优点:字符的位序和数组下标相同
-
缺点:字符串长度不能超过255
-
-
方案三:没有
length变量,以字符'\0'表示结尾(对应ASCII码的0000 0000);- 缺点:需要从头到尾遍历
-
方案四:(最终使用方案)
ch[0]废弃不用,声明int型变量length来存放串的长度(方案一与方案二的结合)
\begin{document}
\begin{tikzpicture}[scale=1,line width=1.5pt]
\tikzstyle{every node}=[font=\huge, draw=black, very thick, rectangle, minimum width=1cm, minimum height=1cm]
\foreach \char [count=\i] in {H,e,l,l,o, ,w,o,r,d,!, , , , , } {
\node[fill=gray!20] at (\i-1,4) {\char};
}
\node[fill=green!20] at(16,4){11};
\foreach \char [count=\i] in {H,e,l,l,o, ,w,o,r,d,!, , , , } {
\node[fill=gray!20] at (\i,3) {\char};
}
\node at(0,3) {11};
\foreach \char [count=\i] in {H,e,l,l,o, ,w,o,r,d,!, , , , , } {
\node[fill=gray!20] at (\i-1,2) {\char};
}
\node at(11,2) {\textbackslash 0};
\foreach \char [count=\i] in {H,e,l,l,o, ,w,o,r,d,!, , , , } {
\node[fill=gray!20] at (\i,1) {\char};
}
\node at(0,1) {};
\node[fill=green!20] at(16,1){11};
\end{tikzpicture}
\end{document}堆分配存储表示(动态数组)
//动态数组实现
typedef struct{
char *ch; //按串长分配存储区,ch指向串的基地址
int length; //串的长度
}HString;
HString S;
S.ch = (char *) malloc(MAXLINE * sizeof(char)); //基地址指针指向连续空间的起始位置
//malloc()需要手动free()
S.length;基本操作实现
基于方案四
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;求子串
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;
}比较两个串的大小
int StrCompare(SString S, SString T){
for (int i; 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;
}定位操作:取子串,判断是否相等
//个人理解:从前往后不断取子串,并判断取到的子串与所给子串是否相等
int Index(SString S, SString T){
int i=1, n = StrLength(S), m = StrLength(T);
SString sub; //用于暂存子串
while(i <= n-m+1){
SubString(Sub,S,i,m);
if(StrCompare(Sub,T)!=0)
++i;
else
return i; // 返回子串在主串中的位置
}
return 0; //S中不存在与T相等的子串
}注:结合顺序表思考优缺点
串的链式存储
typedef struct StringNode{
char ch; //每个结点存1个字符
struct StringNode *next;
}StringNode, * String;-
问题:存储密度低,每个字符1B,每个指针4B;
-
解决方案:每一个链表的结点存储多个字符——每个结点称为块——块链结构
末尾可用一些特殊字符(如‘\0’来填充)
typedef struct StringNode{
char ch[4]; //每个结点存多个字符
struct StringNode *next;
}StringNode, * String;注:结合链表思考优缺点
-
存储分配角度:链式存储的字符串无需占用连续空间,存储空间分配更灵活;
-
操作角度:若要在字符串中插入或删除某些字符,则顺序存储方式需要移动大量字符,而链式存储不用;
-
若要按位序查找字符,则顺序存储支持随机访问,而链式存储只支持顺序访问;
4.2 串的模式匹配
-
什么是字符串的模式匹配?
-
子串:主串的一部分,一定存在
-
模式串:不一定能在主串中找到
-
字符串模式匹配:在主串中找到与模式串相同的子串,并返回其所在的位置
-
4.2.1 朴素模式匹配算法
暴力解法,找到所有可能的子串,把这些这串模式串一一对比,即 Index(S, T)
//个人理解:将主串中所有与模式串长度相同的子串依次与模式串对比,直到找到一个完全匹配的子串或者所有的子串都不匹配为止
int Index(SString S, SString T){
int i=1; //主串上的指针,扫描主串S
int j=1; //模式串上的指针,扫描模式串T
while(i<=S.length && j<=T.length){
if(S.ch[i] == T.ch[j]){
++i;
++j; //继续比较后继字符
} else{
i = i-j+2; //主串指针 i 指向下一个子串的第一个位置【注意】
//主串和模式串上的指针在if(true)内的循环一共后移了(j-1)个位置(从j=1开始算)
//主串指针i需要回到相对于这轮刚开始指针i位置的下一个,故+1
j = 1; //模式串指针 j 回到模式串的起始位置
}
}
if(j>T.length) //子串匹配成功
return i-T.length; //返回子串第一个位置
else
return 0;
}时间复杂度分析:
-
主串长度为 ,模式串长度为 ,最多比较 个子串
-
最坏时间复杂度
-
每个子串都要对比 个字符(对比到最后一个字符才匹配不上),共要对比 个子串,复杂度 =
-
注:大多数时候,
-
-
最好时间复杂度
- 每个子串的第一个字符就匹配失败,共要对比 个子串,复杂度 =
4.2.2 改进的模式匹配算法——KMP算法
-
在子串与模式串不匹配的字符之前,前面的字符一定是和模式串一致的,这些字符是已知的
-
根据模式串
T,求出next数组(只与模式串有关,与主串无关),利用next数组进行匹配,当匹配失败时,主串的指针i不再回溯! -
即在子串的第
j个字符与主串发生失配时,跳到子串的next[j]位置重新与主串当前位置进行比较
当第一个元素匹配失败时,匹配下一个相邻子串,令
j=0, i++, j++
预处理求 next 数组
会手算即可
-
作用:记录
j,当模式串的第j个字符与主串的第i个字符匹配失败时,使用模式串的第next[j]个元素与主串的第i个字符继续匹配; -
对于任何模式串,当第1个字符不匹配时,只能匹配下一个子串,因此,
next[1] = 0——表示模式串应右移一位,主串当前指针后移一位,再和模式串的第1字符进行比较; -
对于任何模式串,当第2个字符不匹配时,应尝试匹配模式串的第一个字符,因此,
next[2] = 1; -
手算:在不匹配的位置前边,画一条分界线,分界线右边为
S[i]和T[j],将模式串一步步后退,直到分界线之前能对上,或者模式串完全跨过分界线,此时j指向哪儿,next数组就是多少
例:对于串 T = 'abaabc'
| 编号 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
next[j] | 0(通用) | 1(通用) | 1 | 2 | 2 | 3 |
关键点
-
假设模式串的第 个字符与主串中的第 个字符发生失配,则前 个字符与主串相同,所以提前把前模式串的 个字符进行对比
-
对比过后,移动到图示位置继续匹配主串当中的第 个元素
-
next[j]表示模式串的第 个元素失配后,使用模式串的第next[j]个元素继续与当中的第 个元素匹配
利用 next 数组进行模式匹配
//个人理解:有了next数组,主串S上的指针就不需要回溯,但需要主串上的失配字符和模式串T上next数组对应的元素再次匹配
int Index_KMP(SString S, SString T, int next[]){
int i=1; //主串上的指针,扫描主串S
int j=1; //模式串上的指针,扫描模式串T
while(i<S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
//当第一个元素不匹配时,直接右移,下次还将匹配第一个元素。
//next[j]=0, 利用j++来让模式串右移1位
++j;
++i; //继续比较后继字符
} else //主串指针i不需要回溯
j=next[j] //模式串根据next数组向右移动
}
if(j>T.length)
return i-T.length; //匹配成功
else
return 0;
}时间复杂度分析:
-
求
next数组时间复杂度 = -
模式匹配过程最坏时间复杂度 =
-
KMP算法的最坏时间复杂度 =
4.2.3 KMP算法的进一步优化
对next数组进一步优化
-
判断next数组所指的字符
ch[next[j]]与原本失配的字符ch[j]是否相等:-
如果不相等,就保持原有状态;
-
如果相等,则没必要再次地比较,可以直接令
j=next[next[j]]
-
-
手算求
nextval数组-
求
next数组 -
若
ch[next[j]]=ch[j],则令nextval[j] = nextval[next[j]]
-
//个人理解:如果next数组所指的元素和失配元素相同的话,那么nextval[j] = nextval[next[j]],可以少匹配几次
nextval[1]=0;
for (int j=2; j<=T.length; j++){
if(T.ch[ next[j] ]==T.ch[j])
nextval[j] = nextval[ next[j] ];
else
nextval[j] = next[j];
}例:对于串 T = 'aaaab'
序号 j | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 模式串 | a | a | a | a | b |
next[j] | 0 | 1 | 2 | 3 | 4 |
nextval[j] | 0 | 0 | 0 | 0 | 4 |
//个人理解:next数组中,主串S上的指针不需要回溯,但需要主串上的失配字符和模式串T上next数组对应的元素再次匹配
//如果next数组对应的元素和模式串上的失配字符相同的话,再次匹配注定会失败,所以直接把nextval数组的值改成这个相同元素的nextval数组的值,从而减少匹配次数,代码上除了数组不同外几乎没有区别
int Index_KMP(SString S, SString T, int nextval[]){
int i=1; //主串上的指针,扫描主串S
int j=1; //模式串上的指针,扫描模式串T
while(i<S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){ //j==0是为了让第一个字符失配时,模式串右移1位
++j;
++i; //继续比较后继字符
}
else
//主串指针i不需要回溯
j=nextval[j] //模式串根据nextval数组向右移动
}
if(j>T.length)
return i-T.length; //匹配成功
else
return 0;
}