指针和动态数据结构张文双.docx
《指针和动态数据结构张文双.docx》由会员分享,可在线阅读,更多相关《指针和动态数据结构张文双.docx(32页珍藏版)》请在冰豆网上搜索。
指针和动态数据结构张文双
第2章指针和动态数据结构
数据结构可分为静态数据结构和动态数据结构。
整型、实型、布尔型等各种简单类型数据和数组、记录、集合等各种结构类型数据都属于静态类型的数据,对应这些类型数据的变量一经定义,编译时系统就会给它们分配相应的内存空间。
也就是说各变量相应的内容空间在程序运行前就已经确定,在程序执行过程中不能加以改变,因而此种数据结构被称为静态数据结构。
但是,在实际的程序设计中,经常会遇到这种问题:
要用一种结构存储一组数据,却又不知道随着程序的执行而存入的这组数据的准确数目,若按照估计的数目用数组来存取这组数据的话,数目估计过大会浪费大量的存储空间,数目估计过小又会引发错误导致程序停止运行;再者,在程序运行中时常需要在结构的某个位置删除或插入一些数据,于是会造成大规模数据的频繁移动,加大了算法的复杂度,此时的静态数据结构已不可取,动态数据结构应运而生。
一个动态数据结构是元素(或称为结点,通常是记录)的汇集。
与数组不同,它不需要包括存储固定数目元素的存储空间,而是在程序执行时,根据程序的数据存储需要而扩充或缩减。
动态数据结构是非常灵活的。
整个结构中的一个一个数据,在物理上分别存储在许多不同的存储块里,这就可以很灵活地随时删除或加入新的数据,而不必事先知道结构的大小。
不仅如此,还通过某种联系把这些分散的数据组织起来成为一个逻辑上完整的结构。
这样,添加或删除数据时只需改变一下数据之间的联系就可以了,不会造成大量数据的频繁移动。
这比修改记录数组要更加方便和迅速。
2.1指针变量的定义及基本使用
2.1.1指针变量的定义
讨论动态类型数据是如何在程序的运行阶段动态地建立起来的,就要讨论与之有关的一种静态类型数据-指针。
在计算机中,我们用一系列特殊的整数给内存中的第一个存储单元编号,相对应某一存储单元的整数被称为该存储单元的“地址”。
存储单元中可以存放各种类型的数据,这些数据就是存储单元的“内容”。
一个存储单元的“内容”根据该存储单元的“地址”写入或读出。
针对一个存储单元而言,它的“地址”和“内容”是两个不同的概念,必须区分清楚。
另一方面,“地址”本身是一个特殊的整数,因此“地址”可以看成是一种特殊的“内容”,被存储到另一个存储单元中。
这样,两个存储单元之间就建立了一种联系。
图2-1解释了上述关系。
其中变量p是一个存储地址的变量,其自身的地址是5000;变量q是一个存储整数的变量,其地址是5020,可以将5020存储到p中,从而p和q就建立了关系。
5020相对于p而言是“内容”,相对于q而言则是“地址”。
为了清晰地表达p和q之间的关系,图2-1一般用图2-2代替,即在p中不再给出q的具体“地址”,而是从p的内部画一个箭头指向q,该箭头形状像一根针,所以又称为指针。
指针实际上是“地址”,相应地,存储指针的变量称为指针变量(如p),指针箭头所存储变量(如q)存储的数据的类型称为指针变量的基类型。
指针变量的基类型可以是除文件类型以外的其他数据类型。
指针属于静态的简单类型,但是和整型、实型、字符型这些简单类型不同。
首先整型、实型、字符型变量单元中存放的是相应类型的数据,而指针变量单元中存放的是某种类型变量单元的地址,通过这个地址可以找到这种类型的数据,所以称它是一个指针,而这种数据就是动态数据;其次整型、实型、字符型等类型都有标准标识符integer、real、char等,而指针没有相应的标准标识符。
这是因为在动态数据产生的过程中,我们关心的是指针指向一个什么类型的数据,即指针变量的基类型是什么。
因此,在pascal程序说明部分定义指针时必须给出该指针变量的基类型,即该指针标识何种类型的变量。
有两种定义方法。
方法一:
先进行指针类型标识符的定义,然后进行指针变量的定义。
形式如下:
Type
指针类型标识符=^基类型标识符;
Var
指针变量名:
指针类型标识符;
例如:
Type
Pointer=^integer;
Var
P1,p2:
pointer;
表明程序定义了一种指针类型pointer,该类型的变量用于存储整数型变量单元的地址,即该类型变量的内容是一个指针,指向一个整型变量;定义了两个pointer类型的变量p1和p2,它们的值都是某个存储单元的地址,而该存储单元恰好能存放一个整型数据。
方法二:
将类型说明和变量说明合并,即和其他类型变量一样,在var区直接定义。
形式如下:
Var
指针变量名:
^基类型标识符;
因此,上例也可以说明如下:
Var
P1,p2:
^integer;
两者效果是一样的。
2.1.2指针变量的使用
1.开辟动态存储单元
在pascal语言中,指针变量的值是通过系统自动分配的,可以通过调用标准过程new,开辟一个新的动态存储单元。
New过程的调用格式如下:
New(指针变量);
例如:
New(q);
功能:
系统将自动分配一个存放数据的存储单元,并将该存储单元的地址赋给指针变量q,此单元能存放的数据的类型正好是q的基类型,存储单元的大小由q的基类型决定。
说明:
这实际上是给指针变量赋初值的基本方法。
例如,设有下列变量定义语句:
Varq:
^integer;
这仅仅说明了q是一个指示整型存储单元的指针变量,但这个存储单元尚未开辟(分配),或者说在指针变量q中尚未有具体的值(某存储单元的首地址)。
当程序中执行了语句new(q)才在内存中开辟(分配)一个整型变量存储单元,并把这个单元的地址放在变量q中,如图2-3所示。
一个指针变量只能存放一个地址。
在同一时间内一个指针只能指向一个变量单元。
如程序再次执行new(q)语句,将在内存中开辟另外一个新的整型变量存储单元,并把新单元的地址放在q中,从而丢失了原存储单元的地址。
2.释放动态存储单元
为了节省内存空间,对于一些已经不使用的现有动态变量单元,应该通过标准过程dispose予以释放。
Dispose语句的一般格式:
dispose(指针变量);
功能:
释放指针所指向的存储单元,使指针变量的值无定义。
例如:
dispose(q);
系统收回指针变量q所指的内存单元,内存空间还给系统,这时指针变量q变成无确切指向。
3.指针变量的赋值和操作
在给一个指针变量赋以某存储单元的地址后,就可以使用这个存储单元了。
引用动态存储单元一般格式:
<指针变量>^
说明:
利用new过程可以将一个存储单元的地址赋给一个指针变量,这个地址值我们并不需要了解,我们真正关心的是该指针变量所指向的存储单元的内容。
假设有指针变量q,pascal用q^来表示指针变量q所指向的存储单元的内容。
对于q和q^我们都可以用赋值语句赋值,只是效果大不相同。
前者赋给的是地址值,可以改变q的指向;后者赋给的是数据内容,改变的是q所指向的存储单元的内容。
假设p和q都是指针变量,其基类型为整型,执行语句p:
=q;将q的值(q所指向存储单元的地址)赋给变量p,这样变量p和q都同时指向q所指向的存储单元,如图2-4所示。
执行语句p^:
=q^;将q所指向的存储单元的值存放到p所指向的存储单元中。
这样p和q虽然指向不同的存储单元,但两个存储单元的值是相同的,如图2-5所示。
有时并不需要指针变量指向任何存储单元,那么可用下面的赋值语句:
P:
=nil;
说明:
表示指针变量p为空,其中nil是turbopascal保留字,表示“空”,相当于c里面的null。
任何类型的指针变量都可以被赋值为nil。
例2-1输入两个整数,按从小到大打印出来。
分析:
不用指针类型可以很方便地编程,但为了示例指针的用法,我们利用指针类型,定义一个过程swap用以交换两个指针的值。
Programtaxis;
Typepointer=^integer;{程序定义了一种指针类型pointer,它的基类型为整型}
Varp,q:
pointer;
Procedureswap(varh,r:
pointer);
Varm:
pointeger;{指针变量m为用于交换指针变量h和r的值的中间变量}
Begin{交换h和r}
M:
=h;h:
=r;r:
=m;
End;
Begin
New(p);{申请新的存储单元,并把该存储单元的地址赋给指针变量p}
New(q);{申请新的存储单元,并把该存储单元的地址赋给指针变量q}
Writeln(‘input2data:
’);
Readln(p^,q^);
Ifp^>q^thenswap(p,q);
Writeln(‘output2data:
’,p^:
4,q^:
4);
End.
指针类型变量所指向的数据类型可以是整型、实型、字符型等简单类型,也可以是数组、集合、记录等结构类型。
推而广之就有了链表、树结构等的产生及其应用。
2.2链表
设有一批整数(66,92,49,86,75,….),如何存放呢?
当然我们可以选择以前学过的数组类型。
但是,如果事先不能确定整数的个数,就要定义一个足够大的数组。
用这种方法处理问题缺乏灵活性,往往会浪费许多内存,这就是静态存储结构的局限性。
利用本章介绍的指针类型可以构造一个简单而实用的动态数据结构-链表。
图2-6所示是一个简单链表结构示意图。
在这个链表中,每个框表示链表的一个元素,称为结点。
框顶端的数字表示该存储单元的地址(这里的地址是假设的)。
每个结点有两个域。
第一个域是数据域(存放整数数据),第二域是指向下一个结点的指针域(存放下一个结点的地址)。
链表的第一个结点称为表头,最后一个结点称为表尾。
指向表头的指针(head)称为头指针(当head为nil时,称为空链表)。
表尾结点的指针域不指向任何结点,指针域值为空(nil),用来表示表的结束。
从图2-6中可以看出,链表的特点是除表头和表尾外,每一个结点都有一个直接的前趋结点和一个直接的后继结点。
相邻结点的地址可以互不连续,它们靠指针域相互联系。
2.2.1链表的定义
要定义一个链表,每个结点要定义成记录型,而且其中有一个域为指针。
Type
Pointer=^rec;
Rec=record
Data:
integer;
Next:
pointer;
End;
Varp1,p2:
pointer;
说明:
指针变量有p1和p2两个,其基类型为rec。
rec是一个自定义记录类型,有两个域:
一个域是data,类型为整型;另一个域为next,类型为pointer型,可以存放另一个rec类型存储单元的地址。
这种定义中,指针中有记录,记录中有指针,形成一种递归的关系。
假设在程序段中出现:
New(p1);new(p2);{申请两个存储单元,此时状态如图2-7所示}
P1^.data:
=66;{指针变量p1所指存储单元的数据域(data),赋值为66}
P1^.next:
=p2;{指针变量p2所指存储单元的地址赋给p1的指针域(next)中,此时状态如图2-8所示}
通过上述方法就可以将本来独立的两个存储单元通过指针域连接起来。
依此类推,如果有多个存储单元通过类似的方法相连的话,就形成了链表。
链表的基本操作主要有链表的建立、链表结点的插入与删除等,这些操作无一不是从指针域入手加以考虑的。
下面将通过一些例题来说明对链表的基本操作。
2.2.2建立链表
一个链表的建立过程简单地说分三步:
第1步:
申请新结点;
第2步:
给结点的数据域和指针域赋值;
第3步:
将结点链接到表中的某一位置。
例2-2建立一个有10个结点的链表,最后输出该链表。
分析:
首先应定义指针类型、结点类型和指针变量,然后构造算法。
Programcreatable(input,output);
Typepointer=^rec;
Rec=record
Data:
string[5];
Next:
pointer;
End;
Varp1,p2,h:
pointer;I:
integer;
Begin
New(p1);{产生新结点,作为链表的头}
Writeln(‘inputdata:
’);readln(p1^.data);
H:
=p1;{指针h指向链表头}
ForI:
=1to9do{用循环产生9个新结点,每个结点都接在上一个结点之后}
Begin
New(p2);writeln(‘inputdata’);readln(p2^.data);
P1^.next:
=p2;p1:
=p2;
End;
P2^.next:
=nil;{给最后一个结点的next域赋空值nil}
P1:
=h;
Whilep1<>nildo{从链表头开始依次输出链表中结点的data域值}
Begin
Write(p1^.data,’’);
P1:
=p1^.next;
End
End.
在输出链表时,将h作为p1的初值,输出p1所指结点的数据域,然后将p1指针移到下一个结点,再输出,直到p1为nil时停止输出。
这个过程就是一个链表的遍历。
进一步分析,常用的链表有两种。
一种是先进先出链表(或称队)。
此种链表按照输入数据的顺序建立。
后输入的数据放在先一个输入的数据之后。
这样,先输入的数据位于表首,后输入的数据位于表尾。
输出时按从表首到表尾的输出次序正好与输入次序一致,所以称为先进先出链表。
这和我们日常生活的排队是一致的,最早进入队列的元素最早离开。
先进先出链表中的元素是按照a1,a2,a3,…,an的顺序输入的,输出时也只能按照这个次序依次输出,也就是说只有a1,a2,a3,…,an-1都输出之后,an才能输出。
图2-9是先进先出链表的示意图。
另一种是先进后出链表(或称栈)。
此种链表是后输入的数据放在先一个输入的数据之前。
这样,先输入的数据位于表尾,后输入的数据位于表首。
输出时按从表首到表尾的输出次序正好与输入次序相反,所以称为先进后出链表。
先进后出链表中的元素是按照a1,a2,a3,…,an的顺序输入的,输出时次序正好相反an先出,然后顺次是an-1,…,a3,a2,a1。
图2-10所示是先进后出链表的示意图。
例2-3读入一批数据,遇负数时停止,将读入的正数组成先进先出的链表并输出。
分析:
首先应定义指针类型、结点类型和指针变量。
从空表开始,头指针置nil,读入第一个数,判断它是否大于零,若是,建立头结点。
读入下一个数,判断它是否大于零,若是,建立新的结点并链接到表尾。
依此类推,直到读入的数小于或等于零为止。
Programfifo(input,output);{建立先进先出链表}
Type
Pointer=^rec;
Rec=record
Data:
real;
Next:
pointer;
End;
Varh,p1,p2:
pointer;x:
real;
Begin
H:
=nil;
Read(x);{读入第一个数}
Write(x:
6:
1);
Whilex>=0do
Begin
Ifh=nil{要输入数据的结点为头结点}
Thenbegin{建立头结点}
new(p1);{申请新结点}
p1^.data:
=x;p1^.next:
=nil;
p2:
=p1;{使尾指针p2指向链表的尾结点}
h:
=p1;{h指向头结点}
end
elsebegin
new(p1);{申请新结点}
p1^.data:
=x;p1^.next:
=nil;
p2^.next:
=p1;{将新结点链接到已有表的表尾}
p2:
=p1;{调整尾指针p2指向新的表尾}
end;
read(x);write(x:
6:
1);{读入下一个数}
end;
writeln;p1:
=h;{输出链表}
whilep1<>nildo
begin
write(p1^.data:
6:
1);p1:
=p1^.next
end;
writeln;
end.
例2-4读入一批数据,遇负数时停止,将读入的正数组成先进后出的链表并输出。
分析:
建立先进后出的链表,应将读入的第一个数放在表尾,以后读入的数每次都链接到表首。
Programfilo(input,output);{建立先进后出链表}
Typepointer=^rec;
Rec=record
Data:
real;
Next:
pointer;
End;
Varh,p:
pointer;x:
real;
Begin
P:
=nil;{初始准备}
Read(x);{读入第一个数}
Write(x:
6:
1);
Whilex>=0do
Begin
New(h);{建立一个新结点}
H^.data:
=x;h^.next:
=p;{链接到表首}
p:
=h;{调整指针p指向表首}
read(x);{读入下一个数}
write(x:
6:
1);
end;
writeln;
whilep<>nildo{输出链表}
begin
write(p^.data:
6:
1);P:
=P^.next;
end;
writeln;
end.
2.2.3在链表中插入结点
我们知道,要往数组中插入一个元素比较困难,首先要确定插入的位置,然后将从该位置以后的所有元素依次后移一个位置,以便空出一个位置插入新元素。
而链表的插入比较简单。
在确定插入位置后,不必移动链表的元素,只需给相应的指针重新赋值就可以了。
根据插入链表的位置不同,我们分三种情况进行分析。
1、插入表头
如图2-11所示,为了将q所指的结点插入到表头,只需执行下列语句:
q^.next:
=h;
h:
=q;{q为表头,所以调整头指针h,指向q}
2、插入表中
如图2-12所示,为了在p1和p2所指的结点之间插入q所指的结点,只需执行下列语句:
p1^.next:
=q;
q^.next:
=p2;
3、插入表尾
如图2-13所示,为了在表尾插入q所指的结点,只需执行下列语句:
p2^.next:
=q;
q^.next:
=nil;
例2-5在一个有序链表中插入一个新的结点,使插入以后仍有序。
说明:
有序链表即链表中的数据是按从小到大(或从大到小)顺序排列的。
分析:
为了插入新的结点,必须找出它在有序链表中的合适位置,然后将它插入。
在插入时,应根据它是插入表头,插入表中,还是插入表尾作不同的处理。
Procedureinsert(x:
real;varh:
pointer);{插入一结点的过程}
Varq,p1,p2:
pointer;
Begin
New(q);q^.data:
=x;{建立新结点}
Ifx<=h^.data{h指向表头结点}
Thenbegin{插入表头}
q^.next:
=h;
h:
=q;
end
elsebegin{找出表中合适的位置}
p2:
=h;
while(x>p2^.data)and(p2^.next<>nil)do
begin
p1:
=p2;
p2:
=p2^.next;
end;
ifx<=p2^.data{插入表中}
thenbegin
p1^.next:
=q;
q^.next:
=p2;
end
elsebegin{插入表尾}
p2^.next:
=q;
q^.next:
=nil
end
end
end;
例2-6读入一批数,遇负数时结束,将正数组成有序链表。
说明:
在一个程序中,要将过程insert安排在inorder的前面,以便后者调用前者。
分析:
要想建立一个有序链表,可以将读入的第一个数作为表的头结点,然后对以后读入的每个数都调用例2-5中的过程insert,将其插入链表中。
由于过程insert插入结点按从小到大的顺序进行,因此得到的链表就是有序链表。
Procedureinorder(varh:
pointer);
Varx:
real;
Begin
Read(x);write(x:
6:
1);{读入第一个数,建立链表的头结点}
New(h);h^.data:
=x;h^.next:
=nil;
Read(x);write(x:
6:
1);{读入第二个数}
Whilex>=0do
Begin
Insert(x,h);{插入一个结点}
Read(x);write(x:
6:
1);
End;
Writeln;
End;
2.2.4删除一个结点
要从一个链表中删去一个结点,首先在链表中找到该结点,然后将其前趋结点的指针域指向其后继结点即可。
如图2-14所示,要删除p2结点,只需执行语句p1^.next:
=p2^.next就可以了。
被删除结点所占用的存储空间,可以通过语句dispose(p2)释放。
例2-7编写一个过程,将链表中值为x的第一个结点删除。
分析:
有三种情况存在,头结点数据域的值为x;除头结点外的某个结点数据域的值为x;没有数据域值为x的结点。
算法分两步完成:
查找、删除。
(1)查找。
从头开始遍历表,直到找到目标到达表尾为止;
(2)删除。
如果目标找到,将其删除,设置标志(delete)为真,否则设置为假。
Proceduredelete(x:
real;varh:
pointer;vardeleted:
Boolean);{删除结点的过程}
Varp1,p2:
pointer;
Begin
P2:
=h;
While(p2^.data<>x)and(p2^.next<>nil)do{遍历表直到找到目标或到达表尾}
Begin
P1:
=p2;p2:
=p2^.next;
End;
Ifp2^.data=x
Thenbegin{如果目标找到,删除包含它的结点,设置deleted为真}
Deleted:
=true;
Ifp2=hthenh:
=h^.next{头结点数据域的值为x}
Elsep1^.next:
=p2^.next{除头结点外的某个结点数据域的值为x}
End
Elsedeleted:
=false
End;
2.2.5循环链表
如图2-15所示的链表均称为单向链表。
在单向链表中,表尾结点的指针域为空(nil)。
如果让表尾结点的指针域指向表头结点,就使整个链表形成一个环。
这种首尾相接的链表称为循环链表。
在循环链表中,从任意一个结点出发可以找到表中的其他结点。
如图2-16所示的就是一个单向的循环链表或简称为单循环链表。
单循环链表的操作和单向链表基本一致。
它们仅在循环终止条件上有所不同:
前者是指针变量再一次指向表头结点;后者是指针变量的指针域为空(nil)。
在单向链表中我们设立头指针,这样就可以在o
(1)时间内找到表中的第一个元素。
然而要找到表中最后一个元素(假设表共包含n个元素)就要花o(n)时间遍历整个链表。
在单循环链表中,也可以设立头指针。
但是,如果设立尾指针,就可以在o
(1)时间内找到表中最后的一个元素。
同时通过尾结点指向头结点的指针,也可以在o
(1)时间内找到表中的第一个元素。
用这种方法,可以使表的某些操作简化。
例如,要将图2-17(a)中的两个表L1和L2合并成一个表,只需修改两个指针值即可,运算时间为o
(1)。
合并后的表如图2-17(b)所示。
提到循环链表总要提到约瑟夫问题,真的好像循环链表就是为了这个问题而存在的。
下面简单介绍一下这个问题。
例2-8一个旅行社要从n名旅客中选出一名幸运旅客,为他提供免费环球旅行服务。
方法是,大家站成一圈,然后选定一个m,从第1个人开始报数1,2,3,…