队列
队列
队列(queue)简称队,它是限定在一端进行插入,另一端进行删除的特殊线性表。正像排队买东西,排在前面的人买完东西后离开队伍(删除),而后来的人总是排在队伍末尾(插入),通常把队列的删除和插入分别称为出队和入队。允许出队的一端称为队首(front),通常用指针f来表,允许入队的一端称为队尾(rear),通常用指针r来表示。初始是f=r=0。所有需要进队的数据项,只能从队尾进入,队列中的数据项只能从队首离去。由于总是先入队的元素先出队(先排队的人先买完东西),这种表也称为“先进先出”(FIFO)表。
队列可以用数组Q[1…m]来存储,数组的上界m即是队列所容许的最大容量。在队列的运算中需设两个指针:
f:队首指针,指向实际队头元素的前一个位置
r:队尾指针,指向实际队尾元素所在的位置
队列中拥有的元素个数为:L=r-f.一般情况下,两个指针的初值设为0,这时队列为空,没有元素。
与堆栈相似,队列的顺序存储空间可以用一维数组来模拟:
const m=队列元素的上限;
type
queue=array[1..m] of datatype;
var
q:queue;
r,f:integer;
下图给出了一个队列在顺序存储方式下的当前状态,此时已有a、b、c三个元素相继出队(为了同队列中的元素相区别,把它们分别括了起来),队首指针f指向队首元素d,队尾指针r指向队尾元素j;若在图(a)的队列中插入一个新元素k,或者删除一个元素后,队列的当前状态分别如图(b)和(c)所示。
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
… |
m |
(a) |
(b) |
(c) |
d |
e |
f |
g |
i |
j |
|
|
… |
|
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
… |
m |
(a) |
(b) |
(c) |
d |
e |
f |
g |
i |
j |
k |
|
… |
|
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
… |
m |
(a) |
(b) |
(c) |
(d) |
e |
f |
g |
i |
j |
|
|
… |
|
顺序队列的插入和删除示意图
每次向队列插入一个元素,都将使队尾指针后移一个位置,当队尾指针指向最后一个位置m时,表明队列已满(实际上,若f不等于1的话,队首前面仍有空闲的单元可再被利用),若再进行插入,则溢出(我们把这种溢出叫做“假溢出”);每次删除队列中的一个元素,也同样使队首指针向后移动一个位置,当队首指针指向最后一个元素(即队尾指针所指的元素)并被删除后,表明队列已空,此时应把f和r同时置为0,以便从第一个单元起重新利用整个存储空间。
在Pascal语言中队列的顺序存储结构可描述如下:
const
qmaxsize={队列的最大容量};
type
{ qelement队列元素的数据类型};
queue=record
data:array[1..qmaxsize] of qelement;
front,rear:0..qmaxsize
end;
var
q:queue;
队列的基本操作
(1) 过程add(q,x,r)—在队列q的尾部插入元素x
procedure add(var q:queue;x:datatype;var r:integer);
begin
if r=m then writeln(‘overflow’)
else begin
r:=r+1;
q[r]:=x;
end;
end;{add}
(2)过程 del(q,y,f)—取出q队列的队首元素y
procedure del(var q:queue;var y:datatype;var f:integer);
begin
if f=r then writeln(‘underflow’)
else begin
f:=f+1;
y:=q[f];
end;
end;{del}
由于队列只能在一端插入,在另一端删除,因此随着入队和出队运算的不断进行,就会出现一种有别于栈的情形:队列在数组中不断地向队尾方向移动,而在队首的前面产生一片不能利用的空闲存储区域,最后会导致当队尾指针指向数组最后一个位置(即r=m)而不能再加入元素时,存储空间的前部却有一片存储区域无端浪费,这种现象称为“假溢出”,如下图所示。解决这种假溢出现象可以利用循环队列。
Qm |
|
Q4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
顺序结构存储的队列中头、尾指针的变化
循环队列及其运算
所谓循环队列,就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用。在循环队列中,当存储空间的最后一个位置已被使用而要进行入队运算时,只要存储空间第一个位置空闲,便可将元素加入到第一个位置,即将存储空间第一个位置作为队尾。采用首尾相连的循环队列结构后,有效解决了假溢出现象。如图所示:
对循环队列操作有以下几种状态:
(1) 开始时队列空,队首指针和队尾指针均指向存储空间的最后一个位置,即f=r=m。
(2) 入队运算时,尾指针进一,即
r:=r+1; if r=m+1 then r:=1; 或r:=r mod m+1 。
(3) 出队运算时,首指针进一,即
f:=f+1; if f=m+1 then f:=1; 或f:=f mod m+1。
(4) 队列空时有f=r。
(5) 队列满时有f=r mod m+1 。(为了区分队列空和队列满,改用“队尾指针追上队首指针”这一特征作为列满标志。这种处理方法的缺点是浪费队列空间的一个存储单元。)
在pascal语言中,循环队列可用下面形式描述:
const
qmaxsize={队列的最大容量};
type
{ qelemen队列元素的数据类型};
queue=record
data:array[0..qmaxsize-1] of qelement;
front,rear:0..qmaxsize-1
end;
var
q:queue;
循环队列的操作有两种:
(1) 过程add(q,x,r)—在循环队列q中插入一个新元素x
Procedure add(var q:queue;x:qtype;var r:integer);
Begin
t:=r mod m+1;
if t=f then writeln(‘full’)
else begin
r:=t;q[r]:=x;
end;
end;
(2) 过程del(q,y,f)—从循环队列q中取出队首元素y
procedure del(var q:queue;var y:qtype;var f:integer);
begin
if f=r then writeln(‘empty’)
else begin
f:=f mod m+1;y:=q[f];
end;
end;
另附队列的基本操作:
1.初始化(iniqueue(q)):设置q为空的循环队列。
procedure iniqueue(var q:queue);
begin
q.front:=0;
q.rear:=0
end;
2.判队列空(qempty(q)):若队列为空,则返回值true,否则返回值false。
function qempty(q:queue):boolean;
begin
qempty:=(q.front=q.rear)
end;
3.判队列满(qfull(q)):若队列满,则返回值true,否则返回值false。
function qempty(q:queue):bollean;
begin
qfull:=(q.front=(q.rear+1) mod qmaxsize)
end;
4.入队(enqueue(q,x)):若队列不满,则将x插入到队尾,否则返回信息“overflow”。
procedure enqueue(var q:queue;x:qelement);
begin
if qfull(q) then writeln(‘overflow’)
else begin
q.rear:=(q.rear+1) mod qmaxsize;
q.data[q.rear]:=x;
end
end;
5.出队(delequeue(q,x)):若队列不空,则把队头元素删除并返回值给x,否则返回信息“underflow”。
procedure delqueue(var q:queue;x:qelement);
begin
if qempty(q) then writeln(‘underflow’)
else begin
q.front:=(q.front+1) mod qmaxsize;
x:=q.data[q.front];
end
end;
6.取队头元素(gethead(q)):若队列不空,返回队头元素的值,否则返回信息“underflow”。
function gethead(q:queue):qelement;
var
m:0..qmaxsize;
begin
if qempty(q) then writeln(‘underflow’)
else begin
m:=(q.front+1) mod qmaxsize;
gethead:=q.data[m];
end
end;
7.求队列长度(lenqueue(q))。
function lenqueue(q:queue):integer;
begin
lenqueue:=(q.rear-q.front+qmaxsize) mod qmaxsize
end;
例1.已知Q是一个非空队列,S是一个空栈。使用Pascal语言编写一个算法,仅用队列和栈的下列指定函数和少量工作变量,将队列Q的所有元素逆置。
栈的函数有:
makeempty(s:stack); 栈置空
push(s:stack;value:datatype); 新元素value进栈
pop(s:stack):datatype; 出栈,返回栈顶值
isempty(s:stack):Boolean; 判栈空否
队列的函数有:
enqueue(q:queue;value:datatype); 元素value进队
dequeue(q:queue):datatype; 出队列,返回队首值
isempty(q:queue):Boolean; 判队列空否
解决该题的基本思想是:顺序取出队元素,入栈;所有元素入栈后,再从栈中逐个取出,入队。
算法如下:
reverse(q:queue;s:stack);
var
data:datatype;
begin
while not isempty(q) do
begin
data:=dequeue(q);
push(s,data);
end;
while not isempty(s) do
begin
data:=pop(s);
enqueue(q,data);
end;
end;
队列的应用范围很广,其中最为典型的应用是广义表的计算和图的宽度优先搜索。
例2、广义表的计算。
设有一个表L={a1,a2,…,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)
程序要求:当全部表给出后,求出所有元素的最大元素。例如上例的最大元素为9,表中的全部元素和为98。
·输入数据
输入全部表,每行一个表。
·输出数据
最大元素值和全部元素和。
算法分析:
广义线性表(简称广义表)是线性表的一种推广。如果允许构成线性表的元素本身又可以是线性表的话,则该线性表即为广义表。由此可见,广义表是一个递归定义的表,允许其元素可以是本身的一个子表。题目标约定表名为大写字母且L为第一个广义表的表名,所有广义表存储在数组t中。
参考程序:
program aa;
const Lmax=100; {广义表串长的上限}
type
tabtype=record {广义表的数据类型}
length:0..Lmax;
element:array[1..Lmax] of char;
end;
qtype=record
base:array[0..Lmax] of char;
front,rear:0..Lmax;
end;
var
t:array[‘A’..’Z’] of tabtype;
q:qtype;
ch:char;
s:string;
i:integer;
procedure inqueue(var q:qtype;c:char);
begin
q.rear:=q.rear+1;
q.base[q.rear]:=c;
end;{inqueue}
function outqueue(var q:qtype):char;
begin
q.front:=q.front+1;
outqueue:=q.base[q.front];
end;{outqueue}
function maxnumber(c:char):char;
var
ch,m,max:char;
i:integer;
begin
max:=’0’;
for i:=1 to t[c].length do
begin
ch:=t[c].element[i];
if ch in [‘A’..’Z’] then m:=maxnumber(ch)
else m:=ch;
if max<m then max:=m;
end;
maxnumber:=max;
end;{maxnumber}
function total(c:char):integer;
var
k,i,m:integer;
ch:char;
begin
k:=0;
for i:=1 to t[c].length do
begin
ch:=t[c].element[i];
if (ch in [‘A’..’Z’]) then m:=total(ch)
else m:=ord(ch)-ord(‘0’);
k:=k+m;
end;
total:=k;
end;
begin
for ch:=’A’ to ‘Z’ do t[ch].length:=0;
q.front:=0;q.rear:=0;
inqueue(q,’L’);
while q.front<>q.rear do
begin
ch:=outqueue(q);
write(ch,’=’);
readln(s);
i:=1;
while s[i]<>’(’ do i:=i+1;
while s[i]<>’)’ do
begin
s[i]:=upcase(s[i]); {将字符转换为大写}
if s[i] in [‘A’..’Z’,’0’..’9’]
then begin
inc(t[ch].length);
t[ch].element[t[ch].length]:=s[i];
if s[i] in [‘A’..’Z’] then inqueue(q,s[i]);
end;{then}
inc(i);
end;{while}
end;{while}
writeln(‘total is:’,total(‘L’));
writeln(‘max is:’,maxnumber(‘L’));
end.{main}
例3 编程计算由“*”号围成的下列图形的面积。面积的计算方法是统计*号所围成的闭全曲线中水平线和垂直线交点的数目。如下图所示,在10*10的二维数组中,有“*”围住了15个点,因此面积为15。
算法分析:
可以用10×10的二维数组gr存储这张图,我们先统计“封闭曲线”外的’0’的个数,把总个数减去外围‘0’的个数,再减去‘*’号的个数,剩下的就是图形面积。外围‘0’的个数统计方法采取从左上角的‘0’开始,找所有与它相邻的‘0’,找到后存入队列。一个‘0’的相邻点找完后,再找队列下一个‘0’的相邻点。这样继续下去,直到所有‘0’找到为止,为使已找过的点不再重复计算,设存入队列的点的标志为2。
为寻找相邻点,我们不妨设一个增量数组d,d的值是上、右、下、左相邻点的坐标增量:
i 1 2 3 4
d[0] 0 1 0 -1
d[1] -1 0 1 0
如果原点坐标为(x0,y0),则相邻点的坐标为:
x:=x0+d[0,i];y:=y0+d[1,i]。
参考程序:
program aa;
const n=10;
gr:array[1..n,1..n] of 0..2=
d:array[0..1,1..4] of integer=((0,1,0,-1),(-1,0,1,0));
var que:array[0..1,1..n*n] of byte;
i,j,f,x,y,start,tail,sum:integer;
begin
sum:=0;
for i:=1 to n do
for j:=1 to n do
if gr[i,j]>0 then sum:=sum+1;
x:=1;y:=5;start:=1;tail:=1;
que[0,1]:=x;que[0,1]:=y;gr[1,1]:=2;
repeat
for f:=1 to 4 do
begin
x:=que[0,start]+d[0,f];
y:=que[1,start]+d[1,f];
if (x>0) and (x<=n) and (y>0) and (y<=n) then
if gr[x,y]=0 then
begin
tail:=tail+1;gr[x,y]:=2;
que[0,tail]:=x;
que[1,tail]:=y;
end;{then}
end;{for}
start:=start+1;
until start>tail;
writeln(‘area=’,n*n-sum-tail);
end.
例4、合并果子。
问题描述:
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所消耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将1,2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力士3+12=15。可以证明15为最小的体力耗费值。
输入:
输入文件friut.in包括两行;第一行是一个整数n(1≤≤民10000),表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数ai(1≤ai≤20000)是第i种果子的数目。
输出:
输出文件fruit.out包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于231。
样例输入:
3
1 2 9
样例输出:
15
(提示:这里要求使用两个有序队列来编程。inc(i,j);{i := i + j;}
dec(i,j);{i := i - j;}
maxlongint是longint的上界,即2147483647 Long int 变量存储为 32 位(4 个字节)有符号的数值形式,其范围从 -2,147,483,648 到 2,147,483,647 )
program aa;
var a,b:array[1..10000] of longint;
n,i,pa,pb,qb,s,t,r:longint;
procedure quicksort(p,q:longint);
var i,j,m,t:longint;
begin
if p<q then
begin
i:=p;j:=q;m:=a[(i+j) div 2];
repeat
while a[i]<m do inc(i);
while m<a[j] do dec(j);
if i≤j then
begin
t:=a[i];a[i]:=a[j];a[j]:=t;
inc(i);dec(j);
end;
until i>=j;
if p<j then quicksort(p,j);
if i<q then quicksort(i,q);
end;
end;
begin
assign(input,’fruit.in’);
assign(output,’fruit.out’);
reset(input);rewrite(output);
readln(n);
for i:=1 to n do
begin
read(a[i]);
b[i]:=maxlongint div 2;
end;
a[n+1]:=maxlongint div 2;
a[n+2]:=maxlongint div 2;
b[n+1]:=maxlongint div 2;
b[n+2]:=maxlongint div 2;
quicksort(1,n);
pa:=1;pb:=1;qb:=0;s:=0;
for i:=1 to n-1 do
begin
t:=a[pa]+a[pa+1];r:=1;
if a[pa]+b[pb]<t then
begin
t:=a[pa]+b[pb];r:=2;
end;
if b[pb]+b[pb+1]<t then
begin
t:=b[pb]+b[pb+1];r:=3;
end;
inc(qb);b[qb]:=t;inc(s,t);
case r of
1:inc(pa,2);
2:begin inc(pa);inc(pb) end;
3:inc(pb,2);
end;
end;
writeln(s);
close(input);close(output);
end.
法二:
program aa;
var a:array[1..10000] of longint;
i,k,v,n,s,max:longint;
function list(s,t:integer):longint;
var i,min:longint;
begin
min:=s;
for i:=s+1 to t do
if a[i]<a[min] then min:=i;
list:=min;
end;
begin
readln(n);
for i:=1 to n do
read(a[i]);
max:=0;
for i:=1 to n-1 do
begin
k:=list(1,n-i+1);
s:=a[k];
a[k]:=a[n-i+1];
v:=list(1,n-i);
a[v]:=a[v]+s;
max:=max+a[v];
end;
writeln(max);
end.
习题
一、单项选择题
1.若用一个大小为6的数组来实现循环队列,且当rear和front的值分别为0和3 .当从队列中删除一个元素,再加入两个元素后,rear和front的值分别为多少?
A. 1 和5 B. 2和4 C. 4和2 D. 5和1
2.设栈5和队列Q的初始状态为空,元素e1、e2、e3、e4、e5和e6依次通过栈S,一个元素出栈后即进入队列Q,若6个元素出队的序列是e2、e4、e3、e6、e5、e1,则栈S的容量至少应该是( )。
A. 6 B. 4 C. 3 D. 2
3.如右图所示的循环队列中元素数目是( ).其中rear=32指向队尾元素,front=15指向队首元素的前一个空位置,队列空间m=60.
A. 42 B.16 C.17 D.41
4.在一个顺序队列中,队首指针指向队首元素的( )位置。
A.前一个 B.后一个 C.当前 D.后面
5.从一个顺序队列删除元素时,首先需要( )。
A.队首指针循环加1
B.队首指针循环减1
C.取出队首指针所指位置上的元素
D.取出队尾指针所指位置上的元素
6.假定一个不设队列长度变量的顺序队列的队首和队尾指针分别为f和r,则判断队空的条件为( ).
A. f+1=r B. r+1=f c. f=0
7.假定利用数组a[N]循环顺序存储一个队列,用f和r分别表示队首和队尾指针,并已知未满,当元素x进队时所执行的操作为( ).
A. a[r mod N+1]:=x B. a[(r+1) mod N]:=x
C. a[r mod N-1]:=x D.a[(r-1) mod N]:=x
8.假定一个带附加头结点的循环队的队首和队尾指针分别用front和rear表示,则判断的条件为( ).
A. front=rear B. rear=NULL
C. front=NULL D.front=rear
9.在一个长度为N的数组空间中,顺序存储着一个队列,该队列的队首和队尾指针分别用front和rear表示,则该队列中的元素个数为( ).
A. (rear-front) mod N B. (rear-front+N) mod N
B. (rear+N) mod N D.(front+N) mod N
10. 栈和队列逻辑上都是线性表
二、编程序
1、细胞问题
一矩形阵列由数字0到9组成,数字1到9代表细胞,细胞的定义为沿细胞数字上下左右还是细胞数字则为同一细胞,求给定矩形阵列的细胞个数。
输入:整数m,n(m行,n列) (1<=m<=80,1<=n<=50)
矩阵
输出:细胞的个数。
样例:
输入:
4 10
0234500067
1034560500
2045600671
0000000089
输出:4
0234500067
1034560500
2045600671
0000000089
共4个细胞
2、合并石子。
小Ray在河边玩耍,无意中发现一些很漂亮的石子堆,于是他决定把这些石子搬回家。河滩上一共有n(1≤n≤30000)堆石子,每次小Ray合并两个石子数最少的两堆石子成为一堆。经过n-1次合并操作以后,只剩下一堆石子,然后小Ray就将这一堆石子搬回家。每合并两堆石子的时候,小Ray消耗的体力是两堆石子的数量之和。请你算一算,小Ray合并所有石子堆消耗的体力是多少呢?
解析:设这些石子堆的数量为w1、w2、……wn,且满足w1≤w2≤…≤wn。首先小Ray肯定将1和2两堆石子合并,设新合并的石子堆为y1=w1+w2。接着小Ray一定是在{y1}∪{w2…wn}中选择两个最小的石子堆合并,那么设新合并的石子堆为y2。如此类推,第三次合并的石子堆记作y3,第四次合并的记作y4…第n-1次合并的石子堆记作yn-1。
可以证明,新合并的石子堆的大小一定满足:y1≤y2≤y3≤…≤yn-1。因为每次合并的新石子堆一定是选择当前所剩下的石子堆中最小的两堆合并,而剩下的石子堆的石子数量又是不断增多的,所以越早合并的石子堆的石子数量越少。
有了上面这条事实,我们知道在合并过程中w和y数组始终是保持有序的。因此假设当前剩下的石子堆为wi…wn和yj…ym,那么最小的石子堆不是wi就是yj,拿出其中最小的一个,然后再拿出次小的一个,合并成新的石子堆一定是放在ym之后。也就是说将y看成一个队列,在队首取出元素,在队尾插入元素,且在插入和删除的过程队列始终保证了从小到大的顺序(越靠近队首越小)。由于w中的每个元素最多删除一次,y中的元素最多插入队列和从队列中取出一次,所以总的复杂度为O(n)。