数据结构之串(字符串)

串的相关术语

串也是一种特殊的线性表。栈和队列是一种操作受限的线性表,而这里的串是一种内容受限的线性表,规定串里面的内容只能是字符。

  • :零个或多个任意字符组成的有限序列,例如,
    s ⎵ ( 串 名 ) = " a 1 a 2 ⋯ a n ⎵ ( 串 值 )   ” ( n ⎵ ( 串 长 ) ≥ 0 ) \underbrace {\rm{s}}_{(串名)} = " \underbrace {{a_1}{a_2} \cdots {a_n}}_{(串值)}\ ” (\underbrace n_{(串长)} \ge 0) () s="() a1a2an (() n0)
    空串:n=0,空串用 ϕ \phi ϕ表示。

  • 子串:串中任意个连续字符组成的子序列(含空串)称为该串的子串,例如,
    “ a b c d e ” 的 子 串 有 : “ ” 、 “ a ” 、 “ a b ” 、 “ a b c ” 、 “ a b c d ” 和 “ a b c d e ” 等 “abcde”的子串有:“”、“a”、“ab”、“abc”、“abcd”和“abcde”等 abcdeaababcabcdabcde
    真字串:不包含自身的所有子串。

  • 主串:包含子串的串相应的称为主串。

  • 字符位置:字符在序列中的序号为该字符在串中的位置。

  • 子串位置子串第一个字符在主串中的位置。

  • 空格串:由一个或多个空格组成的串,与空串不同。

  • 串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,这两个串才是相等的。所有空串都是相等的

例如,字符串a、b、c、d
a=‘BEI’
b=‘JING’
c=‘BEIJING’
d=‘BEI JING’

则,
它们的长度是:3 4 7 8
c的子串有:a b
d的子串有:a b
a在c中的位置:1(从1开始计数)
a在d中的位置:1
b在c中的位置:4
b在d中的位置:5



串的类型定义和存储结构

串的抽象类型定义为;
ADT String
{
     数据对象: D = { a i ∣ a i ∈ C h a r a c t e r S e t , i = 1 , 2 , . . . , n , n ≥ 0 } D=\{a_i|a_i \in CharacterSet,i=1,2,...,n,n\geq 0 \} D={aiaiCharacterSet,i=1,2,...,n,n0}
     数据关系: R = { &lt; a i − 1 , a i &gt; ∣ a i − 1 , a i ∈ D , i = 1 , 2 , . . . , n } R=\{&lt;a_{i-1},a_i&gt;|a_{i-1},a_i \in D,i=1,2,...,n\} R={<ai1,ai>ai1,aiD,i=1,2,...,n}
     基本操作: 有串赋值,串比较等操作。
}ADT String

串的存储结构

  • 串的顺序存储结构:顺序串
    其类型定义如下:
#define MAXLEN 255          //串的最大长度
typedef struct
{
	char ch[MAXLEN + 1];    //存储串的一维数组,下标从0带到255
	int length;             //串的当前长度
}SString;

后面为了方便起见,一般字符数组的0号位闲置不用,从1号位开始存储,即下标从1开始计数。

  • 串的链式存储结构:链串
    按照常规单链表的方式,其结构如下图,
    在这里插入图片描述
    这种链串结构有很明显的缺点:存储密度低
    说明一下,一个字符在内存中占1个字节,而指针域占4个字节,所以其存储密度只有0.2。

在链串中,可以将多个字符放在一个结点中,以克服其存储密度低的缺点——块链结构,如下图所示,
在这里插入图片描述
这里的块的大小可由用户定义。
块链结构的类型定义如下:

#define CHUNKSIZE 80      //块的大小可由用户定义
typedef struct Chunk
{
	char ch[CHUNKSIZE];
	strcat Chunk* next;
}Chunk;


typedef struct
{
	Chunk *head, *tail;   //串的头指针和尾指针
	int curlen;           //串当前的长度
}LString;                 //字符串的块链结构


串的匹配算法

算法目的:确定主串中所含子串第一次出现的位置(定位)。
算法种类:

  • BF算法(Brute-Force)
  • KMP算法(特点:速度快)

BF算法

BF算法((Brute-Force)又称作简单匹配算法,采用穷举法的思路。

下面举例说明: 现有一主串(正文串)S和一子串(模式串)T,
S : a a a a a b , n = 6 T : a a a b , m = 4 S: aaaaab ,n=6 \\ T:aaab,m=4 S:aaaaabn=6T:aaab,m=4
算法思路是将S中的每一个字符开始依次与T中的字符进行匹配。

其匹配过程如下:
1.
S : a ↓ i &ThickSpace; a &ThickSpace; a &ThickSpace; a ⎵ ( 匹 配 字 符 ) &ThickSpace; a &ThickSpace; b T : a ↑ j &ThickSpace; a &ThickSpace; a &ThickSpace; b \begin{array}{l} S:\underbrace {\mathop a\limits^{ \downarrow i} \;a\;a\;a}_{(匹配字符)}\;a\;b\\ \\ T:\mathop a\limits_{ \uparrow j} \;a\;a\;b \end{array} S:() aiaaaabT:jaaab

i从1开始,往后m个字符逐个与T中的字符相匹配。

2.若i=1匹配失败,则把i的初始位置往后移一位,i=2开始,如下图
S : a &ThickSpace; a ↓ i &ThickSpace; a &ThickSpace; a &ThickSpace; a ⎵ ( 匹 配 字 符 ) &ThickSpace; b T : a ↑ j &ThickSpace; a &ThickSpace; a &ThickSpace; b \begin{array}{l} S:a\;\underbrace {\mathop a\limits^{ \downarrow i} \;a\;a\;a}_{(匹配字符)}\;b\\ \\ T:\mathop a\limits_{ \uparrow j} \;a\;a\;b \end{array} S:a() aiaaabT:jaaab

当i=1匹配失败时,此时i指向的是第4个字符,j指向T中第4个字符的,此时怎么让i回到2,这里采用回溯的方法,i和j的值为:

i=i-j+2;    //回溯
j=1;      //从头开始

3.从i=2开始也匹配失败,继续回溯,

S : a &ThickSpace; a &ThickSpace; a ↓ i &ThickSpace; a &ThickSpace; a &ThickSpace; b ⎵ ( 匹 配 字 符 ) T : a ↑ j &ThickSpace; a &ThickSpace; a &ThickSpace; b \begin{array}{l} S:a\;a\;\underbrace {\mathop a\limits^{ \downarrow i} \;a\;a\;b}_{(匹配字符)}\\ \\ T:\mathop a\limits_{ \uparrow j} \;a\;a\;b \end{array} S:aa() aiaabT:jaaab

此时,可以看到当从i=3开始时,匹配成功,此时的 i=7.j=5,返回值为i-t.length=3

这个算法的思路如下:
Index(S,T,pos),这里的pos表示从主串的第pos个字符开始比较

  • 将主串的第pos个字符和模式串的第一个字符比较;
  • 若相等,继续逐个比较后续字符;
  • 若不等,从主串的下一个字符起(回溯),重新与模式串的第一个字符比较。
  • 直到主串的一个连续子串字符序列与模式串相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。
  • 否则,匹配失败,返回0。

算法描述:

int Index_BF(SString S, SString 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;                   //模式匹配不成功
}

BF算法的时间复杂度:
若n为主串的长度,m为子串的长度,

  • 最好的情况是,不用回溯,则一共比较了m次,时间复杂度为 O ( m ) O(m) O(m)
  • 最坏的情况是,主串前面的n-m个位置都部分匹配到子串的最后一位,即,这n-m位比较了m次,另外随后m位也各比较了一次,
    总 次 数 为 : ( n − m ) ∗ m + m = ( n − m + 1 ) ∗ m 总次数为:(n-m)*m+m=(n-m+1)*m (nm)m+m=(nm+1)m
    m &lt; &lt; n m&lt;&lt;n m<<n,则该算法复杂度为 O ( n ∗ m ) O(n*m) O(nm)

总的平均复杂度为 O ( n ∗ m ) O(n*m) O(nm)

KMP算法

KMP算法由D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的,简称KMP算法。

与BF算法相比,它的运算效率更高,主要体现在两个方面:

  • 主串S的指针i不必回溯
  • 它利用已经部分匹配的结果从而加快模式串的滑动速度,即子串的指针j不用每次都从头开始比较。

针对第一点,如下图所示,
在这里插入图片描述
当匹配失败时,指针i不用回溯到2,而是从匹配失败的位置开始,即上图中i=3的字符a开始,再往后比较。

针对第二点,如下图所示,
在这里插入图片描述
上图中,T里的第1个和第4个字符相同,都是a,此时的j就从第2个元素开始(因为此时第1个元素a已经匹配好了)逐个向后比较;
那么,对于j的下一个位置,这里可以用一个数组next[j]来存储。

next[j]数组
next数组表明模式串中第j个字符与主串中相应字符“失配”时,在模式串中需重新和主串中该字符进行比较的字符的位置。

其计算方法如下,

n e x t [ j ] = { max ⁡ { k ∣ 1 &lt; k &lt; j , p 1 ⋯ p k − 1 ⏞ ( 从 头 开 始 的 k − 1 个 元 素 ) = p j − k + 1 ⋯ p j − 1 ⏞ ( j 前 面 的 k − 1 个 元 素 ) } 0 &ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace; 当 j = 1 时 1 &ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace; 其 他 情 况 next[j] = \left\{ {\begin{array}{l} {\max \{ k|1 &lt; k &lt; j,\overbrace {{p_1} \cdots {p_{k - 1}}}^{(从头开始的k-1个元素)} = \overbrace {{p_{j - k + 1}} \cdots {p_{j - 1}}}^{(j前面的k-1个元素)}\} }\\ {0\;\;\;\;\;\;\;\;\;\;\;当j = 1时}\\ {1\;\;\;\;\;\;\;\;\;\;\;其他情况} \end{array}} \right. next[j]=max{k1<k<j,p1pk1 (k1)=pjk+1pj1 (jk1)}0j=11

例子如下:

j1234567891011121314151617
模式串abcaabbcabcaabdab
next[j]01112231123456712


KMP算法描述:

int Index_KMP(SString S, SString T, int pos)
{
	int i = pos, j = 1;
	while (i <= S.length&&j <= T.length)
	{
		if (S.ch[i] == T.ch[j])    
		{
			++i;
			++j;
		}
		else
			j = next[j];            //i不变,j后退
	}
	if (j >= T.length)
		return i - T.length;        //返回匹配的第一个字符的下标
	else
		return 0;                   //模式匹配不成功
}

next[j]数组的求法描述:

void get_next(SString T, int& next[])
{
	i = 1;
	next[1] = 0;
	j = 0;
	while (i < T.length)
	{
		if (j == 0 || T.ch[i] == T.ch[j])
		{
			++i;
			++j;
			next[j] = j;
		}
		else
			j = next[j];
	}
}

next[j]数组的改进
对于某些特殊情况,如下图,
在这里插入图片描述
其next数组为,
在这里插入图片描述
但在这里,因为模式串p中由好几个元素一样,所以j指针的位置如果继续用next数组里的值,就会不合适,这里对next函数进行一个修正,修正过的值存在nextval数组里,修正方法如下图,
在这里插入图片描述

nextval数组的求法描述为:

void get_nextval(SString T, int& nextval[])
{
	i = 1;
	nextval[1] = 0;
	j = 0;
	while (i < T.length)
	{
		if (j == 0 || T.ch[i] == T.ch[j])
		{
			++i;
			++j;
			if (T.ch[i] != T.ch[j])
				nextval[i] = j;
			else
				nextval[i] = nextval[j];
		}
		else
			j = nextval[j];
	}
}

这里,KMP算法的复杂度为 O ( n + m ) O(n+m) O(n+m)

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值