leyb g/qúrhn- Ã yÑ[fqúrhy>  · 1.1 复 习 提 要...

70
普通高等教育电气信息类应用型规划教材 数据结构 —— C++实现 (第二版) 习题解析与实验指导 缪淮扣 顾训穰 编著 科学出版社 职教技术出版中心 www.abook.cn

Upload: others

Post on 10-Oct-2020

0 views

Category:

Documents


0 download

TRANSCRIPT

普通高等教育电气信息类应用型规划教材

数据结构 —— C+ +实现(第二版)

习题解析与实验指导

缪淮扣 沈 俊 顾训穰 编著

北 京

学出版社

职教技术出版中心

www.abook.cn

内 容 简 介

本书是科学出版社出版的《数据结构—— C++实现(第二版)》一书的

配套教学参考书,旨在指导、启发和帮助学生学好数据结构这门课程。本书

对主教材的每一章均给出了复习提要,并给出了主教材中全部习题的参考答

案和分析。本书为每一章都设计了一套上机实验题,并提供了一个可在计算

机上运行的上机实验的实例。此外,还对数据结构考试的题型作了介绍,并

给出了 3 套模拟试卷。

本书可作为高等院校计算机专业数据结构课程的教学参考书,也可供参

加硕士研究生入学考试的考生以及从事计算机开发和应用的工程技术人员

阅读和参考。

图书在版编目(CIP)数据

数据结构:C++实现(第二版)习题解析与实验指导/缪淮扣,沈俊,顾

训穰编著. —北京:科学出版社,2016

(普通高等教育电气信息类应用型规划教材)

ISBN 978-7-03-047295-3

Ⅰ.①数… Ⅱ.①缪…②沈…③顾… Ⅲ.①C 语言-程序设计-高等

学校-教材 Ⅳ. ①TP312

中国版本图书馆 CIP 数据核字(2016)第 026564 号

责任编辑:孙露露 张瑞涛 / 责任校对:马英菊

责任印制:吕春珉 / 封面设计:耕者设计工作室

出版 北京东黄城根北街 16 号

邮政编码:100717

http://www.sciencep.com

双 青 印 刷 厂 印刷 科学出版社发行 各地新华书店经销

*

2016 年 1 月第 一 版 2016 年 1 月第一次印刷

开本:787×1092 1/16 印张:15 3/4

字数:360 000

定价:32.00 元

(如有印装质量问题,我社负责调换〈环伟〉) 销售部电话 010-62136230 编辑部电话 010-62135763-2010

版权所有,侵权必究

举报电话:010-64030229;010-64034315;13501151303

前 言

数据结构课程的教学目的是使学生学会分析研究计算机所要加工处理的数据的特

征,掌握组织数据、存储数据和处理数据的基本方法,并加强在实际应用中选择合适的

数据结构和相应算法的训练。

面向对象技术是软件工程领域中的重要技术,它不仅是一种程序设计方法,更是一

种对真实世界的抽象思维方式。目前,面向对象的软件分析和设计技术已发展成为软件开

发的主流方法,用面向对象的方式来描述数据结构及其算法已成为一种趋势。

数据结构是一门知识性和实践性很强的课程,它内容丰富,学习量大;隐藏在各部

分内容中的方法和技术很多,贯穿于全书的动态链表存储结构和递归技术令不少初学者

望而生畏。要学好这门课程必须花费极大的功夫。除了上课听讲、看书理解之外,还有

两个环节不可忽视:一是做书面练习;二是进行上机实验。只有做大量的习题和上机实

验,才能掌握数据结构的知识,提高算法设计的能力。

本书给出了主教材中的所有习题的参考答案和分析。对于学习数据结构这门课程的

学生来讲,这些解答只可作为参考,切不可完全依赖于它。如果在未做习题之前就先看答

案,那就与作者的初衷背道而驰了。有关数据结构程序的习题可以有多种解答,本书提供

的并不一定是唯一的,有的也不一定是最好的。学生可以设计多个程序,并加以比较。

为了加强实验环节,本书为每一章设计了不同类型的实验题,包括验证性实验、设

计性实验和综合性实验。本书还提供了一个实验报告实例。

此外,本书还对数据结构考试的题型作了介绍,并给出了 3 套模拟试卷。

在本书的写作过程中,上海大学教务处和计算机学院给予了很大支持,在此表示

感谢。

本书是在编者多年教学的基础上收集比较典型的习题编写的。由于作者水平有限,

本书难免存在着疏漏和不足,敬请广大读者批评指正。

缪淮扣

2015 年 10 月于上海

学出版社

职教技术出版中心

www.abook.cn

目 录

前言

第一部分 习题解析 ··································································································· 1

第一章 绪论 ······································································································ 3

第二章 C++程序设计语言简介 ········································································· 12

第三章 线性表 ································································································· 28

第四章 栈、队列和递归 ··················································································· 43

第五章 串、数组和广义表 ··············································································· 61

第六章 树和森林 ····························································································· 76

第七章 图 ········································································································ 91

第八章 查找 ··································································································· 107

第九章 排序 ··································································································· 125

第二部分 课程实验 ································································································ 141

实验一 C++类与对象 ····················································································· 143

实验二 线性表 ······························································································· 151

实验三 栈和队列 ··························································································· 154

实验四 串、数组和广义表 ············································································· 159

实验五 二叉树 ······························································································· 163

实验六 树和森林 ··························································································· 167

实验七 图 ······································································································ 176

实验八 查找 ·································································································· 181

实验九 排序 ·································································································· 185

实验十 综合设计 ··························································································· 191

第三部分 考试指导 ································································································ 201

第四部分 实验示例 ································································································ 235

参考文献 ················································································································· 245

学出版社

职教技术出版中心

www.abook.cn

第一部分

习 题 解 析

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

2

第一部分 习 题 解 析

3

第一章 绪 论

1.1 复 习 提 要

数据结构是计算机科学与技术专业本科生的一门专业基础课,根据教学大纲的要

求,主要讨论在软件开发中如何进行数据的组织、数据的表示和数据的处理,它不仅为

操作系统、编译原理、数据库系统、计算机网络等后续课程提供必要的知识基础,而且

也为读者提供必要的技能训练,所以它是一门知识性、实践性都很强的课程。不少单位

在招聘计算机人才、许多高校和科研机构在招收计算机研究生时,往往将“数据结构”

作为考核的课程之一,足见其重要性。

本章主要介绍贯穿和应用于整个数据结构课程始终的基本概念和算法性能分析

方法。

传统的“数据结构”概念从数据的逻辑结构(数据的组织)、物理结构——存储结

构(数据的表示)和相关操作(数据的处理)等三方面进行讨论,反映了数据结构设

计的不同层次。按照面向对象建模技术的要求,在软件开发中做数据结构设计时,不

但要设计对象——类、类的属性、类的操作,还要建立类的实例,即对象之间的关系。

所以,把数据结构定义为数据对象及对象中数据成员(元素)之间关系的集合是非常合

适的。

操作—算法的设计属于面向过程的开发模式,即传统的“输入—计算—输出”模式。

为比较解决同一问题的不同算法的优劣,进而设计出好的算法,对算法进行性能分析是

很有必要的。

本章的基本要求是:

1.需要理解数据、数据对象、数据元素(数据成员)、数据结构、数据的逻辑结构

与物理结构(存储结构)的概念。

2.需要理解算法的定义、算法的特性、算法的时间复杂度和空间复杂度。

进行数据结构设计及算法的编写与分析是不容易的,往往讲课时学生容易接受,但

课下自学感觉难度大,特别是做练习题时常常无从下手。这就需要付出极大的努力,勤

学多练,多总结、多思考、多做题、勤上机、勤交流,这是学好“数据结构”这门课程

的关键。

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

4

1.2 习 题 解 析

一、选择题

1.数据结构是指( )。

A.数据元素的组织形式 B.数据类型

C.数据存储结构 D.数据定义

答案:A

分析:数据结构是指相互之间存在一种或多种特定关系的数据元素的集合,是计算

机存储、组织数据的方式。数据类型是一个值的集合和定义在这个值集上的一组操作的

总称,数据结构不同于数据类型,它不仅要描述数据类型的数据对象,而且要描述数据

对象各元素之间的相互关系。数据结构包括数据的逻辑结构、数据的存储结构和数据的

运算 3 个部分,数据存储结构指数据的逻辑结构在计算机存储空间的存放形式。数据定

义为信息的载体,是可以被计算机识别、存储并加工处理的描述客观事物的信息符号的

总称,所以答案选 A。

2.数据在计算机存储器内表示时,物理地址与逻辑关系没有联系的称为( )。

A.存储结构 B.逻辑结构

C.链式存储结构 D.顺序存储结构

答案:C

分析:数据结构在计算机中的表示(映像)称为数据的物理(存储)结构。它包括

数据元素的表示和元素之间关系的表示。数据元素之间的关系有两种不同的表示方法:顺

序映像和非顺序映像,并由此得到两种不同的存储结构:顺序存储结构和链式存储结构。

顺序存储结构:它是把逻辑上相邻的元素存储在物理位置相邻的存储单元里,元素

之间的逻辑关系由存储单元的邻接关系来体现。顺序存储结构通常借助于程序设计语言

中的数组来实现。

链式存储结构:它不要求逻辑上相邻的元素在物理位置上亦相邻,元素之间的逻辑

关系是由附加的指针域来表示的。链式存储结构通常借助于程序设计语言中的指针类型

来实现。

3.树形结构是数据元素之间存在( )。

A.一对一关系 B.多对多关系

C.多对一关系 D.一对多关系

答案:D

分析:一个数据结构有 3 个要素:其一是数据元素的集合,其二是元素之间关系的

集合,其三就是数据的运算。根据数据元素间关系的不同特性,通常有下列四类基本的

结构:

第一部分 习 题 解 析

5

(1)集合结构:该结构中数据元素是离散的,数据元素间的关系仅是“属于同一个

集合”。

(2)线性结构:该结构中除第一个元素外,其他元素都有唯一的直接前驱元素;除

后一个元素外,其他元素都有唯一的直接后继元素。所以,数据元素之间存在着一对

一的关系。

(3)树形结构:也称层次结构。在该结构中除根结点外,其他元素都有唯一的直接

前驱;除叶子结点外,其他元素可以有多个直接后继。所以,数据元素之间存在着一对

多的关系。

(4)图形结构:也称网状结构。该结构每个元素可以有多个直接前驱和多个直接后

继,所以数据元素之间存在着多对多的关系。

4.设语句 x++的时间是单位时间,则以下语句的时间复杂度为( )。 for (i=1; i<=n; i++) for (j=i; j<=n; j++) x++;

A.O(n) B.O(n2) C.O(log2n) D.O(n3)

答案:B

分析:这段代码是嵌套的二重循环,外循环由循环变量 i 控制,其变化范围是从 1

到 n;内循环由循环变量 j 控制,其变化范围是从 i 到 n。所以,语句 x++的执行次数为n

i 1

i =n(n+1)/2,其时间复杂度为 O(n2)。

5.算法分析的目的是分析算法的效率以求改进,算法分析的两个主要方面是

( )。

A.空间复杂度和时间复杂度 B.正确性和简明性

C.可读性和文档性 D.数据复杂性和程序复杂性

答案:A

分析:算法分析是对一个算法需要多少计算时间和存储空间作定量的分析,所以它

包括空间复杂度和时间复杂度两个方面内容。

6.计算机算法指的是解决问题的有限运算序列,它具备输入、输出、( )等 5

个特性。

A.可行性、可移植性和可扩充性 B.可行性、确定性和有穷性

C.确定性、有穷性和稳定性 D.易读性、稳定性和安全性

答案:B

分析:一个算法应该具有以下 5 个重要特征。

(1)输入性:一个算法必须具有零个或多个输入量。

(2)输出性:一个算法应有一个或多个输出量,以反映对输入数据加工后的结果。

没有输出的算法是毫无意义的。

(3)确定性:算法中的每一条指令应含义明确,无歧义,即对每一种情况,需要执

行的动作都应严格、清晰地规定。

(4)有穷性:算法中的指令执行序列是有穷的,即算法无论在什么情况下都应在执

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

6

行有穷步后终止。

(5)可行性:也称有效性,算法中的每一条指令必须是切实可执行的,即原则上可

以通过已经实现的基本运算执行有限次来实现。对于每一步操作,仅有确定性是不够的,

它还必须是可行的。

7.数据在计算机内有链式和顺序两种存储方式,在存储空间使用的灵活性上,链

式存储比顺序存储要( )。

A.低 B.高 C.相同 D.不好说

答案:B

分析:顺序存储结构是把逻辑上相邻的数据元素存储在物理位置相邻的存储单元

里,元素之间的逻辑关系由存储单元的邻接关系来体现。顺序存储结构是一种 基本的

存储表示方法,它需要一组连续的存储空间存放数据元素,一般借助于程序设计语言中

的数组来实现。顺序存储结构的主要优点如下:其一是节省存储空间,因为分配的存储

单元全部用来存放数据元素,元素之间的逻辑关系表示没有占用额外的存储空间;其二

是采用这种方法时,可实现对元素的随机存取,即每一个元素对应一个序号,由该序号

可以直接计算出结点的存储地址。顺序存储方法的主要缺点有:其一是在插入、删除运

算时,需要移动数据元素;其二是需要分配连续的存储空间。

链式存储结构在计算机中用一组任意的存储单元存储数据元素(这组存储单元可

以是连续的,也可以是不连续的)。它不要求逻辑上相邻的元素在物理位置上亦相邻,

元素之间的逻辑关系是由附加的指针域表示的。其优点为:一是插入、删除灵活(不

必移动结点,只要改变结点中的指针);二是空间要求灵活,可以使用不连续的空间。

链式存储结构的缺点为:其一,它比顺序存储结构的存储密度小(每个元素的结点都

由数据域和指针域组成);其二,查找元素时,链式存储要比顺序存储慢,它只能进行

顺序查找。

8.从逻辑上可以把数据结构分为( )两大类。

A.动态结构、静态结构 B.顺序结构、链式结构

C.线性结构、非线性结构 D.初等结构、构造型结构

答案:C

分析:数据的逻辑结构是反映数据元素之间的逻辑关系的数据结构,其中的逻辑关

系是指数据元素之间的前后关系,一般分线性结构和非线性结构两大类,非线性结构包

括树和图。另外,集合结构由于数据元素之间没有关系,所以一般不讨论。

二、判断题

1.数据元素是数据的 小单位。( )

答案:错

分析:数据元素是数据的基本单位,在计算机程序中通常作为一个整体考虑。一个

数据元素由若干个数据项组成。数据项是数据的不可分割的 小单位。数据元素有两类:

一类是不可分割的原子型数据元素,如:整数"5",字符 "N" 等;另一类是由多个数据

项构成的数据元素,例如描述一个学生信息的数据元素可由学号、姓名、性别、身高、

第一部分 习 题 解 析

7

年龄和成绩等数据项组成。

2.记录是数据处理的 小单位。( )

答案:错

分析:数据处理是指对数据进行查找、插入、删除、合并、排序、统计以及简单计

算等的操作过程,它处理的对象是数据元素中的数据项,而记录对应的是数据元素。

3.数据的逻辑结构是指数据的各数据项之间的逻辑关系。( )

答案:错

分析:数据的逻辑结构是指数据的各数据元素之间的逻辑关系。

4.算法的优劣与算法描述语言无关,但与所用计算机有关。( )

答案:错

分析:算法优劣主要由算法自身的时间复杂度和空间复杂度决定,跟使用哪种程序

语言描述无关,也跟在何种性能计算机上执行无关。

5.健壮的算法不会因非法的输入数据而出现莫名其妙的状态。( )

答案:对

分析:算法的健壮性就是对不同的输入都要有相应的反应,比如对合法的输入就要

有正确的结果输出;对不合法的输入也要有相应的提示信息输出,提示此输入不合法,

不会因为输入不合法而发生死机等现象。

6.程序一定是算法。( )

答案:错

分析:算法的描述可以根据需要有一定的抽象程度。算法可以用某种程序设计语言

或某种表示法(例如:流程图)来描述,它并不要求一定要在计算机上执行。而程序必

须用某种程序设计语言描述,它要求在计算机上执行。

7.数据的物理结构是指数据在计算机内的实际存储形式。( )

答案:错

分析:数据的物理(存储)结构是指数据结构(而不是数据)在计算机中的表示(映

像)。它包括数据元素的表示和元素之间关系的表示。

8.在顺序存储结构中,有时也存储数据结构中元素之间的关系。( )

答案:错

分析:顺序存储结构是把逻辑上相邻的数据元素存储在物理位置相邻的存储单元

里,元素之间的逻辑关系由存储单元的邻接关系来体现。而不是“有时”也存储数据结

构中元素之间的关系。

9.顺序存储方式的优点是存储密度大,且插入、删除运算效率高。( )

答案:错

分析:顺序存储方式的存储密度大,但插入、删除运算效率低。

10.数据的逻辑结构说明数据元素之间的顺序关系,它依赖于计算机的存储结构。

( )

答案:错

分析:数据的逻辑结构描述的是数据元素之间的逻辑关系,数据的物理结构(存储

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

8

结构)是数据的逻辑结构在计算机中的物理存储方式。所以,不能说逻辑结构依赖于计

算机的存储结构。

三、填空题

1.数据的逻辑结构是指 。

答案:数据元素之间的逻辑关系

2.数据的逻辑结构有四种基本形态,分别是 、 、 和 。

答案:集合结构,线性结构,树(层次)结构,图(网状)结构

3.线性结构反映元素之间的逻辑关系是 的,非线性结构反映元素之间的逻辑

关系是 的。

答案:一对一,一对多或多对多

4.一个数据结构在计算机中的 称为存储结构。

答案:物理存储方式

5.一个算法的效率可分为 效率和 效率。

答案:时间,空间

6. 是描述客观事物的数、字符以及所有能输入到计算机且被计算机程序加

工处理的符号集合。

答案:数据

7.线性结构中元素之间存在 关系;树形结构中元素之间存在 关系;

图形结构中元素之间存在 关系。

答案:一对一,一对多,多对多

8.数据结构主要研讨数据的逻辑结构和物理结构,并对这种结构定义相应

的 。

答案:运算

四、应用题

1.什么是数据的逻辑结构?什么是数据的物理结构?

解答:数据的逻辑结构描述的是数据元素之间的逻辑关系,它属于用户视图,是用

户所看到的数据结构,它是面向问题的,它不考虑数据的存储。

数据的物理结构又称存储结构,是数据的逻辑结构在计算机中的物理存储方式,它

属于具体实现的视图,是面向计算机的。

数据的逻辑结构和物理结构是密切相关的两个方面。一般来说,算法设计是基于数

据的逻辑结构,而算法实现则基于数据的物理结构。

2.数据结构与数据类型有什么区别?

解答:数据结构是计算机存储、组织数据的方式,是指相互之间存在一种或多种特

定关系的数据元素的集合,它包括数据的逻辑结构、存储结构和相应的运算 3 个部分。

数据类型定义了数据的取值范围以及所允许的一组操作。数据类型根据所需内存大小的

不同分成不同类型数据,在编程的时候可以根据需要申请内存,从而充分利用内存,它

第一部分 习 题 解 析

9

不强调元素之间的关系。

3.回答问题

(1)在数据结构课程中,数据的逻辑结构、数据的存储结构及数据的运算之间存在

着怎样的关系?

解答:数据的逻辑结构反映数据元素之间的逻辑关系(即数据元素之间的关系),

数据的存储结构是数据的逻辑结构在计算机中的表示,包括数据元素的表示及其关系的

表示。数据的运算是对数据定义的一组操作,运算是定义在逻辑结构上的,和存储结构

无关,而运算的实现则是依赖于存储结构。

(2)若逻辑结构相同但存储结构不同,则为不同的数据结构。这样的说法对吗?举

例说明之。

解答:逻辑结构相同但存储结构不同,可以是不同的数据结构。例如,线性表的逻

辑结构属于线性结构,采用顺序存储结构为顺序表,而采用链式存储结构称为线性链表。

(3)在给定的逻辑结构及其存储表示上可以定义不同的运算集合,从而得到不同的

数据结构。这样的说法对吗?举例说明之。

解答:这样的说法是对的。例如,栈和队列的逻辑结构相同,其存储表示也可相同

(顺序存储和链式存储),但由于其运算集合不同而成为不同的数据结构。

(4)评价各种不同数据结构的标准是什么?

解答:数据结构的评价非常复杂,可以考虑两个方面:第一,所选数据结构是否准

确、完整地刻画了问题的基本特征;第二,是否容易实现,包括对数据分解是否恰当,

逻辑结构的选择是否适合于运算的功能,是否有利于运算的实现,基本运算的选择是否

恰当等。

4.若有 100 个学生,每个学生有学号、姓名、平均成绩,采用什么样的数据结构

方便?请写出这些结构。

解答:可以将一个学生信息看成一个数据元素,它包括学号、姓名和平均成绩 3 个

数据项,将 100 个学生信息看成一个数据。因为学生信息有 100 个,一般无增删操作,

相对固定,所以采用顺序存储比较适合。

其结构定义如下: typedef struct { int num; //学号 char name[8]; //姓名 float score; //平均成绩 } stu_node; stu_node student[100];

5.在编制管理通讯录的程序时,什么样的数据结构合适?为什么?

解答:逻辑上选用线性结构,将每个人的信息作为一个数据元素,它包括:姓名、

电话号码、邮编和地址等信息。存储结构分以下两种情况:如果通讯录的信息变动较少,

主要用于查询,则以顺序存储比较方便,它既能顺序查找,也能随机查找;如果通讯录

信息变化频繁,经常需要增删信息,则用链式存储结构较为合适,并以姓名作为关键字

把链表储存为有序链表,这样可以提高查询速度。

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

10

6.试举一例,说明对相同的逻辑结构,同一种运算在不同的存储方式下实现,其

运算效率不同。

解答:对于线性表中查找第 i 个元素的操作,在顺序存储方式下可以随机存取,其

时间复杂度为 O(1);而在链式存储方式下,只能从第一个元素开始顺序查找,在等概率

情况下其平均时间复杂度为 O(n)。

7.设计一个数据结构,用来表示某一银行储户的基本信息:账号、姓名、开户年

月日、储蓄类型、存入累加数、利息、账面总数。

解答: struct dnode { int year; int month; int day; }; typedef struct { int num; //账号 char name[8]; //姓名 struct dnode date; //开户年月日 int tag; //储蓄类型,如:0表示零存;1表示一年定期 float put; //存入累加数 float interest; //利息 float total; //账面总数 } count;

8.设 n 是偶数,试计算运行下列程序段后 m 的值,并给出该程序段的时间复杂度。 m:=0; for i:=1 to n do for j:=2*i to n do m:=m+1;

解答:设内层循环次数为 k,当 k=n 时内层循环终止,则内层循环的次数为 n-2i+1,

外层循环次数为 n/2 次,所以 m=n/2

2

i 1

(n 2i 1) n / 4

,时间复杂度为 O(n2)。

9.什么是算法?算法的五个特性是什么?请解释算法与程序的区别。

解答:算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算

法代表着用系统的方法描述解决问题的策略机制。

一个算法应该具有以下五个重要的特征:输入性、输出性、确定性、有穷性和可行性。

算法的描述可以根据需要有一定的抽象程度。算法可以用某种程序设计语言或某种

表示法(例如:流程图)来描述,它并不要求一定要在计算机上执行。而程序必须用某

种程序设计语言描述,它要求在计算机上执行。

10.设 n 为正整数,分析下列各程序段中加下划线的语句的执行次数。

(1) for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) { c[i][j]=0.0;

第一部分 习 题 解 析

11

for (int k=1; k<=n; k++) c[i][j]=c[i][j]+a[i][k]*b[k][j]; }

解答:加下划线的语句外面有三重嵌套 for 循环,3 个循环都执行了 n 次,所以此

语句的执行次数为 n3。

(2) x=0; y=1; for (int i=1; i<=n; i++) for (int j=1; j<=i; j++) for (int k=1; k<=j; k++) x=x+y;

解答:加下划线的语句外面有三重嵌套 for 循环,如果外层循环给定一个数值 i,

则内部两层循环的执行次数为 1+2+3+…+i=i(i+1)/2 次,所以总的循环次数为:n

i 1

i(i 1)/2 n(n 1)(n+2) / 6

(3) int i=1, j=1; while (i<=n && j<=n) { i=i+1; j=j+i; }

解答:此循环中有两个控制条件,第一个条件 i<=n,第二个条件 j<=n。当第 k 次

进入循环进行循环条件判断时,i 和 j 的值分别为 k 和 1+2+ … +k。所以,第一个条件其

实对循环控制不起作用。因此,循环次数 k 为满足 k(k+1)/2<=n 的 大整数,也就是 k

满足 k(k+1)/2 <= n < (k+1)(k+2)/2。

学出版社

职教技术出版中心

www.abook.cn

第二章 C++程序设计语言简介

2.1 复 习 提 要

算法的设计基于数据的逻辑结构,算法的实现基于数据的物理结构,而算法的描述

则可以有多种方式。为适应面向对象的软件开发模式,主教材采用 C++语言描述结构和

算法,这是一种兼有面向对象和面向过程双重特性的描述语言。

本章是 C++语言概要复习,旨在要求读者掌握 C++语言的基本概念和用 C++语言编

写应用程序的基本技术,并以此作为学习“数据结构”课程的基础。

本章的基本要求是:

1.需要理解什么是数据类型,抽象数据类型及封装性,数据抽象和信息隐藏原则,

面向对象=对象+类+继承+通信。

2.需要掌握指针和引用类型的使用。

3.需要理解和掌握函数的概念,包括函数类型,函数特征,函数名重载,函数参

数的传递,函数的返回值,函数中局部变量的作用域、它的创建和回收的有效范围;类

和对象的定义方式,构造函数和析构函数的使用,对象数据成员初始化方式;动态存储

分配和回收的方式;类的实例对象的建立和回收方式;输入输出流文件的定义和使用;

友元类和友元函数的定义和使用;模板类的定义和使用等。

C++对于面向对象程序设计的支持,核心就是类的定义,其优点是实现了软件复用。

为了熟练使用 C++,深刻了解数据结构的机制以及便于数据结构的复用,建议读者将主

教材中介绍的每一个结构都能给出“模板类”和“模板类的成员函数的实现”的完整描

述,并在后续的算法和类的实现中直接复用它们。

2.2 习 题 解 析

一、选择题

1.下面选项中不属于面向对象程序设计特征的是( )。

A.继承性 B.多态性 C.相似性 D.封装性

答案:C

第一部分 习 题 解 析

13

分析:面向对象程序设计主要有以下 3 个特征。

封装性:封装是一种信息隐蔽技术,它体现于类的声明,是对象的重要特性。封装

使数据和加工该数据的方法(函数)封装为一个整体,以实现独立性很强的模块,使得

用户只能见到对象的外部特性(对象能接收哪些消息,具有哪些处理能力),而对象的

内部特性(保存内部状态的私有数据和实现加工能力的算法)对使用者是隐蔽的。封装

的目的在于把对象的设计者和对象的使用者分开,使用者不必知晓行为实现的细节,只

需用设计者提供的消息来访问该对象。

继承性:继承性是子类自动共享父类数据和方法的机制,它由类的派生功能体现。

一个类直接继承其他类的全部描述,同时可以进行修改和扩充,继承具有传递性。继承

分为单继承(一个子类只有一个父类)和多重继承(一个类有多个父类)。类的对象是

各自封闭的,如果没继承性机制,则类对象中数据、方法就会出现大量重复。继承不仅

支持系统的可重用性,而且还促进系统的可扩充性。

多态性:对象根据所接收的消息而做出动作。同一消息为不同的对象接受时可产生

完全不同的行动,这种现象称为多态性。利用多态性,用户可发送一个通用的信息,而将

所有的实现细节都留给接受消息的对象自行决定。因此,同一消息即可调用不同的方法。

例如:Print 消息被发送给一个图对象时调用的打印方法与将同样的 Print 消息发送给一个

正文文件对象而调用的打印方法会完全不同。多态性的实现受到继承性的支持,利用类继

承的层次关系,把具有通用功能的协议存放在类层次中尽可能高的地方,而将实现这一功

能的不同方法置于较低层次,这样在这些低层次上生成的对象就能给通用消息以不同的响

应。在 OOPL(Object-Oriented Programming Language,面向对象程序设计语言)中,可

通过在派生类中重定义基类函数(定义为重载函数或虚函数)来实现多态性。

2.假定一个二维数组的定义语句为“int a[3][4]={{3, 4}, {2, 8, 6}};”,则元素 a[2][1]

的值为( )。

A.0 B.4 C.8 D.6

答案:A

分析:该语句定义了一个三行四列的二维数组,并初始化了第一行的前两列的值分

别为 3 和 4;第二行的前三列的值分别为 2、8 和 6。其他元素值按照 C++语言的规定默

认初始为 0。在 C++语言中下标是从 0 开始的,所以 a[2][1]是数组中的第三行第二列的

元素,其值为 0。

3.以下关键字中,可用于区分重载函数的是( )。

A.extern B.static C.const D.virtual

答案:C

分析:重载函数是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明

几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺

序)必须不同,也就是说用同一个运算符完成不同的运算功能。关键字 const 可以是函

数类型的一部分,它可以用于对重载函数的区分。

4.下列有关继承和派生的叙述中,正确的是( )。

A.派生类不能访问通过私有继承的基类的保护成员

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

14

B.多继承的虚基类不能够实例化

C.如果基类没有默认构造函数,派生类就应当声明带形参的构造函数

D.基类的析构函数和虚函数都不能被继承,需要在派生类中重新实现

答案:C

分析:保护成员在派生类中会变成私有成员,派生类的成员函数是可以访问的,所

以 A 不对。

所谓多继承,是指派生类从多个基类派生,而这些基类又从同一个基类派生,则在

访问此共同基类中的成员时,将产生二义性,即:派生类从同一个共同基类沿不同继承

方向得到多个相同的拷贝,从而不知道要访问哪一个拷贝,这就产生了二义性。对于访

问二义性,通常使用作用域运算符(类名::)来解决,但是这里的成员都是来源于同一

个基类,这时就不能用此方法解决该问题,所以就引入虚基类。虚基类的作用是使公共

基类只产生一个拷贝,即只对第一个调用的有效,对其他的派生类都是虚假的,没有调

用构造函数。虚基类的初始化与一般多继承的初始化在语法上是一样的,但构造函数的

调用次序不同。派生类构造函数的调用次序:先虚基类,后基类,再成员对象, 后自

身。所以 B 不对。

当基类没有提供一个默认构造函数时,派生类可以通过带参数的构造函数,把参数

传给基类中含参数的构造函数,或者由基类提供一个拷贝构造函数进行显式调用。所以

C 正确。

基类的析构函数不能被继承,虚函数可以被继承。所以 D 不对。

5.实现运行时多态的机制是( )。

A.虚函数 B.重载函数 C.静态函数 D.模板函数

答案:A

分析:简单地说,那些被 virtual 关键字修饰的成员函数就是虚函数。虚函数的作用

就是实现多态性(Polymorphism),多态性是将接口与实现进行分离,根据个体差异采

用不同的策略实现共同的方法。

6.在下列关于 C++函数的叙述中,正确的是( )。

A.每个函数至少要有一个参数

B.每个函数都必须返回一个值

C.函数在被调用之前必须先声明

D.函数不能自己调用自己

答案:C

分析:在 C++中声明的函数可以没有参数,例如 bool hanshu(){return true;}。当函数

的返回类型为 void 时,函数无返回值。函数可以嵌套调用,包括自己调用自己(如递

归)。但函数必须先声明后使用。

7.下列运算符中,不能重载的是( )。

A.&& B.!= C. . D.->

答案:C

分析:根据 C++的语法规定,除了类属关系运算符“.”、成员指针运算符“.*”、作

第一部分 习 题 解 析

15

用域运算符“::”、sizeof 运算符和三目运算符“?:”以外,C++中其他运算符都可以重载。

8.类模板的模板参数( )。

A.可以作为数据成员的类型 B.可以作为成员函数的返回类型

C.可以作为成员函数的参数类型 D.以上三者皆可

答案:D

二、判断题

1.一个类定义中,只要有一个函数模板,则这个类是类模板。( )

答案:对

2.类模板的成员函数都是函数模板,类模板实例化后,成员函数也随之实例化。

( )

答案:对

3.如果要把返回值为 void 的函数 A 声明为类 B 的友元函数,则应在类 B 的定义

中加入的语句是 friend void A():。( )

答案:错

分析: 后不能用“:”,而应该用“;”。

4.如果类 B 继承了类 A,则称类 A 为类 B 的基类,类 B 称为类 A 的派生类。( )

答案:对

5.将 x+y 中的+运算符用友元函数重载应写为 operator+(x, y)。( )

答案:错

分析:应该写成 object operator +(object x,object y),因为是二元函数的重载,x、

y 类型不明确。

6.非成员函数应声明为类的友元函数才能访问这个类的 private 成员。( )

答案:对

分析:非成员函数不能直接访问类的私有数据,如果非成员函数要访问私有成员,

它必须是类的友元。但要访问公有成员,可以不是类的友元。

7.派生类中的成员不能直接访问基类中的私有成员。( )

答案:对

分析:派生类可以访问 protected 和 public 成员,private 成员只有该类自身及友元类

可以访问。

8.派生类的成员一般分为两部分,一部分是从基类继承的成员,另一部分是自己

定义的新成员。( )

答案:对

三、填空题

1.C++语句 const char * const p="hello",所定义的指针 p 和它所指的内容都不能被

______。

答案:修改

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

16

2.C++语言的参数传递机制包括传值和传地址两种,如果调用函数时,需要改变实

参或者返回多个值,应该采取 方式。

答案:传地址

3.继承的方式有公有继承、私有继承和 三种。

答案:保护继承

4.表达式 operator+(x, y)还可以表示为 。

答案:x + y

5.使用 new 和 delete 两个运算符进行的分配为 存储分配。

答案:动态

6.C++语言中,访问一个对象的成员所用的运算符是 ,访问一个指针所指向

的对象的成员所用的运算符是 。

答案:. ,->

7.利用继承能够实现 。这种实现缩短了程序的开发时间,促使开发人员重

用已经测试和调试好了的高质量的软件。

答案:软件复用

8.三种成员访问说明符分别是 、 和 。

答案:public,protected,private

9.当公有继承从基类派生出一个类时,基类的公有成员称为派生类的 成员。

答案:公有

10.当用受保护的继承从基类派生一个类时,基类的公有成员称为派生类的

成员。

答案:受保护

四、应用题

1.编写一个完整的 C++函数,用输入/输出流和内联函数 CircleArea、提示用户输

入圆的半径,然后计算并打印出圆的周长和面积。

解答: #include <iostream> using namespace std; #define PI 3.14 inline double CircleArea(double r) { return PI*r*r; } inline double CirclePerimeter (double r) { return PI*2*r; } int main() {

第一部分 习 题 解 析

17

double radius = 0.0; double area = 0.0; double perimeter = 0.0; cout << "请输入半径:"; cin >> radius; area = CircleArea(radius); perimeter = CirclePerimeter (radius); cout << "Area is:" << area << endl; cout << "Perimeter is:"<< perimeter <<endl; return 0; }

2.编写一个完整的 C++程序,分别用下面的三个函数,对在 main 中定义的变量 count

乘以 2,然后比较以下三种方法的优劣。这三个函数分别如下。

(1)doubleCallByValue 通过传值调用传递 count 的拷贝,把 count 值的拷贝乘以 2

后返回新值。

(2)函数 doubleCallByPointer 用指针模拟传引用调用,用复引用运算符*对 main

中 count 的原始拷贝乘以 2。

(3)函数 doubleCallByReference 利用引用参数实现真正的引用调用,用 count 的别

名(即引用参数)把其原始值的拷贝乘以 2。

解答: #include <iostream> using namespace std; double doubleCallByValue(double e) // 传值 { e = e * 2; return e; } double doubleCallByPointer(double *e) // 传指针 { *e = *e * 2; return *e; } double doubleCallByReference(double &e) // 传引用 { e = e * 2; return e; } int main ( ) { double count = 3.0;

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

18

cout << "count = " << count << " before doubleCallByValue" << endl; cout << "Value returned by doubleCallByValue:" << doubleCallByValue(count) << endl; //传值调用 cout << "count = "<< count << " after doubleCallByValue" << endl << endl; count = 3.0; cout << "count = " << count << " before doubleCallByPointer" << endl; cout << "Value returned by doubleCallByPointer:" << doubleCallByPointer(&count) << endl;//传指针调用 cout << "count = "<< count << " after doubleCallByValue" << endl << endl; count = 3.0; cout << "count = " << count << " before doubleCallByReference" << endl; cout << "Value returned by doubleCallByReference:" << doubleCallByReference(count) << endl;//传引用调用 cout << "count = "<< count << " after doubleCallByReference" << endl; return 0; }

运行结果: count = 3 before doubleCallByValue Value returned by doubleCallByValue: 6 count = 3 after doubleCallByValue count = 3 before doubleCallByPointer Value returned by doubleCallByPointer: 6 count = 6 after doubleCallByPointer count = 3 before doubleCallByReference Value returned by doubleCallByReference: 6 count = 6 after doubleCallByReference

从上面的运行结果可以看出,传值调用 doubleCallByValue 不修改实在参数 count,

而传指针调用 doubleCallByPointer和传引用调用 doubleCallByReference 均修改实在参数

count 的值。传值调用的运行结果需要由函数返回,当函数需要返回多个值时就很难实

现。传引用调用使用比较方便,直接通过参数就可以在函数之间传递信息,所以程序员

在使用时要小心,如果把引用参数当作传值调用参数随意修改其值,会通过该参数修改

原变量的值,导致意想不到的副作用。指针传递在使用时需要用指针形式,虽然没有传

引用调用方便,但会引起程序员的注意。

第一部分 习 题 解 析

19

3.定义和实现复数的 C++类,要求:

(1)复数的数据域包括实部和虚部,复数的实部和虚部定义为浮点数。

(2)有三个构造函数:第一个构造函数没有参数;第二个构造函数有一个参数,即

把一个浮点数赋给复数的实部,复数的虚部为 0;第三个构造函数有两个参数,即把两

个浮点数分别赋给复数的实部和虚部。

(3)复数的成员函数包括获取和修改复数的实部和虚部。

(4)复数的成员函数还包括运算符重载形式的复数的+、-、*、/ 运算。

(5)定义友元形式的重载的流函数来输入一个复数和输出一个复数。

解答: #include <iostream> #include <cstdlib> using namespace std; class Complex { // 声明一个复数类 public: // 公共函数成员 Complex(double real=0, double imag=0); // 构造函数 double Real() const; // 取复数的实部的值 double Imag() const; // 取复数的虚部的值 void Set(double real, double imag); // 设置复数的实部及虚部 void SetReal(double real); // 设置复数的实部 void SetImag(double imag); // 设置复数的虚部 friend Complex operator+(const Complex &m,const Complex &n);

//重载加法运算符 friend Complex operator-(const Complex &m,const Complex &n);

//重载减法运算符 friend Complex operator*(const Complex &m,const Complex &n);

//重载乘法运算符 friend Complex operator/(const Complex &m,const Complex &n);

//重载除法运算符 Complex & operator=(const Complex &e);

//重载赋值运算符 friend istream & operator>>(istream &in,Complex &e);

//重载输入运算符 friend ostream & operator<<(ostream &out,const Complex &e);

//重载输出运算符 private: // 私有数据成员 double re, im; }; Complex::Complex(double real, double imag) { // 构造函数 re = real; im = imag; }

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

20

double Complex::Real() const { return re; } // 返回复数的实部的值 double Complex::Imag() const { return im; } // 返回复数的虚部的值 void Complex::Set(double real, double imag) { // 设置复数的实部及虚部 re = real; im = imag; } void Complex::SetReal(double real) { //设置复数的实部 re = real; } void Complex::SetImag(double imag) { //设置复数的虚部 im = imag; } Complex operator+(const Complex &m,const Complex &n){//重载加法运算符 Complex temp; temp.re = m.re + n.re; temp.im = m.im + n.im; return temp; } Complex operator-(const Complex &m,const Complex &n) {//重载减法运算符 Complex temp; temp.re = m.re - n.re; temp.im = m.im - n.im; return temp; } Complex operator*(const Complex &m,const Complex &n) {//重载乘法运算符 Complex temp; temp.re = m.re*n.re - m.im*n.im; temp.im = m.re*n.im + m.im*n.re; return temp; } Complex operator/(const Complex &m,const Complex &n) {//重载除法运算符 Complex temp; temp.re = (m.re*n.re + m.im*n.im)/(n.re*n.re + n.im*n.im); temp.im = (m.im*n.re - m.re*n.im)/(n.re*n.re + n.im*n.im); return temp; } Complex & Complex::operator=(const Complex &e) { //重载赋值运算符 re = e.re;

第一部分 习 题 解 析

21

im = e.im; return *this; } istream & operator>>(istream &in,Complex &e) { //重载输入函数,输入格

式举例:2+3i double real,imag; char temp[100]; in.getline(temp,100,'+'); real = atof(temp); in.getline(temp,100,'i'); imag = atof(temp); e.Set(real,imag); return in; } ostream & operator<<(ostream &out,const Complex &e) {//重载输出运算符 if (e.re) { out<<e.re; if (e.im>0) out<<" + "<<e.im<<"i"; else if(e.im<0) out<<" - "<<-(e.im)<<"i"; } else if(e.im) out<<e.im<<"i"; else out<<"0"; return out; }

4.为例 2-11 中的类 Time 增加成员函数 tick,该函数把存储在类 Time 的对象中的

时间增加 1 秒。以标准格式输出时间。要保证能够测试出如下的结果:

(1)能够进入下一分钟。

(2)能够进入下一小时。

(3)能够进入新的一天,即从 11:59:59PM 加一秒后变成 00:00:00AM。

解答: void Time::tick() { if(second < 59) //判断增加1秒后是否需要进位到“分” second++; else{ //检测到59秒,需进位到“分” second = 0; if(minute < 59) //继续判断增加1分后是否需要进位到“时” minute++; else{ //检测到59分,需进位到“时”

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

22

minute = 0; if(hour < 23) //继续判断增加1小时后是否需要进入新的一天 hour++; else //时间为 “11:59:59 PM”,需进入新的一天 hour=0; } } }

5.试写出所能想到的所有形状(包括二维的和三维的),并生成一个形状层次结构。

生成的层次结构要以 Shape 作为基类,并由此派生出类 TwoDimensionalShape 和

ThreeDimensionalShape。开发出层次结构后,定义其中的每一个类。

解答: #ifndef SHAPES_H #define SHAPES_H #include <iostream> #include <cstdlib> #include <string> using namespace std; const double P=3.1415926; class Shape //Shape基类 { protected: double area; double bulk; void shape(){} }; class TwoDimensionalShape:public Shape //二维图形派生类 { public: double Area () const{return area;} }; class ThreeDimensionalShape:public Shape //三维立体派生类 { public: double Bulk () const {return bulk;} }; class Circle:public TwoDimensionalShape //圆派生类 { private: double R; public: Circle(const double r){R=r;area=P*R*R;}

第一部分 习 题 解 析

23

}; class Rectangle:public TwoDimensionalShape //矩形派生类 { private: double l,w; public: Rectangle(const double L,const double W) { l=L; w=W; area=l*w; } }; class Oval:public TwoDimensionalShape //椭圆派生类 { private: double a,b; public: Oval(const double A,const double B) { a=A; b=B; area=P*a*b; } }; class RegularHexagon:public TwoDimensionalShape //正六边形派生类 { private: double l; public: RegularHexagon(const double L) { l=L; area=2.598*l*l; } }; class Spherome:public ThreeDimensionalShape //球体派生类 { private: double r; public:

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

24

Spherome(const double R) { r=R; bulk=4*P*r*r*r/3; } }; class Cylinder:public ThreeDimensionalShape //圆柱体派生类 { private: double r,h; public: Cylinder(const double R,const double H) { r=R; h=H; bulk=P*R*R*H; } }; class Cube:public ThreeDimensionalShape //立方体派生类 { private: double l; public: Cube(const double L) { l=L; bulk=l*l*l; } }; class Cone:public ThreeDimensionalShape //圆锥派生类 { private: double r, h; public: Cone(const double R,const double H) { h=H; r=R; bulk=P*r*r*h/3; } };

第一部分 习 题 解 析

25

class Pyramid:public ThreeDimensionalShape //正四棱锥派生类 { private: double l,h; public: Pyramid(const double L,const double H) { h=H; l=L; bulk=l*l*h/3; } }; class CircularTruncatedCone:public ThreeDimensionalShape //圆台派生类 { private: double r1,r2,h; public: CircularTruncatedCone(const double R1,const double R2,const double H) { r1=R1; r2=R2; h=H; bulk=P*h*(r1*r1+r1*r2+r2*r2)/3; } }; int main() { Circle c(2.0); Rectangle c1(3,4); Oval c2(3,2); RegularHexagon c3(2); Spherome c4(4); Cylinder c5(2,4); Cube c6(5); Cone c7(3,2); Pyramid c8(3,3); CircularTruncatedCone c9(2,3,4); cout<<C.Area()<<endl <<c1.Area()<<endl <<c2.Area()<<endl <<c3.Area()<<endl

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

26

<<c4.Bulk()<<endl <<c5.Bulk()<<endl <<c6.Bulk()<<endl <<c7.Bulk()<<endl <<c8.Bulk()<<endl; system("pause"); return 0; }

6.修改上一习题所定义的层次结构,使 Shape 成为一个包含接口(供层次结构中

的类使用)的抽象基类。从类 Shape 派生出二维形状类 TwoDimensionalShape 和三维

形状类 ThreeDimensionalShape,它们也都是抽象类,然后用虚函数 print 输出每个类的

类型和维数。为了计算类层次结构中每个具体类的对象,这两个类中还要包括虚函数 area

和 volume。 后再编写一个程序测试类 Shape 的层次结构。

解答: #include <iostream> #include <cstdlib> using namespace std; #define PI 3.1415926 class Shape { public: Shape(){} virtual ~Shape(){} public: virtual double get() = 0; //virtual double get(); }; class ThreeDimensionalShape:public Shape { public: virtual double get()=0; }; class TwoDimensionalShape:public Shape { public: virtual double get()=0; //TwoDimensionalShape(){} //~TwoDimensionalShape(){} };

第一部分 习 题 解 析

27

class Circle : public TwoDimensionalShape { public: Circle(int rad=0):r(rad){} ~Circle(){} public: double get() { return PI*r*r; } private: double r; }; class Ball : public ThreeDimensionalShape { public: Ball(int rad=0):r(rad){} ~Ball(){} public: double get() { return 3/4*PI*r*r*r; } private: double r; }; int main() { Circle shape(4); Ball shape1(3); cout << "Circle's Area=" << shape.get() << endl; cout << "Ball's volume=" << shape1.get() << endl; system("pause"); return 0; }

学出版社

职教技术出版中心

www.abook.cn

第三章 线 性 表

3.1 复 习 提 要

数据结构课程中的知识本身具有良好的结构性。从总体上来说,课程主要内容围绕

线性数据结构(线性表、栈、队列、串、数组、广义表),非线性数据结构(树和图)

及应用(查找和排序算法)来组织。这些数据结构都是基本的数据结构,这些算法都是

常用的算法。在具体介绍一种数据结构时,总是从逻辑结构、操作和存储结构三方面来

展开。数据的逻辑结构是从逻辑关系上描述数据,可以看作是从具体问题中抽象出来的

数据模型,是数据的应用视图,与计算机存储无关。数据的操作是定义于数据逻辑结构

上的一组运算,每种数据结构都有一个运算的集合。数据的存储结构是逻辑结构在计算

机存储器中的实现(数据及其关系的映像),它依赖于计算机,是数据的物理视图;在

确定的存储结构下,本书给出相应的抽象数据类型描述和类的定义及成员函数的实现。

所以,一个“类”实际上是一种数据结构在某一存储结构下的实现。希望读者在学习后

面的章节时始终把握住上述脉络。

本章介绍了一种 常用的线性数据结构—— 线性表的逻辑结构,线性表的操作,线

性表的存储结构和线性表的应用—— 多项式的表示。

本章的基本要求是:

1.需要理解线性表数据结构的特点。

2.需要理解和掌握线性表的顺序存储结构—— 顺序表的特点,顺序表类 SeqList

的定义及其主要操作如搜索、插入和删除算法的实现及复杂度分析。

3.需要理解和掌握线性表的非顺序存储结构—— 链表的特点,单链表类 LinkList

的定义及其成员函数如构造函数、搜索、插入、删除等操作的实现;双向链表类 DoubList

的定义及其成员函数如构造函数、搜索、插入、删除等操作的实现;静态链表的定

义、特点及操作;比较链表带表头结点和不带表头结点的优缺点;比较链表和循环

链表的异同。

4.需要了解多项式的不同表示,多项式链表表示的优点,多项式加、减运算的算

法实现。

一般说来,任何一种数据结构都可以采用顺序或非顺序两种不同的存储结构。然而,

针对具体数据结构的不同特点,可有一些不同的变种,这在后面的学习中会遇到,请读

者务必留意比照。

第一部分 习 题 解 析

29

3.2 习 题 解 析

一、选择题

1.下述( )是顺序存储结构的优点。

A.存储密度大 B.插入运算方便

C.删除运算方便 D.可方便地用于各种逻辑结构的存储表示

答案:A

分析:顺序存储结构的主要优点:其一是存储密度大,节省存储空间;其二是可以

对元素进行随机存取。

2.下面关于线性表的叙述中,错误的是( )。

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

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

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

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

答案:B

分析:线性表采用顺序存储时,插入、删除运算都需要移动数据元素,所以不便于

进行插入和删除操作。

3.若某线性表 常用的操作是存取任一指定序号的元素和在 后进行插入和删除

运算,则利用( )存储方式 节省时间。

A.顺序表 B.双链表

C.带头结点的双循环链表 D.单循环链表

答案:A

分析:由于 常用的操作是存取任一指定序号的元素和在 后进行插入、删除元素,

所以选择顺序表要比链表节省时间。在顺序表中取元素时可以根据元素序号随机取出;

插入元素时由于在表尾进行,所以也不需要移动元素。如果用链表,则不能随机存取元

素,所以存取元素时比较耗时。

4.某线性表中 常用的操作是在 后一个元素之后插入一个元素和删除第一个元

素,则采用( )存储方式 节省运算时间。

A.单链表 B.仅有头指针的单循环链表

C.双向链表 D.仅有尾指针的单循环链表

答案:D

分析:在单链表和双向链表的表尾插入元素时,需要搜索到表尾结点,比较耗时。

在仅有头指针的单循环链表中,不管是插入还是删除元素都要找到表尾结点,所以也比

较耗时。而用仅有尾指针的单循环链表时,在表头删除和在表尾插入元素都只要修改尾

结点的指针,所以 节省时间。

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

30

5.静态链表中指针表示的是( )。

A.内存地址 B.数组下标

C.下一元素地址 D.左、右孩子地址

答案:C

分析:题目要说明的是指针表现的内容,在链表(不管是动态链表还是静态链表)

中,一般指针总是表示下一个元素的地址。当然,在静态链表中指针是用数组下标表示

的(数组下标也是一个地址)。

6.链表不具有的特点是( )。

A.插入、删除不需要移动元素 B.可随机访问任一元素

C.不必事先估计存储空间 D.所需空间与线性表长度成正比

答案:B

分析:在链表中访问一个元素,一般只能从第一个元素结点开始顺序搜索,不能根

据元素序号随机访问。

7.对于一个头指针为 head 的循环链表,其尾结点 p 的特点是( )。

A.p->next == head B.p->next == head->next

C.p == head D.p == head->next

答案:A

分析:在循环链表中,尾结点的指针指向链表的头结点(或首元素结点)。

8.双向链表中,在 p 结点之前插入一个指针为 q 的结点的操作是( )。

A.p->next=q; q->prior=p; p->next->prior=q; q->next=q;

B.p->next=q; p->next->prior=q; q->prior=p; q->next=p->next;

C.q->prior=p; q->next=p->next; p->next->prior=q; p->next=q;

D.q->prior=p->prior; q->next=p; p->prior=q; q->prior->next=q;

答案:D

分析:在双向链表中 p 结点之前插入一个指针为 q 的结点时,需要修改 4 个指针,

把 q 的前驱指针指向 p 原先的前驱结点,把 q 的后继指针指向 p 结点,把 p 的前驱指针

指向 q 结点,把 p 原先的前驱结点的后继指针指向 q 结点。

9.单链表中,在指针为 p 的结点之后插入指针为 s 的结点,正确的操作是( )。

A.p->next=s; s->next=p->next; B.s->next=p->next; p->next=s;

C.p->next=s; p->next=s->next; D.p->next=s->next; p->next=s;

答案:B

分析:在单链表中 p 结点之后插入一个指针为 s 的结点时,需要修改两个指针,把

s 的后继指针指向 p 原先的后继结点,再把 p 的后继指针指向 s 结点。

10.在双向链表存储结构中,删除 p 所指的结点时须修改指针,正确的操作是( )。

A.p->prior->next=p->next; p->next->prior=p->prior;

B.p->prior=p->prior->prior; p->next=p->prior->next;

C.p->next->prior=p->prior; p->next=p->next->next;

D.p->next=p->prior->prior; p->prior=p->next->next;

第一部分 习 题 解 析

31

答案:A

分析:在双向链表中删除 p 结点时,需要先修改两个指针,把 p 的前驱结点的后继

指针指向 p 的后继结点,把 p 的后继结点的前驱指针指向 p 的前驱结点, 后再删除 p

结点。

二、判断题

1.顺序存储方式插入和删除时效率太低,因此它不如链式存储方式好。( )

答案:错

分析:应该说顺序存储方式和链式存储方式各有优缺点,顺序存储方式便于随机访

问数据元素。

2.对任何数据结构链式存储结构一定优于顺序存储结构。( )

答案:错

3.所谓静态链表就是一直不发生变化的链表。( )

答案:错

分析:所谓静态链表就是用数组来模拟链表的结构。在数组中每个元素不但保存了

当前元素的值,还保存了一个指针域,用于指向下一个元素的地址(用数组下标表示)。

这种链表在初始时必须分配足够的空间,也就是空间大小是静态的,在进行插入和删除

时则不需要移动元素,只需修改指针域即可,所以仍然具有链表的主要优点。

4.线性表的特点是每个元素都有一个前驱和一个后继。( )

答案:错

分析:要注意线性表的首元素没有前驱元素,末元素没有后继元素。

5.取线性表的第 i 个元素的时间同 i 的大小有关。( )

答案:错

分析:当线性表用顺序存储时,取每一个元素的时间都是一个常量。

6.线性表只能用顺序存储结构实现。( )

答案:错

分析:线性表可以用顺序存储,也可以用链式存储。

7.线性表就是顺序存储的表。( )

答案:错

分析:线性表是一种逻辑结构,而顺序存储是一种存储结构,两者是不同的概念。

线性表可以用顺序存储。

8.为了很方便地插入和删除数据,可以使用双向链表存放数据。( )

答案:对

分析:在链表中插入或删除元素,一般都需要修改前后结点的指针,而在双向链表

中,对于指定的结点可以方便地找到它的前驱结点和后继结点,所以这句话是对的。

9.顺序存储方式的优点是存储密度大,且插入、删除运算效率高。( )

答案:错

分析:顺序存储方式的优点是存储密度大,且可以随机存取,但是插入、删除运算

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

32

效率低。

10.链表是采用链式存储结构的线性表,进行插入、删除操作时,在链表中比在顺

序存储结构中效率高。( )

答案:对

分析:线性表的链式存储简称链表,在链表中插入或删除元素一般都需要修改相应

结点的指针,而不需要移动数据元素。所以,在链表中进行插入、删除操作要比在顺序

存储结构中效率高。

三、填空题

1.当线性表的元素总数基本稳定,且很少进行插入和删除操作,但要求以 快的

速度存取线性表中的元素时,应采用 存储结构。

答案:顺序

2.在一个长度为 n 的顺序表中第 i 个元素(1<=i<=n)之前插入一个元素时,需向

后移动 个元素。

答案:n-i+1

分析:在第 i 个元素(1<=i<=n)之前插入一个元素就需要把从第 n 个元素开始到

第 i 个元素总共 n-i+1 依次后移 1 个位置。

3.在单链表中设置头结点的作用是 。

答案:使插入、删除等操作统一

分析:如果没有头结点,在插入、删除操作时对空表和非空表要进行区分,以便进

行不同处理。

4.顺序存储结构是通过 表示元素之间的关系的。

答案:物理地址

5.链式存储结构是利用 来表示数据元素之间的逻辑关系。

答案:指针

6.在单链表 L 中,指针 p 所指结点有后继结点的条件是 。

答案:p->next != NULL

7.对于双向链表,在两个结点之间插入一个新结点需修改 个指针。

答案:4

分析:例如在 p、q 两个结点之间插入结点 x,则需要修改 p 的后继指针、q 的前驱

指针、x 的后继指针和 x 的前驱指针。

8.在双向链表中删除一个结点需修改 个指针。

答案:2

分析:在双向链表中删除一个结点,需要修改被删除结点的前驱结点的后继指针和

被删除结点的后继结点的前驱指针。

9.带头结点的双循环链表 L 中只有一个元素结点的条件是 。

答案:L.head->next != L.head && L.head->next->next == L.head

10.带头结点的双循环链表 L 为空表的条件是 。

第一部分 习 题 解 析

33

答案:L.head->next == L.head

四、应用题

1.在顺序表中设计函数实现以下操作:

(1)从顺序表中删除具有 小值的元素(假设顺序表中元素都不相同),并由函数

返回被删元素的值,空出的位置由 后一个元素填补。

(2)从顺序表中删除具有给定值 e 的所有元素。

(3)在一个顺序表中如果一个数据值重复出现,则留下第一个这样的数据值,并删

除其他所有重复的元素,使表中所有元素的值均不相同。

解答:

(1)删除具有 小值的元素时,首先找到 小值元素的位置,然后把表尾元素移到

小值元素的位置,该函数成员的具体实现如下: template <class ElemType> Status SeqList<ElemType>::DeleteMinElem(ElemType &e) { if (length == 0) return UNDER_FLOW; else { int k = 0; for (int i = 1; i < length; i++) if (elems[i] < elems[k]) k = i; e = elems[k]; length--; elems[k] = elems[length]; return SUCCESS; } }

(2)从顺序表中删除具有给定值 e 的所有元素,可以从顺序表中逐一取出元素,如

果取出的元素值等于给定值 e,则删除该元素。该函数成员的实现如下: template <class ElemType> void SeqList<ElemType>::DelElem(ElemType e){ int i = 1; ElemType x; while (i <= length) { GetElem(i, x); if (x == e) DeleteElem(i, x); else i++; } }

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

34

(3)从顺序表中删除重复元素可以从顺序表中逐一取出元素,对取出的元素往后进

行查找,如果有元素的值与取出的元素值相同,则把它删除。该函数成员的实现如下: template <class ElemType> void SeqList<ElemType>::DelRepElem(){ int i = 1, j; ElemType x,y; while (i <= length) { GetElem(i, x); j = i + 1; while (j <= length) { GetElem(j, y); if (x == y) DeleteElem(j, x); else j++; } i++; } }

2.设计一个有序顺序表类,即表中的数据元素按数据元素值递增有序。实现以下

函数:

(1)把给定值 e 插入有序表中。

(2)删除值为 e 的所有数据元素。

(3)合并两个有序表,得到一个新的有序表。

(4)从有序顺序表中删除其值在给定值 s 与 t 之间(s<t)的所有元素,如果 s≥t

或顺序表为空,则显示出错信息,并退出运行。

解答:

有序顺序表 OrdSeqList 与一般顺序表 SeqList 在数据成员的定义上没有差别,而在

操作上主要是插入操作时将根据元素值的大小把元素插入有序顺序表的适当位置,保证

有序顺序表中的元素从小到大的顺序不变。

(1)在有序顺序表中插入元素 e 的函数成员实现如下: template <class ElemType> Status OrdSeqList<ElemType>::InsertElem(const ElemType &e) { int i = length; if (length==maxLength) return OVER_FLOW; else { while (i > 0 && elems[i - 1] > e) { elems[i] = elems[i - 1]; i--; } elems[i] = e;

第一部分 习 题 解 析

35

length++; return SUCCESS; } }

(2)在有序顺序表中删除值为 e 的所有数据元素的函数成员实现如下: template <class ElemType> void OrdSeqList<ElemType>::DeleteElem(ElemType e) { int i = 0, j; while (i <= length - 1 && elems[i] < e) i++; if (i <= length - 1 && elems[i] == e) { j = i + 1; while (j <= length - 1 && elems[j] == e) j++; while (j <= length) { elems[i++] = elems[j++]; } length = length + i - j; } }

(3)由两个有序表合并构造一个新的有序表的函数成员实现如下: template <class ElemType> OrdSeqList<ElemType>::OrdSeqList(const OrdSeqList<ElemType> &sa, const OrdSeqList<ElemType> &sb) { int saLength = sa. GetLength(); int sbLength = sb. GetLength(); ElemType e1, e2; if (sa.maxLength > sb. maxLength) maxLength = 2 * sa. maxLength; else maxLength = 2 * sb. maxLength; elems = new ElemType[maxLength]; assert(elems); length = 0; int i = 1, j = 1; sa.GetElem(1, e1); sb.GetElem(1, e2); while (i <= saLength && j <= sbLength) { if (e1 < e2) { elems[length++] = e1;

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

36

sa. GetElem(++i, e1); } else { elems[length++] = e2; sb. GetElem(++j, e2); } } while (i <= saLength) { elems[length++] = e1; sa. GetElem(++i, e1); } while (j <= sbLength) { elems[length++] = e2; sb. GetElem(++j, e2); } }

(4)从有序顺序表中删除其值在给定值 s 与 t 之间(s<t)的所有元素的函数成员

实现如下: template <class ElemType> void OrdSeqList<ElemType>::DeleteElemBetweenSandT(ElemTypes, ElemTypet) { int i = 0, j; if (length <= 0 || s > t) { cerr << "List is empty or parameters are illegal!" << endl; return; } else { while (i <= length - 1 && elems[i] < s) i++; if (elems[i] <= t) { j = i + 1; while (j <= length - 1 && elems[j] <= t) j++; while (j <= length) { elems[i++] = elems[j++]; } length = length + i - j; } } }

3.设有一个线性表(u1,u2,…,un),试编写一个函数将这个线性表原地逆置,即

线性表逆置为(un,…,u2,u1),要求在顺序表和链表上分别实现,且算法的空间复杂度

为 O(1)。

第一部分 习 题 解 析

37

解答:

(1)在顺序表上进行原地逆置的函数成员实现如下: template <class ElemType> void SeqList<ElemType>::Inverse(){ ElemType t; for ( int i = 0; i <= (length - 1) / 2; i++ ) { t = elems[i]; elems[i] = elems[length - i - 1]; elems[length - i - 1] = t; } }

(2)在链表上进行原地逆置的函数成员实现如下: template <class ElemType> void LinkList<ElemType>::Inverse() { Node<ElemType> *p = head->next, *q; head->next = NULL; while (p != NULL) { q = p->next; p->next = head->next; head->next = p; p = q; } }

4.针对带头结点的单链表,试编写下列函数。

(1)定位函数:在单链表中寻找第 i 个结点。若找到,则返回第 i 个结点的地址,

否则返回空指针。

(2)统计函数:统计单链表中等于给定值 e 的元素个数。

解答:

(1)在单链表中寻找第 i 个结点。若找到,则返回第 i 个结点的地址,否则返回空

指针。 template <class ElemType> Node<ElemType> * LinkList<ElemType>::LocateElem(int i) const { Node<ElemType> *p = head->next; int count = 1; if (i < 1 || i > length) return NULL; else { p = head; while (i > 0) { p = p->next; i--;

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

38

} } return p; }

(2)统计单链表中等于给定值 e 的元素个数。实现如下: template <class ElemType> int LinkList<ElemType>::CountElem(const ElemType &e) const { Node<ElemType> *p = head->next; int count = 0; while (p != NULL) { if (p->data == e) count++; p = p->next; } return count; }

5.设单链表 linklist 采用不带头结点的单链表结构,试编写出插入结点的函数成员

和删除结点的函数成员。

解答:

(1)在不带头结点的单链表中插入第 i 个元素的函数成员实现如下: template <class ElemType> Status LinkList<ElemType>::InsertElem(int i, const ElemType &e) { if (i < 1 || i > length + 1) return RANGE_ERROR; else { Node<ElemType> *p, *q; int count; if (i == 1){ q = new Node<ElemType>(e, head); head = q; } else { p = head; for (count = 2; count < i; count++) p = p->next; q = new Node<ElemType>(e, p->next); p->next = q; } length++; return SUCCESS; } }

第一部分 习 题 解 析

39

(2)在不带头结点的单链表中删除元素的函数成员实现如下: template <class ElemType> Status LinkList<ElemType>::DeleteElem(int i, ElemType &e) { if (i < 1 || i > length) return RANGE_ERROR; else { Node<ElemType> *p, *q; int count; if (i == 1) { q = head; head = head->next; } else { p = head; for (count = 2; count < i; count++) p = p->next; q = p->next; p->next = q->next; } e = q->data; length--; delete q; return SUCCESS; } }

6.设计一个带头结点的有序单链表类,实现以下函数。

(1)插入函数:把元素值 e 作为数据元素插入表中。

(2)删除函数:删除数据元素等于 e 的结点。

解答:

(1)在带头结点的有序单链表中插入元素的函数成员实现如下: template <class ElemType> void OrdLinkList<ElemType>::InsertElem(const ElemType &e) { Node<ElemType> *p, *q; p = head; while (p->next != NULL && p->next->data < e) p = p->next; q = new Node<ElemType>(e, p->next); p->next = q; length++; return; }

(2)在带头结点的有序单链表中删除数据元素等于 e 的函数成员实现如下: template <class ElemType>

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

40

void OrdLinkList<ElemType>::DeleteElem(ElemType &e) { Node<ElemType> *p = head, *q; while (p->next != NULL && p->next->data < e) p = p->next; while (p->next != NULL && p->next->data == e) { q = p->next; p->next = q->next; length--; delete q; } return; }

7.如果用单循环链表表示多项式,试编写一个函数 polynomial :: calc(x),根据参数

x 计算多项式的值。

解答: double Polynomial::Calc(double x) { int i, expt; Status status; PolyItem it; double y = 0, coeft, powX; for (i = 1; i<= Length(); i++){ status = polyList.GetElem(i, it); coeft = it.coef; expt = it.expn; powX = 1; for(int i = 1; i <= expt; i++) { powX = powX * x; } y = y + coeft * powX; } return y; }

8.如果用单循环链表表示多项式,试编写一个函数 polynomial::Derivative(),对多

项式进行求导。

解答: void Polynomial::Derivative() { int i; Status status; PolyItem it; for (i = 1; i<= Length(); i++){ status = polyList.GetElem(i, it);

第一部分 习 题 解 析

41

if (it.expn == 0) { polyList.DeleteElem(i, it); } else { it.coef = it.coef * it.expn; it.expn = it.expn - 1; polyList.SetElem(i, it); } } return; }

9.试设计一个实现下述要求的 Locate 运算的函数。设有一个带表头结点的双向链

表 L,每个结点有四个数据成员:指向前驱结点的指针 prior、指向后继结点的指针 next、

存放数据的成员 data 和访问频度 freq。所有结点的 freq 初始时都为 0。每当在链表上进

行一次 Locate(x)操作时,令元素值为 x 的结点的访问频度 freq 加 1,并将该结点前移,

链接到与它的访问频度相等的结点后面,使得链表中所有结点保持按访问频度递减的顺

序排列,以使频繁访问的结点总是靠近表头。

解答: template <class ElemType> int DblLinkList<ElemType>::LocateElem(const ElemType &e) { DblNode<ElemType> *p = head->next, *q; int count=1; while (p != NULL && p->data != e) { count++; p = p->next; } if (p != NULL) { p->freq++; q = p->prior; while (q != head && q->freq < p->freq){ q = q->prior; count--; } if (q != p->prior){ p->prior->next = p->next; if (p->next != NULL) p->next->prior = p->prior; p->prior = q; p->next = q->next; q->next = p; p->next->prior = p; } return count;

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

42

} else return 0; }

10.有两个不带头结点的单链表,其数据元素为整型数,且递增有序。试编写算法,

将它们合并成一个递减有序的单链表。

解答:

在不带头结点的单链表模板类中定义函数成员 Merge(LinkList<ElemType> &lb),把

链表 lb 中的结点合并到当前链表中。 template <class ElemType> void LinkList<ElemType>::Merge(LinkList<ElemType> &lb) { Node<ElemType> *pa, *pb, *q, *p; pa = head; pb = lB. head; head = NULL; lB.head = NULL; while (pa != NULL && pb != NULL) { if (pa->data <= pb->data) { q = pa; pa = pa->next; } else { q = pb; pb = pb->next; } q->next = head; head = q; } p = (pa != NULL) ? pa : pb; while (p != NULL) { q = p; p = p->next; q->next = head; head = q; } length = length + lB.length; lB. length = 0; }

第四章 栈、队列和递归

4.1 复 习 提 要

线性表的插入和删除操作可以在表的任意位置进行。如果对插入和删除操作的位置

作某些限制,以适应不同的应用场合,就可引出新的数据结构。若限定插入和删除只能

在表尾进行,这样的线性表就叫做栈。若限定插入只能在表尾进行,删除只能在表头进

行,这样的线性表就叫做队列。所以栈和队列只是操作受限的、特殊的线性表,是属于

应用级的数据结构。

本章分别介绍了栈和队列的逻辑结构,相关操作以及其存储结构。作为栈的应用,

本章还讨论了表达式的计算和递归的实现技术。

本章的基本要求是:

1.需要理解和掌握栈的特性;栈的顺序存储结构——顺序栈类 SeqStack 的定义及

其成员函数如构造函数,入栈、退栈、取栈顶元素、判栈空、判栈满、置空栈等操作的

实现;栈的非顺序存储结构——链栈类 LinkStack 的定义及其成员函数如构造函数,入

栈、退栈、取栈顶元素、判栈空、置空栈等操作的实现。

2.需要理解和掌握队列的特性;队列的顺序存储结构——循环队列类 Cirqueue 的

定义及其成员函数如构造函数,入队、出队、取队头元素、判队空、判队满、置空队列

等操作的实现;队列的非顺序存储结构——链队列类 Linkqueue 的定义及其成员函数如

构造函数,入队、出队、取队头元素、判队空、置空队列等操作的实现。

3.需要掌握用后缀表示计算表达式、将中缀表示转换为后缀表示的算法实现,以

加深对栈的应用的认识。

4.需要掌握递归的分层表示——递归树,递归深度(即递归树的深度)与递归工

作栈的关系,单向递归和尾递归的迭代实现,以加深对栈的应用的理解。

需要指出的是递归技术是必须掌握的基本功。一个递归的定义可以用递归的过程计

算,一个递归的数据结构(如后面介绍的广义表,树)可以用递归的过程实现它的各种

操作,一个递归的问题可以用递归的过程求解;但递归过程在时间方面是低效的。务必

充分理解递归过程的机制和利用递归工作栈实现递归的方法。

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

44

4.2 习 题 解 析

一、选择题

1.如果当顺序栈中有 n 个元素时,进行入栈运算发生上溢,则说明该栈的 大容

量为( )。

A.n-1 B.n C.n+1 D.n/2

答案:B

分析:顺序栈的 大容量就是栈所能容纳的元素 大数目,所以答案 B 是正确的。

2.为了增加内存空间的利用率和减少溢出的可能性,由两个顺序栈共享一片连续

的内存空间,此时当( )时,才产生上溢。

A.两个栈的栈顶同时到达栈空间的中心点

B.其中一个栈的栈顶到达栈空间的中心点

C.两个栈的栈顶在栈空间的某一位置相遇

D.两个栈均不空,且一个栈的栈顶到达另一个栈的栈底

答案:C

分析:两个顺序栈共享一片连续的内存空间(数组)时,两个栈顶指针分别设在数

组的两端,当向某个栈插入数据元素时,栈顶指针将向另一端移动(可以越过中心点)

直到和另外一个指针相遇,所以应该选择答案 C。

3.一个栈的输入序列为 1、2、3、…、n,若输出序列的第一个元素是 n,输出第 i

(1<=i<=n)个元素是( )。

A.不确定 B.n-i+1 C. i D.n-i

答案:B

分析:根据栈的性质,输出序列的第一个元素为 n,此时 1、2、3、…、n-1 都在

栈里(从栈底到栈顶)。所以,输出第 i(1<=i<=n)个元素必然是 n-i+1。

4.若一个栈的输入序列为 1、2、3、…、n,输出序列的第一个元素是 i,则第 j 个

输出元素是( )。

A.i-j-1 B.i-j C.j-i+1 D.不确定的

答案:D

分析:输出序列的第一个元素为 i,此时只保证 1、…、i-1 都在栈里。此后入栈和

出栈的次序任意,所以无法确定第 j 个输出元素的值。

5.有六个元素以 6、5、4、3、2、1 的顺序入栈,下列( )不是合法的出栈序列。

A.5 4 3 6 1 2 B.4 5 3 1 2 6

C.3 4 6 5 2 1 D.2 3 4 1 5 6

答案:C

分析:如果用 S 表示入栈操作,X 表示出栈操作,则通过操作 SSXSXSXXSSXX 可

第一部分 习 题 解 析

45

以得到出栈序列 A,通过操作 SSSXXSXSSXXX 可以得到出栈序列 B,通过操作

SSSSSXXXSXXX 可以得到出栈序列 D。而对于出栈序列 C,由于元素 3 是第一个出栈,

此时元素 6、5、4 都在栈里(6 为栈底),元素 6 必须在元素 4、5 之后出栈,因此 C 是

无法完成的出栈序列。

6.下列应用需要用到栈的是( )。

A.递归调用 B.子程序调用 C.表达式求值 D.A、B、C

答案:D

分析:在递归调用和子程序调用需要用栈保护运行现场(各寄存器状态),在表达式求

值过程中需要用栈保存操作数、中间结果和运算符等信息。所以,A、B、C 都需要用到栈。

7.一个递归算法必须包括( )。

A.递归部分 B.终止条件和递归部分

C.迭代部分 D.终止条件和迭代部分

答案:B

分析:递归算法必须包括终止条件和递归部分。迭代部分是迭代算法的组成部分,

而不是递归算法的组成部分。

8.表达式 a*(b+c)-d 的后缀表达式是( )。

A.abcd*+- B.abc+*d- C.abc*+d- D.-+*abcd

答案:B

分析:答案 A 对应的中缀表达式为:a-(b+c*d);答案 C 对应的中缀表达式为:

a+b*c-d;答案 D 不是后缀表达式。

9.用不带头结点的单链表存储队列时,其队头指针指向队头结点,队尾指针指向

队尾结点,则在进行出队操作时( )。

A.仅修改队头指针 B.仅修改队尾指针

C.队头、队尾指针都必须修改 D.队头、队尾指针都可能要修改

答案:D

分析:一般情况(队列中有多个元素)下,出队时只需要修改队头指针,不用修改

队尾指针。但是,当队列中只有一个元素时,进行出队操作会删除队列中的唯一元素(既

是队头,也是队尾),所以需要修改队尾指针。

10.栈和队列的共同点是( )。

A.都是先进先出 B.都是先进后出

C.只允许在端点处插入和删除元素 D.没有共同点

答案:C

分析:根据定义,栈只允许在表尾插入或删除元素,具有先进后出(或后进先出)

的特性;队列只允许在表尾插入元素,在表头删除元素,具有先进先出(或后进后出)

的特性。因此,两者的共同点就是只允许在端点(表头或表尾)处插入和删除元素。

二、判断题

1.两个顺序栈共用存储空间,入栈操作时不存在空间溢出问题。( )

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

46

答案:错

分析:两个顺序栈共用存储空间,可以使两个栈的空间相互调剂,减少栈溢出的概

率。但是,当两个栈顶指针相邻时,存储空间全部放满元素,不能再进行入栈操作。

2.用单链表表示的链式栈的栈底在链表的表尾位置。( )

答案:对

分析:在链式栈中,为了方便入栈和出栈操作,把栈顶设在链表的表头位置,把栈

底设在链表的表尾位置。

3.若输入序列为 1、2、3、4、5、6,则通过一个栈可以输出序列 1、5、4、6、3、

2。( )

答案:对

分析:如果用 S 表示入栈操作,X 表示出栈操作,则通过操作 SXSSSSXXSXXX 可

以得到该出栈序列。

4.只有那种使用了局部变量的递归过程在转换成非递归过程时才必须使用栈。

( )

答案:错

分析:递归工作栈里面包括返回地址、本层的局部变量和递归调用的形参代换用实

参,所以正常情况下,无论递归过程有没有使用局部变量,转换为非递归过程都需要用

栈来进行模拟。当然,有一些特殊递归不用栈就可以直接转换,比如尾递归、常系数递

推等,但是这与是否有局部变量无关。

5.队列是操作受限的线性表,是一种先进后出的线性表。( )

答案:错

分析:队列只允许在表尾插入元素,在表头删除元素,具有先进先出(或后进后出)

的特性。

6.栈是操作受限的线性表,它只允许在表头插入和删除元素。( )

答案:错

分析:栈只允许在表尾插入或删除元素。

7.队列是操作受限的线性表,它只允许在表头插入元素,在表尾删除元素。( )

答案:错

8.通常使用队列来处理函数或过程的调用。( )

答案:错

分析:通常使用栈来处理函数或过程的调用。

9.循环队列也存在空间溢出问题。( )

答案:对

分析:循环队列解决了假溢出问题,但是,当元素放满队列空间时,还是会出现溢出。

10.设栈 S 和队列 Q 的初始状态为空,元素 e1、e2、e3、e4、e5 和 e6 依次通过栈

S,一个元素出栈后即进队列 Q,若 6 个元素出队的序列是 e2、e4、e3、e6、e5、e1,

则栈 S 的容量至少应该是 4。( )

答案:错

第一部分 习 题 解 析

47

分析:出队的顺序实际上就是出栈的顺序。所以,如果用 S 表示入栈操作,X 表示

出栈操作,则通过操作 SSXSSXXSSXXX 可以得到所需要的出栈(也就是出队)序列。

在这个过程中,栈中元素的 大数目为 3,所以栈 S 的容量为 3 就可以完成操作。

三、填空题

1.一个栈的输入序列是 a、b、c,则不可能的出栈序列是 。

答案:c、a、b

2.用 S 表示入栈操作,X 表示出栈操作,若元素入栈的顺序为 1、2、3、4,为了

得到 1、3、4、2 出栈顺序,相应的 S 和 X 操作串为 。

答案:SXSSXSXX

3.表达式 23+((12*3-2)/4+34*5/7)+108/9 的后缀表达式是 。

答案:23 12 3 * 2 - 4 / 34 5 * 7 / + + 108 9 / +

4.表达式 23+((12*3-2)/4+34*5/7)+108/9 的前缀表达式是 。

答案:+ + 23 + / - * 12 3 2 4 / * 34 5 7 / 108 9

5.循环队列的引入,目的是为了克服 。

答案:假溢出现象

6.队列的特点是 。

答案:先进先出(或者后进后出)

7.用 front 和 length 分别表示队头位置和队列长度,则入队操作为 。

答案:elem[front + length++] = e

8.无论对于顺序存储还是链接存储的栈和队列来说,进行插入或删除运算的时间

复杂度相同,均为 。

答案:O(1)

9.有 n 个数依次入栈,出栈序列有 种。

答案:(2n)!/((n+1)n!n!)

分析:对于每一个数来说,都必须进栈一次和出栈一次。在此用 S 表示入栈操作,

X 表示出栈操作,则 n 个数的入栈、出栈操作对应于由 n 个 S 和 n 个 X 组成的长度为

2n 的字符串。由于每个元素必须先入栈、后出栈,所以从左向右扫描这个长度为 2n 的

字符串时,字符“S”的累计数一定不小于字符“X”的累计数。

在长度为 2n 位字符串中,任选 n 位填入字符“S”,其余的 n 位自动填入字符“X”

的方案数为 c(2n,n)。从中减去不符合要求(从左向右扫描,字符“X”的累计数大于

字符“S”的累计数)的方案数即为所求。

不符合要求的字符串的特征是从左向右扫描时,第一次出现字符“X”的数目超过

字符“S”的数目必然在某一奇数位 2m+1 位上(前 2m 位上字符“X”和“S”的数目

一样),也就是在前 2m+1 位中出现 m+1 个字符“X”和 m 个字符“S”,此后的 2(n-m)-1

位中必然有 n-m 个字符“S”和 n-m-1 个字符“X”。如果把后面这 2(n-m)-1 位上的字

符“X”改成字符“S”,字符“S”改成字符“X”,使之成为有 n-m 个字符“X”和 n-m-1

个字符“S”,结果就得到一个由 n+1 个字符“X”和 n-1 个字符“S”组成的 2n 位字符

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

48

串,即一个不合要求的入栈、出栈操作对应于一个由 n+1 个字符“X”和 n-1 个字符“S”

组成的字符串。

反过来,任何一个由 n+1 个字符“X”和 n-1 个字符“S”组成的 2n 位字符串,由

于字符“X”比字符“S”的个数多 2 个,且 2n 为偶数,故必在某一个奇数位上(从左

到右)出现字符“X”的累计数首次超过字符“S”的累计数。同样在后面部分的字符“X”

和字符“S”互换,使之成为由 n 个字符“X”和 n 个字符“S”组成的 2n 位字符串。

这个字符串必定对应一个不合要求的入栈、出栈操作,即由 n+1 个字符“X”和 n-1 个

字符“S”组成的 2n 位字符串必对应一个不符合要求的入栈、出栈操作序列。

因而不合要求的 2n 位字符串与 n+1 个字符“X”、n-1 个字符“S”组成的排列一一

对应。所以,不符合要求的方案数为 c(2n,n+1)。

由此得出输出序列的总数目=c(2n,n)-c(2n,n+1)= (2n)!/((n+1)n!n!)。

10.在双端队列中,元素进入该队列的次序为 a、b、c、d,则既不能由输入受限的

双端队列得到,又不能由输出受限的双端队列得到的输出序列为 。

答案:dbca

分析:所谓双端队列就是在两个端点(表头和表尾)都可以进行插入和删除的线性

表;输入受限的双端队列是指插入操作只能在一端(表头或表尾,一般在表尾)进行,

删除操作可以在两端进行的线性表;输出受限的双端队列是指删除操作只能在一端进行

(表头或表尾,一般在表头),插入操作可以在两端进行的线性表。

在输入受限的双端队列中,输出第一个元素 d 后留在双端队列中元素的排列顺序必

然是 a、b、c(a 为队头,c 为队尾),此时在两端都无法输出第二个元素 b。

在输出受限的双端队列中,输出前 2 个元素 d、b 后留在双端队列中元素的排列顺

序必然是 a、c,此时队头元素为 a,因此无法输出第 3 个元素 c。

四、应用题

1.在中缀表达式的计算时,可用两个栈作辅助工具。对于给出的一个表达式,从

左向右扫描其字符,并将操作数放入栈 S1 中,运算符放入栈 S2 中,但每次扫描到运算

符时,要把它同 S2 的栈顶运算符进行优先级比较,当扫描到的运算符的优先级不高于

栈顶运算符的优先级时,取出栈 S1 的栈顶和次栈顶的两个元素,进行栈 S2 栈顶运算符

的运算,将结果放入栈 S1 中。为方便比较,假设栈 S2 的初始栈顶为®(®运算符的优

先级低于加、减、乘、除中任何一种运算)。现假设要计算表达式:A-B*C/D+E/F。写

出栈 S1 和 S2 的变化过程。

解答:

为了标注表达式的起止,在表达式的前后加符号“®”。下面介绍表达式

®A-B*C/D+E/F®的计算过程及其操作数栈 S1 和操作符栈 S2 的变化过程:

(1)初始时,当前输入符“®”,入操作符栈 S2。

(2)当前输入符为操作数“A”,所以,操作数“A”入操作数栈 S1。

(3)当前输入符为操作符“-”,操作符栈顶运算符“®”比它的优先级低,所以,

操作符“-”入操作符栈 S2。

第一部分 习 题 解 析

49

(4)当前输入符为操作数“B”,所以,操作数“B”入操作数栈 S1。

(5)当前输入符为操作符“*”,操作符栈顶运算符“-”比它的优先级低,所以,

操作符“*”入操作符栈 S2。

(6)当前输入符为操作数“C”,所以,操作数“C”入操作数栈 S1。

(7)当前输入符为操作符“/”,操作符栈顶运算符“*”比它的优先级高。所以,从

操作符栈弹出操作符“*”,从操作数栈弹出栈顶元素 C 作为第二操作数,弹出次栈顶元

素 B 作为第一操作数,并进行 B*C 运算,将运算结果 T1 入操作数栈 S1。

(8)当前输入符为操作符“/”,操作符栈顶运算符“-”比它的优先级低,所以,操

作符“/”入操作符栈 S2。

(9)当前输入符为操作数“D”,所以,操作数“D”入操作数栈 S1。

(10)当前输入符为操作符“+”,操作符栈顶运算符“/”比它的优先级高,所以,

从操作符栈弹出操作符“/”,从操作数栈弹出栈顶元素 D 作为第二操作数,弹出次栈顶

元素 T1 作为第一操作数,并进行 T1/D 运算,将运算结果 T2 入操作数栈 S1。

(11)当前输入符为操作符“+”,操作符栈顶运算符“-”比它的优先级高,所以,

从操作符栈弹出操作符“-”,从操作数栈弹出栈顶元素 T2 作为第二操作数,弹出次栈

顶元素 A 作为第一操作数,并进行 A-T2 运算,将运算结果 T3 入操作数栈 S1。

(12)当前输入符为操作符“+”,操作符栈顶运算符“®”比它的优先级低。所以,

操作符“+”入操作符栈 S2。

(13)当前输入符为操作数“E”,所以,操作数“E”入操作数栈 S1。

(14)当前输入符为操作符“/”,操作符栈顶运算符“+”比它的优先级低,所以,

操作符“/”入操作符栈 S2。

(15)当前输入符为操作数“F”,所以,操作数“F”入操作数栈 S1。

(16)当前输入符为操作符“®”,操作符栈顶运算符“/”比它的优先级高,所以,

从操作符栈弹出操作符“/”,从操作数栈弹出栈顶元素 F 作为第二操作数,弹出次栈顶

元素 E 作为第一操作数,并进行 E/F 运算,将运算结果 T4 入操作数栈 S1。

(17)当前输入符为操作符“®”,操作符栈顶运算符“+”比它的优先级高。所以,

从操作符栈弹出操作符“+”,从操作数栈弹出栈顶元素 T4 作为第二操作数,弹出次栈

顶元素 T3 作为第一操作数,并进行 T3+T4 运算,将运算结果 T5 入操作数栈 S1。

(18)当前输入符为操作符“®”,操作符栈顶运算符“®”,此时表示计算结束,从

操作数栈弹出栈顶元素 T5 作为计算结果。

A-B*C/D+E/F® -B*C/D+E/F® B*C/D+E/F®

-

® A ® A ®

s1 栈 s2 栈 s1 栈 s2 栈 s1 栈 s2 栈

(1)初始状态 (2)操作数 A 入栈 (3)操作符-入栈

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

50

*C/D+E/F® C/D+E/F® /D+E/F®

* C *

B - B - B -

A ® A ® A ®

s1 栈 s2 栈 s1 栈 s2 栈 s1 栈 s2 栈

(4)操作数 B 入栈 (5)操作符*入栈 (6)操作数 C 入栈

/D+E/F® D+E/F® +E/F®

/ D /

T1 - T1 - T1 -

A ® A ® A ®

s1 栈 s2 栈 s1 栈 s2 栈 s1 栈 s2 栈

(7)运算结果 T1 入栈 (8)操作符/入栈 (9)操作数 D 入栈

+E/F® +E/F® E/F®

T2 - +

A ® T3 ® T3 ®

s1 栈 s2 栈 s1 栈 s2 栈 s1 栈 s2 栈

(10)运算结果 T2 入栈 (11)运算结果 T3 入栈 (12)操作符+入栈

/F® F® ®

/ F /

E + E + E +

T3 ® T3 ® T3 ®

s1 栈 s2 栈 s1 栈 s2 栈 s1 栈 s2 栈

(13)操作数 E 入栈 (14)操作符/入栈 (15)操作数 F 入栈

第一部分 习 题 解 析

51

® ® ®

T4 +

T3 ® T5 ® ®

s1 栈 s2 栈 s1 栈 s2 栈 s1 栈 s2 栈

(16)运算结果 T4 入栈 (17)运算结果 T5 入栈 (18)输出结果 T5

2.试用反证法证明:若借助栈可由输入序列 1,2,…,n 得到一个输出序列 p1,

p2,…,pn(它是输入序列的某一种排列),则在输出序列中不可能出现以下情况,即存

在 i<j<k,使得 pj<pk<pi。

解答:

假定 i<j<k,使得 pj<pk<pi

(1)由 j<k 和 pj<pk,则 pj必须在 pk 入栈之前就出栈,即有 push(pj) …pop(pj) …

push(pk) … pop(pk) …。

(2)由 i<j 和 pj<pi,则 pi 必须先于 pj出栈,pj 必须先于 pi入栈,即有 push(pj) …

push(pi) … pop(pi)…pop(pj) …。

(3)由 i<k 和 pk<pi,则 pi 必须先于 pk 出栈,pk 必须先于 pi 入栈,即有 push(pk) …

push(pi)…pop(pi)…pop(pk)…。此处 push(pk) 出现在 pop(pi)之前,而由(1)和(2)知

push(pk) 出现在 pop(pj)之后和 push(pj) 出现在 pop(pi)之后,即有 push(pk) 出现在 pop(pi)

之后,矛盾。

下面详细解释之。

因为借助栈由输入序列 1, 2, 3, …, n 可得到输出序列 p1, p2, p3, …, pn ,如果存在下

标 i, j, k,满足 i < j < k,那么在输出序列中,可能出现如下 5 种情况:

① i 进栈,i 出栈,j 进栈,j 出栈,k 进栈,k 出栈。此时具有 小值的排在 前面

pi 位置,具有中间值的排在其后 pj位置,具有 大值的排在 pk 位置,有 pi < pj < pk, 不

可能出现 pj < pk < pi 的情形。

② i 进栈,i 出栈,j 进栈,k 进栈,k 出栈,j 出栈。此时具有 小值的排在 前面

pi 位置,具有 大值的排在 pj位置,具有中间值的排在 后 pk 位置,有 pi < pk < pj , 不

可能出现 pj < pk < pi 的情形。

③ i 进栈,j 进栈,j 出栈,i 出栈,k 进栈,k 出栈。此时具有中间值的排在 前面

pi 位置,具有 小值的排在其后 pj位置,有 pj < pi < pk, 不可能出现 pj < pk < pi 的情形。

④ i 进栈,j 进栈,j 出栈,k 进栈,k 出栈,i 出栈。此时具有中间值的排在 前面

pi 位置,具有 大值的排在其后 pj位置,具有 小值的排在 pk 位置,有 pk < pi < pj, 也

不可能出现 pj < pk < pi 的情形。

⑤ i 进栈,j 进栈,k 进栈,k 出栈,j 出栈,i 出栈。此时具有 大值的排在 前面

pi 位置,具有中间值的排在其后 pj位置,具有 小值的排在 pk 位置,有 pk < pj < pi, 也

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

52

不可能出现 pj < pk < pi 的情形。

3.若以 1、2、3、4 作为双端队列的输入序列,试分别求出以下条件的序列:

(1)能由输入受限的双端队列得到,但不能由输出受限的双端队列得到的输出序列。

(2)能由输出受限的双端队列得到,但不能由输入受限的双端队列得到的输出序列。

解答:

所谓双端队列就是在两个端点(表头和表尾)都可以进行插入和删除的线性表;输

入受限的双端队列是指插入操作只能在一端(表头或表尾,一般在表尾)进行,删除操

作可以在两端进行的线性表;输出受限的双端队列是指删除操作只能在一端进行(表头

或表尾,一般在表头),插入操作可以在两端进行的线性表。

(1)能由输入受限的双端队列得到,但不能由输出受限的双端队列得到的输出序列

为:4、1、3、2。

(2)能由输出受限的双端队列得到,但不能由输入受限的双端队列得到的输出序

列:4、2、1、3。

4.根据教材中介绍的两个顺序栈共享一个存储空间的设计,完成其中入栈、出栈

和判断栈空函数的实现。

解答:

(1)入栈操作时,先判断栈是否已经满,如果栈已满,则返回 OVER_FLOW,否

则把元素 e 插入指定栈的栈顶,并返回 SUCCESS。其函数成员实现如下: template<class ElemType> Status TwoSeqStack<ElemType>::Push(const ElemType e, int No) { if (top1 == top2 - 1) return OVER_FLOW; else { if (No == 1) elems[++top1] = e; else elems[--top2] = e; return SUCCESS; } }

(2)出栈操作时,先判断指定栈是否为空,如果栈为空,则返回信息 UNDER_FLOW,

否则从指定栈取栈顶元素,用参数 e 带回,并返回信息 SUCCESS。其函数成员实现如下: template<class ElemType> Status TwoSeqStack<ElemType>::Pop(ElemType &e, int No) { if (IsEmpty(No)) return UNDER_FLOW; else { if (No == 1) e = elems[top1--];

第一部分 习 题 解 析

53

else e = elems[top2++]; return SUCCESS; } }

(3)判断栈空的函数成员实现如下,其中参数 No 表示要判断的栈的编号: template<class ElemType> bool TwoSeqStack<ElemType>::IsEmpty(int No) const { if (No ==1 ) return top1 == -1; else return top2 == maxSize; }

5.设计利用两个栈 sl,s2 模拟一个队列,请写出实现入队、出队和判队列空的函

数的实现。

解答:

(1)入队操作只需把元素入 s1 栈,入队操作的函数成员具体实现如下: template<class ElemType> Status TwoStackToQueue<ElemType>::EnQueue(const ElemType e) { return s1.Push(e); }

(2)出队操作时,先把 s1 栈中元素全部倒入 s2,再把 s2 栈的栈顶元素弹出, 后

把 s2 栈中剩下的元素全部倒回 s1 栈,出队操作的函数成员具体实现如下: template<class ElemType> Status TwoStackToQueue<ElemType>::DelQueue(ElemType &e) { ElemType e1; if (!s1.IsEmpty()) { while (!s1.IsEmpty()){ s1.Pop(e1); s2.Push(e1); } s2.Pop(e); while (!s2.IsEmpty()){ s2.Pop(e1); s1.Push(e1); } return SUCCESS; } else return UNDER_FLOW; }

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

54

(3)栈 s1 为空就代表队列为空,所以此函数成员具体实现如下: template<class ElemType> bool TwoStackToQueue<ElemType>::IsEmpty() const { return s1.IsEmpty(); }

6.修改教材中介绍的循环队列设计,以 front 和 length 分别表示循环队列中的队头

位置和队列中所含元素的个数。试完成循环队列判断队空、判断队满、入队和出队函数

的实现。

解答:

(1)判断队空的函数成员如下。如队列为空,则返回 true,否则返回 false。 template<class ElemType> bool SeqQueue<ElemType>::IsEmpty() const { return length == 0; }

(2)判断队满的函数成员如下。如队列为满,则返回 true,否则返回 false。 template<class ElemType> bool SeqQueue<ElemType>::IsFull() const { return length == maxSize; }

(3)入队操作时,先判断队列是否已经满,如果队列已满,返回 OVER_FLOW,

否则把元素 e 插入队尾并返回 SUCCESS。其函数成员实现如下: template<class ElemType> Status SeqQueue<ElemType>::EnQueue(const ElemType e) { if (IsFull()) return OVER_FLOW; else { elems[(front + length) % maxSize] = e; length++; return SUCCESS; } }

(4)出队操作时,先判断队列是否为空,如果队列为空,返回 UNDER_FLOW,否

则取出队头元素用参数 e 带回其值,并返回 SUCCESS。其函数成员实现如下: template<class ElemType> Status SeqQueue<ElemType>::DelQueue(ElemType &e) { if (!IsEmpty()) { e = elems[front]; front = (front + 1) % maxSize;

第一部分 习 题 解 析

55

length--; return SUCCESS; } else return UNDER_FLOW; }

7.二项式(a+b)n 展开后,其系数构成杨辉三角形,写出利用队列实现打印杨辉三角

形的前 n 行的算法。

解答:

先看看杨辉三角形的形式,当 n=5 时,二项式(a+b)n 展开后,其系数构成的杨辉三

角形如下:

1 1

1 2 1

1 3 3 1

1 4 6 4 1

1 5 10 10 5 1

分析第 i 行元素与第 i+1 行元素的关系,如图 1-1 所示。

图 1-1 上下两行元素的关系

根据图 1-1 所示,从前一行的数据可以计算下一行的数据。从第 i 行数据计算并存

放第 i+1 行数据,如图 1-2 所示。

图 1-2 数据变化过程

打印杨辉三角形的前 n 行的算法实现如下: void YangHui(int n) { LinkQueue<int> q; q.EnQueue(1); q.EnQueue(1); //预先放入第一行的两个系数 int s = 0, t; //计算下一行系数时用到的工作单元

s+t

ts

i=2

i=3

i=4

0

0

1 2 1 0

1 3 3 1 0

1 4 6 4 1

q 1 2 1 0 1 3 3 1 0

s=0 t=1 t=2 t=1 t=0 t=1 t=3 t=3 t=1 t=0s=t s+t s=t

s+t s+t s=t

s+ts=t

s+ts=t

s+ts=t s=t s=t

s+t s+t s+t 科

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

56

for(int i = 1; i <= n; i++) { cout << endl; //换一行 q.EnQueue (0); //各行之间插入一个0 for (int j = 1; j <= i+2; j++){ q.DelQueue(t); //读取一个系数 q.EnQueue(s + t); //计算下一行系数,并进队列 s = t; if (j!= i+2) cout << s << ' '; //打印一个系数,第i+2个是0 } } }

8.试将下列递归过程改写为非递归过程。 void test(int &sum) { int x; scanf("%d", &x); if (x == 0) sum=0; else { test(sum); sum+=x; } printf("%d ",sum); }

解答:

该过程输入一个数据序列(以 0 表示输入结束),按照输入的相反顺序进行累加,

输出每一步累加的结果,并用参数 sum 带回累加结果。因此,在改写为非递归过程时,

先将输入数据先入栈,输入结束后,将栈中数据出栈并进行累加。

改写的函数实现如下: void test(int &sum){ LinkStack<int> s; int x; sum = 0; scanf("%d",&x); while (x != 0) { s.Push(x); scanf("%d", &x); } printf("%d ",sum); while (!s.IsEmpty()) { s.Pop(x); sum += x; printf("%d ",sum);

第一部分 习 题 解 析

57

} }

9.设计算法,利用栈完成中缀表达式的计算。

解答:

入队操作只需把元素入 s1 栈,入队操作的函数成员具体实现如下: void InfixExpressionCalculation (){ LinkStack<double> opnd; // 操作数栈 LinkStack<char> optr; // 操作符栈 char ch; // 临时字符 char priorChar; // 当前输入的前一个字符 char optrTop = '#'; // 操作符栈的栈顶字符 char op; // 操作符 double operand, first, second; // 操作数 optr.Push('#'); // 在操作符栈中加入一个'#' priorChar = '#'; // 前一字符 cout << "输入中缀表达式:"; ch = GetChar(); // 读入一个字符 while (optrTop != '#' || ch != '#') { // 当前表达式还未运算结束,

继续运算 if (isdigit(ch) || ch == '.') { // ch为一个操作数的第1个

字符 if (priorChar == '0' || priorChar == ')') throw Error("两个操作数之间缺少运算符!"); // 抛出异常 cin.putback(ch); // 将字符ch放回输入流 cin >> operand; // 读入操作数 opnd. Push(operand); // 操作数入opnd栈 priorChar = '0'; // 前一字符不是操作符,规定前一

字符为'0' ch = GetChar(); // 读入下一个字符 } else if(!IsOperator(ch)) { // 既不是操作数,也不属于操作符 throw Error("表达式中有非法符号!"); // 抛出异常 } else { // ch为操作符 if (ch == '(' && (priorChar == '0' || priorChar == ')')) throw Error("'('前缺少操作符!"); // 抛出异常 while (OperPrior(optrTop, ch) == 2) { optr.Pop(op); if (opnd.Pop(second) == UNDER_FLOW) throw Error("缺少操作数!"); // 抛出异常 if (opnd.Pop(first) == UNDER_FLOW) throw Error("缺少操作数!"); // 抛出异常 opnd.Push(Operate(first, op, second)); // 运算结果入

opnd栈

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

58

if (optr.Top(optrTop) == UNDER_FLOW) throw Error("缺少操作符!"); // 抛出异常 } switch (OperPrior(optrTop, ch)) { case -1 : throw Error("括号不匹配!"); case 0 : optr.Pop(optrTop); if (optr.Top(optrTop) == UNDER_FLOW) throw Error("缺少操作符!"); // 抛出异常 priorChar = ch; // 新的前一字符为( ch = GetChar(); // 读入新字符 break; case 1 : optr.Push(ch); optrTop = ch; priorChar = ch; // 新的前一字符为) ch = GetChar(); // 读入新字符 break; } } } if (opnd.Pop(operand) == UNDER_FLOW) throw Error("缺少操作数!"); // 抛出异常 cout << "表达式结果为:" << operand << endl; // 显示表达式的值 };

10.已知 Ackermann 函数定义如下: n 1 m 0

Akm(m,n) Akm(m 1,1) m 0,n 0

Akm(m 1, Akm(m, n 1)) m 0,n 0

(1)写出 Akm(2,1)的计算过程。

(2)写出计算 Akm(m,n)的非递归算法。

解答:

(1)计算 Akm(2, 1)的递归调用树如图 1-3 所示。

图 1-3 计算 Akm(2, 1)的递归调用树

Akm(2, 1)

v=Akm(2, 0)

Akm(1, 1)

v=Akm(1, 0)

Akm(0, 1) = 2

Akm(0, 2)= 3

Akm(1, 3)

v =Akm(1, 2)

v = Akm(1, 1)

v = Akm(1, 0)

Akm(0, 1) = 2

Akm(0, 2) = 3

Akm(0, 3) = 4

Akm(0, 4) = 5

v = 2

v = 2

v = 3

v = 4

v = 3 Akm = 5

Akm = 5

Akm = 3

第一部分 习 题 解 析

59

用到一个栈记忆每次递归调用时的实参值,每个结点两个域{vm, vn}。对以上实例,

栈的变化如下:

(2)计算 Akm(m,n)的非递归算法实现如下: struct node{ // 定义栈的结点 unsigned vm, vn; // 对应参数m、n }; unsigned Akm(unsigned m, unsigned n) { LinkStack<node> s; // 用于保护现场的链式栈 node w, e; unsigned v; w.vm = m; w.vn = n; s.Push(w); do { s.Top(e); while (e.vm > 0) { //计算Akm(m-1, Akm(m, n-1) ) s.Top(e); while (e.vn > 0) { //计算Akm(m, n-1), 直到Akm(m, 0) w.vn--; s.Push(w); s.Top(e); } s.Pop(w); //计算Akm(m-1, 1) w.vm--; w.vn = 1; s.Push( w ); s.Top(e); do } //直到Akm( 0, Akm( 1, * ) ) s.Pop(w);

2 1 2 1 2 1 2 1 2 1 1 3 2 0 1 1 1 1 1 1 0 2

1 0 0 1

改 Akm(m-1,1) 改 Akm(m-1,1) v = n+1= 2 改 Akm(m-1,v) 改 Akm(m-1,v) v = n+1 = 3

vm vn vm vn vm vn vm vn vm vn vm vn

vm vn vm vn vm vn vm vn vm vn vm vn

1 3 1 3 1 3 1 3 0 4

1 2 1 2 1 2 0 3 1 1 1 1 0 2 1 0 0 1

改 Akm(m-1,1) v = n+1 = 2 改 Akm(m-1,v) 改 Akm(m-1,v) 改 Akm(m-1,v) 栈空, 返回 v = 5 v = n+1 = 3 v = n+1 = 4 v = n+1 = 5

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

60

v = ++w.vn; //计算v = Akm( 1, * )+1 if (!s.IsEmpty( )){ //如果栈不空, 改栈顶为( m-1, v ) s.Pop(w); w.vm--; w.vn = v; s.Push( w ); } } while (!s.IsEmpty( )); return v; }

第五章 串、数组和广义表

5.1 复 习 提 要

线性表是客观现实中一类问题的抽象,其特点是表中数据元素(数据成员)之间具

有线性关系及数据元素属性的一致性。通常将数据元素称为记录,一般由若干个数据项

组成,视具体问题的不同而不同。如果将表中数据元素限定为字符,这样的线性表就叫

做串。如果将表中数据元素扩展为本身也是一个数据结构,就可引出称之为数组的数据

结构。如果放弃表中数据元素属性一致性的限制,就可得到称之为广义表的数据结构。

所以,串是特殊的线性表,而数组和广义表则是线性表的推广。

本章依次介绍了串的逻辑结构,串的操作,串的顺序存储结构;数组的逻辑结构,

数组的操作,数组的顺序存储结构;稀疏矩阵的概念及表示;广义表的逻辑结构,广义

表的操作,广义表的链式存储结构。

本章的基本要求是:

1.需要理解串的概念,掌握串的顺序存储结构——顺序串类 String 的定义及其成

员函数如构造函数,各种重载操作,模式匹配算法的实现。

2.需要理解数组的概念,明确静态数组和动态数组的不同特点和使用,掌握数组

的顺序存储结构—— 一维数组类 array 的定义及其成员函数的算法实现,二维、三维、n

维数组的地址计算方法。

3.需要掌握稀疏矩阵的三元组表的顺序存储结构——三元组顺序表类 Sparsematrix

的定义及其成员函数如构造函数,稀疏矩阵转置算法的实现;稀疏矩阵的三元组表的非

顺序存储结构——三元组十字链表类 matrix 的定义及其稀疏矩阵相关运算的实现。

4.需要理解广义表的概念、广义表的操作、广义表的特性,掌握广义表的链式存

储结构——广义表链表类 genlist 的定义及其成员函数如广义表的访问算法、广义表的递

归算法的实现。

可以将稀疏矩阵看作是一种数据结构,它是以三元组为数据元素的线性表。所以,

串和稀疏矩阵均是应用级的数据结构,而数组则是一种实现级的数据结构。广义表允许

表中套表,它已经不一定是线性数据结构了,甚至可以是复杂的非线性数据结构如层次

型数据结构(后面介绍的树就可用广义表表示),它的递归定义使得广义表的许多操作

可以用递归算法实现,需特别加以注意。

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

62

5.2 习 题 解 析

一、选择题

1.下列关于串的叙述中,( )是不正确的。

A.串是字符的有限序列 B.空串是由空格构成的串

C.模式匹配是串的一种重要运算 D.串可以采用链式存储

答案:B

分析:空串是长度为 0 的字符串;而由空格符组成的串称为空格串,其长度大于 0。

所以选择答案 B。

2.设有两个串 p 和 q,其中 q 是 p 的子串,求 q 在 p 中首次出现的位置的算法称为

( )。

A.插入子串 B.联接 C.模式匹配 D.求串长

答案:C

3.设 S 为一个长度为 n 的字符串,其中的字符各不相同,则 S 中互异的非平凡子

串(非空且不同于 S 本身)的个数为( )。

A.2n-1 B.n2 C.(n2/2)+(n/2) D.(n2/2)+(n/2) -1

答案:D

分析:长度为 1 的子串有 n 个,长度为 2 的子串有 n-1 个……长度为 n-1 的子串有

2 个。所以,S 中互异的非平凡子串数为:n+(n-1)+…+2 = (n2/2)+(n/2)-1。

4.二维数组 A 的元素都是 6 个字符组成的串,行下标 i 的范围从 0 到 8,列下标

j 的范围从 1 到 10。若 A 按行存放,元素 A[8,5]的起始地址与 A 按列存放时的元素

( )的起始地址一致。

A.A[8, 5] B.A[3, 10] C.A[5, 8] D.A[0, 9] 答案:B

分析:如果数组 A 按行存放,则元素 A[8,5]的起始地址为:SA+(8*10+4)*6 =

SA+504。如果数组A按列存放,则元素A[3,10]的起始地址为:SA+(9*9+3)*6 = SA+504。

所以,选择答案 B。

5.若对 n 阶对称矩阵 A 以行序为主序方式将其下三角形的元素(包括主对角线上

所有元素)依次存放于一维数组 B[1…(n(n+1))/2]中,则在 B 中确定 aij(1≤i≤j≤n)

的位置 k 的关系为:k 等于( )。

A.i*(i-1)/2+j B.j*(j-1)/2+i

C.i*(i+1)/2+j D.j*(j+1)/2+i

答案:B

分析:题目要确定元素 aij(1≤i≤j≤n)的存储位置 k,该元素在矩阵的上三角部

分,因此根据对称矩阵的性质,就是要确定下三角元素 aji的位置。在按行优先的下三角

第一部分 习 题 解 析

63

矩阵元素的存储形式中,第 1 行存储 1 个元素,第 2 行存储 2 个元素……第 j-1 行存储

j-1 个元素。所以,第 j 行、第 i 列(aji)的元素地址:k=SA+(1+2+…+(j-1))+ (i–1) =

j*(j-1)/2+i-1(假设每个元素占一个存储单元)。又因为起始地址为 1(SA=1),所以要

求的 k= j*(j-1)/2+i。

6.设 A 是 n*n 的对称矩阵,将 A 的对角线及对角线上方的元素以列为主的次序存

放在一维数组 B[1..n(n+1)/2]中,对上述任一元素 aij(1≤i≤j≤n)在 B 中的位置为( )。

A.i(i-l)/2+j B.j(j-l)/2+i

C.j(j-l)/2+i-1 D.i(i-l)/2+j-1

答案:B

分析:在按列优先的上三角矩阵元素的存储形式中,第 1 列存储 1 个元素,第 2 列

存储 2 个元素……第 j-1 列存储 j-1 个元素。所以,第 i 行、第 j 列(aij)的元素地址:

k=SA+(1+2+…+(j-1))+ (i–1) = j*(j-1)/2+i-1(假设每个元素占一个存储单元)。又因为

起始地址为 1(SA=1),所以要求的 k= j*(j-1)/2+i。

7.对稀疏矩阵进行压缩存储的目的是( )。

A.便于进行矩阵运算 B.便于输入和输出

C.节省存储空间 D.降低运算的时间复杂度

答案:C

分析:稀疏矩阵压缩存储的目的是节省存储空间,但进行压缩存储后进行矩阵的输

入、输出和运算操作就要比非压缩存储时复杂一点,运算的时间也要多。这是典型的以

时间换空间的例子。

8.已知广义表 L=((x,y,z),a,(u,t,w)),从 L 表中取出原子项 t 的运算是( )。

A.head(tail(tail(L)))

B.tail(head(head(tail(L))))

C.head(tail(head(tail(L))))

D.head(tail(head(tail(tail(L)))))

答案:D

9.广义表((a,b,c,d))的表头是( ),表尾是( )。

A.a B.() C.(a,b,c,d) D.(b,c,d)

答案:C、B

10.设广义表 L=((a,b,c)),则 L 的长度和深度分别为( )。

A.1 和 1 B.1 和 3 C.1 和 2 D.2 和 3

答案:C

二、判断题

1.KMP 算法的特点是在模式匹配时指示主串的指针不会变小。( )

答案:对

分析:用 KMP 算法进行模式匹配,当主串中当前字符与模式串(子串)中当前字

符不相等时,模式串的指针根据失效值回退(变小)后,再进行字符比较,主串的指针

学出版社

职教技术出版中心

www.abook.cn

数据结构——C++实现(第二版)习题解析与实验指导

64

不会回退。

2.设模式串的长度为 m,目标串的长度为 n,当 n≈m 且处理只匹配一次的模式时,

朴素的匹配(即子串定位函数)算法所花的时间代价可能会更为节省。( )

答案:对

分析:朴素的匹配算法在 坏情况字符比较的总次数为(m-n+1)*n,当 n≈m 时

总比较次数为 kn(1≤k<2),也就是其时间复杂度为 O(n)。而用 KMP 算法进行模式匹

配,则包括计算失效值在内,其时间复杂度为 O(m+n)。所以,这句话是正确的。

3.串是一种数据对象和操作都特殊的线性表。( )

答案:对

分析:在串中数据对象只能是字符,而且串的基本运算包括:字符串连接、插入子

串、模式匹配(子串定位)等和线性表不同的操作。

4.深度为 1 的广义表相当于线性表。( )

答案:对

分析:在深度为 1 的广义表中一定没有子表元素,所以它实际上已经退化为线性表。

5.从逻辑结构上看,n 维数组可以看成每个元素均为 n-1 维数组的数组。( ) 答案:对

6.稀疏矩阵压缩存储后,必会失去随机存取功能。( )

答案:对

分析:稀疏矩阵压缩存储后,不能用地址公式直接计算出元素的存储位置,所以就

无法进行随机存取。

7.数组元素的值必须是同类型的。( )

答案:对

分析:所谓数组,就是相同数据类型的元素按一定顺序排列的集合,就是把有限个

类型相同的变量用一个名字命名,然后用编号区分它们的变量的集合,这个名字称为数

组名,编号称为下标。

8.数组可看成线性结构的一种推广,因此与线性表一样,可以对它进行插入和删

除等操作。( )

答案:错

分析:数组在维数和边界确定后,其元素个数已经确定,不能进行插入和删除运算。

9.广义表的取表尾运算,其结果通常是个子表,但有时也可是个单元素值。( )

答案:错

分析:根据定义广义表的表尾是指非空广义表除第一个元素外剩下元素组成的子

表,所以其结果一定是一个子表,不可能是单元素(也称原子元素)。

10.若一个广义表的表头和表尾都是空表,则此广义表的长度为 1,深度为 2。( ) 答案:对

分析:表头和表尾都为空表的广义表一定是只有一个空的子表元素的广义表。所以,

其长度为 1,深度为 2。