第四篇 数据结构队列和串.docx

上传人:b****5 文档编号:7738060 上传时间:2023-01-26 格式:DOCX 页数:19 大小:82.83KB
下载 相关 举报
第四篇 数据结构队列和串.docx_第1页
第1页 / 共19页
第四篇 数据结构队列和串.docx_第2页
第2页 / 共19页
第四篇 数据结构队列和串.docx_第3页
第3页 / 共19页
第四篇 数据结构队列和串.docx_第4页
第4页 / 共19页
第四篇 数据结构队列和串.docx_第5页
第5页 / 共19页
点击查看更多>>
下载资源
资源描述

第四篇 数据结构队列和串.docx

《第四篇 数据结构队列和串.docx》由会员分享,可在线阅读,更多相关《第四篇 数据结构队列和串.docx(19页珍藏版)》请在冰豆网上搜索。

第四篇 数据结构队列和串.docx

第四篇数据结构队列和串

§9.3队列

队列是不同于栈的另一种线性表。

在日常生活中,无论是购物、订票或候车都有可能要排队。

排队所遵循的原则是“先来先服务”,后来者总是加到队尾,排头者总是先离开队伍。

队列就是从日常生活中的排队现象抽象出来的。

一、队列的定义

所谓队列,就是允许在一端进行插入,在另一端进行删除的线性表。

允许插入的一端称为队尾,通常用一个队尾指针r指向队尾元素,即r总是指向最后被插入的元素;允许删除的一端称为队首,通常也用一个队首指针f指向排头元素的前面。

初始时f=r=0(如下图)。

显然,在队列这种数据结构中,最先插入在元素将是最先被删除;反之最后插入的元素将最后被删除,因此队列又称为“先进先出”(FIFO—firstinfirstout)的线性表。

与栈相似,队列的顺序存储空间可以用一维数组q[1‥m]模拟:

我们按照如下方式定义队列:

Const

m=队列元素的上限;

Type

equeue=array[1…m]ofqtype;{队列的类型定义}

Var

q:

equeue;{队列}

r,f:

integer;{队尾指针和队首指针}

二、队列的基本运算

队列的运算主要有两种:

入队(aDD)和出队(DEL)

1、过程ADD(q,x,r)—在队列q的尾端插入元素x

procedureADD(varq:

equeue;x:

qtype;varr:

integer);

begin

ifr=mthenwriteln(’overflow’){上溢}

elsebegin{后移队尾指针并插入元素x}

r←r+1;q[r]←x;

end;{else}

end;{ADD}

2、过程DEL(q,y,f,r)—取出q队列的队首元素y

procedureDEL(varq:

equeue;vary:

qtype;varf:

integer);

begin

iff=rthenwriteln(’underflow’){下溢}

elsebegin{后移队首指针并取出队首元素}

f←f+1;y←q[f];

end;{else}

end;{DEL}

由于队列只能在一端插入,在另一端删除,因此随着入队及出队运算的不断进行,就会出现一种有别于栈的情形:

队列在数组中不断地向队尾方向移动,而在队首的前面产生一片不能利用的空闲存储区,最后会导致当尾指针指向数组最后一个位置(即r=m)而不能再加入元素时,存储空间的前部却有一片存储区无端浪费,这种现象称为“假溢出”。

下图给出了一个“假溢出”的示例:

三、循环队列

为了解决“假溢出”的问题,我们不妨作这样的设想:

在队列中,当存储空间的最后一个位置已被使用而要进行入队运算时,只要存储空间第一个位置空闲,便可将元素加入到第一个位置,即将存储空间的第一个位置作为队尾。

采用首尾相接的队列结构后,可以有效地解决假溢出的问题,避免数据元素的移动,这就是所谓的循环队列。

下图给出了循环队列的结构。

循环队列将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用,循环队列的存取方法亦为““先进先出””。

对循环队列操作有以下几种状态:

94⑴初始时队列空,队首指针和队尾指针均指向存储空间的最后一个位置,即f=r=m。

95⑵入队运算时,尾指针进一,即

r←r+1;ifr=m+1thenr←1;

这两条语句可用一条语句替代:

r←rmodm+1;

第一篇⑶出队运算时,首指针进一,即

f←f+1;iff=m+1thenf←1;

这两条语句可用一条语句替代:

f←fmodm+1;

第二篇⑷队列空时有f=r。

第三篇⑸队列满时有f=rmodm+1。

(为了区分队列空和队列满,改用“队尾指针追上队首指针”这一特征作为队列满标志。

这种处理方法的缺点是浪费队列空间的一个存储单元)

循环队列的运算有两种:

1、过程ADD2(q,x,r)—在循环队列q中插入一个新元素x

procedureADD2(varq:

equeue;x:

qtype;varr:

integer);

begin

t←rmodm+1;{计算插入位置}

ift=fthenwriteln(’full’){队列满}

elsebegin{新元素x插入队尾}

r←t;q[r]←x;

end;{else}

end;{ADD2}

2、过程DEL2(q,y,f)—从循环队列q中取出队首元素y

procedureDEL2(varq:

equeue;vary:

qtype;varf:

inteqer);

begin

iff=rthenwriteln(’empty’){队列空}

elsebegin

f←fmodm+1;y←q[f];{取出队首元素}

end;{else}

end;{DEL2}

队列的应用范围很广,其中最为典型的应用是广义表的计算和图的宽度优先搜索。

本章节着重讲解前者,至于后者,放到“§10.2图”中详述。

四、队列的应用——计算广义线性表

【例题9.2.1】广义表的计算

有一个表l={a1,…,an},其中l为第一个广义表的表名,ai为表元素(1≤i≤n)。

当ai为数值时,表示为元素;当ai为大写字母时,表示另一个表,但不能循环定义。

例如下列定义是合法的(约定l是第一个表的表名):

l=(3,4,3,4,k,8,0,8,p)

k=(5,5,8,9,9,4)

p=(4,7,8,9)

输入:

输入全部广义表,每行一个广义表。

输出:

输出两行。

第一行为最大元素值,第二行为全部广义表的数和

分析:

广义线性表(简称广义表)是线性表的一种推广。

如果允许构成线性表的元素本身又可以是线性表的话,则该线性表即为广义表。

由此可见,广义表是一个递归定义的表,允许其元素可以是本身的一个子表。

如果需要将广义表的所有元素排成一个线性序列,则必须指明第一个广义表的表名。

我们可以对广义表进行计算。

例如求出所有元素的最大元素,或者表中全部元素的和。

constlmax=100;{广义表串长的上限}

type

tabtype=record{广义表的数据类型}

length:

0..lmax;{表长}

element:

array[1..lmax]ofchar;{表的数据序列}

end;

qtype=record{队列的数据类型}

base:

array[0..lmax]ofchar;{队列}

front,rear:

0..lmax;{首尾指针}

end;

var

t:

array[’a’..’Z’]oftabtype;{t[ch]—表名为ch的广义表}

q:

qtype;{队列}

1、构造广义表t

每一个广义表用一个字符串s读入,s中的所有数字和字母看作是表的元素,将其中的小写字母统一为大写。

q队列依次存储广义表L中的字母元素(即表名)。

先读入广义表L,将其元素存入t[L]中,表L中出现的字母依次进入队列q;若队列q不空,队首元素ch出队,读入广义表ch,将其元素存入t[ch]中,表ch中出现的字母再依次入队,……,直至队列空为止。

例如

初始时,广义表名‘l’进入队列q

随后,‘l’出q队列,‘l’=‘(3,4,3,4,k,8,0,8,p)’(下划线部分为输入),‘k’和‘p’相继入队,广义表t[‘l’]=‘3,4,3,4,k,8,0,8,p’

‘k’出q队列,‘k’=‘(5,5,8,9,9,4)’(下划线部分为输入),广义表t[‘k’]=‘5,5,8,9,9,4’

‘p’出q队列,‘p’=‘4,7,8,9’(下划线部分为输入),广义表t[‘k’]=‘4,7,8,9’

广义表表名的入队运算inqueue(q,s[i])和出队运算outqueue(q)如下:

procedureinqueue(varq:

qtype;c:

char);{表名c从队尾进入}

begin

q.rear←q.rear+1;{队尾指针+1}

q.base[q.rear]←c;{入队}

end;{inqueue}

functionoutqueue(varq:

qtype):

char;{队列首部的表名出队}

begin

q.front←q.front+1;{队首指针+1}

outqueue←q.base[q.front];{出队}

end;{outqueue}

由此得出广义表的构造过程

forch←’a’to’Z’dot[ch].length←0;{置所有广义表空}

q.front←0;q.rear←0;{队列的首尾指针初始化}

inqueue(q,’L’);{表名L进入队列}

whileq.front<>q.reardo{若队列非空,则循环}

begin

ch←outqueue(q);{取出队列首部的表名}

write(ch,’=’);{输入表名为ch的广义表串}

readln(s);

i←1;{从广义表串的第1个字符开始取元素}

whiles[i]<>’(’doi←i+1;

whiles[i]<>’)’do

begin

s[i]←upcase(s[i]);{将第i个字符统一为大写}

ifs[i]in[’a’..’Z’,’0’..’9’]{若第i个字符为广义表元素,则该字符进入广义表}

thenbegin

inc(t[ch].length);

t[ch].element[t[ch].length]←s[i];

ifs[i]in[’a’..’Z’]theninqueue(q,s[i]);{若第i个字符为表名,则入队}

end;{then}

inc(i);{分析输入串的下一个字符}

end;{while}

end;{while}

我们通过上述方法将所有广义表存储在t序列中。

有了t序列,不难计算广义表L的最大值和数和。

2、计算广义表L的最大值

设广义表L中的最大数码为m,m设为全局变量。

初始时,m=’0’。

我们从t[L]开始搜索:

⑴若表中的第i个元素(t[L].element[i])为数字码,则m与之比较,若m小于该数码,则被取代之;

⑵若表中的第i个元素为字母c,则递归搜索以该字母为表名的广义表t[c]。

依次类推,直至搜索了广义表L的所有元素为止。

这一递归计算过程由子程序maxnumber描述:

functionmaxnumber(c:

char):

char;{计算和返回表名为c的广义表t[c]的最大值}

var

ch,m:

char;

i:

integer;

begin

max←’0’;{最大数码初始化}

fori←1tot[c].lengthdo{搜索广义表t[c]的每一个元素}

begin

ch←t[c].element[i];{取出广义表t[c]的第i个元素}

ifchin[’a’..’Z’]thenm←maxnumber(ch){若该元素为表名,则递归计算最大数码}

elsem←ch;{若该元素为数码,则记下}

ifmax

end;{for}

maxnumber←max;{返回广义表t[c]的最大数码}

end;{maxnumber}

显然,主程序可通过语句

writeln(’themaxnumberintableLis:

’,maxnumber(’L’));

直接计算和输出广义表L的最大值。

3、计算广义表L的数和

设k为当前数和,初始时,k=0。

我们从t[L]开始搜索:

⑴若表中的第i个元素(t[L].element[i])为数字码,则该数字码对应的数值累计入k;

⑵若表中的第i个元素为字母c,则递归搜索以该字母为表名的广义表t[c]。

依次类推,直至搜索了广义表L的所有元素为止。

对应的k即为广义表L中的数和。

这一递归计算过程由函数total描述:

functiontotal(c:

char):

integer;{计算和返回表名为c的广义表t[c]的数和}

var

k,i,m:

integer;

ch:

char;

begin

k←0;{数和初始化}

fori←1tot[c].lengthdo{搜索广义表t[c]的每一个元素}

begin

ch←t[c].element[i];{取出广义表t[c]的第i个元素}

if(chin[’A’..’Z’])thenm←total(ch){若该元素为表名,则递归计算数和}

elsem←ord(ch)-ord(’0’);{若该元素为数码,则记下}

k←k+m;{累计数和}

end;{for}

total←k;{返回广义表t[c]的数和}

end;{total}

显然,主程序可通过语句

writeln(’Totalis:

’,total(’L’));

直接计算和输出广义表L的数和。

§9.4串

一、串的基本概念

前面介绍的线性表的操作都是对一个元素进行处理的,但实际中我们经常要对一串元素进行操作。

例如,输入和输出一个位数超过12位的整数值,但pascal的任何一种数据类型都不可能容纳这么大的一个整数,无奈何,只能采用字符串类型输入和输出。

再如信息检查系统、文字编辑系统、自然语言翻译系统以及音乐分析程序等等,都是以字符串数据作为处理对象的。

随着非数值的广泛应用,字符串的处理将显得越来越重要。

串是由零个或多个字符组成的有限序列。

一个串中包含的字符个数称为这个串的长度。

长度为零的串称为空串,它不包含任何字符。

从本质上讲,串是元素为字符类型的数组。

但稍有不同的是,在输入和输出时,字符数组是逐个字符进行的,而串可以直接输入输出,因此串是一种紧凑型的字符序列。

通常用撇’’将字符串括起来。

例如

⑴’x1’长度为2的串

⑵’123’长度为3的数串

⑶’’长度为0的空串

⑷’’包含一个空白字符(长度为1)的非空串

假设s1和s2是两个串:

s1=‘a1……an’

s2=‘b1……bm’

其中ai、bi代表字符(0≤m≤n)。

如果存在整数i(0≤i≤n-m),使得

bj=ai+jj=1‥m

同时成立,则称s2是s1的子串,又称串s1包含串s2。

串中所能包含的字符依赖于具体机器的字符集,按字符的字符集中的次序可以规定字符的大小。

目前世界上最为广泛的字符集是ASCII(美国信息变换标准码)和EBCDIC(扩充的二进制编码、十进制信息码),它们都规定数字字符’0’‥’9’的字符集是顺序排列的,字母字符’A’‥’Z’(’a’‥’z’)的字符集也是顺序排列的,因此用ord函数计算字符在字符集中的序号就有

ord(’a’)

ord(’0’)

并且对所有数字i(0≤i≤9)满足

i=ord(’i’)-ord(’0’)

通常可对两个字符ch1和ch2作比较,所谓ch1

例如

’a’<’abo’<’x’

’012’<’123’<’2’

程序中使用的串可以分成两种

1、串常数。

串常数具有固定的串值,即可以用直接量表示,用以原样输出,亦可以给串常数命名,以便反复使用时书写和修改方便。

例如

constobject=’datastructure’;{命名串常数}

writeln(’overflow’);{原样输出串常数}

2.串变量。

串变量的取值是可以改变的,但必须用名字来识别,说明串变量的方法与其它变量相似。

例如

var

s:

string[30];

上面定义了一个串变量s,s最多能容纳30个字符,且顺序存在一个字符数组中,类似于

var

s:

array[0‥30]ofchar;

其中s[0]记载了s的实际长度。

若string类型中省略长度标记,则串长上限为255个字符。

二、串运算的库函数

TURBO.PASCAL中提供了一些串运算的库函数,我们将作以简单的介绍

1、连接运算——函数concat(s1,[,s2,…,sn])

其中值参s1,‥,sn为string类型,函数值为string类型。

若连接后的串长大于255,则自动截断超出部分。

2、求子串——函数copy(s,i,l)

其中值参s为string类型,i和l为integer类型。

函数返回s串中第i个字符开始、长度为l的子串(string类型)。

若i大于s的长度,则回送一个空串;若l大于第i个字符开始的余串长度,则仅回送余串。

3、删子串——过程delete(vars,i,l)

其中变量参数s为string类型,值参i、l为ingteger类型。

该过程删去s中第i个字符开始的长度为l的子串,并返回剩余串s。

若i大于原串s的长度,则不删任何字符;若l大于第i个字符开始的余串长度,则删去余串。

4、插入子串——过程insert(s1,vars,i)

变量参数s为string类型,值参s1为string类型。

该过程将s1子串插入空串s的第i个字符位置处,并返回插入后的结果s。

若插入后s的串长大于255个字符,则截断超出部分。

5、求串长——函数length(s)

值参s为string类型。

该函数返回s串的实际长度值(integer类型)。

6、搜索子串位置——函数pos(s1,s2)

值参s1和s2为string类型。

若s1是s2的一个子串,则返回s1中第1个字符在s2串中的位置(integer类型);若s1非s2的一个子串,则返回0。

7、数值转换为数串——过程str(x,vars)

值参x为integer类型或real类型,变量参数s为string类型。

该过程将返回数值x对应的数串s。

8、数串转换为数值——过程val(s,varv,varc)

值参s为string类型,变量参数v为integer类型或real类型,变量参数c为integer类型。

该过程试将s串转换成数值v。

若转换成功,则c为0,并返回对应的数值v;否则c为无效字符的序数。

9、字符的大写转换——函数upcase(ch)

值参ch为char类型。

该函数返回ch字符的大写体(char类型)

在各种串处理系统中包含了串的许多运算,其中子串的模式匹配是最重要的操作之一。

三、串运算的应用——子串模式匹配

设s为主串,t为模式串,q为s中第一个与t相等的子串。

所谓模式匹配指的是

⑴若q存在,则计算出q的首字符在s中的位置;

⑵若q不存在,则返回0。

模式匹配的算法有很多。

为便于讨论,以后我们将主串s的长度设为n,匹配指针为k;模式串t的长度设为m。

朴素的串匹配算法:

1fork←1ton-m+1doifcopy(t,1,m)=copy(s,k,m)then输出k;

如果以字符匹配为计算单位的话,这种算法最坏情况下的运行时间为O((n-m+1)m)。

问题是,有没有时效更高的匹配算法?

有的,kmp算法就是其中最出色的一个算法。

我们从一个实例引出讨论

【例题9.3.1】计算最长重复子串

在一个字串中,多次出现的子串称为重复子串。

如果这样的子串有多个,则其中长度最长的子串称为最长重复子串。

例如s='abcdacdac',则t='cdac'为s的最长重复子串。

请你在尽可能短的时间内找出最长重复子串。

输入:

字符串s,长度不超过255

输出:

s的最长重复子串

分析:

1、kmp算法

朴素的串匹配算法的时效之所以不尽人意,是因为有重复的计算。

看下面的例子:

模式t=‘ATATACG’的第6个字符在当前位置无法匹配,朴素的串匹配算法将k递增1,从t的第一个字符开始重新匹配,但实际上此时可将k递增2,从t的第4个字符开始匹配,因为在这之间的匹配肯定会失败。

类似的例子随着数据量的增大而越来越多,朴素的串匹配算法将做更多的重复工作,使得时效很低。

为什么可以让k递增2,从t的第4个字符开始匹配呢?

这是由t的性质决定的。

如上图所示,t的前缀‘ATA’恰好是t的前缀‘ATATA’的后缀,所以如果直到‘ATATA’都匹配成功,而‘ATATAC’匹配失败,则主串s的子串sk+2‥sk+4必定为‘ATA’(因为‘ATATA’在k处匹配成功,sk‥k+4=‘ATATA’),于是t的前缀‘ATA’肯定在k+2处匹配成功。

KMP算法正是利用了这种特性使得算法的时间复杂度降为O(n+m)。

算法的关键是求t的前缀函数next,使得next[j]=max{k|k

next[j]=

例如模式串‘ATATACG’

J=

1

2

3

4

5

6

7

模式

A

T

A

T

A

C

G

next[j]

0

1

1

2

3

4

1

观察上表

t[1]=t[1],得出next[2]=next[1]+1=1;

t[1]≠t[2],得出next[3]=1;

t[3]=t[4]=’A’,得出next[4]=next[3]+1=2;

t[1..2]=t[3..4]=’AT’,得出next[5]=next[4]+1==3;

t[1..3]=t[3..5]=‘ATA’,得出next[6]=next[5]+1==4;

t[6]前的任何一个前缀都不含字符‘C’,得出next[7]=1。

显然,在s与t顺序匹配的过程中,如果t[j]与主串s的当前字符不匹配,则s的当前字符与t[next[j]]比较,不需要从t[1]开始重新比较。

移动的位置与s无关,取决于模式串t本身,求next(j)实际上就相当于用t匹配t的过程。

由于这个匹配过程是按照字符顺序依次进行的,因此是一个递推过程。

Type

nexttype=array[1..255]ofinteger;

Var

next:

nexttype;{t的前缀函数}

我们通过get_next过程计算模式串t中每个字符的next值

procedureget_next(t:

string;varnext:

nexttype);

varj,k:

integer;

begin

j←1;k←0;next[1]←0;{初始化}

whilej<=length(t)do{循环,求每一个字符的next值}

if(k=0)or(t[j]=t[k])

thenbegin{若不存在可匹配的子串或比较相等,则next[j+1]←next[j]+1}

j←j+1;k←k+1;next[j]←k;

end{then}

elsek←next[k];{否则依次类推找更短的子串}

end;{get_next}

显然,如果模式串t的串长为m,则get_next过程的时间复杂度为W(m)。

2、使用kmp算法

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 初中教育 > 学科竞赛

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1