数据结构(c语言版)


数据结构(C语言版) 目 录 第1章 绪论 第2章 线性表 第3章 栈和队列 第4章 串 第5章 数组 第6章 树 第7章 图 第8章 查找 第9章 排序 第10章 文件 第1章 绪论 第2章 线性表 第3章 栈和队列 第4章 串 第5章 数组 第6章 树 第7章 图 第8章 查找 第9章 排序 第10章 文件 数据结构(C语言版) 第1章 绪 论 1.1 数据结构的基本概念和术语 1.2 算法描述与分析 1.3 实习:常用算法实现及分析 习题1 1.1 数据结构的基本概念和术语 1.2 算法描述与分析 1.3 实习:常用算法实现及分析 习题1 数据结构(C语言版) 1.1 数据结构的基本概念和术语 1.1.1 引例 首先分析学籍档案类问题。设一个班级有50个学生,这个 班级的学籍表如表1.1所示。 表1.1 学 籍 表  表1.1 学 籍 表 序号 学号 姓名 性别 英语 数学 物理 01 200303 01 李明 男 86 91 80 02 200303 02 马琳 男 76 83 85 50 200303 50 刘薇薇 女 88 93 90 数据结构(C语言版) 我们可以把表中每个学生的信息看成一个记录,表中的每 个记录又由7个数据项组成。该学籍表由50个记录组成,记录 之间是一种顺序关系。这种表通常称为线性表,数据之间的逻 辑结构称为线性结构,其主要操作有检索、查找、插入或删除 等。 又如,对于学院的行政机构,可以把该学院的名称看成 树根,把下设的若干个系看成它的树枝中间结点,把每个系分 出的若干专业方向看成树叶,这样就形成一个树型结构,如图 1.1所示。 我们可以把表中每个学生的信息看成一个记录,表中的每 个记录又由7个数据项组成。该学籍表由50个记录组成,记录 之间是一种顺序关系。这种表通常称为线性表,数据之间的逻 辑结构称为线性结构,其主要操作有检索、查找、插入或删除 等。 又如,对于学院的行政机构,可以把该学院的名称看成 树根,把下设的若干个系看成它的树枝中间结点,把每个系分 出的若干专业方向看成树叶,这样就形成一个树型结构,如图 1.1所示。 数据结构(C语言版) 理工学院 机械工程 材料工程 信息工程 机械制造与自动化 工业设计 模具设计 热处理 计算机科学与技术 图1.1 专业设置 理工学院 机械工程 材料工程 信息工程 机械制造与自动化 工业设计 模具设计 热处理 计算机科学与技术 数据结构(C语言版) 树中的每个结点可以包含较多的信息,结点之间的关系不 再是顺序的,而是分层、分叉的结构。树型结构的主要操作有 遍历、查找、插入或删除等。 最后分析交通问题。如果把若干个城镇看成若干个顶点, 再把城镇之间的道路看成边,它们可以构成一个网状的图(如图 1.2所示),这种关系称为图型结构或网状结构。在实际应用中, 假设某地区有5个城镇,有一调查小组要对该地区每个城镇进行 调查研究,并且每个城镇仅能调查一次,试问调查路线怎样设 计才能以最高的效率完成此项工作?这是一个图论方面的问题。 交通图的存储和管理确实不属于单纯的数值计算问题,而是一 种非数值的信息处理问题。 树中的每个结点可以包含较多的信息,结点之间的关系不 再是顺序的,而是分层、分叉的结构。树型结构的主要操作有 遍历、查找、插入或删除等。 最后分析交通问题。如果把若干个城镇看成若干个顶点, 再把城镇之间的道路看成边,它们可以构成一个网状的图(如图 1.2所示),这种关系称为图型结构或网状结构。在实际应用中, 假设某地区有5个城镇,有一调查小组要对该地区每个城镇进行 调查研究,并且每个城镇仅能调查一次,试问调查路线怎样设 计才能以最高的效率完成此项工作?这是一个图论方面的问题。 交通图的存储和管理确实不属于单纯的数值计算问题,而是一 种非数值的信息处理问题。 数据结构(C语言版) A地 B地 D地 C地 E地 图1.2 交通示意图 A地 B地 D地 C地 E地 数据结构(C语言版) 1.1.2 数据结构有关概念及术语 一般来说,数据结构研究的是一类普通数据的表示及其相 关的运算操作。数据结构是一门主要研究怎样合理地组织数据, 建立合适的数据结构,提高计算机执行程序所用的时间效率和 空间效率的学科。1968年,美国的D.E.Knuth教授开创了数据结 构的最初体系,他的名著《计算机程序设计技巧》较为系统地 阐述了数据的逻辑结构和存储结构及其操作。 “数据结构”是计算机专业的一门专业基础课。它为操作 系统、数据库原理、编译原理等后继专业课程的学习奠定了基 础。数据结构涉及到各方面的知识,如计算机硬件范围中的存 储装置和存取方法,计算机软件范围中的文件系统、数据的动 态存储与管理、信息检索,数学范围中的许多算法知识。 1.1.2 数据结构有关概念及术语 一般来说,数据结构研究的是一类普通数据的表示及其相 关的运算操作。数据结构是一门主要研究怎样合理地组织数据, 建立合适的数据结构,提高计算机执行程序所用的时间效率和 空间效率的学科。1968年,美国的D.E.Knuth教授开创了数据结 构的最初体系,他的名著《计算机程序设计技巧》较为系统地 阐述了数据的逻辑结构和存储结构及其操作。 “数据结构”是计算机专业的一门专业基础课。它为操作 系统、数据库原理、编译原理等后继专业课程的学习奠定了基 础。数据结构涉及到各方面的知识,如计算机硬件范围中的存 储装置和存取方法,计算机软件范围中的文件系统、数据的动 态存储与管理、信息检索,数学范围中的许多算法知识。 数据结构(C语言版) 在计算机科学中,数据(Data)是描述客观事物的数字、字 符以及所有能够输入到计算机中并被计算机处理的信息的总称。 除了数字、字符之外,还有用英文、汉字或其他语种字母组成 的词组、语句,以及表示图形、图像和声音等的信息,这些也 可称为数据。 数据元素(Data Element)是数据的基本单位,在计算机中通 常作为一个整体进行考虑和处理。例如,图1.1“专业设置树” 中的一个专业,图1.2“交通图”中的一个城镇都可称为一个数 据元素。数据元素除了可以是一个数字或一个字符串以外,它 也可以由一个或多个数据项组成。例如,表1.1中每个学生的学 籍信息作为一个数据元素,在表中占一行。每个数据元素由序 号、学号、姓名、性别、英语成绩等7个数据项组成。数据项 (Data Item)是有独立含义的数据的最小单位,有时也称为字段 (Field)。 在计算机科学中,数据(Data)是描述客观事物的数字、字 符以及所有能够输入到计算机中并被计算机处理的信息的总称。 除了数字、字符之外,还有用英文、汉字或其他语种字母组成 的词组、语句,以及表示图形、图像和声音等的信息,这些也 可称为数据。 数据元素(Data Element)是数据的基本单位,在计算机中通 常作为一个整体进行考虑和处理。例如,图1.1“专业设置树” 中的一个专业,图1.2“交通图”中的一个城镇都可称为一个数 据元素。数据元素除了可以是一个数字或一个字符串以外,它 也可以由一个或多个数据项组成。例如,表1.1中每个学生的学 籍信息作为一个数据元素,在表中占一行。每个数据元素由序 号、学号、姓名、性别、英语成绩等7个数据项组成。数据项 (Data Item)是有独立含义的数据的最小单位,有时也称为字段 (Field)。 数据结构(C语言版) 数据对象(Data Object)是具有相同性质的数据元素的集合, 是数据的一个子集。例如,整数数据对象是集合N={0,±1, ±2,…},字母字符数据对象是集合C={'A', 'B', …, 'Z'}。本节表 1.1中的学籍表也可看成一个数据对象。 数据结构(Data Structure)是带有结构的数据元素的集合,它 是指数据元素之间的相互关系,即数据的组织形式。我们把数 据元素间的逻辑上的联系,称为数据的逻辑结构。常见的数据 结构有前文所介绍的线性结构、树型结构、图型结构。数据的 逻辑结构体现数据元素间的抽象化相互关系,并不涉及数据元 素在计算机中具体的存储方式,是独立于计算机的。 数据对象(Data Object)是具有相同性质的数据元素的集合, 是数据的一个子集。例如,整数数据对象是集合N={0,±1, ±2,…},字母字符数据对象是集合C={'A', 'B', …, 'Z'}。本节表 1.1中的学籍表也可看成一个数据对象。 数据结构(Data Structure)是带有结构的数据元素的集合,它 是指数据元素之间的相互关系,即数据的组织形式。我们把数 据元素间的逻辑上的联系,称为数据的逻辑结构。常见的数据 结构有前文所介绍的线性结构、树型结构、图型结构。数据的 逻辑结构体现数据元素间的抽象化相互关系,并不涉及数据元 素在计算机中具体的存储方式,是独立于计算机的。 数据结构(C语言版) 然而,讨论数据结构的目的是为了在计算机中实现对数据 的操作,因此还需要研究如何在计算机中表示数据。数据的逻 辑结构在计算机存储设备中的映像被称为数据的存储结构,也 可以说数据的存储结构是逻辑结构在计算机存储器中的实现, 又称物理结构。数据的存储结构是依赖于计算机的。常见的存 储结构有顺序存储结构、链式存储结构等。关于它们的详细解 释将在以后的章节中逐步给出。 通常所谓的“数据结构”是指数据的逻辑结构、数据的存 储结构以及定义在它们之上的一组运算。不论是存储结构的设 计,还是运算的算法设计,都必须考虑存储空间的开销和运行 时间的效率。因此,“数据结构”课程不仅讲授数据信息在计 算机中的组织和表示方法,同时也训练学生高效地解决复杂问 题程序设计的能力。 然而,讨论数据结构的目的是为了在计算机中实现对数据 的操作,因此还需要研究如何在计算机中表示数据。数据的逻 辑结构在计算机存储设备中的映像被称为数据的存储结构,也 可以说数据的存储结构是逻辑结构在计算机存储器中的实现, 又称物理结构。数据的存储结构是依赖于计算机的。常见的存 储结构有顺序存储结构、链式存储结构等。关于它们的详细解 释将在以后的章节中逐步给出。 通常所谓的“数据结构”是指数据的逻辑结构、数据的存 储结构以及定义在它们之上的一组运算。不论是存储结构的设 计,还是运算的算法设计,都必须考虑存储空间的开销和运行 时间的效率。因此,“数据结构”课程不仅讲授数据信息在计 算机中的组织和表示方法,同时也训练学生高效地解决复杂问 题程序设计的能力。 数据结构(C语言版) 1.2 算法描述与分析 1.2.1 什么是算法 在解决实际问题时,当确定了数据的逻辑结构和存储结构 之后,需进一步研究与之相关的一组操作(也称运算),主要有 插入、删除、排序、查找等。为了实现某种操作(如查找),常 常需要设计一种算法。算法(Algorithm)是对特定问题求解步骤 的一种描述,是指令的有限序列。描述算法需要一种语言,可 以是自然语言、数学语言或者是某种计算机语言。算法一般具 有下列5个重要特性: 1.2.1 什么是算法 在解决实际问题时,当确定了数据的逻辑结构和存储结构 之后,需进一步研究与之相关的一组操作(也称运算),主要有 插入、删除、排序、查找等。为了实现某种操作(如查找),常 常需要设计一种算法。算法(Algorithm)是对特定问题求解步骤 的一种描述,是指令的有限序列。描述算法需要一种语言,可 以是自然语言、数学语言或者是某种计算机语言。算法一般具 有下列5个重要特性: 数据结构(C语言版) (1) 输入:一个算法应该有零个、一个或多个输入。 (2) 有穷性:一个算法必须在执行有穷步骤之后正常结束, 而不能形成无穷循环。 (3) 确定性:算法中的每一条指令必须有确切的含义,不能 产生多义性。 (4) 可行性:算法中的每一个指令必须是切实可执行的,即 原则上可以通过已经实现的基本运算执行有限次来实现。 (5) 输出:一个算法应该至少有一个输出,这些输出是同输 入有某种特定关系的量。 (1) 输入:一个算法应该有零个、一个或多个输入。 (2) 有穷性:一个算法必须在执行有穷步骤之后正常结束, 而不能形成无穷循环。 (3) 确定性:算法中的每一条指令必须有确切的含义,不能 产生多义性。 (4) 可行性:算法中的每一个指令必须是切实可执行的,即 原则上可以通过已经实现的基本运算执行有限次来实现。 (5) 输出:一个算法应该至少有一个输出,这些输出是同输 入有某种特定关系的量。 数据结构(C语言版) 1.2.2 算法描述工具—— C语言 如何选择描述数据结构和算法的语言是十分重要的问题。 传统的描述方法是用PASCAL语言。在Windows环境下涌现出 一系列功能强大、面向对象的描述工具,如Visual C++, Borland C++,Visual Basic,Visual FoxPro等。近年来在计算机 科学研究、系统开发、教学以及应用开发中,C语言的使用越 来越广泛。因此,本教材采用C语言进行算法描述。为了能够 简明扼要地描述算法,突出算法的思路,而不拘泥于语言语法 的细节,本书有以下约定: 1.2.2 算法描述工具—— C语言 如何选择描述数据结构和算法的语言是十分重要的问题。 传统的描述方法是用PASCAL语言。在Windows环境下涌现出 一系列功能强大、面向对象的描述工具,如Visual C++, Borland C++,Visual Basic,Visual FoxPro等。近年来在计算机 科学研究、系统开发、教学以及应用开发中,C语言的使用越 来越广泛。因此,本教材采用C语言进行算法描述。为了能够 简明扼要地描述算法,突出算法的思路,而不拘泥于语言语法 的细节,本书有以下约定: 数据结构(C语言版) (1) 问题的规模尺寸用MAXSIZE表示,约定在宏定义中已 经预先定义过,例如: #define MAXSIZE 100 (2) 数据元素的类型一般写成ELEMTP,可以认为在宏定义 中预先定义过,例如: #define ELEMTP int 在上机实验时根据需要,可临时用其他某个具体的类型标 识符来代替。 (1) 问题的规模尺寸用MAXSIZE表示,约定在宏定义中已 经预先定义过,例如: #define MAXSIZE 100 (2) 数据元素的类型一般写成ELEMTP,可以认为在宏定义 中预先定义过,例如: #define ELEMTP int 在上机实验时根据需要,可临时用其他某个具体的类型标 识符来代替。 数据结构(C语言版) (3) 一个算法要以函数形式给出: 类型标识符 函数名(带类型说明的形参表) {语句组} 例如: int add (int a,int b) {int c; c=a+b; return(c); } 除了形参类型说明放在圆括号中之外,在描述算法的函数 中其他变量的类型说明一般省略不写,这样使算法的处理过程 更加突出明了。 (3) 一个算法要以函数形式给出: 类型标识符 函数名(带类型说明的形参表) {语句组} 例如: int add (int a,int b) {int c; c=a+b; return(c); } 除了形参类型说明放在圆括号中之外,在描述算法的函数 中其他变量的类型说明一般省略不写,这样使算法的处理过程 更加突出明了。 数据结构(C语言版) (4) 关于数据存储结构的类型定义以及全局变量的说明等均 应在写算法之前进行说明。 下面的例子给出了书写算法的一般步骤。 例1.1 有n个整数,将它们按由大到小的顺序排序,并且输 出。 分析:n个数据的逻辑结构是线性表(a1,a2,a3,…,an);选用 一维数组作存储结构。每个数组元素有两个域:一个是数据的 序号域,一个是数据的值域。 (4) 关于数据存储结构的类型定义以及全局变量的说明等均 应在写算法之前进行说明。 下面的例子给出了书写算法的一般步骤。 例1.1 有n个整数,将它们按由大到小的顺序排序,并且输 出。 分析:n个数据的逻辑结构是线性表(a1,a2,a3,…,an);选用 一维数组作存储结构。每个数组元素有两个域:一个是数据的 序号域,一个是数据的值域。 数据结构(C语言版) struct node {int num; /*序号域*/ int data; /*值域*/ } /*算法描述1.1*/ void simsort(struct node a [MAXSIZE], int n)/*数组a的数据由主函数提供*/ {int i,j, m; for(i=1;ij) j++; else i++; } } (5) {x=n; /*n>1*/ y=0; while(x>=(y+1)*(y+1)) y++; } (4) {i=1;j=0; while(i+j<=n) {if(i>j) j++; else i++; } } (5) {x=n; /*n>1*/ y=0; while(x>=(y+1)*(y+1)) y++; } 数据结构(C语言版) (6) {m=91;n=100; while(n>0) {if(m>0){m=m-1;n--;} else m++; } } (6) {m=91;n=100; while(n>0) {if(m>0){m=m-1;n--;} else m++; } } 数据结构(C语言版) 第2章 线性表 2.1 线性表引例 2.2 线性表的定义和基本运算 2.3 线性表的顺序存储结构 2.4 线性表的链式存储结构 2.5 循环链表和双向链表 2.6 实习:线性表的应用实例 习题2 2.1 线性表引例 2.2 线性表的定义和基本运算 2.3 线性表的顺序存储结构 2.4 线性表的链式存储结构 2.5 循环链表和双向链表 2.6 实习:线性表的应用实例 习题2 数据结构(C语言版) 2.1 线性表引例 例2.1 某大学欲进行一次数学竞赛,约有200名学生报名 参赛。现将报名登记表(如表2.1所示)存入计算机以便完成如下 工作: (1) 能正确录入学生记录; (2) 按成绩对该表进行重新排序; (3) 按学号或姓名查询学生成绩。 例2.1 某大学欲进行一次数学竞赛,约有200名学生报名 参赛。现将报名登记表(如表2.1所示)存入计算机以便完成如下 工作: (1) 能正确录入学生记录; (2) 按成绩对该表进行重新排序; (3) 按学号或姓名查询学生成绩。 数据结构(C语言版) 表2.1 报 名 登 记 表 学 号 姓 名 性 别 成 绩 2003 张三 男 84  2003 张三 男 84 2024 李四 男 79 2035 王五 女 75    数据结构(C语言版) 2.2 线性表的定义和基本运算 2.2.1 线性表的概念 线性表是指n(n≥0)个具有相同类型数据元素(或称结点)的有 限序列,可表示为(a1,a2,...,ai,...,an)。其中,ai代表一个数据元 素,a1称为表头(或头结点),an称为表尾(或尾结点),ai(0≤i<n) 称为ai+1的直接前驱,ai+1称为ai的直接后继。线性表中数据元 素的个数称为线性表的长度,长度为0的线性表称为空表,记 为()。 2.2.1 线性表的概念 线性表是指n(n≥0)个具有相同类型数据元素(或称结点)的有 限序列,可表示为(a1,a2,...,ai,...,an)。其中,ai代表一个数据元 素,a1称为表头(或头结点),an称为表尾(或尾结点),ai(0≤i<n) 称为ai+1的直接前驱,ai+1称为ai的直接后继。线性表中数据元 素的个数称为线性表的长度,长度为0的线性表称为空表,记 为()。 数据结构(C语言版) 在不同的问题中,数据元素代表的具体含义不同,它可 以是一个数字﹑一个字符,也可以是一句话,甚至其他更复杂 的信息。例如: 线性表L1: (12, 58, 45, 2, 45, 46), 其元素为数字; 线性表L2: (a, g, r, d, s, t), 其元素为字母。 表2.1也是一个线性表,其数据元素较为复杂,每个学生 的学号﹑姓名﹑性别﹑成绩构成一个数据元素。这种由若干数 据项构成的数据元素常称为记录,含有大量记录的线性表称为 文件。 在不同的问题中,数据元素代表的具体含义不同,它可 以是一个数字﹑一个字符,也可以是一句话,甚至其他更复杂 的信息。例如: 线性表L1: (12, 58, 45, 2, 45, 46), 其元素为数字; 线性表L2: (a, g, r, d, s, t), 其元素为字母。 表2.1也是一个线性表,其数据元素较为复杂,每个学生 的学号﹑姓名﹑性别﹑成绩构成一个数据元素。这种由若干数 据项构成的数据元素常称为记录,含有大量记录的线性表称为 文件。 数据结构(C语言版) 2.2.2 表的基本运算 线性表是一种相当灵活的数据结构,对其数据元素可以进 行各种运算(操作)。如对表2.1,应不仅能查询成绩,还能根据 需要增加或删除学生记录。下面给出线性表一些基本运算的含 义,这些运算的实现算法后面将具体讨论。 (1) Initiate (L):初始化运算。该函数用于设定一个空的线 性表L。 (2) Length (L):求长度函数。该函数返回给定线性表L中 数据元素的个数。 (3) Get (L, i ):取元素操作。若1≤i≤Length(L),则函数值 为给定线性表中第i个数据元素,否则为空元素NULL。 2.2.2 表的基本运算 线性表是一种相当灵活的数据结构,对其数据元素可以进 行各种运算(操作)。如对表2.1,应不仅能查询成绩,还能根据 需要增加或删除学生记录。下面给出线性表一些基本运算的含 义,这些运算的实现算法后面将具体讨论。 (1) Initiate (L):初始化运算。该函数用于设定一个空的线 性表L。 (2) Length (L):求长度函数。该函数返回给定线性表L中 数据元素的个数。 (3) Get (L, i ):取元素操作。若1≤i≤Length(L),则函数值 为给定线性表中第i个数据元素,否则为空元素NULL。 数据结构(C语言版) (4) Prior (L, x):求前驱函数。当x在线性表L中,且其位序 大于1,则函数值为x的直接前驱,否则为空元素。 (5) Next (L, x):求后继函数。当x在线性表L中,且其位序 小于Length(L), 则函数值为x的直接后继,否则为空元素。 (6) Locate (L,x):定位操作。如线性表中存在和x 相等的数 据元素,则返回该数据元素的位序。若满足条件的元素不惟一, 则返回最小的位序。 (7) Insert (L, i, x): 前插操作。若1≤i≤Length(L)+1,则在线 性表L中第i个元素之前插入新结点x。 (4) Prior (L, x):求前驱函数。当x在线性表L中,且其位序 大于1,则函数值为x的直接前驱,否则为空元素。 (5) Next (L, x):求后继函数。当x在线性表L中,且其位序 小于Length(L), 则函数值为x的直接后继,否则为空元素。 (6) Locate (L,x):定位操作。如线性表中存在和x 相等的数 据元素,则返回该数据元素的位序。若满足条件的元素不惟一, 则返回最小的位序。 (7) Insert (L, i, x): 前插操作。若1≤i≤Length(L)+1,则在线 性表L中第i个元素之前插入新结点x。 数据结构(C语言版) (8) Delete (L, i): 删除操作。若1≤i≤Length(L),则删除线性 表L中第 i个元素。 (9) Empty (L):判空函数。若L为空表,则返回值为1(表示 “真”),否则返回值为0(表示“假”)。 (10) Clear (L):置空操作。将线性表L 值为空表。 利用这些基本运算还可实现对线性表的各种复杂操作。如 将两个线性表进行合并,重新复制一个线性表,对线性表中的 元素按某个数据项递增(或递减)的顺序进行重新排序等。读者 可将以上基本运算应用于表2.1,理解在具体问题中各种运算的 具体含义。 (8) Delete (L, i): 删除操作。若1≤i≤Length(L),则删除线性 表L中第 i个元素。 (9) Empty (L):判空函数。若L为空表,则返回值为1(表示 “真”),否则返回值为0(表示“假”)。 (10) Clear (L):置空操作。将线性表L 值为空表。 利用这些基本运算还可实现对线性表的各种复杂操作。如 将两个线性表进行合并,重新复制一个线性表,对线性表中的 元素按某个数据项递增(或递减)的顺序进行重新排序等。读者 可将以上基本运算应用于表2.1,理解在具体问题中各种运算的 具体含义。 数据结构(C语言版) 2.3 线性表的顺序存储结构 2.3.1 向量的存储特点 在计算机内,线性表可以用不同的方式来存储。其中最简 单﹑最常用的方式就是顺序存储,即用一组连续的存储单元依 次存放线性表中的元素。这种顺序存储的线性表称为顺序表, 又叫向量。 假设线性表每个元素占s个存储单元,并以其所占的第一个 单元的存储地址作为数据元素的存储位置, 则线性表中第i+1个 元素的存储位置Loc(ai+1)和第i个数据元素的存储位置Loc(ai)之 间满足下列关系:Loc(ai+1) = Loc(ai) + s 2.3.1 向量的存储特点 在计算机内,线性表可以用不同的方式来存储。其中最简 单﹑最常用的方式就是顺序存储,即用一组连续的存储单元依 次存放线性表中的元素。这种顺序存储的线性表称为顺序表, 又叫向量。 假设线性表每个元素占s个存储单元,并以其所占的第一个 单元的存储地址作为数据元素的存储位置, 则线性表中第i+1个 元素的存储位置Loc(ai+1)和第i个数据元素的存储位置Loc(ai)之 间满足下列关系:Loc(ai+1) = Loc(ai) + s 数据结构(C语言版) 设线性表的起始位置(或称基址)是Loc(a1), 因每个元素所占用 的空间大小相同, 则元素ai的存放位置为: Loc(ai)= Loc(a1)+s*(i-1) 由此可见,线性表的顺序存储结构是用数据元素在计算机 内“物理位置相邻”来表示数据元素之间的逻辑相邻关系,其 特点是向量中逻辑上相邻的结点在计算机的存储结构中也相邻, 如图2.1所示。而且,只要知道了向量的基地址,由上式即可确 定向量中任一数据元素的地址,从而对其可随机存取。 设线性表的起始位置(或称基址)是Loc(a1), 因每个元素所占用 的空间大小相同, 则元素ai的存放位置为: Loc(ai)= Loc(a1)+s*(i-1) 由此可见,线性表的顺序存储结构是用数据元素在计算机 内“物理位置相邻”来表示数据元素之间的逻辑相邻关系,其 特点是向量中逻辑上相邻的结点在计算机的存储结构中也相邻, 如图2.1所示。而且,只要知道了向量的基地址,由上式即可确 定向量中任一数据元素的地址,从而对其可随机存取。 数据结构(C语言版) 元素在线性 表中的位置 存储地址 内存状态 1 b 2 b+ s … b+ s (i- 1)* … b+ s (maxsize- 1)*n n- 1 … i … 图2.1 线性表的顺序存储结构示意图 元素在线性 表中的位置 存储地址 内存状态 1 b 2 b+ s … b+ s (i- 1)* … b+ s (maxsize- 1)*n n- 1 … i … 数据结构(C语言版) 在C语言中,可以用一维数组来描述向量。 # define maxsize N; /*设置线性表的最大长度为N, N为整数*/ typedef struct {datatype data[maxsize+1]; /*datatype为元素的数据类 型,它可是Turbo C中*/ /*允许的任何数据类型*/ int last; /*记录当前表中元素的个数*/ } Sqlist; 在C语言中,可以用一维数组来描述向量。 # define maxsize N; /*设置线性表的最大长度为N, N为整数*/ typedef struct {datatype data[maxsize+1]; /*datatype为元素的数据类 型,它可是Turbo C中*/ /*允许的任何数据类型*/ int last; /*记录当前表中元素的个数*/ } Sqlist; 数据结构(C语言版) 上述描述方法,将线性表顺序存储结构中的信息封装隐 藏在类型Sqlist结构中。data数组描述了线性表中数据元素占用 的空间,数组中第i个分量就是线性表中第i个数据元素。last描 述了当前表中数据元素的个数即表长。 说明:在C语言中,数组的下标是从0开始的,但为了算 法描述方便,本书中凡涉及数组的算法,规定下标从1开始, 这样,读者可不必考虑下标为0的数组元素。 上述描述方法,将线性表顺序存储结构中的信息封装隐 藏在类型Sqlist结构中。data数组描述了线性表中数据元素占用 的空间,数组中第i个分量就是线性表中第i个数据元素。last描 述了当前表中数据元素的个数即表长。 说明:在C语言中,数组的下标是从0开始的,但为了算 法描述方便,本书中凡涉及数组的算法,规定下标从1开始, 这样,读者可不必考虑下标为0的数组元素。 数据结构(C语言版) 2.3.2 向量中基本运算的实现 1. 定位操作Locate (L, x) 定位操作返回线性表L中值和x相同的第一个元素的位置。 算法如下: /*算法描述2.1*/ Int Locate (Sqlist L, Datatype x) {int i=1; while ((iL.last ) printf ("infeasible position! \n"); else { if ( L.last+1>maxsize ) printf("overflow!\n"); else {for (j=L.last; j>=i; j--) L.data[j+1]=L.data[j]; L.data[i]=x; L.last++; } } } /*算法描述2.2*/ void Insert (Sqlist L, int i, Datatype x ) {int j; if (i<1 || i >L.last ) printf ("infeasible position! \n"); else { if ( L.last+1>maxsize ) printf("overflow!\n"); else {for (j=L.last; j>=i; j--) L.data[j+1]=L.data[j]; L.data[i]=x; L.last++; } } } 数据结构(C语言版) 3. 向量的删除运算 Delete (L, x) 与向量的插入运算道理相同,当删除线性表中第i 个元素时, 也改变了原数据间的逻辑关系,故需将第i+1(iL.last ) printf ("infeasible \n"); else {for (j=i; jL.last ) printf ("infeasible \n"); else {for (j=i; jnext=NULL。 一个单向链表对应一个头指针head,head是一个LList类型 的变量,即它是一个指向Node类型结点的指针变量,并指向单 向链表的第1个结点,通过它可以访问该单向链表。若头指针 为“空”(即head=NULL),则表示一个空表。 一般在单向链表中附加一个头结点,其指针域指向链表的 第一个结点,而其数据域可以存储一些如链表长度之类的附加 信息,也可以什么都不存储。这样,链表的头指针将指向头结 点。如图2.4所示,表空的条件是头结点的指针域为“空”,即 head->next=NULL。 数据结构(C语言版) 4 ABCD head head D 图2.4 带表头的单链表 4 ABCD head head D 数据结构(C语言版) 2.4.2 单向链表基本运算的实现 1. 取单链表中元素Get (L , i) 该函数返回线性表L中第 i个数据元素的值。算法思路:从头 指针出发,借用指针p,从第1个结点开始,顺着后继指针向后寻 找第i个元素。若存在第i 个元素,即1≤i<Length(L),则通过p返 回该元素的值。 2.4.2 单向链表基本运算的实现 1. 取单链表中元素Get (L , i) 该函数返回线性表L中第 i个数据元素的值。算法思路:从头 指针出发,借用指针p,从第1个结点开始,顺着后继指针向后寻 找第i个元素。若存在第i 个元素,即1≤i<Length(L),则通过p返 回该元素的值。 数据结构(C语言版) /*算法描述2.4*/ Datatype GetLList(LList L, int i) { LList p; int j =1; p=L->next; while( p!=NULL && jnext; j++; } if (p= = NULL || j>i ) printf("no this data!\n"); else return(p->data); } /*算法描述2.4*/ Datatype GetLList(LList L, int i) { LList p; int j =1; p=L->next; while( p!=NULL && jnext; j++; } if (p= = NULL || j>i ) printf("no this data!\n"); else return(p->data); } 该算法的基本操作是比较j和i 并后移指针,若第i 个元素存 在,则需执行基本操作i-1次,否则执行n次,故算法2.4的时 间复杂度均为O(n)。 数据结构(C语言版) 2. 定位函数Locate(L, x) 该函数在线性链表中寻找值与x相等的数据元素,若有,则 返回其存储位置,否则返回NULL。其算法2.5思路与算法2.4相 似, 其时间复杂度均也为O(n)。 /*算法描述2.5*/ LList Locate (LList L, Datatype x) {LList p; p=L->next; while (p!=NULL && p->data!=x ) p=p->next; return (p); } 2. 定位函数Locate(L, x) 该函数在线性链表中寻找值与x相等的数据元素,若有,则 返回其存储位置,否则返回NULL。其算法2.5思路与算法2.4相 似, 其时间复杂度均也为O(n)。 /*算法描述2.5*/ LList Locate (LList L, Datatype x) {LList p; p=L->next; while (p!=NULL && p->data!=x ) p=p->next; return (p); } 数据结构(C语言版) 3. 单链表的插入Insert (L, i,x) 该函数在线性链表第i个元素之前插入一个数据元素x。算法 思路:先生成一个包含数据元素x的新结点(用s指向它),再找到 链表中第i-1个结点(用p指向它),修改这两个结点的指针即可。 指针修改如图2.5所示,用语句描述为: s->next=p->next; p->next=s; 注意:修改指针的顺序,若先修改第i-1个结点的指针,使 其指向待插结点,那么,第i个结点的地址将丢失,链表“断开”, 待插结点将无法与第i个结点链接。 3. 单链表的插入Insert (L, i,x) 该函数在线性链表第i个元素之前插入一个数据元素x。算法 思路:先生成一个包含数据元素x的新结点(用s指向它),再找到 链表中第i-1个结点(用p指向它),修改这两个结点的指针即可。 指针修改如图2.5所示,用语句描述为: s->next=p->next; p->next=s; 注意:修改指针的顺序,若先修改第i-1个结点的指针,使 其指向待插结点,那么,第i个结点的地址将丢失,链表“断开”, 待插结点将无法与第i个结点链接。 数据结构(C语言版) 4 ABCD head p (a) 4 ABCD head p x s (b) 图2.5 单向链表中插入结点时指针的变化情况 (a) 插入前; (b) 插入后 4 ABCD head p (a) 4 ABCD head p x s (b) 数据结构(C语言版) /*算法描述2.6*/ void InsetLList(LList L, int i, Datatype x) {LList p, s; int j=0; p= L; while (p!=NULL && jnext; j++; } if (p= = NULL || j>i-1) printf("No this position!\n"); else {s=(LList) malloc (sizeof (Node) ); s->data=x; s->next=p->next; p->next=s;} } /*算法描述2.6*/ void InsetLList(LList L, int i, Datatype x) {LList p, s; int j=0; p= L; while (p!=NULL && jnext; j++; } if (p= = NULL || j>i-1) printf("No this position!\n"); else {s=(LList) malloc (sizeof (Node) ); s->data=x; s->next=p->next; p->next=s;} } 数据结构(C语言版) 4. 单链表的删除Delete( L, i) 该函数删除线性链表中第i个数据结点。显然,只要找到第i -1个结点修改其指针使它跳过第i个结点,而直接指向第i+1个 结点即可。但要注意,删除的结点应及时向系统释放,以便系 统再次利用。指针变化如图2.6所示,语句描述为 p->next=p->next->next ; 4. 单链表的删除Delete( L, i) 该函数删除线性链表中第i个数据结点。显然,只要找到第i -1个结点修改其指针使它跳过第i个结点,而直接指向第i+1个 结点即可。但要注意,删除的结点应及时向系统释放,以便系 统再次利用。指针变化如图2.6所示,语句描述为 p->next=p->next->next ; 数据结构(C语言版) 4 ABCD head 图2.6 单向链表中删除结点时指针的变化情况 数据结构(C语言版) 其具体算法描述如下: /*算法描述2.7*/ void Delete (LList L, int i) {LList p, q; int j=0; p=L; while ( p!=NULL && jnext; j++; } if ( p= = NULL || j>i-1) printf(" No this data!\n"); else { q=p->next; p->next =p->next->next; free (q); } } 其具体算法描述如下: /*算法描述2.7*/ void Delete (LList L, int i) {LList p, q; int j=0; p=L; while ( p!=NULL && jnext; j++; } if ( p= = NULL || j>i-1) printf(" No this data!\n"); else { q=p->next; p->next =p->next->next; free (q); } } 数据结构(C语言版) 由于在单向链表中插入和删除结点时,仅需修改相应结点 的指针,而不需移动元素,该程序的执行时间主要耗费在查找 结点上,由算法2.4知访问结点的时间复杂度为O(n), 所以算法 2.6和算法2.7的时间复杂度均为O(n)。 数据结构(C语言版) 5. 单链表的建立Crt-LList(L,n) 建立线性表的链式存储结构的过程就是一个动态生成链表 的过程,即从“空表”的初始状态起,依次建立各元素结点, 并逐个插入链表。下面是一个从表尾到表头建立单链表的算法, 其时间复杂度是O(n)。 5. 单链表的建立Crt-LList(L,n) 建立线性表的链式存储结构的过程就是一个动态生成链表 的过程,即从“空表”的初始状态起,依次建立各元素结点, 并逐个插入链表。下面是一个从表尾到表头建立单链表的算法, 其时间复杂度是O(n)。 数据结构(C语言版) /*算法描述2.8*/ void Crt-LList(LList h, int n) {LList p,q; int i; h=(LList)malloc(sizeof (Node) ); h->next=NULL; p=h; for(i=1;i<=n;i++) {q=(LList)malloc(sizeof(Node)); scanf("%&",&q->data); q->next=NULL; p->next=q; p=q;} } /*算法描述2.8*/ void Crt-LList(LList h, int n) {LList p,q; int i; h=(LList)malloc(sizeof (Node) ); h->next=NULL; p=h; for(i=1;i<=n;i++) {q=(LList)malloc(sizeof(Node)); scanf("%&",&q->data); q->next=NULL; p->next=q; p=q;} } 数据结构(C语言版) 说明:上面算法中分别引用了Turbo C 语言的两个标准函 数malloc()和free()。设p为LList 型变量,则执行 p=(LList)malloc(sizeof (Node)) 的作用是向系统申请一个Node型 的结点,同时让p指向该结点;执行free(p)的作用是向系统释放 一个由p所指的Node型的结点,已释放的空间可供系统再次使 用。 说明:上面算法中分别引用了Turbo C 语言的两个标准函 数malloc()和free()。设p为LList 型变量,则执行 p=(LList)malloc(sizeof (Node)) 的作用是向系统申请一个Node型 的结点,同时让p指向该结点;执行free(p)的作用是向系统释放 一个由p所指的Node型的结点,已释放的空间可供系统再次使 用。 数据结构(C语言版) 2.5 循环链表和双向链表 2.5.1 循环链表 循环链表是另一种形式的链式存储结构。其特点是表中最 后一个结点的指针域指向头结点,整个链表呈环状。从表中任 意结点出发都可到达其他结点,如图2.7所示为单循环链表。 2.5.1 循环链表 循环链表是另一种形式的链式存储结构。其特点是表中最 后一个结点的指针域指向头结点,整个链表呈环状。从表中任 意结点出发都可到达其他结点,如图2.7所示为单循环链表。 图2.7 单循环链表 (a) 非空表;(b) 空表 BCD head head (a) (b) 数据结构(C语言版) 循环链表和单链表算法实现基本相同,差别仅在于前者算 法中的循环条件是判p或p->next是否为空,而后者是判它们是 否等于头指针。有时为了简化某些操作在链表中设立尾指针, 而不是头指针。例如,将两个用循环链表存储的线性表合并成 一个线性表,此时仅需将一个表的表尾和另一个表的表头相连 即可。指针变化如图2.8所示,用语句描述为: p=A->next; A->next =B->next->next ; B->next=p; 操作只改变了两个指针值,其算法的时间复杂度均为O(1)。 循环链表和单链表算法实现基本相同,差别仅在于前者算 法中的循环条件是判p或p->next是否为空,而后者是判它们是 否等于头指针。有时为了简化某些操作在链表中设立尾指针, 而不是头指针。例如,将两个用循环链表存储的线性表合并成 一个线性表,此时仅需将一个表的表尾和另一个表的表头相连 即可。指针变化如图2.8所示,用语句描述为: p=A->next; A->next =B->next->next ; B->next=p; 操作只改变了两个指针值,其算法的时间复杂度均为O(1)。 数据结构(C语言版) A BB A (a) (b) … … … … 图2.8 循环链表合并示意图 (a) 合并前;(b) 合并后 A BB A (a) (b) … … … … 数据结构(C语言版) 2.5.2 双向链表 单向链表的结点只有一个指示其直接后继的指针域, 顺着 某结点的指针可很容易地访问其后诸结点。但若要访问某结点 的直接前驱,前驱虽与该结点相邻却无法直达,此时需从表头 出发,且寻访时要记录相关信息。为克服单向链表这种访问方 式的单向性,特设计了双向链表, 如图2.9(b)所示。 显然,在双向链表的结点中应有两个指针域,一个指向直 接后继,一个指向直接前驱,如图2.9(a)所示。双向链表在 Turbo C语言中可描述如下: 2.5.2 双向链表 单向链表的结点只有一个指示其直接后继的指针域, 顺着 某结点的指针可很容易地访问其后诸结点。但若要访问某结点 的直接前驱,前驱虽与该结点相邻却无法直达,此时需从表头 出发,且寻访时要记录相关信息。为克服单向链表这种访问方 式的单向性,特设计了双向链表, 如图2.9(b)所示。 显然,在双向链表的结点中应有两个指针域,一个指向直 接后继,一个指向直接前驱,如图2.9(a)所示。双向链表在 Turbo C语言中可描述如下: 数据结构(C语言版) typedef struct dnode {datatype data; struct dnode *prior; struct dnode *next; }DNode, *DList; typedef struct dnode {datatype data; struct dnode *prior; struct dnode *next; }DNode, *DList; 数据结构(C语言版) prior data next A head BC (a) (b) 图2.9 双向链表示例 (a) 结点结构;(b) 双向链表 数据结构(C语言版) head A head BC (a) (b) 图2.10 双向循环链表示例 (a) 空表;(b) 非空表 数据结构(C语言版) 在双向链表中,Length(L),Get(L,i),Locate(L,x)等操作仅 涉及一个方向的指针,其算法描述与单链表相同。但插入和删 除操作有所不同,在双向链表中需同时修改两个方向的指针, 图2.11和图2.12分别显示了删除和插入结点时指针的修改情况, 其具体算法读者可自己完成。 在双向链表中,Length(L),Get(L,i),Locate(L,x)等操作仅 涉及一个方向的指针,其算法描述与单链表相同。但插入和删 除操作有所不同,在双向链表中需同时修改两个方向的指针, 图2.11和图2.12分别显示了删除和插入结点时指针的修改情况, 其具体算法读者可自己完成。 数据结构(C语言版) ABC p 图2.11 双向链表中删除结点时指针的修改情况 ABC p 数据结构(C语言版) A B C p 图2.12 双向链表中插入结点时指针的修改情况 A B C p 数据结构(C语言版) 在双向链表中删除结点时指针的变化用语句描述为: p->prior->next=p->next; p->next->prior=p->prior; free(p); 在双向链表中插入结点时指针的变化用语句描述为: s->prior=p->prior; p->prior->next=s; s->next=p; p->prior=s; 在双向链表中删除结点时指针的变化用语句描述为: p->prior->next=p->next; p->next->prior=p->prior; free(p); 在双向链表中插入结点时指针的变化用语句描述为: s->prior=p->prior; p->prior->next=s; s->next=p; p->prior=s; 数据结构(C语言版) 2.5.3 线性表的顺序存储结构和链式存储结构的比较 在计算机中,线性表有两类不同的存储结构:顺序存储结 构和链式存储结构,它们各有特点。顺序表的特点是逻辑上相 邻的结点在存储结构中也相邻,它是一种可随机存取存储结构, 在C语言中,用一维数组来描述,有以下三方面的缺点: (1) 在插入和删除结点时,需移动大量元素; (2) 在对长度较大的线性表预先分配空间时,必须按最大空 间分配,从而使存储空间得不到充分利用; 2.5.3 线性表的顺序存储结构和链式存储结构的比较 在计算机中,线性表有两类不同的存储结构:顺序存储结 构和链式存储结构,它们各有特点。顺序表的特点是逻辑上相 邻的结点在存储结构中也相邻,它是一种可随机存取存储结构, 在C语言中,用一维数组来描述,有以下三方面的缺点: (1) 在插入和删除结点时,需移动大量元素; (2) 在对长度较大的线性表预先分配空间时,必须按最大空 间分配,从而使存储空间得不到充分利用; 数据结构(C语言版) (3) 表的容量难以扩充。 链式存储结构的特点是逻辑上相邻的数据元素其物理位置 不要求紧邻,它是一种非随机存取存储结构,在C语言中用 “结点指针”来描述。它克服了顺序表上述的三个缺点,但却 不具备像顺序表那样随机存取的优点。 在实践中应仔细分析,根据不同研究对象的特点和经常进 行的操作选择合适的存储结构。 (3) 表的容量难以扩充。 链式存储结构的特点是逻辑上相邻的数据元素其物理位置 不要求紧邻,它是一种非随机存取存储结构,在C语言中用 “结点指针”来描述。它克服了顺序表上述的三个缺点,但却 不具备像顺序表那样随机存取的优点。 在实践中应仔细分析,根据不同研究对象的特点和经常进 行的操作选择合适的存储结构。 数据结构(C语言版) 2.6 实习:线性表的应用实例 实例1:利用顺序表实现例2.1的完整C语言程序。 # def maxsize 250 typedef struct { struct student {int num; char *name; char gender; float score; } data[maxsize+1]; int last; } Sqlist; 实例1:利用顺序表实现例2.1的完整C语言程序。 # def maxsize 250 typedef struct { struct student {int num; char *name; char gender; float score; } data[maxsize+1]; int last; } Sqlist; 数据结构(C语言版) # include "stdio.h" void Initiate(L) Sqlist L; {L.last=0;} int Locate ( L, x) Sqlist L; int x ; {int i=1; while (ix) i++; if (i<=L.last) return (i); else return (0 ); } # include "stdio.h" void Initiate(L) Sqlist L; {L.last=0;} int Locate ( L, x) Sqlist L; int x ; {int i=1; while (ix) i++; if (i<=L.last) return (i); else return (0 ); } 数据结构(C语言版) void sort(L) Sqlist L; {int i, j; student x; for(i=2; i-1&&i<=maxsize) {scanf("name=%s,sex=%c,score=%f\n",&L.data[i].name,&L.data[i]. sex,&L.data[i].score); i++; scanf("num=%d", &Ldata[i].num);} Sort(L); for(i=1; i<=L.last;i++) printf("%d %s %c %f\n", L.data[i].num, L.data[i].name, L.data[i].sex, L.data[i].score); printf("input data, please!( "-1"-------------end)\n"); scanf("num=%d", &L.data[i].num); While(L.data[i].num<>-1&&i<=maxsize) {scanf("name=%s,sex=%c,score=%f\n",&L.data[i].name,&L.data[i]. sex,&L.data[i].score); i++; scanf("num=%d", &Ldata[i].num);} Sort(L); for(i=1; i<=L.last;i++) printf("%d %s %c %f\n", L.data[i].num, L.data[i].name, L.data[i].sex, L.data[i].score); 数据结构(C语言版) printf("do you want to find a student?(y\n) "); while(getchar()=='y') {printf(" input the number of the student ! "); scanf("%d", &num); i=Locate(L, num); printf("%d, %s %c %f\n", L.data[i].num, L.data[i].name, L.data[i].sex, L.data[i].score);} } printf("do you want to find a student?(y\n) "); while(getchar()=='y') {printf(" input the number of the student ! "); scanf("%d", &num); i=Locate(L, num); printf("%d, %s %c %f\n", L.data[i].num, L.data[i].name, L.data[i].sex, L.data[i].score);} } 数据结构(C语言版) 实例2:多项式相加问题。 1) 存储结构的选取 任一一元多项式可表示为Pn(x)=P0+P1x+P2x2+...+Pnxn,显然, 由其n+1个系数可惟一确定该多项式。故一元多项式可用一个仅 存储其系数的线性表来表示,多项式指数i隐含于Pi的序号中。 P=( P0,P1,P2,...,Pn) 若采用顺序存储结构来存储这个线性表,那么多项式相加 的算法实现十分容易,同位序元素相加即可。 实例2:多项式相加问题。 1) 存储结构的选取 任一一元多项式可表示为Pn(x)=P0+P1x+P2x2+...+Pnxn,显然, 由其n+1个系数可惟一确定该多项式。故一元多项式可用一个仅 存储其系数的线性表来表示,多项式指数i隐含于Pi的序号中。 P=( P0,P1,P2,...,Pn) 若采用顺序存储结构来存储这个线性表,那么多项式相加 的算法实现十分容易,同位序元素相加即可。 数据结构(C语言版) 但当多项式的次数很高而且变化很大时,采用这种顺序 存储结构极不合理。例如,多项式S(x)= 1+ 3x+12x999需用一长 度为1000的线性表来表示,而表中仅有三个非零元素,这样将 大量浪费内存空间。此时可考虑另一种表示方法,如线性表 S(x)可表示成S=((1,0),(3,1), (12,999)),其元素包含两个数据 项:系数项和指数项。 这种表示方法在计算机内对应两种存储方式:当只对多项 式进行访问、求值等不改变多项式指数(即表的长度不变化)的 操作时,宜采用顺序存储结构;当要对多项式进行加法、减法、 乘法等改变多项式指数的操作时,宜采用链式存储结构。 但当多项式的次数很高而且变化很大时,采用这种顺序 存储结构极不合理。例如,多项式S(x)= 1+ 3x+12x999需用一长 度为1000的线性表来表示,而表中仅有三个非零元素,这样将 大量浪费内存空间。此时可考虑另一种表示方法,如线性表 S(x)可表示成S=((1,0),(3,1), (12,999)),其元素包含两个数据 项:系数项和指数项。 这种表示方法在计算机内对应两种存储方式:当只对多项 式进行访问、求值等不改变多项式指数(即表的长度不变化)的 操作时,宜采用顺序存储结构;当要对多项式进行加法、减法、 乘法等改变多项式指数的操作时,宜采用链式存储结构。 数据结构(C语言版) 2) 一元多项加法运算的实现 采用单链表结构来实现多项加法运算,无非是前述单向链表 基本运算的综合应用。其数据结构描述如下, typedef stuct Pnode {float coef; int exp; struct pnode *next; }Pnode, *Ploytp; 图2.13给出了多项式A(x) = 15+ 6x+ 9x7+3x18 和B(x)= 4x+5x6+ 16x7的链式存储结构(设一元多项式均按升幂形式存储, 首指针为-1)。 2) 一元多项加法运算的实现 采用单链表结构来实现多项加法运算,无非是前述单向链表 基本运算的综合应用。其数据结构描述如下, typedef stuct Pnode {float coef; int exp; struct pnode *next; }Pnode, *Ploytp; 图2.13给出了多项式A(x) = 15+ 6x+ 9x7+3x18 和B(x)= 4x+5x6+ 16x7的链式存储结构(设一元多项式均按升幂形式存储, 首指针为-1)。 数据结构(C语言版) - 1 015 16 79 183A(x) - 1 14 65 716B(x) 图2.13 一元多项式的存储 数据结构(C语言版) 若上例A+B结果仍存于A中,根据一元多项式相加的运算规 则,其实质是将B逐项按指数分情况合并于“和多项式”A中。 设p, q分别指向A, B的第一个结点,如图2.14所示,其算法思路 如下: (1) p->expexp, 应使指针后移p=p->next,如图2.14(a)所 示。 (2) p->exp=q->exp, 将两个结点系数相加,若系数和不为零, 则修改p->ceof,并借助s释放当前q结点, 而使q指向多项式B的下 一个结点,如图2.14(b)所示;若系数和为零,则应借助s释放p, q 结点, 而使p, q分别指向多项式A,B的下一个结点。 若上例A+B结果仍存于A中,根据一元多项式相加的运算规 则,其实质是将B逐项按指数分情况合并于“和多项式”A中。 设p, q分别指向A, B的第一个结点,如图2.14所示,其算法思路 如下: (1) p->expexp, 应使指针后移p=p->next,如图2.14(a)所 示。 (2) p->exp=q->exp, 将两个结点系数相加,若系数和不为零, 则修改p->ceof,并借助s释放当前q结点, 而使q指向多项式B的下 一个结点,如图2.14(b)所示;若系数和为零,则应借助s释放p, q 结点, 而使p, q分别指向多项式A,B的下一个结点。 数据结构(C语言版) (3) p->exp > q->exp,将q结点在p结点之前插入A中, 并使q 指向多项式B的下一个结点,如图2.14(c)所示。 直到q=NULL为止或p=NULL,将B的剩余项链到A尾为止。 最后释放B的头结点。 数据结构(C语言版) - 1 015 16 79 183A - 1 14 65 716B C p p p (a) - 1 015 16 79 183A - 1 14 65 716B C p s (b) q q - 1 015 16 79 183A C - 1 14 65 716B q (c) 图 2. 14 多项式相加运算示例 - 1 015 16 79 183A - 1 14 65 716B C p p p (a) - 1 015 16 79 183A - 1 14 65 716B C p s (b) q q - 1 015 16 79 183A C - 1 14 65 716B q (c) 图 2. 14 多项式相加运算示例 数据结构(C语言版)下面给出从接收多项式到完成多项式相加运算的完整C语言程序。 # include "stdio.h" void Crt-Polytp (h, n) Polytp h; int n; { Polytp,q; int i; h=( Polytp)malloc(sizeof (Pnode) ); h->next=NULL; p=h; for(i=1;i<=n;i++) {q=( Polytp)malloc(sizeof(Pnode)); scanf("%f,%d",&q->ceof,&q->exp); q->next=NULL; p->next=q; p=q;} } 下面给出从接收多项式到完成多项式相加运算的完整C语言程序。 # include "stdio.h" void Crt-Polytp (h, n) Polytp h; int n; { Polytp,q; int i; h=( Polytp)malloc(sizeof (Pnode) ); h->next=NULL; p=h; for(i=1;i<=n;i++) {q=( Polytp)malloc(sizeof(Pnode)); scanf("%f,%d",&q->ceof,&q->exp); q->next=NULL; p->next=q; p=q;} } 数据结构(C语言版)int Cmp(a,b) float a,b; { if(ab)return(1); } void Add Poly(pa, pb,pc) Polytp pa,pb,pc; { Polytp p,q,pre,s; p=pa->next; q=pb->next; pre=pa; pc=pa; while(p!=NULL & q<> NULL) int Cmp(a,b) float a,b; { if(ab)return(1); } void Add Poly(pa, pb,pc) Polytp pa,pb,pc; { Polytp p,q,pre,s; p=pa->next; q=pb->next; pre=pa; pc=pa; while(p!=NULL & q<> NULL) 数据结构(C语言版) {w=cmp(p->exp, q->exp); switch(w) {case -1: pre=p; p=p->next; break; case 0: sum=p->coef+q->coef; if (sum<>0) {p->coef=sum; pre=p; } else {pre ->next=p->next; free(p); } p=pre->next; s=q; q=q->next; free(s); break; case 1: s=q->next; q->next=p; pre->next=q; pre=q; q=s; break; } } {w=cmp(p->exp, q->exp); switch(w) {case -1: pre=p; p=p->next; break; case 0: sum=p->coef+q->coef; if (sum<>0) {p->coef=sum; pre=p; } else {pre ->next=p->next; free(p); } p=pre->next; s=q; q=q->next; free(s); break; case 1: s=q->next; q->next=p; pre->next=q; pre=q; q=s; break; } } 数据结构(C语言版) if (pb<>NULL) pre->next=q; free(pb); } main() {Ploytp pa,pb.pc,q; int n1 , n2; printf(" input the length of pa and pb"); scanf("n1= %d, n2=%d", &n1, &n2); if (pb<>NULL) pre->next=q; free(pb); } main() {Ploytp pa,pb.pc,q; int n1 , n2; printf(" input the length of pa and pb"); scanf("n1= %d, n2=%d", &n1, &n2); 数据结构(C语言版) Crt-Polytp (pa,n1); Crt-Polytp (pb,n2); AddPolytp(pa,pb,pc); p=pc->next; printf("pc=pa+pb="); while(p<>NULL) {printf(" %f,%d", p->ceof, p->exp); p=p->next;} } Crt-Polytp (pa,n1); Crt-Polytp (pb,n2); AddPolytp(pa,pb,pc); p=pc->next; printf("pc=pa+pb="); while(p<>NULL) {printf(" %f,%d", p->ceof, p->exp); p=p->next;} } 数据结构(C语言版) 习 题 2 1.判断下列概念的正确性: (1) 线性表在物理存储空间中也一定是连续的。 (2) 链表的物理存储结构具有与链表一样的顺序。 (3) 链表的删除算法很简单,因为当删去链表中某个结点后, 计算机会自动地将后继的各个单元向前移动。 2.试比较顺序存储结构和链式存储结构的优缺点。 1.判断下列概念的正确性: (1) 线性表在物理存储空间中也一定是连续的。 (2) 链表的物理存储结构具有与链表一样的顺序。 (3) 链表的删除算法很简单,因为当删去链表中某个结点后, 计算机会自动地将后继的各个单元向前移动。 2.试比较顺序存储结构和链式存储结构的优缺点。 数据结构(C语言版) 3.试写出一个计算链表中数据元素结点个数的算法,其 中指针p指向该链表的第一个结点。 4.试设计实现在单链表中删去值相同的多余结点的算法。 5.有一个性表(a1,a2,…,an),它存储在有附加表头结 点的单链表中,写一个算法,求出该线性表中值为x的元素的 序号。如果x不存在,则输出序号为0。 6. 写一个算法将一单链表逆置。要求操作在原链表上进 行。 3.试写出一个计算链表中数据元素结点个数的算法,其 中指针p指向该链表的第一个结点。 4.试设计实现在单链表中删去值相同的多余结点的算法。 5.有一个性表(a1,a2,…,an),它存储在有附加表头结 点的单链表中,写一个算法,求出该线性表中值为x的元素的 序号。如果x不存在,则输出序号为0。 6. 写一个算法将一单链表逆置。要求操作在原链表上进 行。 数据结构(C语言版) 7.设有两个链表A、B,它们的数据元素分别为(x1,x2,…, xm)和(y1,y2,…,yn)。写一个算法将它们合并为一个线性表C, 使得: 当m≥n时,C=xl,y1,x2,y2,…,xn,yn,…,xm; 当m > < < < > > - > > < < < > > * > > > > < > > 表3.1 算符优先级表 * > > > > < > > / > > > > < > > ( < < < < < = ) > > > > > > @ < < < < < = 数据结构(C语言版) 表中空白表示运算符p1,p2不可能相遇的情况,若相遇则 表明出现了语法错误。“#”是表达式的结束符,为算法方便在 表达式的左边也虚设一个“#”使其配对。这样,当“(”=“)” 时表示左右括号相遇,括号内运算已经结束;同理,当 “#”=“#”时表示整个表达式求值完毕。 表中空白表示运算符p1,p2不可能相遇的情况,若相遇则 表明出现了语法错误。“#”是表达式的结束符,为算法方便在 表达式的左边也虚设一个“#”使其配对。这样,当“(”=“)” 时表示左右括号相遇,括号内运算已经结束;同理,当 “#”=“#”时表示整个表达式求值完毕。 数据结构(C语言版) 3.算法思路 使用两个栈S1和S2, 其中,S1为运算符栈,用以寄存运算 符,而S2为操作数栈,用以寄存操作数或运算结果。其算法思 路如下, 1)首先设置两栈为空,将“#”作为表达式起始符压入运算 符栈S1作为栈底元素; 3.算法思路 使用两个栈S1和S2, 其中,S1为运算符栈,用以寄存运算 符,而S2为操作数栈,用以寄存操作数或运算结果。其算法思 路如下, 1)首先设置两栈为空,将“#”作为表达式起始符压入运算 符栈S1作为栈底元素; 数据结构(C语言版) 2)依次读入表达式的每个字符,若是操作数则进入操作 数栈S2;若是运算符,则与S1的栈顶运算符比较优先级,若栈 顶运算符优先级低,则进入栈S1,若栈顶运算符优先级高,则 弹出S1的栈顶运算符,并从栈S2中弹出两个操作数,作相应运 算后,将结果压入操作数栈S2,然后再次与S1的栈顶运算符比 较优先级,直至栈顶运算符优先级低为止 3)当S1的栈顶运算符为“#”时,表达式求值结束,操作 数栈S2中的数即为表达式的值。 2)依次读入表达式的每个字符,若是操作数则进入操作 数栈S2;若是运算符,则与S1的栈顶运算符比较优先级,若栈 顶运算符优先级低,则进入栈S1,若栈顶运算符优先级高,则 弹出S1的栈顶运算符,并从栈S2中弹出两个操作数,作相应运 算后,将结果压入操作数栈S2,然后再次与S1的栈顶运算符比 较优先级,直至栈顶运算符优先级低为止 3)当S1的栈顶运算符为“#”时,表达式求值结束,操作 数栈S2中的数即为表达式的值。 数据结构(C语言版) 表3.2求表达式3*(7-2)时的栈变化 数据结构(C语言版) 4.完整的C语言程序 file1 #define maxsize 30 typedef struct {char data[maxsize+1]; int top; } Stack; int Push(S,x) Stack S; Char x; {if (S.top==maxsize) {printf("overflow\n"); return(0);} S.data[++S.top]=x; return(1); } 4.完整的C语言程序 file1 #define maxsize 30 typedef struct {char data[maxsize+1]; int top; } Stack; int Push(S,x) Stack S; Char x; {if (S.top==maxsize) {printf("overflow\n"); return(0);} S.data[++S.top]=x; return(1); } 数据结构(C语言版) int Pop(s,x) Stack S; Char x; {if(S.top==0){printf("nudertflow\n");return(0)}; x=S.data[S.top]; S.top--; return(1); } file2 char readtop(S) Stack S; {char a; int Pop(s,x) Stack S; Char x; {if(S.top==0){printf("nudertflow\n");return(0)}; x=S.data[S.top]; S.top--; return(1); } file2 char readtop(S) Stack S; {char a; 数据结构(C语言版) Pop(S, a); Push(S, a); Return( a); } double operate(ch, x, y) char ch; double x,y; {double z; switch (ch) {case '+' : z=x+y; break; case '-' : z=x-y; break; case '*' : z=x*y; break; Pop(S, a); Push(S, a); Return( a); } double operate(ch, x, y) char ch; double x,y; {double z; switch (ch) {case '+' : z=x+y; break; case '-' : z=x-y; break; case '*' : z=x*y; break; 数据结构(C语言版) case '/' : z=x/y; break;} return(z);} int precede(p1,p2) { int flag; switch (p1) {case '+' : if(p2=='*' || p2=='/' || p2== '(' ) flag=-1; else flag=1; break; case '-' : if(p2=='*' || p2=='/' || p2== '(' ) flag=-1; else flag=1; break; case '*' : if(p2=='(' ) flag=-1; else flag=1; break; case '/' : z=x/y; break;} return(z);} int precede(p1,p2) { int flag; switch (p1) {case '+' : if(p2=='*' || p2=='/' || p2== '(' ) flag=-1; else flag=1; break; case '-' : if(p2=='*' || p2=='/' || p2== '(' ) flag=-1; else flag=1; break; case '*' : if(p2=='(' ) flag=-1; else flag=1; break; 数据结构(C语言版) case '/' : if(p2=='(' ) flag=-1; else flag=1; break; case '(' : if(p2==')') flag=0; else if(p2=='#')printf("error operator!\n"); else flag=-1; break; case ')' : if(p2=='(' )printf("error operator!\n"); else flag=1; break; case '/' : if(p2=='(' ) flag=-1; else flag=1; break; case '(' : if(p2==')') flag=0; else if(p2=='#')printf("error operator!\n"); else flag=-1; break; case ')' : if(p2=='(' )printf("error operator!\n"); else flag=1; break; 数据结构(C语言版) case '# ' : if(p2==')' )printf("error operator!\n"); else if(p2=='#' ) flag=0; else flag=-1; break;'} return(flag);} double calcul( a); char a[]; {Stack S1, S2; double x, y, z; char r, ch; int I; case '# ' : if(p2==')' )printf("error operator!\n"); else if(p2=='#' ) flag=0; else flag=-1; break;'} return(flag);} double calcul( a); char a[]; {Stack S1, S2; double x, y, z; char r, ch; int I; 数据结构(C语言版) push(S1,'#'); r=a[I]; while(r<>'#' || readtop(S1)<>'#') {if(r<='9' && r>='0'){ x=0; while(r<='9' && r>='0') {x=x*10+r-'0'; r=a[++I];} push(S2,x);} else switch(precede(S1,r)) push(S1,'#'); r=a[I]; while(r<>'#' || readtop(S1)<>'#') {if(r<='9' && r>='0'){ x=0; while(r<='9' && r>='0') {x=x*10+r-'0'; r=a[++I];} push(S2,x);} else switch(precede(S1,r)) 数据结构(C语言版) {case -1: push(S1,r); r=a[++I]; break; case 0: pop(S1,ch); r=a[++I]; break; case 1: pop(S1, ch); pop(S2, x1); pop(S2, x2); push(S2, operate(ch, x1.x2)); r=a[++I]; break;}} return(read(S2)); } {case -1: push(S1,r); r=a[++I]; break; case 0: pop(S1,ch); r=a[++I]; break; case 1: pop(S1, ch); pop(S2, x1); pop(S2, x2); push(S2, operate(ch, x1.x2)); r=a[++I]; break;}} return(read(S2)); } 数据结构(C语言版) 习题3 1.假定有编号为A、B、C、D的4辆列车,顺序开进一个栈 式结构的站台,请写出开出车站站台的列车顺序(注:每一列车 由站台开出时均可进栈,出栈开出站台,但不允许出栈后回退)。 写出每一种可能的序列。 2.已知堆栈采用链式存储结构,初始时为空,试画出a、b、 c、d 4个元素依次以后堆栈的状态,然后再画出此时的栈顶元素 出栈后的状态。 3.写出链栈的取栈顶元素和置栈空的算法。 1.假定有编号为A、B、C、D的4辆列车,顺序开进一个栈 式结构的站台,请写出开出车站站台的列车顺序(注:每一列车 由站台开出时均可进栈,出栈开出站台,但不允许出栈后回退)。 写出每一种可能的序列。 2.已知堆栈采用链式存储结构,初始时为空,试画出a、b、 c、d 4个元素依次以后堆栈的状态,然后再画出此时的栈顶元素 出栈后的状态。 3.写出链栈的取栈顶元素和置栈空的算法。 数据结构(C语言版) 4.写出多个链表栈中取第j个链表栈顶元素值的算法。 5.写出计算表达式3+4/25,8—6时操作数栈和运算符栈 的变化情况。 6.课文中规定:无论是循环队列还是链表队列,队头指 针总是指向队头元素的前一位置,队尾指针指向队尾元素。试 画出有两个元素A、B的不同存储结构的图示,及将这两个元素 出对后循环队列和链表队列的状态示意图。 7.对于一个具有m个单元的循环队列,写出求队列中元素 个数的公式。 4.写出多个链表栈中取第j个链表栈顶元素值的算法。 5.写出计算表达式3+4/25,8—6时操作数栈和运算符栈 的变化情况。 6.课文中规定:无论是循环队列还是链表队列,队头指 针总是指向队头元素的前一位置,队尾指针指向队尾元素。试 画出有两个元素A、B的不同存储结构的图示,及将这两个元素 出对后循环队列和链表队列的状态示意图。 7.对于一个具有m个单元的循环队列,写出求队列中元素 个数的公式。 数据结构(C语言版) 8.对于一个具有n个单元(n≥2)的循环队列,若从进入第一 个元素开始,每隔T1个时间单位进入下一个元素,同时从进入 第一个元素开始,每隔t2(t2≥t1)个时间单位处理一个元素并令其 出队。试编写一个算法,求出在第几个元素进队时将发生溢出。 9.假设以带头结点的循环链表表示队列,并且只设一个 指针指向队尾元素结点(注意不设头指针),试编写出相应的 置空队列、入队列和出队列的算法。 8.对于一个具有n个单元(n≥2)的循环队列,若从进入第一 个元素开始,每隔T1个时间单位进入下一个元素,同时从进入 第一个元素开始,每隔t2(t2≥t1)个时间单位处理一个元素并令其 出队。试编写一个算法,求出在第几个元素进队时将发生溢出。 9.假设以带头结点的循环链表表示队列,并且只设一个 指针指向队尾元素结点(注意不设头指针),试编写出相应的 置空队列、入队列和出队列的算法。 数据结构(C语言版) 10.设有两栈共享一存储空间 stack[n] ,如图3-8所示,每 个栈的大小不定试编写对任一栈进栈和出栈的算法push(x,i)和 pop(i),I=1,2分别表示左右两个栈。上溢条件应该是整个存储空 间全满。 图3.7 第10题图 数据结构(C语言版) 第4章 串 4.1 串的基本概念 4.2 串的存储结构 4.3 串的基本运算的实现 4.4 实习:串运算实例 习题4 4.1 串的基本概念 4.2 串的存储结构 4.3 串的基本运算的实现 4.4 实习:串运算实例 习题4 数据结构(C语言版) 4.1 串的基本概念 串(String)是由零个或多个字符组成的有限序列。一般记作: S="a1 a2 … an"(n≥0) 其中S是串名,用双引号括起来的字符序列为串值,引号 是界限符,ai(1≤i≤n)是一个任意字符(字母、数字或其他字符), 它称为串的元素,是构成串的基本单位,串中所包含的字符 个数n称为串的长度,当n=0时,称为空串。 串(String)是由零个或多个字符组成的有限序列。一般记作: S="a1 a2 … an"(n≥0) 其中S是串名,用双引号括起来的字符序列为串值,引号 是界限符,ai(1≤i≤n)是一个任意字符(字母、数字或其他字符), 它称为串的元素,是构成串的基本单位,串中所包含的字符 个数n称为串的长度,当n=0时,称为空串。 数据结构(C语言版) 将串值括起来的双引号本身不属于串,它的作用是避免串 与常数或与标识符混淆。 例如,A="123"是数字字符串,长度为3,它不同于整常数 123;B="lx"是长度为2的字符串,而lx通常表示一个标识符。 常将仅由一个或多个空格组成的串称为空白串。注意空串 和空白串的不同,例如""和""分别表示长度为1的空白串和长度 为0的空串。 串中任意连续的字符组成的子序列称为该串的子串。包含 子串的串相应地称为主串。 将串值括起来的双引号本身不属于串,它的作用是避免串 与常数或与标识符混淆。 例如,A="123"是数字字符串,长度为3,它不同于整常数 123;B="lx"是长度为2的字符串,而lx通常表示一个标识符。 常将仅由一个或多个空格组成的串称为空白串。注意空串 和空白串的不同,例如""和""分别表示长度为1的空白串和长度 为0的空串。 串中任意连续的字符组成的子序列称为该串的子串。包含 子串的串相应地称为主串。 数据结构(C语言版) 通常称字符在序列中的序号为该字符在串中的位置。子串 在主串中的位置则以子串的第一个字符首次出现在主串中的位 置来表示。 例如,设有两个字符串C和D: C="This is a string." D="is" 则它们的长度分别为17、2;D是C的子串,C为主串。D在C中 出现了两次,其中首次出现所对应的主串位置是3。因此,称D 在C中的序号(或位置)为3。 若两个串的长度相等且对应字符都相等,则称两个串是相 等的。当两个串不相等时,可按“字典顺序”区分大小。 通常称字符在序列中的序号为该字符在串中的位置。子串 在主串中的位置则以子串的第一个字符首次出现在主串中的位 置来表示。 例如,设有两个字符串C和D: C="This is a string." D="is" 则它们的长度分别为17、2;D是C的子串,C为主串。D在C中 出现了两次,其中首次出现所对应的主串位置是3。因此,称D 在C中的序号(或位置)为3。 若两个串的长度相等且对应字符都相等,则称两个串是相 等的。当两个串不相等时,可按“字典顺序”区分大小。 数据结构(C语言版) 串也是线性表的一种,因此串的逻辑结构和线性表极为 相似,区别仅在于串的数据对象限定为字符集。 串的运算有很多,下面介绍部分基本运算。 (1) 串赋值 StrAssign(&s,chars):已知串常量chars,生成一 个值等于chars的串s。 (2) 求串长 StrLength(s):已知串s,返回串s的长度。 (3) 串连接 StrConcat (&s1,s2):已知串s1,s2,在s1的后面 连接s2的串值。 (4) 求子串 SubStr(s,i,len):已知串s,返回从串s的第i个字 符开始的长度为len的子串。 串也是线性表的一种,因此串的逻辑结构和线性表极为 相似,区别仅在于串的数据对象限定为字符集。 串的运算有很多,下面介绍部分基本运算。 (1) 串赋值 StrAssign(&s,chars):已知串常量chars,生成一 个值等于chars的串s。 (2) 求串长 StrLength(s):已知串s,返回串s的长度。 (3) 串连接 StrConcat (&s1,s2):已知串s1,s2,在s1的后面 连接s2的串值。 (4) 求子串 SubStr(s,i,len):已知串s,返回从串s的第i个字 符开始的长度为len的子串。 数据结构(C语言版) (5) 串比较 StrCmp(s1,s2):已知串s1,s2,若s1==s2,操作 返回值为0;若s1s2, 返回值大于0。 (6) 子串定位 StrIndex(s,t):已知串s,t,找子串t在主串s中首 次出现的位置,即若t∈s,则操作返回t在s中首次出现的位置, 否则返回值为-1。 (7) 串插入 StrInsert(&s,i,t):已知串s,t,将串t插入到串s 的 第i个字符位置上。 (8) 串删除 StrDelete(&s,i,len):已知串s,删除串s 中从第i 个字符开始的长度为len的子串。 (5) 串比较 StrCmp(s1,s2):已知串s1,s2,若s1==s2,操作 返回值为0;若s1s2, 返回值大于0。 (6) 子串定位 StrIndex(s,t):已知串s,t,找子串t在主串s中首 次出现的位置,即若t∈s,则操作返回t在s中首次出现的位置, 否则返回值为-1。 (7) 串插入 StrInsert(&s,i,t):已知串s,t,将串t插入到串s 的 第i个字符位置上。 (8) 串删除 StrDelete(&s,i,len):已知串s,删除串s 中从第i 个字符开始的长度为len的子串。 数据结构(C语言版) (9) 串替换 StrRep(&s,t,r):已知串s,t,r,用串r 替换串s中出 现的所有与串t相等的不重叠的子串。 (10) 销毁串 StrDestroy(&s):已知串s,销毁串s。 以上是串的一些基本操作。其中前5个操作是最为基本的, 它们不能用其他的操作来合成,因此通常将这5个基本操作称 为最小操作集,反之,其他操作可在这个最小操作集上实现。 (9) 串替换 StrRep(&s,t,r):已知串s,t,r,用串r 替换串s中出 现的所有与串t相等的不重叠的子串。 (10) 销毁串 StrDestroy(&s):已知串s,销毁串s。 以上是串的一些基本操作。其中前5个操作是最为基本的, 它们不能用其他的操作来合成,因此通常将这5个基本操作称 为最小操作集,反之,其他操作可在这个最小操作集上实现。 数据结构(C语言版) 4.2 串的存储结构 4.2.1 串的顺序存储 串的顺序存储结构简称为顺序串。 与顺序表类似,顺序串是用一组地址连续的存储单元来存 储串中的字符序列的。因此可用高级语言的字符数组来实现, 按其存储分配的不同可将顺序串分为静态存储分配的顺序串和 动态存储分配的顺序串两类。 4.2.1 串的顺序存储 串的顺序存储结构简称为顺序串。 与顺序表类似,顺序串是用一组地址连续的存储单元来存 储串中的字符序列的。因此可用高级语言的字符数组来实现, 按其存储分配的不同可将顺序串分为静态存储分配的顺序串和 动态存储分配的顺序串两类。 数据结构(C语言版) 1. 静态存储分配的顺序串 所谓静态存储分配,是指按预定义的大小为每一个串变量 分配一个固定长度的存储区,即串值空间的大小在编译时刻就 已确定,是静态的。所以串空间最大值为MAXSIZE时,最多只 能放MAXSIZE-1个字符。其类型描述如下: 1. 静态存储分配的顺序串 所谓静态存储分配,是指按预定义的大小为每一个串变量 分配一个固定长度的存储区,即串值空间的大小在编译时刻就 已确定,是静态的。所以串空间最大值为MAXSIZE时,最多只 能放MAXSIZE-1个字符。其类型描述如下: 数据结构(C语言版) #define MAXSIZE 256 /*该值依赖于应用,由用户定义*/ typedef struct {char ch[MAXSIZE]; /*256个字符依次存储在 ch[0]..ch[MAXSIZE-1]中*/ int len; }SString; /*SString是顺序串类型,则串的最 大长度不能超过255*/ SString s; /*定义串变量s*/ #define MAXSIZE 256 /*该值依赖于应用,由用户定义*/ typedef struct {char ch[MAXSIZE]; /*256个字符依次存储在 ch[0]..ch[MAXSIZE-1]中*/ int len; }SString; /*SString是顺序串类型,则串的最 大长度不能超过255*/ SString s; /*定义串变量s*/ 数据结构(C语言版) 在直接使用定长的字符数组存放串内容外,一般可使用一 个不会出现在串中的特殊字符放在串值的末尾来表示串的结束。 例如,C语言中以字符'\0'表示串值的终结。 C语言中串的静态存储结构如图4.1所示。 s 0 t 1 u 2 d 3 e 4 n 5 t 6 \0 7 … … … … MAXSIZE-1 s.ch[MAXSIZE]: s.len= 7 s 0 t 1 u 2 d 3 e 4 n 5 t 6 \0 7 … … … … MAXSIZE-1 s.ch[MAXSIZE]: s.len= 7 图4.1 C语言中串的静态存储结构 数据结构(C语言版) 2. 动态存储分配的顺序串(堆串) 这种存储表示的特点是,仍以一组地址连续的存储单元 存放串值字符序列,但它们的存储空间是在程序执行过程中动 态分配而得的。系统将一个地址连续、容量很大的存储空间作 为字符串的可用空间,每当建立一个新串时,系统就从这个空 间中分配一个大小和字符串长度相同的空间存储新串的串值。 2. 动态存储分配的顺序串(堆串) 这种存储表示的特点是,仍以一组地址连续的存储单元 存放串值字符序列,但它们的存储空间是在程序执行过程中动 态分配而得的。系统将一个地址连续、容量很大的存储空间作 为字符串的可用空间,每当建立一个新串时,系统就从这个空 间中分配一个大小和字符串长度相同的空间存储新串的串值。 数据结构(C语言版) 假设以一维数组heap[MAXSIZE]表示可供字符串进行动态 分配的存储空间,并设一整型变量free指向heap中未分配区域的 开始地址。在程序执行过程中,当生成一个新串时,就从free指 示的位置起为新串分配一个所需大小的存储空间,同时记录该 串的相关信息。这种存储结构称为堆结构。动态分配存储空间 的顺序串也叫堆串。堆串可定义如下: typedef struct { int length; int start; }HeapString; 假设以一维数组heap[MAXSIZE]表示可供字符串进行动态 分配的存储空间,并设一整型变量free指向heap中未分配区域的 开始地址。在程序执行过程中,当生成一个新串时,就从free指 示的位置起为新串分配一个所需大小的存储空间,同时记录该 串的相关信息。这种存储结构称为堆结构。动态分配存储空间 的顺序串也叫堆串。堆串可定义如下: typedef struct { int length; int start; }HeapString; 数据结构(C语言版) 在C语言中,已经有一个称为“堆”的自由存储空间,并 可利用malloc()和free()等动态存储管理函数,根据实际需要动态 分配和释放字符数组空间,如图4.2所示。其类型可描述如下: typedef struct { char *ch; /*指示串的起始地址,可按实际的串长分配存储区 */ int len; }HString; HString s; /*定义一个串变量*/ 在C语言中,已经有一个称为“堆”的自由存储空间,并 可利用malloc()和free()等动态存储管理函数,根据实际需要动态 分配和释放字符数组空间,如图4.2所示。其类型可描述如下: typedef struct { char *ch; /*指示串的起始地址,可按实际的串长分配存储区 */ int len; }HString; HString s; /*定义一个串变量*/ 数据结构(C语言版) 图4.2 顺序串的动态存储结构 s t u d e n t \0s.ch s.len 7 在程序中,可根据实际需求为这种类型的串变量动态分配 存储空间,这样做非常有效、方便,只是在程序执行过程中要 不断地生成新串和销毁旧串。 数据结构(C语言版) 4.2.2 串的链式存储 顺序串上的插入和删除操作极不方便,需要移动大量的 字符。因此,我们可用单链表方式来存储串值,串的这种链式 存储结构简称为链串,如图4.3所示。 a b c ds e f 图4.3 结点大小为1的链串s 数据结构(C语言版) 链串的类型可描述如下: typedef struct node { char ch; struct node *next; /*next为指向结点的指针*/ }LString; LString s; /*定义一个串变量s*/ 一个链串由头指针惟一确定。 链串的类型可描述如下: typedef struct node { char ch; struct node *next; /*next为指向结点的指针*/ }LString; LString s; /*定义一个串变量s*/ 一个链串由头指针惟一确定。 数据结构(C语言版) 这种结构便于进行插入和删除运算,但存储空间利用率 太低。 为了提高存储密度,可使每个结点存放多个字符。如图 4.4所示,通常将结点数据域存放的字符个数定义为结点的大 小,显然,当结点大小大于1时,串的长度不一定正好是结点 的整数倍,因此要用特殊字符来填充最后一个结点,以表示 串的终结。 这种结构便于进行插入和删除运算,但存储空间利用率 太低。 为了提高存储密度,可使每个结点存放多个字符。如图 4.4所示,通常将结点数据域存放的字符个数定义为结点的大 小,显然,当结点大小大于1时,串的长度不一定正好是结点 的整数倍,因此要用特殊字符来填充最后一个结点,以表示 串的终结。 数据结构(C语言版) a b c d e f g # 图4.4 结点大小为4的链串 数据结构(C语言版) 对于结点大小不为1的链串,其类型定义只需对上述的结 点类型做简单的修改即可。 #define nodesize 80 typedef struct node { char data[nodesize]; struct node *next; }LString; 虽然增大结点的数据域使得存储密度增大,但是做插入、 删除运算时,需要考虑结点的分拆与合并,可能会引起大量字 符的移动,给运算带来不便。图4.5所示为在图4.4所示链串的第 三个字符之后插入“xyz”后的链串。 对于结点大小不为1的链串,其类型定义只需对上述的结 点类型做简单的修改即可。 #define nodesize 80 typedef struct node { char data[nodesize]; struct node *next; }LString; 虽然增大结点的数据域使得存储密度增大,但是做插入、 删除运算时,需要考虑结点的分拆与合并,可能会引起大量字 符的移动,给运算带来不便。图4.5所示为在图4.4所示链串的第 三个字符之后插入“xyz”后的链串。 数据结构(C语言版) a b c x f g ## y z d e 图4.5 链串的插入 a b c x f g ## y z d e 数据结构(C语言版) 4.3 串的基本运算的实现 1. 求子串运算(采用静态存储顺序串) /*算法描述4.1 求子串*/ int StrSub(SString *sub, SString s, int pos, int len) /* 用sub返回串s中序号pos开始的长度为len 的子串*/ { int i; if (pos<0 || pos>s.len || len<1 || len>s.len-pos) { sub->len=0; return(0); } /*子串起始位置及长度是否合适*/ else{ for(i=0;ich[i]=s.ch[i+pos]; sub->[len]= '\0'; /*子串结束*/ sub->len=len; return(1); } } 1. 求子串运算(采用静态存储顺序串) /*算法描述4.1 求子串*/ int StrSub(SString *sub, SString s, int pos, int len) /* 用sub返回串s中序号pos开始的长度为len 的子串*/ { int i; if (pos<0 || pos>s.len || len<1 || len>s.len-pos) { sub->len=0; return(0); } /*子串起始位置及长度是否合适*/ else{ for(i=0;ich[i]=s.ch[i+pos]; sub->[len]= '\0'; /*子串结束*/ sub->len=len; return(1); } } 数据结构(C语言版) 2. 定位运算(采用静态存储顺序串) 串的定位运算也称为串的模式匹配,是一种重要的串运算。 设s和t是给定的两个串,在主串s中找到等于子串t的过程称 为模式匹配,如果在s中找到等于t的子串,则称匹配成功,函 数返回t在s中的首次出现的存储位置(或序号),否则匹配失败, 返回-1。t也称为模式。 2. 定位运算(采用静态存储顺序串) 串的定位运算也称为串的模式匹配,是一种重要的串运算。 设s和t是给定的两个串,在主串s中找到等于子串t的过程称 为模式匹配,如果在s中找到等于t的子串,则称匹配成功,函 数返回t在s中的首次出现的存储位置(或序号),否则匹配失败, 返回-1。t也称为模式。 数据结构(C语言版) 算法思想如下:首先将s0与t0进行比较,若不同,就将s1与 t0进行比较……,直到s的某一个字符si和t0相同,再将它们之后 的字符进行比较,若也相同,则如此继续往下比较,当s的某一 个字符si与t的字符tj不同时,则s返回到本趟开始字符的下一个 字符,即si-j+1,t返回到t0,继续开始下一趟的比较,重复上述 过程。若t中的字符全部比较完,则说明本趟匹配成功,本趟的 起始位置是i-j,否则,匹配失败。 设主串s="ababcabcacbab",模式t="abcac",匹配过程 如图4.6所示。 算法思想如下:首先将s0与t0进行比较,若不同,就将s1与 t0进行比较……,直到s的某一个字符si和t0相同,再将它们之后 的字符进行比较,若也相同,则如此继续往下比较,当s的某一 个字符si与t的字符tj不同时,则s返回到本趟开始字符的下一个 字符,即si-j+1,t返回到t0,继续开始下一趟的比较,重复上述 过程。若t中的字符全部比较完,则说明本趟匹配成功,本趟的 起始位置是i-j,否则,匹配失败。 设主串s="ababcabcacbab",模式t="abcac",匹配过程 如图4.6所示。 数据结构(C语言版)a b a b c a b c a bc a b a b c 第一趟 i= 3 j= 3 a b a b c a b c a bc a b a 第二趟 i= 2 j= 1 a b a b c a b c a bc a b第三趟 i= 7 j= 5 a b c a c a b a b c a b c a bc a b第四趟 i= 4 j= 1 a a b a b c a b c a bc a b第五趟 i= 5 j= 1 a a b a b c a b c a bc a b第六趟 i= 11 j= 6 a b c a c 图 4. 6 简单模式匹配的匹配过程 a b a b c a b c a bc a b a b c 第一趟 i= 3 j= 3 a b a b c a b c a bc a b a 第二趟 i= 2 j= 1 a b a b c a b c a bc a b第三趟 i= 7 j= 5 a b c a c a b a b c a b c a bc a b第四趟 i= 4 j= 1 a a b a b c a b c a bc a b第五趟 i= 5 j= 1 a a b a b c a b c a bc a b第六趟 i= 11 j= 6 a b c a c 图 4. 6 简单模式匹配的匹配过程 数据结构(C语言版) /*算法描述4.2 模式匹配*/ int StrIndex(SString s,int pos,SString t) /*求串t在串s中的位置*/ { int i,j; if(t.len==0) return(0); i=pos;j=0; while(i=t.len) return(i-j); /*匹配成功,返回存储位置*/ else return(-1); } /*算法描述4.2 模式匹配*/ int StrIndex(SString s,int pos,SString t) /*求串t在串s中的位置*/ { int i,j; if(t.len==0) return(0); i=pos;j=0; while(i=t.len) return(i-j); /*匹配成功,返回存储位置*/ else return(-1); } 数据结构(C语言版) 3.插入运算(采用动态存储串) /*算法描述4.3 串插入*/ StrInsert(HString *s, int pos, HString t) /*在串s的第pos个字符之前插入串t*/ { char *temp; int i; if(pos<0 || pos>s->len) return(0); /*pos不合理*/ if(t.len) /*t非空*/ { temp=(char*)malloc((s->len+t.len)*sizeof(char)); /*临时变量,用来暂存插入后的结果*/ 3.插入运算(采用动态存储串) /*算法描述4.3 串插入*/ StrInsert(HString *s, int pos, HString t) /*在串s的第pos个字符之前插入串t*/ { char *temp; int i; if(pos<0 || pos>s->len) return(0); /*pos不合理*/ if(t.len) /*t非空*/ { temp=(char*)malloc((s->len+t.len)*sizeof(char)); /*临时变量,用来暂存插入后的结果*/ 数据结构(C语言版) if(temp==NULL) return(0); for(i=0;ich[i]; for(i=0;ilen;i++) temp[i+t.len]=s->ch[i]; s->len+=t.len; free(s->ch); /*释放原串s*/ s->ch=temp; /*s获得相加结果*/ return(1); } } if(temp==NULL) return(0); for(i=0;ich[i]; for(i=0;ilen;i++) temp[i+t.len]=s->ch[i]; s->len+=t.len; free(s->ch); /*释放原串s*/ s->ch=temp; /*s获得相加结果*/ return(1); } } 数据结构(C语言版) 4.连接运算(采用动态存储串) /*算法描述4.4 串连接*/ StrCat(HString *s,HString t) /*将串t连接在串s的后面*/ { char *temp; int i; temp=(char *) malloc((s->len+t.len)*sizeof(char)); if(temp==NULL) return(0); for(i=0;i<=s->len;i++) temp[i]=s->ch[i]; /*复制s串*/ for(i=s->len;i<=s->len+t.len;i++) /*复制t串*/ temp[i]=t.ch[i-s->len]; s->len+=t.len; free(s->ch); s->ch=temp; return(1); } 4.连接运算(采用动态存储串) /*算法描述4.4 串连接*/ StrCat(HString *s,HString t) /*将串t连接在串s的后面*/ { char *temp; int i; temp=(char *) malloc((s->len+t.len)*sizeof(char)); if(temp==NULL) return(0); for(i=0;i<=s->len;i++) temp[i]=s->ch[i]; /*复制s串*/ for(i=s->len;i<=s->len+t.len;i++) /*复制t串*/ temp[i]=t.ch[i-s->len]; s->len+=t.len; free(s->ch); s->ch=temp; return(1); } 数据结构(C语言版) 5.串定位(采用链串存储) /*算法描述4.5 串定位*/ LString *lindex(LString s,LString t) /*求串t在串s中的位置,返回指向t串起始位置的指针*/ { LString *loc,*p,*q; loc=s; p=loc;q=t; while(p && q) /*当t、s串均未结束时*/ { if(p->data==q->data) /*字符匹配时,指针后移*/ 5.串定位(采用链串存储) /*算法描述4.5 串定位*/ LString *lindex(LString s,LString t) /*求串t在串s中的位置,返回指向t串起始位置的指针*/ { LString *loc,*p,*q; loc=s; p=loc;q=t; while(p && q) /*当t、s串均未结束时*/ { if(p->data==q->data) /*字符匹配时,指针后移*/ 数据结构(C语言版) { p=p->next; q=q->next; } else /*字符不匹配时,回溯*/ { loc=loc->next; p=loc; q=t; } } if(q==NULL) return(loc); /*匹配完成,返回*/ else return(NULL); } { p=p->next; q=q->next; } else /*字符不匹配时,回溯*/ { loc=loc->next; p=loc; q=t; } } if(q==NULL) return(loc); /*匹配完成,返回*/ else return(NULL); } 数据结构(C语言版) 4.4 实习:串运算实例 文本编辑程序用于源程序的输入和修改,公文书信、报刊 和书籍的编辑排版等。常用的文本编辑程序有Word、WPS等。 文本编辑的实质是修改字符数据的形式和格式,虽然各个文本 编辑程序的功能强弱不同,但基本操作是一样的,都包括串的 查找、插入和删除等操作。 这一节我们来实现一个简单的文本编辑操作演示程序,包 括字符串的部分基本运算:赋值、比较、连接、插入、删除和 清除运算。字符串采用动态存储结构(即堆分配)存储。 文本编辑程序用于源程序的输入和修改,公文书信、报刊 和书籍的编辑排版等。常用的文本编辑程序有Word、WPS等。 文本编辑的实质是修改字符数据的形式和格式,虽然各个文本 编辑程序的功能强弱不同,但基本操作是一样的,都包括串的 查找、插入和删除等操作。 这一节我们来实现一个简单的文本编辑操作演示程序,包 括字符串的部分基本运算:赋值、比较、连接、插入、删除和 清除运算。字符串采用动态存储结构(即堆分配)存储。 数据结构(C语言版) #define NULL 0 typedef struct { char *ch; int len; }HString; int StrAssign(HString *s,char *chars) { int i=0,slen; if(s->ch!=NULL) free(s->ch); while(chars[i]!='\0') i++; slen=i; if(slen){ #define NULL 0 typedef struct { char *ch; int len; }HString; int StrAssign(HString *s,char *chars) { int i=0,slen; if(s->ch!=NULL) free(s->ch); while(chars[i]!='\0') i++; slen=i; if(slen){ 数据结构(C语言版) s->ch=(char *)malloc(slen*sizeof(char)); if(s->ch==NULL) return(0); for(i=0;i<=slen;i++) s->ch[i]=chars[i]; } else s->ch=NULL; s->len=slen; return(1); } int StrCompare(HString s,HString t) { int i; for(i=0;ich=(char *)malloc(slen*sizeof(char)); if(s->ch==NULL) return(0); for(i=0;i<=slen;i++) s->ch[i]=chars[i]; } else s->ch=NULL; s->len=slen; return(1); } int StrCompare(HString s,HString t) { int i; for(i=0;it.ch[i]) return(1); else return(-1); } StrCat(HString *s,HString t) /*参见算法4.4*/ StrInsert(HString *s, int pos, HString t) /*参见算法4.3*/ StrDelete(HString *s, int pos,int len) { int i; char *temp; if(pos<0 || pos>s->len-len) return(0); if(len){ if (s.ch[i]==t.ch[i]) return(0); else if(s.ch[i]>t.ch[i]) return(1); else return(-1); } StrCat(HString *s,HString t) /*参见算法4.4*/ StrInsert(HString *s, int pos, HString t) /*参见算法4.3*/ StrDelete(HString *s, int pos,int len) { int i; char *temp; if(pos<0 || pos>s->len-len) return(0); if(len){ 数据结构(C语言版) temp=(char*)malloc((s->len-len)*sizeof(char)); if(temp==NULL) return(0); for(i=0;ich[i]; for(i=pos;i<=s->len-len;i++) temp[i]=s->ch[i+len]; s->len-=len; free(s->ch); s->ch=temp; return(1); } } temp=(char*)malloc((s->len-len)*sizeof(char)); if(temp==NULL) return(0); for(i=0;ich[i]; for(i=pos;i<=s->len-len;i++) temp[i]=s->ch[i+len]; s->len-=len; free(s->ch); s->ch=temp; return(1); } } 数据结构(C语言版) int ClearString(HString *s) { if(s->ch) { free(s->ch); s->ch=NULL; s->len=0; } return(0); } main() { int inp,flag=1; char *s,*t; HString s1,s2,*res; int ClearString(HString *s) { if(s->ch) { free(s->ch); s->ch=NULL; s->len=0; } return(0); } main() { int inp,flag=1; char *s,*t; HString s1,s2,*res; 数据结构(C语言版) int pos,len,ret; printf("1-------StrAssing\n"); printf("2-------StrCompare\n"); printf("3-------StrCat\n"); printf("4-------StrInsert\n"); printf("5-------StrDelete\n"); printf("6-------ClearString\n"); printf("7-------Exit"); printf("please input 1--7\n\n"); while(flag) { scanf("%d",&inp); switch(inp){ case 1:{ int pos,len,ret; printf("1-------StrAssing\n"); printf("2-------StrCompare\n"); printf("3-------StrCat\n"); printf("4-------StrInsert\n"); printf("5-------StrDelete\n"); printf("6-------ClearString\n"); printf("7-------Exit"); printf("please input 1--7\n\n"); while(flag) { scanf("%d",&inp); switch(inp){ case 1:{ 数据结构(C语言版) scanf("%s",s); ret=StrAssign(&s1,s); if(ret!=0) printf("the string is:%s\n",s1.ch); else printf("error\n"); break;} case 2:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); printf("t="); scanf("%s",t); StrAssign(&s2,t); ret=StrCompare(s1,s2); switch(ret){ scanf("%s",s); ret=StrAssign(&s1,s); if(ret!=0) printf("the string is:%s\n",s1.ch); else printf("error\n"); break;} case 2:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); printf("t="); scanf("%s",t); StrAssign(&s2,t); ret=StrCompare(s1,s2); switch(ret){ 数据结构(C语言版) case 0: printf("two strings are equle\n"); break; case 1: printf("the first string > the second string\n"); break; case -1: printf("the first string < the second string\n"); break; } break;} case 3:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); printf("t="); scanf("%s",t); StrAssign(&s2,t); StrCat(&s1,s2); printf("s1 cat s2 is:%s\n",s1); break;} case 0: printf("two strings are equle\n"); break; case 1: printf("the first string > the second string\n"); break; case -1: printf("the first string < the second string\n"); break; } break;} case 3:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); printf("t="); scanf("%s",t); StrAssign(&s2,t); StrCat(&s1,s2); printf("s1 cat s2 is:%s\n",s1); break;} 数据结构(C语言版) case 4:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); printf("t="); scanf("%s,",t); StrAssign(&s2,t); printf("pos="); scanf("%d",&pos); StrInsert(&s1,pos,s2); printf("result:%s\n",s1); break;} case 4:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); printf("t="); scanf("%s,",t); StrAssign(&s2,t); printf("pos="); scanf("%d",&pos); StrInsert(&s1,pos,s2); printf("result:%s\n",s1); break;} 数据结构(C语言版) case 5:{ printf("s="); scanf("%s,",s); StrAssign(&s1,s); printf("pos="); scanf("%d",&pos); printf("len="); scanf("%d",&len); StrDelete(&s1,pos,len); printf("result: %s\n",s1); break;} case 5:{ printf("s="); scanf("%s,",s); StrAssign(&s1,s); printf("pos="); scanf("%d",&pos); printf("len="); scanf("%d",&len); StrDelete(&s1,pos,len); printf("result: %s\n",s1); break;} 数据结构(C语言版) case 6:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); ret=ClearString(&s1); if(ret==0) printf("the string is NULL\n"); else printf("error\n"); break;} case 7:flag=0;break; } } } case 6:{ printf("s="); scanf("%s",s); StrAssign(&s1,s); ret=ClearString(&s1); if(ret==0) printf("the string is NULL\n"); else printf("error\n"); break;} case 7:flag=0;break; } } } 数据结构(C语言版) 习 题 4 1. 简述下列每对术语的区别:空串和空白串,串常量和串 变量,主串和子串,静态分配的顺序串和动态分配的顺序串。 2. 设s="I am a student",t="good",q="programer"。给出下 列操作的结果: (1) StrLength(s) (2) SubString(sub1,s,1,7) (3) StrIndex(s, 'a',4) (4) StrReplace(s, 'student',q) (5) Strcat(StrCat(sub1,t)) 1. 简述下列每对术语的区别:空串和空白串,串常量和串 变量,主串和子串,静态分配的顺序串和动态分配的顺序串。 2. 设s="I am a student",t="good",q="programer"。给出下 列操作的结果: (1) StrLength(s) (2) SubString(sub1,s,1,7) (3) StrIndex(s, 'a',4) (4) StrReplace(s, 'student',q) (5) Strcat(StrCat(sub1,t)) 数据结构(C语言版) 3. 利用C的库函数strlen,strcpy和strcat写一算法void StrInsert(char *S, char *T, int i),将串T插入到串S的第i个位置上。 若i大于S的长度,则插入不执行。 4. 利用C的库函数strlen 和strcpy写一算法void StrDelete(char *S,int i, int m),删去串S中从位置i开始的连续m 个字符。若i≥strlen(S),则没有字符被删除;若i+m≥strlen(S),则 将S中从位置i开始直至末尾的字符均删去。 5. 以HString为存储表示,写一个求子串的算法。 3. 利用C的库函数strlen,strcpy和strcat写一算法void StrInsert(char *S, char *T, int i),将串T插入到串S的第i个位置上。 若i大于S的长度,则插入不执行。 4. 利用C的库函数strlen 和strcpy写一算法void StrDelete(char *S,int i, int m),删去串S中从位置i开始的连续m 个字符。若i≥strlen(S),则没有字符被删除;若i+m≥strlen(S),则 将S中从位置i开始直至末尾的字符均删去。 5. 以HString为存储表示,写一个求子串的算法。 数据结构(C语言版) 6. 一个文本串可用事先给定的字母映射表进行加密。例如, 设字母映射表为: a b c d e f g h I j k l m n o p q r s t u v w x y z n g z q t c o b m u h e l k p d a w x f y I v r s j 则字符串"encrypt"被加密为"tkzwsdf"。试写一算法将输入的文 本串进行加密后输出;另写一算法,将输入的已加密的文本串 进行解密后输出。 6. 一个文本串可用事先给定的字母映射表进行加密。例如, 设字母映射表为: a b c d e f g h I j k l m n o p q r s t u v w x y z n g z q t c o b m u h e l k p d a w x f y I v r s j 则字符串"encrypt"被加密为"tkzwsdf"。试写一算法将输入的文 本串进行加密后输出;另写一算法,将输入的已加密的文本串 进行解密后输出。 数据结构(C语言版) 7. 写一算法void StrReplace(char *T, char *P, char *S),将T 中首次出现的子串P替换为串S。注意:S和P的长度不一定相等。 8. 若S和T是用结点大小为1的单链表存储的两个串,试设 计一个算法找出S中第一个不在T中出现的字符。 数据结构(C语言版) 第5章 数 组 5.1 数组的定义和运算 5.2 数组的顺序存储和实现 5.3 特殊矩阵的压缩存储 5.4 实习:数组应用实例 习题5 5.1 数组的定义和运算 5.2 数组的顺序存储和实现 5.3 特殊矩阵的压缩存储 5.4 实习:数组应用实例 习题5 数据结构(C语言版) 5.1 数组的定义和运算 数组是我们很熟悉的一种数据类型,很多高级语言都支持 这种数据类型。从逻辑结构上看,数组可以看作一般线性表的 推广。数组作为一种数据结构,其特点是结构中的元素本身可 以是具有某种结构的数据,但属于同一数据类型,比如:一维 数组可以看作一个线性表,二维数组可以看作“数据元素是一 维数组”的一维数组,三维数组可以看作“数据元素是二维数 组”的一维数组,依次类推。 数组是我们很熟悉的一种数据类型,很多高级语言都支持 这种数据类型。从逻辑结构上看,数组可以看作一般线性表的 推广。数组作为一种数据结构,其特点是结构中的元素本身可 以是具有某种结构的数据,但属于同一数据类型,比如:一维 数组可以看作一个线性表,二维数组可以看作“数据元素是一 维数组”的一维数组,三维数组可以看作“数据元素是二维数 组”的一维数组,依次类推。 数据结构(C语言版) 例如,对线性表A=(A0,A1,A2,…,An-1),如果其中的任 意元素Aj(0≤j≤n-1)是简单类型,则A是一个一维数组,如果Aj 本身也是一个线性表,即Aj=(a1j,a2j,…,am-1,j),则A是一 个二维数组,如图5.1所示。 数据结构(C语言版) a00 a0, n- 1a01 … a0j … a10 a1, n- 1a11 … a1j … ………… ai0 ai, n- 1ai1 … aij … ………… am- 1, 0am- 1, 1 … am- 1, j … am- 1, n- 1 (A0,A1,…Aj ,…,An- 1)A= A = 图5.1 由线性表推广得到的二维数组 a00 a0, n- 1a01 … a0j … a10 a1, n- 1a11 … a1j … ………… ai0 ai, n- 1ai1 … aij … ………… am- 1, 0am- 1, 1 … am- 1, j … am- 1, n- 1 (A0,A1,…Aj ,…,An- 1)A= A = 数据结构(C语言版) 同理,三维数组可以看成是这样的一个线性表,其中每个 数据元素均是一个二维数组。依次类推,可以得到多维数组。 从数组的特殊结构,我们可以看出,数组中的每一个元素 由一个值和一组下标来描述。“值”代表数组中元素的数据信 息,一组下标用来描述该元素在数组中的相对位置信息。数组 的维数不同,描述其对应位置的下标的个数也不同。例如,在 二维数组中,元素aij由两个下标值i,j来描述,其中i表示该元 素所在的行号,j表示该元素所在的列号。同样,我们可以将这 个特性推广到多维数组,对于n维数组而言,其元素由n个下标 值来描述其在n维数组中的相对位置。 同理,三维数组可以看成是这样的一个线性表,其中每个 数据元素均是一个二维数组。依次类推,可以得到多维数组。 从数组的特殊结构,我们可以看出,数组中的每一个元素 由一个值和一组下标来描述。“值”代表数组中元素的数据信 息,一组下标用来描述该元素在数组中的相对位置信息。数组 的维数不同,描述其对应位置的下标的个数也不同。例如,在 二维数组中,元素aij由两个下标值i,j来描述,其中i表示该元 素所在的行号,j表示该元素所在的列号。同样,我们可以将这 个特性推广到多维数组,对于n维数组而言,其元素由n个下标 值来描述其在n维数组中的相对位置。 数据结构(C语言版) 根据数组的结构特性可以看出,数组实际上是一组有固 定个数的元素的集合。也就是说,一旦定义了数组的维数和 每一维的上下限,数组中元素个数就固定了。例如二维数组 A3×4,它有3行、4列,即由12个元素组成。由于这个性质, 使得对数组的操作不像对线性表的操作那样可以在表中任意 一个合法的位置插入或删除一个元素。通常在各种高级语言 中数组一旦被定义,每一维的大小及上下界都不能改变。对 于数组的操作一般只有以下几种: 根据数组的结构特性可以看出,数组实际上是一组有固 定个数的元素的集合。也就是说,一旦定义了数组的维数和 每一维的上下限,数组中元素个数就固定了。例如二维数组 A3×4,它有3行、4列,即由12个元素组成。由于这个性质, 使得对数组的操作不像对线性表的操作那样可以在表中任意 一个合法的位置插入或删除一个元素。通常在各种高级语言 中数组一旦被定义,每一维的大小及上下界都不能改变。对 于数组的操作一般只有以下几种: 数据结构(C语言版) (1) 构造数组; (2) 撤消数组; (3) 取值操作:给定一组下标,读取对应的数据元素值; (4) 赋值操作:给定一组下标,存储或修改与其相对应的 数据元素值。 我们着重研究二维数组,因为它的应用最为广泛。 (1) 构造数组; (2) 撤消数组; (3) 取值操作:给定一组下标,读取对应的数据元素值; (4) 赋值操作:给定一组下标,存储或修改与其相对应的 数据元素值。 我们着重研究二维数组,因为它的应用最为广泛。 数据结构(C语言版) 5.2 数组的顺序存储和实现 对于数组A,一旦给定其维数n及各维长度bi(1≤i≤n),则该 数组中元素的个数是固定的,不能对数组做插入和删除操作, 不涉及移动数据元素操作,因此对于数组而言,采用顺序存储 方式比较合适。 我们知道,计算机内存储器的结构是一维的,因此对于一 维数组按下标顺序分配即可,而对多维数组,就必须按照某种 次序将数据元素排成一个线性序列,然后将这个线性序列存放 在存储器中。 对于数组A,一旦给定其维数n及各维长度bi(1≤i≤n),则该 数组中元素的个数是固定的,不能对数组做插入和删除操作, 不涉及移动数据元素操作,因此对于数组而言,采用顺序存储 方式比较合适。 我们知道,计算机内存储器的结构是一维的,因此对于一 维数组按下标顺序分配即可,而对多维数组,就必须按照某种 次序将数据元素排成一个线性序列,然后将这个线性序列存放 在存储器中。 数据结构(C语言版) 数组的顺序存储结构有两种:一是以行为主序(或先行后 列)的顺序存放,如BASIC、PASCAL、COBOL、C等程序设计 语言中用的是以行为主的顺序分配,即一行分配完了接着分配 下一行;另一种是以列为主序(先列后行)的顺序存放,如 FORTRAN语言中,用的是以列为主序的分配顺序,即一列一 列地分配。以行为主序的分配规律是:最右边的下标先变化, 即最右下标从小到大,循环一遍后,右边第二个下标再变,..., 从右向左,最后是左下标。以列为主序分配的规律恰好相反: 最左边的下标先变化,即最左下标从小到大,循环一遍后,左 边第二个下标再变,...,从左向右,最后是右下标。 数组的顺序存储结构有两种:一是以行为主序(或先行后 列)的顺序存放,如BASIC、PASCAL、COBOL、C等程序设计 语言中用的是以行为主的顺序分配,即一行分配完了接着分配 下一行;另一种是以列为主序(先列后行)的顺序存放,如 FORTRAN语言中,用的是以列为主序的分配顺序,即一列一 列地分配。以行为主序的分配规律是:最右边的下标先变化, 即最右下标从小到大,循环一遍后,右边第二个下标再变,..., 从右向左,最后是左下标。以列为主序分配的规律恰好相反: 最左边的下标先变化,即最左下标从小到大,循环一遍后,左 边第二个下标再变,...,从左向右,最后是右下标。 数据结构(C语言版) 例如,二维数组Am×n以行为主序的存储序列为: a00,a01,…,a0,n-1,a10,a11,…,a1,n-1,…,am-1,0,am- 1,1,…,am-1,n-1 而以列为主序的存储序列为: a00,a10,…,am-1,0,a01,a11,…,am-1,1,…,a0,n-1,a1,n -1,…,am-1,n-1 例如,一个2×3的二维数组,逻辑结构可以用图5.2(a)表 示。以行为主序的内存映像如图5.2(b)所示,分配顺序为a00, a01,a02,a10,a11,a12;以列为主序的分配顺序为a00,a10,a01, a11,a02,a12,它的内存映像如图5.2(c)所示。 例如,二维数组Am×n以行为主序的存储序列为: a00,a01,…,a0,n-1,a10,a11,…,a1,n-1,…,am-1,0,am- 1,1,…,am-1,n-1 而以列为主序的存储序列为: a00,a10,…,am-1,0,a01,a11,…,am-1,1,…,a0,n-1,a1,n -1,…,am-1,n-1 例如,一个2×3的二维数组,逻辑结构可以用图5.2(a)表 示。以行为主序的内存映像如图5.2(b)所示,分配顺序为a00, a01,a02,a10,a11,a12;以列为主序的分配顺序为a00,a10,a01, a11,a02,a12,它的内存映像如图5.2(c)所示。 数据结构(C语言版) a00 a10 a01 a11 a02 a12 a00 a01 a02 a10 a11 a12 a00 a10 a01 a11 a02 a12 (a) (b) (c) a00 a10 a01 a11 a02 a12 a00 a01 a02 a10 a11 a12 a00 a10 a01 a11 a02 a12 (a) (b) (c) 图5.2 2×3数组存储 (a) 逻辑状态;(b) 以行为主序;(c) 以列为主序 数据结构(C语言版) 假设有一个3×4×2的三维数组A,共有24个元素,其逻 辑结构如图5.3所示。 a000 a010 a020 a030 a130 a230 a120 a220 a110 a210 a100 a200 a001 a011 a021 a031 0 1 2 0 1 2 3 二维一维 0 1 三维 a131 a231 a000 a010 a020 a030 a130 a230 a120 a220 a110 a210 a100 a200 a001 a011 a021 a031 0 1 2 0 1 2 3 二维一维 0 1 三维 a131 a231 图5.3 三维数组的逻辑结构 数据结构(C语言版) 三维数组元素的标号由三个数字表示。如果对A3×4×2采用以 行为主序的方式存放,则顺序为: a000,a001,a010,a011,…,a220,a221,a230,a231 采用以列为主序的方式存放,则顺序为: a000,a100,a200,a010,…,a221,a031,a131,a231 以上的存放规则可推广到多维数组的情况。总之,知道了 多维数组的维数,以及每维的上下界,就可以方便地将多维数 组按顺序存储结构存放在计算机中了。同时,根据数组的下标, 可以计算出其在存储器中的位置。因此,数组的顺序存储是一 种随机存取的结构。 三维数组元素的标号由三个数字表示。如果对A3×4×2采用以 行为主序的方式存放,则顺序为: a000,a001,a010,a011,…,a220,a221,a230,a231 采用以列为主序的方式存放,则顺序为: a000,a100,a200,a010,…,a221,a031,a131,a231 以上的存放规则可推广到多维数组的情况。总之,知道了 多维数组的维数,以及每维的上下界,就可以方便地将多维数 组按顺序存储结构存放在计算机中了。同时,根据数组的下标, 可以计算出其在存储器中的位置。因此,数组的顺序存储是一 种随机存取的结构。 数据结构(C语言版) 下面,以“以行为主序”的分配为例,讨论数组中数据元 素存储位置的计算。 设有二维数组Am×n,,下标从0开始,假设每个数组元素 占size个存储单元,首元素a00的存储地址为Loc[0,0],对任意 元素aij来说,因为aij是排在第i行、第j列,前面的i行有n×i个 元素,第i行第j列个元素前面还有j个元素,所以,可得aij的地 址计算公式如下: Loc[i,j] = Loc[0,0] + ( i×n + j ) × size 下面,以“以行为主序”的分配为例,讨论数组中数据元 素存储位置的计算。 设有二维数组Am×n,,下标从0开始,假设每个数组元素 占size个存储单元,首元素a00的存储地址为Loc[0,0],对任意 元素aij来说,因为aij是排在第i行、第j列,前面的i行有n×i个 元素,第i行第j列个元素前面还有j个元素,所以,可得aij的地 址计算公式如下: Loc[i,j] = Loc[0,0] + ( i×n + j ) × size 数据结构(C语言版) 同理,三维数组Ar×m×n可以看成是r个m×n的二维数组,若 首元素的存储地址为Loc[0,0,0],则元素ai11的存储地址为 Loc[i,1,1] = Loc[0,0,0] + ( i×m×n ) × size,这是因为在 该元素之前,有i个m×n的二维数组。由ai11的地址和二维数组 的地址计算公式,不难得到三维数组任意元素aijk的地址计算公 式如下: Loc[i,j,k] = Loc[0,0,0] + ( i×m×n + j×n+k ) × size 其中0≤i≤r-1,0≤j≤m-1,0≤k≤n-1。 同理,三维数组Ar×m×n可以看成是r个m×n的二维数组,若 首元素的存储地址为Loc[0,0,0],则元素ai11的存储地址为 Loc[i,1,1] = Loc[0,0,0] + ( i×m×n ) × size,这是因为在 该元素之前,有i个m×n的二维数组。由ai11的地址和二维数组 的地址计算公式,不难得到三维数组任意元素aijk的地址计算公 式如下: Loc[i,j,k] = Loc[0,0,0] + ( i×m×n + j×n+k ) × size 其中0≤i≤r-1,0≤j≤m-1,0≤k≤n-1。 数据结构(C语言版) 数组是各种高级语言中已经实现的数据结构。在高级语言 的应用层上,一般不会涉及到数据元素的存储地址的计算,这 一计算内存地址的任务是由高级语言的编译系统完成的。当我 们使用数组进行程序设计时,只需给出数组的下标范围,编译 系统将根据用户提供的必要参数进行地址分配,存取数据元素 时,也只要给出下标,而不必考虑其内存情况。 例5.1 若矩阵Am×n中存在某个元素aij,且满足aij是第i行中 最小值且是第j列中的最大值,则称该元素为矩阵A的一个鞍点。 试编写一个算法,找出A中的所有鞍点。 数组是各种高级语言中已经实现的数据结构。在高级语言 的应用层上,一般不会涉及到数据元素的存储地址的计算,这 一计算内存地址的任务是由高级语言的编译系统完成的。当我 们使用数组进行程序设计时,只需给出数组的下标范围,编译 系统将根据用户提供的必要参数进行地址分配,存取数据元素 时,也只要给出下标,而不必考虑其内存情况。 例5.1 若矩阵Am×n中存在某个元素aij,且满足aij是第i行中 最小值且是第j列中的最大值,则称该元素为矩阵A的一个鞍点。 试编写一个算法,找出A中的所有鞍点。 数据结构(C语言版) 基本思想:在矩阵A中求出每一行的最小值元素,然后判断 该元素是否是它所在列中的最大值,是则打印出,接着处理下 一行。矩阵A用一个二维数组表示。 算法如下: /*算法描述5.1 矩阵的鞍点*/ void saddle (int A[][],int m, int n) /*m,n是矩阵A的行和列*/ { int i,j,min; for (i=0;i=m) printf ("%d,%d,%d\n", i ,k,min); } } } for (j=1; j=m) printf ("%d,%d,%d\n", i ,k,min); } } } 数据结构(C语言版) 5.3 特殊矩阵的压缩存储 矩阵是科学计算、工程数学中大量研究的对象。对于一个 矩阵结构显然用一个二维数组来表示是非常恰当的。但在有些 情况下,例如有些高阶矩阵中,阶次很高,数据量很大,而非 零元素非常少(远小于m×n),若仍采用二维数组存放,就会有 很多存储单元都存放的是0,这不仅造成存储空间的浪费,而 且给运算带来了很大的不便,这时,就很希望能够只存储部分 有效元素。另外,还有一些矩阵的数据元素分布有一定规律, 如三角矩阵、对称矩阵、带状矩阵等,那么就可以利用这些规 律,只存储部分元素,从而提高存储空间的利用率。这些问题 都需要对矩阵进行压缩存储,一般的压缩原则是:对有规律的 元素和值相同的元素只分配一个存储空间,对零元素不分配空 间。 矩阵是科学计算、工程数学中大量研究的对象。对于一个 矩阵结构显然用一个二维数组来表示是非常恰当的。但在有些 情况下,例如有些高阶矩阵中,阶次很高,数据量很大,而非 零元素非常少(远小于m×n),若仍采用二维数组存放,就会有 很多存储单元都存放的是0,这不仅造成存储空间的浪费,而 且给运算带来了很大的不便,这时,就很希望能够只存储部分 有效元素。另外,还有一些矩阵的数据元素分布有一定规律, 如三角矩阵、对称矩阵、带状矩阵等,那么就可以利用这些规 律,只存储部分元素,从而提高存储空间的利用率。这些问题 都需要对矩阵进行压缩存储,一般的压缩原则是:对有规律的 元素和值相同的元素只分配一个存储空间,对零元素不分配空 间。 数据结构(C语言版) 5.3.1 三角矩阵 三角矩阵可以分为三类:下三角矩阵、上三角矩阵、对称矩 阵。 对于一个n阶矩阵A来说,若当ij时,有aij=0或aij=c(c 为常数),则称此矩阵为上三角矩阵;若矩阵中的所有元素均满 足aij=aji,则称此矩阵为对称矩阵。 例如,图5.4就是一个n×n的下三角矩阵,我们以此为例来 讨论三角矩阵的压缩存储。 5.3.1 三角矩阵 三角矩阵可以分为三类:下三角矩阵、上三角矩阵、对称矩 阵。 对于一个n阶矩阵A来说,若当ij时,有aij=0或aij=c(c 为常数),则称此矩阵为上三角矩阵;若矩阵中的所有元素均满 足aij=aji,则称此矩阵为对称矩阵。 例如,图5.4就是一个n×n的下三角矩阵,我们以此为例来 讨论三角矩阵的压缩存储。 数据结构(C语言版) a00 a10 a20 … an- 1,0 a11 a21 … an- 1,1 a22 … an- 1,2 … … an- 1,n- 1 0 A= a00 a10 a20 … an- 1,0 a11 a21 … an- 1,1 a22 … an- 1,2 … … an- 1,n- 1 0 A= 图5.4 n×n的下三角矩阵A 数据结构(C语言版) 对于下三角矩阵,只需存储下三角的非零元素,对于零元素 则不存储。若以行序为主序进行存储,得到的序列是: a00,a10,a11,a20,…,an-1,0,an-1,1,…,an-1,n-1 由于下三角矩阵的元素个数为n(n+1)/2,即 第0行:1个 第1行:2个 第2行:3个 …… 第n-1行:n个 对于下三角矩阵,只需存储下三角的非零元素,对于零元素 则不存储。若以行序为主序进行存储,得到的序列是: a00,a10,a11,a20,…,an-1,0,an-1,1,…,an-1,n-1 由于下三角矩阵的元素个数为n(n+1)/2,即 第0行:1个 第1行:2个 第2行:3个 …… 第n-1行:n个 数据结构(C语言版) 共有1+2+3+…+n=n(n+1)个,所以可将三角矩阵压缩到一 个大小为n(n+1)/2的一维数组C中,如图5.5所示。 Loc[i,j] 0 1 2 3 4 5 … n(n+1)/2-1 数组C a00 a10 a11 a20 a21 a22 … an-1,n-1数组C a00 a10 a11 a20 a21 a22 … an-1,n-1 图5.5 三角矩阵的压缩存储 数据结构(C语言版) 假设每个数据元素占size个存储单元,下三角矩阵中任意 元素aij在一维数组C中的存储位置可通过下式计算: Loc[i,j]=Loc[0,0]+ ×size (i≥j)÷ ø öç è æ ++ j2 )1i(i 也就是说,如果C中数据元素C[k]的下标为k,则k与下三 角矩阵中数据元素aij的下标i、j之间的关系为: 也就是说,如果C中数据元素C[k]的下标为k,则k与下三 角矩阵中数据元素aij的下标i、j之间的关系为: k= +j2 )1i(i + 数据结构(C语言版) 若上三角部分为一常数c,则数组的大小可设为C[n(n+1)/2+1], 其中C[n(n+1)/2]存放常数c。 例如,一个5×5的下三角矩阵A,我们可以用一维数组 C[15]对其进行压缩存储,如图5.6所示。 ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é = 75928 00647 00184 00026 00003 A ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é = 75928 00647 00184 00026 00003 A 0 1 2 3 4 5 6 7 8 9 1 0 1 1 1 2 1 3 1 4 3 6 2 4 8 1 7 4 6 0 8 2 9 5 7 图5.6 5×5的下三角矩阵的压缩存储 数据结构(C语言版) 同理,对上三角矩阵,只需存储上三角的非零元素,对于 零 元 素 则 不 存 储,同样可以将其压缩存储到一个大小为 n(n+1)/2的一维数组中。若以行序为主序进行存储,得到的序 列是: a00,a01,a02,…,a0,n-1,a1,n-1,…,an-1,n-1 其中元素aij(i≤j)在数组C中的存储位置为: 同理,对上三角矩阵,只需存储上三角的非零元素,对于 零 元 素 则 不 存 储,同样可以将其压缩存储到一个大小为 n(n+1)/2的一维数组中。若以行序为主序进行存储,得到的序 列是: a00,a01,a02,…,a0,n-1,a1,n-1,…,an-1,n-1 其中元素aij(i≤j)在数组C中的存储位置为: k= +j-i (i≤j)2 )1in2(i +- 数据结构(C语言版) 对于对称矩阵,则可以只存储上三角部分,也可只存储下 三角部分,将其压缩存储到一个大小为n(n+1)/2的一维数组中。 若只存储下三角部分,那么元素aij在数组C中的存储位置为: k= +j ( i≥j) k= +i (i≤j) 2 )1i(i +k= +j ( i≥j) k= +i (i≤j) 2 )1i(i + 2 )1j(j + 数据结构(C语言版) 5.3.2 稀疏矩阵 设m×n矩阵中有t个非零元素,且t<mu=A.nu;B->nu=A.mu;B->tu=A.tu; /*稀疏矩阵的行、列、元素个数*/ if (B->tu>0) /*有非零元素则转换*/ 附设一个位置计数器j,用于指向当前转置后元素应放入三元 组表B.data中的位置。处理完一个元素后,j加1,j的初值为1。 具体转置算法如下: /*算法描述5.2 稀疏矩阵转置*/ void TransTSMatrix1(TSMatrix A,TSMatrix *B) /*为了能使调用函数得到转置结果,将B设为指针类型*/ { int i,j,k; B->mu=A.nu;B->nu=A.mu;B->tu=A.tu; /*稀疏矩阵的行、列、元素个数*/ if (B->tu>0) /*有非零元素则转换*/ 数据结构(C语言版) { j=1; for (k=1; col<=A.nu; k++) /*按A的列序转换*/ for (i=1; i<= (A.tu); i++) /*扫描整个三元组表*/ if (A.data[i].col==k ) { B->data[j].row= A.data[i].col ; B->data[j].col= A.data[i].row ; B->data[j].v= A.data[i].v; j++; } } } { j=1; for (k=1; col<=A.nu; k++) /*按A的列序转换*/ for (i=1; i<= (A.tu); i++) /*扫描整个三元组表*/ if (A.data[i].col==k ) { B->data[j].row= A.data[i].col ; B->data[j].col= A.data[i].row ; B->data[j].v= A.data[i].v; j++; } } } 数据结构(C语言版) 分析该算法,其时间主要耗费在col和p的二重循环上, 所以时间复杂性为O(n×t), (设m、n是原矩阵的行、列,t是稀疏矩阵的非零元素个 数),显然当非零元素的个数t和m×n同数量级时,算法的时 间复杂度为O(m×n2),和通常存储方式下矩阵转置算法相比, 可能节约了一定量的存储空间,但算法的时间性能更差一些。 分析该算法,其时间主要耗费在col和p的二重循环上, 所以时间复杂性为O(n×t), (设m、n是原矩阵的行、列,t是稀疏矩阵的非零元素个 数),显然当非零元素的个数t和m×n同数量级时,算法的时 间复杂度为O(m×n2),和通常存储方式下矩阵转置算法相比, 可能节约了一定量的存储空间,但算法的时间性能更差一些。 数据结构(C语言版) 方法二: 算法5.2的效率低的原因是算法从A的三元组表中寻找第一 列的、第二列的……直至最后一列的非零元素时,要反复搜索 A表多次,若能直接确定A中每一三元组在B中的位置,则对A 的三元组表扫描一次即可。这是可以做到的,因为A中第一列 的第一个非零元素一定存储在B.data[1],如果还知道第一列的 非零元素的个数,那么第二列的第一个非零元素在B.data中的 位置便等于第一列的第一个非零元素在B.data中的位置加上第 一列的非零元素的个数,依次类推,因为A中三元组的存放顺 序是先行后列,对同一行来说,必定先遇到列号小的元素,这 样只需扫描一遍A.data即可。 方法二: 算法5.2的效率低的原因是算法从A的三元组表中寻找第一 列的、第二列的……直至最后一列的非零元素时,要反复搜索 A表多次,若能直接确定A中每一三元组在B中的位置,则对A 的三元组表扫描一次即可。这是可以做到的,因为A中第一列 的第一个非零元素一定存储在B.data[1],如果还知道第一列的 非零元素的个数,那么第二列的第一个非零元素在B.data中的 位置便等于第一列的第一个非零元素在B.data中的位置加上第 一列的非零元素的个数,依次类推,因为A中三元组的存放顺 序是先行后列,对同一行来说,必定先遇到列号小的元素,这 样只需扫描一遍A.data即可。 数据结构(C语言版) 根据这个思路,需引入两个辅助数组num[n+1]和pos[n+1]来 实现,num[col]表示矩阵A中第col列的非零元素的个数(为了方 便均从1单元用起),pos[col]初始值表示矩阵A中的第col列的第 一个非零元素在B.data中的位置。于是pos的初始值为: pos[1]=1; pos[col]=pos[col-1]+num[col-1]; 2≤col≤n 例如,对于图5.11所示的矩阵A,num 和pos的值如图5.12 所示。 根据这个思路,需引入两个辅助数组num[n+1]和pos[n+1]来 实现,num[col]表示矩阵A中第col列的非零元素的个数(为了方 便均从1单元用起),pos[col]初始值表示矩阵A中的第col列的第 一个非零元素在B.data中的位置。于是pos的初始值为: pos[1]=1; pos[col]=pos[col-1]+num[col-1]; 2≤col≤n 例如,对于图5.11所示的矩阵A,num 和pos的值如图5.12 所示。 数据结构(C语言版) num[col] 2 2 1 0 0 1 1 pos[col] 1 3 5 6 6 6 7 col 1 2 3 4 5 6 7 pos[col] 1 3 5 6 6 6 7 图5.12 矩阵A的num与pos值 数据结构(C语言版) 依次扫描A.data,当扫描到一个col列元素时,直接将其存 放在B.data的pos[col]位置上,pos[col]加1,pos[col]中始终是 下一个col列元素在B.data中的位置。 下面按以上思路改进转置算法。 /*算法描述5.3 稀疏矩阵的快速转置*/ void TransTSMatrix2(TSMatrix A,TSMatrix *B) { int i,j,k,col; int num[n+1],pos[n+1]; B->mu=A.nu; B->nu=A.mu; B->tu=A.tu; 依次扫描A.data,当扫描到一个col列元素时,直接将其存 放在B.data的pos[col]位置上,pos[col]加1,pos[col]中始终是 下一个col列元素在B.data中的位置。 下面按以上思路改进转置算法。 /*算法描述5.3 稀疏矩阵的快速转置*/ void TransTSMatrix2(TSMatrix A,TSMatrix *B) { int i,j,k,col; int num[n+1],pos[n+1]; B->mu=A.nu; B->nu=A.mu; B->tu=A.tu; 数据结构(C语言版) /*稀疏矩阵的行、列、元素个数*/ if (B->tu) /*有非零元素则转换*/ { for (col=1; col<=A.nu; col++) num[j]=0; for (k=1; k<=A.tu; k++) /*求矩阵A中每一列非零元素的个数*/ num[A.data[i].col ]++; pos[1]=1; /*求矩阵A中每一列第一个非零元 素在B.data中的位置*/ for (col=2; col<=A.nu; col++) pos[col]= pos[col-1]+num[col-1]; for (i=1; i<= A.tu; i++) /*扫描三元组表*/ /*稀疏矩阵的行、列、元素个数*/ if (B->tu) /*有非零元素则转换*/ { for (col=1; col<=A.nu; col++) num[j]=0; for (k=1; k<=A.tu; k++) /*求矩阵A中每一列非零元素的个数*/ num[A.data[i].col ]++; pos[1]=1; /*求矩阵A中每一列第一个非零元 素在B.data中的位置*/ for (col=2; col<=A.nu; col++) pos[col]= pos[col-1]+num[col-1]; for (i=1; i<= A.tu; i++) /*扫描三元组表*/ 数据结构(C语言版) { col=A.data[i].col; /*当前三元组的列号*/ j=pos[col]; /*当前三元组在B.data中的位置*/ B->data[j].col= A.data[i].row ; B->data[j].row= A.data[i].col; B->data[j].v= A.data[i].v; pos[col]++; } } } { col=A.data[i].col; /*当前三元组的列号*/ j=pos[col]; /*当前三元组在B.data中的位置*/ B->data[j].col= A.data[i].row ; B->data[j].row= A.data[i].col; B->data[j].v= A.data[i].v; pos[col]++; } } } 数据结构(C语言版) 分析这个算法的时间复杂度:这个算法中有四个循环,分 别执行n,t,n-1,t次,在每个循环中,每次迭代的时间是一 常量,因此总的计算量是O(n+t)。当然它所需要的存储空间比 前一个算法多了两个辅助数组。 数据结构(C语言版) 3. 稀疏矩阵的十字链表存储 三元组表可以看作稀疏矩阵顺序存储,但是在做一些操作 (如加法、乘法)时,非零项数目及非零元素的位置会发生变化, 这时这种表示就十分不便。下面我们介绍稀疏矩阵的一种链式 存储结构——十字链表,它同样具备链式存储的特点,因此, 在某些情况下,采用十字链表表示稀疏矩阵是很方便的。 3. 稀疏矩阵的十字链表存储 三元组表可以看作稀疏矩阵顺序存储,但是在做一些操作 (如加法、乘法)时,非零项数目及非零元素的位置会发生变化, 这时这种表示就十分不便。下面我们介绍稀疏矩阵的一种链式 存储结构——十字链表,它同样具备链式存储的特点,因此, 在某些情况下,采用十字链表表示稀疏矩阵是很方便的。 数据结构(C语言版) 用十字链表表示稀疏矩阵时每个非零元素存储为一个结点, 结点由5个域组成,其结构如图5.13表示。 (1)  row:存储非零元素的行号; (2)  col:存储非零元素的列号; (3)  value:存储本元素的值; (4)  right:链接同一行的下一个非零元素; (5)  down:链接同一列的下一个非零元素。 用十字链表表示稀疏矩阵时每个非零元素存储为一个结点, 结点由5个域组成,其结构如图5.13表示。 (1)  row:存储非零元素的行号; (2)  col:存储非零元素的列号; (3)  value:存储本元素的值; (4)  right:链接同一行的下一个非零元素; (5)  down:链接同一列的下一个非零元素。 数据结构(C语言版) row col value down right 图5.13 十字链表中的结点结构 数据结构(C语言版) 稀疏矩阵中每一行的非零元素结点按其列号从小到大的顺 序由right域链成一个带表头结点的单向行链表,同样每一列中 的非零元素按其行号从小到大的顺序由down域也链成一个带表 头结点的单向列链表,即每个非零元素aij既是第i行链表中的一 个结点,又是第j列链表中的一个结点。这好像是处在一个十字 交叉路上,所以称其为十字链表。同时再附设一个存放所有行 链表的头指针的一维数组和一个存放所有列链表的头指针的一 维数组。如图5.14是一个稀疏矩阵和其对应的十字链表。 稀疏矩阵中每一行的非零元素结点按其列号从小到大的顺 序由right域链成一个带表头结点的单向行链表,同样每一列中 的非零元素按其行号从小到大的顺序由down域也链成一个带表 头结点的单向列链表,即每个非零元素aij既是第i行链表中的一 个结点,又是第j列链表中的一个结点。这好像是处在一个十字 交叉路上,所以称其为十字链表。同时再附设一个存放所有行 链表的头指针的一维数组和一个存放所有列链表的头指针的一 维数组。如图5.14是一个稀疏矩阵和其对应的十字链表。 数据结构(C语言版) 1 1 3 1 4 5 2 2 1 3 1 3 3 0 0 5 0 1 0 0 2 0 0 0 M= 1 1 3 1 4 5 2 2 1 3 1 3 3 0 0 5 0 1 0 0 2 0 0 0 M= 图5.14 稀疏矩阵和对应的十字链表 数据结构(C语言版) 十字链表的结构类型说明如下: typedef struct OLNode { int row, col; /*非零元素的行和列下标*/ datatype value; struct OLNode *down , *right; /*非零元素所在的行表、列表的后继链域*/ }OLNode; *Olink; typedef struct { Olink *row_head; *col_head; /*行、列链表的头指针*/ int mu,nu,tu; /*稀疏矩阵的行数、列数和非零元素的个数*/ }CrossLink; 十字链表的结构类型说明如下: typedef struct OLNode { int row, col; /*非零元素的行和列下标*/ datatype value; struct OLNode *down , *right; /*非零元素所在的行表、列表的后继链域*/ }OLNode; *Olink; typedef struct { Olink *row_head; *col_head; /*行、列链表的头指针*/ int mu,nu,tu; /*稀疏矩阵的行数、列数和非零元素的个数*/ }CrossLink; 数据结构(C语言版) 5.4 实习:数组应用实例 我们可以用一个二维数组maze[m+2][n+2]表示迷宫,其中 元素0表示走得通,1表示走不通。为了叙述上的方便,设定由 maze [1][1]进入迷宫,由maze [m][n]走出迷宫。迷宫中任意一点 的位置可由maze [row][col]来表示。在MAZE[row][col]的周围有 八 个 方 向 可 走 , 为 了 避 免 检 测 边 界 状 态 , 二 维 数 组 maze [m+2][n+2]的零行、零列及m+1行、n+1列的值均为1。另外, 为了简化计算,设一个二维数组move[8][2]记录可走的八个方向 坐标增量。move [k][0]表示第k个方向上row的增量,move [k][1] 表示第k个方向上col的增量。例如,从maze[row][col]出发,沿 东南的方向达到下一个位置maze[i][j]为: 我们可以用一个二维数组maze[m+2][n+2]表示迷宫,其中 元素0表示走得通,1表示走不通。为了叙述上的方便,设定由 maze [1][1]进入迷宫,由maze [m][n]走出迷宫。迷宫中任意一点 的位置可由maze [row][col]来表示。在MAZE[row][col]的周围有 八 个 方 向 可 走 , 为 了 避 免 检 测 边 界 状 态 , 二 维 数 组 maze [m+2][n+2]的零行、零列及m+1行、n+1列的值均为1。另外, 为了简化计算,设一个二维数组move[8][2]记录可走的八个方向 坐标增量。move [k][0]表示第k个方向上row的增量,move [k][1] 表示第k个方向上col的增量。例如,从maze[row][col]出发,沿 东南的方向达到下一个位置maze[i][j]为: 数据结构(C语言版) i=row+move[1][0]=row+1 j=row+move[1][1]=col+1 为了避免走重复路径,我们用mark[m+2][n+2]来记走过 的路径。它的初值为0,一旦达到某个位置maze[row][col],就 把mark[row][col]置为1。 i=row+move[1][0]=row+1 j=row+move[1][1]=col+1 为了避免走重复路径,我们用mark[m+2][n+2]来记走过 的路径。它的初值为0,一旦达到某个位置maze[row][col],就 把mark[row][col]置为1。 数据结构(C语言版) 我们规定计算机走迷宫时,每次都从正东的方向起顺时针 检测,当检测到某个方向下一个位置的值为0,并且没有走过 这一位置时,就沿此方向走一步。这样依次重复检测,当某一 步七个可前进方向都为1时,则退回一步,重新检测下一个方 向。因此,我们必须让计算机记下所走过的路径和方向,以便 退到刚走过的方位,并继续检测下一个方位。我们可以设置一 个栈stack来记每一步的坐标row、col。 我们规定计算机走迷宫时,每次都从正东的方向起顺时针 检测,当检测到某个方向下一个位置的值为0,并且没有走过 这一位置时,就沿此方向走一步。这样依次重复检测,当某一 步七个可前进方向都为1时,则退回一步,重新检测下一个方 向。因此,我们必须让计算机记下所走过的路径和方向,以便 退到刚走过的方位,并继续检测下一个方位。我们可以设置一 个栈stack来记每一步的坐标row、col。 数据结构(C语言版) 下面是一个7×10的迷宫实例。 #include main() { int maze[9][12] = { 1, 1,1,1,1,1,1,1,1,1,1, 1, 1, 0,1,0,0,0,1,1,0,1,1, 1, 1, 1,0,1,1,0,0,0,1,1,0, 1, 1, 1,0,1,1,0,1,1,0,0,1, 1, 1, 1,1,1,1,0,0,1,1,1,1, 1, 1, 0,0,1,1,1,1,0,1,1,0, 1, 1, 0,1,1,0,0,0,1,1,0,1, 1, 1, 1,0,1,1,0,1,0,0,1,0, 1, 1, 1,1,1,1,1,1,1,1,1,1, 1 }; 下面是一个7×10的迷宫实例。 #include main() { int maze[9][12] = { 1, 1,1,1,1,1,1,1,1,1,1, 1, 1, 0,1,0,0,0,1,1,0,1,1, 1, 1, 1,0,1,1,0,0,0,1,1,0, 1, 1, 1,0,1,1,0,1,1,0,0,1, 1, 1, 1,1,1,1,0,0,1,1,1,1, 1, 1, 0,0,1,1,1,1,0,1,1,0, 1, 1, 0,1,1,0,0,0,1,1,0,1, 1, 1, 1,0,1,1,0,1,0,0,1,0, 1, 1, 1,1,1,1,1,1,1,1,1,1, 1 }; 数据结构(C语言版) int move[8][2] = { 0,1, 1,1, 1,0, 1,-1, 0,-1,-1,-1,-1,0,-1,1 }; /*move为下一步行走方向,值为坐标增量*/ int stack[64][2], mark[9][12] = { 0 }; /*stack为栈,记录路径;mark为标记,记录已走过的点*/ int top=1, row=1, col=1, k=0; /*k标记方向,从正东开始*/ int i,j; mark[row][col]=1; /*第一步*/ stack[top][0]=row; stack[top][1]=col; while (!(row==7&&col==10)&&!top==0) int move[8][2] = { 0,1, 1,1, 1,0, 1,-1, 0,-1,-1,-1,-1,0,-1,1 }; /*move为下一步行走方向,值为坐标增量*/ int stack[64][2], mark[9][12] = { 0 }; /*stack为栈,记录路径;mark为标记,记录已走过的点*/ int top=1, row=1, col=1, k=0; /*k标记方向,从正东开始*/ int i,j; mark[row][col]=1; /*第一步*/ stack[top][0]=row; stack[top][1]=col; while (!(row==7&&col==10)&&!top==0) 数据结构(C语言版) { i=row+move[k][0]; j=col+move[k][1]; /*下一步*/ if (maze[i][j]==0&&mark[i][j]==0) { top=top+1; k=0; row=i; col=j; /*可行走*/ stack[top][0]=row; stack[top][1]=col; mark[row][col]=1; } else { k++; /*不可行走, 看下一个方向*/ if (k==8) { k=0; top--; row=stack[top][0]; col=stack[top][1]; }/*八个方向全部不通*/ } } { i=row+move[k][0]; j=col+move[k][1]; /*下一步*/ if (maze[i][j]==0&&mark[i][j]==0) { top=top+1; k=0; row=i; col=j; /*可行走*/ stack[top][0]=row; stack[top][1]=col; mark[row][col]=1; } else { k++; /*不可行走, 看下一个方向*/ if (k==8) { k=0; top--; row=stack[top][0]; col=stack[top][1]; }/*八个方向全部不通*/ } } 数据结构(C语言版) if (top==0) printf("NO PATH!!!\n"); else { printf("ALL PATH : "); for(i=1;i lch=s[i];如果i为奇数,则 它是双亲结点的右孩子,即让s[j]->rch =s[i]。这样就将新输入 的结点逐一与其双亲结点相连,生成二叉树。 当 结点编号i>1时,产生一个新的结点之后,也要将指向 该结点的指针存入s[i]。由 性质5可知:j=i/2为它的双亲结点编号。如果i为偶数,则 它是双亲结点的左孩子,即让s [j]-> lch=s[i];如果i为奇数,则 它是双亲结点的右孩子,即让s[j]->rch =s[i]。这样就将新输入 的结点逐一与其双亲结点相连,生成二叉树。 数据结构(C语言版) i x 1 2 3 4 6 9 11 12 13 14 15 16 13 16 1 2 3 4 6 9 (a) (b) 14 15 12 11 i x 1 2 3 4 6 9 11 12 13 14 15 16 13 16 1 2 3 4 6 9 (a) (b) 14 15 12 11 图6.10 二叉树及数据表 数据结构(C语言版) 结点结构如前所描述,为struct treenode2。辅助向量为struct treenode2 *s[20]。二叉树生成算法如下: /*算法描述6.1 二叉树生成算法*/ stuct treenode2 * creat() int i,x; struct treenode2 *q, *t; {printf("i,x="); scanf("%d,%d",&i,&x); while ((i!=0)&&(x!=0)) {q=(struct treenode2 *) malloc(sizeof(struct treenode2)) /*产生一个结点*/ 结点结构如前所描述,为struct treenode2。辅助向量为struct treenode2 *s[20]。二叉树生成算法如下: /*算法描述6.1 二叉树生成算法*/ stuct treenode2 * creat() int i,x; struct treenode2 *q, *t; {printf("i,x="); scanf("%d,%d",&i,&x); while ((i!=0)&&(x!=0)) {q=(struct treenode2 *) malloc(sizeof(struct treenode2)) /*产生一个结点*/ 数据结构(C语言版) q->data=x;q->lch=NULL;q->rch=NULL; s[i]=q; if(i= =1) t=q; /*t为局部变量,代表树根结点*/ else{j=i/2; /*双亲结点编号*/ if(i%2)= =0) s[j]->lch=q;else s[j]->rch=q; } printf("i,x=");scanf("%d%d",&i,&x); } return(t); } /*creat end*/ q->data=x;q->lch=NULL;q->rch=NULL; s[i]=q; if(i= =1) t=q; /*t为局部变量,代表树根结点*/ else{j=i/2; /*双亲结点编号*/ if(i%2)= =0) s[j]->lch=q;else s[j]->rch=q; } printf("i,x=");scanf("%d%d",&i,&x); } return(t); } /*creat end*/ 数据结构(C语言版) 6.4 遍历二叉树 在二叉树的应用中,常常需要在树中搜索具有某种特征的结 点,或对树中全部的结点逐一进行处理。这就涉及到一个遍历 二叉树的问题。遍历二叉树是指以一定的次序访问二叉树中的 每个结点,并且每个结点仅被访问一次。所谓访问结点,就是 指对结点进行各种操作。例如,查询结点数据域的内容,或输 出它的值,或找出结点位置,或执行对结点的其他操作。遍历 二叉树的过程实质是把二叉树的结点进行线性排列的过程。对 于线性结构来说,遍历很容易实现,顺序扫描结构中的每个数 据元素即可。但二叉树是非线性结构,遍历时是先访问根结点 还是先访问子树,是先访问左子树还是先访问右子树必须有所 规定,这就是遍历规则。采用不同的遍历规则会产生不同的遍 历结果,因此必须人为设定遍历规则。 在二叉树的应用中,常常需要在树中搜索具有某种特征的结 点,或对树中全部的结点逐一进行处理。这就涉及到一个遍历 二叉树的问题。遍历二叉树是指以一定的次序访问二叉树中的 每个结点,并且每个结点仅被访问一次。所谓访问结点,就是 指对结点进行各种操作。例如,查询结点数据域的内容,或输 出它的值,或找出结点位置,或执行对结点的其他操作。遍历 二叉树的过程实质是把二叉树的结点进行线性排列的过程。对 于线性结构来说,遍历很容易实现,顺序扫描结构中的每个数 据元素即可。但二叉树是非线性结构,遍历时是先访问根结点 还是先访问子树,是先访问左子树还是先访问右子树必须有所 规定,这就是遍历规则。采用不同的遍历规则会产生不同的遍 历结果,因此必须人为设定遍历规则。 数据结构(C语言版) 由于一棵非空二叉树是由根结点、左子树和右子树三个基 本部分组成的,遍历二叉树时只要按顺序依次遍历这三部分 即可。假定我们以D、L、R分别表示访问根结点、遍历左子 树和遍历右子树,则可以有六种遍历形式:DLR、LDR、 LRD、DRL、RDL、RLD,若依习惯规定先左后右,则上述 六种形式可归并为三种形式,即: DLR 先根遍历 LDR 中根遍历 LRD 后根遍历 由于一棵非空二叉树是由根结点、左子树和右子树三个基 本部分组成的,遍历二叉树时只要按顺序依次遍历这三部分 即可。假定我们以D、L、R分别表示访问根结点、遍历左子 树和遍历右子树,则可以有六种遍历形式:DLR、LDR、 LRD、DRL、RDL、RLD,若依习惯规定先左后右,则上述 六种形式可归并为三种形式,即: DLR 先根遍历 LDR 中根遍历 LRD 后根遍历 数据结构(C语言版) 二叉树的链表存储,其结点结构如下: struct treenode2 {char data;/*假设数据类型char,根据需要也可为其他类型*/ struct treenode2 *lch,*rch; } 二叉树的链表存储,其结点结构如下: struct treenode2 {char data;/*假设数据类型char,根据需要也可为其他类型*/ struct treenode2 *lch,*rch; } 数据结构(C语言版) 6.4.1 先根遍历 先根遍历可以递归地描述如下: 如果根不空,则依次执行① 访问根结点,② 按先根次序遍 历左子树,③ 按先根次序遍历右子树,否则返回。 先根遍历的递归算法如下: 6.4.1 先根遍历 先根遍历可以递归地描述如下: 如果根不空,则依次执行① 访问根结点,② 按先根次序遍 历左子树,③ 按先根次序遍历右子树,否则返回。 先根遍历的递归算法如下: 数据结构(C语言版) /*算法描述6.2 先根遍历的递归算法*/ void preorder(struct treenode2 * p) { if(p!=NULL) {printf("%c\n",p->data);/*访问根结点*/ preorder(p->lch);/*按先根次序遍历左子树*/ preorder(p->rch);/*按先根次序遍历右子树*/ } } /*preorder*/ /*算法描述6.2 先根遍历的递归算法*/ void preorder(struct treenode2 * p) { if(p!=NULL) {printf("%c\n",p->data);/*访问根结点*/ preorder(p->lch);/*按先根次序遍历左子树*/ preorder(p->rch);/*按先根次序遍历右子树*/ } } /*preorder*/ 数据结构(C语言版) EF D C B A CB A (a) (b) EF D C B A CB A (a) (b) 图6.11 遍历序列示例 数据结构(C语言版) 6.4.2 中根遍历 中根遍历可以递归地描述如下: 如果根不空,则依次执行① 按中根次序遍历左子树,② 访 问根结点,③ 按中根次序遍历右子树,否则返回。 中根遍历递归算法如下: 6.4.2 中根遍历 中根遍历可以递归地描述如下: 如果根不空,则依次执行① 按中根次序遍历左子树,② 访 问根结点,③ 按中根次序遍历右子树,否则返回。 中根遍历递归算法如下: 数据结构(C语言版) /*算法描述6.3 中根遍历的递归算法*/ void inorder(struct treenode2 *p) {if (p!=NULL) {inorder(p->lch);/*中根遍历左子树*/ printf("%c\n",p->data);/*访问根结点*/ inorder(p->rch);/*中根遍历右子树*/ } }/*inorder*/ 例如,图6.11(a)所示二叉树的中根遍历序列为BAC, 图 6.11(b)所示二叉树的中根遍历序列为BCAEDF。 /*算法描述6.3 中根遍历的递归算法*/ void inorder(struct treenode2 *p) {if (p!=NULL) {inorder(p->lch);/*中根遍历左子树*/ printf("%c\n",p->data);/*访问根结点*/ inorder(p->rch);/*中根遍历右子树*/ } }/*inorder*/ 例如,图6.11(a)所示二叉树的中根遍历序列为BAC, 图 6.11(b)所示二叉树的中根遍历序列为BCAEDF。 数据结构(C语言版) 6.4.3 后根遍历 后根遍历可以递归地描述如下: 如果根不空,则依次执行① 按后根次序遍历左子树,② 按 后根次序遍历右子树,③ 访问根结点,否则返回。 后根遍历递归算法如下: 6.4.3 后根遍历 后根遍历可以递归地描述如下: 如果根不空,则依次执行① 按后根次序遍历左子树,② 按 后根次序遍历右子树,③ 访问根结点,否则返回。 后根遍历递归算法如下: 数据结构(C语言版) /*算法描述6.4 后根遍历的递归算法*/ void postorder (stuct treenode2 * p ) { if ( p!= NULL ) { postorder ( p->lch);/*后根遍历左子树*/ postorder ( p->rch );/*后根遍历右子树*/ printf ("%c\n",p->data);/*访问根结点*/ } }/*postorder*/ 例如,图6.11(a)所示二叉树的后根遍历序列为BCA, 图6.11(b)所 示二叉树的后根遍历序列为CBEFDA。 /*算法描述6.4 后根遍历的递归算法*/ void postorder (stuct treenode2 * p ) { if ( p!= NULL ) { postorder ( p->lch);/*后根遍历左子树*/ postorder ( p->rch );/*后根遍历右子树*/ printf ("%c\n",p->data);/*访问根结点*/ } }/*postorder*/ 例如,图6.11(a)所示二叉树的后根遍历序列为BCA, 图6.11(b)所 示二叉树的后根遍历序列为CBEFDA。 数据结构(C语言版) 6.4.4 二叉树遍历算法的应用 1.统计二叉树中结点个数m和叶子结点个数n 算法思路分析:在调用遍历算法时设计两个计数器变量m、 n。我们知道,所谓遍历二叉树,即以某种次序去访问二叉树的 每个结点,且每个结点仅访问一次,这就提供了方便的条件。 每当访问一个结点时,在原访问语句printf 后边,加上一计数器 语句m ++和一个判断该结点是否为叶子的语句,便可解决问题。 在这里,所谓的访问结点操作已拓宽为一组语句,见下列算法 的第4、5、6行。 1.统计二叉树中结点个数m和叶子结点个数n 算法思路分析:在调用遍历算法时设计两个计数器变量m、 n。我们知道,所谓遍历二叉树,即以某种次序去访问二叉树的 每个结点,且每个结点仅访问一次,这就提供了方便的条件。 每当访问一个结点时,在原访问语句printf 后边,加上一计数器 语句m ++和一个判断该结点是否为叶子的语句,便可解决问题。 在这里,所谓的访问结点操作已拓宽为一组语句,见下列算法 的第4、5、6行。 数据结构(C语言版) 假设用中根遍历方法统计叶子结点的个数,算法如下: /*算法描述6.5 中根遍历方法统计叶子结点的个数*/ void injishu (struct treenode2 * t ) { if (t != NULL) {injishu ( t->lch ) ; /*中根遍历左子树*/ printf ("%c\n", t->data ) ; /*访问根结点*/ m ++ ; /*结点计数*/ if (( t->lch = =NULL) && (t->rch = = NULL )) n ++ ; /*叶子结点计数*/ injishu ( t->rch ) ; /*中根遍历右子树*/ } } /*injishu*/ 假设用中根遍历方法统计叶子结点的个数,算法如下: /*算法描述6.5 中根遍历方法统计叶子结点的个数*/ void injishu (struct treenode2 * t ) { if (t != NULL) {injishu ( t->lch ) ; /*中根遍历左子树*/ printf ("%c\n", t->data ) ; /*访问根结点*/ m ++ ; /*结点计数*/ if (( t->lch = =NULL) && (t->rch = = NULL )) n ++ ; /*叶子结点计数*/ injishu ( t->rch ) ; /*中根遍历右子树*/ } } /*injishu*/ 数据结构(C语言版) 如果数据域类型不是字符型而是整型,语句应该为“printf ("%d\n",t->data);”。假设数据域类型更为复杂,则应结合具体 实际重新设计输出模块。上面函数中m、n是全局变量,在主程 序先置0,在调用injishu函数结束后,m值便是结点总个数,n值 便是叶子结点的个数。主函数示意如下: main () {t=creat();/*建立二叉树t,为全局变量*/ m=0,n=0;/*全局变量m,n置初值*/ injishu(t);/*求树中结点总数m,叶子结点个数n*/ printf("m=%d,n=%d",m,n);/*输出结果*/ } 当然,也可用先根或后根遍历方法统计结点个数。 如果数据域类型不是字符型而是整型,语句应该为“printf ("%d\n",t->data);”。假设数据域类型更为复杂,则应结合具体 实际重新设计输出模块。上面函数中m、n是全局变量,在主程 序先置0,在调用injishu函数结束后,m值便是结点总个数,n值 便是叶子结点的个数。主函数示意如下: main () {t=creat();/*建立二叉树t,为全局变量*/ m=0,n=0;/*全局变量m,n置初值*/ injishu(t);/*求树中结点总数m,叶子结点个数n*/ printf("m=%d,n=%d",m,n);/*输出结果*/ } 当然,也可用先根或后根遍历方法统计结点个数。 数据结构(C语言版) 2. 求二叉树的树深 首先看如下算法。 /*算法描述6.6 求二叉树的树深*/ void predeep (struct treenode2 * t ,int i) { if (t != NULL) {printf("%c\n",t->data);/*访问根结点*/ i++; if(k<i)k=i; predeep(t->lch,i); /*先根遍历左子树*/ predeep(t->rch,i); /*先根遍历右子树*/ } } /*predeep*/ 2. 求二叉树的树深 首先看如下算法。 /*算法描述6.6 求二叉树的树深*/ void predeep (struct treenode2 * t ,int i) { if (t != NULL) {printf("%c\n",t->data);/*访问根结点*/ i++; if(k<i)k=i; predeep(t->lch,i); /*先根遍历左子树*/ predeep(t->rch,i); /*先根遍历右子树*/ } } /*predeep*/ 数据结构(C语言版) 可以看出,此算法利用了先根遍历二叉树的思路,只是在 这里“访问结点”操作复杂了一些,如算法中第3、4、5行。 其中k为全局变量,在主程序中置初值0,在调用函数predeep之 后,k值就是树的深度。形参i在主程序调用时,用一个初值为0 的实参代入。当深度递归调用时,i会不断增大,k记下它的值。 当返回时,退到哪一个调用层次,i会保持在本层次的原先较小 值。而在返回时不论退到哪一个调用层次,k将保持较大值不 变。这样,k值就是树深。相应主函数示意如下: 可以看出,此算法利用了先根遍历二叉树的思路,只是在 这里“访问结点”操作复杂了一些,如算法中第3、4、5行。 其中k为全局变量,在主程序中置初值0,在调用函数predeep之 后,k值就是树的深度。形参i在主程序调用时,用一个初值为0 的实参代入。当深度递归调用时,i会不断增大,k记下它的值。 当返回时,退到哪一个调用层次,i会保持在本层次的原先较小 值。而在返回时不论退到哪一个调用层次,k将保持较大值不 变。这样,k值就是树深。相应主函数示意如下: 数据结构(C语言版) main() {t=creat(); /*建立二叉树t,为全局变量*/ k=0;i=0; /*k,i置初值,其中k为全局变量*/ predeep(t,i); /*求树t的深度,i为一个辅助变量*/ printf("k=%d",k); /*输出树深k*/ } main() {t=creat(); /*建立二叉树t,为全局变量*/ k=0;i=0; /*k,i置初值,其中k为全局变量*/ predeep(t,i); /*求树t的深度,i为一个辅助变量*/ printf("k=%d",k); /*输出树深k*/ } 数据结构(C语言版) 6.5 线索二叉树 6.5.1 线索二叉树的基本概念 我们发现,具有n个结点的二叉树中有n – 1条边指向其左、右 孩子,这意味着在二叉链表中的2n个孩子指针域中只用到了n –1 个域,还有另外n+1个指针域是空的。我们可充分利用这些空指 针来存放结点的线性前驱和后继信息。 试作如下规定:若结点有左子树,则其lch域指示其左孩子, 否则令lch域指示其直接前驱;若结点有右子树,则其rch域指示 其右孩子,否则令rch域指示其直接后继。为了严格区分结点的 孩子指针域究竟指向孩子结点还是指向前驱或后继结点,需在 原结点结构中增加两个标志域。新的结点结构为: 6.5.1 线索二叉树的基本概念 我们发现,具有n个结点的二叉树中有n – 1条边指向其左、右 孩子,这意味着在二叉链表中的2n个孩子指针域中只用到了n –1 个域,还有另外n+1个指针域是空的。我们可充分利用这些空指 针来存放结点的线性前驱和后继信息。 试作如下规定:若结点有左子树,则其lch域指示其左孩子, 否则令lch域指示其直接前驱;若结点有右子树,则其rch域指示 其右孩子,否则令rch域指示其直接后继。为了严格区分结点的 孩子指针域究竟指向孩子结点还是指向前驱或后继结点,需在 原结点结构中增加两个标志域。新的结点结构为: 数据结构(C语言版) lch ltag data rtag rch 其中: ltag=0 表示lch指示结点的左孩子 ltag=1 表示lch指示结点的直接前驱 rtag=0 表示rch指示结点的右孩子 rtag=1 表示rch指示结点的直接后继 算法描述为: struct xtreenode {char data; struct xtreenode *lch,*rch; int ltag,rtag; /*左、右标志域*/ } 其中: ltag=0 表示lch指示结点的左孩子 ltag=1 表示lch指示结点的直接前驱 rtag=0 表示rch指示结点的右孩子 rtag=1 表示rch指示结点的直接后继 算法描述为: struct xtreenode {char data; struct xtreenode *lch,*rch; int ltag,rtag; /*左、右标志域*/ } 数据结构(C语言版) 通常把指向前驱或后继的指针称做线索。对二叉树以某种次 序进行遍历并且加上线索的过程称做线索化。经过线索化之后 生成的二叉链表表示称为线索二叉树。 对一个已建好的二叉树的二叉链表进行线索化时规定(对p结 点): (1)  p有左孩子时,则令左特征域p->ltag = 0; (2)  p无左孩子时,令p->ltag = 1, 并且p->lch指向p的前驱结点; (3)  p有右孩子时,令p->rtag = 0; (4)  p无右孩子时,令p->rtag = 1,并且让p->rch指向p的后继结 点。 通常把指向前驱或后继的指针称做线索。对二叉树以某种次 序进行遍历并且加上线索的过程称做线索化。经过线索化之后 生成的二叉链表表示称为线索二叉树。 对一个已建好的二叉树的二叉链表进行线索化时规定(对p结 点): (1)  p有左孩子时,则令左特征域p->ltag = 0; (2)  p无左孩子时,令p->ltag = 1, 并且p->lch指向p的前驱结点; (3)  p有右孩子时,令p->rtag = 0; (4)  p无右孩子时,令p->rtag = 1,并且让p->rch指向p的后继结 点。 数据结构(C语言版) 1 B 0 0 C 1 1 D 1E C D B A 1 E 1 0 A 0 (a) (b) 1 B 0 0 C 1 1 D 1E C D B A 1 E 1 0 A 0 (a) (b) 图6.12 中根次序线索树 (a) 二叉树;(b) 中根次序线索树 数据结构(C语言版) 6.5.2 线索二叉树的逻辑表示图 按照不同的次序进行线索化,可得到不同的线索二叉树, 即先根线索二叉树、中根线索二叉树和后根线索二叉树。对图 6.13(a)所示的二叉树进行线索化,可得到图6.13(b)、(c)、(d)所 示的三种线索二叉树的逻辑表示。 6.5.2 线索二叉树的逻辑表示图 按照不同的次序进行线索化,可得到不同的线索二叉树, 即先根线索二叉树、中根线索二叉树和后根线索二叉树。对图 6.13(a)所示的二叉树进行线索化,可得到图6.13(b)、(c)、(d)所 示的三种线索二叉树的逻辑表示。 数据结构(C语言版) FHI E C G A B D (c) NULL FHI E C G A B D (d) NULL FHI C G A B D (b) E FHI C G A B DE (a) FHI E C G A B D (c) NULL FHI E C G A B D (d) NULL FHI C G A B D (b) E FHI C G A B DE (a) 图6.13 线索二叉树的逻辑表示图 (a) 二叉树;(b) 先根线索二叉树; (c) 中根线索二叉树;(d) 后根线索二叉树 数据结构(C语言版) 6.5.3 中根次序线索化算法 这里重点介绍中根次序线索化的算法。中根次序线索化是在 已建立好的二叉链表之上(每个结点有5个域)按中根遍历的方法 在访问根结点时建立线索。 中根次序线索化递归算法如下: /*算法描述6.7 中根次序线索化递归算法*/ void inthread(struct xtreenode *p) {if (p!=NULL) {inthread(p->lch); 6.5.3 中根次序线索化算法 这里重点介绍中根次序线索化的算法。中根次序线索化是在 已建立好的二叉链表之上(每个结点有5个域)按中根遍历的方法 在访问根结点时建立线索。 中根次序线索化递归算法如下: /*算法描述6.7 中根次序线索化递归算法*/ void inthread(struct xtreenode *p) {if (p!=NULL) {inthread(p->lch); 数据结构(C语言版) printf ("%6c\t",p->data); if(p->lch!=NULL)p->ltag=0; else{p->ltag=1; p->lch=pr; }/*建p结点的左线索,指向前驱结点pr*/ if(pr!=NULL) { if(pr->rch!=NULL) pr->rtag=0; else{ pr->rtag=1; pr->rch=p; }/*前驱结点pr建右线索,指向结点p*/ } pr=p; /*pr跟上p,以便p向后移动*/ inthread(p->rch);  } }/*inthread*/ printf ("%6c\t",p->data); if(p->lch!=NULL)p->ltag=0; else{p->ltag=1; p->lch=pr; }/*建p结点的左线索,指向前驱结点pr*/ if(pr!=NULL) { if(pr->rch!=NULL) pr->rtag=0; else{ pr->rtag=1; pr->rch=p; }/*前驱结点pr建右线索,指向结点p*/ } pr=p; /*pr跟上p,以便p向后移动*/ inthread(p->rch);  } }/*inthread*/ 数据结构(C语言版) 此算法中pr是全局变量,在主程序中置初值为空。在inthread 函数中pr 始终作为当前结点p的前驱结点的指针。在线索化过 程中,边判断二叉树的结点有无左、右孩子,边给相应标志域 置0或1,边建立线索。在阅读此算法时,将inthread(p->lch )和 inthread(p->rch)之间的一组语句看成一个整体,把这段语句理 解为“访问”,很明显这里应用了典型的中根遍历算法思路。 在递归调用结束时p为空,这表明pr已是最后一个结点,应该没 有后继结点。所以在返回主程序后还要使pr->rch=NULL,至此 整个线索化结束。主函数语句示意如下: 此算法中pr是全局变量,在主程序中置初值为空。在inthread 函数中pr 始终作为当前结点p的前驱结点的指针。在线索化过 程中,边判断二叉树的结点有无左、右孩子,边给相应标志域 置0或1,边建立线索。在阅读此算法时,将inthread(p->lch )和 inthread(p->rch)之间的一组语句看成一个整体,把这段语句理 解为“访问”,很明显这里应用了典型的中根遍历算法思路。 在递归调用结束时p为空,这表明pr已是最后一个结点,应该没 有后继结点。所以在返回主程序后还要使pr->rch=NULL,至此 整个线索化结束。主函数语句示意如下: 数据结构(C语言版) main() { pr=NULL; /*全局变量*/ t=creat(); /*建立二叉树*/ inthread(t); /*中根线索化二叉树*/ pr->rch=NULL;/*善后处理*/ } main() { pr=NULL; /*全局变量*/ t=creat(); /*建立二叉树*/ inthread(t); /*中根线索化二叉树*/ pr->rch=NULL;/*善后处理*/ } 数据结构(C语言版) 初学者在这里往往易犯错误,常把预处理pr=NULL和善后 处理pr->rch=NULL放在线索化子函数void inthread (struct xtreenode * p )中,一个放最前面,另一个放最后面。这样每递 归调用一次,pr就置一次空,无法记下p的前驱结点。而在从 深度递归返回时,每返回一次就让pr->rch置一次空,这显然是 错误的。因此,在描述递归算法时,提倡同时写出主函数来示 意递归调用前的初始化处理和递归调用结束后的善后处理。 初学者在这里往往易犯错误,常把预处理pr=NULL和善后 处理pr->rch=NULL放在线索化子函数void inthread (struct xtreenode * p )中,一个放最前面,另一个放最后面。这样每递 归调用一次,pr就置一次空,无法记下p的前驱结点。而在从 深度递归返回时,每返回一次就让pr->rch置一次空,这显然是 错误的。因此,在描述递归算法时,提倡同时写出主函数来示 意递归调用前的初始化处理和递归调用结束后的善后处理。 数据结构(C语言版) 6.5.4 在中根线索树上检索某结点的前驱或后继 1. 已知q结点找出它的前驱结点 根据线索树的基本概念,当q->ltag=1时,q->lch就指向q的 前驱。当q->ltag=0时,表明q有左孩子。由中根遍历的规律可知, 作为根q的前驱结点(或者说以根结点为后继的结点),它应是中 根遍历q的左子树时访问的最后一个结点,即左子树的最右尾结 点。图6.13(b)中,结点D是A的左子树的最右尾结点,它就是A 的前驱结点。而D的后继指针指向了A,A就是D的后继结点。 若用p记录q的前趋,则算法如下: 6.5.4 在中根线索树上检索某结点的前驱或后继 1. 已知q结点找出它的前驱结点 根据线索树的基本概念,当q->ltag=1时,q->lch就指向q的 前驱。当q->ltag=0时,表明q有左孩子。由中根遍历的规律可知, 作为根q的前驱结点(或者说以根结点为后继的结点),它应是中 根遍历q的左子树时访问的最后一个结点,即左子树的最右尾结 点。图6.13(b)中,结点D是A的左子树的最右尾结点,它就是A 的前驱结点。而D的后继指针指向了A,A就是D的后继结点。 若用p记录q的前趋,则算法如下: 数据结构(C语言版) /*算法描述6.8 已知q结点,找出它的前驱结点*/ struct xtreenode *inpre(struct xtreenode *q) { if(q->ltag==1) p=q->lch; else { r=q->lch; while (r->rtag!==1) r=r->rch; p=r; } return(p); } /*算法描述6.8 已知q结点,找出它的前驱结点*/ struct xtreenode *inpre(struct xtreenode *q) { if(q->ltag==1) p=q->lch; else { r=q->lch; while (r->rtag!==1) r=r->rch; p=r; } return(p); } 数据结构(C语言版) 2. 已知q结点找出它的后继结点 当q->rtag=1时,q->rch即指向q的后继结点。若q->rtag=0, 表明q有右孩子,那么q的后继应是中根遍历q的右子树时访问 的第一个结点,即右子树的最左尾结点。图6.13(c)中,A的后 继为F,C的后继为H。依照找前驱结点的算法请读者自己思 考该算法的写法,这里就不再细讲。 2. 已知q结点找出它的后继结点 当q->rtag=1时,q->rch即指向q的后继结点。若q->rtag=0, 表明q有右孩子,那么q的后继应是中根遍历q的右子树时访问 的第一个结点,即右子树的最左尾结点。图6.13(c)中,A的后 继为F,C的后继为H。依照找前驱结点的算法请读者自己思 考该算法的写法,这里就不再细讲。 数据结构(C语言版) 6.5.5 在中根线索树上遍历二叉树 在中根线索树上遍历二叉树,首先从根结点开始查找二叉 树的最左结点,对最左结点进行访问。然后,利用在中根线索 树上求某结点后继的算法,逐一找出每个结点加以访问,直到 某结点的右孩子指针域为空为止。 6.5.5 在中根线索树上遍历二叉树 在中根线索树上遍历二叉树,首先从根结点开始查找二叉 树的最左结点,对最左结点进行访问。然后,利用在中根线索 树上求某结点后继的算法,逐一找出每个结点加以访问,直到 某结点的右孩子指针域为空为止。 数据结构(C语言版) 6.6 二叉树、树和森林 6.6.1 树的存储结构 树的存储结构有顺序结构和链表结构。顺序存储结构即向 量,一般将树结点按自上而下、自左至右的顺序一一存放。如 前文所介绍的完全二叉树就可以采用顺序存储结构。对于一般 树结构更适合使用链表存储结构。常用的有结点定长的多叉链 表和孩子-兄弟二叉链表。 6.6.1 树的存储结构 树的存储结构有顺序结构和链表结构。顺序存储结构即向 量,一般将树结点按自上而下、自左至右的顺序一一存放。如 前文所介绍的完全二叉树就可以采用顺序存储结构。对于一般 树结构更适合使用链表存储结构。常用的有结点定长的多叉链 表和孩子-兄弟二叉链表。 数据结构(C语言版) 图6.14所示的树是一个三叉树,可用三重链表来存储,其 结点结构为:有一个数据域和三个指针域,指针域用于指向该 结点的各个孩子。该树的三重链表如图6.15(a)所示。如果用孩 子-兄弟链表作存储结构,其结点结构为:有一个数据域和两 个指针域,一个指针指向它的长子,另一指针指向它的一个兄 弟。孩子-兄弟链表如图6.15(b)所示。 图6.14所示的树是一个三叉树,可用三重链表来存储,其 结点结构为:有一个数据域和三个指针域,指针域用于指向该 结点的各个孩子。该树的三重链表如图6.15(a)所示。如果用孩 子-兄弟链表作存储结构,其结点结构为:有一个数据域和两 个指针域,一个指针指向它的长子,另一指针指向它的一个兄 弟。孩子-兄弟链表如图6.15(b)所示。 数据结构(C语言版) EF CB A GHI DEF CB A GHI D 图6.14 树 数据结构(C语言版) GHI DEF BC AA B D C E G F HI (a) (b) GHI DEF BC AA B D C E G F HI (a) (b) 图6.15 树的存储结构 (a) 多重链表;(b) 孩子-兄弟链表 数据结构(C语言版) 6.6.2 树与二叉树之间的转换 对于一般树,树中孩子的次序并不重要,只要双亲与孩 子的关系正确即可。但在二叉树中,左、右孩子的次序是严格 区分的。所以在讨论二叉树与一般树之间的转换时,为了不引 起混淆,就约定按树上现有结点次序进行转换。 6.6.2 树与二叉树之间的转换 对于一般树,树中孩子的次序并不重要,只要双亲与孩 子的关系正确即可。但在二叉树中,左、右孩子的次序是严格 区分的。所以在讨论二叉树与一般树之间的转换时,为了不引 起混淆,就约定按树上现有结点次序进行转换。 数据结构(C语言版) 1. 一般树转化为二叉树 将一般树转化为二叉树的思路,主要根据树的孩子-兄弟存 储方式而来,步骤是: (1) 加线:在各兄弟结点之间用虚线相连。可理解为每个结点 的兄弟指针指向它的一个兄弟。 (2) 抹线:对每个结点仅保留它与其最左一个孩子的连线, 抹去该结点与其它孩子之间的连线。可理解为每个结点仅有一 个孩子指针,让它指向自己的长子。 (3) 旋转:把虚线改为实线从水平方向向下旋转45°,成 右斜下方向。原树中实线成左斜下方向。这样就形成一棵二叉 树。 1. 一般树转化为二叉树 将一般树转化为二叉树的思路,主要根据树的孩子-兄弟存 储方式而来,步骤是: (1) 加线:在各兄弟结点之间用虚线相连。可理解为每个结点 的兄弟指针指向它的一个兄弟。 (2) 抹线:对每个结点仅保留它与其最左一个孩子的连线, 抹去该结点与其它孩子之间的连线。可理解为每个结点仅有一 个孩子指针,让它指向自己的长子。 (3) 旋转:把虚线改为实线从水平方向向下旋转45°,成 右斜下方向。原树中实线成左斜下方向。这样就形成一棵二叉 树。 数据结构(C语言版) 由于二叉树中各结点的右孩子都是原一般树中该结点的 兄弟,而一般树的根结点又没有兄弟结点,因此所生成的二 叉树的根结点没有右子树。在所生成的二叉树中某一结点的 左孩子仍是原来树中该结点的长子,并且是它的最左孩子。 图6.16是一个由一般树转为二叉树的实例。 由于二叉树中各结点的右孩子都是原一般树中该结点的 兄弟,而一般树的根结点又没有兄弟结点,因此所生成的二 叉树的根结点没有右子树。在所生成的二叉树中某一结点的 左孩子仍是原来树中该结点的长子,并且是它的最左孩子。 图6.16是一个由一般树转为二叉树的实例。 数据结构(C语言版) G H F CBD A (a) EG H F CBD A (b) EG H F CBD A (c) ED CB A (d) E HG FG H F CBD A (a) EG H F CBD A (b) EG H F CBD A (c) ED CB A (d) E HG F 图6.16 一般树转换为二叉树 (a) 一般树;(b) 加线;(c) 抹线;(d) 旋转整理 数据结构(C语言版) 2. 二叉树还原为一般树 二叉树还原为一般树时,二叉树必须是由某一树转换而来的 且没有右子树。并非任意一棵二叉树都能还原成一般树。其还 原过程也分为三步: (1) 加线:若某结点i是双亲结点的左孩子,则将该结点i的右 孩子以及当且仅当连续地沿着右孩子的右链不断搜索到所有右 孩子都分别与结点i的双亲结点用虚线连接。 (2) 抹线:把原二叉树中所有双亲结点与其右孩子的连线抹 去。这里的右孩子实质上是原一般树中结点的兄弟,抹去的连 线是兄弟间的关系。 (3) 整理:把虚线改为实线,把结点按层次排列。 2. 二叉树还原为一般树 二叉树还原为一般树时,二叉树必须是由某一树转换而来的 且没有右子树。并非任意一棵二叉树都能还原成一般树。其还 原过程也分为三步: (1) 加线:若某结点i是双亲结点的左孩子,则将该结点i的右 孩子以及当且仅当连续地沿着右孩子的右链不断搜索到所有右 孩子都分别与结点i的双亲结点用虚线连接。 (2) 抹线:把原二叉树中所有双亲结点与其右孩子的连线抹 去。这里的右孩子实质上是原一般树中结点的兄弟,抹去的连 线是兄弟间的关系。 (3) 整理:把虚线改为实线,把结点按层次排列。 数据结构(C语言版) (a) B D A C E F G HI (b) B D A C E F G HI (c) B D A C E F G HI BCE A DIG F H (d)(a) B D A C E F G HI (b) B D A C E F G HI (c) B D A C E F G HI BCE A DIG F H (d) 图6.17 二叉树还原为一般树 (a) 二叉树;(b) 还原加线;(c) 还原抹线;(d) 还原整理 数据结构(C语言版)6.6.3 森林与二叉树之间的转换 FB A BCFHBD AE I G J H EG I J (a) (b) H G J C B D E A FI (c) D CFG H I J B A E (d) C D FB A BCFHBD AE I G J H EG I J (a) (b) H G J C B D E A FI (c) D CFG H I J B A E (d) C D 图6.18 森林转换为二叉树 数据结构(C语言版) 1. 森林转换为二叉树 森林转换为二叉树的步骤为: (1) 将森林中每棵子树转换成相应的二叉树,形成有若干二叉 树的森林。 (2) 按森林图形中树的先后次序,依次将后边一棵二叉树作 为前边一棵二叉树根结点的右子树,这样整个森林就生成了一 棵二叉树,实际上第一棵树的根结点便是生成后的二叉树的根 结点。图6.18是森林转化为二叉树的示例。 1. 森林转换为二叉树 森林转换为二叉树的步骤为: (1) 将森林中每棵子树转换成相应的二叉树,形成有若干二叉 树的森林。 (2) 按森林图形中树的先后次序,依次将后边一棵二叉树作 为前边一棵二叉树根结点的右子树,这样整个森林就生成了一 棵二叉树,实际上第一棵树的根结点便是生成后的二叉树的根 结点。图6.18是森林转化为二叉树的示例。 数据结构(C语言版) 2. 二叉树还原为森林 将一棵由森林转换得到的二叉树还原为森林的步骤是: (1) 抹线:将二叉树的根结点与其右孩子的连线以及当且仅当 连续地沿着右链不断地搜索到的所有右孩子的连线全部抹去, 这样就得到包含有若干棵二叉树的森林。 (2) 还原:将每棵二叉树按二叉树还原为一般树的方法还原为 一般树,于是得到森林。 这部分的图示,请读者自己练习画出。 2. 二叉树还原为森林 将一棵由森林转换得到的二叉树还原为森林的步骤是: (1) 抹线:将二叉树的根结点与其右孩子的连线以及当且仅当 连续地沿着右链不断地搜索到的所有右孩子的连线全部抹去, 这样就得到包含有若干棵二叉树的森林。 (2) 还原:将每棵二叉树按二叉树还原为一般树的方法还原为 一般树,于是得到森林。 这部分的图示,请读者自己练习画出。 数据结构(C语言版) 6.6.4 一般树或森林的遍历 一般树的遍历主要是先根和后根遍历,一般不讨论中根遍 历。树的先根遍历首先访问树的根结点,然后从左至右逐一先 根遍历每一棵子树。树的后根遍历首先后根遍历树的最左边的 第一棵子树,接着从左至右逐一后根遍历每一棵子树,最后访 问树的根结点。一般树转换为二叉树后,此二叉树没有右子树, 对此二叉树的中根遍历结果与上述一般树的后根遍历结果相同。 6.6.4 一般树或森林的遍历 一般树的遍历主要是先根和后根遍历,一般不讨论中根遍 历。树的先根遍历首先访问树的根结点,然后从左至右逐一先 根遍历每一棵子树。树的后根遍历首先后根遍历树的最左边的 第一棵子树,接着从左至右逐一后根遍历每一棵子树,最后访 问树的根结点。一般树转换为二叉树后,此二叉树没有右子树, 对此二叉树的中根遍历结果与上述一般树的后根遍历结果相同。 数据结构(C语言版) 6.7 树 的 应 用 6.7.1 二叉排序树 1. 二叉排序树的定义和特点 定义:二叉排序树(Binary Sort Tree)或是空树,或是非空树。 对于非空树: (1) 若左子树不空,则左子树上各结点的值均小于它的根结点 的值; (2) 若右子树不空,则右子树上各结点的值均大于等于它的根 结点的值; (3)它的左、右子树又分别是二叉排序树。 6.7.1 二叉排序树 1. 二叉排序树的定义和特点 定义:二叉排序树(Binary Sort Tree)或是空树,或是非空树。 对于非空树: (1) 若左子树不空,则左子树上各结点的值均小于它的根结点 的值; (2) 若右子树不空,则右子树上各结点的值均大于等于它的根 结点的值; (3)它的左、右子树又分别是二叉排序树。 数据结构(C语言版) 6 9 5 4 7 1 图 6. 19 二叉排序树 6 9 5 4 7 1 图 6. 19 二叉排序树 特点:对二叉排序树进行中根遍历,可得到一个由小到大 的序列。例如,对图6.19所示的二叉排序树进行中根遍历,则得 到序列:1,4,5,6,7,9。 数据结构(C语言版) 2. 建立二叉排序树的算法 建立二叉排序树,实质上是不断地进行插入结点的操作。设 有一组数据:K={k1,k2,…,kn},将它们一一输入建成二叉 排序树。我们用二叉链表作为存储结构。结点结构如下: struct bstreenode {int data; struct bstreenode *lch,*rch; } 2. 建立二叉排序树的算法 建立二叉排序树,实质上是不断地进行插入结点的操作。设 有一组数据:K={k1,k2,…,kn},将它们一一输入建成二叉 排序树。我们用二叉链表作为存储结构。结点结构如下: struct bstreenode {int data; struct bstreenode *lch,*rch; } 数据结构(C语言版) 算法思路分析: (1) 让k1作根; (2) 对于k2,若k2<k1,令k2做k1的左孩子;否则令k2做k1的右 孩子; (3) 对于k3,从根k1开始比较。若k3<k1,到左子树中找,否 则到右子树中找;找到适当位置进行插入; (4) 对于k4,k5,…,kn,重复第(3)步,直到kn处理完为止。 算法思路分析: (1) 让k1作根; (2) 对于k2,若k2<k1,令k2做k1的左孩子;否则令k2做k1的右 孩子; (3) 对于k3,从根k1开始比较。若k3<k1,到左子树中找,否 则到右子树中找;找到适当位置进行插入; (4) 对于k4,k5,…,kn,重复第(3)步,直到kn处理完为止。 数据结构(C语言版) 在建立过程中,每输入一个数据元素,就插入一次。现把插 入一个结点的算法单独编写,而在建立二叉排序树的函数中对其 进行调用。 首先看建立二叉排序树的主体算法。 数据结构(C语言版) /*算法描述6.9 建立二叉排序树的主体算法*/ struct bstreenode creat() { printf("n=?"); scanf("%d",&n ); t==NULL; for(i=1;i<=n; i++) {printf("k%d=?",i); scanf("%d", &k); s=(struct bstreenode * )malloc(sizeof(struct bstreenode)); s->data=k; s->lch=NULL;s->rch=NULL; insertl(t,s); /*或调用insert2(t, s)*/ } return(t); } /*算法描述6.9 建立二叉排序树的主体算法*/ struct bstreenode creat() { printf("n=?"); scanf("%d",&n ); t==NULL; for(i=1;i<=n; i++) {printf("k%d=?",i); scanf("%d", &k); s=(struct bstreenode * )malloc(sizeof(struct bstreenode)); s->data=k; s->lch=NULL;s->rch=NULL; insertl(t,s); /*或调用insert2(t, s)*/ } return(t); } 数据结构(C语言版) 在二叉排序树中,插入一个结点s的递归算法如下: /*算法描述6.10 在二叉排序树中,插入一个结点s的递归算法*/ void insertl(struct bstreenode *t, struct bstreenode *s) {if(t==NULL) t=s; else if(s->datadata) insert1(t->lch,s); /*将s插入t的左子树*/ else insert1(t->rch,s); /*将s插入t的右子树*/ } 在二叉排序树中,插入一个结点s的递归算法如下: /*算法描述6.10 在二叉排序树中,插入一个结点s的递归算法*/ void insertl(struct bstreenode *t, struct bstreenode *s) {if(t==NULL) t=s; else if(s->datadata) insert1(t->lch,s); /*将s插入t的左子树*/ else insert1(t->rch,s); /*将s插入t的右子树*/ } 数据结构(C语言版) 在二叉排序树中,插入一个结点s的非递归算法如下: /*算法描述6.11 在二叉排序树中,插入一个结点s的非递归算法*/ void insert2(struct bstreenode *t, struct bstreenode *s) { if(t==NULL) t=s; else { p=t; while(p!=NULL) { q=p; /*当p向子树结点移动时,q记p的双亲位置*/ if(s->datadata) p=p->lch; else p=p->rch; } if(s->datadata) q->lch=s; else q->rch=s; /*当p为空时, q就是可插入的地方*/ } } 在二叉排序树中,插入一个结点s的非递归算法如下: /*算法描述6.11 在二叉排序树中,插入一个结点s的非递归算法*/ void insert2(struct bstreenode *t, struct bstreenode *s) { if(t==NULL) t=s; else { p=t; while(p!=NULL) { q=p; /*当p向子树结点移动时,q记p的双亲位置*/ if(s->datadata) p=p->lch; else p=p->rch; } if(s->datadata) q->lch=s; else q->rch=s; /*当p为空时, q就是可插入的地方*/ } } 数据结构(C语言版) 假设给出一组数据{5,3,7,6,9,2},对照上述算法生 成二叉排序树的过程如图6.20所示。 5 6 9 5 3 7 26 9 5 3 7 6 5 3 7 5 3 7 5 3 (f)(e)(d)(c)(b)(a) 5 6 9 5 3 7 26 9 5 3 7 6 5 3 7 5 3 7 5 3 (f)(e)(d)(c)(b)(a) 图6.20 二叉排序树的生成 数据结构(C语言版) 由此可见,在二叉排序树上插入结点不需遍历树,仅需从 根结点出发,走一条根到某个有空子树的结点的路径,使该结 点的空指针指向被插入结点,使被插入结点成为新的叶子结点。 如果仍使用前边6个数据,但输入先后顺序发生变化,那么生成 的二叉排序树如何?请思考。 由此可见,在二叉排序树上插入结点不需遍历树,仅需从 根结点出发,走一条根到某个有空子树的结点的路径,使该结 点的空指针指向被插入结点,使被插入结点成为新的叶子结点。 如果仍使用前边6个数据,但输入先后顺序发生变化,那么生成 的二叉排序树如何?请思考。 数据结构(C语言版) 3. 在二叉排序树中删除结点 算法思路分析:在二叉排序树上删除一个结点,应该在删除 之后仍保持二叉排序树的特点。要删除某结点p,p结点和它的 双亲结点f都是已知条件,这里的关键是怎样找一个结点s来替 换p结点。下面分三种情况来讨论。 (1)  p结点无右孩子,则让p的左孩子s上移替代p结点; (2)  p结点无左孩子,则让p的右孩子s上移替代p结点; 3. 在二叉排序树中删除结点 算法思路分析:在二叉排序树上删除一个结点,应该在删除 之后仍保持二叉排序树的特点。要删除某结点p,p结点和它的 双亲结点f都是已知条件,这里的关键是怎样找一个结点s来替 换p结点。下面分三种情况来讨论。 (1)  p结点无右孩子,则让p的左孩子s上移替代p结点; (2)  p结点无左孩子,则让p的右孩子s上移替代p结点; 数据结构(C语言版) (3)  p结点有左孩子和右孩子,可用它的前驱(或后继)结点 s代替p结点。现假定用它的后继结点来代替,这个结点s应是p 的右子树中数据域值最小的结点,或者说是p的右子树中的最 左结点。因其值域最小(在右子树中),所以它一定没有左孩子。 这时先让p结点取s结点的值,然后可按第(2)种情况处理,删除 s结点,这就等效于删除了原p结点。具体算法这里不再细讲, 请读者思考。 (3)  p结点有左孩子和右孩子,可用它的前驱(或后继)结点 s代替p结点。现假定用它的后继结点来代替,这个结点s应是p 的右子树中数据域值最小的结点,或者说是p的右子树中的最 左结点。因其值域最小(在右子树中),所以它一定没有左孩子。 这时先让p结点取s结点的值,然后可按第(2)种情况处理,删除 s结点,这就等效于删除了原p结点。具体算法这里不再细讲, 请读者思考。 数据结构(C语言版) 6.7.2 哈夫曼树及其应用 哈夫曼(Huffman)树,又称最优二叉树,是一类带权路径长 度最短的树,有着广泛的应用。 1. 哈夫曼树的基本概念 首先我们要学习一些与哈夫曼树有关的术语。 两个结点之间的路径长度:树中一个结点到另一个结点之 间的分支数目。 树的路径长度:从根结点到每个结点的路径长度之和。 6.7.2 哈夫曼树及其应用 哈夫曼(Huffman)树,又称最优二叉树,是一类带权路径长 度最短的树,有着广泛的应用。 1. 哈夫曼树的基本概念 首先我们要学习一些与哈夫曼树有关的术语。 两个结点之间的路径长度:树中一个结点到另一个结点之 间的分支数目。 树的路径长度:从根结点到每个结点的路径长度之和。 数据结构(C语言版) 树的带权路径长度:设一棵二叉树有n个叶子,每个叶子结 点拥有一个权值W1,W2,…,Wn,从根结点到每个叶子结点 的路径长度分别为L1,L2,…,Ln,那么树的带权路径长度为 每个叶子的路径长度与该叶子权值乘积之和,通常记作: å = = n 1k kk LWWPL 数据结构(C语言版) 为了直观起见,在图6.21中,把带权的叶子结点画成方形, 其它非叶子结点仍为圆形。请看图6.21中的三棵二叉树以及它 们的带权路径长度。 (a)  WPL=2×2+4×2+5×2+8×2=38 (b)  WPL=4×2+5×3+8×3+2×1=49 (c)  WPL=8×1+5×2+4×3+2×3=36 注意: 这三棵二叉树叶子结点数相同,它们的权值也相同, 但是它们的WPL带权路径长各不相同。图6.21(c)所示二叉树的 WPL最小。它就是哈夫曼树,最优树。 为了直观起见,在图6.21中,把带权的叶子结点画成方形, 其它非叶子结点仍为圆形。请看图6.21中的三棵二叉树以及它 们的带权路径长度。 (a)  WPL=2×2+4×2+5×2+8×2=38 (b)  WPL=4×2+5×3+8×3+2×1=49 (c)  WPL=8×1+5×2+4×3+2×3=36 注意: 这三棵二叉树叶子结点数相同,它们的权值也相同, 但是它们的WPL带权路径长各不相同。图6.21(c)所示二叉树的 WPL最小。它就是哈夫曼树,最优树。 数据结构(C语言版) 哈夫曼树:在具有同一组权值的叶子结点的不同二叉树中, 带权路径长度最短的树。 2 4 5 8 4 5 8 2 58 5 4 2 (c)(b)(a) 2 4 5 8 4 5 8 2 58 5 4 2 (c)(b)(a) 图6.21 具有不同带权路径长度的二叉树 (a)  WPL=38;(b)  WPL=49;(c)  WPL=36 数据结构(C语言版) 2. 哈夫曼树的构造及其算法 1) 构造哈夫曼树的方法 对于已知的一组叶子的权值W1,W2,…,Wn: (1) 首先把n个叶子结点看作n棵树(有一个结点的二叉树),把 它们看作一个森林。 (2) 在森林中把权值最小和次小的两棵树合并成一棵树,该树 根结点的权值是两棵子树权值之和。这时森林中还有n-1棵树。 (3) 重复第(2)步直到森林中只有一棵树为止。(此树就是哈 夫曼树。) 2. 哈夫曼树的构造及其算法 1) 构造哈夫曼树的方法 对于已知的一组叶子的权值W1,W2,…,Wn: (1) 首先把n个叶子结点看作n棵树(有一个结点的二叉树),把 它们看作一个森林。 (2) 在森林中把权值最小和次小的两棵树合并成一棵树,该树 根结点的权值是两棵子树权值之和。这时森林中还有n-1棵树。 (3) 重复第(2)步直到森林中只有一棵树为止。(此树就是哈 夫曼树。) 数据结构(C语言版) 58 19 5 4 2 (d)(c)(b) 6 115 2 4 6 11 885 2 4 62 4 5 8 (a) 58 19 5 4 2 (d)(c)(b) 6 115 2 4 6 11 885 2 4 62 4 5 8 (a) 图6.22 哈夫曼树构造过程 (a) 森林中有四棵树;(b) 森林中有三棵树; (c) 森林中有两棵树;(d) 生成一棵树 数据结构(C语言版) 此时我们或许会问,n个叶子构成的哈夫曼树其带权路径 长度唯一吗?答案是确实唯一。树形唯一吗?答案是不唯一。 因为将森林中两棵权值最小和次小的子树合并时,哪棵做左子 树,哪棵做右子树并不严格限制。图6.22之中的做法是把权值 较小的当作左子树,权值较大的当作右子树。如果反过来也可 以,画出的树形有所不同,但WPL值相同。 此时我们或许会问,n个叶子构成的哈夫曼树其带权路径 长度唯一吗?答案是确实唯一。树形唯一吗?答案是不唯一。 因为将森林中两棵权值最小和次小的子树合并时,哪棵做左子 树,哪棵做右子树并不严格限制。图6.22之中的做法是把权值 较小的当作左子树,权值较大的当作右子树。如果反过来也可 以,画出的树形有所不同,但WPL值相同。 数据结构(C语言版) 2) 哈夫曼算法实现 讨论算法实现需选择合适的存储结构,因为哈夫曼树中没有 度为1的结点。这里选用顺序存储结构。由二叉树性质可知 n0=n2+1,而现在总结点数为n0+n2,也即2n0-1。叶子数n0若用 n表示则二叉树结点总数为2n-1,向量的大小就定义为2n-1。 假设n<10,存储结构如下: struct hftreenode {int data;/*权值域*/ int lch,rch;/*左、右孩子结点在数组中的下标*/ int tag;/*tag=0结点独立,tag= 1结点已并入树中*/ } struct hftreenode r[20]; 2) 哈夫曼算法实现 讨论算法实现需选择合适的存储结构,因为哈夫曼树中没有 度为1的结点。这里选用顺序存储结构。由二叉树性质可知 n0=n2+1,而现在总结点数为n0+n2,也即2n0-1。叶子数n0若用 n表示则二叉树结点总数为2n-1,向量的大小就定义为2n-1。 假设n<10,存储结构如下: struct hftreenode {int data;/*权值域*/ int lch,rch;/*左、右孩子结点在数组中的下标*/ int tag;/*tag=0结点独立,tag= 1结点已并入树中*/ } struct hftreenode r[20]; 数据结构(C语言版) 首先需将叶子权值输入r向量,lch,rch,tag域全置零,如果 用前边的一组数值{2,4,5,8}初始化向量r(见图6.23(a)),然 后执行算法,可得出如图6.23(b)所示的结果。设t为指向哈夫曼 树的根结点(在此是数组元素)的指针,则算法如算法6.12所示。 数据结构(C语言版) 0 0 2 0 1 0 2 0 0 0 4 0 1 0 4 0 0 0 5 0 1 0 5 0 0 0 8 0 1 0 8 0 1 1 6 21 1 6 2 1 3 11 5 0 4 19 6 (a) (b) 图6.23 哈夫曼树向量存储结构示意图 (a) 初始状态;(b) 最终状态 数据结构(C语言版) /*算法描述6.12 哈夫曼算法*/ int huffman (struct hftreenode r[20]) {scanf("n=%d",&n); /*n为叶子结点的个数*/ for(j=1;j<=n; j++) {scanf("%d", &r[j].data); r[j].tag=0;r[j].lch=0;r[j].rch=0; } i=0; while (idata=x; q->lch=NULL;q->rch=NULL; s[i]=q; if(i==1)t=q; /*t代表树根结点*/ else{j=i/2; /*双亲结点编号*/ struct node * creat() /*建立二叉树*/ { struct node *t, *q, *s[30]; int i,j,x; printf("i, x=");scanf("%d%d",&i,&x); /* i是按满二叉树编号x结点应有的序号, x是结点数据*/ while ((i!=0)&&(x!=0)) {q=(struct node *)malloc(sizeof(struct node )) /*产生一个结点*/ q->data=x; q->lch=NULL;q->rch=NULL; s[i]=q; if(i==1)t=q; /*t代表树根结点*/ else{j=i/2; /*双亲结点编号*/ 数据结构(C语言版) if((i%2)==0) {s[j]->lch=q;else s[j]->rch=q;} } printf("i,x=");scanf("&d, &d",&i, &x); } return(t); }/*creat*/ void preorderz(struct node *p) /*先根非递归算法*/ {struct node *q,*s[30]; /*s是辅助栈*/ int top,bool; q=p; top=0; bool=1; /*bool=1为真值继续循环;bool=0为假值栈空,结束循环*/ if((i%2)==0) {s[j]->lch=q;else s[j]->rch=q;} } printf("i,x=");scanf("&d, &d",&i, &x); } return(t); }/*creat*/ void preorderz(struct node *p) /*先根非递归算法*/ {struct node *q,*s[30]; /*s是辅助栈*/ int top,bool; q=p; top=0; bool=1; /*bool=1为真值继续循环;bool=0为假值栈空,结束循环*/ 数据结构(C语言版) do {while(q!=NULL){ printf("%6d",q->data);/*访问根结点*/ top++;s[top]=q;q=q->lch; } if(top==0) bool=0; . else{q=s[top];top--;q=q->rch;} }while (bool); printf("/n"); }/*preorder*/ void inorder(struct node *p) {if(p!=NULL) {inorder(p->lch); do {while(q!=NULL){ printf("%6d",q->data);/*访问根结点*/ top++;s[top]=q;q=q->lch; } if(top==0) bool=0; . else{q=s[top];top--;q=q->rch;} }while (bool); printf("/n"); }/*preorder*/ void inorder(struct node *p) {if(p!=NULL) {inorder(p->lch); 数据结构(C语言版) printf("%6d",p->data) inorder(p->rch); } }/*inorder*/ void postorder(struct node *p) {if(p!=NULL) {postorder(p->lch); postorder(p->rch); printf("%6d",p->data); if(p->lch==NULL&&p->rch==NULL) m++; /*统计叶结点*/ } }/*postorder*/ printf("%6d",p->data) inorder(p->rch); } }/*inorder*/ void postorder(struct node *p) {if(p!=NULL) {postorder(p->lch); postorder(p->rch); printf("%6d",p->data); if(p->lch==NULL&&p->rch==NULL) m++; /*统计叶结点*/ } }/*postorder*/ 数据结构(C语言版) 上面有关二叉树的源程序在上机调试时有几点应注意:在 主函数main中调用二叉树的建立函数creat时,指向根结点的指 针root必须是全局变量;在调用递归遍历函数时,访问输出的 格式不带换行符"/n",当调用结束返回主调函数后,在主调函 数中设一输出语句令其回车换行。 上面有关二叉树的源程序在上机调试时有几点应注意:在 主函数main中调用二叉树的建立函数creat时,指向根结点的指 针root必须是全局变量;在调用递归遍历函数时,访问输出的 格式不带换行符"/n",当调用结束返回主调函数后,在主调函 数中设一输出语句令其回车换行。 数据结构(C语言版) 习 题 6 1. 找出图6.26所示树T的深度、度、分支结点和叶子结点。 2. 对于三个结点A、B、C,可组成多少种不同的二叉树? 请画出。 3. 分别写出图6.27所示二叉树的三种遍历次序的序列。 1. 找出图6.26所示树T的深度、度、分支结点和叶子结点。 2. 对于三个结点A、B、C,可组成多少种不同的二叉树? 请画出。 3. 分别写出图6.27所示二叉树的三种遍历次序的序列。 数据结构(C语言版) FGH B L C IJK M ED A FGH B L C IJK M ED A 图6.26 树T 数据结构(C语言版) DEFG BC HIJ A DEFG BC HIJ A 图6.27 二叉树 数据结构(C语言版) 4. 写出先根遍历的非递归算法。 5. 现有按后根遍历二叉树的结果为C、B、A,有几种不同 的二叉树可以得到这一结果? 6. 若已知某二叉树的两种遍历序列,可推断出二叉树的形状 吗?结果是否唯一?为 什么? 7. 画出图6.27所示二叉树先根、中根、后根线索化树的逻 辑表示图。 8. 对于同一组数据,若以不同的顺序输入,所建立的二叉 树树形、树深是否相同?中根遍历结果是否相同?为什么? 4. 写出先根遍历的非递归算法。 5. 现有按后根遍历二叉树的结果为C、B、A,有几种不同 的二叉树可以得到这一结果? 6. 若已知某二叉树的两种遍历序列,可推断出二叉树的形状 吗?结果是否唯一?为 什么? 7. 画出图6.27所示二叉树先根、中根、后根线索化树的逻 辑表示图。 8. 对于同一组数据,若以不同的顺序输入,所建立的二叉 树树形、树深是否相同?中根遍历结果是否相同?为什么? 数据结构(C语言版) 9. 已知三个互不相等的整数,若以这三个数来生成二叉排序 树,可以生成几种不同形状的树? 10. 有一组数值14、21、32、15、28,画出哈夫曼树的生成过 程及最后结果。 11. 用哈夫曼算法构造哈夫曼树时森林中两棵权值最小和次小 的子树合并时,哪棵做左子树并不影响WPL?影响哈夫曼编码 吗? 12. 试写出在二叉排序树中删除一个结点的算法。 13. 为什么说一般二叉树一般不采用顺序存储结构,而满二 叉树和完全二叉树多采用顺序存储结构? 9. 已知三个互不相等的整数,若以这三个数来生成二叉排序 树,可以生成几种不同形状的树? 10. 有一组数值14、21、32、15、28,画出哈夫曼树的生成过 程及最后结果。 11. 用哈夫曼算法构造哈夫曼树时森林中两棵权值最小和次小 的子树合并时,哪棵做左子树并不影响WPL?影响哈夫曼编码 吗? 12. 试写出在二叉排序树中删除一个结点的算法。 13. 为什么说一般二叉树一般不采用顺序存储结构,而满二 叉树和完全二叉树多采用顺序存储结构? 数据结构(C语言版) 第7章 图 7.1 基本术语 7.2 图的存储结构 7.3 遍历图 7.4 最短路径 7.5 拓扑排序 7.6 实习: 最短路径的实现 习题7 7.1 基本术语 7.2 图的存储结构 7.3 遍历图 7.4 最短路径 7.5 拓扑排序 7.6 实习: 最短路径的实现 习题7 数据结构(C语言版) 7.1 基 本 术 语 图(Graph):图G由两个集合V(G)和E(G)所组成,记作G=(V, E),其中V(G)是图中顶点的非空有限集合,E(G)是图中边的有 限集合。 有向图(Digraph):如果图中每条边都是顶点有序对,即每条 边在图示时都用箭头表示方向,则称此图为有向图。有向图的 边也称为弧。如图7.1中G1是有向图,它由V(G1)和E(G1)组成。 其中: V(G1)={v1,v2,v3} E(G1)={} 图(Graph):图G由两个集合V(G)和E(G)所组成,记作G=(V, E),其中V(G)是图中顶点的非空有限集合,E(G)是图中边的有 限集合。 有向图(Digraph):如果图中每条边都是顶点有序对,即每条 边在图示时都用箭头表示方向,则称此图为有向图。有向图的 边也称为弧。如图7.1中G1是有向图,它由V(G1)和E(G1)组成。 其中: V(G1)={v1,v2,v3} E(G1)={} 数据结构(C语言版) v1 v2 v3 v4 v1 v2 v3 v4 v5 v6 v7 v2 v3 v1 G1 G2 G3 v1 v2 v3 v4 v1 v2 v3 v4 v5 v6 v7 v2 v3 v1 G1 G2 G3 图7.1 图的示例 数据结构(C语言版) 无向图(Undigraph):如果图中每条边都是顶点无序对,则 称此图为无向图。无向边用圆括号括起的两个相关顶点来表示。 所以在无向图中,(v1 v2)和(v2 v1)表示的是同一条边。 如图7.1中的G2和G3都是无向图。其中 V(G2)={v1,v2,v3,v4} E(G2)={(v1,v2),(v1,v3),(v2,v3),(v2,v4),(v3,v4)} V(G3)={v1,v2,v3,v4,v5,v16,v7} E(G3)= {(v1,v2),(v1,v3),(v2,v4),(v2,v5),(v3,v6), (v3,v7)} 无向图(Undigraph):如果图中每条边都是顶点无序对,则 称此图为无向图。无向边用圆括号括起的两个相关顶点来表示。 所以在无向图中,(v1 v2)和(v2 v1)表示的是同一条边。 如图7.1中的G2和G3都是无向图。其中 V(G2)={v1,v2,v3,v4} E(G2)={(v1,v2),(v1,v3),(v2,v3),(v2,v4),(v3,v4)} V(G3)={v1,v2,v3,v4,v5,v16,v7} E(G3)= {(v1,v2),(v1,v3),(v2,v4),(v2,v5),(v3,v6), (v3,v7)} 数据结构(C语言版) 无 向 完 全 图 (Completed Undigraph) 和 有 向 完 全 图 (Completed Digraph):若一个无向图有n个顶点,而每一个顶点 与其他n-1个顶点之间都有边,这样的图称之为无向完全图, 即共有n(n-1)/2条边。类似地,在有n个顶点的有向图中,若 有n(n-1)条弧,即任意两点顶点之间都有双向相反的两条弧 连接,则称此图为有向完全图。 子图(Subgraph):设有两个图A和B且满足条件 V(B)V(A) E(B)E(A) 则称B是A的子图。 无 向 完 全 图 (Completed Undigraph) 和 有 向 完 全 图 (Completed Digraph):若一个无向图有n个顶点,而每一个顶点 与其他n-1个顶点之间都有边,这样的图称之为无向完全图, 即共有n(n-1)/2条边。类似地,在有n个顶点的有向图中,若 有n(n-1)条弧,即任意两点顶点之间都有双向相反的两条弧 连接,则称此图为有向完全图。 子图(Subgraph):设有两个图A和B且满足条件 V(B)V(A) E(B)E(A) 则称B是A的子图。 数据结构(C语言版) 路径(Path):在图G中,从顶点vp到vq的一条路径是顶点序 列(vp,vi1,vi2,…,vin,vq)且(vp,vi1),(vi1,vi2),…,(vin, vq)是E(G)中的边,路径上边的数目称之为该路径长度。对于有 向图,其路径也是有向的,路径由弧组成。 简单路径(Simple Path):如果一条路径上所有顶点除起始 点和终止点外彼此都是不同的,则称该路径是简单路径。 回路(Cycle)和简单回路:在一条路径中,如果起始点和终 止点是同一顶点,则称其为回路。简单路径相应的回路称为简 单回路。 路径(Path):在图G中,从顶点vp到vq的一条路径是顶点序 列(vp,vi1,vi2,…,vin,vq)且(vp,vi1),(vi1,vi2),…,(vin, vq)是E(G)中的边,路径上边的数目称之为该路径长度。对于有 向图,其路径也是有向的,路径由弧组成。 简单路径(Simple Path):如果一条路径上所有顶点除起始 点和终止点外彼此都是不同的,则称该路径是简单路径。 回路(Cycle)和简单回路:在一条路径中,如果起始点和终 止点是同一顶点,则称其为回路。简单路径相应的回路称为简 单回路。 数据结构(C语言版) 连通图(Connected Graph)和强连通图(Strongly Connected Graph):在无向图G中,若从vi到vj有路径,则称vi和vj是连通的。 若G中任意两顶点都是连通的,则称G是连通图。对于有向图而 言,若G中每一对不同顶点vi和vj之间都有vi到vj和vj到vi的路径, 则称G为强连通图。 度(Degree)、入度(Indegree)和出度(Outdegree):若(vi vj)是 E(G)中的一条边,则称顶点vi和vj是邻接的,并称边(vi vj)依附 于顶点vi和vj。所谓顶点的度,就是依附于该顶点的边数。在有 向图中,以某顶点为头,即终止于该顶点的弧的数目称为该顶 点的入度;以某顶点为尾,即起始于该顶点的弧的数目称为该 顶点的出度。该顶点的入度和出度之和称为该顶点的度。 连通图(Connected Graph)和强连通图(Strongly Connected Graph):在无向图G中,若从vi到vj有路径,则称vi和vj是连通的。 若G中任意两顶点都是连通的,则称G是连通图。对于有向图而 言,若G中每一对不同顶点vi和vj之间都有vi到vj和vj到vi的路径, 则称G为强连通图。 度(Degree)、入度(Indegree)和出度(Outdegree):若(vi vj)是 E(G)中的一条边,则称顶点vi和vj是邻接的,并称边(vi vj)依附 于顶点vi和vj。所谓顶点的度,就是依附于该顶点的边数。在有 向图中,以某顶点为头,即终止于该顶点的弧的数目称为该顶 点的入度;以某顶点为尾,即起始于该顶点的弧的数目称为该 顶点的出度。该顶点的入度和出度之和称为该顶点的度。 数据结构(C语言版) 7.2 图的存储结构 7.2.1 邻接矩阵 图的邻接矩阵表示法是用一个二维数组来表示图中顶点的 相邻关系。设图G=(V,E)有n(n≥1)个顶点,则G的邻接矩阵是 按如下定义的n阶方阵: 7.2.1 邻接矩阵 图的邻接矩阵表示法是用一个二维数组来表示图中顶点的 相邻关系。设图G=(V,E)有n(n≥1)个顶点,则G的邻接矩阵是 按如下定义的n阶方阵: 1 若(vi,vj)或∈E(G) cost[i][j]= 0 反之 数据结构(C语言版) 例如,图7.1中G1,G2,G3的邻接矩阵分别表示为B1,B2 和B3,矩阵的行列号对应于图中结点的序号。 ú ú ú û ù ê ê ê ë é = 000 100 110 B1 ú ú ú ú ú ú ú ú ú û ù ê ê ê ê ê ê ê ê ê ë é = 0000100 0000100 0000010 0000010 1100001 0011001 0000110 B3 ú ú ú ú û ù ê ê ê ê ë é = 0110 1011 1101 0110 B2 ú ú ú ú ú ú ú ú ú û ù ê ê ê ê ê ê ê ê ê ë é = 0000100 0000100 0000010 0000010 1100001 0011001 0000110 B3 不难看出,无向图的邻接矩阵是对称的,而有向图的邻矩阵 不一定对称。 数据结构(C语言版) 在C语言中,图的邻接矩阵存储表示如下。 #define MAXVERTEXNUM 50 int cost[][MAXVERTEXNUM] 根据邻接矩阵的定义,可以得出邻接矩阵建立的算法如下: /*算法描述7.1 邻接矩阵的建立*/ int creatadjmatrix (int cost[][MAXVERTEXNUM]) /*cost二维 数组为建立的邻接矩阵*/ {int vexnum,arcnum,i,j,k,v1,v2; scanf ("%d,%d",&vexnum,&arcnum); /*输入图的顶点数和弧数或二倍边数*/ for (i=0;ivextex=v2; ptr->next=list[v1].firstarc; list [v1].firstarc=ptr; /*将相邻结点v2插入表头结点v1之后以形成链表*/ } return(vexnum); } for(k=0;k<=arcnum;k++) /*循环为list数组的各元素分 别建立各自的链表*/ {printf("v1,v2="); scanf ("%d %d",&v1,&v2); ptr=(arcnode*)malloc(sizeof(arcnode)); /*给结点v1的相邻结点v2分配内存空间*/ ptr->vextex=v2; ptr->next=list[v1].firstarc; list [v1].firstarc=ptr; /*将相邻结点v2插入表头结点v1之后以形成链表*/ } return(vexnum); } 数据结构(C语言版) 7.3 遍 历 图 给定一个无向连通图G,G=(V,E),当从V(G)中的任一顶 点V出发,去访问图中其余顶点,使每个顶点仅被访问一次, 这个过程叫做图的遍历。 图的遍历和树的遍历相似,但要比树的遍历复杂得多。因 为图中的任一顶点都可能和其余的顶点相邻接,所以在访问某 个顶点后,可能沿着某条路径搜索之后,又回到该顶点。例如, 由于图7.3中存在回路,因此在访问了v1 v2 v4 v3之后沿着边(v3, v1)又可访问到v1。为了避免同一顶点被访问多次,在遍历图的 过程中,必须记下每个已访问过的顶点。为此,我们设一个辅 助数组,可以利用表头向量list,让其data域作为标志域,即 给定一个无向连通图G,G=(V,E),当从V(G)中的任一顶 点V出发,去访问图中其余顶点,使每个顶点仅被访问一次, 这个过程叫做图的遍历。 图的遍历和树的遍历相似,但要比树的遍历复杂得多。因 为图中的任一顶点都可能和其余的顶点相邻接,所以在访问某 个顶点后,可能沿着某条路径搜索之后,又回到该顶点。例如, 由于图7.3中存在回路,因此在访问了v1 v2 v4 v3之后沿着边(v3, v1)又可访问到v1。为了避免同一顶点被访问多次,在遍历图的 过程中,必须记下每个已访问过的顶点。为此,我们设一个辅 助数组,可以利用表头向量list,让其data域作为标志域,即 数据结构(C语言版) List[vi].data=1 已访问过 List[vi].data=0 未访问过 通常有两种遍历图的方法,一种是深度优先搜索法,另一 种为广度优先搜索法。 v4 v1 v2 v3 v4 v1 v2 v3 图7.3 图遍历示例 数据结构(C语言版) 7.3.1 深度优先搜索法 设有无向连通图G(V,E),从V(G)中任一顶点V出发深度优 先搜索法进行遍历的步骤是: 先访问指定顶点v1,然后访问与该顶点v1相邻的顶点v2, 再从v2出发,访问与v2相邻且未被访问过的任意顶点v3,然后 从顶点v3出发,重复上述过程,直到所有邻接点都被访问过为 止,然后回溯到此顶点的直接前驱,从这儿出发再继续访问。 显然搜索是一个递归过程。 7.3.1 深度优先搜索法 设有无向连通图G(V,E),从V(G)中任一顶点V出发深度优 先搜索法进行遍历的步骤是: 先访问指定顶点v1,然后访问与该顶点v1相邻的顶点v2, 再从v2出发,访问与v2相邻且未被访问过的任意顶点v3,然后 从顶点v3出发,重复上述过程,直到所有邻接点都被访问过为 止,然后回溯到此顶点的直接前驱,从这儿出发再继续访问。 显然搜索是一个递归过程。 数据结构(C语言版) 对于图7.1中的G2,邻接表为图7.2(b),先选取v1顶点作为搜 索的起始点。顶点v1的相邻顶点分别是v3、v2,所以沿着它的一 个相邻顶点往下走。当走到顶点v3时,它有三个相邻顶点v4、v2、 v1,沿着它的一个相邻顶点往下走到v4,而v4有两个相邻顶点v3 和v2,因v3已被访问过所以应原路返回,访问v2,当走到v2时, v2有三个相邻顶点v4、v3、v1,因v4、v3、v1已被访问,原路返 回上层,上层为空再返回,v3、v1被访问过,再返回上层,v2被 访问过,为空则结束,如图7.4所示。 对于图7.1中的G2,邻接表为图7.2(b),先选取v1顶点作为搜 索的起始点。顶点v1的相邻顶点分别是v3、v2,所以沿着它的一 个相邻顶点往下走。当走到顶点v3时,它有三个相邻顶点v4、v2、 v1,沿着它的一个相邻顶点往下走到v4,而v4有两个相邻顶点v3 和v2,因v3已被访问过所以应原路返回,访问v2,当走到v2时, v2有三个相邻顶点v4、v3、v1,因v4、v3、v1已被访问,原路返 回上层,上层为空再返回,v3、v1被访问过,再返回上层,v2被 访问过,为空则结束,如图7.4所示。 数据结构(C语言版) v4 v1 v2 v3 v4 v1 v2 v3 图7.4 深度优先搜索示意图 数据结构(C语言版) 下面以邻接表为图的存储结构给出深度优先搜索算法。 /*算法描述7.3 深度优先搜索*/ vexnode list[MAXVERTEXNUM],*ptr[MAXVERTEXNUM]; int n; main() {int v; n=creatadjlist(list); /*建立邻接表,返回结点数*/ for(v=1;v<=n;v++) /*初始化指针数组及标志域*/ {ptr [v]=list[v].firstarc; list[v].data=0; } 下面以邻接表为图的存储结构给出深度优先搜索算法。 /*算法描述7.3 深度优先搜索*/ vexnode list[MAXVERTEXNUM],*ptr[MAXVERTEXNUM]; int n; main() {int v; n=creatadjlist(list); /*建立邻接表,返回结点数*/ for(v=1;v<=n;v++) /*初始化指针数组及标志域*/ {ptr [v]=list[v].firstarc; list[v].data=0; } 数据结构(C语言版) for(v=1;v<=n;v++) if (list[v].data==0) dfs(v); } dfs(int v) /*从某顶点v出发深度优先搜索子函数 */ { int w;  printf ("%d",v);/*输出顶点*/  List [v].data=1;/*顶点标志域置1*/ while (ptr[v]!=NULL) for(v=1;v<=n;v++) if (list[v].data==0) dfs(v); } dfs(int v) /*从某顶点v出发深度优先搜索子函数 */ { int w;  printf ("%d",v);/*输出顶点*/  List [v].data=1;/*顶点标志域置1*/ while (ptr[v]!=NULL) 数据结构(C语言版) { w=ptr[v]->vextex;/*取出顶点v的某相邻顶点的序号*/ if (list [w].data==0) dfs(w); /*如果该顶点未被访问过则递归调用,从该顶点w出发, 沿着它的各相邻顶点向下*/ /*搜索*/ ptr[v]=ptr[v]->next; /*若从顶点v出发沿着某个相邻顶点的向下搜索已走到头, 则换一个相邻顶点,沿着*/   /*它往下搜索*/ }/*从顶点v出发对各相邻顶点逐个搜索,直至从顶点v出发 的所有并行路线已被搜索*/ } { w=ptr[v]->vextex;/*取出顶点v的某相邻顶点的序号*/ if (list [w].data==0) dfs(w); /*如果该顶点未被访问过则递归调用,从该顶点w出发, 沿着它的各相邻顶点向下*/ /*搜索*/ ptr[v]=ptr[v]->next; /*若从顶点v出发沿着某个相邻顶点的向下搜索已走到头, 则换一个相邻顶点,沿着*/   /*它往下搜索*/ }/*从顶点v出发对各相邻顶点逐个搜索,直至从顶点v出发 的所有并行路线已被搜索*/ } 数据结构(C语言版) 7.3.2 广度优先搜索法 设无向图G(V,E)是连通的,从V(G)中的任一顶点v1出发按 广度优先搜索法遍历图的步骤是: 首先访问指定的起始顶点v1,从v1出发,依次访问与v1相邻 的未被访问过的顶点w1,w2,w3,…,wt,然后依次从w1,w2, w3,…,wt出发,重复上述访问过程,直到所有顶点都被访问 过为止。以图7.5为例,如选顶点v1为起始点先进行访问。顶点 有三个相邻顶点,依次是v4、v3、v2,则广度优先搜索时对这几 个顶点依次访问,然后再将这些相邻顶点的第一相邻顶点v4作为 起始顶点重复上述步骤,再优先访问v6。再将第二个相邻顶点v3 作为起始顶点,重复上述步骤,顶点v3有三个相邻顶点依次为v6、 v5、v2,其中v2、v6被访问过(不再访问),所以访问v5。依次再判 v2、v6、v5,重复上述步骤,因相邻顶点都被访问过即结束,如 图7.5所示。 7.3.2 广度优先搜索法 设无向图G(V,E)是连通的,从V(G)中的任一顶点v1出发按 广度优先搜索法遍历图的步骤是: 首先访问指定的起始顶点v1,从v1出发,依次访问与v1相邻 的未被访问过的顶点w1,w2,w3,…,wt,然后依次从w1,w2, w3,…,wt出发,重复上述访问过程,直到所有顶点都被访问 过为止。以图7.5为例,如选顶点v1为起始点先进行访问。顶点 有三个相邻顶点,依次是v4、v3、v2,则广度优先搜索时对这几 个顶点依次访问,然后再将这些相邻顶点的第一相邻顶点v4作为 起始顶点重复上述步骤,再优先访问v6。再将第二个相邻顶点v3 作为起始顶点,重复上述步骤,顶点v3有三个相邻顶点依次为v6、 v5、v2,其中v2、v6被访问过(不再访问),所以访问v5。依次再判 v2、v6、v5,重复上述步骤,因相邻顶点都被访问过即结束,如 图7.5所示。 数据结构(C语言版) v1 v5 v3 v6 v4v2 v1 v5 v3 v6 v4v2 v1 v5 v3 v6 v4v2 v1 v5 v3 v6 v4v2 图7.5 广度优先搜索示意图 数据结构(C语言版) 下面以邻接表为图的存储结构给出广度优先搜索算法。 /*算法描述7.4 广度优先搜索*/ vexnode list[MAXVERTEXNUM]; int n; main() {int v; n=creatadjlist(list); /*建立邻接表,返回结点数*/ for(v=1;v<=n;v++) /*初始化标志域*/ list[v].data=0; for(v=1;v<=n;v++) if (list[v].data==0) bfs(v); } 下面以邻接表为图的存储结构给出广度优先搜索算法。 /*算法描述7.4 广度优先搜索*/ vexnode list[MAXVERTEXNUM]; int n; main() {int v; n=creatadjlist(list); /*建立邻接表,返回结点数*/ for(v=1;v<=n;v++) /*初始化标志域*/ list[v].data=0; for(v=1;v<=n;v++) if (list[v].data==0) bfs(v); } 数据结构(C语言版) void bfs(int v) /*从v出发广度优先搜索函数*/ {arcnode * ptr; int v1,w; printf ("%d"v);/*输出该顶点*/ list [v].data=1;/*标志域置1*/ enqueue(v);/*将该顶点入队尾*/ while((v1=dequeue())!=EOF) /*while循环使属于同一层顶点的相邻顶点的依次出队。由于 这些相邻顶点作为上一*/ /*层顶点均被访问过,出队的目的是访问它的下层相邻顶点*/ { ptr=list[v1].firstarc;/*取出该顶点的第一个相邻顶点地址*/ void bfs(int v) /*从v出发广度优先搜索函数*/ {arcnode * ptr; int v1,w; printf ("%d"v);/*输出该顶点*/ list [v].data=1;/*标志域置1*/ enqueue(v);/*将该顶点入队尾*/ while((v1=dequeue())!=EOF) /*while循环使属于同一层顶点的相邻顶点的依次出队。由于 这些相邻顶点作为上一*/ /*层顶点均被访问过,出队的目的是访问它的下层相邻顶点*/ { ptr=list[v1].firstarc;/*取出该顶点的第一个相邻顶点地址*/ 数据结构(C语言版) while (ptr!=NULL) /*while循环依次访问各相邻顶点*/ {w=ptr->vertex;/*取出该顶点的序号*/ ptr=ptr->next; /*从邻接表的相应链表中,取出下一个相邻顶点的地址, 以备访问*/ if (list[w].data==0) /*若该相邻顶点未被访问过*/ {printf("%d",w) /*则访问该相邻顶点*/ list [w].data=1;/*修改顶点标志域为1*/ enqueue (w); /*将访问过的顶点入队,以备在进入 下一层搜索时使用*/ } } } } while (ptr!=NULL) /*while循环依次访问各相邻顶点*/ {w=ptr->vertex;/*取出该顶点的序号*/ ptr=ptr->next; /*从邻接表的相应链表中,取出下一个相邻顶点的地址, 以备访问*/ if (list[w].data==0) /*若该相邻顶点未被访问过*/ {printf("%d",w) /*则访问该相邻顶点*/ list [w].data=1;/*修改顶点标志域为1*/ enqueue (w); /*将访问过的顶点入队,以备在进入 下一层搜索时使用*/ } } } } 数据结构(C语言版) 7.4 最 短 路 径 我们先举一个例子来说明什么是最短路径。如果我们用顶 点表示城市,用边表示城市之间的公路,则由这些顶点和边组 成的图可以表示沟通各城市的公路网。若把两个城市之间的距 离作为权值,赋给图中的边,就构成了带权图。 对于一个汽车司机来说,他一般关心两个问题: (1) 从甲地到乙地是否有公路? (2) 从甲地到乙地有多条公路可以到达时,那么,哪条公路 路径最短或花费代价最小? 我们先举一个例子来说明什么是最短路径。如果我们用顶 点表示城市,用边表示城市之间的公路,则由这些顶点和边组 成的图可以表示沟通各城市的公路网。若把两个城市之间的距 离作为权值,赋给图中的边,就构成了带权图。 对于一个汽车司机来说,他一般关心两个问题: (1) 从甲地到乙地是否有公路? (2) 从甲地到乙地有多条公路可以到达时,那么,哪条公路 路径最短或花费代价最小? 数据结构(C语言版) 这就是本节要讨论的最短路径问题。这里所谓的最短路径, 是指所经过的边的权值之和为最小的路径,而不是经过边的数 目最少。考虑到公路的有向性,在讨论中结合有向带权图来进 行。设定边的权值为正值,并将路径的开始顶点称为源点,路 径的最后一个顶点称为终点。本节给出两个算法,一个是求从 某个源点到其他顶点的最短路径,另一个是求每对顶点间的最 短路径。 这就是本节要讨论的最短路径问题。这里所谓的最短路径, 是指所经过的边的权值之和为最小的路径,而不是经过边的数 目最少。考虑到公路的有向性,在讨论中结合有向带权图来进 行。设定边的权值为正值,并将路径的开始顶点称为源点,路 径的最后一个顶点称为终点。本节给出两个算法,一个是求从 某个源点到其他顶点的最短路径,另一个是求每对顶点间的最 短路径。 数据结构(C语言版) 7.4.1 从某个源点到其他各顶点的最短路径 设有向带权图G=(V,E),我们用cost[][]表示图G的邻接矩阵, 其中 w 若∈E(G),w为边权值 cost[i][j]=   反之 例如,图7.6(a)所示带权图的邻接矩阵如图7.6(b)所示。 7.4.1 从某个源点到其他各顶点的最短路径 设有向带权图G=(V,E),我们用cost[][]表示图G的邻接矩阵, 其中 w 若∈E(G),w为边权值 cost[i][j]=   反之 例如,图7.6(a)所示带权图的邻接矩阵如图7.6(b)所示。 ¥ 数据结构(C语言版) v3 v2 v4 v5 v0 v1 100 30 60 10 10 20 505 ∞ ∞ 10 ∞ 30 100 ∞ ∞ 5 ∞ ∞ ∞ ∞ ∞ ∞ 50 ∞ ∞ ∞ ∞ ∞ ∞ ∞ 10 ∞ ∞ ∞ ∞ ∞ 60 ∞ ∞ ∞ ∞ ∞ ∞ (a) (b) v3 v2 v4 v5 v0 v1 100 30 60 10 10 20 505 ∞ ∞ 10 ∞ 30 100 ∞ ∞ 5 ∞ ∞ ∞ ∞ ∞ ∞ 50 ∞ ∞ ∞ ∞ ∞ ∞ ∞ 10 ∞ ∞ ∞ ∞ ∞ 60 ∞ ∞ ∞ ∞ ∞ ∞ (a) (b) 图7.6 有向带权图及其邻接矩阵 (a) 有向带权图;(b) 带权邻接矩阵 数据结构(C语言版) 对于这样的存储结构,如何能较方便地在计算机上求得最短 路径呢?迪杰斯特拉(Dijkstra)提出了按路径长度递增的次序产生 最短路径的算法。此算法把网中所有顶点分成两个集合。凡以 v0为源点已确定了最短路径的终点并入S集合,S集合的初态只 包含v0;另一个集合V-S为尚未确定最短路径的顶点的集合。 按各顶点与v0间的最短路径长度递增的次序,逐个把V-S集合 中的顶点加入到S集合中去,使得从v0到S集合中各顶点的路径 长度始终不大于从v0到V-S集合中各顶点的路径长度。为了能 方便地求出从v0到V-S集合中最短路径的递增次序,算法中引 入一个辅助向量dist[]。它的某一分量dist[i],表示当前求出的从 v0到vi的最短路径长度。这个路径长度不一定是真正的路径长度。 它的初始状态即是邻接矩阵cost[][]中v0行内各列的值,显然, 从v0到各顶点的最短路径中最短的一条路径长度应为 dist[w]=mindist[i]/vi∈V(G) 对于这样的存储结构,如何能较方便地在计算机上求得最短 路径呢?迪杰斯特拉(Dijkstra)提出了按路径长度递增的次序产生 最短路径的算法。此算法把网中所有顶点分成两个集合。凡以 v0为源点已确定了最短路径的终点并入S集合,S集合的初态只 包含v0;另一个集合V-S为尚未确定最短路径的顶点的集合。 按各顶点与v0间的最短路径长度递增的次序,逐个把V-S集合 中的顶点加入到S集合中去,使得从v0到S集合中各顶点的路径 长度始终不大于从v0到V-S集合中各顶点的路径长度。为了能 方便地求出从v0到V-S集合中最短路径的递增次序,算法中引 入一个辅助向量dist[]。它的某一分量dist[i],表示当前求出的从 v0到vi的最短路径长度。这个路径长度不一定是真正的路径长度。 它的初始状态即是邻接矩阵cost[][]中v0行内各列的值,显然, 从v0到各顶点的最短路径中最短的一条路径长度应为 dist[w]=mindist[i]/vi∈V(G) 数据结构(C语言版) 第一次求得的这条最短路径必然是,这时顶点w 应从V-S中删除而并入S集合中。每当选出一个顶点w并使之 并入S集合之后,修改V-S集合中各顶点的最短路径长度dist。 对于V-S集合中的某一顶点vi来说,其当前的最短路径或者是 或者是,而决不可能有其他选择。也就是 说 如果 dist[w]+cost[w][vi ],这时顶点w 应从V-S中删除而并入S集合中。每当选出一个顶点w并使之 并入S集合之后,修改V-S集合中各顶点的最短路径长度dist。 对于V-S集合中的某一顶点vi来说,其当前的最短路径或者是 或者是,而决不可能有其他选择。也就是 说 如果 dist[w]+cost[w][vi ]所对应的权值(若不存在, 则A[i][j]为∞),我们不妨记为A(–1),A(–1)的值不可能是最短路径 长度。 我们先讨论如何求得各顶点间的最短路径长度,初始时,复 制网的代价矩阵cost为矩阵A的值,即顶点vi到顶点vj的最短路 径长度A[i][j]就是弧所对应的权值(若不存在, 则A[i][j]为∞),我们不妨记为A(–1),A(–1)的值不可能是最短路径 长度。 数据结构(C语言版) 如何求得最短路径?要进行n次试探。对于从顶点vi到顶点vj 的最短路径长度,首先考虑让路径经过顶点v0,比较路径的长度,取其短者为当前求得的最短路径。对 每一对顶点都作这样的试探,可求得A(0)。然后,再考虑在A(0) 的基础上让路径经过顶点v1,于是求得A(1)。依次类推,一般地, 如果从顶点vi到顶点vj的路径经过新顶点vk能使路径缩短,则修 改A(k)[i][j]=A(k–1) [i][k]+A(k–1) [k][j],所以,A(k) [i][j]就是当前求 得的从顶点vi到顶点vj的最短路径长度,且其路径上的顶点(除 源点、终点外)序号均不大于k。 如何求得最短路径?要进行n次试探。对于从顶点vi到顶点vj 的最短路径长度,首先考虑让路径经过顶点v0,比较路径的长度,取其短者为当前求得的最短路径。对 每一对顶点都作这样的试探,可求得A(0)。然后,再考虑在A(0) 的基础上让路径经过顶点v1,于是求得A(1)。依次类推,一般地, 如果从顶点vi到顶点vj的路径经过新顶点vk能使路径缩短,则修 改A(k)[i][j]=A(k–1) [i][k]+A(k–1) [k][j],所以,A(k) [i][j]就是当前求 得的从顶点vi到顶点vj的最短路径长度,且其路径上的顶点(除 源点、终点外)序号均不大于k。 数据结构(C语言版) 这样经过几次试探,就把几个顶点都考虑到相应的路径中 去了。最后求得的A(n–1)就一定是各顶点间的最短路径长度。综 上所述,弗洛伊德算法的基本思想是递推地产生两个几阶的矩 阵序列。其中,表示最短路径长度的矩阵序列是A(–1),A(0),A(1), A(2),…,A(k),…,A(n–1),其递推关系是 A(–1)[i][j]=cost[i][j] A(k) [i][j]=min{A(k–1) [i][j],A(k–1) [i][k]+A(k–1) [k][j]} (i≥0,j≥0, k≤n-1) 这样经过几次试探,就把几个顶点都考虑到相应的路径中 去了。最后求得的A(n–1)就一定是各顶点间的最短路径长度。综 上所述,弗洛伊德算法的基本思想是递推地产生两个几阶的矩 阵序列。其中,表示最短路径长度的矩阵序列是A(–1),A(0),A(1), A(2),…,A(k),…,A(n–1),其递推关系是 A(–1)[i][j]=cost[i][j] A(k) [i][j]=min{A(k–1) [i][j],A(k–1) [i][k]+A(k–1) [k][j]} (i≥0,j≥0, k≤n-1) 数据结构(C语言版) 现在我们再讨论如何求解最短路径长度的同时求解最短路 径?初始时矩阵P的各元素都赋零。P[i][j]=0表示vi到vj的路径是 直接到达,中间不经过其他顶点。以后,当考虑路径经过某个 顶点vk时,如果使路径更短,则修改A(k–1) [i][j]的同时令 P[i][j]=k,即P[i][j]中存放的是从vi到vj的路径上所经过的某个顶 点(若P[i][j]≠0)。那么,如何求得从vi到vj的路径上的全部顶点 呢? 这只需要编写一个递归过程即可解决,因为所有最短路径 的信息都包含在矩阵P中了。设经过n次试探后,P[i][j]=k,即 从vi到vj的最短路径经过顶点vk(若k≠0)。该路径上还有哪些顶 点呢? 只需去查P[i][k]和P[k][j]即可。依次类推,直到所查元素 为零。 现在我们再讨论如何求解最短路径长度的同时求解最短路 径?初始时矩阵P的各元素都赋零。P[i][j]=0表示vi到vj的路径是 直接到达,中间不经过其他顶点。以后,当考虑路径经过某个 顶点vk时,如果使路径更短,则修改A(k–1) [i][j]的同时令 P[i][j]=k,即P[i][j]中存放的是从vi到vj的路径上所经过的某个顶 点(若P[i][j]≠0)。那么,如何求得从vi到vj的路径上的全部顶点 呢? 这只需要编写一个递归过程即可解决,因为所有最短路径 的信息都包含在矩阵P中了。设经过n次试探后,P[i][j]=k,即 从vi到vj的最短路径经过顶点vk(若k≠0)。该路径上还有哪些顶 点呢? 只需去查P[i][k]和P[k][j]即可。依次类推,直到所查元素 为零。 数据结构(C语言版) 对于图7.7所示的有向带权图G,按照弗洛伊德算法由递推 产生的两个矩阵序列如图7.8所示。 v1 v2 v4 v3v0 10 14 115 4 1 0 ∞ ∞ ∞ ∞ ∞ 0 4 11 ∞ ∞ 10 0 4 1 ∞ 5 ∞ 0 ∞ ∞ ∞ ∞ 1 0 v1 v2 v4 v3v0 10 14 115 4 1 0 ∞ ∞ ∞ ∞ ∞ 0 4 11 ∞ ∞ 10 0 4 1 ∞ 5 ∞ 0 ∞ ∞ ∞ ∞ 1 0 图7.7 网G和它的邻接矩阵 数据结构(C语言版) ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é ¥¥¥ ¥¥¥ ¥ ¥¥ ¥¥¥¥ =- 01 05 14010 1140 0 A)1( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é =- 00000 00000 00000 00000 00000 p )1( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é ¥¥¥ ¥¥¥ ¥ ¥¥ ¥¥¥¥ = 01 05 14010 1140 0 A)0( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é = 00000 00000 00000 00000 00000 p )0( 图7.8 网G的各对顶点间最短路径及长度 数据结构(C语言版) ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é ¥¥¥ ¥ ¥ ¥¥ ¥¥¥¥ = 01 10095 14010 1140 0 A)1( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é = 00000 00100 00000 00000 00000 p )1( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é ¥¥¥ ¥ ¥ ¥ ¥¥¥¥ = 01 10095 14010 8540 0 A)2( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é = 00330 20100 00000 22000 00000 p )2( 图7.8 网G的各对顶点间最短路径及长度 数据结构(C语言版) ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é ¥ ¥ ¥ ¥ ¥¥¥¥ = 01106 10095 1409 5840 0 A)3( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é = 00330 20100 00030 22000 00000 p )3( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é ¥ ¥ ¥ ¥ ¥¥¥¥ = 01106 10095 1207 5640 0 A)4( ú ú ú ú ú ú û ù ê ê ê ê ê ê ë é = 00330 20100 04040 24000 00000 p )4( 图7.8 网G的各对顶点间最短路径及长度 数据结构(C语言版) 由此可以得到如下的弗洛伊德算法描述: /*算法描述7.9*/ void floyed (cost,a,p,n) int cost [][MAX], a[][MAX],p[][MAX],n; /*a[][]表示最短路径长度,数组p[][]表示最短路径的数组*/ { int i,j,k; for(i=0;i,其中 c3和c9分别表示“普通物理”和“计算机组成原理”的教学活动, 这说明“普通物理”是“计算机组成原理”的直接前驱,“普 通物理”教学活动一定要安排在“计算组成原理”教学活动之 前。 AOV网中的弧表示了“活动”之间的优先关系,也可以说 是一种制约关系。例如,计算机专业学生必须学完一系列规定 的课程后才能毕业。这可看作一个工程,我们用图7.9所示的 AOV网加以表示,网中的顶点表示各门课程的教学活动,有向 边表示各门课程的制约关系。如图7.9中有一条弧,其中 c3和c9分别表示“普通物理”和“计算机组成原理”的教学活动, 这说明“普通物理”是“计算机组成原理”的直接前驱,“普 通物理”教学活动一定要安排在“计算组成原理”教学活动之 前。 数据结构(C语言版) c3 c2 c6 c9 c1 c8 c7 c4 c10 c5 c3 c2 c6 c9 c1 c8 c7 c4 c10 c5 图7.9 表示课程之间优先关系的AOV网 数据结构(C语言版) 课程代号  课程名称 先行课程 c1 高等数学 无 c2 工程数学 c1 c3 普通物理 c1 c4 程序设计基础 无 c5 C语言程序设计 c1 c2 c4 c6 离散数学 c1 c7 数据结构 c4 c5 c6 c8 编译方法 c5 c7 c9 计算机组成原理 c3 c10 操作系统 c7 c9 课程代号  课程名称 先行课程 c1 高等数学 无 c2 工程数学 c1 c3 普通物理 c1 c4 程序设计基础 无 c5 C语言程序设计 c1 c2 c4 c6 离散数学 c1 c7 数据结构 c4 c5 c6 c8 编译方法 c5 c7 c9 计算机组成原理 c3 c10 操作系统 c7 c9 数据结构(C语言版) 在图7.9中,顶点c1,c4是顶点c5的直接前驱;顶点c7是顶 点c4,c5,c6的直接后继;顶点c1是顶点c9的前驱,但不是直接 前驱。显然,在AOV网中,由弧表示的优先关系有传递性, 如顶点c1是c3的前驱,而c3是c9的前驱,则c1也是c9的前驱。在 AOV网中不能出现有向回路,如果存在回路,则说明某个 “活动”能否进行要以自身任务的完成作为先决条件,显然, 这样的工程是无法完成的。如果要检测一个工程是否可行,首 先就得检查对应的AOV网是否存在回路。检查AOV网中是否 存在回路的方法就是拓扑排序。 在图7.9中,顶点c1,c4是顶点c5的直接前驱;顶点c7是顶 点c4,c5,c6的直接后继;顶点c1是顶点c9的前驱,但不是直接 前驱。显然,在AOV网中,由弧表示的优先关系有传递性, 如顶点c1是c3的前驱,而c3是c9的前驱,则c1也是c9的前驱。在 AOV网中不能出现有向回路,如果存在回路,则说明某个 “活动”能否进行要以自身任务的完成作为先决条件,显然, 这样的工程是无法完成的。如果要检测一个工程是否可行,首 先就得检查对应的AOV网是否存在回路。检查AOV网中是否 存在回路的方法就是拓扑排序。 数据结构(C语言版) 7.5.2 拓扑排序 对于一个AOV网,构造其所有顶点的线性序列,使此序列 不仅保持网中各顶点间原有的先后次序,而且使原来没有先后 次序关系的顶点之间也建立起人为的先后关系,这样的序列称 为拓扑有序序列。构造AOV网的拓扑有序序列的运算称为拓扑 排序。 某个AOV网,如果它的拓扑有序序列被构造成功,则该网 中不存在有向回路,其各子工程可按拓扑有序序列的次序进行 安排。一个AOV网的拓扑有序序列并不是惟一的,例如,下面 的两个序列都是图7.9所示AOV网的拓扑有序序列。 c1 c4 c3 c2 c5 c6 c9 c7 c8 c10 c4 c1 c2 c3 c9 c6 c5 c7 c8 c10 7.5.2 拓扑排序 对于一个AOV网,构造其所有顶点的线性序列,使此序列 不仅保持网中各顶点间原有的先后次序,而且使原来没有先后 次序关系的顶点之间也建立起人为的先后关系,这样的序列称 为拓扑有序序列。构造AOV网的拓扑有序序列的运算称为拓扑 排序。 某个AOV网,如果它的拓扑有序序列被构造成功,则该网 中不存在有向回路,其各子工程可按拓扑有序序列的次序进行 安排。一个AOV网的拓扑有序序列并不是惟一的,例如,下面 的两个序列都是图7.9所示AOV网的拓扑有序序列。 c1 c4 c3 c2 c5 c6 c9 c7 c8 c10 c4 c1 c2 c3 c9 c6 c5 c7 c8 c10 数据结构(C语言版) 对AOV网进行拓扑排序的步骤是: (1) 在网中选择一个没有前驱的顶点且输出之。 (2) 在网中删去该顶点,并且删去从该顶点发出的全部有 向边。 (3) 重复上述两步,直到网中不存在没有前驱的顶点为止。 这样操作结果有两种:一种是网中全部顶点均被输出,说 明网中不存在有向回路;另一种是网中顶点未被全部输出,剩 余的顶点均有前驱顶点,说明网中存在有向回路。 拓扑排序的方法很多,主要有深度优先搜索排序和广度优 先搜索排序两种,下面分别介绍。 对AOV网进行拓扑排序的步骤是: (1) 在网中选择一个没有前驱的顶点且输出之。 (2) 在网中删去该顶点,并且删去从该顶点发出的全部有 向边。 (3) 重复上述两步,直到网中不存在没有前驱的顶点为止。 这样操作结果有两种:一种是网中全部顶点均被输出,说 明网中不存在有向回路;另一种是网中顶点未被全部输出,剩 余的顶点均有前驱顶点,说明网中存在有向回路。 拓扑排序的方法很多,主要有深度优先搜索排序和广度优 先搜索排序两种,下面分别介绍。 数据结构(C语言版) 1. 广度优先搜索拓扑排序 根据拓扑排序的方法,把入度为0的顶点插入一个队列,按 顺序输出。本算法中将顶点的入度记录在邻接表数组的数据域 中,即记录在list[v].data中。算法如下: void topsort(vexnode list[]) {arcnode * ptr; int v,w,n1=0; for (v=1;v<=n;v++) if (list[v].data==0) enqueue (v); 1. 广度优先搜索拓扑排序 根据拓扑排序的方法,把入度为0的顶点插入一个队列,按 顺序输出。本算法中将顶点的入度记录在邻接表数组的数据域 中,即记录在list[v].data中。算法如下: void topsort(vexnode list[]) {arcnode * ptr; int v,w,n1=0; for (v=1;v<=n;v++) if (list[v].data==0) enqueue (v); 数据结构(C语言版) /*利用循环检测入度为0的顶点并入队,即将无前驱的顶点加入队列*/ while((v=dequeue())!=EOF) { printf("%-5d%c",v,(++n1%10==0)? '/n':''); /*显示无前驱顶点,即删除这些顶点,并计数*/ ptr=list[v].firstarc; /*取上面被删除顶点所相邻顶点的地址,以便删除指向它的边*/ while (ptr!=NULL) { w=ptr->vertex;/*取相邻顶点的序号*/ if (--list[w].data==0) enqueue(w); /*利用循环检测入度为0的顶点并入队,即将无前驱的顶点加入队列*/ while((v=dequeue())!=EOF) { printf("%-5d%c",v,(++n1%10==0)? '/n':''); /*显示无前驱顶点,即删除这些顶点,并计数*/ ptr=list[v].firstarc; /*取上面被删除顶点所相邻顶点的地址,以便删除指向它的边*/ while (ptr!=NULL) { w=ptr->vertex;/*取相邻顶点的序号*/ if (--list[w].data==0) enqueue(w); 数据结构(C语言版) /*将相邻顶点的入度减1,即删除指向该相邻顶点的一条边。若该 顶点的入度减1后,*/ /*入度为0,则成为无前驱顶点,所以入队列以便删除*/ ptr=ptr->next;/*取下一个相邻顶点的地址*/ } }/*循环结构,无前驱的顶点逐个出队 if (n1next;/*取下一个相邻顶点的地址*/ } }/*循环结构,无前驱的顶点逐个出队 if (n1vertex;/*则取出相邻顶序的序号,看相邻顶点是否搜索过*/ if (list[w].data==0) topodfs(w); /*若相邻顶点未被搜索过, 则递归调用拓扑排序函数,去深度优先搜索相邻 顶点*/ ptr[v]=ptr[v]->next;/*取另一个相邻顶点*/ printf ("%5d",pop ());/*按相反的拓扑序列显示拓朴序列的顶序*/ list[v].data=1;/*对搜索过的顶点标志改为1,以免重复,并进栈*/ push(v); while (ptr[v]!=NULL) w=ptr[v]->vertex;/*则取出相邻顶序的序号,看相邻顶点是否搜索过*/ if (list[w].data==0) topodfs(w); /*若相邻顶点未被搜索过, 则递归调用拓扑排序函数,去深度优先搜索相邻 顶点*/ ptr[v]=ptr[v]->next;/*取另一个相邻顶点*/ printf ("%5d",pop ());/*按相反的拓扑序列显示拓朴序列的顶序*/ 数据结构(C语言版) 7.6 实习:最短路径的实现 用图表示出全国直辖市和省会城市的铁路网络,量出地图上 直辖市和省会城市的距离,并输入计算机,要求能求出从任一城 市出发到其他城市的最短路径并输出。 主程序如下: #include main() int cost[MAX][MAX],n;/*cost[][]是带权邻接矩阵*/ n=adjmatrix(cost);/*调用子函数建立邻接矩阵*/ prmatrix(cost,n);/*调用子函数显示邻接矩阵*/ dijkstra(cost,n);/*调用子函数求单源最短路径*/ 用图表示出全国直辖市和省会城市的铁路网络,量出地图上 直辖市和省会城市的距离,并输入计算机,要求能求出从任一城 市出发到其他城市的最短路径并输出。 主程序如下: #include main() int cost[MAX][MAX],n;/*cost[][]是带权邻接矩阵*/ n=adjmatrix(cost);/*调用子函数建立邻接矩阵*/ prmatrix(cost,n);/*调用子函数显示邻接矩阵*/ dijkstra(cost,n);/*调用子函数求单源最短路径*/ 数据结构(C语言版) 建立带权邻接矩阵的算法如下: admatrix(matrix) int matrix[][max]; int vexnum,arcnum,i,j v1,v2,weight; /*v1,v2分别为边的两个顶点,weight是它们的权*/ printf ("total vexnum="); scanf ("%d",&vexnum); /*键入矩阵的结点数*/ for (i=0 ;in) i=0; return (i); } /*算法描述8.1 顺序查找*/ int seqsearch (r,k,n) linelist r ; int k,n ;/*n为线性表r中元素个数*/ { int i=1; while (r[i].key!=k) i++; if (i>n) i=0; return (i); } 数据结构(C语言版) 算法分析:对于含有n 个结点的线性表,结点的查找在等 概率的前提下,即pi=1/n,平均查找长度为 å = = n 1i ii cpASL 2 )1n()n21(*n 1in 1n 1i +=+×××++÷ ø öç è æ=·÷ ø öç è æ= å = 顺序查找和我们后面将要讨论到的其他查找算法相比,其 缺点是平均查找长度较大,特别是当n很大时,查找效率较低, 速度较慢,平均查找长度为O(n)。然而,它有很大的优点,即算 法简单且适应面广。它对表的结构无任何要求,无论记录是否 按关键字有序(若表中所有记录的关键字满足下列关系: r[i].key≤r[i+1].key(i=1,2,…,n-1),则称表中记录按关键字有序) 均可应用。 顺序查找和我们后面将要讨论到的其他查找算法相比,其 缺点是平均查找长度较大,特别是当n很大时,查找效率较低, 速度较慢,平均查找长度为O(n)。然而,它有很大的优点,即算 法简单且适应面广。它对表的结构无任何要求,无论记录是否 按关键字有序(若表中所有记录的关键字满足下列关系: r[i].key≤r[i+1].key(i=1,2,…,n-1),则称表中记录按关键字有序) 均可应用。 数据结构(C语言版) 8.1.2 有序表的查找 以有序表表示静态查找表时,search 函数可用折半查找来 实现,折半查找(Binary Search)的查找过程是:先确定待查记录 所在的范围(区间),然后逐步缩小范围直到找到或找不到该记 录为止。也就是要求线性表中的结点必须已按关键字值的递增 或递减顺序排列。它首先把要查找的关键字k与中间位置结点的 关键字相比较,这个中间结点把线性表分成了两个子表,若比 较结果相等则查找完成;若不相等,再根据k与该中间结点关键 字的比较结果确定下一步查找哪个子表,这样递归进行下去, 直到找到满足条件的结点或者该线性表中没有这样的结点。 8.1.2 有序表的查找 以有序表表示静态查找表时,search 函数可用折半查找来 实现,折半查找(Binary Search)的查找过程是:先确定待查记录 所在的范围(区间),然后逐步缩小范围直到找到或找不到该记 录为止。也就是要求线性表中的结点必须已按关键字值的递增 或递减顺序排列。它首先把要查找的关键字k与中间位置结点的 关键字相比较,这个中间结点把线性表分成了两个子表,若比 较结果相等则查找完成;若不相等,再根据k与该中间结点关键 字的比较结果确定下一步查找哪个子表,这样递归进行下去, 直到找到满足条件的结点或者该线性表中没有这样的结点。 数据结构(C语言版) 例8.2 在关键字有序序列{3,5,6,8,9,12,17,23,30, 35,39,42}中采用折半查找法查找关键字为8的元素。 指针low和high分别指示待查元素所在范围的下界和上界,指 针mid指示区间的中间位置,即mid=[(low+high)/2]。在此例中, low 和high的初值分别为1和12,即[1,12]为待查范围。 折半查找过程如图8.2所示。 例8.2 在关键字有序序列{3,5,6,8,9,12,17,23,30, 35,39,42}中采用折半查找法查找关键字为8的元素。 指针low和high分别指示待查元素所在范围的下界和上界,指 针mid指示区间的中间位置,即mid=[(low+high)/2]。在此例中, low 和high的初值分别为1和12,即[1,12]为待查范围。 折半查找过程如图8.2所示。 数据结构(C语言版) 开始: 3 5 6 8 9 12 17 23 30 35 39 42 low= 1 high= 12 第1次比较: 3 5 6 8 9 12 17 23 30 35 39 42 low= 1 high= 5 mid= 6 第2次比较: 3 5 6 8 9 12 17 23 30 35 39 42 low= 1 mid= 3 high= 5 第3次比较: 3 5 6 8 9 12 17 23 30 35 39 42 high= 5 mid= 4 low= 4 查找成功,返回序号3 开始: 3 5 6 8 9 12 17 23 30 35 39 42 low= 1 high= 12 第1次比较: 3 5 6 8 9 12 17 23 30 35 39 42 low= 1 high= 5 mid= 6 第2次比较: 3 5 6 8 9 12 17 23 30 35 39 42 low= 1 mid= 3 high= 5 第3次比较: 3 5 6 8 9 12 17 23 30 35 39 42 high= 5 mid= 4 low= 4 查找成功,返回序号3 图8.2 折半查找过程 数据结构(C语言版) 折半查找的函数如算法8.2,其功能是在线性表r中二分查找 关键字为k的结点,查找到,则返回其位置i;若找不到返回0。 /*算法描述8.2 折半查找*/ int binsearch (r,k,n) sqlist r; int k,n;/*n为线性表r 中元素个数*/ { int i,low=1,high=n,mid; int find=0;/*find=0表示未找到;find=1 表示已找到*/ while (low <=high &&!find) { 折半查找的函数如算法8.2,其功能是在线性表r中二分查找 关键字为k的结点,查找到,则返回其位置i;若找不到返回0。 /*算法描述8.2 折半查找*/ int binsearch (r,k,n) sqlist r; int k,n;/*n为线性表r 中元素个数*/ { int i,low=1,high=n,mid; int find=0;/*find=0表示未找到;find=1 表示已找到*/ while (low <=high &&!find) { 数据结构(C语言版) mid =(low+high)/2;/*整除*/ if (kr[mid].key) low=mid+1; else { i=mid; find=1; } } if (!find) i=0; return (i); } mid =(low+high)/2;/*整除*/ if (kr[mid].key) low=mid+1; else { i=mid; find=1; } } if (!find) i=0; return (i); } 数据结构(C语言版) 算法分析:折半查找函数找到一个记录的过程恰好是一条 从判定树的根到被查找结点的一条路径,而比较的次数恰好是 树深。借助于二叉判定树很容易求得折半查找的平均查找长度。 假设表长为n ,树深h= lb(n+1),则平均查找长度为 å - = n 1i iicpASL 1)1n(lb1)1n(lbn 1n2•in 1 n 1i 1i -+»-++== å = -å - = n 1i iicpASL 1)1n(lb1)1n(lbn 1n2•in 1 n 1i 1i -+»-++== å = - 因此,折半查找法的平均查找长度为O(lbn),与顺序查找 方法相比,折半查找的效率比顺序查找高,速度比顺序查找快, 但折半查找只能适用于有序表,需要对n个元素预先进行排序, 仅限于顺序存储结构(对线性链表无法进行折半查找)。 数据结构(C语言版) 8.1.3 索引顺序表的查找 分块查找又称为索引顺序查找,是顺序查找的一种改进, 其性能介于顺序查找和折半查找之间。分块查找把线性表分成 若干块,每一块中的元素存储顺序是任意的,但块与块之间必 须按关键字大小排序,即前一块中的最大关键字小于(或大于) 后一块中的最小(或最大)关键字值。另外,还需要建立一个索 引表,索引表中的一项对应线性表中的一块,索引项由关键字 域和链域组成,关键字域存放相应块的最大关键字,链域存放 指向本块第一个结点的指针。索引表按关键字值递增(或递减) 顺序排列。 8.1.3 索引顺序表的查找 分块查找又称为索引顺序查找,是顺序查找的一种改进, 其性能介于顺序查找和折半查找之间。分块查找把线性表分成 若干块,每一块中的元素存储顺序是任意的,但块与块之间必 须按关键字大小排序,即前一块中的最大关键字小于(或大于) 后一块中的最小(或最大)关键字值。另外,还需要建立一个索 引表,索引表中的一项对应线性表中的一块,索引项由关键字 域和链域组成,关键字域存放相应块的最大关键字,链域存放 指向本块第一个结点的指针。索引表按关键字值递增(或递减) 顺序排列。 数据结构(C语言版) 分块查找的查找函数分为两步进行:首先确定待查找的 结点属于哪一块,即查找其所在的块;然后在块内查找要查的 结点。由于索引表是递增有序的,采用折半查找时,块内元素 个数较少,采用顺序法在块内查找,不会对执行速度有太大的 影响。 分块查找的查找函数分为两步进行:首先确定待查找的 结点属于哪一块,即查找其所在的块;然后在块内查找要查的 结点。由于索引表是递增有序的,采用折半查找时,块内元素 个数较少,采用顺序法在块内查找,不会对执行速度有太大的 影响。 数据结构(C语言版) 例8.3 对于关键字序列为{8,20,13,17,40,42,45, 32,49,58,50,52,67,70,78,80}的线性表采用分块查 找法查找关键字为50的元素。假定分块索引和索引表如图8.3 所示。 数据结构(C语言版) 序号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 8 20 13 17 40 42 45 32 49 58 50 52 67 79 78 80 (a) key low high 20 1 420 1 4 45 5 8 58 9 12 80 13 16 图8.3 块表与索引表 (a) 块表;(b) 索引表 数据结构(C语言版) 分块查找过程是:先在索引表中采用二分查找法查找关键字 50所在的块,即在第3块中有4个元素,其关键字分别为49,58, 50,52,在其中按顺序查找法进行查找,找到第3个元素(即总序 号为11的元素)即关键字为50的元素。 索引表的定义如下: struct indexterm { KeyType key; int low,high; } typedef struct indexterm index[MAXITEM]; 分块查找过程是:先在索引表中采用二分查找法查找关键字 50所在的块,即在第3块中有4个元素,其关键字分别为49,58, 50,52,在其中按顺序查找法进行查找,找到第3个元素(即总序 号为11的元素)即关键字为50的元素。 索引表的定义如下: struct indexterm { KeyType key; int low,high; } typedef struct indexterm index[MAXITEM]; 数据结构(C语言版) 这里的KeyType是关键字的数据类型,可以是任何相应的数 据类型,这里默认为int型。 分块查找的函数如算法8.3,其功能是在线性表r中分块查找 关键字为k的结点,若找到,则返回其位置i;若找不到,返回0。 数据结构(C语言版) /*算法描述8.3 分块查找*/ int blksearch (r,idx,k,kug) sqlist r; index idx; int k,kug; {/*kug为块的个数*/ int i,low 1=1,high1=kug,mid1,db,find=0; while (low<=high1 && !find) /*二分查找索引表*/ { mid1=(low1+high1)/2; /*算法描述8.3 分块查找*/ int blksearch (r,idx,k,kug) sqlist r; index idx; int k,kug; {/*kug为块的个数*/ int i,low 1=1,high1=kug,mid1,db,find=0; while (low<=high1 && !find) /*二分查找索引表*/ { mid1=(low1+high1)/2; 数据结构(C语言版) if (kidx[mid1].key) low1=mid1+1; else { high1=mid1-1; find=1;    } } if (low1idx[mid1].key) low1=mid1+1; else { high1=mid1-1; find=1;    } } if (low1 key < t->key) bstinsert(s,t->lchild); /*插入到左子树上*/ else bstinsert (s,t->rchild); /*插入到右子树上*/ } /*bstinsert*/ /*算法描述8.4*/ procedure bstinsert (bstnode *s,bstnode*t) /*将新结点s插入到指针t所指的二叉排序树中*/ { if t =NULL t=s; else if (s -> key < t->key) bstinsert(s,t->lchild); /*插入到左子树上*/ else bstinsert (s,t->rchild); /*插入到右子树上*/ } /*bstinsert*/ 数据结构(C语言版) 例如,在图8.4(a)上插入关键字为55的过程为:由于此时 二叉树非空,则将55与根结点50相比较,因为55>50,则应将 55插入到50的右子树上,又因为50的右子树非空,将55与右 子树的根70比较,因55<70,则55应插入到70的左子树中,依 次类推,当最后因55<65,且65的左子树为空,将55作为65的 左子树插入到树中。 例如,在图8.4(a)上插入关键字为55的过程为:由于此时 二叉树非空,则将55与根结点50相比较,因为55>50,则应将 55插入到50的右子树上,又因为50的右子树非空,将55与右 子树的根70比较,因55<70,则55应插入到70的左子树中,依 次类推,当最后因55<65,且65的左子树为空,将55作为65的 左子树插入到树中。 数据结构(C语言版) 整棵树的构造过程的算法描述如下: /*算法描述8.5*/ bstset (bstnode *T) /*输入一个数据元素序列,建立一棵二叉排序树,endmark为输入结束标志*/ { int x ; bstnode *s; T=NULL; scanf("%d",&x); while (x!=endmark ) do 整棵树的构造过程的算法描述如下: /*算法描述8.5*/ bstset (bstnode *T) /*输入一个数据元素序列,建立一棵二叉排序树,endmark为输入结束标志*/ { int x ; bstnode *s; T=NULL; scanf("%d",&x); while (x!=endmark ) do 数据结构(C语言版) {s=(bstnode*) malloc(sizeof(bstnode)); s->key=x; scanf("%d",&other); s->lchild= NULL; s->rchild= NULL;/*建立新结点*/ bstinsert (s,p);/*新结点插入*/ scanf("%d",&x);}/*读入下一个结点*/ } /*bstset*/ {s=(bstnode*) malloc(sizeof(bstnode)); s->key=x; scanf("%d",&other); s->lchild= NULL; s->rchild= NULL;/*建立新结点*/ bstinsert (s,p);/*新结点插入*/ scanf("%d",&x);}/*读入下一个结点*/ } /*bstset*/ 数据结构(C语言版) 例8.4 已知一关键字序列为{40,25,52,10,28,80}, 则生成二叉排序树的过程如图8.5所示。 (g)(f)(e) (d)(c)(a) (b) 10 25 52 40 2810 25 52 40 2810 80 25 52 40 40 25 40 25 52 40 图8.5 二叉排序树的构造过程 (g)(f)(e) (d)(c)(a) (b) 10 25 52 40 2810 25 52 40 2810 80 25 52 40 40 25 40 25 52 40 数据结构(C语言版) 由此可以看出:二叉排序树的形态完全由一个输入序列决定, 一个无序序列可以通过构造一棵二叉排序树而变成一个有序序 列。若输入序列为10,25,28,40,52,80,则所对应的二叉 排序树为单支树,如图8.6所示。 此外,从图8.5所示的二叉排序树构造过程中还可以看出, 每次插入的新结点都是作二叉排序树的叶子结点,在插入过程 中不需要移动其他元素,只需将某个结点的指针由空变为非空 即可,所以二叉排序树结构适合于元素的经常插入,那么如何 在二叉排序树上删除一个结点呢?在二叉排序树中删除一个结点, 不能把以该结点为根的子树都删去,只能删除这个结点并仍旧 保证二叉排序树的特性,也就是说删除二叉排序树上一个结点 相当于删除有序序列中的一个元素。 由此可以看出:二叉排序树的形态完全由一个输入序列决定, 一个无序序列可以通过构造一棵二叉排序树而变成一个有序序 列。若输入序列为10,25,28,40,52,80,则所对应的二叉 排序树为单支树,如图8.6所示。 此外,从图8.5所示的二叉排序树构造过程中还可以看出, 每次插入的新结点都是作二叉排序树的叶子结点,在插入过程 中不需要移动其他元素,只需将某个结点的指针由空变为非空 即可,所以二叉排序树结构适合于元素的经常插入,那么如何 在二叉排序树上删除一个结点呢?在二叉排序树中删除一个结点, 不能把以该结点为根的子树都删去,只能删除这个结点并仍旧 保证二叉排序树的特性,也就是说删除二叉排序树上一个结点 相当于删除有序序列中的一个元素。 数据结构(C语言版) 10 25 28 40 52 80 10 25 28 40 52 80 图8.6 单支二叉排序树 数据结构(C语言版) 假设在二叉排序树上被删除结点为p(p指针指向被删除结点), f为其双亲结点,则删除结点p的过程分三种情况讨论: (1) 若p结点为叶子结点,即p->lchild及p->rchild均为空,则由 于删去叶子结点后不破坏整棵树的结构,因此,只需修改p结点 的双亲结点指针即可: f->lchild (或f-> rchild) =NULL; 假设在二叉排序树上被删除结点为p(p指针指向被删除结点), f为其双亲结点,则删除结点p的过程分三种情况讨论: (1) 若p结点为叶子结点,即p->lchild及p->rchild均为空,则由 于删去叶子结点后不破坏整棵树的结构,因此,只需修改p结点 的双亲结点指针即可: f->lchild (或f-> rchild) =NULL; 数据结构(C语言版) (2) 若p结点只有左子树或者只有右子树,此时只需用p的左 子树或右子树的根结点取代p成为双亲f的左子树(或右子树), 即令 f->lchild (或f-> rchild)=p->lchild; 或 f->lchild (或f->rchild)=p->rchild; (2) 若p结点只有左子树或者只有右子树,此时只需用p的左 子树或右子树的根结点取代p成为双亲f的左子树(或右子树), 即令 f->lchild (或f-> rchild)=p->lchild; 或 f->lchild (或f->rchild)=p->rchild; 数据结构(C语言版) (3) 若p结点的左、右子树均不空,此时不能像上面那样简单 处理,删除p结点时应考虑将p的左子树、右子树连接到适当的 位置,并保证二叉排序树的特性。有两种方法,一是令p的左子 树为双亲f的左子树,而p的右子树下接到p的中序遍历前驱结点 s的右指针上;二是令p的中序前驱结点s结点代替p结点,然后 删除s结点。 查找p结点中序前驱结点的操作为: q=p; s=p->lchild; while (s->rchild!=NULL) do {q=s;s= s->rchild;} (3) 若p结点的左、右子树均不空,此时不能像上面那样简单 处理,删除p结点时应考虑将p的左子树、右子树连接到适当的 位置,并保证二叉排序树的特性。有两种方法,一是令p的左子 树为双亲f的左子树,而p的右子树下接到p的中序遍历前驱结点 s的右指针上;二是令p的中序前驱结点s结点代替p结点,然后 删除s结点。 查找p结点中序前驱结点的操作为: q=p; s=p->lchild; while (s->rchild!=NULL) do {q=s;s= s->rchild;} 数据结构(C语言版) 因为二叉排序树可以看成是一个有序表,在二叉排序树中 左子树上所有结点的关键字均小于根结点的关键字,右子树上 所有结点的关键字均大于或等于根结点的关键字,所以在二叉 排序树上进行查找与折半查找类似。 查找过程为:若二叉排序树非空,将给定值与根结点值相 比较,若相等,则查找成功;若不等,则当根结点值大于给定 值时,到根的左子树中查找,否则在根的右子树中查找。这显 然仍是一个递归过程。 因为二叉排序树可以看成是一个有序表,在二叉排序树中 左子树上所有结点的关键字均小于根结点的关键字,右子树上 所有结点的关键字均大于或等于根结点的关键字,所以在二叉 排序树上进行查找与折半查找类似。 查找过程为:若二叉排序树非空,将给定值与根结点值相 比较,若相等,则查找成功;若不等,则当根结点值大于给定 值时,到根的左子树中查找,否则在根的右子树中查找。这显 然仍是一个递归过程。 数据结构(C语言版) /*算法描述8.6*/ bstnode *bstsrch (bstnode *t,int k) /*在t 所指的二叉排序树中查找关键字等于给定值k的记录*/ { if t!=NULL if (t->key =k) return(t); /*查找成功,返回t指针*/ else if (kkey) bstsrch(t->lchild;k); else bstsrch(t->rchild;k); else return(t); } /*bstsrch*/ /*算法描述8.6*/ bstnode *bstsrch (bstnode *t,int k) /*在t 所指的二叉排序树中查找关键字等于给定值k的记录*/ { if t!=NULL if (t->key =k) return(t); /*查找成功,返回t指针*/ else if (kkey) bstsrch(t->lchild;k); else bstsrch(t->rchild;k); else return(t); } /*bstsrch*/ 数据结构(C语言版) 可见,在二叉排序树上查找其关键字等于给定值的结点的 过程,恰是走了一条从根结点到该结点的路径的过程,和给定 值进行比较的关键字个数等于路径长度加1(或结点所在层次数)。 二叉排序数的平均查找长度为: å - = n 1i iibst CPASL 数据结构(C语言版) 长度为n的有序表其判定树惟一,故平均查找长度惟一;但 二叉排序树的形态与关键字的输入有关。例如,图8.4(a)所示的 二叉树的平均查找长度为: 89.29 26)3433221(9 1ASL a »=´+´+´+´= 同样结点数的单支二叉排序树,其平均查找长度为:同样结点数的单支二叉排序树,其平均查找长度为: 59 45)987654321(9 1ASL a ==++++++++´=单 数据结构(C语言版) 最坏的情况下,二叉排序树变为单支树,最好的情况是二叉 排序树比较均匀。 二叉排序树的性能与折半查找相差不大,但二叉排序树对 于结点的插入和删除十分方便,因此对于经常进行插入、删除 和查找运算的表,应采用二叉排序树。 最坏的情况下,二叉排序树变为单支树,最好的情况是二叉 排序树比较均匀。 二叉排序树的性能与折半查找相差不大,但二叉排序树对 于结点的插入和删除十分方便,因此对于经常进行插入、删除 和查找运算的表,应采用二叉排序树。 数据结构(C语言版) 8.2.2 平衡二叉树 平衡二叉树(Balanced Binary Tree)又称AVL树。 它或者是一棵空树,或者是具有下列性质的二叉树:它的 左子树和右子树都是平衡二叉树,且左子树和右子树的深度之 差的绝对值不超过1。二叉树上结点的平衡因子定义为该结点的 左子树的深度减去它的右子树的深度。因此,平衡二叉树上所 有结点的平衡因子为-1、0或1中的一个值,只要二叉树上有一 个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。 图8.7(a)、(b)分别给出了两棵平衡二叉树和两棵不平衡二叉树, 树中结点内的数字是该结点的平衡因子。 8.2.2 平衡二叉树 平衡二叉树(Balanced Binary Tree)又称AVL树。 它或者是一棵空树,或者是具有下列性质的二叉树:它的 左子树和右子树都是平衡二叉树,且左子树和右子树的深度之 差的绝对值不超过1。二叉树上结点的平衡因子定义为该结点的 左子树的深度减去它的右子树的深度。因此,平衡二叉树上所 有结点的平衡因子为-1、0或1中的一个值,只要二叉树上有一 个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。 图8.7(a)、(b)分别给出了两棵平衡二叉树和两棵不平衡二叉树, 树中结点内的数字是该结点的平衡因子。 数据结构(C语言版) 00 - 1 2 0 - 1 0 00 - 1 1 1 1 0 0 - 1 0 - 1 - 10 1 0 - 2 0 (a) (b) 00 - 1 2 0 - 1 0 00 - 1 1 1 1 0 0 - 1 0 - 1 - 10 1 0 - 2 0 (a) (b) 图8.7 平衡二叉树和不平衡二叉树 (a) 平衡二叉树;(b) 不平衡二叉树 数据结构(C语言版) 由上节得知,二叉排序树形态均匀时性能才能最好,而形态 为单支树时其性能与顺序查找相同,因此,我们希望在初始时 及在有元素变动时的二叉排序树都是平衡二叉树,那么如何使 构成的二叉排序树成为平衡树呢? 例如,输入关键字序列为(15,20,35,80,50),构造一棵 二叉排序树。 (1) 输入15,15作为根结点,见图8.8(a)。 (2) 输入20,20作为15的右子树,见图8.8(b),此时仍为平 衡树。 由上节得知,二叉排序树形态均匀时性能才能最好,而形态 为单支树时其性能与顺序查找相同,因此,我们希望在初始时 及在有元素变动时的二叉排序树都是平衡二叉树,那么如何使 构成的二叉排序树成为平衡树呢? 例如,输入关键字序列为(15,20,35,80,50),构造一棵 二叉排序树。 (1) 输入15,15作为根结点,见图8.8(a)。 (2) 输入20,20作为15的右子树,见图8.8(b),此时仍为平 衡树。 数据结构(C语言版) (3) 输入35,35作为20的右子树,见图8.8(c),此时结点15的 平衡因子是-2,该树为非平衡树,必须进行调整。显然要进 行逆时针右转,20作为根结点,见图8.8(d),此时又是一棵平 衡二叉树。 (4) 输入80,80作为35的右子树,见图8.8(e),此时树为一棵 平衡二叉树。 (5) 输入50,50作为80的左子树,见图8.8(f),由于树中又出 现有绝对值大于1的平衡因子,所以必须进行调整。先将50作 为35的右子树,80作为50的右子树,见图8.8(g),然后令50作 为20的右子树,35和80分别作50的左、右子树,见图8.8(h)。 (3) 输入35,35作为20的右子树,见图8.8(c),此时结点15的 平衡因子是-2,该树为非平衡树,必须进行调整。显然要进 行逆时针右转,20作为根结点,见图8.8(d),此时又是一棵平 衡二叉树。 (4) 输入80,80作为35的右子树,见图8.8(e),此时树为一棵 平衡二叉树。 (5) 输入50,50作为80的左子树,见图8.8(f),由于树中又出 现有绝对值大于1的平衡因子,所以必须进行调整。先将50作 为35的右子树,80作为50的右子树,见图8.8(g),然后令50作 为20的右子树,35和80分别作50的左、右子树,见图8.8(h)。 数据结构(C语言版) 50 15 35 20 80 80 15 35 20 3580 15 35 20 50 80 15 35 20 - 1 - 1 0 0 15 35 20 35 20 15 - 2 - 1 0 20 15 - 1 0 15 0 (a) (b) (c) (d) (e) (f) (g) (h) 50 15 35 20 80 80 15 35 20 3580 15 35 20 50 80 15 35 20 - 1 - 1 0 0 15 35 20 35 20 15 - 2 - 1 0 20 15 - 1 0 15 0 (a) (b) (c) (d) (e) (f) (g) (h) 图8.8 平衡二叉树的构造示例 数据结构(C语言版) 可见,保持二叉树为平衡二叉树的基本思想是:每当对二叉 排序树插入一个结点时,首先检查是否因插入而破坏了树的平 衡,若是,则找出其中最小的不平衡树,在保持二叉排序树的 特性情况下,调整最小不平衡子树中结点之间的关系,以达到 新的平衡。离插入结点最近且以平衡因子的绝对值大于1的结点 作根的子树被称为最小平衡树。 可见,保持二叉树为平衡二叉树的基本思想是:每当对二叉 排序树插入一个结点时,首先检查是否因插入而破坏了树的平 衡,若是,则找出其中最小的不平衡树,在保持二叉排序树的 特性情况下,调整最小不平衡子树中结点之间的关系,以达到 新的平衡。离插入结点最近且以平衡因子的绝对值大于1的结点 作根的子树被称为最小平衡树。 数据结构(C语言版) 失去平衡后调整方法可归纳为四种类型:①  LL型平衡旋转; ②  RR型平衡旋转;③  LR型平衡旋转;④  RL型平衡旋转。 平衡二叉排序树上的查找与二叉排序树中的查找相同,由 于平衡二叉树的形态不再变为单支树的情形,且比较均匀,故 在AVL树上查找的时间复杂度为O(lbn)。由于在动态平衡的过程 中所进行的调整操作仍需花费不少时间,因此AVL树的使用要 视具体情况而定。 失去平衡后调整方法可归纳为四种类型:①  LL型平衡旋转; ②  RR型平衡旋转;③  LR型平衡旋转;④  RL型平衡旋转。 平衡二叉排序树上的查找与二叉排序树中的查找相同,由 于平衡二叉树的形态不再变为单支树的情形,且比较均匀,故 在AVL树上查找的时间复杂度为O(lbn)。由于在动态平衡的过程 中所进行的调整操作仍需花费不少时间,因此AVL树的使用要 视具体情况而定。 数据结构(C语言版) 8.3 哈希表及其查找 8.3.1 哈希表与哈希函数 在前面讨论的各种结构(线性表、树等)中,记录在结构中 的相对位置是随机的,和记录的关键字之间不存在确定的关系。 因此,在结构中查找记录时需进行一系列和关键字的比较。这 一类查找方法建立在“比较”的基础上。在顺序查找时,比较 的结果为“=”与“≠”两种可能;在折半查找、二叉排序树查 找时,比较的结果为“<”、“=”和“>“三种可能。查找的效率 依赖于查找过程中所进行的比较次数。 8.3.1 哈希表与哈希函数 在前面讨论的各种结构(线性表、树等)中,记录在结构中 的相对位置是随机的,和记录的关键字之间不存在确定的关系。 因此,在结构中查找记录时需进行一系列和关键字的比较。这 一类查找方法建立在“比较”的基础上。在顺序查找时,比较 的结果为“=”与“≠”两种可能;在折半查找、二叉排序树查 找时,比较的结果为“<”、“=”和“>“三种可能。查找的效率 依赖于查找过程中所进行的比较次数。 数据结构(C语言版) 理想的情况是希望不经过任何比较,一次存取便能得到所 查记录,那就必须在记录的存储位置和它的关键字之间建立一 个确定的对应关系f,使每个关键字和结构中一个惟一的存储位 置相对应。因而在查找时,只要根据这个对应关系f找到给定值 k的像f(k)即可。若结构中存在关键字和k相等的记录,则必定 在f(k)的存储位置上,由此,不需要进行比较便可直接取得所 查记录。在此,我们称这个对应关系f为哈希(Hash)函数,按这 个设想建立的表为哈希表。 理想的情况是希望不经过任何比较,一次存取便能得到所 查记录,那就必须在记录的存储位置和它的关键字之间建立一 个确定的对应关系f,使每个关键字和结构中一个惟一的存储位 置相对应。因而在查找时,只要根据这个对应关系f找到给定值 k的像f(k)即可。若结构中存在关键字和k相等的记录,则必定 在f(k)的存储位置上,由此,不需要进行比较便可直接取得所 查记录。在此,我们称这个对应关系f为哈希(Hash)函数,按这 个设想建立的表为哈希表。 数据结构(C语言版) 举一个哈希表的最简单的例子。假设要建立一张全国30个 地区的各民族人口统计表,每个地区为一个记录,记录的各数 据项为: 编号 地区名 总人口 汉族 回族 …… 数据结构(C语言版) 显然,可以用一个一维数组c(1:30)来存放这张表,其中c[i] 是编号为i的地区的人口情况。编号i便为记录的关键字,由它惟 一确定记录的存储位置c[i]。例如,假设北京市的编号为1,则若 要查看北京市的各民族人口,只要取出c[1]的记录即可。假如把 这个数组看成是哈希表,则哈希函数f(key)=key。然而很多情况 下的哈希函数并不如此简单。不能简单地取哈希函数f(key)=key, 而是首先要将它们转化为数字,有时还要做些简单的处理。例 如,假设有这样的哈希函数:取关键字中第一个字母在字母表 中的序号作为哈希函数,其中Beijing的哈希函数值为字母“B” 在字母表中的序号,等于02,Tianjin的哈希函数值为字母“T” 在字母表中的序号,等于20,见表8.1。 显然,可以用一个一维数组c(1:30)来存放这张表,其中c[i] 是编号为i的地区的人口情况。编号i便为记录的关键字,由它惟 一确定记录的存储位置c[i]。例如,假设北京市的编号为1,则若 要查看北京市的各民族人口,只要取出c[1]的记录即可。假如把 这个数组看成是哈希表,则哈希函数f(key)=key。然而很多情况 下的哈希函数并不如此简单。不能简单地取哈希函数f(key)=key, 而是首先要将它们转化为数字,有时还要做些简单的处理。例 如,假设有这样的哈希函数:取关键字中第一个字母在字母表 中的序号作为哈希函数,其中Beijing的哈希函数值为字母“B” 在字母表中的序号,等于02,Tianjin的哈希函数值为字母“T” 在字母表中的序号,等于20,见表8.1。 数据结构(C语言版) 表8.1 简单的哈希函数示例 key Beijing (北京) Tianjin (天津) Shanghai (上海) Hebei (河北) Henan (河南) Shandong (山东) Shanxi (山西) Sichuan (四川) f(key) 02 20 19 08 08 19 19 19 从这个例子可见: (1) 哈希函数是一个映像,因此哈希函数的给定很灵活,只要 使得任何关键字由此所得的哈希函数值都落在表长允许范围之 内即可。 (2) 对不同的关键字可能得到同一哈希地址,即key1≠key2, 而f(key1)=f(key2),这种现象称为冲突(Collision)。 从这个例子可见: (1) 哈希函数是一个映像,因此哈希函数的给定很灵活,只要 使得任何关键字由此所得的哈希函数值都落在表长允许范围之 内即可。 (2) 对不同的关键字可能得到同一哈希地址,即key1≠key2, 而f(key1)=f(key2),这种现象称为冲突(Collision)。 数据结构(C语言版) 综上所述,可像如下这样描述哈希表:根据设定的哈希函数 H(key)和处理冲突的方法将一组关键字映像到一个有限的连续 的地址集(区间)上,并以关键字在地址集中的“像”作为记录 在表中的存储位置,这种表便称为哈希表,这一映像过程称为 哈希表或散列,所得存储位置称哈希地址或散列地址。 下面分别就哈希函数和处理冲突的方法进行讨论。 综上所述,可像如下这样描述哈希表:根据设定的哈希函数 H(key)和处理冲突的方法将一组关键字映像到一个有限的连续 的地址集(区间)上,并以关键字在地址集中的“像”作为记录 在表中的存储位置,这种表便称为哈希表,这一映像过程称为 哈希表或散列,所得存储位置称哈希地址或散列地址。 下面分别就哈希函数和处理冲突的方法进行讨论。 数据结构(C语言版) 8.3.2 构造哈希函数的常用方法 构造哈希函数的方法很多,如何构造一个“好”的哈希函数 是技术性和实践性都很强的问题。这里的“好”指的是哈希函 数的构造比较简单,并且用此哈希函数产生的映像所发生冲突 的可能性最小,换句话说,一个好的哈希函数能将给定的数据 集合均匀地映射到所给定的地址区间中。 我们知道,关键字可以惟一地对应一个记录,因此,在构造 哈希函数时,应尽可能地使关键字的各个成分都对它的哈希地 址产生影响。下面介绍几种常用的构造哈希函数的方法。 8.3.2 构造哈希函数的常用方法 构造哈希函数的方法很多,如何构造一个“好”的哈希函数 是技术性和实践性都很强的问题。这里的“好”指的是哈希函 数的构造比较简单,并且用此哈希函数产生的映像所发生冲突 的可能性最小,换句话说,一个好的哈希函数能将给定的数据 集合均匀地映射到所给定的地址区间中。 我们知道,关键字可以惟一地对应一个记录,因此,在构造 哈希函数时,应尽可能地使关键字的各个成分都对它的哈希地 址产生影响。下面介绍几种常用的构造哈希函数的方法。 数据结构(C语言版) 1. 直接地址法 对于关键字是整数类型的数据,直接地址法的哈希函数H直 接利用关键字求得哈希地址。 H(Ki)=aKi+b,(a,b为常量) 在使用时,为了使哈希地址与存储空间吻合,可以调整a和b。 例如,取H(Ki)=Ki+10。 直接地址法的特点是:哈希函数简单,并且对于不同的关 键字不会产生冲突。但在实际问题中,由于关键字集中的元素 很少是连续的,用该方法产生的哈希表会造成空间的大量浪费。 因此,这种方法很少使用。 1. 直接地址法 对于关键字是整数类型的数据,直接地址法的哈希函数H直 接利用关键字求得哈希地址。 H(Ki)=aKi+b,(a,b为常量) 在使用时,为了使哈希地址与存储空间吻合,可以调整a和b。 例如,取H(Ki)=Ki+10。 直接地址法的特点是:哈希函数简单,并且对于不同的关 键字不会产生冲突。但在实际问题中,由于关键字集中的元素 很少是连续的,用该方法产生的哈希表会造成空间的大量浪费。 因此,这种方法很少使用。 数据结构(C语言版) 2. 数字分析法 数字分析法是假设有一组关键字,每个关键字由几位数字组 成,如K1 K2 K3…Kn。数字分析法是从中提取数字分布比较均 匀的若干位作为哈希地址。 例如,对于关键字K1到K8的序列{100011211,100011322, 100011413,100011556,100011613,100011756,100011822, 100011911},可以取第6位和第7位作为哈希地址,即H(K1)=12, H(K2)=13,H(K3)=14,H(K4)=15,H(K5)=16,H(K6)=17, H(K7)=18,H(K8)=19。 2. 数字分析法 数字分析法是假设有一组关键字,每个关键字由几位数字组 成,如K1 K2 K3…Kn。数字分析法是从中提取数字分布比较均 匀的若干位作为哈希地址。 例如,对于关键字K1到K8的序列{100011211,100011322, 100011413,100011556,100011613,100011756,100011822, 100011911},可以取第6位和第7位作为哈希地址,即H(K1)=12, H(K2)=13,H(K3)=14,H(K4)=15,H(K5)=16,H(K6)=17, H(K7)=18,H(K8)=19。 数据结构(C语言版) 3. 平方取中法 平方取中法是取关键字平方的中间几位作为散列地址的方法, 具体取多少位视实际情况而定,即 H(Ki)=“Ki的平方的中间几位” 这也是一种常用的较好的设计哈希函数的方法。关键字平 方后使得它的中间几位和组成关键字的多位值均有关,从而使 哈希地址的分布更为均匀,减少了发生冲突的可能性。 3. 平方取中法 平方取中法是取关键字平方的中间几位作为散列地址的方法, 具体取多少位视实际情况而定,即 H(Ki)=“Ki的平方的中间几位” 这也是一种常用的较好的设计哈希函数的方法。关键字平 方后使得它的中间几位和组成关键字的多位值均有关,从而使 哈希地址的分布更为均匀,减少了发生冲突的可能性。 数据结构(C语言版) 4. 折叠法 折叠法是首先把关键字分割成位数相同的几段(最后一段的 位数可少一些),段的位数取决于哈希地址的位数,由实际情况 而定,然后将它们的叠加和(舍去最高进位)作为哈希地址的方 法。 与平方取中法类似,折叠法也使得关键字的各位值都对哈 希地址产生影响。 4. 折叠法 折叠法是首先把关键字分割成位数相同的几段(最后一段的 位数可少一些),段的位数取决于哈希地址的位数,由实际情况 而定,然后将它们的叠加和(舍去最高进位)作为哈希地址的方 法。 与平方取中法类似,折叠法也使得关键字的各位值都对哈 希地址产生影响。 数据结构(C语言版) 5. 除留余数法 除留余数法是用关键字Ki除以一个合适的不大于哈希表长度 的正整数P,所得余数作为哈希地址的方法。对应的哈希函数 H(Ki)为: H(Ki)=Ki MOD P 这里的MOD表示求余数运算,用该方法产生的哈希函数的好坏 取决于P值的选取。实践证明,当P取小于哈希表长的最大质数 时,产生的哈希函数较好。 除留余数法是一种简单而行之有效的构造哈希函数的方法。 5. 除留余数法 除留余数法是用关键字Ki除以一个合适的不大于哈希表长度 的正整数P,所得余数作为哈希地址的方法。对应的哈希函数 H(Ki)为: H(Ki)=Ki MOD P 这里的MOD表示求余数运算,用该方法产生的哈希函数的好坏 取决于P值的选取。实践证明,当P取小于哈希表长的最大质数 时,产生的哈希函数较好。 除留余数法是一种简单而行之有效的构造哈希函数的方法。 数据结构(C语言版) 8.3.3 解决冲突的主要方法 1. 开放地址法 开放地址法又分为线性探测再散列、二次探测再散列和随机 探测再散列。 假设哈希表空间为T(0..M-1),哈希函数为H(Ki)。MOD表示 求模运算。线性探测再散列解决冲突求“下一个”地址的公式 是: d1=H(Ki) dj=(dj-1+j) MOD M (j=1, 2, 1. 开放地址法 开放地址法又分为线性探测再散列、二次探测再散列和随机 探测再散列。 假设哈希表空间为T(0..M-1),哈希函数为H(Ki)。MOD表示 求模运算。线性探测再散列解决冲突求“下一个”地址的公式 是: d1=H(Ki) dj=(dj-1+j) MOD M (j=1, 2, 数据结构(C语言版) 二次探测再散列解决冲突求“下一个”地址的公式是: d1=H(Ki) d2j=(d1+j2) MOD M d2j+1=(d1-j2) MOD M (j=1, 2, …, M/2) 随机探测再散列解决冲突求“下一个”地址的公式是: d1=H(Ki) dj+1=(d1+R) MOD M 其中,R为伪随机数序列。 二次探测再散列解决冲突求“下一个”地址的公式是: d1=H(Ki) d2j=(d1+j2) MOD M d2j+1=(d1-j2) MOD M (j=1, 2, …, M/2) 随机探测再散列解决冲突求“下一个”地址的公式是: d1=H(Ki) dj+1=(d1+R) MOD M 其中,R为伪随机数序列。 数据结构(C语言版) 例8.5 使用的哈希函数为: H(Ki)=3Ki MOD 11 并采用开放地址法处理冲突,其求下一个地址的函数为: d1=H(Ki) di=(di-1+(7Ki MOD10)+1)MOD 11 (i=2, 3,…) 试在0~10的散列地址空间中对关键字序列(22,41,53, 46,30,13,01,67)构造哈希表,求等概率情况下查找成功的 平均查找长度,并设计构造哈希表的完整函数。 例8.5 使用的哈希函数为: H(Ki)=3Ki MOD 11 并采用开放地址法处理冲突,其求下一个地址的函数为: d1=H(Ki) di=(di-1+(7Ki MOD10)+1)MOD 11 (i=2, 3,…) 试在0~10的散列地址空间中对关键字序列(22,41,53, 46,30,13,01,67)构造哈希表,求等概率情况下查找成功的 平均查找长度,并设计构造哈希表的完整函数。 数据结构(C语言版) 本题的哈希表构造过程如下: (1)  H(22)=3*22 MOD 11=0 (2)  H(41)=3*41 MOD 11=2 (3)  H(53)=3*53 MOD 11=5 (4)  H(46)=3*46 MOD 11=6 (5)  H(30)=3*30 MOD 11=2 (冲突)  d1=H(30)=2    H(30)=(2+(7*30 MOD 10)+1) MOD 11=3 (6)  H(13)=3*13 MOD 11=6 (冲突)  d1=H(13)=6    H(13)=(6+(7*13 MOD 10)+1) MOD 11=8 本题的哈希表构造过程如下: (1)  H(22)=3*22 MOD 11=0 (2)  H(41)=3*41 MOD 11=2 (3)  H(53)=3*53 MOD 11=5 (4)  H(46)=3*46 MOD 11=6 (5)  H(30)=3*30 MOD 11=2 (冲突)  d1=H(30)=2    H(30)=(2+(7*30 MOD 10)+1) MOD 11=3 (6)  H(13)=3*13 MOD 11=6 (冲突)  d1=H(13)=6    H(13)=(6+(7*13 MOD 10)+1) MOD 11=8 数据结构(C语言版) (7)  H(1)=3*01 MOD 11=3 (冲突)    d1=H(1)=3    H(1)=(3+(1*7 MOD 10)+1) MOD 11=0 (冲突)    d2=H(1)=0    H(1)=(0+(1*7 MOD 10)+1)MOD 11=8 (冲突)    d3=H(1)=8    H(1)=(8+(1*7 MOD 10)+1) MOD 11=5 (冲突)    d4=H(1)=5    H(1)=(5+(1*7 MOD 10)+1)MOD 11=2 (冲突)    d5=H(1)=2    H(1)=(2+(1*7 MOD 10)+1)MOD 11=10 (7)  H(1)=3*01 MOD 11=3 (冲突)    d1=H(1)=3    H(1)=(3+(1*7 MOD 10)+1) MOD 11=0 (冲突)    d2=H(1)=0    H(1)=(0+(1*7 MOD 10)+1)MOD 11=8 (冲突)    d3=H(1)=8    H(1)=(8+(1*7 MOD 10)+1) MOD 11=5 (冲突)    d4=H(1)=5    H(1)=(5+(1*7 MOD 10)+1)MOD 11=2 (冲突)    d5=H(1)=2    H(1)=(2+(1*7 MOD 10)+1)MOD 11=10 数据结构(C语言版) (8)  H(67)=3*67 MOD 11=3 ( 冲突)    d1=H(67)=3    H(67)=(3+(7*67 MOD 10)+1) MOD 11=2 (冲突)    d2=H(67)=2    H(67)=(2+(7*67 MOD 10)+1) MOD 11=1 查找成功的平均查找长度为: ASL=(1×4+2×2+3×1+6×1)×(1/8)=2.125 (8)  H(67)=3*67 MOD 11=3 ( 冲突)    d1=H(67)=3    H(67)=(3+(7*67 MOD 10)+1) MOD 11=2 (冲突)    d2=H(67)=2    H(67)=(2+(7*67 MOD 10)+1) MOD 11=1 查找成功的平均查找长度为: ASL=(1×4+2×2+3×1+6×1)×(1/8)=2.125 数据结构(C语言版) 构造本哈希表的程序如下: /*算法描述8.7*/ #include #define M 11 #define N 8 struct hterm { int key; /*关键字值*/ int si; /*散列次数*/ } 构造本哈希表的程序如下: /*算法描述8.7*/ #include #define M 11 #define N 8 struct hterm { int key; /*关键字值*/ int si; /*散列次数*/ } 数据结构(C语言版) struct hterm hashlist[M+1]; int i, address, sum, d, x[N+1]; float average; main() { for(i=1;i<=M;i++) /*置初值*/ { hashlist[i].key=0; hashlist[i].si=0; } struct hterm hashlist[M+1]; int i, address, sum, d, x[N+1]; float average; main() { for(i=1;i<=M;i++) /*置初值*/ { hashlist[i].key=0; hashlist[i].si=0; } 数据结构(C语言版) x[1]=22; x[2]=41; x[3]=53; x[4]=46; x[5]=30; x[6]=13; x[7]=1; x[8]=67; for(i=1;i<=N,i++) { sum=0; address=(3 *x[i]) % M; d=address; if (hashlist [address].key==0) { hashlist[address].key=x[i]; hashlist[address].si=1; } x[1]=22; x[2]=41; x[3]=53; x[4]=46; x[5]=30; x[6]=13; x[7]=1; x[8]=67; for(i=1;i<=N,i++) { sum=0; address=(3 *x[i]) % M; d=address; if (hashlist [address].key==0) { hashlist[address].key=x[i]; hashlist[address].si=1; } 数据结构(C语言版) else { do /*处理冲突*/ { d=(d+(x[i]*7) %10+1) % 11; sum=sum+1; } while (hashlist[d].key!=0); hashlist[d].key=x[i]; hashlist[d].si=sum+1; } } else { do /*处理冲突*/ { d=(d+(x[i]*7) %10+1) % 11; sum=sum+1; } while (hashlist[d].key!=0); hashlist[d].key=x[i]; hashlist[d].si=sum+1; } } 数据结构(C语言版) printf("哈希表地址:"); for (i=0; i #define MAX 100 int ha[MAX], hlen[MAX ], n, m, p; void creathash() { int i, j, d, d1, odd, s, key[MAX]; printf ("﹦﹦﹦﹦﹦建立散列表﹦﹦﹦﹦﹦\n") printf ("输入元素个数n:"); scanf ("%d",&n); 实验程序 #include #define MAX 100 int ha[MAX], hlen[MAX ], n, m, p; void creathash() { int i, j, d, d1, odd, s, key[MAX]; printf ("﹦﹦﹦﹦﹦建立散列表﹦﹦﹦﹦﹦\n") printf ("输入元素个数n:"); scanf ("%d",&n); 数据结构(C语言版) printf ("输入哈希表长m:"); scanf ("%d",&m); printf ("散列函数:h(k)=k mod p:"); scanf ("%d",&p); for (i=0; i (CR表示回车) 输入哈希表长m:18 散列函数:h(k)=k mod p :17 第1个元素:53 第2个元素:17 第3个元素:12 第4个元素:61 第5个元素:98 第6个元素:70 程序执行过程如下: ﹦﹦﹦﹦﹦﹦﹦建立散列表﹦﹦﹦﹦﹦﹦﹦﹦ 输入元素个数n:14 (CR表示回车) 输入哈希表长m:18 散列函数:h(k)=k mod p :17 第1个元素:53 第2个元素:17 第3个元素:12 第4个元素:61 第5个元素:98 第6个元素:70 数据结构(C语言版) 第7个元素:87 第8个元素:25 第9个元素:63 第10个元素:46 第11个元素:14 第12个元素:59 第13个元素:67 第14个元素:75 第7个元素:87 第8个元素:25 第9个元素:63 第10个元素:46 第11个元素:14 第12个元素:59 第13个元素:67 第14个元素:75 数据结构(C语言版) 哈希表建立如下: H(53)=53 MOD 17 =2 比较1次 H(17)=17 MOD 17 =0 比较1次 H(12)=12 MOD 17 =12 比较1次 H(61)=61 MOD 17 =10 比较1次 H(98)=98 MOD 17 =13 比较1次 H(70)=70 MOD 17 =2 冲突 H(70)=(2+1*1) MOD 18 =3 比较2次 H(87)=87 MOD 17 =2 冲突 哈希表建立如下: H(53)=53 MOD 17 =2 比较1次 H(17)=17 MOD 17 =0 比较1次 H(12)=12 MOD 17 =12 比较1次 H(61)=61 MOD 17 =10 比较1次 H(98)=98 MOD 17 =13 比较1次 H(70)=70 MOD 17 =2 冲突 H(70)=(2+1*1) MOD 18 =3 比较2次 H(87)=87 MOD 17 =2 冲突 数据结构(C语言版) H(87)= (2+1*1) MOD 18 =3 冲突 H(87)= (2-1*1) MOD 18 =1 比较3次 H(25)=25 MOD 17 =8 比较1次 H(63)=63 MOD 17 =12 冲突 H(63)= (12+ 1*1) MOD 18 =13 冲突 H(63)= (12-1*1) MOD 18 =11 比较3次 H(46)=46 MOD 17 =12 冲突 H(46)= (12+1*1) MOD 18 =13 冲突 H(46)= (12-1*1) MOD 18 =11 冲突 H(87)= (2+1*1) MOD 18 =3 冲突 H(87)= (2-1*1) MOD 18 =1 比较3次 H(25)=25 MOD 17 =8 比较1次 H(63)=63 MOD 17 =12 冲突 H(63)= (12+ 1*1) MOD 18 =13 冲突 H(63)= (12-1*1) MOD 18 =11 比较3次 H(46)=46 MOD 17 =12 冲突 H(46)= (12+1*1) MOD 18 =13 冲突 H(46)= (12-1*1) MOD 18 =11 冲突 数据结构(C语言版) H(46)= (12+2*2) MOD 18 =16 比较4次 H (14)= 14 MOD 17 =14 比较1次 H (59)= 59 MOD 17 =8 冲突 H (59)= (8+1*1) MOD 18 =9 比较2次 H (67)= 67 MOD 17 =16 冲突 H (67)= (16+1*1) MOD 18 =17 比较2次 H (75)= 75 MOD 17 =7 比较1次 H(46)= (12+2*2) MOD 18 =16 比较4次 H (14)= 14 MOD 17 =14 比较1次 H (59)= 59 MOD 17 =8 冲突 H (59)= (8+1*1) MOD 18 =9 比较2次 H (67)= 67 MOD 17 =16 冲突 H (67)= (16+1*1) MOD 18 =17 比较2次 H (75)= 75 MOD 17 =7 比较1次 数据结构(C语言版) 散列表H为: 0 1 2 3 4 5 6 7 8 9 1 0 1 1 1 2 1 3 1 4 1 5 1 6 1 7 17 87 53 70 0 0 0 75 25 59 61 63 12 98 14 0 46 67 1 3 1 2 0 0 0 1 1 2 1 3 1 1 1 0 4 21 3 1 2 0 0 0 1 1 2 1 3 1 1 1 0 4 2 ASL(14)=1.71 输入查找的值:70 查找值:ha[3]=70! 数据结构(C语言版) 习 题 8 1. 单项选择题: (1) 顺序查找法适合于存储结构为_______的线性表。 A) 散列存储 B) 顺序存储或链接存储 C) 压缩存储 D) 索引存储 (2) 采用顺序查找方法查找长度为n的线性表时,每个元素的平 均查找长度为_______。 A)  n B)  n/2 C)  (n+1)/2 D  (n-1)/2 1. 单项选择题: (1) 顺序查找法适合于存储结构为_______的线性表。 A) 散列存储 B) 顺序存储或链接存储 C) 压缩存储 D) 索引存储 (2) 采用顺序查找方法查找长度为n的线性表时,每个元素的平 均查找长度为_______。 A)  n B)  n/2 C)  (n+1)/2 D  (n-1)/2 数据结构(C语言版) (3) 采用折半查找方法查找长度为n的线性表时,每个元素 的平均查找长度为_______。 A)  O(n2) B)  O(n lb n) C)  O(n) D)  O(lb n) (4) 有一个有序表为{1,3,9,12,32,41,45,62,75, 77,82,95,100},当二分查找值为82的结点时,_______次 比较后查找成功。 A)  1 B)  2 C)  4 D)  8 (3) 采用折半查找方法查找长度为n的线性表时,每个元素 的平均查找长度为_______。 A)  O(n2) B)  O(n lb n) C)  O(n) D)  O(lb n) (4) 有一个有序表为{1,3,9,12,32,41,45,62,75, 77,82,95,100},当二分查找值为82的结点时,_______次 比较后查找成功。 A)  1 B)  2 C)  4 D)  8 数据结构(C语言版) (5) 设哈希表长m=14,哈希函数H(key)=key%11。表中已有4 个结点: Addr(15)=4 Addr(38)=5 Addr (61)=6 Addr(84)=7 其余地址为空 如用二次探测再散列处理冲突,关键字为49的结点的地址是 _______。 A)  8 B)  3 C)  5 D)  9 (5) 设哈希表长m=14,哈希函数H(key)=key%11。表中已有4 个结点: Addr(15)=4 Addr(38)=5 Addr (61)=6 Addr(84)=7 其余地址为空 如用二次探测再散列处理冲突,关键字为49的结点的地址是 _______。 A)  8 B)  3 C)  5 D)  9 数据结构(C语言版) 2. 填空题: (1) 顺序查找法的平均查找长度为_______;二分查找法的平 均查找长度为_______;分块查找法(以顺序查找确定块)的平均 查找长度为_______;分块查找法(以二分查找确定块)的平均查 找长度_______;哈希表查找法采用链接法处理冲突时的平均查 找长度为_______。 (2) 在多种查找方法中,平均查找长度与结点个数n无关的查 找方法是_______。 (3) 二 分 查 找(折 半 查 找)的 存 储 结 构 仅 限 于_______且 是 _______。 (4) 在分块查找方法中,首先查找_______,然后再查找相 应的_______。 2. 填空题: (1) 顺序查找法的平均查找长度为_______;二分查找法的平 均查找长度为_______;分块查找法(以顺序查找确定块)的平均 查找长度为_______;分块查找法(以二分查找确定块)的平均查 找长度_______;哈希表查找法采用链接法处理冲突时的平均查 找长度为_______。 (2) 在多种查找方法中,平均查找长度与结点个数n无关的查 找方法是_______。 (3) 二 分 查 找(折 半 查 找)的 存 储 结 构 仅 限 于_______且 是 _______。 (4) 在分块查找方法中,首先查找_______,然后再查找相 应的_______。 数据结构(C语言版) (5) 假设在有序线性表A[1··20]上进行二分查找,则比较 一次查找成功的结点数为_______,比较两次查找成功的结点 数为_______,比较三次查找成功的结点数为_______,比较四 次查找成功的结点数为_______,则比较五次查找成功的结点 数为_______,平均查找长度为_______。 (5) 假设在有序线性表A[1··20]上进行二分查找,则比较 一次查找成功的结点数为_______,比较两次查找成功的结点 数为_______,比较三次查找成功的结点数为_______,比较四 次查找成功的结点数为_______,则比较五次查找成功的结点 数为_______,平均查找长度为_______。 数据结构(C语言版) 3. 设有一组关健字{19, 01, 23, 14, 55, 20, 84, 27, 68, 11,10, 77},所用哈希函数为 H(ki)=ki MOD 13 采用开放地址法的线性探测再散列方法解决冲突,试在 0~18的散列地址空间中对该关键字序列构造哈希表。 3. 设有一组关健字{19, 01, 23, 14, 55, 20, 84, 27, 68, 11,10, 77},所用哈希函数为 H(ki)=ki MOD 13 采用开放地址法的线性探测再散列方法解决冲突,试在 0~18的散列地址空间中对该关键字序列构造哈希表。 数据结构(C语言版) 第9章 排 序 9.1 排序的基本概念 9.2 插入排序 9.3 交换排序 9.4 选择排序 9.5 内部排序方法的比较 9.6 实习: 排序算法的实现——学生成绩管理 习题9 9.1 排序的基本概念 9.2 插入排序 9.3 交换排序 9.4 选择排序 9.5 内部排序方法的比较 9.6 实习: 排序算法的实现——学生成绩管理 习题9 数据结构(C语言版) 9.1 排序的基本概念 排序(Sorting)是把一个无序的数据元素序列按某个关键字进行 有序(递增或递减)排列的过程。排序中经常把数据元素称为记录 (Record)。把记录中作为排序依据的某个数据项称为排序关键字, 简称关键字(Key)。 排序时选取哪一个数据项作为关键字,应根据具体情况而 定。例如,表9.1为某次考试成绩表,表中每个学生的记录包括 考号、姓名、三门课成绩以及这三门课的总分。 排序(Sorting)是把一个无序的数据元素序列按某个关键字进行 有序(递增或递减)排列的过程。排序中经常把数据元素称为记录 (Record)。把记录中作为排序依据的某个数据项称为排序关键字, 简称关键字(Key)。 排序时选取哪一个数据项作为关键字,应根据具体情况而 定。例如,表9.1为某次考试成绩表,表中每个学生的记录包括 考号、姓名、三门课成绩以及这三门课的总分。 数据结构(C语言版) 表9.1 学生成绩表 考号 姓名 英语 数学 微机 总分 010 李 明 89 80 91 260 202 王小萌 76 92 68 236 103 张 炎 78 85 77 240 204 赵 沼 94 82 98 274 011 王 丽 77 89 90 256 数据结构(C语言版) 以“考号”作为关键字排序,可以快速查找到某个学生的成 绩,因为考号可以惟一识别一个学生的记录。若想以“总分” 排列名次,就应把“总分”作为关键字对成绩表进行排序。 待排序的记录可以是任意的数据类型,其关键字可以是整 型、实型、实符型等基本数据类型,通过排序可以构造一种新 的按关键字有序的排列。如果待排序的记录序列中存在关键字 相同的记录,例如,有一序列(10,45,12,32,45,78),其中45区别 于45。排序前45在序列中的位置先于45,排序后的新序列若为 (10,12,32,45,45,78),45的位置仍先于45,则称这种排序方法是 稳定的;反之,如果数据序列变为(10,12,32,45,45,78),此排序 方法是不稳定的。 以“考号”作为关键字排序,可以快速查找到某个学生的成 绩,因为考号可以惟一识别一个学生的记录。若想以“总分” 排列名次,就应把“总分”作为关键字对成绩表进行排序。 待排序的记录可以是任意的数据类型,其关键字可以是整 型、实型、实符型等基本数据类型,通过排序可以构造一种新 的按关键字有序的排列。如果待排序的记录序列中存在关键字 相同的记录,例如,有一序列(10,45,12,32,45,78),其中45区别 于45。排序前45在序列中的位置先于45,排序后的新序列若为 (10,12,32,45,45,78),45的位置仍先于45,则称这种排序方法是 稳定的;反之,如果数据序列变为(10,12,32,45,45,78),此排序 方法是不稳定的。 数据结构(C语言版) 排序的方式根据待排记录数量不同可以分为两类: (1) 在排序过程中,只使用计算机的内存储器存放待排序的 记录,称为内部排序。内部排序用于排序的记录个数较少时, 全部排序可在内存中完成,不涉及外存储器,因此,排序速度 快。 (2) 当排序的记录数很大时,全部记录不能同时存放在内 存中,需要借助外存储器,也就是说排序过程中不仅要使用内 存,还要使用外存,记录要在内、外存之间移动,这种排序称 为外部排序。外部排序运行速度较慢。 排序的方式根据待排记录数量不同可以分为两类: (1) 在排序过程中,只使用计算机的内存储器存放待排序的 记录,称为内部排序。内部排序用于排序的记录个数较少时, 全部排序可在内存中完成,不涉及外存储器,因此,排序速度 快。 (2) 当排序的记录数很大时,全部记录不能同时存放在内 存中,需要借助外存储器,也就是说排序过程中不仅要使用内 存,还要使用外存,记录要在内、外存之间移动,这种排序称 为外部排序。外部排序运行速度较慢。 数据结构(C语言版) 本章只讨论内部排序,不涉及外部排序。 内部排序的方法很多,但不论哪种排序过程,通常都要进行 两种基本操作: (1) 比较两个记录关键字的大小。 (2) 根据比较结果,将记录从一个位置移到另一个位置。 所以,在分析排序算法的时间复杂度时,主要分析关键字的 比较次数和记录的移动次数。 特别需要说明的是:本章介绍的排序算法都是采用顺序存 储结构,即用数组存储,且按关键字递增排序。函数中记录类 型及数组结构定义如下: 本章只讨论内部排序,不涉及外部排序。 内部排序的方法很多,但不论哪种排序过程,通常都要进行 两种基本操作: (1) 比较两个记录关键字的大小。 (2) 根据比较结果,将记录从一个位置移到另一个位置。 所以,在分析排序算法的时间复杂度时,主要分析关键字的 比较次数和记录的移动次数。 特别需要说明的是:本章介绍的排序算法都是采用顺序存 储结构,即用数组存储,且按关键字递增排序。函数中记录类 型及数组结构定义如下: 数据结构(C语言版) # define Maxsize 100 typedef struct Recordnode { keytype key;   elemtype data; } Recordnode; Recordnode r[Maxsize] 这里的keytype和elemtype可以是任何相应的数据类型,如int, float及char等,在算法中规定keytype和elemtype默认为int型。 # define Maxsize 100 typedef struct Recordnode { keytype key;   elemtype data; } Recordnode; Recordnode r[Maxsize] 这里的keytype和elemtype可以是任何相应的数据类型,如int, float及char等,在算法中规定keytype和elemtype默认为int型。 数据结构(C语言版) 9.2 插 入 排 序 9.2.1 直接插入排序 直接插入排序是最简单的排序方法之一。其基本思想是:在 有序区中进行顺序查找,以确定插入的位置,然后移动记录腾 出空间,以便插入关键字相应的记录。 例9.1 设有6个待排序的记录,它们排序的关键字序列为{20, 6,15,7,3,6}。在顺序查找中,为了防止循环变量越界, 在有序区前段增设了一个“岗哨”r[0],暂存当前待插入的记 录。具体的排序过程如图9.1所示。 9.2.1 直接插入排序 直接插入排序是最简单的排序方法之一。其基本思想是:在 有序区中进行顺序查找,以确定插入的位置,然后移动记录腾 出空间,以便插入关键字相应的记录。 例9.1 设有6个待排序的记录,它们排序的关键字序列为{20, 6,15,7,3,6}。在顺序查找中,为了防止循环变量越界, 在有序区前段增设了一个“岗哨”r[0],暂存当前待插入的记 录。具体的排序过程如图9.1所示。 数据结构(C语言版) r[0] 岗哨初始序列: 第一趟: 第二趟: r[1] r[2] r[3] r[4] r[5] r[6] 20 6 15 7 3 6 6 [6 20] 15 7 3 6 15 第三趟: 7 第四趟: 3 第五趟: 6 [6 [6 [3 [3 15 7 6 6 20] 15 7 6 7 20] 15 7 3 3 20] 15 20] 6 6 6 r[0] 岗哨初始序列: 第一趟: 第二趟: r[1] r[2] r[3] r[4] r[5] r[6] 20 6 15 7 3 6 6 [6 20] 15 7 3 6 15 第三趟: 7 第四趟: 3 第五趟: 6 [6 [6 [3 [3 15 7 6 6 20] 15 7 6 7 20] 15 7 3 3 20] 15 20] 6 6 6 图9.1 直接插入排序示例 数据结构(C语言版) 从例9.1看出:① 直接插入排序是从第二个记录开始的,对 记录数为6的序列,需要进行5趟排序才能完成;②  6和6的相对 位置没有变化。因此,直接插入排序是稳定的排序方法。 数据结构(C语言版) 直接插入排序算法描述如算法9.1。 /*算法描述9.1 直接插入排序*/ Insertsort(Recordnode r[],int n) /*对r[1]~r[n]进行插入排序*/ {int i,j;   for(i=2;i<=n;i++) { r[0]=r[i]; /*待插入记录送入岗哨*/ j=i-1; while (r[0].keyr[mid].key,令 low=mid+1,继续查找;若r[i].keyr[mid].key,令 low=mid+1,继续查找;若r[i].key=low;j--) r[j+1]=r[j]; /*记录后移*/ r[low]=r[0]; /*插入*/ } } if(r[0].key=low;j--) r[j+1]=r[j]; /*记录后移*/ r[low]=r[0]; /*插入*/ } } 算法分析:折半插入排序的比较次数比直接插入排序的少, 而移动次数相同,因此,总的时间复杂度仍为O(n2),另外,折 半插入排序也是一种稳定的排序方法。 数据结构(C语言版) 9.2.3 希尔排序 希尔排序(Shell’s Sort)也称为“缩小增量法排序”,是由 D.L.Shell对直接插入排序进行改进后提出来的。其基本思想是: 不断地把待排序的一组记录按间隔值分成若干小组,分别进行 组内直接插入排序,待整个序列中的记录“基本有序”时,再 对全体记录进行依次直接插入排序。 对间隔值(用d表示)的取法有多种,希尔提出的方法是: d1=[n/2],di+1=[di/2],最后一次排序时间的间隔值必须为1,其 中n为记录数。 9.2.3 希尔排序 希尔排序(Shell’s Sort)也称为“缩小增量法排序”,是由 D.L.Shell对直接插入排序进行改进后提出来的。其基本思想是: 不断地把待排序的一组记录按间隔值分成若干小组,分别进行 组内直接插入排序,待整个序列中的记录“基本有序”时,再 对全体记录进行依次直接插入排序。 对间隔值(用d表示)的取法有多种,希尔提出的方法是: d1=[n/2],di+1=[di/2],最后一次排序时间的间隔值必须为1,其 中n为记录数。 数据结构(C语言版) 例9.3 设待排序的记录数n为8,它们的关键字分别为{47, 55,10,40,15,94,5,70},间隔值序列取4、2、1。希尔排 序过程如图9.3所示。 数据结构(C语言版) r[1] r[2] r[3] r[4] r[5] 47 55 10 40 15 94 r[6] 5 r[7] 70 r[8] 初始关键字: (1)第一趟排序(d= 4):分4组,组内采用直接插入排序法。 {47 {55 15} 94} {10 5} {40 70} 结果: 15 55 5 40 47 94 10 70 (2)第二趟排序(d= 2):分2组,组内采用直接插入排序法。 {15 5 47 10} {55 40 94 70} 结果: 5 40 10 55 15 70 47 94 (3)第三趟排序(d=1):整个数据合成1组,采用直接插入排序法。 {5 40 10 55 15 70 47 94} 最后结果: 5 10 15 40 47 55 70 94 r[1] r[2] r[3] r[4] r[5] 47 55 10 40 15 94 r[6] 5 r[7] 70 r[8] 初始关键字: (1)第一趟排序(d= 4):分4组,组内采用直接插入排序法。 {47 {55 15} 94} {10 5} {40 70} 结果: 15 55 5 40 47 94 10 70 (2)第二趟排序(d= 2):分2组,组内采用直接插入排序法。 {15 5 47 10} {55 40 94 70} 结果: 5 40 10 55 15 70 47 94 (3)第三趟排序(d=1):整个数据合成1组,采用直接插入排序法。 {5 40 10 55 15 70 47 94} 最后结果: 5 10 15 40 47 55 70 94 图9.3 希尔排序示例 数据结构(C语言版) 希尔排序算法描述如下: /*算法描述9.3 希尔排序*/ Shellsort (Recordnode r[],int n) { int i,j,d; struct recordnode x; d=n/2; /*设置初值*/ while(d>1) {for(i=d+1;i<=n;i++) {j=i-d; while(j>=0) 希尔排序算法描述如下: /*算法描述9.3 希尔排序*/ Shellsort (Recordnode r[],int n) { int i,j,d; struct recordnode x; d=n/2; /*设置初值*/ while(d>1) {for(i=d+1;i<=n;i++) {j=i-d; while(j>=0) 数据结构(C语言版) if (r[j].key>r[j+d].key) {x=r[j]; /*将r[j]与r[j+d]进行交换*/ r[j]=r[j+d]; r[j+d]=x; j=j-d; } else j=0; } d=d/2; /*减小间隔值*/ } } if (r[j].key>r[j+d].key) {x=r[j]; /*将r[j]与r[j+d]进行交换*/ r[j]=r[j+d]; r[j+d]=x; j=j-d; } else j=0; } d=d/2; /*减小间隔值*/ } } 数据结构(C语言版) 该算法通过三重循环来实现,外循环由不同间隔值d来控制, 直到d=1为止,中间循环是在某个d值下对各组进行排序,用变 量i控制。 可以看出,希尔排序实际上是对直接插入排序的一种改进, 它的排序速度一般要比直接插入排序快。希尔排序的时间复杂 速度取决于所取的间隔,一般认为是O(n lb n)。希尔排序是一 个较复杂的问题,并且它还是一种不稳定的排序方法。 该算法通过三重循环来实现,外循环由不同间隔值d来控制, 直到d=1为止,中间循环是在某个d值下对各组进行排序,用变 量i控制。 可以看出,希尔排序实际上是对直接插入排序的一种改进, 它的排序速度一般要比直接插入排序快。希尔排序的时间复杂 速度取决于所取的间隔,一般认为是O(n lb n)。希尔排序是一 个较复杂的问题,并且它还是一种不稳定的排序方法。 数据结构(C语言版) 9.3 交 换 排 序 9.3.1 冒泡排序 冒泡排序是一种简单常用的排序方法。其排序思想是:通 过相邻记录关键字间的比较和交换,使关键字最小的记录像气 泡一样逐渐上浮。比较可以采用不同的方法,本算法是从最下 面的记录开始,对两个相邻的关键字进行比较并且使关键字较 小的记录换至关键字较大的记录之上,使得经过一次冒泡后, 关键字最小的记录到达最上端,接着,再在剩下的记录中找关 键字最小的记录,把它换到第二个位置上。依次类推,一直到 所有记录都有序为止。一般情况下,记录数为n,需要做n-1次 冒泡。 9.3.1 冒泡排序 冒泡排序是一种简单常用的排序方法。其排序思想是:通 过相邻记录关键字间的比较和交换,使关键字最小的记录像气 泡一样逐渐上浮。比较可以采用不同的方法,本算法是从最下 面的记录开始,对两个相邻的关键字进行比较并且使关键字较 小的记录换至关键字较大的记录之上,使得经过一次冒泡后, 关键字最小的记录到达最上端,接着,再在剩下的记录中找关 键字最小的记录,把它换到第二个位置上。依次类推,一直到 所有记录都有序为止。一般情况下,记录数为n,需要做n-1次 冒泡。 数据结构(C语言版) 例9.4 设待排记录的关键字分别为{37,19,90,64,13,49, 20,40},进行冒泡排序的具体过程如图9.4所示。 初始(n=8) i= 1 i= 2 i= 3 i= 4 i= 5 i= 6 i= 7 37r[1] 13 13 13 13 13 13 13 19r[2] 37 19 19 19 19 19 19 r[3] 90 19 39 20 20 20 20 20 r[4] r[5] r[6] r[7] r[8] 64 13 49 20 40 90 64 20 49 40 20 90 64 40 49 37 37 37 37 37 40 40 40 40 40 90 64 49 64 90 49 49 64 90 49 49 64 64 90 90 初始(n=8) i= 1 i= 2 i= 3 i= 4 i= 5 i= 6 i= 7 37r[1] 13 13 13 13 13 13 13 19r[2] 37 19 19 19 19 19 19 r[3] 90 19 39 20 20 20 20 20 r[4] r[5] r[6] r[7] r[8] 64 13 49 20 40 90 64 20 49 40 20 90 64 40 49 37 37 37 37 37 40 40 40 40 40 90 64 49 64 90 49 49 64 90 49 49 64 64 90 90 图9.4 冒泡排序示例 数据结构(C语言版) 从排序的过程看,记录数为8,需要做7次冒泡,但实际进 行到第5次冒泡时,整个记录已经有序了,因此,不需要再进行 6、7次冒泡了,也就是说,在某次比较过程中,如果设有交换 记录,则排序可提前结束。这点在算法中给予考虑,可节省排 序时间,为此在算法中设置一个变量F来监视排序情况,F=1时 表示有记录交换,F=0时无记录交换。 从排序的过程看,记录数为8,需要做7次冒泡,但实际进 行到第5次冒泡时,整个记录已经有序了,因此,不需要再进行 6、7次冒泡了,也就是说,在某次比较过程中,如果设有交换 记录,则排序可提前结束。这点在算法中给予考虑,可节省排 序时间,为此在算法中设置一个变量F来监视排序情况,F=1时 表示有记录交换,F=0时无记录交换。 数据结构(C语言版) 冒泡排序算法描述如下: /*算法描述9.4 冒泡排序*/ Bubblesort (Recordnode r[],int n) {int i,j,F; F=1; For(i=1;i<=n-1&&F==1;i++) {F=0; for(j=n;j>=i+1;j--) if(r[j].key=i+1;j--) if(r[j].keyi,则 将rj放到腾空的位置上,使rj腾空,同时使i+1。然后进行(2)。 下面具体介绍一次快速排序的过程:设置两个指示器i,j分别 表示当前待排记录序列中的第一个记录和最后一个记录位置; 将第一个记录作为基准关键字放到变量x中,使它所处的位置腾 空,然后从序列的两端开始逐步向中间扫描: (1) 在序列的右端扫描时,从序列的当前右端j处开始,把记 录ri.key与基准关键字x.key进行比较,若前者大于后者,令j=j- 1,继续进行比较,直到前者小于或等于后者,此时,若j>i,则 将rj放到腾空的位置上,使rj腾空,同时使i+1。然后进行(2)。 数据结构(C语言版) (2) 在序列的左端扫描时,从序列的当前左端i处开始将ri.key 与x.key进行比较,若前者小于后者,令i=i+1,继续向右扫描, 直到前者大于等于后者,此时,若i=x.key) 快速排序算法描述如下: /*算法描述9.5 快速排序*/ Quicskort(Recordnode r[],int low,int high) /*low和high为记录序列的下界和上界*/ {int i,j; struct Recordnode x; i=low; j=high; x=r[low]; while(i=x.key) 数据结构(C语言版) j--; if(i34故不必交换,则以65为根的子树即为堆,接着用相同的步 骤对第4个结点12进行调整,直至第1个结点36。如果在中间调 整过程中,由于交换破坏了以其孩子为根的堆,则要对破坏了 的堆进行调整,依次类推,直到父结点大于等于左、右孩子的 元素结点或者孩子结点为空的元素结点。当这一系列调整过程 完成时,图9.10所示的二叉树即成为一个堆树,这个调整过程也 叫做“筛选”。 由二叉树的性质得知,二叉树中序号最大的一个非终点是 [n/2],即图中的5号结点65,序号最小的列终结点是序号为1的 结点,即根结点36,对这些结点需一一进行调整,使其满足堆 的条件。调整过程为:首先把5号结点元素65与其两个孩子中值 大者进行比较,由于只有1个左孩子34,故只和34比较,因为 65>34故不必交换,则以65为根的子树即为堆,接着用相同的步 骤对第4个结点12进行调整,直至第1个结点36。如果在中间调 整过程中,由于交换破坏了以其孩子为根的堆,则要对破坏了 的堆进行调整,依次类推,直到父结点大于等于左、右孩子的 元素结点或者孩子结点为空的元素结点。当这一系列调整过程 完成时,图9.10所示的二叉树即成为一个堆树,这个调整过程也 叫做“筛选”。 数据结构(C语言版) 12 59 76 25 43 48 36 65 34 24 筛65 不动 12 59 76 25 43 48 36 65 34 24 ②① 12 59 76 25 43 48 36 65 34 24 筛48 不动 76 59 12 25 43 48 36 65 34 24 ④③ 筛12 下移一层 59 24 12 25 43 48 36 65 34 76 筛36 下移两层 59 24 12 25 43 48 76 36 34 65 ⑥⑤ 筛24 下移两层 图 9. 10 建立初始堆示例 12 59 76 25 43 48 36 65 34 24 筛65 不动 12 59 76 25 43 48 36 65 34 24 ②① 12 59 76 25 43 48 36 65 34 24 筛48 不动 76 59 12 25 43 48 36 65 34 24 ④③ 筛12 下移一层 59 24 12 25 43 48 36 65 34 76 筛36 下移两层 59 24 12 25 43 48 76 36 34 65 ⑥⑤ 筛24 下移两层 图 9. 10 建立初始堆示例 数据结构(C语言版) 下面给出建立初始堆的筛选过程及其算法。 /*算法描述9.7 建立初始堆*/ sift (Recordnode r[], int L, int m) /*对有n个元素的数组r中的第L个元素进行筛选*/ {int i, j ; struct Recordnode x; i=L;j=2*i; /*r[j]是r[i]的左孩子*/ x=r[i]; /*把筛选结点的值存入变量 x中*/ while (j<=m) { if (j0;i--)/*建立初始堆*/ sift(r,i,n); for(i=n;i>1;i--)/*进行n-1次循环,完成堆排序*/ 下面给出整个排序过程及算法描述。(说明:已经有序的子树 部分省去。) 堆排序算法描述如下: /*算法描述9.8 堆排序*/ heapsort (Recordnode r[],int n) {int i,k; struck Recordnode x; for(i=n/2;i>0;i--)/*建立初始堆*/ sift(r,i,n); for(i=n;i>1;i--)/*进行n-1次循环,完成堆排序*/ 数据结构(C语言版) { x=r[1]; /*将第一个元素与当前区间的最后一个元素对调*/ r[1]=r[i]; r[i]=x; sift(r,1,i-1); /*调用筛选子函数*/ } } { x=r[1]; /*将第一个元素与当前区间的最后一个元素对调*/ r[1]=r[i]; r[i]=x; sift(r,1,i-1); /*调用筛选子函数*/ } } 从堆排序的算法知道,堆排序所需的比较次数是建立初始 堆与重新建堆所需的比较次数之和,其平均时间复杂度和最坏 的时间复杂度均为O(n lbn)。它是一种不稳定的排序方法。 数据结构(C语言版) 9.5 内部排序方法的比较 一个好的排序方法所需要的比较次数和占用存储空间应该要 少。从表9.2可以看出,每种排序方法各有优缺点,不存在十全 十美的排序方法,因此,在不同的情况下可选择不同的方法, 选取排序方法时,一般需要考虑以下几点: (1) 算法的简单性。它分为简单算法和改进后的算法,简单算 法有直接插入、直接选择和冒泡法,这些方法都简单明了。改 进的算法有希尔排序、快速排序和堆排序等,这些算法比较复 杂。 一个好的排序方法所需要的比较次数和占用存储空间应该要 少。从表9.2可以看出,每种排序方法各有优缺点,不存在十全 十美的排序方法,因此,在不同的情况下可选择不同的方法, 选取排序方法时,一般需要考虑以下几点: (1) 算法的简单性。它分为简单算法和改进后的算法,简单算 法有直接插入、直接选择和冒泡法,这些方法都简单明了。改 进的算法有希尔排序、快速排序和堆排序等,这些算法比较复 杂。 数据结构(C语言版) (2) 待排序的记录多少。记录数越少越适合简单算法,记录 数越多越适合改进算法,这样,效率可以明显提高。 (3) 记录本身所带的信息量的大小。记录信息量越大,用 的字节越多,那么移动这些记录需花费的时间也越多,所以选 择算法应尽量避免选用移动次数较多的算法。 (2) 待排序的记录多少。记录数越少越适合简单算法,记录 数越多越适合改进算法,这样,效率可以明显提高。 (3) 记录本身所带的信息量的大小。记录信息量越大,用 的字节越多,那么移动这些记录需花费的时间也越多,所以选 择算法应尽量避免选用移动次数较多的算法。 数据结构(C语言版) 表9.2 7种排序方法的比较 排序方法 平均时间 最坏情况 辅助空间 稳定性 直接插入排序 O(n2)O(n2)O(1) √ 折半插入排序 O(n2)O(n2)O(1) √ 希尔排序 O(n1.3) O(n2)O(1) ×希尔排序 O(n1.3) O(n2)O(1) × 冒泡排序 O(n2)O(n2)O(1) √ 快速排序 O(n lb n) O(n2) O(lb n) × 直接选择排序 O(n2)O(n2)O(1) √ 堆排序 O(n lb n) O(n lb n) O(1) × 数据结构(C语言版) 9.6 实习:排序算法的实现——学生成绩管理 给出n个学生的考试成绩表,每条信息由姓名与分数组成, 试设计一个算法:① 按分数高低次序,求出每个学生在考试中 获得的名次,分数相同的为同一名次;② 按名次列出每个学生 的姓名与分数。 算法实现:下面给出的是用直接选择排序算法实现的C语 言程序,仅供参考。在此基础上,读者可以尝试用直接插入、 Shell排序、冒泡排序、快速排序等排序算法实现对该问题的求 解。 给出n个学生的考试成绩表,每条信息由姓名与分数组成, 试设计一个算法:① 按分数高低次序,求出每个学生在考试中 获得的名次,分数相同的为同一名次;② 按名次列出每个学生 的姓名与分数。 算法实现:下面给出的是用直接选择排序算法实现的C语 言程序,仅供参考。在此基础上,读者可以尝试用直接插入、 Shell排序、冒泡排序、快速排序等排序算法实现对该问题的求 解。 数据结构(C语言版) #include #define maxsize 100 typedef struct Recordnode {char name[8]; float score; }Recordnode; Recordnode r[maxsize]; void selectsort (Recordnode r[], int n) /*函数说明*/ main() {int num,i,j,n; printf("\n请输入学生成绩:\n"); #include #define maxsize 100 typedef struct Recordnode {char name[8]; float score; }Recordnode; Recordnode r[maxsize]; void selectsort (Recordnode r[], int n) /*函数说明*/ main() {int num,i,j,n; printf("\n请输入学生成绩:\n"); 数据结构(C语言版) for (i=1;i<=n;i++) {printf("姓名: "); scanf("%s",r[i].name); scanf("%4f",&r[i].score); } void selectsort(r , n); /*调用selectsort子函数*/ num=1; / *变量num表示名次*/ printf("%4d%s%4f\n",num,r[1].name,r[1].score); for (i=2;i<=n;i++) /*按名次打印成绩表*/ {if (r[i].score < r[i-1].score) num=num+1; for (i=1;i<=n;i++) {printf("姓名: "); scanf("%s",r[i].name); scanf("%4f",&r[i].score); } void selectsort(r , n); /*调用selectsort子函数*/ num=1; / *变量num表示名次*/ printf("%4d%s%4f\n",num,r[1].name,r[1].score); for (i=2;i<=n;i++) /*按名次打印成绩表*/ {if (r[i].score < r[i-1].score) num=num+1; 数据结构(C语言版) printf("%4d%s%4f\n",num,r[ i ].name,r[ i ].score); } } void selectsort (Recordnode r[] , int n) /*排序子函数*/ {int i , j , k ; struct Recordnode x; for(i=1;i<=n-1;i++) {k=i; printf("%4d%s%4f\n",num,r[ i ].name,r[ i ].score); } } void selectsort (Recordnode r[] , int n) /*排序子函数*/ {int i , j , k ; struct Recordnode x; for(i=1;i<=n-1;i++) {k=i; 数据结构(C语言版) for( j = i + 1 ; j < = n ; j + +) if (r [ j ].score>r[ k ].score) k=j; if (k != i ) { x = r [ i ]; r[ i ]=r[ k ]; r[ k ]=x; } } } for( j = i + 1 ; j < = n ; j + +) if (r [ j ].score>r[ k ].score) k=j; if (k != i ) { x = r [ i ]; r[ i ]=r[ k ]; r[ k ]=x; } } } 数据结构(C语言版) 习 题 9 1. 选择题 (1) 在所有排序方法中,关键字的比较次数与记录的初始排列 无关的是 。 A)  Shell排序 B) 冒泡排序 C) 插入排序 D) 选择排序 (2) 直接插入排序和冒泡排序的时间复杂度为 ,若初始数 据有序(即正序),则时间复杂度为 。 A)  O(n) B)  O(lb n) C)  O(n lb n) D)  O(n2) 1. 选择题 (1) 在所有排序方法中,关键字的比较次数与记录的初始排列 无关的是 。 A)  Shell排序 B) 冒泡排序 C) 插入排序 D) 选择排序 (2) 直接插入排序和冒泡排序的时间复杂度为 ,若初始数 据有序(即正序),则时间复杂度为 。 A)  O(n) B)  O(lb n) C)  O(n lb n) D)  O(n2) 数据结构(C语言版) (3) 排序的方法有多种, 法从未排序序列中依次取出元素 与已排序序列(初始时为空)中元素作比较,将其放入已排序序 列中的正确位置上; 法从未排序序列中挑选元素,并将其依 次放入已排序序列的一端; 法对序列中相邻元素进行一系列 比较,当被比较的两个元素逆序时就进行交换。 A) 冒泡排序 B) 插入排序 C) 快速排序 D) 选择排序 (4) 对n个元素的序列进行冒泡排序时, 最少的比较次数是 。 A)  n B)  n-1 C)  0 D)  1 (3) 排序的方法有多种, 法从未排序序列中依次取出元素 与已排序序列(初始时为空)中元素作比较,将其放入已排序序 列中的正确位置上; 法从未排序序列中挑选元素,并将其依 次放入已排序序列的一端; 法对序列中相邻元素进行一系列 比较,当被比较的两个元素逆序时就进行交换。 A) 冒泡排序 B) 插入排序 C) 快速排序 D) 选择排序 (4) 对n个元素的序列进行冒泡排序时, 最少的比较次数是 。 A)  n B)  n-1 C)  0 D)  1 数据结构(C语言版) (5) 一组记录的关键字为(45,80,55,40,42,85),则利用 堆排序的方法建立的初始堆为 。 A)(80,45,55,40,42,85) B)  (85,80,55,40,42,45) C)(85,80,55,45,42,40) D)  (85,55,80,42,45,40) (6) 用某种排序方法对线性表(25,84,21,47,15,27,68,35, 20)进行排序时,元素序列的变化情况如下: 1 (25,84,21,47,15,27,68,35,20) 2 (20,15,21,25,47,27,68,35,84) 3 (15,20,21,25,35,27,47,68,84) 4 (15,20,21,25,27,35,47,68,84) (5) 一组记录的关键字为(45,80,55,40,42,85),则利用 堆排序的方法建立的初始堆为 。 A)(80,45,55,40,42,85) B)  (85,80,55,40,42,45) C)(85,80,55,45,42,40) D)  (85,55,80,42,45,40) (6) 用某种排序方法对线性表(25,84,21,47,15,27,68,35, 20)进行排序时,元素序列的变化情况如下: 1 (25,84,21,47,15,27,68,35,20) 2 (20,15,21,25,47,27,68,35,84) 3 (15,20,21,25,35,27,47,68,84) 4 (15,20,21,25,27,35,47,68,84) 数据结构(C语言版) 则所采用的排序方法是 。 A) 插入排序法 B) 选择排序法 C) 快速排序法 D) 堆排序法 2. 已知序列(10,18,4,3,6,12,1,9,18,8), 采用希尔排 序法排序, 写出每一趟的结果。 3. 已知序列(45,87,12,56,67,6,90,39,83), 采用快速 排序法排序, 写出每一趟的结果。 4. 判别以下序列是否为堆,如果不是,则把它调整成堆。 (1)(150,86,48,73,35,39,42,57,66,22) (2) (120,70,33,65,24,56,48,92,86,30) 则所采用的排序方法是 。 A) 插入排序法 B) 选择排序法 C) 快速排序法 D) 堆排序法 2. 已知序列(10,18,4,3,6,12,1,9,18,8), 采用希尔排 序法排序, 写出每一趟的结果。 3. 已知序列(45,87,12,56,67,6,90,39,83), 采用快速 排序法排序, 写出每一趟的结果。 4. 判别以下序列是否为堆,如果不是,则把它调整成堆。 (1)(150,86,48,73,35,39,42,57,66,22) (2) (120,70,33,65,24,56,48,92,86,30) 数据结构(C语言版) 5.输入若干国家名称,请按字典顺序将这些国家名称排 序。(设所有的名称均用大写或小写表示。) 数据结构(C语言版) 第10章 文 件 10.1 文件的基本概念 10.2 文件的组织 习题10 10.1 文件的基本概念 10.2 文件的组织 习题10 数据结构(C语言版) 10.1 文件的基本概念 所谓文件,一般是指存储在外部存储介质上的数据的集合。这 里所讨论的文件主要是数据库意义上的文件。数据库所研究的文 件是带有结构的记录集合,每个记录可以由若干个数据项构成, 数据项有时也称为字段。可以说,文件就是性质相同的记录的集 合。文件所涉及数据量通常较大,因此被放置在外存中。 表10.1是一个简单的职工文件,,每个职工情况是一个记录,每 个记录由6个数据项组成。 所谓文件,一般是指存储在外部存储介质上的数据的集合。这 里所讨论的文件主要是数据库意义上的文件。数据库所研究的文 件是带有结构的记录集合,每个记录可以由若干个数据项构成, 数据项有时也称为字段。可以说,文件就是性质相同的记录的集 合。文件所涉及数据量通常较大,因此被放置在外存中。 表10.1是一个简单的职工文件,,每个职工情况是一个记录,每 个记录由6个数据项组成。 数据结构(C语言版) 表10.1 职 工 文 件 编号 姓名 性别 职称 婚姻状况 工资 1001 王 华 女 工程师 已婚 1500 1002 赵小方 女 经理 未婚 1200 1003 张 弓 男 高工 已婚 2000 1004 李 明 男 工程师 已婚 1800 数据结构(C语言版) 文件的操作有两类:检索和修改。 检索就是在文件中查找满足给定条件的记录,它可以根据记 录的序号或记录的相对位置进行查找,也可以按关键字进行查 找。 文件的修改包括记录的插入、删除和更新三种操作。 文件的操作有两类:检索和修改。 检索就是在文件中查找满足给定条件的记录,它可以根据记 录的序号或记录的相对位置进行查找,也可以按关键字进行查 找。 文件的修改包括记录的插入、删除和更新三种操作。 数据结构(C语言版) 10.2 文件的组织 10.2.1 顺序文件 顺序文件是指文件中的物理记录按其在文件中的逻辑顺序依 次存入存储介质而建立的文件。 顺序文件的存取是根据记录的序号或记录的相对位置进行 检索,如果要存取第i个记录,则必须先搜索它前面的i-1个记 录;如果要插入一个新记录,则只能添加在文件的末尾;如果 要更新文件中的记录,则对要修改的记录用新记录代替。 10.2.1 顺序文件 顺序文件是指文件中的物理记录按其在文件中的逻辑顺序依 次存入存储介质而建立的文件。 顺序文件的存取是根据记录的序号或记录的相对位置进行 检索,如果要存取第i个记录,则必须先搜索它前面的i-1个记 录;如果要插入一个新记录,则只能添加在文件的末尾;如果 要更新文件中的记录,则对要修改的记录用新记录代替。 数据结构(C语言版) 磁带是一种典型的顺序存取设备,存储在磁带上的文件只能 是顺序文件,其存储时间与数据在磁带上的位置及当前读写头 所在位置有关。如果文件中的第i个记录被存取过,而下一个要 存取的记录是第i+1个记录,则依次存取会很快完成;如果某个 文件在磁带的末尾,而磁头当前位置在磁带首部,则必须空转 磁带到尾部才能读取文件,此时,磁带的存取速度较慢。 磁盘是一种直接存储介质,存放于磁带上的文件可以是顺 序文件,也可以是其他结构类型的文件。磁盘上的文件可以用 顺序查找存取,也可以用分块查找或折半查找进行存取。 磁带是一种典型的顺序存取设备,存储在磁带上的文件只能 是顺序文件,其存储时间与数据在磁带上的位置及当前读写头 所在位置有关。如果文件中的第i个记录被存取过,而下一个要 存取的记录是第i+1个记录,则依次存取会很快完成;如果某个 文件在磁带的末尾,而磁头当前位置在磁带首部,则必须空转 磁带到尾部才能读取文件,此时,磁带的存取速度较慢。 磁盘是一种直接存储介质,存放于磁带上的文件可以是顺 序文件,也可以是其他结构类型的文件。磁盘上的文件可以用 顺序查找存取,也可以用分块查找或折半查找进行存取。 数据结构(C语言版) 10.2.2 索引文件 为了提高文件的检索效率,可以建立一个索引表,给出关 键字与相应记录地址之间的对应关系,这种包括文件记录数据 和索引表两大部分的文件称为索引文件。索引表中的每一项称 作索引项,不论主文件是否按关键字有序排列,索引项总是按 关键字有序。若主文件的记录也按关键字有序排列,则称为索 引顺序文件,反之,若主文件的记录不按关键字有序排列,则 称为索引无序文件。 索引文件只能存放在直接存储的外存储器(例如磁盘)中, 不能存放在只能顺序存取的磁带中。表10.2是一个学生档案数 据文件。 10.2.2 索引文件 为了提高文件的检索效率,可以建立一个索引表,给出关 键字与相应记录地址之间的对应关系,这种包括文件记录数据 和索引表两大部分的文件称为索引文件。索引表中的每一项称 作索引项,不论主文件是否按关键字有序排列,索引项总是按 关键字有序。若主文件的记录也按关键字有序排列,则称为索 引顺序文件,反之,若主文件的记录不按关键字有序排列,则 称为索引无序文件。 索引文件只能存放在直接存储的外存储器(例如磁盘)中, 不能存放在只能顺序存取的磁带中。表10.2是一个学生档案数 据文件。 数据结构(C语言版) 表10.2 学生档案数据文件 物理记录号 学号 姓名 性别 入学总分 1 200203 王芳 女 535.0 2 200218 刘志 男 550.02 200218 刘志 男 550.0 3 200227 李大成 男 530.0 4 200207 张民 男 540.0 5 200104 梁小平 女 520.0 数据结构(C语言版) 表10.3 以总分为关键字排序后的索引表 总分 物理记录号(指针) 520.0 5 530.0 3 535.0 1 540.0 4 550.0 2 数据结构(C语言版) 有了表10.3所示以总分为关键字排序后的索引表后,按总分 查找学生时可先在索引表中查找,得到物理记录号后再到数据 区取出对应的物理记录。 因此,索引文件的检索是分两步进行的,即先读索引表, 根据索引表得到所要检索记录的物理地址,然后根据该物理地 址读取记录。由于索引表已按关键字排序,因此对索引表的查 找可采用效率较高的折半查找方法。 有了表10.3所示以总分为关键字排序后的索引表后,按总分 查找学生时可先在索引表中查找,得到物理记录号后再到数据 区取出对应的物理记录。 因此,索引文件的检索是分两步进行的,即先读索引表, 根据索引表得到所要检索记录的物理地址,然后根据该物理地 址读取记录。由于索引表已按关键字排序,因此对索引表的查 找可采用效率较高的折半查找方法。 数据结构(C语言版) 索引文件的其他操作也很简单。插入记录时,先将记录 插入到主文件的尾部,然后在索引表中增加相应的索引项并 重新排序;删除记录时,仅需要删去相应的索引项;更新记 录时,若不更新记录的关键字,则不必修改索引表,只需检 索到对应的记录后修改要求的数据项内容,若需要更新记录 的关键字,则既要修改主文件记录,也要更新索引表。 索引文件的其他操作也很简单。插入记录时,先将记录 插入到主文件的尾部,然后在索引表中增加相应的索引项并 重新排序;删除记录时,仅需要删去相应的索引项;更新记 录时,若不更新记录的关键字,则不必修改索引表,只需检 索到对应的记录后修改要求的数据项内容,若需要更新记录 的关键字,则既要修改主文件记录,也要更新索引表。 数据结构(C语言版) 10.2.3 索引顺序文件ISAM ISAM是Index Sequential Access Method(索引顺序存取方法)的 缩写,是一种专为磁盘存取设计的文件组织方式,采用的是静 态索引结构。ISAM文件是采用索引顺序存取方法的文件。由于 磁盘是以盘组、柱面和磁道三级地址存取的设备,因此ISAM方 法对磁盘上的数据文件要建立主索引、柱面和磁道三级索引。 在ISAM文件中,存储的数据文件先按关键字排序。文件 记录在同一盘组上存放时,应先集中放在一个柱面上,然后再 顺序存放在相邻的柱面上,对于同一个柱面,则应按盘面的次 序顺序存放。 10.2.3 索引顺序文件ISAM ISAM是Index Sequential Access Method(索引顺序存取方法)的 缩写,是一种专为磁盘存取设计的文件组织方式,采用的是静 态索引结构。ISAM文件是采用索引顺序存取方法的文件。由于 磁盘是以盘组、柱面和磁道三级地址存取的设备,因此ISAM方 法对磁盘上的数据文件要建立主索引、柱面和磁道三级索引。 在ISAM文件中,存储的数据文件先按关键字排序。文件 记录在同一盘组上存放时,应先集中放在一个柱面上,然后再 顺序存放在相邻的柱面上,对于同一个柱面,则应按盘面的次 序顺序存放。 数据结构(C语言版) 图10.1为一个ISAM文件的结构示例。每个柱面建立一个磁 道索引,每个磁道索引由两个部分组成:基本索引项和溢出索 引项,每一部分都包括关键字和指针两项,关键字表示该磁道 最末一个记录的关键字(最大关键字),指针指示该磁道中第一 个记录的位置。柱面索引的每一个索引项也由关键字和指针两 部分组成,前者表示该柱面中最末一个记录的关键字,后者指 示该柱面上的磁道索引首地址。柱面索引放在某一个柱面上, 如果柱面索引较大,占用多个磁道时,则可以建立柱面索引的 索引,称为主索引。 图10.1为一个ISAM文件的结构示例。每个柱面建立一个磁 道索引,每个磁道索引由两个部分组成:基本索引项和溢出索 引项,每一部分都包括关键字和指针两项,关键字表示该磁道 最末一个记录的关键字(最大关键字),指针指示该磁道中第一 个记录的位置。柱面索引的每一个索引项也由关键字和指针两 部分组成,前者表示该柱面中最末一个记录的关键字,后者指 示该柱面上的磁道索引首地址。柱面索引放在某一个柱面上, 如果柱面索引较大,占用多个磁道时,则可以建立柱面索引的 索引,称为主索引。 数据结构(C语言版) 磁道索引 … R50 … … R164 溢出区 柱面C1 磁道索引 … R215 … … R330 溢出区 柱面C2 50 … 164 215 … 330 磁道索引 … R869 … … R1100 溢出区 柱面Cn 869 … 1100 磁道索引 164 330 … 720 … 1100 柱面索引 200 500 … 1100 主索引 图 10. 1 ISAM 文件的结构示例 磁道索引 … R50 … … R164 溢出区 柱面C1 磁道索引 … R215 … … R330 溢出区 柱面C2 50 … 164 215 … 330 磁道索引 … R869 … … R1100 溢出区 柱面Cn 869 … 1100 磁道索引 164 330 … 720 … 1100 柱面索引 200 500 … 1100 主索引 图 10. 1 ISAM 文件的结构示例 数据结构(C语言版) 在ISAM文件上查找一个记录时,先从主索引出发,再从 柱面索引找到记录所在柱面的磁道索引,最后从磁道索引的基 本项找到记录所在磁道第一个记录的位置,由此出发在该磁道 上进行顺序查找,进而根据溢出索引项在溢出区查找,查到找 到为止,反之,如果找遍该磁道及其溢出区不存在此记录,则 表明该文件中无此记录。 在ISAM文件上查找一个记录时,先从主索引出发,再从 柱面索引找到记录所在柱面的磁道索引,最后从磁道索引的基 本项找到记录所在磁道第一个记录的位置,由此出发在该磁道 上进行顺序查找,进而根据溢出索引项在溢出区查找,查到找 到为止,反之,如果找遍该磁道及其溢出区不存在此记录,则 表明该文件中无此记录。 数据结构(C语言版) 每个柱面上还开辟有一个溢出区,并且磁道索引项中设有溢 出索引项,这是为插入记录所设置的。由于ISAM文件中的记录是 按关键字大小顺序存放的,则在插入记录时需移动记录,并将同 一磁道上最后一个记录移至溢出区,同时修改磁道溢出项。每个 柱面的基本区是顺序存储结构,而溢出区是链表结构。同一磁道 溢出的记录由指针相链,该磁道索引的溢出索引项中的关键字指 示该磁道溢出的记录的最大关键字,指针则指示在溢出区中的第 一个记录。ISAM文件删除记录时,只需找到待删除的记录位置, 并在其存储位置上作删除标记即可,而不需移动记录或改变指针。 经过多次增删记录后,文件的结构有可能变得很不合理,此时, 大量的记录进入溢出区,而基本区中又因存在被删除的记录而浪 费很多存储空间。所以,有必要周期性地整理ISAM文件,将记录 读入内存,重新排序,复制成一个新的ISAM文件,填满基本区而 空出溢出区,以达到节约存储空间的目的。 每个柱面上还开辟有一个溢出区,并且磁道索引项中设有溢 出索引项,这是为插入记录所设置的。由于ISAM文件中的记录是 按关键字大小顺序存放的,则在插入记录时需移动记录,并将同 一磁道上最后一个记录移至溢出区,同时修改磁道溢出项。每个 柱面的基本区是顺序存储结构,而溢出区是链表结构。同一磁道 溢出的记录由指针相链,该磁道索引的溢出索引项中的关键字指 示该磁道溢出的记录的最大关键字,指针则指示在溢出区中的第 一个记录。ISAM文件删除记录时,只需找到待删除的记录位置, 并在其存储位置上作删除标记即可,而不需移动记录或改变指针。 经过多次增删记录后,文件的结构有可能变得很不合理,此时, 大量的记录进入溢出区,而基本区中又因存在被删除的记录而浪 费很多存储空间。所以,有必要周期性地整理ISAM文件,将记录 读入内存,重新排序,复制成一个新的ISAM文件,填满基本区而 空出溢出区,以达到节约存储空间的目的。 数据结构(C语言版) 习 题 10 1. 叙述顺序文件、索引文件的结构特点。 2. 设有一个职工文件如下表所示,由5个记录组织,其中 职工号为关键字。 (1) 若该文件为顺序文件,请写出文件的存储结构。 (2) 若文件为索引文件,请写出索引表。 1. 叙述顺序文件、索引文件的结构特点。 2. 设有一个职工文件如下表所示,由5个记录组织,其中 职工号为关键字。 (1) 若该文件为顺序文件,请写出文件的存储结构。 (2) 若文件为索引文件,请写出索引表。 数据结构(C语言版) 物理记录号 职工号 姓名 性别 工资 1 39 张珊 女 1000.00 2 50 王芳 女 1200.00 3 10 李斌 男 800.003 10 李斌 男 800.00 4 60 丁达 男 1500.00 5 25 赵刚 男 2000.00 数据结构(C语言版) 3. 图10.2给出了一个ISAM文件的局部表示,其中记录用 关键字代表。 柱面索引 磁道索引 图10.2 第3题图 数据结构(C语言版) 柱面C1 33 50 57 60 62 65 67 71 72 80 91 102 110 120 135 溢出区溢出区 图10.2 第3题图 (1) 画出相应柱面索引和磁道索引。 (2) 当插入75,40两个记录后,画出索引的变化情况和溢出区 的情况。
还剩656页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

1362168862

贡献于2016-08-21

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!