第四篇 数据结构队列和串.docx
《第四篇 数据结构队列和串.docx》由会员分享,可在线阅读,更多相关《第四篇 数据结构队列和串.docx(19页珍藏版)》请在冰豆网上搜索。
第四篇数据结构队列和串
§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;{若该元素为数码,则记下}
ifmaxend;{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|knext[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算法