配色: 字号:
《数据结构》考研考点讲义
2022-11-24 | 阅:  转:  |  分享 
  
目 录

《数据结构》考研分析与指导(1)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第一章 绪论(4)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第二章 线性表(11)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第三章 限定性线性表———栈和队列(31)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第四章 串(50)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第五章 数组和广义表(59)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第六章 树与二叉树(68)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第七章 图(99)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第八章 查找(121)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

第九章 内部排序(148)G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21 G21

《数据结构》考研考点讲义

《数据结构》考研分析与指导

一、考试特点分析

1.在计算机专业硕士研究生入学全国统考中,作为专业课综合中的一个版块进行考察。在部分

自主命题院校硕士研究生入学考试中有单独作为一个科目进行考察,也有和其他一门或者两门科目

联合出题。

2.出题形式多为选择题和综合应用题

3.侧重于基础知识点及对知识点灵活运用的考核

二、复习方法

1.分清复习的阶段,把握复习进度

2.亲手做题,在练习中总结出题方向和方法,重视真题,透彻分析,揣摩出题人心理。只有做好一

定量的习题,才能帮助理解和牢固掌握考点。

3.讲过的知识点和题彻底掌握并及时回顾,不要学过忘过。

4.注重知识点之间的联系

5.不可忽视基础概念和知识体系。但是同时还要做到重点突出,突破难点,查缺补漏。

三、考试内容及分值分布:

章节重点难点必考点考试题型

1.绪论选择

2.线性表√√√选择、综合分析

3.栈和队列√√√选择、综合分析

4.串√√选择、填空

5.数组和广义表选择、填空

6.树√√√选择、综合分析

7.图√√√选择、综合分析

8.查找√√√选择、综合分析

9.排序√√√选择、综合分析

四、章节知识点及题型分析

章知识点题型题型易考点



数据结构的定义、逻辑结构和物理结构

的分类、算法的定义和算法性能的复

杂度

选择题

逻辑结构和物理结构的分类、算法的定义

和算法性能的复杂度



1.线性表的概念及运算

2.线性表的顺序存储

3.线性表的链式存储

4.一元多项式的表示及相加

选择题

1.线性表的定义和基本运算

2.线性表的顺序存储

3.线性表的链式存储

综合分析题1.线性表算法的设计



1.栈的定义和运算

2.栈的顺序存储和链式存储

3.栈的应用

1.队列的定义和运算

2.队列的顺序存储和链式存储

3.队列的应用

选择题

1.栈和队列的定义和运算

2.栈和队列的顺序存储和链式存储

综合分析题栈和队列的应用算法



1.串类型的定义

2.串的顺序存储和链式存储

3.串的模式匹配算法

选择题1.串的定义和存储

综合分析题2.串的模式匹配算法



1.数组的定义和运算

2.数组的顺序存储和实现

3.特殊矩阵的压缩存储

4.广义表

选择题

数组和广义表的定义和存储

矩阵的压缩存储



1.树的概念与定义

2.二叉树的定义和存储结构

3.二叉树的遍历与线索化

4.树、森林和二叉树的关系

5.哈夫曼树及其应用

选择题

树与二叉树的定义和存储结构

树、森林和二叉树的关系

综合分析题哈夫曼树及其应用



1.图的定义与图的存储结构

2.图的遍历

3.最小生成树

4.拓扑排序和关键路径

5.最短路径

选择题图的定义和存储结构

综合分析题

最小生成树

拓扑排序

关键路径

最短路径

续表

章知识点题型题型易考点



1.查找的基本概念

2.基于静态查找表的查找算法

3.基于动态查找表的查找算法

4.哈希表的查找

选择题查找的定义

综合分析题

顺序查找

折半查找

索引顺序表查找

二叉排序树

平衡二叉排序树

B树

哈希冲突的解决



①插入排序

②交换排序

③选择排序

④归并排序

⑤基数排序

综合分析题各种排序方法的应用

第一章 绪论

一、考试分析

考点重点与难点考试中常见题型复习思路与方法

数据、数据结构、算法的基

本概念,数据的逻辑结构和

物理结构

数据的四种逻辑结构和四

种物理结构、算法复杂度

选择题、

综合分析题

1.熟记概念;

2.理解并熟练掌握数据的逻辑结构

和物理结构,以及算法复杂度。

二、考点讲解

数据结构是一门研究非数值计算的程序设计问题时处理的操作对象以及它们之间的关系和操作

等等的学科。

1.基本概念

·数据(Data):

对客观事物的符号描述,能输入到计算机中并被计算机程序处理的符号的总称;

能被计算机识别、存储和加工处理的信息的载体。

例,数字:自然数、整数

字母:a~z,单词

图像

视频、音频信号等

表格

·数据元素(DataElement):

数据元素是组成数据的基本单位,是数据集合的个体,在计算机中通常作为一个整体进行考虑和

处理。

例,“对弈树”中的一个格局

书目信息中的一条书目

数据项:一个数据元素可由若干个数据项组成。

例,一条书目信息是由书名、作者名、分类等多个数据项组成的数据项是数据的不可分割的最小

单位。

例如:有一个学生表如下所示。这个表中的数据元素是学生记录,每个数据元素由四个数据项

(即学号、姓别、性别和班号)组成。

学号姓名性别班号

1张斌男9901

8刘丽女9902

34李英女9901

20陈华男9902

12王奇男9901

26董强男9902

5王萍女9901

·数据结构(DataStructure)

数据结构是指相互之间存在一种或多种特定关系的数据元素

集合

结构(Structure):数据元素相互之间的关系。

在形式上可用二元组表示:

Data_Structure=(D,S)

   D:数据元素的有限集

   S:D上关系的有限集

D={k



|1≤i≤n,n≥0}

·k



表示集合D中的第i个结点或数据元素

·n为D中结点的个数

·若n=0,则D是一个空集,表示D无结构可言,有时也可以认为它具有任意的结构

S={r



|1≤j≤m,m≥0}

·r



表示集合S中的第j个二元关系(简称关系)

·m为S中关系的个数

·若m=0,则S是一个空集,表明集合D中的元结点间不存在任何关系,彼此是独立的

D上的一个关系r是序偶的集合,对于r中的任一序偶<x,y>(x,y∈D),我们称序偶的第一结

点为第二结点的直接前驱结点(通常简称前驱结点),称第二结点为第一结点的直接后继结点(通常简

称后继结点)。如在<x,y>的序偶中,x为y的前驱结点,而y为x的后继结点。

若某个结点没有前驱,则称该结点为开始结点;若某个结点没有后继,则称该结点为终端结点;除

此之外的节点称为内部节点。

“尖括号”表示有向关系,“圆括号”表示无向关系。

例如:用二元组表示学生表,学生表中共有7个结点,依次用k



~k



表示,则对应的二元组表

示为:

Data_Structure=(D,S)

其中:

D={k



,k



,k



,k



,k



,k



,k





S={<k



,k



>,<k



,k



>,<k



,k



>,<k



,k



>,<k



,k



>,<k



,k



>}

逻辑结构图:可以将数据结构用图形形象地表示出来,图形中的每个结点对应着一个数据元素,

两结点之间的连线对应着关系中的一个序偶。

上述“学生表”数据结构用下图的图形表示。

2.数据结构的内容

·逻辑结构

数据元素之间的关系

逻辑结构可看作是从具体问题抽象出来的数学模型

按照逻辑关系的不同特性分类:

逻辑结构类型的分类

(1)线性结构

所谓线性结构,该结构中的结点之间存在一对一的关系。

其特点是:开始结点和终端结点都是惟一的,除了开始结点和终端结点以外,其余结点都有且仅

有一个前驱结点,有且仅有一个后继结点。

顺序表就是典型的线性结构。

(2)非线性结构

所谓非线性结构,该结构中的结点之间存在一对多或多对多的关系。它又可以细分为树形结构

和图形结构两类。

所谓树形结构,该结构中的结点之间存在一对多的关系。其特点是每个结点最多只有一个前驱,

—6—

但可以有多个后继,可以有多个终端结点。非线性结构树形结构简称为树。

UNIX文件系统的系统结构图

所谓图形结构,该结构中的结点之间存在多对多的关系。其特点是每个结点的前驱和后继的个

数都可以是任意的。因此,可能没有开始结点和终端结点,也可能有多个开始结点、多个终端结点。

图形结构简称为图。

·存储结构(物理结构)

逻辑结构在计算机中的存储映象,是逻辑结构在计算机中的实现,它包括数据元素的表示和关系

的表示。

顺序存储结构

非顺序存储结构(链式存储结构)

索引存储结构

散列存储结构

例如:用顺序存储法和链式存储法表示下面的学生表。

学号姓名性别班号

1张斌男9901

8刘丽女9902

34李英女9901

20陈华男9902

12王奇男9901

26董强男9902

5王萍女9901

用顺序存储法存放学生表的结构体定义为:

struct Stud{

   intno;        /学号/

   charname[8];    /姓名/

   charsex[2];     /性别/

   charclass[4];   /班号/

 } Studs[7]={

   {1,“张斌”,“男”,“9901”},

   …,

   {5,"王萍","女","9901"}

   };

结构体数组Studs各元素在内存中按顺序存放,即第i(1≤i≤6)个学生对应的元素Studs[i]存放

在第i+1个学生对应的元素Studs[i+1]之前,Studs[i+1]正好在Studs[i]之后。

用链式存储法存放学生表的结构体定义为:

typedefstructnode

  {

intno;    /学号/

charname[8];   /姓名/

charsex[2];     /性别/

charclass[4];   /班号/

structnodenext;/指向下个学生的指针/

  }StudType;

学生表构成的链表如下图所示。其中的head为第一个数据元素的指针。

链式存储法的缺点:

·存储空间占用大

·无法随机访问

链式存储法的优点:

·便于修改(插入、删除、移动)

3.算法

(1)算法(Algorithm)的定义

Algorithmisafinitesetofruleswhichgivesasequenceofoperationforsolvingaspecifictypeofprob

lem.(算法是规则的有限集合,是为解决特定问题而规定的一系列操作。)

(2)算法的特性

①有穷性:有限步骤之内正常结束,不能形成无穷循环。

②确定性:算法中的每一个步骤必须有确定含义,无二义性。

③可行性:原则上能精确进行,操作可通过已实现的基本运算执行有限次而完成。

④输入:有多个或0个输入。

⑤输出:至少有一个或多个输出。

在算法的五大特性中,最基本的是有限性、确定性和可行性。

4.算法描述的工具

描述算法的方法

·自然语言:优点———简单。缺点———有歧异,表达复杂思想不明晰,不能和实现方式很好结合

·高级程序设计语言,如Pascal,C/C++,Java等。优点———克服了自然语言的缺点,可直接执

行。缺点———对部分问题的描述比较烦杂,嗦

·类语言。和高级程序设计语言类似,但是对其中一些比较烦杂的部分进行和简化(原因:算

法主要目的是为了清晰的表述思想)

举例:两个数据a,b交换空间

自然语言:交换a,b的存储空间;

高级语言:{x=a;a=b;b=x;}

类语言:ab;//交换空间

5.对算法作性能评价

衡量算法效率的方法主要有两大类:

·事后统计:利用计算机的时钟;

·事前分析估算:用高级语言编写的程序运行的时间主要取决于如下因素:

算法;

问题规模;

使用语言:级别越高,效率越低;

编译程序;

机器;

通常,从算法中选取一种对于研究的问题来说是基本操作的原操作,以该基本操作重复执行的次

数作为算法执行的时间度量。

基本操作重复执行的次数分别为1,n,n



《数据结构》考研考点精讲及复习思路

设算法的问题规模为n;

频度:语句重复执行的次数称为该语句的频度,记f(n)。

对算法各基本操作的频度求和,便可得算法的时间复杂度。但实际中我们所关心的主要是一个

算法所花时间的数量级,即取算法各基本操作的最大频度数量级。

时间复杂度:算法执行时间度量,记T(n)=O(maxlevel(f(n)))。

f(n)=1+n+n



+n



T(n)=O(n





O的数学定义:

若T(n)和f(n)是定义在正整数集合上的两个函数,则如果存在正常数C和n



,使得当n≥n



时,

总满足0≤T(n)≤Cf(n),则记做T(n)=O(f(n))

也就是只求出T(n)的最高阶(数量级),忽略其低阶项和常系数,这样既可简化T(n)的计算,又

能比较客观地反映出当n很大时,算法的时间性能。

6.算法的空间复杂度

关于算法的存储空间需求,类似于算法的时间复杂度,我们采用空间复杂度作为算法所需存储空

间的量度,记作:

S(n)=O(f(n))

三、真题举例

1.从逻辑结构上可以把数据结构分为(  )两大类.【武汉交通科技大学】

A.动态结构、静态结构           

B.顺序结构、链式结构

C.线性结构、非线性结构

D.初等结构、构造型结构

2.在下面的程序段中,对x的赋值语句的频度为(  )【北京工商大学】

FORi:=1TOnDO

 FORj:=1TOnDO

    x:=x+1;

A.O(2n)     B.O(n)     C.O(n



)     D.O(log



n)

3.以下属于逻辑结构的是(  )【西安电子科技大学】

A.顺序表B.哈希表C.有序表D.单链表

四、本讲小结

本章讲解了数据、数据结构、算法的基本概念,数据的逻辑结构和物理结构。

重点讲解了数据的4种逻辑结构和4种物理结构,以及算法复杂度。

第二章 线性表

目录分析

2.1 线性表的概念及运算[一般了解]

2.2 线性表的顺序存储[熟练掌握]

2.3 线性表的链式存储[熟练掌握]

第1讲

一、考试分析

考点重点与难点考试中常见题型复习思路与方法

线性表的基本概念和常用

操作、线性表的顺序存储

方式。

线性表在常用操作、线性表

的顺序存储。

选择题、

综合分析题

1.熟记概念;

2.理解并熟练掌握线性表的顺序存

储方式和线性表的基本运算在现行

存储方式下的实现方法。

二、考点讲解

2.1 线性表的概念及运算

1.线性表的定义

一个线性表是具有n个数据元素的有限序列。记为(a



,…,a

i-1

,a



,a

i+1

,…,a





2.线性表的长度

线性表中元素的个数n(n>=0),n=0时,称为空表。

3.位序





是第i个元素,称i为数据元素a



在线性表中的位序。

4.线性表的逻辑结构

例子:

·英文字母表(A,B,…,Z);

·车辆登记表。

5.线性表的特点

·同一性:线性表由同类数据元素组成,每一个a



必须属于同一数据对象。

·有穷性:线性表由有限个数据元素组成,表长度就是表中数据元素的个数。

·有序性:线性表中相邻数据元素之间存在着序偶关系<a



,a

i+1

>。

6.线性表的基本运算

·初始化InitList(&L)建立一个空表。

·求表长ListLength(L)返回线性表的长度。

·读表元素GetElem(L,i,&e)用e返回L中第i个数据元素的值。

·定位LocateElem(L,e,compare())返回满足关系的数据元素的位序。

·插入ListInsert(&L,i,e)在L中第i个位置之前插入新的数据元素e,线性表的长度增1。

·删除ListDelete(&L,i,&e)删除L的第i个位置上的数据元素e,线性表的长度减1。

·输出ListDisplay(L)按前后次序输出线性表的所有元素。

练习1:两个线性表LA和LB分别表示两个集合A和B,现求一个新的集合A=A∪B。

voidunion(List&La,ListLb)



La_len=ListLength(La);

Lb_len=ListLength(Lb);

   for(i=1;i<=Lb_len;i++){

GetElem(Lb,i,e);

      if(!LocateElem(La,e,equal))

ListInsert(La,++La_len,e); 

    }



     O(ListLength(La)×ListLength(Lb))

练习2:

两个线性表LA和LB中的数据元素按值非递减有序排列,现将LA和LB归并为一个新的线性

表,LC中的数据元素仍按值非递减有序排列。

      LA=(3,5,8,11)

      LB=(2,6,8,9,11,15,20)

      LC=(2,3,5,6,8,8,9,11,11,15,20) 

c=

a,当aG21b时

b,当a>b

{



voidMergeList(ListLa,ListLb,List&Lc)

{InitList(Lc);

La_len=ListLength(La); Lb_len=ListLength(Lb);

   i=j=1;k=0;

   while((i<=La_len)&&(j<=Lb_len)){

GetElem(La,i,a);GetElem(Lb,j,b);

      if(a<=b){ListInsert(Lc,++k,a);++i;}

      else{ListInsert(Lc,++k,b);++j;}

   }

   while(i<=La_len){

GetElem(La,i++,a);ListInsert(Lc,++k,a);}

   while(j<=Lb_len){

GetElem(Lb,j++,b);ListInsert(Lc,++k,b);}



          O(ListLength(La)+ListLength(Lb))

例,La=(3,5,8),Lb=(2,6,8,9,15)

构造Lc=(2,3,5,6,8,8,9,15)

首先,La_len=3;Lb_len=5;

2.2 线性表的顺序表示和实现

1.顺序表:

按顺序存储方式构造的线性表。

《数据结构》考研考点精讲及复习思路

假设线性表中有n个元素,每个元素占k个单元,第一个元素的地址为loc(a



),则可以通过如下

公式计算出第i个元素的地址loc(a



):

loc(a



)=loc(a



)+(i-1)×k

其中loc(a



)称为基地址。

2.顺序表的特点:

·逻辑结构中相邻的数据元素在存储结构中仍然相邻。

·线性表的顺序存储结构是一种随机存取的存储结构。

3.顺序表的描述:

typedef struct



ElemType elem;

int   length; //当前长度

int    listsize; //分配的存储容量   

}SqList;

//ElemTypeelem[MAXSIZE];

typedef #ElemType;  #为根据具体问题确定的数据类型

typedef int Status;

4.顺序表上基本运算的实现

·初始化StatusInitList_Sq(SqList&L)



L.elem=(ElemType)malloc(LIST_INIT_SIZE

sizeof(ElemType));

  if(!L.elem)

     exit(OVERFLOW);

L.length=0;

L.listsize=LIST_INIT_SIZE;

  returnOK;



L.elem=newElemType[LIST_INIT_SIZE];

顺序表的插入:在表中第4个元素之前插入“21“。

顺序表中插入元素

·插入StatusListInsert_Sq(SqList&L,inti,ElemTypee)



   if((i<1)||(i>L.length+1))

      returnERROR;

   if(L.length>=L.listsize){

realloc(…);….;//越界处理;

   }

  q=&(L.elem[i-1]);

  for(p=&(L.elem[L.length-1];p>=q;--p)

     (p+1)=p;

  q=e;

  ++L.length;

  returnOK;



//越界处理

if (L.length>=L.listsize) {

newbase=(ElemType)realloc(L.elem,

(L.listsize+LISTINCREMENT)sizeof(ElemType));

if (!newbase) exit(OVERFLOW);

L.elem=newbase;

L.listsize+=LISTINCREMENT;

算法时间复杂度:

时间主要花在移动元素上,而移动元素的个数取决于插入元素位置。

i=1,需移动n个元素;

i=n+1,需移动0个元素;

i=i,需移动n-i+1个元素;

假设p



是在第i个元素之前插入一个新元素的概率

则长度为n的线性表中插入一个元素所需移动元素次数的期望E

is





n+1

i=1





(n-i+1)。

设在任何位置插入元素等概率,p







n+1





is





n+1



n+1

i=1

(n-i+1)=





O(n)

·顺序表的归并,表中元素非递减排列。

voidMergeList_Sq(SqListLa,SqListLb,SqList&Lc)



  pa=La.elem;pb=Lb.elem;

Lc.listsize=Lc.length=La.length+Lb.length;

  pc=Lc.elem=(ElemType)malloc(…);

  if(!Lc.elem)exit(OVERFLOW);

pa_last=La.elem+La.length-1;pb_last=Lb.elem+Lb.length-1;

  while((pa<=pa_last)&&pb<=pb_last)){

     if(pa<=pb) pc++=pa++;

     else pc++=pb++;}

  while(pa<=pa_last)pc++=pa++;

  while(pb<=pb_last)pc++=pb++;



顺序表的基础要点:

1.无需为表示元素间的逻辑关系而增加额外的存储空间,存储密度大(100%);

2.可随机存取表中的任一元素。

3.插入或删除一个元素时,需平均移动表的一半元素,具体的个数与该元素的位置有关,在等概

率情况下,插入n/2,删除(n-1)/2;O(n)

4.存储分配只能预先进行分配。

5.将两个各有n个元素的有序表归并为一个有序表,其最少的比较次数是:n

练习2.29

已知A、B、C为三个元素值递增有序的顺序表,现要求对A作如下运算,删去那些既在B中出现

又在C中出现的元素,实现上述算法并分析时间复杂度。

A=A-(B∩C)

A=(1,2,6,6,8,9,10,10,11,15)

B=(1,2,6,6,7,9,10,15)

C=(3,4,6,7,7,9,9,9,10,12)

A=(1,2,8,11,15)

分析:

·先从B和C中找出公有元素,记为same;

·A中从当前位置开始,凡小于same的元素均保留(存到新的位置),等于same的跳过;

·大于same时就再找下一个same.

voidSqList_Intersect_Delete(SqList&A,SqListB,SqListC)

{  pa=A.elem;pa_last;pb;pb_last;pc;pc_last;p0;

   while((pa<=pa_last)&&(pb<=pb_last)&&(pc<=pc_last)){

       if(pb<pc) pb++;

       elseif(pb>pc) pc++;

       else{

          same=pb;

          while((pb<=pb_last)&&(pb==same))  pb++;

          while((pc<=pc_last)&&(pc==same))   pc++;

          while((pa<=pa_last)&&(pa<same))

            p0++=pa++;

          while((pa<=pa_last)&&(pa==same)) pa++;

       }//else

   }//while

   while(pa<=pa_last)

       p0++=pa++;

A.length=p0G22A.elem;



三、真题举例

1.下面关于线性表的叙述中,错误的是哪一个?(  )【北方交通大学】

A.线性表采用顺序存储,必须占用一片连续的存储单元。

B.线性表采用顺序存储,便于进行插入和删除操作。

C.线性表采用链接存储,不必占用一片连续的存储单元。

D.线性表采用链接存储,便于插入和删除操作。

2.若长度为n的线性表采用顺序存储结构,在其第i个位置插入一个新元素的算法的时间复杂度

为(  )(1<=i<=n+1)。【北京航空航天大学】

A.O(0)B.O(1)C.O(n)D.O(n





四、本讲小结

本讲主要讲解了:线性表的基本概念和常用操作、线性表的顺序存储方式。

常考题型:选择题,综合分析题。

—71—

应试方法:理解并熟练掌握线性表的顺序存储方式和线性表的基本运算在现行存储方式下在实

现方法。

第2讲

一、考试分析

考点重点与难点考试中常见题型复习思路与方法

线性表的链式存储结构。

选择题、

综合分析题

理解并熟练掌握线性表的链式存储

方式和线性表的基本运算在链式存

储方式下的实现方法。

二、考点讲解

2.3 线性表的链式表示和实现

线性表链式存储结构的特点:

·用一组任意的存储单元存储线性表的元素,不要求逻辑上相邻的元素在物理位置上也相邻;

·插入删除时不需移动大量元素;

·失去顺序表可随机存取的优点。

例,整数数组a[3]={3,5,6}

1.线性链表(单链表)

·结点:数据元素的存储映象。

数据域用来存储结点的值;

指针域用来存储数据元素的直接后继的地址(或位置)。

·头指针

指示链表中第一个结点的存储位置,单链表可由头指针唯一确定。

·单链表的存储映象

·头结点

在链表的第一个结点之前附设一个结点,头指针指向头结点。设置头结点的目的是统一空表与

非空表的操作,简化链表操作的实现。

·首元结点

链表中存储线性表中第一个数据元素的结点。

·链表存储结构描述:

TypedefstructLNode

}LNode, LinkList;

单链表基本运算实现

(1)初始化线性表InitList(L)

该运算建立一个空的单链表,即创建一个头结点。

voidInitList(LinkList&L



   ElemTypedata;

   structLNode next;





L=(LinkList)malloc(sizeof(LNode));

       /创建头结点/

L->next=NULL;

   }

(2)销毁线性表DestroyList(L)

释放单链表L占用的内存空间。即逐一释放全部结点的空间。

voidDestroyList(LinkListL)

  {LinkListp=L,q=p->next;

while(q!=NULL)

{   free(p);

    p=q;q=p->next;



free(p);

  }

(3)判线性表是否为空表ListEmpty(L)

若单链表L没有数据结点,则返回真,否则返回假。

intListEmpty(LinkListL)

   {

return(L->next==NULL);

   }

(4)求线性表的长度ListLength(L)

返回单链表L中数据结点的个数。

intListLength(LinkListL)

  {LinkListp=L;inti=0;

while(p->next!=NULL)

{  i++;

   p=p->next;



return(i);

  }

(5)输出线性表DispList(L)

逐一扫描单链表L的每个数据结点,并显示各结点的data域值。

voidDispList(LinkListL)

  {LinkListp=L->next;

while(p!=NULL)

{   printf("%c",p->data);

    p=p->next;



printf("\n");

  }

(6)取表元素

StatusGetElem(LinkListL,inti,ElemType&e)

—02—



  p=L->next; j=1;

  while(p&&j<i){

     p=p->next; ++j;

  }

  if(!p||j>i)returnERROR;

  e=p->data;

  returnOK;



例,取第i=3个元素。

e=p->data=Sun

时间复杂度:O(n)

·在单链表第i个结点前插入一个结点的过程

(7)插入

StatusListInsert(LinkList&L,inti,ElemTypee)



  p=L;j=0;

  while(p&&j<i-1){p=p->next;++j}

  if(!p||j>i-1) returnERROR;

  s= (LinkList)malloc(sizeof(LNode));

  s->data=e;

  s->next=p->next;①

  p->next=s;②

  returnOK;



·删除单链表的第i个结点的过程

(8)删除

StatusListDelete(LinkList&L,inti,ElemType&e)



  p=L;j=0;

  while(p->next&&j<i-1){p=p->next;++j}

  if(!(p->next)||j>i-1) returnERROR;

  r=p->next;

  e=r->data;

  p->next=p->next-next; //(p->next=r->next;)①

  free(r);

  returnOK;



·动态建立单链表的过程

(9)头插法建表

CreateList_H(LinkList&L,intn)



  L=(LinkList)malloc(sizeof(LNode));

  L->next=NULL;

  for(i=n;i>0;--i){

    s=(LinkList)malloc(sizeof(LNode));

    scanf(&s->data);

    s->next=L->next;①

    L->next=s;②

  }



·尾插法建表

(10)尾插法建表

CreateList_T(LinkList&L,intn)



  tail=L=(LinkList)malloc(sizeof(LNode));

  L->next=NULL;

  for(i=n;i>0;--i){

    s=(LinkList)malloc(sizeof(LNode));

    scanf(&s->data);

    s->next=NULL;

 tail->next=s;①

    tail=s;②

  }



(11)按元素值查找LocateElem(L,e)

思路:在单链表L中从头开始找第1个值域与e相等的结点,若存在这样的结点,则返回位置,否

则返回0。

 intLocateElem(LinkListL,ElemTypee)

 {LinkListp=L->next;intn=1;

while(p!=NULL&&p->data!=e)

{   p=p->next; n++; }

if(p==NULL) return(0);

else return(n);

 }

练习:已知L是带头结点的非空单链表,指针p所指的结点既不是第一个结点,也不是最后一个

结点。

·删除p结点的直接后继结点的语句序列

  q=p->next;

  p->next=q->next;

free(q);

·删除p结点的直接前驱结点的语句序列

  q=L;

  while(q->next->next!=p) q=q->next;

  s=q->next;

  q->next=p;

free(s);

·删除p结点的语句序列

 q=L;

 while(q->next!=p) q=q->next;

 q->next=p->next;

free(p);

·删除首元结点的语句序列

 q=L->next;

 L->next=q->next;

free(q);

·删除最后一个结点的语句序列

  while(p->next->next!=NULL) p=p->next;

  q=p->next;

  p->next=NULL;

free(q);

链式结构的特点:

·非随机存贮结构,所以取表元素要慢于顺序表。

节约了大块内存

·适合于插入和删除操作

实际上用空间换取了时间,结点中加入了指针,使得这两种操作转换为指针操作;

2.静态链表

有些高级程序设计语言并没有指针类型,如FORTRAN和JAVA。我们可以用数组来表示和实现

一个链表,称为静态链表。

可定义如下:

#define MAXSIZE 1000 //最多元素个数

typedef struct{

   ElemTypedata;

   int       cur;//游标,指示器

}component,SLinkList[MAXSIZE];

·i=s[i].cur;指针后移操作

·Malloc: i=s[0].cur;第一个可用结点位置

        if(s[0].cur) s[0].cur=s[i].cur;

·Free: //释放k结点

        s[k].cur=s[0].cur;

        s[0].cur=k;

·Insert://将i插在r之后

        s[i].cur=s[r].cur;

        s[r].cur=i;

·Delete:;//p为k的直接前驱,释放k

         s[p].cur=s[k].cur

         Free(k);

单链表基础要点:

·在单链表中,不能从当前结点出发访问到任一结点。

·在单链表中,删除某一指定结点时,必须找到该结点的前驱结点。

·线性表的链式存储结构是一种顺序存取的存储结构,不具有随机访问任一元素的特点。

·设置头结点的作用:使在链表的第一个位置上的操作和表中其它位置上的操作一致,无需进行

特殊处理,对空表和非空表的处理统一。

循环链表:

·循环链表是另一种形式的链式存储结构;

·可从当前结点出发,访问到任一结点;

·循环单链表;

·多重循环链表。

单循环链表

设置尾指针rear,比设头指针更好。

连接两个只设尾指针的单循环链表L1和L2

操作如下:

p=R1G22>next;  //保存L1的头结点指针

R1->next=R2->next->next;//头尾连接

free(R2->next);  //释放第二个表的头结点

 R2->next=p;

操作与线性单链表基本一致,差别只是在于算法中的循环结束条件不是p是否为空,而是p是否

等于头指针。

例:取循环链表第i个元素。

Status GetElem_L(LinkList L,int i,ElemType &e) {

p=L->next;j=1;

while (p!=L&& j<i) {

p=p->next;++j;



if (p==L||j>i) return ERROR;

e=p->data;

return OK;



双链表:

希望查找前驱的时间复杂度达到O(1),我们可以用空间换时间,每个结点再加一个指向前驱的

指针域,使链表可以进行双方向查找。用这种结点结构组成的链表称为双向链表。

结点的结构图:

双向链表的逻辑表示:

双向链表(DoubleLinkedList)

类型描述

typedefstructDuLNode{

  ElemType       data;

  structDuLNode  prior;

  structDuLNode  next;

}DuLNode, DuLinkList;

双向循环链表

p->next->prior=p->prior->next;

·双向链表的前(后)插入操作

①s->prior=p->prior;    ②p->prior->next=s;

③s->next=p;      ④p->prior=s;

①s->next=q->next;    ②q->next->prior=s;

③s->prior=q;      ④q->next=s;

·双向链表的删除操作

①p->prior->next=p->next;

②p->next->prior=p->prior;

·删除p的直接后继结点的语句序列

  q=p->next;

  p->next=p->next->next;

  p->next->prior=p;

  free(q);

·删除p的直接前驱结点的语句序列

  q=p->prior;

  p->prior=p->prior->prior;

  p->prior->next=p;

  free(q);

  returnpre;



②找结点的中序后继结点

结点p,无右孩子,右指针指向其后继,否则p的右子树中“最左下”结点。

BiThrTreePostNode(BiThrTreep)



  post=p->rchild;

  if(p->RTag==0)//有右孩子

    while(post->LTag==0)

       post=post->lchild;

  returnpost;



·带头结点的线索二叉链表

带头结点的中序线索二叉链表

中序遍历线索二叉树//0:有孩子;1:无孩子

VoidInOrderTraverse_Thr(BiThrTree T)//T:头结点



  p=T->lchild;

  while(p!=T){

    while(p->LTag==0)p=p->lchild;

cout<<p->data<<“”;

    while((p->RTag==1)&&(p->rchild!=T))

    { p=p->rchild;

cout<<p->data<<“”;

    }

    p=p->rchild;

  }



·建立线索化链表(以中序为例)

按某种次序将二叉链表线索化,实质是在遍历过程中用线索取代空指针。

对线索树进行遍历,显然其效率要比传统方式高些。如果程序中经常要进行二叉树的遍历或者

需要查找在遍历过程中所的线性序列中前驱和后继,此时应当采用线索链表表示。

6.4 树和森林

1.树的存储结构(三种方法)

双亲表示法:用一组连续的空间来存储树中的结点,在保存每个结点的同时附设一个指示器来指

示其双亲结点在表中的位置,其结点的结构如下:

DataParent

树的双亲存储结构示意图

#defineMAX_TREE_SIZE 100

typedefstructPTNode{

TElemType data;

int       parent;

}PTNode;

Typedefstruct{

PTNode nodes[MAX_TREE_SIZE];

int     r,n; //根的位置和结点数

}PTree;

双亲表示法的类型说明:

·孩子表示法:

①定长结点长度

空链域个数:nkG22(n-1)=n(k-1)+1.

②把每个结点的孩子结点排列起来,构成一个单链表,称为孩子链表。n个结点共有n个孩子链

表(叶子结点的孩子链表为空表),而n个结点的数据和n个孩子链表的头指针又组成一个顺序表。

typedefstructCTNode{/孩子结点的定义/

int   Child;   /该孩子结点在线性表中的位置/

structCTNode next; /指向下一个孩子结点的指针/

}ChildPtr;

typedefstruct{/顺序表结点的结构定义/

TElemTypedata;      /结点的信息/

ChildPtr  FirstChild;/指向孩子链表的头指针/

}CTBox;

typedefstruct{/树的定义/

CTBox nodes[MAX_TREE_SIZE];/顺序表/

introot,num;/根结点的位置和树的结点个数/

}CTree;

·孩子兄弟表示法

typedefstructCSNode{

ElemTypedata;  /结点信息/

StructCSNode FirstChild, NextSibling;  /第一个孩子,下一个兄弟/

}CSNode, CSTree;

这种存储结构便于实现树的各种操作。

树的孩子兄弟链存储结构示意图

2.树、森林与二叉树的相互转换

·树转换为二叉树

(1)在所有相邻兄弟结点之间加一水平连线。

(2)对每个非叶结点k,除了其最左边的孩子结点外,删去k与其他孩子结点的连线。

(3)所有水平线段以左边结点为轴心顺时针旋转45度,使之结构层次分明。

树做这样的转换所构成的二叉树是唯一的。

树与二叉树的对应

·森林转换为二叉树

森林也可以方便地用孩子兄弟链表表示。森林转换为二叉树的方法如下:

(1)将森林中的每棵树转换成相应的二叉树。

(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树

根结点的右孩子,当所有二叉树连在一起后,所得到的二叉树就是由森林转换得到的二叉树。

·二叉树还原为树或森林

将一棵二叉树还原为树或森林,具体方法如下:

(1)若某结点是其双亲的左孩子,则把该结点的右孩子、右孩子的右孩子……都与该结点的双亲

结点用线连起来。

(2)删掉原二叉树中所有双亲结点与右孩子结点的连线。

(3)整理由(1)、(2)两步所得到的树或森林,使之结构层次分明。

二叉树到森林的转换示例

3.树与森林的遍历

·树的遍历(两种)

1)先根遍历

若树非空,则遍历方法为:

①访问根结点。

②从左到右,依次先根遍历根结点的每一棵子树。 

等同于转换的二叉树进行先序遍历

先根遍历序列ABECFHGD

2)后根遍历

若树非空,则遍历方法为:

①从左到右,依次后根遍历根结点的每一棵子树。

②访问根结点。   

等同于转换的二叉树进行中序遍历

后根遍历序列为EBHFGCDA

·森林的遍历(2种)

1)中序遍历

若森林非空,则遍历方法为:

①访问森林中第一棵树的根结点。

②先序遍历第一棵树的根结点的子树森林。

③先序遍历除去第一棵树之后剩余的树构成的森林。

先序遍历序列为ABCDEFGHIJ

等同于转换的二叉树进行先序遍历

2)先序遍历

若森林非空,则遍历方法为:

①中序遍历森林中第一棵树的根结点的子树森林。

②访问第一棵树的根结点。

③中序遍历除去第一棵树之后剩余的树构成的森林。 

中序遍历序列为 BCDAFEHJIG

等同于转换的二叉树进行中序遍历

4.几个问题

①给定树的先根遍历序列和后根遍历序列可唯一画出一棵树。

先根遍历序列:ABECFHGD

后根遍历序列:EBHFGCDA

②给定森林的先序遍历序列和中序遍历序列可唯一确定一森林。

先序遍历序列:ABCDEFGHIJ

中序遍历序列:BCDAFEHJIG

③关于二叉树的先序、中序和后序遍历序列确定二叉树的问题。

·任何n(n≥0)个不同结点的二叉树,都可由它的中序序列和先序序列唯一地确定。

证明:

先序序列是a







…a



中序序列是b







…b



根结点:a





在中序序列中与a



相同的结点为:b





{b



…b

j-1

}b



{b

j+1

…b





←→





{a



…a



}{a

k+1

…a





例 已知先序序列为ABDGCEF,中序序列为DGBAECF

·任何n(n>0)个不同结点的二叉树,都可由它的中序序列和后序序列唯一地确定。

证明:

后序序列是a







…a



中序序列是b







…b



根结点:a





在中序序列中与a



相同的结点为:b





{b



…b

j-1

}b



{b

j+1

…b





←→

{a







…a



}{a

k+1

…a

n-1

}a



例 后序序列为GDBEFCA,中序序列为DGBAECF

6.5 树与等价问题

离散数学中的定义

·等价关系:若集合S中的关系R是自反的、对称的和传递的,则称为等价关系。

·等价类:R是集合S的等价关系,由[x]R={y|y∈S∧xRy}给出的集合[x]R称为由x∈S生成

的一个R等价类。

·划分:R是S上的等价关系,可以按R将S划分为若干不相交的子集S



,S



,……,它们的并即

为S,则这些子集S



就是S的R等价类。

如何划分等价类?

·假设集合S有n个元素,m个形如(x,y)的等价偶对确定了等价关系R,求S的划分。

一种算法:

1)令S中每个元素各自形成一个只含单个成员的子集,记为S



,S



,…,S





2)重复读入m个偶对,对每个偶对(x,y),判断x和y所属的子集,设x∈S



,y∈S



,若S



≠S



,则将





并入S



,并置S



为空。

处理完m个偶对后剩下的非空子集就是S的R等价类。

划分等价类需要的操作:

1)构造只含单个元素的集合

2)判定某个元素所属集合

3)合并两个互不相交的集合

ADTMFSet:若S是MFSet类型的集合,则它由子集S



构成,S



∪S



∪…∪S



=S。

基本操作:

Initial(&S,n,x



,x



,…,x



):构造由n个子集构成的集合S,每个子集只含单个元素。

Find(S,x):查找x所属的子集S





Merge(&S,i,j):合并两个不相交的集合S



和S





MFSet类型的实现

·根据Find和Merge两个操作的特点,用树来实现MFSet。

·以森林F=(T



,T



,…,T



)表示MFSet类型的集合S,每颗树T



表示一个子集S





·树中每个结点表示子集中的一个成员x。

·令每个结点中包含一个指向其双亲的指针。

·约定根结点兼作子集的名称。

集合的合并:

将一棵树的根指向另一颗树的根。

#defineMAX_TREE_SIZE 100

typedefstructPTNode{

TElemType data;

int       parent;

}PTNode;

Typedefstruct{

PTNode nodes[MAX_TREE_SIZE];

int r,n; //根的位置和结点数

}PTree;

TypedefPTree MFSet;

intfind_mfset(MFSetS,inti)



if(i<1||i>S.n)return-1;

for(j=i;S.nodes[j].parent>0;j=S.node[j].parent) ;

  returnj;



Statusmerge_mfset(MFSet&S,inti,intj)



if(i<1||i>S.n||j<1||j>S.n)returnERROR;

S.node[i].parent=j;

returnOK;



时间复杂度分别为O(d)和O(1),d为树的深度

(7)掌握线索二叉树的概念和相关算法的实现。

(8)掌握哈夫曼树的定义、哈夫曼树的构造过程和哈夫曼编码产生方法。

(9)灵活运用二叉树这种数据结构解决一些综合应用问题。

二、真题举例

1.已知一棵二叉树的前序遍历结果为ABCDEF,中序遍历结果为CBAEDF,则后序遍历的结果为

(  )【浙江大学】

A.CBEFDAB.FEDCBAC.CBEDFAD.不定

2.已知某二叉树的后序遍历序列是dabec,中序遍历序列是debac,它的前序遍历是(  )【山东

大学】

A.acbdeB.decabC.deacbD.cedba

3.一棵左子树为空的二叉树在先序线索化后,其中空的链域的个数是(  )【合肥工业大学】

A.不确定B.0C.1D.2

4.若X是二叉中序线索树中一个有左孩子的结点,且X不为根,则X的前驱为(  )【南京理工

大学】

A.X的双亲B.X的右子树中最左的结点

C.X的左子树中最右结点D.X的左子树中最右叶节点

5.设F是一个森林,B是由F变换得的二叉树。若F中有n个非终端结点,则B中右指针域为空

的结点有(  )个。【西安电子科技大学】

A.n-1B.nC.n+1D.n+2

三、本讲小结

线索二叉树的概念和相关算法的实现。

树、森林与二叉树的关系

树的简单应用

第3讲

一、考点讲解

极端情况:

改进方法?

·Merge时,总是将成员少的子集根结点指向含成员多的子集的根

·修改存储结构,令根结点的parent域存储子集中所含成员数目的负值

·可以将find_mfset的复杂度降到O(logn)

Statusmix_mfset(MFSet&S,inti,intj)



if(i<1||i>S.n||j<1||j>S.n)returnERROR;

if(S.nodes[i].parent>S.nodes[j].parent){

S.nodes[j].parent+=S.nodes[i].parent;

S.nodes[i].parent=j;

}else{

S.nodes[i].parent+=S.nodes[j].parent;

S.nodes[j].parent=i;



returnOK;



进一步的改进:Find时压缩路径

·当所查元素不在第二层时,将所有从根到该元素路径上的元素都变成根结点的孩子

intfix_mfset(MFSet&S,inti)



if(i<1||i>S.n)return-1;

for(j=i;S.nodes[j].parent>0;j=S.node[j].parent) ;

for(k=i;k!=j;k=t)



t=S.nodes[k].parent; S.nodes[k].parent=j;



  returnj;



6.6 哈夫曼树及其应用

1.哈夫曼树

①路径长度

从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目称做

路径长度。

②树的路径长度

从树根到每一结点的路径长度之和。

③结点的权和带权路径长度

给树的每个结点赋予一个具有某种实际意义的实数,我们称该实数为这个结点的权。在树形结

构中,我们把从树根到某一结点的路径长度与该结点的权的乘积,叫做该结点的带权路径长度。

④树的带权路径长度WPL(WeightedPathLengthofTree)

树中所有叶子结点的带权路径长度之和,通常记为:

WPL=





k=1









WPL(a)=7×2+5×2+2×2+4×2=36

WPL(b)=4×2+7×3+5×3+2×1=46

WPL(c)=7×1+5×2+2×3+4×3=35

⑤哈夫曼树(最优二叉树)

设二叉树具有n个带权值的叶子结点,那么从根结点到各个叶子结点的路径长度与相应结点权

值的乘积的和,叫做二叉树的带权路径长度。

具有最小带权路径长度的二叉树称为哈夫曼树。

2.构造哈夫曼树(哈夫曼算法)

1)由给定的n个权值{W



,W



,...,W



},构造n棵只有一个叶子结点的二叉树,从而得到一个二

叉树的集合F={T



,T



,...,T



};

2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵

新的二叉树根结点的权值为其左、右子树根结点权值之和;

3)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中;

4)重复(2)、(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。

给定权值w=(1,3,5,7)来构造一棵哈夫曼树。

3.哈夫曼编码

1)编码

例,传送ABACCD,四种字符,可以分别编码为00,01,10,11。

则原电文转换为000100101011。

对方接收后,采用二位一分进行译码。

当然,为电文编码时,总是希望总长越短越好。

如果对每个字符设计长度不等的编码,且让电文中出现次数较多的字符采用较短的编码,则可以

减短电文的总长。

例 对ABACCD重新编码,分别编码为0,00,1,01。

                 A B C D

则原电文转换为00001101。减短了。

问题: 

如何译码?

前四个二进制字符就可以多种译法。

AAAA           BB

2)前缀编码

若设计的长短不等的编码,满足任一个编码都不是另一个编码的前缀,则这样的编码称为前缀

编码。

例,A,B,C,D前缀编码可以为0,110,10,111利用二叉树设计二进制前缀编码。

叶子结点表示A,B,C,D这4个字符左分支表示‘0’,右分支表示‘1’;从根结点到叶子结

点的路径上经过的二进制符号串作为该叶子结点字符的编码,证明路径长度为编码长度

其必为前缀编码。

如何得到最短的二进制前缀编码?

3)赫夫曼编码

设每种字符在电文中出现的概率w



为,则依此n个字符出现的概率做权,可以设计一棵赫夫曼

树,使

WPL=





i=1









最小





为叶子结点的出现概率(权)





为根结点到叶子结点的路径长度

例 某通信可能出现ABCDEFGH8个字符,其概率分别为0.05,0.29,0.07,0.08,0.

14,0.23,0.03,0.11,试设计赫夫曼编码。

ACEA编码为0110 11101100110

如何译码?

1.从根结点出发,从左至右扫描编码;

2.若为‘0’则走左分支,若为‘1’则走右分支,直至叶结点为止;

3.取叶结点字符为译码结果,返回重复执行1,2,3直至全部译完为止;

4.哈夫曼编码算法的实现。

哈夫曼树中没有度为1的结点(严格的或正则的二叉树)。

n个叶子结点,共有2n-1个结点。

typedefstruct



   unsignedintweight; //结点的权值

   unsignedintparent, lchild,rchild;

}HTNode, HuffmanTree; //动态分配数组存储哈夫曼树

typedefchar HuffmanCode;//动态分配数组存储哈夫曼编码

voidHuffmanCoding(HuffmanTree&HT, 

HuffmanCode&HC,intw, intn)



 m=2n-1;

 HT=(HuffmanTree)malloc((m+1)sizeof(HTNode));

 for(p=HT+1,i=1;i<=n;++i,++p,++w)p={w,0,0,0};

 for(;i<=m;++i)p={0,0,0,0};

 for(i=n+1;i<=m;i++){

   select(HT,i-1,s1,s2);//在HT[1..i-1],

 HT[s1].parent=i; HT[s2].parent=i;

 HT[i].lchild=s1;  HT[i].rchild=s2;

 HT[i].weight=HT[s1].weight+HT[s2].weight;

 }



HC=(HuffmanCode)malloc((n+1)sizeof(char)); 

cd=(char)malloc(nsizeof(char)); 

cd[n-1]=‘\0’;

for(i=1;i<=n;i++){

  start=n-1;

  for(c=i,f=HT[i].parent;f!=0;c=f,f=HT[f].parent)

     if(HT[f].lchild==c) cd[--start]=‘0’;

     else cd[--start]=‘1’;

  HC[i]=(char)malloc((n-start)sizeof(char));

strcpy(HC[i],&cd[start]);



free(cd);

HC=malloc(…);  cd=malloc(…);

p=m;cdlen=0; for(i=1;i<=m;++i) HT[i].weight=0;

while(p){

  if(HT[p].weight==0){//向左

    HT[p].weight=1;

    if(HT[p].lchild!=0){p=HT[p].lchild;cd[cdlen++]=‘0’;}

    elseif(HT[p].rchild==0){

      HC[p]=(char)malloc((cdlen+1)sizeof(char));

U={v



},V-U={v



,v



,v



,v



,v



}    TE={}

U={v



,v



},V-U={v



,v



,v



,v



}    <v



,v





U={v



,v



,v



},V-U={v



,v



,v



}    <v



,v





U={v



,v



,v



,v



},V-U={v



,v



}   <v



,v





U={v



,v



,v



,v



,v



},V-U={v



}   <v



,v





U={v



,v



,v



,v



,v



,v



},V-U={}   <v



,v





重点:边一定存在于U与V-U之间。

设置一个辅助数组,对当前V-U集中的每个顶点,记录和顶点集U中顶点相连接的代价最小

的边:

对每个顶点v



∈V-U,在辅助数组中存在一个分量closedge[v



],它包括两个域adjvex和lowcost,

其中lowcost存储该边上的权,显然有closedge[i-1].lowcost=Min({cost(u,v



)|u∈U})

struct{

VertexType adjvex;//顶点名称

int lowcost;

}closedge[MAX_VERTEX_NUM];

例 (详见视频)

voidMiniSpanTree_PRIM(MGraphG,VertexTypeu)

{  k=LocateVex(G,u);

  for(j=0;j<G.vexnum;++j)

     if(j!=k)closedge[j]={u,G.arcs[k][j].adj};

closedge[k].lowcost=0;

  for(i=1;i<G.vexnum;++i){

     k=minimun(closedge);

printf(colsedge[k].adjvex,G.vexs[k]);

closedge[k].lowcost=0;

     for(j=0;j<G.vexnum;++j)

       if(G.arcs[k][j].adj<closedge[j].lowcost)

colsedge[j]={G.vexs[k],G.arcs[k][j].adj};

  }



  Prim()算法中有两重for循环,所以时间复杂度为O(n



)。与网中的边数无关,适用于求边稠

密的网的最小生成树。

Kruskal算法

·Kruskal于1956年提出

思想:考虑问题的出发点:为使生成树上边的权值之和达到最小,则应使生成树中每一条边的权

值尽可能地小。

N=(V,E)是n顶点的连通网,设E是连通网中边的集合;

构造最小生成树N’=(V,TE),TE是最小生成树中边的集合,初始TE={};

重复执行:

选取E中权值最小的边(u,v),判断边(u,v)与TE中的边是否构成回路?

例 (详见视频)

完整的克鲁斯卡尔算法应包括对边按权值递增排序,上述算法假设边已排序的情况下,时间复杂

度为O(n



)。

如果给定的带权连通无向图G有e条边,n个顶点,采用堆排序(在第10章中介绍)对边按权值

递增排序,那么用克鲁斯卡尔算法构造最小生成树的时间复杂度降为O(eloge)。由于它与n无关,只

与e有关,所以说克鲁斯卡尔算法适合于求边稀疏的网的最小生成树。

7.5 有向无环图及其应用

1.拓扑排序(TopologicalSort)

设G=(V,E)是一个具有n个顶点的有向图,V中顶点序列v



,v



,…,v



称为一个拓扑(有序)序

列,当且仅当该顶点序列满足下列条件:若<v



,v



>是图中的弧(即从顶点v



到v



有一条路径),则在

序列中顶点v



必须排在顶点v



之前。

在一个有向图中找一个拓扑序列的过程称为拓扑排序。

拓扑序列:C



,C



,C



,C



,C



,C



,C



,C



,C





拓扑序列:C



,C



,C



,C



,C



,C



,C



,C



,C





用顶点表示活动,用弧表示活动间的优先关系的有向图,称为顶点表示活动的网(ActivityOnVer

texNetwork),简称为AOV-网。

如何进行拓扑排序?

方法一:(从图中顶点的入度考虑)

1)从有向图中选择一个没有前驱(即入度为0)的顶点并且输出它。

2)从网中删去该顶点和所有以它为尾的弧;

3)重复上述两步,直到图全部顶点输出;或当前图中不再存在没有前驱的顶点。

方法二:(从图中顶点的出度考虑,得到逆拓扑序列)

1)从有向图中选择一个出度为0的顶点并且输出它。

2)从网中删去该顶点和所有以它为头的弧;

3)重复上述两步,直到图全部顶点输出;或当前图中不再存在出度为0的顶点。

方法三:当有向图中无环时,利用深度优先遍历进行拓扑排序

从某点出发进行DFS遍历时,最先退出DFS函数的顶点即出度为0的顶点,是拓扑序列中最后一

个顶点。按退出DFS函数的先后记录下来的顶点序列即为逆拓扑序列。

问题:判定一个图是否有圈(回路)的方法?

StatusTopologicalSort(ALGraphG)

{intSt[MAXV],top=-1; /栈St的指针为top/

FindInDegree(G,indegree);

   for(i=0;i<G.vexnum;i++)

      if(!indegree[i]) { top++;St[top]=i;}

   count=0;

   while(top>-1){/栈不为空时循环/

      i=St[top]; top--;printf("%d",i); ++count;

      for(p=G.vertices[i].firstarc;p;p=p->nextarc){

         k=p->adjvex;

         if(!(--indegree[k])){ top++; St[top]=k; }



   }

   if(count<G.vexnum) returnERROR; elsereturnOK;



voidFindInDegree(ALGraphG, intindegree)

{inti;  ArcNodep;

  for(i=0;i<G.vexnum;i++)  indegree[i]=0;

for(i=0;i<G.vexnum;i++){

    p=G.vertexes[i].firstarc;

    while(p! =NULL){

indegree[p->adjvex]++;

       p=p->nextarc;}//while

}//for



二、真题举例

1.(1)如果G1是一个具有n个顶点的连通无向图,那么G1最多有多少条边?G1最少有多少

条边?

(2)如果G2是一个具有n个顶点的强连通有向图,那么G2最多有多少条边?G2最少有多少条

边?【复旦大学】

2.已知一个无向图如下图所示,要求分别用Prim和Kruskal算法生成最小树(假设以①为起点,

试画出构造过程)。【哈尔滨工业大学】

3.一带权无向图的邻接矩阵如右图,试画出它的一棵最小生成树。【浙江大学】

三、本讲小结

图的最小生成树、拓扑排序算法。

第3讲

一、考点讲解

2.关键路径

有向图在工程计划和经营管理中有着广泛的应用。通常用有向图来表示工程计划时有两种方法:

·用顶点表示活动,用有向弧表示活动间的优先关系,即上节所讨论的AOV-网。

·用顶点表示事件,用弧表示活动,弧的权值表示活动所需要的时间。带权的有向无环图叫做

边表示活动的网(ActivityOnEdgeNetwork),简称AOE-网。

·事件:表示在它之前的活动已经完成,在它之后的活动可以开始。

·AOE-网有待解决的问题:

①哪些活动是影响工程进度的关键活动?

②至少需要多长时间能完成整个工程?

·源点:在AOE网中存在唯一的、入度为零的顶点;

·汇点:存在唯一的、出度为零的顶点。

·关键路径:从源点到汇点的最长路径的长度即为完成整个工程任务所需的时间,该路径叫做关

键路径。

·关键活动:关键路径上的活动。

定义几个与计算关键活动有关的量:

·事件V



的最早发生时间ve(j)是从源点V



到顶点V



的最长路径长度。

·事件V



的最迟发生时间vl(j)是在保证汇点V

n-1

在ve(n-1)时刻完成的前提下,事件V



的允

许的最迟开始时间。

·活动a



的最早开始时间e(i)

设活动a



在弧<V



,V



>上,则e(i)是从源点V



到顶点V



的最长路径长度。因此,e(i)=ve

(j)。

·活动a



的最迟开始时间l(i)

l(i)是在不会引起时间延误的前提下,该活动允许的最迟开始时间。

l(i)=vl(k)-dur(<j,k>)。其中,dur(<j,k>)是完成a



所需的时间。

·时间余量l(i)G22e(i)

表示活动a



的最早开始时间和最迟开始时间的时间余量。

l(i)==e(i)表示活动a



是没有时间余量的关键活动。

·为找出关键活动,需要求各个活动的e(i)与l(i),以判别是否l(i)==e(i)。

·为求得e(i)与l(i),需要先求得从源点V



到各个顶点V



的ve(j)和vl(j)。

·从ve(0)=0开始,向前递推

ve(j)=max



{ve(i)+dur(<V



,V



>)},<V



,V



>G28T,j=1,2,G29,n-1

·其中T是所有以V



为头的弧的集合。

从vl(n-1)=ve(n-1)开始,反向递推

vl(i)=min



{vl(j)-dur(<V



,V



>)},<V



,V



>S,i=n-2,n-3,G29,0

其中S是所有以V



为尾的弧的集合。

·e(i)=ve(j),l(i)=vl(k)-dur(<j,k>)

(详见视频)

·算法时间复杂度:

在拓扑排序求Ve(i)和逆拓扑有序求Vl(i)时,所需时间为O(n+e),求各个活动的e(i)和l(i)

时所需时间为O(e),总共花费时间仍然是O(n+e)。

7.6 最短路径

旅客希望停靠站越少越好,则应选择:济南———北京———太原———兰州

旅客考虑的是旅程越短越好,济南———徐州———郑州———西安———兰州

带权图的最短路径计算问题

通常在实际中,航运、铁路、船行都具有有向性,故我们以带权有向图为例介绍最短路径算法。

带权无向图的最短路径算法也通用。

从单个源点到其余各顶点的最短路径算法。

从一个顶点到其余各顶点的最短路径

问题:给定一个带权有向图G与源点v,求从v到G中其他顶点的最短路径,并限定各边上的权

值大于或等于0。

迪杰斯特拉(Dijkstra)算法思想:

贪心算法(局部最优),按路径长度递增的次序产生最短路径。

贪心算法:利用局部最优来计算全局最优。

利用已得到的顶点的最短路径来计算其它顶点的最短路径。

·路径长度最短的最短路径的特点:

在这条路径上,必定只含一条弧,并且这条弧的权值最小。

·下一条路径长度次短的最短路径的特点:

它只可能有两种情况:或者是直接从源点到该点(只含一条弧);或者是,从源点经过顶点v



,再到

达该顶点(由两条弧组成)。

·其余最短路径的特点:

它或者是直接从源点到该点(只含一条弧);或者是,从源点经过已求得最短路径的顶点,再到达

该顶点。

·采用迪杰斯特拉(Dijkstra)算法求解

Dijkstra提出按路径长度的递增次序,逐步产生最短路径的算法。

引入一个辅助数组D。它的每一个分量D[i]表示当前找到的从源点v



到终点v



的最短路径的长

度。初始状态:

若从源点v



到顶点v



有边,则D[i]为该边上的权值;

若从源点v



到顶点v



没有边,则D[i]为+G27。

一般情况下,假设S是已求得的最短路径的终点的集合,则可证明:下一条最短路径必然是从v



出发,中间只经过S中的顶点便可到达的那些顶点v



(v



G28V-S)的路径中的一条。

每次求得一条最短路径之后,其终点v



加入集合S,然后对所有的v



G28V-S,修改其D[i]值。

·Dijkstra算法可描述如下:

①初始化:S←{v



}; 

D[j]



arcs[0][j], j=1,2,…,n-1;

②求出最短路径的长度:

D[k]



min{D[i]}, iG28V-S;





S∪{k};

③修改: 

    D[i]



min{D[i], D[k]+arcs[k][i]},

对于每一个iG28V-S;

④判断:若S=V,则算法结束,否则转②。

一般情况,假设S为已求得最短路径的终点的集合,则有:下一条最短路径(设终点为x)或者是

弧(v



,x),或者是v



出发中间只经过S中的顶点而最后到达顶点x的路径。

反证法:

假设下一条最短路径上有一个顶点不在S中,不妨设v’;

则必存在一条终点为v’的最短路径,其长度比该路径短;

可这是不可能的,因为我们是按照路径长度递增的次序来依次产生最短路径,即长度比该路径短

的所有路径都已产生;矛盾。

D[n]:从源点到其余顶点的最短路径长度;

F[n]:已找到最短路径的顶点,属于S集or属于V-S集;

P[n]:记录已找到的路径。P[i]记录路径上v



的前驱。

例 (详见视频)

voidDijkstra(MGraphG)

{intD[MAXV],P[MAXV],F[MAXV];F[0]=1;

   for(i=1;i<G.vernum;i++){

    D[i]=G.arcs[0][i];F[i]=0;

    if(D[i]<INT_MAX) P[i]=0;

    else P[i]=-1;



  for(i=1;i<G.vernum;i++){

  …..

  }

Dispath(D,P,F,G.vernum,0);



for(i=1;i<G.vernum;i++){

  min=INT_MAX;

  for(j=1;j<G.vernum;j++)

     if(!F[j])

       if(D[j]<min){w=j;min=D[j];}

  F[w]=1;

  for(j=1;j<G.vernum;j++){

    if(!F[j]&&((D[w]+G.arcs[w][j])<D[j])){

       D[j]=D[w]+G.arcs[w][j];

       P[j]=w;

  }



voidPpath(intpath,inti,intv0)/前向递归查找路径上的顶点/



   k=path[i];

   if(k==v0) return;  /找到了起点则返回/

Ppath(path,k,v0);  /找k顶点的前一个顶点/

printf("%d,",k);  /输出k顶点/



voidDispath(intdist,intpath,intfinal,intn,intv0)



  for(i=0;i<n;i++){

if(final[i]==1){

printf(“从%d到%d的最短路径长度为:

                %d\t路径为:",v0,i,dist[i]);

printf("%d,",v0);/输出路径上的起点/

Ppath(path,i,v0);/输出路径上的中间点/

printf("%d\n",i);/输出路径上的终点/



elseprintf("从%d到%d不存在路径\n",v0,i);

  }



2.每对顶点之间的最短路径

·问题的提法:已知一个各边权值均大于0的带权有向图,对每一对顶点v



G2Av



,要求求出v



与v



之间的最短路径和最短路径长度。

·弗洛伊德(Floyd)算法的基本思想:

定义一个n阶方阵序列:D

(-1)

,D

(0)

,…,D

(n-1)



其中D

(-1)

[i][j]=arcs[i][j];



(k)

[i][j]=Min{D

(k-1)

[i][j],D

(k-1)

[i][k]+D

(k-1)

[k][j]},k=0,1,…,n-1



(0)

[i][j]是从顶点v



到v



,中间顶点是v



的最短路径的长度,D

(k)

[i][j]是从顶点v



到v



,中

间顶点的序号不大于k的最短路径的长度,D(n-1)[i][j]是从顶点v



到v



的最短路径长度。

voidShortestPath_Floyd(MGraphG)

{ 

intpath[NumVertices][NumVertices];

  for(i=0;i<G.vexnum;i++)  //矩阵D与path初始化

    for(j=0;j<G.vexnum;j++){

      D[i][j]=G.arcs[i][j];

      for(k=0;k<G.vexnum;k++)path[i][j][k]=FALSE;

      if(D[i][j]<MAXINT){

        path[i][j][i]=TRUE;path[i][j][j]=TRUE;}  

    }

  for(k=0;k<G.vexnum;k++)   

    for(i=0;i<G.vexnum;i++)

for(j=0;j<G.vexnum;j++)

         if(D[i][j]>D[i][k]+D[k][j]){

            D[i][j]=D[i][k]+D[k][j];

for(l=0;l<G.vexnum;l++)

               path[i][j][l]=path[i][k][l]||path[k][j][l];

         }



二、真题举例

1.用最短路径算法,求如下图中a到z的最短通路。【西南财经大学】

2.下图是带权的有向图G的邻接表表示法,求:

(1)以结点V



出发深度遍历图G所得的结点序列;

(2)以结点V



出发广度遍历图G所得的结点序列;

(3)从结点V



到结点V



的最短路径;

(4)从结点V



到结点V



的关键路径。【青岛海洋大学】

三、本讲小结

图的关键路径、最短路径

第八章 查找

一、目录分析

9.1查找的基本概念

9.2静态查找表———基于线性表的查找法

9.3动态查找表———基于树表的查找法

9.4哈希表———计算式查找法

二、考点讲解

查找和排序是数据处理系统中最重要的两个操作;

其次是插入、删除操作;

讨论查找、排序,不可避免要涉及文件、记录、关键字等概念。

文件———查找表,是由同一类型的数据元素(记录)构成的集合

记录———构成文件的数据元素,是文件中可存取的数据的基本单位

字段———数据项,数据的最小单位

关键字———某个可以用来标识记录的数据项

主关键字———某个可以用来唯一标识记录的数据项

次关键字———可以用来识别若干记录的数据项

对文件经常进行的操作有:

1)查询某个“特定”的数据元素是否存在   查找算法

2)插入某个数据元素

3)删除某个数据元素     

4)对数据元素进行排序    排序算法

不管何种操作,都遵循一个重要的性质:都是对主关键字操作

1.查找的基本概念

—121—

·查找表

由同一类型的数据元素(记录)构成的集合。

·查找的定义

给定一个值key,在含有n个记录的表中找出关键字等于key的记录。若找到,则查找成功,返回

该记录的信息或该记录在表中的位置;否则查找失败,返回相关的指示信息。

采用何种查找方法?

(1)使用哪种数据结构来表示“表”,即表中记录是按何种方式组织的。

(2)表中关键字的次序。是对无序集合查找还是对有序集合查找?

·静态查找表(StaticSearchTable):查询某个特定的元素是否在表中;检索某个特定的元素的各

种属性。

·动态查找表(DynamicSearchTable):若在查找的同时对表做修改运算(如插入和删除)。

2.查找操作的性能分析

ASL=



·基本操作:将记录的关键字和给定值进行比较。

·平均查找长度ASL(AverageSearchLength):为确定数据元素在查找表中的位置,需和给定值

进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度。



i=1













为查找表中第i个记录的概率,C



为找到第i个记录时,和给定值已经进行过比较的关键字

个数。

9.1 静态查找表

在表的组织方式中,线性表是最简单的一种。三种在线性表上进行查找的方法:

(1)顺序查找

(2)折半查找(二分查找)

(3)索引顺序表查找(分块查找)。

因为不考虑在查找的同时对表做修改,故上述三种查找操作都是在静态查找表上实现的。

1.顺序查找

顺序查找法的特点:用所给关键字与线性表中各元素的关键字逐个比较,直到成功或失败。存储

结构通常为顺序结构,也可为链式结构。

例如,在关键字序列为{3,9,1,5,8,10,6,7,2,4}的线性表查找关键字为8的元素。

typedefstruct{

ElemTypeelem;

intlength;

}SSTable;

intSearch_Seq(SStableST,KeyTypekey)



ST.elem[0].key=key;

  for(i=ST.length;!EQ(ST.elem[i].key,key);--i);

  returni;



for(i=ST.length;i>0&&!EQ(ST.elem[i].key,key);--i);

·顺序查找的平均查找长度ASL:

假设表长度为n,那么查找到第i个记录时,和给定值已进行过比较的关键字个数为n-i+1,即





=n-i+1。又假设查找每个数据元素的概率相等,即P



=1/n,则顺序查找算法的平均查找长

度为:

ASL

SS







i=1



















i=1















i=1

(n-i+1)=





(n+1)

·查找不成功时的平均查找长度:

假设查找成功和不成功的可能性相等,且每个记录的查找概率也相等,即P



=1/(2n),则

ASL

SS





2n





i=1

(n-i+1)+



2n

n(n+1)=





(n+1)

2.折半查找法(二分法查找法)

要求待查找的表必须是按关键字大小有序排列的顺序表。

折半查找的思想:将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否

则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一

步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,

或直到子表不存在为止,此时查找不成功。

折半查找(详见课程视频)

intSearch_Bin(SSTableST, intkey)



   low=1; high=ST.length;

   while(low<=high){

       mid=(low+high)/2;

 if(ST.elem[mid].key==key) /查找成功返回/

   return mid;

 if(ST.elem[mid].key>key)

   high=mid-1;  /继续在[low..mid-1]中查找/

 else

   low=mid+1;  /继续在[mid+1..high]中查找/

   }

   return0;



判定树(比较树):二分查找过程可用二叉树来描述,把当前查找区间的中间位置上的记录作为

根,左子表和右子表中的记录分别作为根的左子树和右子树。

·折半查找成功时的平均查找长度ASL

假定表的长度n=2



-1,则相应判定树必为深度是h的满二叉树,h=log



(n+1)。又假设每个

记录的查找概率相等,则折半查找成功时的平均查找长度为:

ASL

bs







i=1



















j=1

j×2

j-1



n+1



log



(n+1)-1

当n较大(n>50)时,有近似结果:

ASL

bs

=log



(n+1)-1

折半查找判定树

(1)在查找成功时,会找到图中某个圆形结点,则成功时的平均查找长度:

ASLsucc=

1×1+2×2+4×3+4×4

11

=3

(2)在查找不成功时,会找到图中某个方形结点,则不成功时的平均查找长度:

ASLunsucc=

4×3+8×4

12

=3.67

3.索引顺序查找(分块查找)

是一种性能介于顺序查找和二分查找之间的查找方法。

将表[1..n-1]均分为b块,前b-1块中记录个数为s=「n/b?,最后一块即第b块的记录数小

于等于s;

每一块中的关键字不一定有序,但前一块中的最大关键字必须小于后一块中的最小关键字,即要

“分块有序”。

抽取各块中的最大关键字及其起始位置构成一个索引表ID[b]。由于表R[n]是分块有序的,所

以索引表是一个递增有序表。

例如,设有一个线性表,其中包含25个记录,其关键字序列为{8,14,6,9,10,22,34,18,19,31,

40,38,54,66,46,71,78,68,80,85,100,94,88,96,87}。假设将25个记录分为5块,每块中有5个

记录,该线性表的索引存储结构如下图所示。

查找索引表的ASL为:L



;块内进行顺序查找的ASL为L





ASL

bs

=L



+L



b块,每块含s个元素。查找概率相等,则每个索引项的查找概率为1/b,块中每个元素的查找概

率为1/s。

·若用顺序查找法确定待查元素所在的块,则有

ASL

bs

=L



+L













j=1

j+









i=1

i=

b+s



+1=











+s)+1

·若用折半查找法确定待查元素所在的块,则有

ASL

bs

=log



(b+1)-1+

s+1



≈log









( )

1+





·静态查找表的三种查找方法的比较

·顺序查找对对于表有序、无序均适用;折半查找仅适用于有序表;分块查找要求表分块后“分块

有序”。

·从表的存储结构上看,顺序查找和分块查找对于表的顺序和链式存储结构均适用,而折半查找

只适用于顺序存储结构。

·平均查找长度ASL而言,折半最小(log



(n+1)-1),分块次之(



n+1),顺序最大((n+1)/2)。

9.2 动态查找表

动态查找表的特点:表结构本身在查找过程中动态生成,即对于给定值key,若表中存在关键字等

于key的记录,则查找成功,否则插入关键字等于key的记录。

二叉排序树、平衡二叉排序树和B树等。

1.二叉排序树BST(BinarySortTree)的定义

或者是一棵空树,或者是具有如下性质的二叉树:

(1)若它的左子树不空,则左子树上所有结点的值均小于根结点的值;

(2)若它的右子树不空,则右子树上所有结点的值均大于根结点的值;

(3)它的左右子树也分别为二叉排序树。

typedefSqListHeapType;

voidHeapAdjust(HeapType&H,ints,intm)



rc=H.r[s];

  for(j=2s;j<=m;j=2){

    if(j<m&&H.r[j].key<H.r[j+1].key) ++j;

    if(rc.key>=H.r[j].key) break;

H.r[s]=H.r[j];s=j;

  }

H.r[s]=rc;



voidHeapSort(HeapType&H)



  for(i=H.length/2;i>0;--i) //建立初始堆

HeapAdjust(H,i,H.length);

  for(i=H.length;i>1;--i){

H.r[1]

←→

H.r[i];

HeapAdjust(H,1,i-1);

  }



·算法分析

深度为k的堆,筛选算法中进行的关键字比较次数至多为2(k-1)。

建初始堆进行了G22n/2G23次筛选,关键字比较次数至多为:





i=h-1



i-1

·2(h-i)=





i=h-1





·(h-i)=



h-1

j=1



h-j

·jG21(2n)



h-1

j=1

j/2



G214n

n个关键字,完全二叉树的深度G22log



nG23+1。调整建立新堆时,调用HeapAdjust过程n-1次,关

键字比较次数至多为:

2(?log



(n-1)G23+?log



(n-2)G23+...+log



2)<2n(?log



nG23)

堆排序的时间复杂度为O(nlog



n),空间复杂性为O(1).

堆排序是一个不稳定的排序方法。

10.5 归并排序(MergingSort)

·归并:是将两个或两个以上的有序表合并成一个新的有序表。

·2-路归并:假设初始序列有n个记录,首先把它看成是n个长度为1的有序子序列(归并

项),先做两两归并,得到G24n/2G25个长度为2的归并项(如果n为奇数,则最后一个有序子序列的长度

为1);再做两两归并,…,如此重复,最后得到一个长度为n的有序序列。

采用2-路归并排序方法进行排序的过程(11个记录)。

一趟归并进行G24n/(2h)G25次两个有序子表的归并操作Merger.

将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n].

voidMerge(RcdTypeSR[],RcdType&TR[],inti,intm,intn)



   for(j=m+1,k=i;i<=m&&j<=n;++k){

     if(SR[i].key<=SR[j].key) TR[k]=SR[i++];

     else TR[k]=SR[j++];

  }

  if(i<=m) TR[k..n]=SR[i..m];

  if(j<=n) TR[k..n]=SR[j..n];



有序子表长度分别为:n,m. 则Merge的时间复杂度为:O(n+m).

voidMergePass(RcdTypeSR[],RcdType&TR[],inth,intn)



  for(i=1;i+2h-1<=n;i=i+2h) //归并h长的两相邻子表  

      Merge(SR,TR,i,i+h-1,i+2h-1);

  if(i+h-1<=n) //余下部分

   Merge(SR,TR,i,i+h-1,n);



迭代的归并排序算法(一趟归并排序的情形)

voidMergeSort(SqList&L)  //自底向上的二路归并算法

{RcdTypeTR[];

  for(h=1;h<L.length;h=2h)

{MergePass(L.r,TR,h,L.length);

 L.r[1..L.length]=TR[1..L.length];





算法总的时间复杂度:O(nlog



n)

递归的归并排序算法

voidMSort(RcdTypeSR[],RcdType&TR1[],ints,intt)



  if(s==t) TR1[s]=SR[s];

  else{

    m=(s+t)/2;

MSort(SR,TR2,s,m);

MSort(SR,TR2,m+1,t);

    Merge(TR2,TR1,s,m,t);

  }



voidMergeSort(SqList&L)



MSort(L.r,L.r,1,L.length);



算法分析

·在迭代的归并排序算法中,函数MergePass()做一趟两路归并排序,要调用merge()函数G24n/

(2h)G25次,函数MergeSort()调用MergePass()正好G24log



nG25次,而每次merge()至多执行比较2h次,

所以算法总的时间复杂度为O(nlog



n)。

·递归的归并排序方法的递归深度为G24log



nG25,算法总的时间复杂度为O(nlog



n)。

·归并排序占用附加存储较多,需要另外一个与原待排序记录数组同样大小的辅助数组。O(n)

这是这个算法的缺点。

·归并排序是一个稳定的排序方法。

10.6 基数排序(RadixSort)

基数排序是通过“分配”和“收集”过程来实现排序,是一种借助于多关键字排序的思想对单关键

字排序的方法。

1.多关键字排序

每张扑克牌有两个“关键码”:花色和面值。其有序关系为:

花色:G2C G2D G2E G2D G2F G2D G30

面值:2<3<4<5<6<7<8<9<10<J<Q<K<A

所有扑克牌排成以下次序:

G2C2,…,G2CA,G2E2,…,G2EA,G2F2,…,G2FA,G302,…,G30A

有n个记录的序列{R



,R



,…,R



},且每个记录R



中含有d个关键字







,K





,…,K

( )





序列中任意两个对象R



和R



 (1G21i<jG21n)都满足:







,K





,…,K

( )













,K





,…,K

( )





则称序列对关键字(K



,K



,…,K



)有序。其中,K



称为最主

位关键字,K



称为最次位关键字。

实现多关键字排序有两种常用的方法

·最高位优先MSD(MostSignificantDigitfirst)

·先根据最高位关键字K



排序,得到若干组,每组中每个记录都有相同关键字K





·再分别对每组中记录根据关键字K



进行排序,按K



值的不同,再分成若干个更小的子组,每个

子组中的记录具有相同的K



和K



值。

·依此重复,直到对关键字K



完成排序为止。

·最后,把所有子组中的记录依次连接起来,就得到一个有序的序列。

·最低位优先LSD(LeastSignificantDigitfirst)

首先依据最低位关键字K



对所有记录进行一趟排序,再依据次低位关键字K

d-1

对上一趟排序的

结果再排序,依次重复,直到依据关键字K1最后一趟排序完成,就可以得到一个有序的序列。使用这

种排序方法对每一个关键字进行排序时,不需要再分组,而是整个记录都参加排序。

52张牌排序方法:

最高位优先法(MSDF):

先按不同“花色”分成有次序的4堆,每一堆均具有相同的花色;

然后分别对每一堆按“面值”大小整理有序。

最低位优先法(LSDF):

先按不同“面值”分成13堆;

然后将这13堆牌自小至大叠在一起(2,3,...,A);

然后将这付牌整个颠倒过来再重新按不同的“花色”分成4堆;

最后将这4堆牌按自小至大的次序合在一起。

2.链式基数排序

·基数排序是典型的LSD排序方法,利用“分配”和“收集”两种运算对单关键字进行排序。在这

种方法中,把单关键字K



看成是一个d元组:







,K





,…,K

( )





·分量有radix种取值,则称radix为基数,即分量的取值范围。

关键字984可以看成是一个3元组(9,8,4),每一位有0,1,…,9等10种取值,基数radix=

10。关键字‘data’可以看成是一个4元组(d,a,t,a),每一位有‘a’,‘b’,…,‘z’等26种取值,ra

dix=26。

·记录的关键字K



,K



,…,K

d-1

,依次对各位的分量,分别用“分配”、“收集”的运算逐趟进行

排序,

·各队列采用链式队列结构,分配到同一队列的关键字用指针链接起来。每一队列设置两个指

针:front[radix]指示队头,rear[radix]指向队尾。

·以静态链表作为n个记录的存储结构。在记录重排时不必移动记录,只需修改各记录的链接

指针即可。

例 序列 278  109  063  930  589  184  505 269  008  083

(详见视频)

算法分析

·若每个关键字有d位,需要重复执行d趟“分配”与“收集”。每趟对n个记录进行“分配”,对

radix个队列进行“收集”。总时间复杂度为O(d(n+radix))。

·若基数radix相同,对于记录个数较多而关键字位数较少的情况,使用链式基数排序较好。

·基数排序需要增加n+2radix个附加链接指针。

·基数排序是稳定的排序方法。

10.7 各种内部排序方法的比较讨论

1.选择排序方法时需考虑的因素

·待排序的记录数目;

·记录本身信息量的大小;

·关键字的结构及其分布情况;

·对排序稳定性的要求;

·语言工具的条件,辅助空间的大小。

2.各种内部排序方法的性能

3.结论:没有哪一种排序方法是绝对好的,都有其优缺点

·若n较小,可采用直接插入排序或简单选择排序

·若序列的初始状态已按关键字基本有序,则选用直接插入或起泡排序为宜;

·若n较大,采用O(nlog



n)的排序方法;

·若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好;

·避免移动记录,用链表作存储结构,如表插入;

·内部排序可能达到的最快速度是什么?时间下界?

时间上界O(n





任何借助于“比较”的排序方法,至少需要O(nlog



n)的时间。

·三个关键字:k



,k



,k



,则描述3个记录排序过程的判定树必有3!=6个叶子结点。

·n个记录的序列,初始状态有n!个,则描述n个记录排序过程的判定树必有n!个叶子结点。

则判定树的树高至少为G24log



n!G25+1,log



n!≈nlog



n,

·最坏情况下能达到的最好的时间复杂度为O(nlog



n).

二、真题举例

1.已知一关键码序列为:3,87,12,61,70,97,26,45。试根据堆排序原理,填写完整下示各步骤结

果。【首都经贸大学】

建立堆结构:        

交换与调整:(1)877026614512397;(2);

(3)614526312708797;(4);

(5)261234561708797;(6);;

(7)312264561708797;

献花(0)
+1
(本文系书香园分享首藏)