408复习笔记——数据结构(四):串和模式匹配算法(KMP)

408考研笔记系列(四)(PS:本人使用的是王道四本书和王道视频)


前言

在上一章节中,我们了解到栈和队列这样的两端受限的线性表结构,这一章节中,我们将了解一种新的线性表结构,他的两端并没有收到限制,但是他并不能如同线性表一般可以存储任意类型的数据,他只能存储字符类型;没错,这就是他就是我们日常生活中用到最为广泛的一种数据结构——字符串;这一章,我们将更好的了解字符串,以及他的逻辑结构、存储结构、运算和基于字符串的模式匹配算法;


一、简介

字符串对于我们而言可以说是非常之熟悉,一本书,一句话都可以用字符串来表示,为此作为一种线性结构,字符串的一些常见运算便非常值得我们进行学习;除此之外,我们在生活中最经常使用的一种字符串运算——字符串模式匹配,如何让模式匹配变得更加高效、更加快捷便也成为了本章学习的重点部分;

二、主要内容

2.1 串及串的基本操作

字符串简称串,是一种由零个或多个字符多个字符组成的有限序列;这跟线性表的定义很像,不过线性表是指有相同数据类型即可,而串则是指由字符组成,这注定了串和线性表之间的关系将会十分之密切;

通过线性表,我们可以知道线性表是一种逻辑结构为线性,并且具有顺序存储结构和链式存储结构两种存储结构,顺序存储结构既可以进行静态分配也可以动态分配,运算包括创建、增加、删除等等;没错以上的特点串都具备了,而我们主要看的是串与线性表在运算上不同之处;

串在线性表基础上除了最基本的创销增删改查,还有这几个基本操作也是非常常见的:求子串、比较操作和定位操作,而今天我们只要介绍的便是这几种基本操作;

我们选取采用顺序存储结构的串来实现以上几种操作,
在这里插入图片描述

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MaxSize 255
// 定义串的顺序存储
typedef struct {
	char ch[MaxSize];
	int length;// 这里的length标记的位置是下一次需要存放字符的位置
}SString;

初始化时,我们直接将length的值给到了数组1的位置,因此length每次标记的位置便是下一次需要存放字符的位置;

#include "func.h"
// 对字符串进行初始化
SString Init_String(SString * S)
{
	S->length = 1;
}

求子串操作代码:

#include "func.h"
// 求子串,用sub返回串S的第Pos个字符起长度为len的子串
void SubString(SString S, SString * sub, int pos, int len)
{
	sub->length = 1;
	// 首先判断子串是否包含在当前字符串内
	if (pos + len  > S.length)
	{
		sub = NULL;
	}
	else
	{
		for (int i = 0; i < len; i++)
		{
			sub->ch[sub->length++] = S.ch[pos + i];
		}
	}
}

比较操作的代码:

#include "func.h"
// 做字符串比较操作
// 若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0;
int StrComp(SString S, SString T)
{
	int i = 1;
	while (i < min(S.length, T.length))
	{
		if (S.ch[i] == T.ch[i])
		{
			i++;
		}
		else
		{
			return (S.ch[i] - T.ch[i]);
		}
	}
	return S.length - T.length;
}

定位操作的代码:

#include "func.h"
// 定位操作,查看主串S中是否包含有与串T相同的子串,若有则返回他在主串中的起始位置,若没有则返回0
int Index(SString S, SString T)
{
	int i = 1;
	SString sub;
	// 设置需要比较的次数
	while (i < S.length - T.length + 2)
	{
		SubString(S, &sub, i, T.length-1);
		if (StrComp(T, sub) != 0)
		{
			i++;
		}
		else
		{
			return i;
		}
	}
	return 0;
}

2.2 串的模式匹配

2.2.1 朴素模式匹配

在上述的定位操作代码中,我们不仅需要求子串,而且还需要对每次求出来的子串与模式串进行字符串比较,因此过程十分复杂,而我们的模式匹配工作是十分频繁的,每一次如果这样复杂的话,会导致效率十分低下;

为此,这里在采用定长顺序存储的基础上,给出了一种不依赖于其他船操作的暴力匹配算法,整个过程主要通过两个分别指向主串和模式串的指针来实现,我们又称此为朴素模式匹配算法,代码如下:

#include "func.h"
// 朴素模式匹配算法,也就是暴力匹配算法,
// 这里与定位操作不同的是没有依赖于其他串操作,只对两个数据串的数组进行操作
// 若匹配上则返回其在主串中的起始位置;若未匹配则返回0
int NMP(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 + 1;
	}
	else
	{
		return 0;
	}
}

2.2.2 改进的模式匹配算法——KMP算法

在朴素模式匹配算法中,我们发现,每次出现字符不匹配时我们都需要将我们的主串指针和模式串指针修改到主串和模式串开始的地方,重新进行匹配,这样操作明显是不聪明的,当出现最坏的情况时,我们的时间复杂度高达O(mn)(m是主串长度、n是模式串长度);

那么针对这种情况我们应当采取怎样的优化操作呢?例如出现下图中图(a)的情况:
在这里插入图片描述
如果此时我们依然按照朴素模式匹配的方式,很显然那会是非常愚蠢的,我们必须将主串和模式串的指针都进行回溯,可是我们很清楚的看到我们可以按照图(b)的方式作为我们下一次配对的开始,这样很显然是节省时间;

那么问题来了,我们应当如何操作,才能向图(b)那样明智,没错,这也是KMP算法的基本思想所在,我们需要为我们每一个模式串创建一个属于他的next数组,这个数组的功能便在于帮助我们在出现不匹配的情况下很方便的找到我们下次匹配时模式串的起始位置;有了next数组后,我们匹配的流程大概就变成了这样:
在这里插入图片描述

KMP代码如下:

// T 模式串, S目标串, pos第几个字符之后搜索
// 约定索引起始值为1,也有可能是0,我们需要审题时注意这一点
void Index_KMP(SString S, SString T, int pos){
	// i 目标串指针,j 模式串指针
    i = pos; j = 1;
    while( i <= length(S) && j <= length(T)){
        // 如果将要和 目标串元素 匹配的元素是模式串首元素前一位的元素
        // 当前目标串元素 和 模式串元素可以匹配
        // if (j == first_indexof(T) - 1 || S[i] == T[j]){
        if(j == 0 || S[i] == T[j]){
        	// 指针各自右移一位,这也是为什么j==0时需要放在一起判断的原因
            ++i;
            ++j;
        }else{
            // 发生了失配,查Next数组移动模式串指针
            j = next[j];
        }
    }
    if (j > length(T)){
        // 如果模式串指针溢出了(模式串指针匹配完毕了所有模式串中的元素)
    	return i - length(T);
    }
    else return 0;
}

代码中我们需要注意以下几个地方:

  1. 我们需要注意模式串是从数组0还是数组1开始的,这个十分关键,因为next数组指向的数和模式串元素在数组中的位置是对应的,不经意间,这题可能就会丢分;
  2. 当模式串第一个元素未匹配时,是将其next数组元素设置为0的,并且是会专门判断j==0的;

接下来,便是如何求next数组啦!

求解next数组其实很简单,网上一般通过最长可匹配前缀的方法解决;但对于我们考试而言,我们可以有自己的理解和手算求解方法,我们可以自己将现未匹配元素前的元素与模式串中进行匹配,看能否找到匹配项,若可以匹配项长度加1便是next数组的值啦;若不可以就再将未匹配元素从开始缩短一个长度重新匹配,直到所有的未匹配元素都无法找到匹配项,我们就将其next设置为1;

这里可能描述的比较模糊,大家可以看一看王道书或者王道视频,如果之后有时间会结合图片再说一次的;

一般408考试出现的都是next数组,基本上不会出现nextval数组,因此就不做展开了;


三、常见题及易错题归纳

栈和队列答案:B、C、C、C、B、A、A、A、C
在这里插入图片描述
答案见下一章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值