王道数据结构第四章 串

4.1.1 串的定义和基本操作

串,即字符串string,是由0个或多个字符组成的有限序列。一般记为S='asdf......'其中S是串名,单括号括起来的是串的值,字符个数n称为串的长度,n=0时称为空串。

子串:串中任意个连续的字符组成的子序列。

主串:包含子串的串。

字符在主串当中的位置:字符在串中的序号。编号从1开始,和线性表的位序是一样的。

子串在主串中的位置:子串的第一个字符在主串当中的序号。

串其实就是一种线性表,数据元素必须是字符型。

串的基本操作,如增删改查等通常以子串为操作对象。

StrAssign(String&T,chars):赋值操作,把串T赋值为chars.

StrCopy(String&T,S):复制操作,由串S复制得到串T。

StrEmpty(S):判空操作,若S为空,返回true,否则返回false.

StrLength(S):求串长,返回S的元素个数。

ClearString(S):清空操作,清空S为空串。

DestroyString(S):销毁串,将串S销毁(回收存储空间)。

Concat(&T,S1,S2):用T返回由S1和S2连接而成的串。

SubString(&Sub,S,pos,len):求子串。用Sub返回串S的第pos个字符起长度为len的子串。

Index(S,T):定位操作,若主串S中存在与串T相同的子串,则返回它在主串中第一次出现的位置,                      否则返回0.

StrCompare(S,T):比较操作。若S>T,返回>0。

4.1.2 串的存储结构

1.串的顺序存储(定长)

#define MaxLen 20
typedef struct {
	char ch[MaxLen];
	int length;
}SString;

串的顺序存储(动态长度)

typedef struct {
	char* ch;
	int length;
}HString;
	HString S;
	S.ch = (char*)malloc(sizeof(HString));
	S.length = 0;

 堆分配内存,需要我们手动释放空间。

为了将数组下标和字符位序匹配,我们采取这样的方法:ch[0]废弃不用,在字符串末尾声明一个int型变量表示数组有多长。

串的链式存储:

typedef struct {
	char ch;//每个结点存放一个字符
	String* next;
}String;

这样做有很大的缺点:每一个字符变量只占一个字节,但是我们却多用了4个字节的指针来匹配他,换句话说,存储密度太低了,为此,我们改进这个方案,每一个结点存放4个字符(若不够长,可以用####代替)。

typedef struct {
	char ch[4];//每个结点存放一个字符
	String* next;
}String;

基本操作的实现:

求子串SubString(&Sub,S,pos,len)用Sub返回串S的第pos个字符起长度为len的子串。

bool SubString(String S, String& sub, int pos,int len) {
	if (sub + pos - 1 > S.length)//子串范围越界
		return false;
	for (int i = pos; i < pos + len; i++) {
		sub.ch[i-pos+1] = S.ch[i];//我们规定了从数组下标1开始存
	}
	sub.length = len;
	return true;
}

字符串的比较StrCompare(S,T)比较操作,若S>T则返回值>0;相等则为0,否则<0。

int StrCompare(String S, String T) {
	for (int i = 1; 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;
}

定位操作Index(S,T)若主串中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0.

//利用前面实现的两个基本操作,从主串中取出长度为len的子串,依次比较是否相等


int Index(String S, String T) {
	int m = S.length; int n = T.length;
	int i = 1;
	String sub;
	while (i <= n - m + 1) {
		SubString(S, sub, i, n);
		if (StrCompare(sub, T) != 0)
			i++;
		else return i;
	}
	return 0;
}

4.2.1 朴素模式匹配算法

字符串模式匹配:在主串中找到与模式串相同的子串,并返回其所在的位置。

讲主串中所有长度为m的子串依次与模式串匹配,直到配对或者所有的子串都不匹配为止。

主串长度为n,模式串长度为m,那么主串中长度为m的子串一共有n-m+1个。

int Index(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;
}

时间复杂度O(m(n-m+1))=O(mn)

4.2.2 KMP算法 (难度高,不好理解)

KMP算法核心思想:要利用好模式串中隐含的一些信息,处理好模式串失配时主串的信息。

因此,当我们匹配时,前面字符的信息是已知的,朴素模式匹配算法需要从第二个位置再来,如果我们采用KMP算法就可以避免这样。 

拿这个字符串为例子,我们第六个元素匹配失败时,令j=3,可以直接匹配上。 

所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任

怎么用代码实现这个逻辑?

if(S[i]!=T[j])
   j=next[i];
if(j==0){
   i++;j++;
}

相比于朴素模式匹配算法,KMP算法主串指针不需要回溯。

下面给出KMP算法的流程图:

next数组只和短短的模式串有关,和主串无关。 

408考研中,只要求我们能手动求出next数组即可,不要求用代码实现。同时我们要知道KMP算法的流程以及时间复杂度。

朴素模式匹配算法:最坏时间复杂度O(mn);

KMP算法:最坏时间复杂度O(m+n),其中计算next数组时间复杂度O(m),匹配过程时间复杂度O(n)。 

4.2.3 求next数组

1.手算练习

答应我,往后余生,next[0]=0,next[1]=1直接这样写好吗?

 

图片中给出了next数组的计算方法:在不匹配位置的前边画一条线,模式串一步步往后退,直到分界线以前都能对上或者模式串完全跨过分界线为止。

 再次说明,408不会出KMP算法大题,也不会代码要求实现next数组,我们需要掌握手算即可。

4.2.4 KMP算法的进一步优化

如图所示,我们先求出了next数组,当发现第五个匹配失败时,我们会返回到next[2],但是,第五个匹配失败,我们能得知主串元素一定不是b,现在回到了位置为2的next数组,这不是多此一举吗?因此next数组可以进一步优化, 因此我们更新了nextval数组,用它来作为KMP算法最终的参考。

图上给出了nextval数组的求法流程。,我们按照这样的逻辑求出的模式串nextval数组如下:

注意本章在考研复习时不必纠结KMP算法的具体实现,如果对KMP算法有更高兴趣,欢迎阅读我的另一篇文章: 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值