文章目录
串
串(string)是由零个或多个字符组成的有限序列,又名叫字符串。
一、简介
一般记为 s = " a 1 , a 2 , … … , a n " ( n > 0 ) s= "a_1,a_2,……,a_n "(n>0) s="a1,a2,……,an"(n>0), 其中,s 是串的名称,用双引号 (有些书中也用单引号) 括起来的字符序列是串的值.
- a i ( 1 < i < n ) a_i (1<i<n) ai(1<i<n) 可以是字母、数字或其他字符, i i i 就是该字符在串中的位置。
- 串中的字符数目 n 称为申的长度,定义中谈到 限”是指长度 n 是一个有限的数值。
- 零个字符的串称为空串 (null string), 它的长度为零,可以直接用两双引号 表示,也可以用希腊字母 “ Φ \Phi Φ” 来表示。所谓的序列,说明串的相邻字符之间具有前驱和后继的关系。
- 空格串,是只包含空格的串。注意它与空串的区别,空格串是有内容有长度的,而且可以不止一个空格。
- 子串与主申,串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串。
- 子串在主串中的位置就是子串的第一个字符在主串中的序号。
二、比较
串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号
1、编码类型
- 标准的 ASCII 编码:计算机中的常用字符是使用标准的 ASCII 编码,更准确一点,由 7 位二进制数表示一个字符,总共可以表示 128 个字符。
- 扩展 ASCII 码:后来发现一些特殊符号的出现,128 个不够用,于是扩展 ASCII 码由 8 位二进制数表示一个字符,总共可以表示 256 个字符,这已经足够满足以英语为主的语言和特殊符号进行输入、存储、输出等操作的字符需要了。
- Unicode 编码:全世界估计要有成百上千种语言与文字,显然这 256 个字符是不够的,因此后来就有了 Unicode 编码,比较常用的是由 16 位的二进制数表示一个字符,这样总共就可以表示 216 个字符,约是 65 万多个字符,足够表示世界上所有语言的所有字符了。为了和 ASCH 码兼容,Unicode 的前 256 个字符与 ASCII 码完全相同。
2、字符串相等、小于
- 相等:给定两个串: s = “ a 1 , a 2 , … … , a n " , t = " b 1 , b 2 , … … , b m ” s= “a_1,a_2,……,a_n", t="b_1,b_2,……,b_m” s=“a1,a2,……,an",t="b1,b2,……,bm”, 当且仅当 n = m n=m n=m, 且 a 1 = b 1 , a 2 = b 2 , … … , a n = b m a_1=b_1, a_2=b_2,……,a_n=b_m a1=b1,a2=b2,……,an=bm时, 我们认为 s = t s=t s=t
- 小于:给定两个串:
s
=
“
a
1
,
a
2
,
…
…
,
a
n
"
,
t
=
"
b
1
,
b
2
,
…
…
,
b
m
”
s= “a_1,a_2,……,a_n", t="b_1,b_2,……,b_m”
s=“a1,a2,……,an",t="b1,b2,……,bm”, 当满足以下条件之一时,
s
<
t
s<t
s<t
- n < m n<m n<m, 且 a i = b i ( i = 1 , 2 , … … , n ) a_i = b_i (i=1, 2,…… ,n) ai=bi(i=1,2,……,n)
- 存在某个 k ≤ m i n ( m , n ) k≤min (m, n) k≤min(m,n), 使得 a i = b i ( i = 1 , 2 , … … , k − 1 ) , a k < b k a_i = b_i (i=1, 2,…… ,k-1) , a_k <b_k ai=bi(i=1,2,……,k−1),ak<bk
3、串的抽象数据类型
StrAssign (T,*chars)
:生成一个其值等于字符串常量 chars 的串 TStrCopy(T,S)
:串 S 存在,由串 S 复制得串 TClearString( S )
:串 S 存在,将串清空StringEmpty ( S )
:若串 S 为空,返回 true, 否则返回 falseStrLength ( S )
:返回串 S 的元素个数,即串的长度StrCompare(S,T)
:若 S>T, 返回值>0; 若 S=T, 返回 0; 若 S<T, 返回值<0Concat(T,S1,S2)
:用 T 返回由 S1 和 S2 联接而成的新串SubString ( Sub,S,pos,len )
:串 S 存在, l ≤ p o s ≤ S t r L e n g t h ( S ) l\leq pos \leq StrLength(S) l≤pos≤StrLength(S),且 0 ≤ l e n ≤ S t r L e n g t h ( S ) − p o s + 1 0\leq len \leq StrLength( S ) -pos+1 0≤len≤StrLength(S)−pos+1, 用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串Index ( S,T,pos )
: 串 S 和 T 存在,T 是非空串, 1 ≤ p o s ≤ S t r L e n g t h ( S ) 1≤pos≤StrLength(S) 1≤pos≤StrLength(S),若主串 S 中存在和串 T 值相同的子串, 则返回它在主串 S 中第 pos 个字符之后第一次出现的位置. 否则返回 0。Replace(S,T,V)
: 串 S、T 和 V 存在,T 是非空串。用 V 替换主串 S 中出现的所有与 T 相等的不重叠的子串。Strinsert(SzposrT)
:串 S 和 T 存在, 1 ≤ p o s ≤ S t r L e n g t h ( S ) + 1 1 \leq pos \leq StrLength( S ) +1 1≤pos≤StrLength(S)+1,在串 S 的第 pos 个字符之前插入串 T。StrDelete(S,pos,len)
:串 S 存在, 1 ≤ p o s ≤ S t r L e n g t h ( S ) − l e n + l 1≤pos≤StrLength(S)-len+l 1≤pos≤StrLength(S)−len+l,从串 S 中删除第 pos 个字符起长度为 len 的子率。
三、串的存储结构
1、串的顺序存储结构
用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。
- 既然是定长数组,就存在一个预定义的最大串长度
- 一般可以将实际的串长度值保存在数组的 0 下标位置,
- 有的书中也会定义存储在数组的最后一个下标位置。
- 但也有些编程语言不想这么干,觉得存个数字占个空间麻烦。它规定在串值后面加一个不计入串长度的结束标记字符,比如 “\0” 来表示串值的终结
2、串的链式存储结构
对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的每个元素数据是一个字符,如果也简单的应用链表存储串值,一个结点对应一个字符,就会存在很大的空间浪费。
因此,一个结点可以存放一个字符,也可以考虑存放多个字符,最后一个结点若是未被占满时,可以用 或其他非串值字符补全.
这里一个结点存多少个字符才合适就变得很重要,这会直接影响着串处理的效率,需要根据实际情况做出选择。
但串的链式存储结构除了在连接串与串操作时有一定方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。
四、朴素的模式匹配算法
子串的定位操作通常称做串的模式匹配.
- 匹配流程:
对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做 T 的长度的小循环,直到匹配成功或全部遍历完成为止。
- 最坏情况的时间复杂度为 O((n-m+1)*m)
许许多多个 0 和 1 的串组成。所以在计算机的运算当中,模式匹配操作可说是随处可见,而刚才的这个算法,就显得太低效了。
五、KMP 模式匹配算法
D.E.Knuth, J.H.Morris 和 V.R.Pratt(其中 Knuth 和 Pratt 共同研究,Morris 独立研究)发表一个模式匹配算法,可以大大避免重复遍历的情况,我们把它称之为克努特一莫里斯一普拉特算法,简称 KMP 算法。
- 它的主要思想是避免在文本中的每一步都重新比较已经比较过的字符,从而减少比较的次数,提高匹配效率
1、KMP 模式匹配算法原理
-
部分匹配表:KMP算法的关键在于构建一个部分匹配表,也叫失配函数。这个表记录了模式字符串中每个位置的前缀和后缀的最长公共长度,帮助算法在匹配失败时跳过一些字符。在文中是数组 next。
-
从模式字符串的第一个字符开始,比较前缀和后缀,记录最长公共长度。
-
依次计算每个位置的最长公共长度,构建部分匹配表。
-
匹配过程:
- 从文本字符串的第一个字符开始,与模式字符串的对应位置比较。
- 如果匹配成功,继续比较下一个字符。
- 如果匹配失败,根据部分匹配表的信息,将模式字符串右移一定距离,以减少不必要的比较。重复上述过程,直到找到匹配或者遍历完整个文本。
-
算法优势:
KMP算法通过构建部分匹配表,避免了在文本中的每一步都重新比较已经比较过的字符,从而提高了匹配效率。它的时间复杂度为O(N+M),其中N是文本的长度,M是模式的长度,相较于暴力匹配的O(N*M),KMP算法在大规模文本匹配中表现更出色。
2、部分匹配表的求解(数组 next)
-
例1
-
例2
-
例3
-
例4
3、代码实现
计算出当前要匹配的串 T 的 next 数组:
/* 通过计算返回子串 T 的 next 数组。*/
void get_next ( String T, int *next)
{
int i,j;
i=1;
j=0;
next[1]=0;
while (i<T[0]) /* 此处 T[0]表示串 T 的长度 */
{
if(j==0||T[i]== T[j]) /* T[i]表示后缀的单个字符,*/
/* T[j]发示前缀的单个字符 */
{
++i;
++j;
next[i] = j;
}
else
j = next[j]; /* 若字符不相同,则 j 值回溯 */
}
}
实现匹配:
/* 返回子串 T 在主率 S 中第 pos 个字符之后的位JL。若不存在,则函数返回值为 0。*/
/* T 非空,l≤pos≤StrLength ( S ) */
int Index_KMP ( String S, String T, int pos)
{
int i = pos; /*i 用于主串 S 当前位置下标值,若 pos 不为 1, */
/*则从 pos 位置开始匹配 */
int j = 1; /*j 用于子串 T 中当前位置下标值 */
int next[255]; /* 定义一next数组 */
get_next(T, next); /* 对串T 作分析,得到 next 数组 */
while (i <= S[0] && j <= T[0])/*若 i 小于 S 的长度且 j 小于 T 的长度时,*/ /*循环继续 */
{
if (j==0 ||S[i] == T[j]) /*两字母相等则继续,与朴素算法增加了*//*j=0 判断 */
{
++i;
++j;
}
else /* 指针后退重新开始匹配 */
{
j = next[j]; /*j 退回合适的位置,i 值不变 */
}
}
if (j > T[0] )
return i-T[0];
else
return 0;
}
对于 get_next 函数来说,若 T 的长度为 m, 因只涉及到简单的单循环,其时间复杂度为 O(m), 而由于i 值的不回溯,使得 index_KMP 算法效率得到了提高,while 循环的时间复杂度为 O(n)。因此,整个算法的时间复杂度为 O(m+n)。相较于朴素模式匹配算法的 O((n-m+1)*m)来说,是要好一些。
KMP 算法仅当模式与主串之间存在许多“部分匹配”的情况下才体现出它的优势,否则两者差异并不明显
4、KMP 模式匹配算法改进
比如,如果我们的主串 S=“aaaabcde”, 子串T=“aaaaax”
当中的② 步骤,其实是多余的判断。由于 T 串的第二、三、四、五位置的字符都与首位的 相等,那么可以用首位 next 去取代与它相等的字符后续 next[j]的值,因此我们对求 next 函数进行了改良。
假设取代的数组为 nextval,修改代码:
void get_nextval ( String T, int *nextval)
{
int i,j;
i=1;
j=0;
nextval[1]=0;
while (i<T[0]) /* 此处 T[0]表示串 T 的长度 */
{
if(j==0||T[i]== T[j]) /* T[i]表示后缀的单个字符,*//* T[j]发示前缀的单个字符 */
{
++i;
++j;
if (T[i] != T[j]) /*若当首字符与前级字符不同 */
nextval[i] = j; /* 则当前的 j 为nextval 在 i 位置的值 */
else
nextval[i] = nextval[j];/* 如果与前缀字符相同,则将前缀字符的 nextval 值赋值给 nextval 在 i 位置的值 */
}
else
j = nextval[j]; /* 若字符不相同,则 j 值回溯 */
}
}
5、nextval 数组值推导
-
例子1
-
例子2