您现在的位置是: 首页 -成长专区 >>精选奥赛
趣谈数据结构
2008-10-6 11:13:07 来源:http://www.qderzhong.net/homepage/xinxijishu/aosai
 

趣谈数据结构
福州一中 陈颖

  ◇青岛二中信息技术组◇  

 

趣谈数据结构(一)

说到学习和掌握数据结构,很容易让人想到的就是其最本的数据结构模式:栈、队、链。
这一讲,我们就来谈谈“栈”。
“栈”的应用很广泛,大家在PASCAL程序设计中,常遇的一种错误就是“栈”超界,那么,“栈”为何物呢?
   栈是只能在某一端插入和删除的特殊线性表。
   用桶堆积物品,先堆进来的压在底下,随后一件一件往堆。取走时,只能从上面一件一件取。堆和取都在顶部进行,底部一般是不动的。
   栈就是一种类似桶堆积物品的数据结构,进行删除和插入的一端称栈顶,另一堆称栈底。插入一般称为进栈(PUSH),删除则称为退栈(POP)。 栈也称为后进先出表(LIFO表)。
   一个栈可以用定长为N的数组S来表示,用一个栈指针TOP指向栈顶。若TOP=0,表示栈空,TOP=N时栈满。进栈时TOP加1。退栈时TOP减1。当TOP<0时为下溢。栈指针在运算中永远指向栈顶(如图11.6所示)。


x x



top
top
top



原来状态 X进栈 TOP=TOP+1 退栈送X TOP=TOP- 1
      表示处理前的栈指针
      表示处理后的栈指针
   图11.6 栈
   1、进栈(PUSH)算法
   ①若TOP≥n时,则给出溢出信息,作出错处理(进栈前首先检查栈是否已满,满则溢出;不满则作②);
   ②置TOP=TOP+1(栈指针加1,指向进栈地址);
   ③S(TOP)=X,结束(X为新进栈的元素);
   (2)退栈(POP)算法
   ①若TOP≤0,则给出下溢信息,作出错处理(退栈前先检查是否已为空栈, 空则下溢;不空则作②);
   ②X=S(SOP),(退栈后的元素赋给X);
   ③TOP=TOP-1,结束(栈指针减1,指向栈顶)。
   进栈、出栈的Pascal实现过程程序:
CONST
n=100;
TYPE
stack=ARRAY[1..n] OF integer;
PROCEDURE PUSH(VAR s:stack;VAR top,x:integer);{入栈}
BEGIN
IF top=n THEN writeln('overflow')
ELSE
BEGIN
top:=top+1;s[top]:=x;
END
END;
PROCEDURE POP(VAR s:stack;VAR y,top:integer);{出栈}
BEGIN
IF top=0 THEN writeln('underflow')
ELSE
BEGIN
y:=s[top];top:=top-1;
END
END;
   对于出栈运算中的“下溢”,程序中仅给出了一个标志信息,而在实际应用中,下溢可用来作为控制程序转移的判断标志,是十分有用的。对于入栈运算中的“上溢”,则是一种致命的错误,将使程序无法继续运行,所以要设法避免。
   栈的用途极为广泛,在源程序编译中表达式的计算、过程的嵌套调用和递归调用等都要用到栈,下面以表达式计算为例子加以说明。
   源程序编译中,若要把一个含有表达式的赋值语句翻译成正确求值的机器语言,首先应正确地解释表达式。例如,对赋值语句
   X:=4+8×2-3; (式 11.1)
其正确的计算结果应该是17,但若在编译程序中简单地按自左向右扫描的原则进行计算,则为
   X=12×2-3=24-3=21
这结果显然是错误的。因此,为了使编译程序能够正确地求值,必须事先规定求值的顺序和规则。通常采用运算符优先数法。
   一般表达式中会遇到操作数、运算符和语句结束符等,以算术运算符为例,对每种运算赋予一个优先数,如:
   运算符:× ÷ + -
   优先数:2 2 1 1
   (语句结束符“;”的优先数为零)
   在运算过程中,优先数高的运算符应先进行运算(但遇到括号时,应另作处理)。按这样的规定,对式(11.1)自左向右进行运算时,其计算顺序就被唯一地确定下来了。计算顺序确定后,在对表达式进行编译时,一般设立两个栈,一个称为运算符栈(OPS),另一个称为操作数栈(OVS),以便分别存放表达式中的运算符和操作数。编译程序自左向右扫描表达式直至语句结束,其处理原则是:
   ①凡遇到操作数,一律进入操作数栈;
   ②当遇到运算符时,则将运算符的优先数与运算符栈中的栈顶元素的优先数相比较;若该运算符的优先数大,则进栈;反之,则取出栈顶的运算符,并在操作数栈中连续取出两个栈顶元素作为运算对象进行运算,并将运算结果存入操作数栈,然后继续比较该运算符与栈顶元素的优先数。
   例如式(11.1)中,当扫描到“+”和“×”时都要将运算符入栈。扫描到操作数“2”时,两个栈的情况如图11.7(a)所示。接着扫描到“-”号, 其优先数小于乘号所以乘号退栈,并执行8×2,将结果16再存入操作数栈,如图11.7(b)所示。再将“-”号的优先数与运算符栈的栈顶元素“+”号的优先数相比较,两者相等,所以再将加号退栈,进行4+16,结果为20,再入栈,如11.7(c) 所示,接着,由于运算栈已空,所以减号入栈。当扫描到“3”时,操作数入栈,如图11.7(d)所示。当扫描到“;”时,其优先数最低, 所以减号退栈并执行20-3,结果为17并入栈。因已扫描到语句结束符,所以表达式的求值结束,结果为17,如图11.7(e)所示。


(a) (b) (c) (d) (e)
图11.7 编译式(11.1)时栈的变化情况
例11.4 模拟计算机处理算术表达式过程。从键盘上输入算术表达式串(只含+、-、×、÷运算符,充许含括号),输出算术表达式的值。设输入的表达式串是合法的。
   分析
   建立两个栈,一个是操作数栈(number),一个是运算符栈(symbol),根据运算符的优先级对两个栈进行相应的操作。
源程序
program ex11_4;
const
max=100;
var
number:array[0..max] of integer;
symbol:array[1..max] of char;
s,t:string;
i,p,j,code:integer;
procedure push;{算符入栈运算}
begin
inc(p);symbol[p]:=s[i];
end;
procedure pop;{运算符栈顶元素出栈,并取出操作数栈元素完成相应的运算}
begin
dec(p);
case symbol[p+1] of
'+':inc(number[p],number[p+1]);
'-':dec(number[p],number[p+1]);
'*':number[p]:=number[p]*number[p+1];
'/':number[p]:=number[p] div number[p+1];
end;
end;
function can:boolean;{判断运算符的优先级别,建立标志函数}
begin
can:=true;
if (s[i] in ['+','-']) and (symbol[p]<>'(') then exit;
if (s[i] in ['*','/']) and (symbol[p] in ['*','/']) then exit;
can:=false;
end;
begin
write('String : '); readln(s); s:='('+s+')'; i:=1; p:=0;
while i<=length(s) do
begin
while s[i]='(' do {左括号处理]
begin
push; inc(i);
end;
j:=i;
repeat {取数入操作数栈}
inc(i);
until (s[i]<'0') or (s[i]>'9');
t:=copy(s,j,i-j); val(t,number[p],code);
repeat
if s[i]=')' then {右括号处理}
begin
while symbol[p]<>'(' do pop;
dec(p); number[p]:=number[p+1];
end
else
begin {根据标志函数值作运算符入栈或出栈运算处理}
while can do pop;
push;
end;
inc(i);
until (i>length(s)) or (s[i-1]<>')');
end;
write('Result=',number[0]);
readln;
end.

趣谈数据结构(二)

   在趣谈数据结构(一)中,我们谈了"栈"的应用,下面我给出一道问题,大家看看能否用栈的知识来解决?
问题:化学中原子的表示是由一个大写字母或一个大写字母加一个小写字母来表示的。原子团是由原子或更小的原子团参与构成的,原子团的表达式是由左、右圆括号括起来的,NH4原子团的表达式为(NH4)。但当原子团只出现一次,则原子团表达式两端的圆括号可以省略不写。分子是由原子组成,分子式是原子式的组合。例如NH4和co3是两种不同的原子团,碳酸铵分子中有两个NH4原子团和一个co3原子团,故该分子的分子式可以书写成两种形式:(NH4)2CO3、(NH4)2(CO3)。

   分子量就是分子的质量,是构成该分子的所有的原子(包括构成该分子的原子团中的各个原子)的质量总和。 例如N的原子量为14,H的原子量为1,C的原子量为12,O的原子量16,因此(NH4)2CO3的分子量为(14+l*4)*2+12+16*3=96。

   现已知各原子的原子量,求出给定的各个分子式所对应的分子量。

  这一讲,我们就来谈谈"栈"。

   队列是限定在一端进行插入,另一端进行删除和特殊线性表。正象排列买东西,排在前面的人买完东西后离开队伍(删除),而后来的人总是排在队伍未尾(插入)。通常把队列的删除和插入分别称为出队和入队。允许出队的一端称为队头,允许入队的一端称为队尾。所有需要进队的数据项,只能从队尾进入,队列中的数据项只能从队头离去。由于总是先入队的元素先出队(先排队的人先买完东西),这种表也称为先进先表(FIFO)表。

   队列可以用数组Q[1…m]来存储,数组的上界m即是队列所容许的最大容量。在队列的运算中需设两个指针:

   head:队头指针,指向实际队头元素的前一个位置
   tall:队尾指针,指向实际队尾元素所在的位置

  一般情况下,两个指针的初值设为0,这时队列为空,没有元素。图1 ( a)画出了一个由6个元素构成的队列,数组定义Q[1…10]。
   Q(i) i=3,4,5,6,7,8
头指针head=2,尾指针tail=8。队列中拥有的元素个数为:L=tail-head

   现要让排头的元素出队,则需将头指针加1。即head=head+1
这时头指针向上移动一个位置,指向Q(3),表示Q(3)已出队。见图1 (b)。

   如果想让一个新元素入队,则需尾指针向上移动一个位置。即tail=tail+1
这时Q(9)入队,见图1 (c)。

当队尾已经处理在最上面时,即tail=10,见图1 (d),如果还要执行入队操作,则要发生"上溢",但实际上队列中还有三个空位置,所以这种溢出称为"假溢出"。



  克服假溢出的方法有两种。一种是将队列中的所有元素均向低地址区移动,显然这种方法是很浪费时间的;另一种方法是将数组存储区看成是一个首尾相接的环形区域。当存放到n地址后,下一个地址就"翻转"为1。在结构上采用这种技巧来存储的队列称为循环队列,见图2




   循环队的入队算法如下:
   1、tail=tail+1;
   2、若tail=n+1,则tail=1;
   3、若head=tail尾指针与头指针重合了,表示元素已装满队列, 则作上溢出错处理;
   4、否则,Q(tail)=X,结束(X为新入出元素)。

   队列和栈一样,有着非常广泛的应用。

   考虑一个分时系统,如果一台计算机联有四个终端,即允许四个用户同时使用这一台计算机。那么, 计算机系统必须设立一个队列, 用以管理各终端用户使用CPU的请求。当某个用户要求使用CPU时,相应的终端代号就入队(插入队尾),而队头的终端用户则是CPU当前服务的对象。我们考虑最简单的情况, 对于当前用户(队头),系统每次分配一个为时间片的时间间隔,在一个时间片内,如果当前用户的作业没有结束,则该终端用户的代号出队后重新入队,插入队尾,等待下一次CPU服务。如果某个用户的作业运行结束,则先退出,出队后不再入队, 整个运行过程就是各终端代号不断地入队、出队,CPU 轮流地为n(n≤4)个终端用户服务。由于计算机的运行速度极快,所以,对于每个终端用户来说,似乎计算机是单独在为其服务。
和线性表一样,栈和队可以采用链表存储结构,当要实现多个栈共享内存或多个队共享内存时,选择链式分配结构则更为合适。

例1 求两个一元多项式的和。输入多项式方式为,多项式项数,每项系数和指数,按指数从大到小的顺序输入。

分析

   多项式的算术运算是表处理的一个经典问题。建立两张表a、b分别存放两个多项式的内容,建立表指针ta、tb,指向表a和表b的元素,根据表a、b元素中的指数大小合并输出。

   1、比较ta、tb指向元素的大小,若ta的指数大于tb的指数,输出ta元素,改变指针ta;
   2、若ta的指数小于tb的指数,输出tb元素,改变指针tb;
   3、若ta的指数等于tb的指数,ta、tb元素的系数相加输出,同时改变指针ta和tb;
   4、若有一表取空,则输出另一表剩余的内容。

源程序一:多项式相加的顺序表实现

program ex11_5a;
type
node=record
zhi,xi:integer;
end;
ar=array[1..1000] of node;
var
a,b:ar;
ta,tb,n:integer;
begin
write('One : '); readln(n);{输入第一个多项式的系数和指数}
for ta:=n downto 1 do readln(a[ta].xi,a[ta].zhi);
ta:=n;
write('Two : '); readln(n);{输入第二个多项式的系数和指数}
for tb:=n downto 1 do readln(b[tb].xi,b[tb].zhi);
tb:=n;
write('Result is ');
while (ta>0) and (tb>0) do {当两个表均不空时}
begin {比较两表指针指向的项指数,输出指数小的项系数和指数, 同时改变该表指针}
if a[ta].zhi>b[tb].zhi then
begin
if a[ta].xi<0 then write(#8' '#8);
write(a[ta].xi,'x',a[ta].zhi,'+');
dec(ta);
end
else
if a[ta].zhi<b[tb].zhi then
begin
if b[tb].xi<0 then write(#8' '#8);
write(b[tb].xi,'x',b[tb].zhi,'+');
dec(tb);
end
else
begin {若两表指针指向的项指数相等,则两系数相加输出, 两表指针同时改变}
if b[tb].xi+a[ta].xi<>0 then
begin
if b[tb].xi+a[ta].xi<0 then write(#8' '#8);
write(b[tb].xi+a[ta].xi,'x',b[tb].zhi,'+');
end;
dec(ta);
dec(tb);
end;
end;
while ta>0 do {若有一表空,则输出另一表的剩余项}
begin
if a[ta].xi<0 then write(#8' '#8);
write(a[ta].xi,'x',a[ta].zhi,'+');
dec(ta);
end;
while tb>0 do
begin
if b[tb].xi<0 then write(#8' '#8);
write(b[tb].xi,'x',b[tb].zhi,'+');
dec(tb);
end;
writeln(#8' '#8);
readln;
end.

源程序二:多项式相加的链表实现

program ex11_5b;
type
link=^node;
node=record
zhi,xi:integer;
nxt:link;
end;
var
a,b:link;
n:integer;
procedure createfifo(var c:link);{建立多项式系数、指数链表}
var
p:link;
i:integer;
begin
new(p);
readln(p^.xi,p^.zhi);
c:=p;
for i:=1 to n-1 do
begin
new(p^.nxt);p:=p^.nxt;readln(p^.xi,p^.zhi);
end;
p^.nxt:=nil;
end;
begin
write('One : '); readln(n); createfifo(a);
write('Two : '); readln(n); createfifo(b);
write('Result is ');
while (a<>nil) and (b<>nil) do
begin
if a^.zhi>b^.zhi then
begin
if a^.xi<0 then write(#8' '#8);
write(a^.xi,'x',a^.zhi,'+');
a:=a^.nxt;
end
else
if a^.zhi<b^.zhi then
begin
if b^.xi<0 then write(#8' '#8);
write(b^.xi,'x',b^.zhi,'+');
b:=b^.nxt;
end
else
begin
if b^.xi+a^.xi<>0 then
begin
if b^.xi+a^.xi<0 then write(#8' '#8);
write(b^.xi+a^.xi,'x',b^.zhi,'+');
end;
b:=b^.nxt;
a:=a^.nxt;
end;
end;
while a<>nil do
begin
if a^.xi<0 then write(#8' '#8);
write(a^.xi,'x',a^.zhi,'+');
a:=a^.nxt;
end;
while b<>nil do
begin
if b^.xi<0 then write(#8' '#8);
write(b^.xi,'x',b^.zhi,'+');
b:=b^.nxt;
end;
writeln(#8' '#8);
readln;
end.

趣谈数据结构(三)

趣谈数据结构(二)中,我们介绍了"队"及"队"的应用,在这一讲中,我们将再通过两道例题,进一步的了解的学习"队"的灵活使用。

例2、
   设有n个人依次围成一圈,从第1个人开始报数,数到第m个人出列,然后从出列的下一个人开始报数,数到第m个人又出列,…,如此反复到所有的人全部出列为止。设n个人的编号分别为1,2,…,n,打印出出列的顺序。

分析 
   本题我们可以用数组建立标志位等方法求解,但如果用上数据结构中循环链的思想,则更贴切题意,解题效率更高。
   n人围成一圈,把一人看成一个结点,n人之间的关系采用链接方式,即每一结点有一个前继结点和一个后继结点,每一个结点有一个指针指向下一个结点,最后一个结点指针指向第一个结点。这就是单循环链的数据结构。
   当m人出列时,将m结点的前继结点指针指向m结点的后继结点指针,即把m结点驱出循环链。

1、 建立循环链表。
   当用数组实现本题链式结构时,数组a[i]作为"指针"变量来使用,a[i]存放下一个结点的位置。设立指针j指向当前结点,则移动结点过程为j:=a[j],当数到m时,m结点出链,则a[j]:=a[a[j]]。
   当直接用链来实现时,则比较直观,每个结点有两个域:一个数值域,一个指针域,当数到m时,m出链,将m结点的前继结点指针指向其后继结点;
2、 设立指针,指向当前结点,设立计数器,计数数到多少人;
3、 沿链移动指针,每移动一个结点,计数器值加1,当计数器值为m时,则m结点出链。计数器值置为1;
4、 重复3、直到n个结点均出链为止。

源程序一: 用数组实现链式结构
program ex11-6a;
  const n=14;m=4;{设有10个人,报到4的人出列}
  var a:array[1..n] of integer;
   i,j,k,p:integer;
  begin
   for i:=1 to n-1 do a[i]:=i+1;{建立链表}
   a[n]:=1;j:=n;k:=1;p:=0;{第n人指向第1人,并置初始}
   repeat
    j:=a[j];k:=k+1;{报数,计数器加1}
    if k=m then {数到m,m人出队,计数器置1}
     begin
      write(a[j]:4);p:=p+1;a[j]:=a[a[j]];k:=1;
     end
    until p=n;{直到n个人均出队为止}
  end.

源程序二: 单链环实现
program ex11-6b;
  type pointer=^node;
   node=record
    val:integer;
    link:pointer
    end;
  var ptr,ptb:pointer;
   i,j,n,m:integer;
  begin
   readln(n,m);
   new(ptb);ptb^.val:=1;ptr:=ptb;{申请第1个结点}
   for i:=2 to n do
    begin
     new(ptr^.link);ptr:=ptr^.link; {申请第2到n结点}
     ptr^.val:=i;
    end;
   ptr^.link:=ptb;j:=0;{第n结点指针指向第1个结点}
   repeat
    i:=1;
    repeat {报数,报到m人出列}
     ptr:=ptb;ptb:=ptb^.link;i:=i+1;
    until i=m;
    write(ptb^.val:2);
    ptb:=ptb^.link;ptr^.link:=ptb;j:=j+1;{将m结点驱出链表}
   until j=n-1;{直到n个人均出队为止}
   writeln(ptb^.val:4)
  end.

例3、
   一矩形阵列由数字0到9组成,数字1到9代表细胞,细胞的定义为沿细胞数字上下左右还是细胞数字则为同一细胞,求给定矩形阵列的细胞个数。
如:阵列 0234500067
1034560500
2045600671
0000000089
有4个细胞。

算法步骤:

1. 从文件中读入m*n矩阵阵列,将其转换为boolean矩阵存入bz数组中;
2. 沿bz数组矩阵从上到下,从左到右,找到遇到的第一个细胞;
3. 将细胞的位置入队h,并沿其上、下、左、右四个方向上的细胞位置入队,入队后的位置bz数组置为FLASE;
4. 将h队的队头出队,沿其上、下、左、右四个方向上的细胞位置入队,入队后的位置bz数组置为FLASE;
5. 重复4,直至h队空为止,则此时找出了一个细胞;
6. 重复2,直至矩阵找不到细胞;
7. 输出找到的细胞数。

program xibao;
const dx:array[1..4] of -1..1=(-1,0,1,0);
    dy:array[1..4] of -1..1=(0,1,0,-1);
var int:text;
   name,s:string;
   pic:array[1..50,1..79] of byte;
   bz:array[1..50,1..79] of boolean;
   m,n,i,j,num:integer;
   h:array[1..4000,1..2] of byte;
procedure doing(p,q:integer);
  var i,t,w,x,y:integer;
  begin
   inc(num);bz[p,q]:=false;
   t:=1;w:=1;h[1,1]:=p;h[1,2]:=q;{遇到的第一个细胞入队}
   repeat
    for i:=1 to 4 do{沿细胞的上下左右四个方向搜索细胞}
     begin 
      x:=h[t,1]+dx[i];y:=h[t,2]+dy[i];
      if (x>0) and (x<=m) and (y>0) and (y<=n) and bz[x,y]
       then begin inc(w);h[w,1]:=x;h[w,2]:=y;bz[x,y]:=false;end;{为细胞的入队}
      end;
     inc(t);{队头指针加1}
    until t>w;{直至队空为止}
   end;
  begin
   fillchar(bz,sizeof(bz),true);num:=0;
   write('input file:');readln(name);
   assign(int,name);reset(int);
   readln(int,m,n);
   for i:=1 to m do
    begin readln(int,s);
    for j:=1 to n do
     begin pic[i,j]:=ord(s[j])-ord('0');
      if pic[i,j]=0 then bz[i,j]:=false;
     end;
    end;
   close(int);
   for i:=1 to m do
    for j:=1 to n do if bz[i,j] then doing(i,j);{在矩阵中寻找细胞}
   writeln('NUMBER of cells=',num);readln;
  end.