第一章 数据结构与算法的引入
1.1 数据结构的基本概念
一、 学习数据结构的意义
程序设计 = 数据结构 + 算法
目前,80%的待处理的数据具有“算法简单”(四则运算、检索、排序等),“对象复杂”(数据类型不同、数据量大、需要保存)等特点,故合理组织数据、选择较好的数据结构可为高效算法(时间少、占用空间小)提供理想的对象。
二、基本术语
1.数据(data):
是对客观事物的符号的表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。(P1表1-1中,学号、姓名、性别、民族等列为字符型数据,语文、数学等列为数值型数据)
表1-1 学生档案
学号 |
姓名 |
性别 |
民族 |
语文 |
数学 |
英语 |
总成绩 |
101 |
张华 |
男 |
汉族 |
104.5 |
124 |
115 |
|
102 |
郝博 |
男 |
汉族 |
113 |
125 |
94.5 |
|
… |
… |
… |
… |
… |
… |
… |
|
303 |
白晓燕 |
女 |
朝鲜族 |
96 |
105 |
135 |
2. 数据项(data item)
数据项是数据不可分割的最小单位。(P1表1-1中的一个字段就是一个数据项)
3.数据元素(data element):
是数据的基本单位,在计算机程序中通常作为一个整体来处理。一个数据元素由多个数据项组成,(P1表1-1中的一行就是一个数据元素)
4.数据结构(data structure):
是相互之间存在一种或多种特定关系的数据元素的集合。数据结构是一个二元组,记为: B=(D,S).其中D为数据元素的集合,S是D上关系的集合。
数据元素相互之间的关系称为结构(structure)。根据数据元素之间关系的不同特性,通常由下列四类基本结构:
(1)集合:数据元素间的关系是同属一个集合。(图1)
(2)线性结构:数据元素间存在一对一的关系。(图2)
(3)树形结构:结构中的元素间的关系是一对多的关系。(图3)
(4)图(网)状结构:结构中的元素间的关系是多对多的关系。(图4)
图1 图2 图3 图4
4. 数据的逻辑结构和物理结构
逻辑结构:数据元素之间存在 的关系(逻辑关系)叫数据的逻辑结构。
物理结构:数据结构在计算机中的表示(映象)叫数据的物理结构。
一种逻辑结构可映象成不同的存储结构:顺序存储结构和非顺序存储结构(链式存储结构)。
⑴. 顺序存储结构: 是指用一块连续的存储单元依次存储线性表的各元素,通常由数组实现。它是按首址加位移来访问每个元素。
若某线性表的各元素存入一维数组中,且每个元素a[i]占用l个单元
a[c], a[c+1], … , a[i], … a[n]
b b+ l b+(i-1) l ,b+(n-1) l
则a[i] 的地址 LOC(a[i]) = LOC(a[c]) + (i-c)*l = b+(i-c) l
首地址 位移
例如:var
a :array[4..100] of integer;
已知:a[4]的地址为x,
则 LOC(a[15]) = LOC(a[4]) +(15- 4)*2 = x+22
例一:删除一个数组中所有的X
Program delx;
var
s:array[1..100]of integer;
i,j,n,x:integer;
begin
readln(n);
for i:=1 to n do begin read(s[i]);write(s[i]:5) end;
readln(x);
i:=1;
while i<=n do if s[i]<>x then i:=i+1
else begin
for j:=i to n-1 do s[j]:=s[j+1];
n:=n-1;
end;
for i:=1 to n do write(s[i],' ');
writeln;writeln('n=',n)
end.
⑵.链式存储结构: 当线性表的结点很多且经常要进行插入、删除操作时用数组表示线性表运算颇为费时,此时可用链式存储结构存储线性表,一般用动态指针实现。
哨兵 数据域 指针域
例二、插入排序:在一个文本文件中存放的N个人的姓名,文本文件的格式为:第一行为N,以下第二至第N+1行分别是N个人的姓名(姓名不重复,由英文字母组成,长度不超过10),请编一个程序,将这些姓名按字典顺序排列。
分析:(1)排序有多种方法,插入排序是其中的一种,其原理同摸扑克牌类似:摸到一张牌,把它按大小顺序插入牌中,每一张都如此处理,摸完后,得到的就是一副有序的扑克牌。本题可以用插入排序求解;
(2)为了减少移动数据的工作,可以采用链式存储结构。每个结点由两个域构成,数据域(用来存放姓名)和指针域(用来存放后继结点的地址)。如图A是将1,3,6,7按顺序构成一个线性链表的示意图。这样在这个有序表中插入一个5时,只需对指针进行相应地操作即可,如下图B:
┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐
头结点→│1 │ --→ │3 │ --→│6 │ --→ │7 │^ │←尾结点
└─┴─┘ └─┴─┘ └─┴─┘ └─┴─┘
图 A
┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐
头结点→│1 │ --→ │3 │ ││6 │ --→│7 │^ │←尾结点
└─┴─┘ └─┴┼┘ └↑┴─┘ └─┴─┘
↓ ┌┘
┌─┬┼┐
│5 │ │←插入的结点
└─┴─┘
图 B
解:Pascal程序:
Program lx2.14;
type point=^people; 定义结点类型
people=record
name:string[10]; name--数据域,存放姓名
next:point; next--指针域,存放后继结点的地址
end;
var head,p,q1,q2:point;
n,i:integer;
begin
new(head);head^.next:=nil; 定义头结点,初始链表为空
write('n= ');
readln(n);
for i:=1 to n do
begin
new(p);readln(p^.name);p^.next:=nil;
if head^.next=nil then head^.next:=p 将P指向的结点插入空链表
else
begin
q1:=head;q2:=q1^.next;
while (q2<>nil) and (q2^.name<p^.name) do
begin q1:=q2;q2:=q1^.next; end; 查找结点p应插入的位置
q1^.next:=p;p^.next:=q2; 将p插入q1之后q2之前
end;
end;
writeln;
p:=head^.next;
while p<>nil do 输出
begin
writeln(p^.name);
p:=p^.next;
end;
end.
三、数据结构研究的对象
1. 数据的逻辑结构(数据之间的关系)。
2. 数据的物理结构(数据在计算机中的存储方法)。
3. 数据的算法处理(处理不同结构数据的方法)。
四、数据的运算:插入、删除、更新、排序、查找。
例:删除线性表中所有的X
Program delx;
var
s:array[1..100]of integer;
i,j,n,x:integer;
begin
readln(n);
for i:=1 to n do begin read(s[i]);write(s[i]:5) end;
readln(x);
i:=1;
while i<=n do if s[i]<>x then i:=i+1
else begin
for j:=i to n-1 do s[j]:=s[j+1];
n:=n-1;
end;
for i:=1 to n do write(s[i],' ');
writeln;writeln('n=',n)
end.
请同学们用链表完成
五、并查集:并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。并查集有三个操作:初始化,合并,查询。
1、并查集的简单实现(见P5)
例如,给定集合 S ={1,2,…,7},及等价性条件:1≡2,5≡6,3≡4,1≡4,对集合S作等价类划分如下:首先将S的每一个元素看成一个等价类。然后顺序地处理所给的等价性条件。每次处理一个等价性条件,所得到的相应等价类列表如下:
{1}、{2}、{3}…{7}
1≡2 {1,2}{3}{4}{5}{6}{7};
5≡6 {1,2}{3}{4}{5,6}{7};
3≡4 {1,2}{3,4}{5,6}{7};
1≡4 {1,2,3,4}{5,6}{7};
最终所得到的集合S的等价类划分为:{1,2,3,4}{5,6}{7}。
例:若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
输入格式
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Ai和Bi具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。
输出格式
P行,每行一个'Yes'或'No'。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
program family;
var c:array[0..5000]of longint;
i,j,m,n,p,x,y,a,b:longint;
function find(x:longint):longint;
begin
find:=c[x];
end;
procedure union(a,b:longint);
begin
for i:=1 to n do
if c[i]=b then c[i]:=a;
end;
begin
read(n,m,p);
for i:= 1 to n do c[i]:=i;
for j:= 1 to m do
begin
read(x,y);
a:=find(x);
b:=find(y);
if a<>b then union(a,b);
end;
for i:= 1 to p do
begin
read(x,y);
if c[x]=c[y] then writeln('Yes')
else writeln('No');
end;
end.
2、并查集的树型实现(见P5~7)
六、哈希表
建立关键字k到存储地址p之间的一个对应关系p=H(k)称为哈希函数,与之对应的表叫哈希表。
1、哈希函数的构造
⑴直接定址法:取关键字或关键字的某个线性函数值作为哈希地址,即:H(k)=k或H(k)=ak+b。
⑵数字分析法:取关键字中分布较均匀的若干位,构成哈希地址。
⑶平方取中法:取关键字k的平方值的中间几位作为哈希地址。
⑷移位法和折叠法:
⑸除留余数法:即 H(k)=k mod p (p为小于哈希表长的最大素数)
2、处理冲突的方法
⑴开放地址法:由j=H(k)找到地址后,若j位置上已存在数据,可用如下方法查找空位:
Ri=(H(k)+di)mod m (i=1,2,3,…,m-1) (m为哈希表长
di =1,2,3,…,m-1 (线性探测再散列)
di =12,-12,22,-22,32,-32,…,k2(二次探测再散列)
⑵链地址法:给哈希表中的每个节点增加一个指针字段,用来链接发生冲突的记录。
3、哈希表的查找
首先计算p0=H(k),若单元p0为空, 则所查元素不存在,若单元p0中元素的关键字为k则找到,否则,按解决冲突的方案找出下一个哈希地址pi。
1.2 算法:是在有限步骤内求解某一问题所使用的一组定义明确的规则。
一、算法的特点
1. 有限性,即序列的项数有限,且每一运算项都可在有限的时间内完成;
2. 确定性,即序列的每一项运算都有明确的定义,无二义性;
3. 可以没有输入运算项,但一定要有输出运算项;
4. 可行性,即对于任意给定的合法的输入都能得到相应的正确的输出。
二、算法的表示:自然语言、流程图、N-S结构流程图、伪代码、类Pascal语言
三、算法的评价:
程序优劣的标准:正确性、可读性、健壮性、高效性。(运行时间少,占用空间少)
1、算法的时间复杂度
事后统计法:与机器硬件有关,不可取。
事前分析法:用程序执行语句次数的主体 T(n)≈
O(nk)即时间复杂度的价来度量程序的优劣。
⑴时间复杂度的价
例:设一程序段如下: 各行语句执行次数
① for i:=1 to n do n+1
② for j:=1 to n do n(n+1)
③ x:=x+1 n*n
该程序段总的执行语句次数 f ( n ) = 2n2 + 2n + 1,我们将多项式的主体部分2n2定义为程序的时间复杂度。记为:T(n)≈ O(n2) 其中O(n2)表示该程序段的时间复杂度的价。
⑵时间复杂度的计算
例:计算
y = anxn + an-1xn-1 + … + a1x + a0
算法一: y:=a[0]
For k:=1 to n do
Begin
S:=a[k];
For j:=1 to k do s:=s*x;
Y:=y+s;
End;
Writeln(‘y=’,y);
计算y共用乘法 1+2+3… +n = 次, 共用加法n次, 时间复杂度为, 阶为O(n2)
将算式变为 y=(…( ( an*x + an-1 )*x + an-2 )*x + … + a1)*x+a0
算法二: y:=a[n];
For k:=n-1 to 0 do y:=y*x + a[k];
Writeln(‘y=’, y );
计算y共用乘法 n次, 共用加法n次, 时间复杂度为2n, 阶为O(n)
⑶时间复杂度价的比较
O(1) 问题规模变化时,复杂性变化不大,
优秀。
O(n) 问题规模变化是线性的,很好。
O(n log2n) 任然时一个较好的算法。
O( nk) 应尽可能的改进,使k 的值越小越好。
O(2n) n较大时,复杂性会变的很大,避免使用。
⑷优化时间效率的方法
1. 尽可能在编译时赋值。
2. 用快速运算代替慢速运算。
3. 避免重复运算。
4. 减少,紧索,优化循环。
5. 减少重复判断。
6. 尽量使用模块等方法。
2、算法的空间复杂度
1.压缩存储技术
2.原地工作
例:约瑟夫问题 设有n个人围坐在一个圆桌周围,现从第s个人开始报数,数到第m个人出列,然后从出列的下一个人重新开始所数,数到第m个人又出列,……,如此重复,直到所有人出列为止。输入:n,s,m, 输出:出列顺序
program ysfwt;
var
i,n,k,m,s,w,j :integer;
p:array[1..100] of integer;
begin
repeat readln(n,s,m); until s<=n;
for i:=1 to n do p[i]:=i;
k:=s;
for i:=n downto 2 do //参加报数的人数从n到2 逐次减1
begin
k:=(k+m-1)mod i; //k:数组中出列的位置,m:每次报数m个,i:参加报数的人数
if k=0 then k:=i;
w:=p[k]; //保留出列的序号
if k<i then for j:=k to i-1 do p[j]:=p[j+1]; //移动数组,腾出第i位
p[i]:=w; // 在原数组中保留出列的序号
end;
for i:=n downto 1 do write(n-i+1,':',p[i],' ');
writeln;
end.
上例中直接使用队列a计算出队顺序就是原地工作
3、程序设计中时间与空间的转换
例:编程找出所有3位数到7位数中的水仙花数(见P15~16)
program exe1_3_1; //枚举每个数时都临时计算各位数字的乘方幂。
var
x,s,e:longint;
i,j,m,l,d:integer;
st:string;
begin
for x:=100 to 9999999 do
begin
str(x,st); l:=length(st); s:=0;
for i:=1 to l do
begin
e:=1; val(st[i],m,d);
for j:=1 to l do e:=e*m;
s:=s+e;
end;
if s=x then writeln(x);
end;
end.
program exe1_3_2; //先计算0~9各个数字的3~7次方保存在数组中,在以
var 后的枚举中随时调用
lists:array[0..9,3..7]of longint;
x,s:longint;
i,j,m,l,d:integer;
st:string;
begin
for i:=3 to 7 do lists[0,i]:=0;
for i:=3 to 7 do lists[1,i]:=1;
for i:=2 to 9 do lists[i,3]:=i*i*i;
for i:=2 to 9 do
for j:=4 to 7 do lists[i,j]:=lists[i,j-1]*i;
for x:=100 to 9999999 do
begin
str(x,st); l:=length(st); s:=0;
for i:=1 to l do
begin
val(st[i],m,d);
s:=s+lists[m,l];
end;
if s=x then writeln(x);
end;
end.
1.3 建立数学模型
一、概述
1.定义: 数学建模是利用数学语言(符号、式子与图像)模拟现实的模型。(数学模型揭示了实际问题最本质的特征,而算法则是建立在数学模型之上,更多考虑模型在计算机上如何实现)
例如:洗碗的数学模型为 x/2+x/3+x/4=65
2.性质:抽象性、高效性、可推广性 (例1-4 p17)
① for i:=1000 to 9999 do
if (I div 1000=(I div 100)mod
10)and(I mod 10=(I mod 100)div 10)and(sqrt(i)=trunk(sqrt(i)))
② for y:=32 to 99 do begin
x:=y*y; b1:=x mod
10; b2:=(x div 10)mod 10;
a1:=(x div 100)mod 10; a2:=x div 1000
if (b1=b2)and(a1=a2)then write(x)
end
3.建立数学模型的方法和步骤
模型准备 → 建立模型 → 模型求解 → 编程实现
4.数学模型的评价:可靠性(准确地反映出现实)、可解性(由它产生的算法所需的时间与空间在可以承受的范围之内)
5.应用举例(例1-5 P18)
对任意的x∈(1000,9999),分解x的各位数字得到a,b,c,d四个数字
模型一:for x:=1000 to 9999 do 时间复杂度T(n)=9999-1000+1=9000
分解x的各位数字,存放到变量a,b,c,d中
Flag:=(10*a+b)<(10*c+d)and((10*a+b)/(10*c+d)=a/d)and(b=c)
If flag=true then writeln(ab,’/’,cd);
模型二: for x=10 to 99 do 时间复杂度T(n)=90*45=4050,大约降低1/2
For y=x+1 to 99 to
分解分子x的各位数字,存放到变量a,b中
分解分母y的各位数字,存放到变量c,d中
If (x/y=a/d)and(b=c) then writeln(x,’/’,y);
模型三:for a:=1 to 9 do 时间复杂度T(n)=9*9*9=729,大约降低8/9
for b:=1 to 9 do
for d:=1 to 9 do
x=10*a+b;y:=10*b+d (合成分子x和分母y)
If x/y=a/d then writeln(x,’/’,y);
1.4 程序的调试
程序的错误有两类 1.语法错误:编译程序可查出(compile)
2.语义错误:(错误公式、溢出、越界、死循环、程序结构或逻辑出错)
调试程序的方法
1.静态查错:不执行程序,仅根据程序清单和流程图查错
2.动态查错:在程序执行的过程中寻找错误
一、系统的测试工具
Turbo pascal 含大量的动态调试功能,主要集中在子菜单(Run Debug
option 中,见P6图1.2)
二、调试程序的一般过程
1.调试初始化
①设置检查状态:按Ctrl+o+o 将数值越界,堆栈、内存检查设为on状态
②设置观察状态:选Debug中的Watch项,在屏幕下放开辟观察窗口,选Debug中的add Watch项,加入要观察的变量。
2.调试方式的选择
①单步跟综:选Run中的Step over (F8) 一次执行过程和函数调用。
选Run中的Trace
into(F7) 以一次一行的方式执行程序。
②执行到光标所在行:选Run中的Goto
cursor (F4) 程序执行到光标所在行暂停。
③断点:选Debug / add break point (Ctrl+F8) 将光标所在行设为断点。
选Debug / breakpoints 可删除断点或设定下一断点的位置。
3.准备调试后的程序运行
为提高运行的效率,在调试完后应将数值越界、堆栈检查还原为off状态。
三、测试用例的选取:逻辑覆盖测试的白箱法。
程序功能测试的黑箱法。
1.白箱法
①语句覆盖:用足够多的测试用例,使程序中的每个语句都执行一遍,以尽可能地发现错误。
②分支覆盖:用足够多的测试用例,使程序中的每个分支至少通过一次。
③条件覆盖:用足够多的测试用例,使每个判定中的每个条件都能得到两种不同的结果。
④组合条件覆盖:用足够多的测试用例,使每个判定中的条件的各种可能组合至少出现一次。
2.黑箱法
①等价分类法:将所有能输入的数据(有效和无效)划分成若干等价类,每类中取一组数为测试用例。
②边值分析法:对输入范围的边界情况以及稍超出范围的无效情况进行测试。
③错误推测法:用一些极端的数据测试,如排序程序可检查表为空、表里只有一个元素、所有元素相同、已经有序等。
3.综合策略
目前,没有一种方法能够单独地产生一套“定型”的测试用例,在实际测试中应联合使用,一般步骤是:首先从黑箱法开始用等价分类、边值分析、错误推测等方法找出程序中不具备功能要求的地方,然后用白箱法的分支覆盖、条件覆盖、语句覆盖检测程序内部是那些子程序段导致其错误,修改后再用黑箱法测试结果,这样循环反复,提高程序的正确率。
练习:1、翻硬币。
2、删除线性表中所有的x。
3、并查集的实现。
4、约瑟夫问题
第二章指针和动态数据结构
静态数据结构:各变量对应的空间程序运行前已确定,运行中不能改变数据结构 动态数据结构: 程序在运行时能根据数据需要而扩充或缩减
2.1指针变量的定义及基本使用
一、指针变量的定义:指针变量是一种特殊的变量,它是用来存放地址(指针)的。
为了更直观地理解,我们用下图来说明
指针变量p“记忆”了动态变量内存单元地址值,我们称之为p“指向”某内
存单元。指向用箭头表示
指针变量是静态的简单变量,但它与整型、实型不同,它存放的是地址,指向除文件外的某一种数据类型,固指针变量应如下说明:
指针变量说明的一般格式为:
Var
指针变量名表:^基类型;
如: var
p,q:
^integer;
二、指针变量的使用
1.开辟动态存储单元
格式:new(指针变量) 如 new(p)
功能:系统将自动分配一块内存,并把它的首地址存入指针变量中,也就是使指针变量指向新生成的动态变量。如 new(p1)
2.释放动态存储单元
格式:dispose(指针变量) 如 dispose(p)
功能: 释放指针所指向的存储单元,使指针变量的值无定义。
3.指针变量的赋值和操作
格式:<指针变量>:=<指针变量> 如 p:=q(特殊情况
p:=nil表示不指向任何存储单元)
功能:使p和q同时指向q所指向的存储单元。
格式:<指针变量>^:=<指针变量>^ 如p^:=q^
功能:将q所指向的存储单元的值存放到p所指向的存储单元中。
4.例:输入两个整数,按从小到大打印出来。(P30~31)
2.2链表
链表是动态数据结构的一种基本形式。如果我们把指针所指的一个存贮单元叫做结点,那么链表就是把若干个结点链成了一串。下面就是一个链表:
每个结点有两个域,一个是数据域,一个是指向下一个结点的指针域。最后一个结点的指针域为NIL,NIL表示空指针。
一、链表的定义
要定义一个链表,每个结点要定义成记录型,而且其中有一个域为指针。
TYPE
POINT=^PP;
PP=RECORD
DATA:STRING[5];
LINK:POINT;
END;
VAR P1,P2:POINT;
二、建立链表
(1)建立第一个结点
NEW(P1);
P1↑.DATA:=D1;
(2) 建立第二个结点,并把它接在P1的后面
NEW(P2);
P2↑.DATA:=D2;
P1↑.LINK:=P2;
(3) 建立第三个结点,并把它接在P2的后面.
NEW(P3);
P3↑.DATA:=D3;
P2↑.LINK:=P3;
(4) 建立最后一个结点,并把它接在P3的后面.
NEW(P4);
P4↑.DATA:=D4;
P4↑.LINK:=NIL;
P3↑.LINK:=P4;
例1 建立一个有10个结点的链表,最后输出该链表。
PROGRAM E1(INPUT,OUTPUT);
TYPE point=^pp;
pp=record
data:string[5];
link:point;
end;
VAR
p1,p2,k:point;
i:integer;
BEGIN {产生新结点P1,作为链表的头}
new(p1);
writeln('input data');
readln(p1^.data);
k:=p1; {指针K指向链表头}
{用循环产生9个新结点,每个结点都接在上一个结点之后}
for i:=1 to 9 do
begin
new(p2);
writeln('input
data');
readln(p2^.data);
p1^.link:=p2;
p1:=p2;
end;
{给最后一个结点的LINK域赋空值NIL}
p2^.link:=nil;
{从链表头开始依次输出链表中的DATA域}
while k^.link<>nil do
begin
write(k^.data,'->');
k:=k^.link;
end;
END.
链表的遍历:输出链表的过程就是一个链表的遍历。给出一个链表的头结点,依次输出后面每一个结点的内容,指针依次向后走的语句用K:=K↑.LINK 来实现。
先进先出链表(或称队)
例2 读入一批数据,遇负数时停止,将读入的正数组成先进先出的链表并输出。
分析:首先应定义指针类型,结点类型和指针变量,读入第一个值,建立首结点,读入第二个值,判断它是否大于零,若是,建立新结点。
PROGRAM fifo(input,output);
{建立先进先出链表}
TYPE
Point=^node;
Node=RECORD
Data:real;
Link:point
END;
VAR
head,last,next:point;
x:real;
BEGIN
{读入第一个值,建立首结点}
read(x);
write(x:6:1);
new(head);
head^.data:=x;
last:=head; {读入第二个值}
read(x);
write(x:6:1);
WHILE x>=0 DO
BEGIN {建立新结点}
new(next);
next^.data:=x; {链接到表尾}
last^.link:=next; {指针下移}
last:=next;
{读入下一个值}
read(x);
write(x:6:1)
END;
Writeln; {表尾指针域置NIL}
Last^.link:=NIL;
Next:=head;
WHILE next<>NIL DO
BEGIN
{输出链表}
Write(next^.data:6:1);
Next:=next^.link
END;
Writeln
END.
先进后出链表(或称栈)
例3读入一批数据,遇负数时停止,将读入的正数组成先进后出的链表并输出。
PROGRAM fifo(input,output);
{建立先进后出链表}
TYPE
point=^node;
node=RECORD
data:real;
link:point
END;
VAR
head,last,next:point;
x:real;
BEGIN
{初始准备}
next:=NIL;
read(x); {读入第一个数}
write(x:6:1);
WHILE x>=0 DO
BEGIN {建立一个新结点}
new(head);
head^.data:=x; {链接到表首}
head^.link:=next;{指针前移}
next:=head; {读入下一个数}
read(x);
write(x:6:1)
END;
writeln; {输出链表}
WHILE next<>NIL DO
BEGIN
write(next^.data:6:1);
next:=next^.link
END;
writeln
END.
三、在链表中插入结点
1、插入表头 new(q); q^.next:=h; h:=q;
2、插入表中 new(q); p1^.next:=q; q^.next:=p2;
3、插入表尾 new(q); p2^.next:=q; q^.next:=nil;
例1:在一个有序链表中插入一个新的结点,使
插入以后任然有序(应注意表头、表中、表尾的不同处理,见P37)。
例2:读入一批数,遇负数时结束,将正数组成有序链表(先读入一个数作为表头,以后读入每个数都调用上例中插入过程,见P38)
四、在链表中删除一个结点
算法分两步:①查找(应考虑头结点、非头结点、没找到三种情况),
②删除(见P39)
五、循环链表
将单向链表的表尾结点的指针域指向表头,就使整个链表形成一个环,这种首尾相连的链表称为循环链表 (例:约瑟夫问题 见P41)
六、双向链表
除首尾结点外,每个结点都有指向前驱和后继的两个指针域,这种链表称为双向链表。(同样也可以定义一个循环双向链表)
1、 定义一个双向链表
type
Pointer=^note;
Note=record
Data:integer;
Llink:pointer;
Rlink:pointer;
End;
2、
在双向链表中插入一个结点(在p所指向的结点之前插入一个新结点s)
①s^.llink:=p^.llink; ②s^.rlink:=p; ③p^.llink^.rlink:=s; ④p^.llink:=s;
3、 在双向链表中删除一个结点(删除p所指向的结点)
①p^.llink^.rlink:=p^.rlink; ②p^.rlink^.llink:=p^.llink; ③dispose(p);
练习:1、建立一个先进先出链表并输出。
2、建立一个先进后出链表并输出。
3、编写一个简单的中学新生入校登记表处理程序。
4、链表原地逆置。
5、删除链表中最大和最小的节点。
6、双向链表交换相邻的两个节点。
7、约瑟夫问题(链表完成)。
第三章文件
Pascal中的文件有三种类型,文本文件、随机文件和无类型文件,NOI竞赛中都采用文本文件形式。
3.1文本文件的逻辑组织
一、文件名 格式: 主文件名(最多8个字符) . 扩展名(最多3个字符,可以没有)
文件名中使用的字符不区分大小写,一律看作大写,如 ab.dat AB.DAT 都看作AB.DAT。
二、文本文件的逻辑组成
文本文件是一种行结构的字符文件,数据元素没有统一的长度,(由0个和多个字符构成)元素之间采用回车符(ord(CR)=13)或回车/换行符(ord(CR/LF)=13/10)分隔,文本文件中的一个数据称为一行。
三、文件中的指针
1、打开一个文件,文件指针指向第一个数据的第一个字符
2、文本文件是一种顺序存取文件
3、对于同一个文本文件不能同时既读又写
3.2文本文件的基本操作
一、定义 格式: var f1,f2: text; 用标准的输入(input)、输出文件(output)文件时不需要变量说明。
二、文本文件的操作步骤: 打开文件(首先用assign连接外部与内部文件,然后用rewrite、reset或append等方式打开文件)→ 读/写文件(用read读取或用 write 写入数据)→ 关闭文件(用close关闭文件)
1、打开一个在磁盘上已经存在的文件,从文件中读取数据
assign(input, ‘li01.in’);
reset(input); read(x); close(input);
2、打开一个新文件,向文件中写入数据
assign(output, ‘li01.out’); rewrite(output); write(x); close(output);
3、打开一个已经存在的文件,并向文件末尾追加数据
assign(output, ‘li01.out’); append(output); write(x); close(output);
其中x可以是字符、整型、实型、字符串,系统将自动从文本文件中读取一个或多个字符,并将其转换为变量所要求的类型。
三、文件操作的函数
1、eoln(文件变量) 若文件指针指向文件行结束或文件结束符,则返回true,否则返回false。
2、eof(文件变量) 若文件指针指向文件结束符,则返回true,否则返回false。
3、seekeoln(文件变量) 与eoln相似,只是文件指针会自动跳过ASCII码0-32的控制符和空格。
4、seekeof(文件变量) 与eof相似,只是文件指针会自动跳过ASCII码0-32的控制符和空格。
练习:1、建立一个以“#”结束的磁盘文件。
2、统计数字 (要求用链表、数组两种方法完成)
某次科研调查时得到了n个自然数,每个数均不超过1500000000(1.5*109)。已知不相同的数不超过10000个,现在需要统计这些自然数各自出现的次数,并按照自然数从小到大的顺序输出统计结果。
【输入】
输入文件count.in包含n+1行;
第一行是整数n,表示自然数的个数;
第2~n+1每行一个自然数。
【输出】
输出文件count.out包含m行(m为n个自然数中不相同数的个数),按照自然数从小到大的顺序输出。每行输出两个整数,分别是自然数和该数出现的次数,其间用一个空格隔开。
【输入输出样例】
count.in
8
2
4
2
4
5
100
2
100
count.out
2 3
4 2
5 1
100 2
program ex2_b1;(链表解法)
type
pointer=^rec;
rec=record
data,count:longint;
next:pointer;
end;
var
k,n,i:longint;
s,h:pointer;
procedure search(x:longint);
var q,p1,p2:pointer;
begin
new(q); q^.data:=x; inc(q^.count);
if x<h^.data then begin q^.next:=h;h:=q;end
else begin
p2:=h;
while (x>p2^.data)and(p2^.next<>nil) do begin p1:=p2;p2:=p2^.next; end;
if p2^.data=x then p2^.count:=p2^.count+1
else if x<p2^.data then begin p1^.next:=q; q^.next:=p2; end
else begin p2^.next:=q; q^.next:=nil end
end;
end;
procedure printlist(p:pointer);
begin
while p<>nil do begin writeln(p^.data,' ',p^.count);p:=p^.next; end;
end;
begin
assign(input,'count3.in'); reset(input);
assign(output,'count.out'); rewrite(output);
readln(n);
new(h);readln(h^.data);h^.count:=1;h^.next:=nil;
for i:=1 to n-1 do begin readln(k);search(k); end;
printlist(h);
close(input);close(output);
end.
program lx212;(数组解法)
var a:array[1..200001] of longint;
i,j,k,m,n:longint;
procedure qsort(s,t:longint);
var i,j,mid,temp:longint;
begin
i:=s;j:=t;mid:=a[(s+t) div 2];
while i<=j do
begin
while a[i]<mid do inc(i);
while a[j]>mid do dec(j);
if i<=j then
begin
temp:=a[i];a[i]:=a[j];a[j]:=temp;
inc(i);dec(j);
end;
end;
if i<t then qsort(i,t);
if j>s then qsort(s,j);
end;
begin
assign(input,'count.in');
reset(input);
assign(output,'count.out');
rewrite(output);
readln(n);
for i:=1 to n do readln(a[i]);
qsort(1,n);
a[n+1]:=maxlongint;
k:=1;
for i:=2 to n+1 do
if a[i]<>a[i-1] then
begin writeln(a[i-1],' ',k); k:=1;end
else k:=k+1;
close(input);
close(output);
end.
第四章 树
树是一种重要的非线性数据结构,直观地看,它是数据元素(在树中称为结点)按分支关系组织起来的结构,很象自然界中的树那样。树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树形象表示。树在计算机领域中也得到广泛应用,如在编译源程序如下时,可用树表示源源程序如下的语法结构。又如在数据库系统中,树型结构也是信息的重要组织形式之一。一切具有层次关系的问题都可用树来描述。
4.1基本概念
树结构的特点是:它的每一个结点都可以有不止一个直接后继,除根结点外的所有结点都有且只有一个直接前趋。
1. 定义:一棵树是由n(n>0)个元素组成的有限集合,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点,称为根结点或树根(root);
(3)除根结点外,其余结点能分成m(m>=0)个互不相交的有限集合 T0,T1,T2,……Tm-1
其中的每个子集又都是一棵树,这些集合称为这棵树的子树。
如下图是一棵典型的树:
2. 树的表示
①广义表表示法:(A(B(E(K,L),F),C(G),D(H(M),I,J)))
②树形表示法:
3. 有关术语
①结点名称——根、支结点、叶子。
②结点的关系——前缀(双亲)、后继(孩子)、兄弟(同一层双亲的孩子)。
③结点的层次——根为第一层,它的孩子为第二层……。
④树的深度——组成该树各结点的最大层次,如上图,其深度为4。
⑤树的度——也即是宽度,简单地说,就是结点的分支数。以组成该树各结点中最大的度作为该树的度,如上图的树,其度为3;树中度为零的结点称为叶结点或终端结点。
⑥子树——树的一部分称为原树的子树。
⑦森林——指若干棵互不相交的树的集合,如上图,去掉根结点A,其原来的三棵子树T1、T2、T3的集合{T1,T2,T3}就为森林。
⑧有序树——指树中同层结点从左到右有次序排列,它们之间的次序不能互换,这样的树称为有序树,否则称为无序树。
4.2 二叉树
1. 定义:二叉树是结点的有限集合,这个集合或是空的,或是由一个根结点和两棵互不相交的称之为左子树和右子树的二叉树组成。
2. 二叉树的五种基本形态。
(a) 空二叉树;(b)只有一个结点的二叉树;(c)只有左子树的二叉树;(d)只有右子树的二叉树;(e)左右完全的二叉树。
3. 二叉树的两个特殊形态:
(1)完全二叉树——只有最下面的两层结点度小于2,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树;
(2)满二叉树——除了叶结点外每一个结点都有左右子女且叶结点都处在最底层的二叉树,。
完全二叉树 满二叉树
4. 二叉树的主要性质
(1) 二叉树的的i层上最多只有个结点()。
证明:当 i=1 时,= 20 =1 显然成立;
假设第 i=k层时命题成立,即第k层上最多有2k-1 个结点。
当i=k+1时,由于第k层的2k-1 个结点每个最多有2个子结点,故最多有 2*2k-1 =2(k+1)-1个结点 。
(2) 深度为 k的二叉树至多有 2k–1 个结点(k>=1),最少有k个结点。
即:1+2+4+8+……+2k-1 =
(3)对于任意一棵二叉树,如果其叶结点数为n0,而度数为2的结点总数为n2,则n0=n2+1;
证明:假设ni(i=0、1、2),为度是i的结点数,即:n= n0+ n1+ n2
设 B为二叉树的前驱个数,则B=n-1 ②(二叉树只有根没有前驱),所有这些前驱同时又是度为1和2的结点的后继,故B= n1+2*n2,③
由③代②得:n= n1+2*n2+1。
由④代①得:n1+2*n2+1 = n0+ n1+ n2 即:n0=n2+1
5. 树或森林转换为二叉树
① 普通树 → 二叉树 方法: 左链联孩子,右链联兄弟
例:
② 森林 → 二叉树 方法: 将森林每颗树的根视为兄弟,以第一棵树的根为二叉树的根,左链联孩子,右链联兄弟的方法转换即可。
例:
由于树和森林可方便唯一的转换成二叉树,故以后主要讨论二叉树。
4.3 树的存储结构(顺序存储结构和链表存储结构)
4.3.1二叉树的存储结构
1. 顺序存储结构(满二叉树,完全二叉树一般采用存顺序储结构)
Const m= 树中结点树的上限;
Type node= record
Data :datatype;
Prt ,lch, rch : 0..m;
End;
Treetype = array[1..m] of node;
Var tree : treetype;
例:采用数组tree 存储满二叉树
data |
ptr |
lch |
rch |
|
1 |
A |
0 |
2 |
3 |
2 |
B |
1 |
4 |
5 |
3 |
C |
1 |
6 |
7 |
4 |
D |
2 |
0 |
0 |
5 |
E |
2 |
0 |
0 |
6 |
F |
3 |
0 |
0 |
7 |
G |
0 |
0 |
0 |
2. 链式存储结构(一般二叉树通常采用链式存储结构)
语言描述: 结点结构:
type tree=^node;
node=record
data:char;
lchild,rchild:tree
end;
var bt:tree;
例:采用链表存储二叉树
4.3.2树的存储结构
(1)顺序存储方式
Const
n=树的度;
m=结点数的上限;
type node=record
data:datatype
child:array[1..n] of integer;
end;
var tree:array[1..m] of node;
(2) 链表存储方式
Const
n=树的度;
type btree=^node;
node=record
data:datatye;
next:array[1..n] of btree;
end;
var root: btree
n度数的结点结构为
data |
Child1 |
Child2 |
…… |
childn |
4.4树的遍历
一、二叉树的遍历
⑴ 遍历:就是按一定的规则和顺序走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。
⑵ 遍历的方式:设 L:遍历左子树
R:遍历右子树
D:访问根结点
则所有可能的遍历方式为LDR,LRD,RDL,RLD, DLR, DRL
若考虑先左后右则方式为:DLR(前序遍历),LDR(中序遍历),LRD (后序遍历)。
1、前序遍历的操作定义如下:
若二叉树为空,则空操作,否则
① 访问根结点
② 先序遍历左子树
③ 先序遍历右子树
先序遍历右图结果为:124753689
2、中序遍历的操作定义如下:
若二叉树为空,则空操作,否则
① 中序遍历左子树
② 访问根结点
③ 中序遍历右子树
中序遍历右图结果为:742513869
3、后序遍历的操作定义如下:
若二叉树为空,则空操作,否则
① 后序遍历左子树
② 后序遍历右子树
③ 访问根结点
后序遍历右图结果为:745289631
习题1:上面的图表示的二叉树的前序遍历序列:______________________ 中序遍历序列:____________________ 后续遍历序列:______________________ 。
从根出发,逆时针外像绕行全树,按第一次经过的时间次序列出各结点即为前序列表、按叶结点第一次经过时列出,内部结点第二次经过时列出即为中序列表、按最后一次经过的次序列表即为后序列表。
习题2:在磁盘的目录结构中,我们将与某个子目录有关联的目录数称为度。例如下图:
该图表达了A盘的目录结构:Dl,Dll_…D2均表示子目录的名字.在这里,根目录的度为2。Dl子目录的度为2,D1l子目录的度为3, D12,D2,Dlll,D112,D113的度均为0。若不考虑子目录的名字.则可简单的图示为如下的树结构:若知道一个磁盘的目录结构中.度为2的子目录有2度为3的子目录有l个,度为4的子目录有3个。
试问:度为1的子目录共有几个。
习题3:设M叉树采用列表法表示,即每棵子树对应一个列表,列表的结构为:子树根结点的值部分(设为一个字符)和用“()”括起来的各子树的列表(如有子树的话),各子列表问用“,”分隔。例如下面的三叉树可用列表a(b(c,d),e,f(g,h,i))表示。
写出右图的三叉树用列表形式表示的结果:________________________________________。
⑶遍历算法:
① 链表算法:
procedure preorder(bt:tree);{先序遍历根结点为 bt 的二叉树的递归算法}
begin
if bt<>nil then begin
write(bt^.data);
preorder(bt^.lchild);
preorder(bt^.rchild);
end;
end;
② 数组算法:
procedure preorder(i:integer);
begin
if i<>0 then begin
write(tree[i].data);
preorder(tree[i].lchild);
preorder(tree[i].rchild);
end;
end;
大家很容易就可以改写出中序和后序遍历的递归过程。当然,我们还可以把递归过程改成用栈实现的非递归过程,以先序遍历为例,其它的留给作者完成。
Procedure inorder(bt:tree); {先序遍历bt所指的二叉树}
Var stack:array[1..n] of tree;
Top:integer; P:tree;
begin
top:=0;
While not ((bt=nil)and(top=0)) do
Begin
While bt<>nil do begin
Write(bt^.data);
Top:=top+1;
Stack[top]:=bt^.rchild
bt:=bt^.lchild;
end;
If top<>0 then begin
b:=stack[top];
top:=top-1;
End;
End;
End;
二、二叉树的其它重要操作:
(1)、建立一棵二叉树
Procedure pre_crt(var bt:tree);{按先序次序输入二叉树中结点的值,生成
begin 二叉树的单链表存储结构,bt 为指向根结点的指针,’’表示空树}
Read(ch);
If ch=’.’ then bt:=nil
Else begin
New(bt); {建根结点}
Bt^.data:=ch;
Pre_crt(bt^.lchild); {建左子树}
Pre_crt(bt^.rchild); {建右子树}
End;
End;
(2)、删除二叉树
Procedure dis(var bt:tree); {删除二叉树}
Begin
If bt<>nil then begin
Dis(bt^.lchild); {删左子树}
Dis(bt^.rchild); {删右子树}
Dispose(bt); {释放父结点}
End;
End;
(3)、插入一个结点到二叉树中
Procedure insert(var bt:tree;n:integer); {插入一个结点到二叉树中}
Begin
If bt=nil then begin
new(bt);
bt^.data:=n;
bt^.lchild:=nil;
bt^.rchild:=nil;
End
Else if n<bt^.data then insert(bt^.lchild,n)
else if n>bt^.data then insert(bt^.rchild,n);
End;
(4)、在二叉树中查找一个数,找到返回该结点,否则返回 nil
Function find(bt:tree;n:integer):tree;{在二叉树中查找一个数,
Begin 找到返回该结点,否则返回 nil }
if bt=nil then find:=nil
else if n<bt^.data then find(bt^.lchild,n)
else if n>bt^.data then find(bt^.rchild,n)
else find:=bt;
end;
(5)、用嵌套括号表示法输出二叉树
Procedure print(bt:tree); {用嵌套括号表示法输出二叉树}
Begin
If bt<>nil then begin
Write(bt^.data);
If (bt^.lchild<>nil)or(bt^.rchild<>nil) then
Begin
Write(‘(’);
Print(bt^.lchild);
If bt^.rchild<>nil then write(‘,’);
Print(bt^.rchild);
Write(‘)’);
End;
End;
End;
例:输入一颗二叉树,分别输出前序、中序、后序遍历的结果。
如:二叉树 应在键盘输入: AB.C.D..EF..GH..IJ…
参考程序:program lx61(input,output);{指针类型}
type
tpointer=^node;
node=record
lch,rch:tpointer;
data:char
end;
var
root:tpointer;
ch:char;
procedure preorder(p:tpointer);
begin
if p<>nil then
begin write(p^.data);preorder(p^.lch); preorder(p^.rch);end;
end;
procedure inorder(p:tpointer);
begin
if p<>nil then
begin inorder(p^.lch);write(p^.data);inorder(p^.rch);end;
end;
procedure postorder(p:tpointer);
begin
if p<>nil then
begin postorder(p^.lch); postorder(p^.rch);write(p^.data); end;
end;
procedure create(var p:tpointer);
begin
read(ch);
if ch<> '.' then
begin new(p);p^.data:=ch;create(p^.lch);create(p^.rch) end
else p:=nil
end;
begin
write('input node= ');create(root);
preorder(root);writeln;inorder(root);writeln;postorder(root);writeln
end.
program lx61; {数组类型}
tree[i].data |
tree[i].lch |
tree[i].rch |
A |
2 |
3 |
B |
0 |
4 |
E |
5 |
6 |
C |
0 |
7 |
F |
0 |
0 |
G |
8 |
9 |
D |
0 |
0 |
H |
0 |
0 |
I |
10 |
0 |
J |
0 |
0 |
const m=100;
type node=record
data:char;
lch,rch:0..m;
end;
var tree:array[1..m]of node;
n,i,j:1..m;
procedure preorder(i:integer);
begin
if i<>0 then
begin write(tree[i].data:4);preorder(tree[i].lch);preorder(tree[i].rch) end;
end;
procedure inorder(i:integer);
begin
if i<>0 then
begin inorder(tree[i].lch);write(tree[i].data:4);inorder(tree[i].rch) end;
end;
procedure postorder(i:integer);
begin
if i<>0 then
begin postorder(tree[i].lch);postorder(tree[i].rch);write(tree[i].data:4) end;
end;
begin
readln(n);
for i:=1 to n do
begin
read(tree[i].data);
read(tree[i].lch);
readln(tree[i].rch);
end;
preorder(1);writeln;inorder(1);writeln;postorder(1);writeln;
end.
三、普通有序树的另一种遍历方法
先根遍历:转化为对应的二叉树后由前序遍历实现。
ABEKLFCGDHIJ
后根遍历:转化为对应的二叉树后由中序遍历实现。
KLEFRGCHIJDA
例:P83 输入一颗普通有序树,输出该树的先根次序和后根次序。
参考程序:program lx83;
const m=100;
type
node=record
data:char;
prt,lch,rch:1..m;
end;
var tree:array[1..m]of node;
i,j,p,n:1..m;
procedure preorder(i:integer);
begin
if i<>0 then begin
write(tree[i].data:3);
preorder(tree[i].lch);
preorder(tree[i].rch);
end;
end;
procedure inorder(i:integer);
begin
if i<>0 then begin
inorder(tree[i].lch);
write(tree[i].data:3);
inorder(tree[i].rch);
end;
end;
begin
fillchar(tree,sizeof(tree),0);
readln(n);
for i:=1 to n do
begin
read(tree[i].data);
read(j);
if j<>0 then begin
tree[i].lch:=j;tree[j].prt:=i;
p:=j;
repeat
read(j);
if j<>0 then begin
tree[p].rch:=j;tree[j].prt:=p;
p:=j;
end;
until j=0;
end;
readln;
end;
preorder(1);writeln;inorder(1);
end.
四、 由二叉树的两种遍历顺序确定树结构
结论:已知前序序列和中序序列可以确定出二叉树;
已知中序序列和后序序列也可以确定出二叉树;
但,已知前序序列和后序序列却不可以确定出二叉树;为什么?举个 3个结点的反例。
例如:已知结点的前序序列为 ABCDEFG,中序序列为 CBEDAFG。构造出二叉树。过程见下图:
例:输入二叉树的前序与中序,输出二叉树的后序。
参考程序:
program lx89;
var c1,c2:string;
procedure solve2(s1,s2:string);
var k:integer;
begin
k:=pos(s2[1],s1);
if k>1 then solve2(copy(s1,1,k-1),copy(s2,2,k));
if k<length(s1) then solve2(copy(s1,k+1,length(s1)-k),copy(s2,k+1,length(s2)-k));
write(s2[1]);
end;
begin
write('input inorder='); readln(c1);
write('input preorder='); readln(c2);
solve2(c1,c2);
end.
例:输入二叉树的中序与后序,输出二叉树的前序。
参考程序:
program lx88;
var c1,c2:string;
procedure solve(s1,s2:string);
var k:integer;
begin
if length(s2)=1 then write(s2)
else begin
k:=pos(s2[length(s2)],s1);
write(s1[k]);
if k>1 then solve(copy(s1,1,k-1),copy(s2,1,k-1));
if k<length(s1) then solve(copy(s1,k+1,length(s1)-k),copy(s2,k,length(s2)-k));
end;
end;
begin
write('input inorder='); readln(c1);
write('input postorder='); readln(c2);
solve(c1,c2);
end.
4.5 二叉树的重要应用
(1)二叉排序树
①定义:二叉排序树是空树或具有以下性质:
若根结点的左子树不空,则左子树的所有结点值均小于根结点值。
若根结点的右子树不空,则右子树的所有结点值均大于根结点值。
根结点的左右子树也分别为二叉排序树。
②应用:中序遍历二叉排序树,即可顺序输出各结点。
例:输入 n (1≤ n ≤100)
a1, a2, …. An
输出: 排序后的结果
program lx91;
const m=1000;
type benode=record {用于存储树的结构}
data:integer;
lch,rch:1..m;
end;
var b:array[1..m]of benode;
p,n,i:1..m; a:array[1..m]of integer; {用于保存输入的n个数}
procedure createtree;
begin
fillchar(b,sizeof(b),0);
b[1].data:=a[1];
for i:=2 to n do
begin
b[i].data:=a[i]; p:=1;
while true do
begin
if a[i]<b[p].data then
if b[p].lch<>0 then p:=b[p].lch
else begin b[p].lch:=i; break; end
else if b[p].rch<>0 then p:=b[p].rch
else begin b[p].rch:=i; break end;
end;
end;
end;
procedure inorder(i:integer);
begin
if i<>0 then begin
inorder(b[i].lch);
write(b[i].data:5);
inorder(b[i].rch);
end;
end;
begin
write('n= ');readln(n);
write('a[i]= '); for i:=1 to n do read(a[i]); writeln;
createtree;
inorder(1);
end.
构造二叉排序树的平均时间复杂度为(n long 2N),优于选择排序和冒泡排序算法的(n2)
(2)最优二叉树
①定义:在具有n个带权叶子结点的二叉树中,使所有叶子结点的带权路径长度之和为最小的二叉树称为最小二叉树(或哈夫曼树)。 即:最小二叉树使 L =达到最小。
( 其中Wk 表示第k个叶结点的权值,P k表示第个k叶结点的路径长)
例:以下四棵二叉树中,叶结点为A、B、C、D、E ,权值分别为7、5、2、4、6。
L=3 (7+5)+2(2+4+6)=60 L=4(4+2)+3*5+2*6+7=58 L=7+2*5+3*2+4*5+6*5=63 L=2(7+5+6)+3(2+4)=54
②最优二叉树的构造方法:将n个结点看着是n个二叉树的根,则得到森林 F={T1, T2, ….Tn}
在F中取出权值最小的两棵树合并成新树,其根值为两棵子树根之和。
用新树代替原来的两棵树构成新的森林。
重复以上两步,直到森林合并成一颗树为止。
例:全校学生的成绩由百分制转换为五等分制,在五个等级上的分布不均匀,分布规律如下表:
分布范围: 0~59 60~69 70~79 80~89 90~100
分布情况%: 5 15 35 30 15
如果给出sum(1≤sum≤100000)个学生的百分制成绩,将它们转换成五分制成绩,至少要经过多少次比较,其中每个等次成绩的最少比较多少次。
输入:sum
w1, w2, w3, w4, w5 (五个等级的成绩的分布规律)
输出:最少比较次数
5个整数,分别为每个等次成绩的最少比较次数。
参考程序
program lx95;
const n=5;
m=2*n-1;
type
node=record
data:integer;
prt,lch,rch,lth:0..m;
end;
wtype=array[1..n]of integer;
treetype=array[1..m]of node;
var tree:treetype;
w:wtype;
bt,m1,i,j,k,p,sum:integer;
procedure hufm(w:wtype;var tree:treetype;var bt:integer);
function min(h:integer):integer;
var i:integer;
begin
m1:=maxint;
for p:=1 to h do
if (tree[p].prt=0)and(m1>tree[p].data)
then begin i:=p;m1:=tree[p].data;end;
min:=i;
end;
begin
fillchar(tree,sizeof(tree),0);
for i:=1 to n do tree[i].data:=w[i];
for k:=n+1 to m do begin
i:=min(k-1);tree[i].prt:=k;tree[k].lch:=i;
j:=min(k-1);tree[j].prt:=k;tree[k].rch:=j;
tree[k].data:=tree[i].data+tree[j].data;
end;
bt:=m;
end;
procedure ht(t:integer);
begin
if t=m then tree[t].lth:=0 else tree[t].lth:=tree[tree[t].prt].lth+1;
if tree[t].lch<>0 then begin ht(tree[t].lch);ht(tree[t].rch) end;
end;
begin
readln(sum);
for i:=1 to 5 do read(w[i]);
hufm(w,tree,bt);
ht(bt);
writeln(sum*tree[bt].data);
for i:=1 to 5 do write(sum*tree[i].lth*tree[i].data:7);
end.
第五章图
5.1 概念
1、定义:图是由顶点V的集合和边E的集合组成的二元组记G=(V,E)
下图是一无向图(顶点的前后顺序不限)
图1
V={V1,V2,V3,V4,V5}
E={(V1,V2),(V2,V3),(V3,V4),(V4,V5),(V5,V1),(V2,V5),(V4,V1)}
下图是一有向图(顶点分先后顺序)
V={V1,V2,V3,V4}
E={<V1,V2>,<V2,V4>,<V1,V3>,<V3,V4>,<V4,V1>}
2、完全图(每一对不同的顶点都有一条边相连,n个顶点的完全图共有n(n-1)/2条边)
3、顶点的度:与顶点关联的边的数目,有向图中等于该顶点的入度与出度之和。
入度——以该顶点为终点的边的数目和
出度——以该顶点为起点的边的数目和
度数为奇数的顶点叫做奇点,度数为偶数的点叫做偶点。
[定理1] 图G中所有顶点的度数之和等于边数的2倍。因为计算顶点的度数时。每条边均用到2次。
[定理2] 任意一个图一定有偶数个奇点。
4、路径和连通集
在图G=(V,E)中,如果对于结点a,b,存在满足下述条件的结点序列x1……xk(k>1)
⑴ x1=a,xk=b
⑵ (xi,xi+1)∈E i=1¨k-1
则称结点序列x1=a,x2,…,xk=b为结点a到结点b的一条路径,而路径上边的数目k-1称为该路径的长度,并称结点集合{x1,…,xk}为连通集。
5、简单路径和回路
如果一条路径上的结点除起点x1和终点xk可以相同外,其它结点均不相同,则称此路径为一条简单路径。上图1中v1→v2→v3是一条简单路径,v1→v2→v5→v1→v4不是简单路径。x1=xk的简单路径称为回路(也称为环)。例如上图2中,v1→v2→v4→v1为一条回路。
6、有根图
在一个有向图或无向图中,若存在一个结点w,它与其他结点都是连通的,则称此图为有根图,结点w即为它的根。上图1为有根图,v1、v2、v3、v4都可以作为根。
7、连通图和最大连通子图
对于无向图而言,若其中任两个结点之间的连通,则称该图为连通图。一个无向图的连通分支定义为此图的最大连通子图。上图(a)所示的图是连通的,它的最大连通子图即为其本身。
8、强连通图和强连通分支
若对于有向图的任意两个结点vi、vj间(vi≠vj),都有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称该有向图是强连通的。有向图中强连通的最大子图称为该图的强连通分支。
5.2图的存储
1.邻接矩阵
1(或权值) 表示 顶点i和顶点j有边(i和j的路程)
A(i,j)={
0 表示顶点i和顶点j无边
如图1的矩阵表示为
2.邻接表
数组方法:
顶点 相邻顶点 邻接顶点数
v[i] d[i,j] c[i]
1 |
|
2 |
4 |
5 |
|
|
3 |
2 |
|
1 |
3 |
5 |
|
|
3 |
3 |
|
2 |
4 |
|
|
|
2 |
4 |
|
1 |
3 |
5 |
|
|
3 |
5 |
|
1 |
2 |
4 |
|
|
3 |
链表方法:
1 |
--> |
2 |
--> |
4 |
--> |
5 |
^ |
2 |
--> |
1 |
--> |
3 |
--> |
5 |
^ |
3 |
--> |
2 |
--> |
4 |
^ |
|
|
4 |
--> |
1 |
--> |
3 |
--> |
5 |
^ |
5 |
--> |
1 |
--> |
2 |
--> |
4 |
^ |
3.边目录
如图2
起点 |
1 |
1 |
2 |
3 |
4 |
终点 |
2 |
3 |
4 |
4 |
1 |
长度 |
|
|
|
|
|
6.3图的遍历
1.深度优先遍历
遍历算法:
1)从某一顶点出发开始访问,被访问的顶点作相应的标记,输出访问顶点号.
2)从被访问的顶点出发,搜索与该顶点有边的关联的某个未被访问的邻接点
再从该邻接点出发进一步搜索与该顶点有边的关联的某个未被访问的邻接点,直到全部接点访问完毕.如图1从V1开始的深度优先遍历序列为V1,V2,V3,V4,V5。图2从V1开始的深度优先遍历序列为V1,V2,V4,V3。
算法过程:
procedure shendu(i);
begin
write(i);
v[i]:=true;
for j:=1 to n do
if (a[i,j]=1) and not(v[j]) then shendu(j);
end;
2.广度优先遍历
遍历算法:
1)从某个顶点出发开始访问,被访问的顶点作相应的标记,并输出访问顶点号;
2)从被访问的顶点出发,依次搜索与该顶点有边的关联的所有未被访问的邻接点,并作相应的标记。
3)再依次根据2)中所有被访问的邻接点,访问与这些邻接点相关的所有未被访问的邻接点,直到所有顶点被访问为止。如图3的广度优先遍历序列为C1,C2,C3,C4,C5,C6。
算法过程:
procedure guangdu(i);
begin
write(i);
v[i]:=true;
i进队;
repeat
队首元素出队设为k
for j:=1 to n do
if (a[k,j]=1) and (not v[j]) then
begin
write(j);
v[j]:=true;
j进队;
end;
until 队列q为空;
6.3 图的应用
例1:有A,B,C,D,E 5本书,要分给张、王、刘、赵、钱5位同学,每人只选一本。每人将喜爱的书填写下表,输出每人都满意的分书方案。
|
A |
B |
C |
D |
E |
张 |
|
|
1 |
1 |
|
王 |
1 |
1 |
|
|
1 |
刘 |
|
1 |
1 |
|
|
赵 |
|
|
|
1 |
|
钱 |
|
1 |
|
|
1 |
用递归方式程序如下:
program allotbook;
type five=1..5;
const like:array[five,five] of 0..1=
((0,0,1,1,0),(1,1,0,0,1),(0,1,1,0,0),(0,0,0,1,0),(0,1,0,0,1));
name:array[five] of string[5]=('zhang','wang','liu','zhao','qian');
var book:array[five] of five;
flag:set of five;
c:integer;
procedure print;
var i:integer;
begin
inc(c);
writeln('answer',c,':');
for i:=1 to 5 do
writeln(name[i]:10,':',chr(64+book[i]));
end;
procedure try(i:integer);
var j:integer;
begin
for j:=1 to 5 do
if not(j in flag) and (like[i,j]>0) then
begin flag:=flag+[j];
book[i]:=j;
if i=5 then print else
try(i+1);
flag:=flag-[j];
end
end;
begin
flag:=[];
c:=0;
try(1);
readln;
end.
用非递归方法编程如下:
program allotbook;
type five=1..5;
const like:array[five,five] of 0..1=
((0,0,1,1,0),(1,1,0,0,1),(0,1,1,0,0),(0,0,0,1,0),(0,1,0,0,1));
name:array[five] of string[5]=('zhang','wang','liu','zhao','qian');
var book:array[five] of five;
flag:set of five;
c,dep,r:integer;
p:boolean;
procedure print;
var i:integer;
begin
inc(c);
writeln('answer',c,':');
for i:=1 to 5 do
writeln(name[i]:10,':',chr(64+book[i]));
end;
begin
flag:=[];
c:=0;dep:=0;
repeat
dep:=dep+1;
r:=0;
p:=false;
repeat
r:=r+1;
if not(r in flag) and (like[dep,r]>0) then
begin
flag:=flag+[r];
book[dep]:=r;
if dep=5 then print else p:=true
end
else if r>=5 then
begin
dep:=dep-1;
if dep=0 then p:=true else begin
r:=book[dep];flag:=flag-[r] end;
end else p:=false;
until p=true;
until dep=0;
readln;
在应用树结构解决问题时,往往要求按照某种次序获得树中全部结点的信息,这种操作
叫作树的遍历。遍历的方法有多种,常用的有:
A、先序(根)遍历:先访问根结点,再从左到右按照前序思想遍历各棵子树。
如上图前序遍历的结果为:ABEKLFCGDHMIJ;
B、后序(根)遍历:先从左到右遍历各棵子树,再访问根结点。如上图后序
遍历的结果为:KLEFBGCMHIJDA;
C、层次遍历:按层次从小到大逐个访问,同一层次按照从左到右的次序。
如上图层次遍历的结果为:ABCDEFGHIJKLM;
大家可以看出,AB 两种方法的定义是递归的,所以在程序实现时往往也是采用递归的
思想。既通常所说的“深度优先搜索”。如用前序遍历编写的过程如下:
procedure tra1(t,m) {递归}
begin
if t <>nil then begin
write(t^.data,’ ’); {访问根结点}
for I:=1to m do {前序遍历各子树}
tra1(t^.child[I],m);
end;
end;
C 这种方法应用也较多,实际上是我们将的“广度优先搜索”。思想如下:若某个结点
被访问,则该结点的子结点应登录,等待被访问。顺序访问各层次上结点,直至不再有未访
问过的结点。为此,引入一个队列来存储等待访问的子结点,设一个队首和队尾指针分别表
示出队、进队的下标。程序框架如下:
Const n=100;
Var hend,tail,I:integer;
Q:array[1..n] of tree;
P:tree;
Begin
Tail:=1;head:=1;
Q[tail]:=t; {t进队}
Tail:=tail+1;
While ( head<tail) do {队列非空}
Begin
P:=q[head]; {取出队首结点}
Head:=head+1;
Write(p^.data,‘ ‘); {访问某结点}
For I:=1 to m do {该结点的所有子结点按顺序进队}
If p^.child[I]<>nil then begin
q[tail]:=p^.child[I];
tail:=tail+1;
end;
End;
End;
Chaobs编辑,转载请注明出处