队列的基本知识及应用
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
队列的基本知识及应用
[内容提要]
1.通过本章学习,掌握队列的定义及队列的存储结构
2.掌握队列的基本操作运算:建队、插入、删除、队列空等,用数组、链接方式所建立队列及操作运算
3.掌握循环队列概念及运算
4.能够利用队列解决一些实际问题:广度优先搜索算法
[重点难点]
1.队列、循环队列概念及存储结构
2.队列的基本操作
3.综合运用队列结构解决实际问题
[内容讲授]
一、队列的基本知识
队列(Queue)是一种特殊的线性表。
它是一种运算受限的线性表。
它只允许在表的一端进行插入,而在另一端进行删除。
允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。
因此队列亦称作先进先出(First In First Out)的线性表,简称FIFO表。
1.队列的性质
假设队列为a1,a2,..,a n,那么a1就是队头元素,a n为队尾元素。
队列中的元素是按a1,a2,..,a n 的顺序进入的,退出队列也只能按照这个次序依次退出。
也就是说,只有在a1离开队列之后,a2才能退出队列,只有在a1,a2,..,a n-1都离开队列之后,a n才能退出队列。
图1是队列的示意图。
图1 队列的先进先出示意图
2.队列的存储结构
(1) 顺序存储:可用记录数组实现 (2) 链接存储:用链接存储方式实现
如图所示:
front rear A1
A2
A3
……
……
……
An-1
An
顺序存储结构——数组
3.基本术语:
(1) 队空:当队列中没有元素时称为空队列。
(2) 队满:当队列中单元全部被占用 (3) 队列操作规则:
在队列中依次加入元素a1,a2,…an 之后,a1是队头元素,an 是队尾元素。
其出队操作规定从队头进行,进队操作从队尾进行。
即队列的操作是依先进先出的原则进行的。
(4) 上溢、下溢:真溢、假溢
4.队列的基本操作
用顺序队列存储结构的表示方法: type queue=record
vec : array[1..m] of elemtype f, r : integer ; end;
f, r 分别指向队列的头和尾
(1)进队操作(插入运算) Procedure insert( q, x ) ; begin
① if q.r =m then 输出上溢 ② q.r := q.r+1
③ q.vec[q.r]:=x ; { 进入队尾 } ④ if q.f= 0 then q.f:=1
end;
(2)出队操作:删除操作 procedure dele (q , x) ; begin
① if q.f=0 then 输出下溢
链队示意图
… a 2 rear
front
a 1 ∧
②x=q.vec[q.f]
③if q.f=q.r then [ q.f=0; q.r =0 ]
else q.f:=q.f + 1
end;
(3)置空队算法
procedure setnull (Q) ;
begin
q.f:=0 ; q.r:=0 ;
end;
5. 循环队列
为充分利用向量空间,克服"假上溢"现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。
存储在其中的队列称为循环队列(Circular Queue)。
(1)定义:将队列的首、尾连接起来,形成一个环状。
队尾指向m ,队首指向1。
对循环队列的操作:
(2)插入操作:
procedure insert2(q.x);
begin
if (q.r mod m)+1 =q.f then 溢出
else q.r:= [ (q.r mod m)+1 ;q.vec[q.r]:=x ]
end;
(3) 删除操作:
procedure delete2( q, x ) ;
begin
if q.f=q.r then 输出队空
else [ q.f = ( q.f mod m ) +1 ;x=q.vec[q.f ] ]
end;
(4) 循环队列的长度:
( r- f+ n ) mod n
6. 链队列
链队是指队列的链接存储表示,也就是说它只允许在表尾进行插入和在表头进行删除的单链表。
一个链队需要队首、队尾两个指针,一个指向表头,一个指向表尾,如下图所示:
设有如下的数据类型定义:
type linklist= ^dynanode ;
dynanode = record
data : elemtype ;
next : linklist ;
end;
type linkqueue=record
f, r : linklist ;
end;
链接队列的操作运算如下:
(1)插入算法
procedure insert ( HQ,x) ;
begin
●new(p) ; p^.data:=x ; p^.next := nil ;
●if HQ.r = nil then [ HQ.f := p; HQ.r :=p ]
else [ HQ.r^.next := p ; HQ.r := p ]
end;
(2)删除算法
procedure delete (HQ, x ) ;
begin
●if HQ.f= nil then error ( ‘ underflow ‘); { 队为空}
●x:=HQ.f ^.data ;
●p:= HQ.f ;
●if HQ.f = HQ.r then [ HQ.f:= nil ; HQ.r := nil ] { 删除结点}
else HQ.f := HQ.f ^ .next ;
●dispose (p) ;
end;
二、队列的应用
队列在日常生活中应用很多,特别是在计算机科学领域中所起的作用很大。
例如在解决主机与外部设备之间速度不匹配问题,解决多用户引起的资源竞争问题等,都运用了队列这样的数据结构算法,下面通过一些实例,了解运用队列解决问题方法。
运用队列解决广度优先搜索算法中的最短路径问题是一种比较好的算法。
例题1. 1995年高中组基础题第4 题,从入口(1)到出口(17)的可行路线图中,数字标号表示关卡:
现将上面的路线图,按记录结构存储如下 :
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
请设计一种能从存储数据中求出从入口到出口经过最少关卡路径的算法。
(1)该题是一个路径搜索问题,根据图示,从入口(1)到出口(17)可以有多种途径,其中最短的路径只有一条,那么如何找最短路径是问题的关键。
(2)根据题意,用一维数组存储各关卡号(设NO ),用另一个数组存储访问到某关卡号的前趋关卡号(设PRE )。
(3)由于本题是一个典型的图的遍历问题,此题可以采用图的广度优先遍历算法,并利用队列的方式存储顶点之间的联系。
即访问一个点,将其后继结点入队,并存储它的前趋结点,直到最后从(17)点出口。
(4)从最后出口的关卡号(17)开始回访它的前趋关卡号,则回返的搜索路径便是最短路径。
(跳过许多不必要搜索的关卡)。
(5)从列表中可以看出出口关卡号(17)的被访问路径最短的是:
(17)← (16)←(19)←(18)←(1)←开始
由此,我们得到广度优先遍历求最短路径的基本方法:访问一个点,将其后继结点入队,并存储它的前趋结点,直到所需要到达的地,然后通过最终目标点的结点的前驱记录,返回搜索最短路径的轨迹。
例题2:如下图表示的是从城市A 到城市H 的交通图。
从图中可以看出,从城市A 到城市H 要经过若干个城市。
现要找出一条经过城市最少的一条路线。
1 2 18 7 3 12 4 19 8 5 13 16 6 14 15 9 17 0
1
1
1
2
2
2
3
4
5
6
8
10
11
11
11
12
城市交通图矩阵存储结构
算法如下:
用队列的方法。
用a记录搜索过程,a.city记录经过的城市,a.pre记录前趋元素,这样就可以倒推出最短线路。
具体过程如下:
(1)将城市A入队,队首、队尾都为1。
(2)将队首所指的城市所有可直通的城市入队(如果这个城市在队中出现过就不入队,可用一个集合来判断),将入队城市的pre指向队首的位置。
然后将队首加1,得到新的队首城市。
重复以上步骤,直到城市H入队为止。
当搜到城市H时,搜索结束。
利用pre可倒推出最少城市线路。
程序如下:
program exp_2 ;
const ju: array[1..8,1..8] of 0..1=((1,0,0,0,1,0,1,1),
(0,1,1,1,1,0,1,1),
(0,1,1,0,0,1,1,1),
(0,1,0,1,1,1,0,1),
(1,1,0,1,1,1,0,0),
(0,0,1,1,1,1,1,0),
(1,1,1,0,0,1,1,0),
(1,1,1,1,0,0,0,1));
type R=record {记录定义}
city:array[1..100] of char;
pre:array[1..100] of integer;
end;
var h, d, i:integer;
a: R;
s: set of 'A'..'H';
procedure out; {输出过程}
begin
write(a.city[d]);
repeat
d:= a.pre[d];
write ('--',a.city[d]);
until a.pre[d]=0;
writeln;
halt;
end;
procedure doit;
begin
h:=0; d:=1;
a.city[1]:='A';
a.pre[1]:=0;
s:=['A'];
repeat {步骤2}
inc(h); {队首加一,出队}
for i:=1 to 8 do {搜索直通的城市}
if (ju [ord(a.city[h])-64, I ]=0)and (not(chr(i+64)in s))then { 判断城市是否走过} begin
inc(d); {队尾加一,入队}
a.city[d]:=chr(64+i);
a.pre[d]:=h;
s:=s+[a.city[d]];
if a.city[d]='H' then out;
end;
until h=d;
end;
begin {主程序}
doit;
end.
输出:H-F--A
例题3 迷宫问题:设有一个n*n方格的迷宫,入口和出口分别在左上角和右下角,如图所示,其走路规则是:在任何一个格子中,可以向8个方向前进,格子中0表示可以走,1表示不通,当迷宫给定后,找出一条从入口到出口的通路。
入口 出口
迷宫图 搜索的8个方向
算法分析: (1)
本题采用回溯算法,应用队列存放走过的路径:即先进先出,后进后出原理,输出走迷宫的路径。
A[I,J]:= 0 表示可以走了
1 表示不可以走
(2)
超界条件为: x<=1,x>n ,y<1, y>n , a[x,y]=1 ,入口处条件:x=1 ,y=1, x=n , y=1
(3) 增量和文字的方向:用数组表示 程序如下: program exp_3 ;
var A: array [1..20 , 1..20 ] of 0…1 ;
c: array [1..20 , 1..20 ] of 0…1 ; b : array [o ..400 ] of integer ; dx,dy : array [1..20 , 1..20 ] of 0…1 ; n, m, k,I,x,y :integer ; begin
write (‘n= ‘) ; readln (n) ;
for y:=1 to n do begin { 初始化程序段 } for x:=1 to n do begin
read(a[x.y]) ;c [ x,y] :=o ;
end;
dx[1]:=1; dy[1]:=-1 ; dx[2]:=1; dy[2]:=0 ;
dx[3]: =-1 ; dy[3]:= 1; dx[4]:= -1 ; dy[4]:=0 ;
0 0 0 1 1 0 1 0 1 0 1 1 0 1 1 0 0
1 0 0 1 0 0 1 0 0 1 1 0 1 0 1 0 1 0 0 0 1 1 0 0 1 1 1 1 1 0 1 0 0 1 1 1 0 1 1 1
1
dx[5]: = 1; dy[5]:= 1; dx[6] : = -1 ; dy[6]: = -1 ;
dx[7]:=0 ; dy[7]:= 1 ; dx[8]:= 0 ; dy[8]:= -1 ;
x:=1 ; y:=1 ; m:=0 ; k:=0 ;
for I:= 0 to 400 do b[I]:=0 ;
while (x<> n) or (y<>1 ) do { 按八个方向搜索}
begin
k:=k+1 ;
if k> 8 then begin { 八个方向均搜索后,无法前进}
k:=b[m] ; m:=m-1 ; {退回到上一步}
x: = x- dx[k] ; { 清当前所走的记录内容}
y:= y - dy[k] ;
end
else begin { 可以向前走一步}
x:=x+dx[k] ;
y:=y+dy[k] ;
if ( x<1) or (x>n) or (y<1) or (y>n) or ( a[x,y]=1 ) or ( c[x,y]=1 ) then
begin
x:=x-dx[k] ; y: = y- dy[k] ; { 超界或已经访问过,回退}
end
else
begin
m:= m+1 ; c[x,y] := 1 ; b[m]:= k ; k:=0 ; { 进队,记录所访问的格子} end;
end ;
end ;
x:=1 ; y:=1 ;
write (‘( ‘, x , ‘, ‘ , y , ‘ ) ‘) ;
for I:= 1 to m do
begin
x:=x+ dx[b[I]]; y:=y+ dy[b[I]] ;
write(‘-( ‘, x , ‘, ‘ , y , ‘) ‘ ) ;
end ;
writeln ;
end.
程序运行结果:(1 , 1 ) ——(2,1)——(3,1)——(2,2)——(1,3)——(2,4)——(3,3)——(4,3)——(5,2)——(6,3)——(7,3)——(8,2)—(8,1)例题4 有10升油在10升的容器中,另有两个7升和3升的空容器,现要求用这三个容器倒油,使得最后在10升和7升的容器中各有5升油。
提示:三个容器可以看作三个变量C10,C7,C3,每次倒油的可能性只有如下六种情况:
①C10 向C7 倒油②C10 向C3 倒油
③C7 向C10 倒油④C7 向C3 倒油
⑤C3 向C10 倒油⑥C3 向C7 倒油
算法分析:
(1)从一个容器的状态(三个容器中油的容量)看,虽然有可能经过上述六种倒油的方法产生六种容器状态,但实际上这六种新产生的容器状态,许多是已经出现过的状态。
例如初始状态(10,0,0)表示C10=10,C7=0,C3=0,经过上述六种倒油方法只能产生出两种新的容器状态(3,7,0),表示C10向C7倒油的结果和(7,0,3),表示C10向C3倒油的结果。
如果再增加应该表示新容器状态是由什么状态产生的指示pre,那么用这三个容器倒油的过程就可以用队列的技法来实现了
(2)队列可以理解为一个数组,数组元素是如下记录:
RECORD
C10,C7,C3, pre: integer;
END;
数组下标为容器状态号。
下面是倒油过程的队列图示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 C10 10 3 7 0 3 7 6 4 6 4 9 1 9 1 2 8 2 8 5 C7 0 7 0 7 4 3 4 3 1 6 1 6 0 7 7 0 5 2 5 C3 0 0 3 3 3 0 0 3 3 0 0 3 1 2 1 2 3 0 0 pre 0 1 1 2 2 3 5 6 7 8 9 10 11 12 13 14 15 16 17 当倒油产生出第19个容器状态时已达到了题解的目的。
这时只要根据pre中的状态号17可以回溯到第17个容器状态的pre值为15,依次可再获得13,11,9,7,5,2,1容器状态号,从而即可得到本题的倒油过程(共倒9次),而且是最少的倒油次数。
注意,从一个容器中向另一个容器中倒油,人操作是很直观的,对编程来说则必须考虑:
1) 有没有油可倒?
2) 究竟倒多少?可能要全部倒入另一容器,也可能只要倒一部分另一容器已经满了.
队列元素说明了100个,是因为10升容器最多有0~10种状态,7升容器最多有0~7种状态,3升容器最多有0~3种状态,全部可能的状态为11*8*4=352种,但油的总量为10,因此352种可能状态中大部分是不存在的。
变量fp,rp在程序中用作队列的头指针和尾指针,flag在程序中标识是否已倒出了需要的容器状态(C10=5,C7=5),下面是程序清单:
program exp_4 ;
var fp, rp, i, k, w10, w7, w3:integer;
flag: boolean;
s:array[1..100] of 1..100;
q:array[1..100] of record
c10,c7,c3:integer;
pre:0..100
end;
procedure int;
begin
w10:=q[fp].c10;
w7:=q[fp].c7;
w3:=q[fp].c3;
end;
procedure push; { 进队操作}
var flag1 :boolean;
begin
flag1:=true;
for k:=1 to rp do
if (q[k].c10=w10) and (q[k].c7=w7) and (q[k].c3=w3) then flag1:=false;
if flag1 then begin
rp:=rp+1;
q[rp].c10:=w10;
q[rp].c7:=w7;
q[rp].c3:=w3;
end;
end;
PROCEDURE cup; { 倒油的操作过程}
BEGIN
int;
IF w10>0 THEN BEGIN { 将10升油桶中的油倒入7升油桶} IF w10+w7>=7 THEN BEGIN w10:=w10-7+w7; w7:=7;END ELSE BEGIN w7:=w10+w7; w10:=0;END;
push;
IF (q[rp].c10=5) and (q[rp].c7=5) THEN { 判断是否满足问题的解} BEGIN flag:=true; exit ;END
END;
int;
if w10>0 then begin { 将10升油桶向3升油桶倒油}
if w10+w3>=3 then begin w10:=w10-3+w3; w3:=3 ;end
else begin w3:=w10+w3; w10:=0 ; end;
push;
if (q[rp].c10=5) and (q[rp].c7=5 ) then begin flag:=true; exit ;end end; { 判断是否满足问题求解}
int;
IF w7>0 THEN BEGIN w10:=w10+w7; w7:=0; push; { 将7升倒入10升油桶} IF (q[rp].c10=5) and (q[rp].c7=5 THEN BEGIN flag:=true; exit ; END ;
END; { 判断是否满足问题求解}
int;
IF w7>0 THEN BEGIN { 将7升倒入3升油桶}
IF w7+w3>=3 THEN BEGIN w7:=w7-3+w3; w3:=3;END
ELSE BEGIN w3:=w7+w3; w7:=0; END;
push;
IF (q[rp].c10=5) and (q[rp].c7=5 THEN
BEGIN flag:=true; exit ; END ; { 判断是否满足问题求解} END; { 以下同理}
int;
IF w3>0 THEN BEGIN w10:=w10+w3; w3:=0; push;
IF (q[rp].c10=5) and (q[rp].c7=5 THEN BEGIN flag:=true; exit ; END
END;
int;
IF w3>0 THEN BEGIN
IF w7+w3>=7 THEN BEGIN w3:=w3-7+w7; w7:=7 END
ELSE BEGIN w7:=w7+w3; w3:=0 ; END;
push;
IF (q[rp].c10=5) and (q[rp].c7=5 ) THEN BEGIN flag:=true; exit ;END
END;
END;
BEGIN { 主程序}
fp:=1;
rp:=1;
q[1].c10:=10; { 初始化}
q[1].c7:=0;
q[1].c3:=0;
q[1].pre:=0;
flag:=false;
repeat
cup;
fp:=fp+1;
until flag or (fp>rp); { 反复倒油,直到完成倒油过程或队列中所有元素都访问过}
if fp>rp then write ('have not solution')
else
begin
writeln('queue');
for k:=1 to rp do { 输出进队过程}
writeln (q[k].c10:3,q[k].c7:3,q[k].c3:3,q[k].pre:3);
s[1]:=rp;
i:=1;
while s[i]>0 do { 用S数组存放最少倒油过程的路径}
begin
i:=i+1; s[i]:=q[s[i-1]].pre
end;
writeln ('solution:');
for k:=i-1 downto 1 do { 输出倒油步骤}
writeln('step',i-1-k;2,':',q[s[k]].c10:3,q[s[k]].c7:3,q[s[k]].c3:3) ;
end;
end. {主程序结束}
说明:例题4比较难和繁,将该程序介绍给大家的目的在于,供老师们辅导学生竞赛时参考,希望您的学生能够用队列的特点、记录数组的结构,灵活地解决问题。
参考练习:
1.上机调试完成例题1最短路径问题
2.上机调试完成例题2迷宫问题
3.假设在周末舞会上,男士们和女士们进入舞厅时,各自排成一队。
跳舞开始时,依次从男队和女队的队头上各出一人配成舞伴。
若两队初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲。
现要求写一算法模拟上述舞伴配对问题。
4.设有数2,3,5,7,13,运算符号为+,—,* ,且运算符无优先级之分,如2+3*5=25,3*5+2=17。
现给出任意一个整数n,要求用以上的数和运算符,以最少的运算次数产生出。