本书内容全面、细致、通俗易懂。涵盖线性表、栈和队列、树和二叉树、堆、哈夫曼树、并查集、AVL树、红黑树、B树和B+树、串、图、散列表等数据结构,以及枚举、二分、递归、分治、动态规划、贪心、深搜、广搜、最短路、最小生成树、拓扑排序、关键路径、内外排序等算法。对各类数据结构和算法,不但要掌握理论,还应熟练地编程实现。本书的最大特点是高标准的实践性。除了少数几个特别复杂的数据结构,95%的数据结构和算法都给出了完整可运行的代码,一共130多份,并且这些代码几乎都出现在具体的例题中。本书的例题和编程习题,都可以在北京大学在线程序评测平台OpenJudge上提交解题程序并自动评判对错。本书内容和习题按难度做了明确分级,因此不论是高等学校计算机专业还是非计算机专业的师生,都可以从中各取所需用于教学。本书既可以用作数据结构和算法入门教材,又可以作为考研、找工作面试的提高秘籍,还可以用于程序设计竞赛的基础培训。
市场上讲述数据结构的书较多,但兼顾算法的不算多。本教材深度广度相对都更大些。大多数数据结构和算法教材采用伪代码描述数据结构和算法,实践性稍弱。给出了绝大多数数据结构和算法的完整可运行的C++程序,而且C++程序采用C++17标准,充分体现C++语言在实现数据结构和算法方面的优势。本教材结合了在线程序评测平台,例题习题都可以在平台上提交,有很强的实践性,符合目前考研和应聘大多需要在在线评测平台上编程做题的需求。大多数计算机专业的数据结构课程教学,采用的是C语言。C语言不具备面向对象、泛型和函数式的特点,其实并不适合用来描述数据结构和算法。因此计算机专业的数据结构和算法课程教学,存在用C++语言替代C语言的改革需求。然而,C++语言学习较高的学习门槛,成为数据结构和算法教学改革的障碍。本书精心挑选C++语言的核心内容,去除编写小型程序并不需要的特性,设计了适合4个课时课堂教学的“C++语言快速入门”章节,使得掌握C语言的学生迅速可以掌握C++语言的核心,看懂用C++语言实现的数据结构和算法,并能模仿编写,从而为从C语言转变到C++语言的数据结构和算法教学改革扫清了障碍。
前言
要成为的程序员,必须学习“数据结构与算法”课程。虽然同类课程或教材有些只是名为“数据结构”,没有提到“算法”,但实际上,数据结构和算法,没有必要也无法严格区分,两者是“你中有我、我中有你”的关系。或者,将数据结构算作算法的一个分支也未尝不可,如著名教材《算法导论》,其中就包含大量数据结构的内容。本书中涉及的问题,如果需要将数据以复杂的方式组织起来,就归类为数据结构;否则,就归类为算法。
大多数课程或教材,用C语言实现数据结构和算法。然而,相比C++语言,甚至Java和Python语言,用C语言实现数据结构和算法有明显的劣势。C语言没有对面向对象程序设计方法的支持,实现的数据结构无法做到“封装”和“隐藏”。不支持“封装”,导致代码缺乏局部性,难以理解,难以维护。不支持“隐藏”,导致无法防止数据结构从外部被不慎破坏,缺乏安全性。C语言不支持泛型程序设计,导致实现的数据结构和算法缺乏通用性或可重用性。例如,编写一个可以对任何数据类型的数组排序的函数、编写一个可以存放任何数据类型的二叉查找树数据结构是比较困难的,即便实现了,这样的函数和数据结构的使用方式也较为麻烦和不自然。C语言无可变长数组,这导致在实现图的邻接表、树等数据结构时不得不采用链表、儿子兄弟树等编程效率或运行效率不佳的方案。总之,“数据结构与算法”是理论结合实践的课程,对数据结构和算法的编程实现,不但应该正确,还应在工程上是优美的,即安全、简洁、好用、可重用,这用C语言很难做到。目前,程序设计方法的潮流是面向对象、泛型和函数式编程,C语言对这三大特性均无支持,故作者认为,当前的数据结构和算法教学,亟须改革,将用C语言描述数据结构和算法,转变为用支持面向对象、支持泛型和函数式编程的C++语言描述。
然而,系统掌握C++语言有较高的学习成本,即便对已经掌握C语言的编程者也是如此,这是阻碍C++语言在数据结构和算法教学中得以推广的主要原因。实际上,C++语言的大量特性,如异常处理、继承、多态等,是为了编写大型程序而设计的,数十行或一百多行代码即可实现的数据结构和算法不需要使用这些特性,因而用C++语言进行数据结构与算法教学,其实无须学员系统地掌握C++语言,只须学会C++语言中一小部分最核心的面向对象、泛型和函数式编程特性即可。为此,作者抽取了这部分核心特性,精心编写了“C++语言快速入门和温故知新”一章,依此章进行4个课时的教学,可使学过C语言的学生掌握C++语言的精髓,从而可以充分理解本书的所有程序,并用C++语言编写小型程序。本书完全适用于只学过C语言的读者,故虽然所有程序均用C++语言实现,却依然命名为《数据结构与算法(C/C++语言实现)》。
作者在北京大学讲授“数据结构与算法”和“数据结构与算法实习”课程多年,并曾担任北京大学ACM国际大学生程序设计竞赛队教练9年。作者讲授的这些课程,既有面向非计算机专业的,也有面向计算机专业的。本书即是对这些课程教学经验的归纳与整合。
本书数据结构部分包括线性表、栈、队列、二叉树、堆、哈夫曼树、树和森林、并查集、散列表、二叉查找树、AVL树、红黑树、B树、B+树、图、矩阵等内容。算法部分包括枚举、二分、递归、深度优先搜索、广度优先搜索、贪心、动态规划、内排序、外排序、最短路、最小生成树、拓扑排序、关键路径等内容。数据结构部分和算法部分交替讲述。本书内容覆盖计算机专业408考研大纲。
相比大部分同类教材,本书的知识覆盖面更广,尤其是算法部分。
阅读本书需要读者已经掌握C语言程序设计的基础知识。对于学过C语言程序设计的读者,本书非常适合作为第二门编程课的教材。
本书内容和习题按难度明确分级,不论是高校计算机专业还是非计算机专业的师生,都可以从中各取所需。不带“★”标记的是基本内容,适用于所有读者;计算机专业的读者应掌握带“★”标记的章节;标记为“★★”的内容,则适用于高水平院校计算机专业的教学;少数标记为“★★★”的例题和习题,难度与大学生程序设计竞赛的中等题相当。有些例题习题来自早年的程序设计竞赛,或在竞赛中本就是简单题,难度不高,因而没有“★★★”标记,甚至没有“★”标记。
作者考察了许多国内外流行的数据结构与算法教材,发现许多教材中多用伪代码,或不完整的代码来描述数据结构和算法,很少给出能直接运行的完整程序。不但需要实打实编程解决的例题很少,而且配套的习题,基本都是考查概念,或只要求描述解决问题的过程,几乎不会要求写出完整的、完全正确的程序。即便一些教材中有编程习题,读者也无法评判自己编写的解题程序是否完全正确无隐错。用这样的教材教学,虽然可以应付考研等笔试考试,但是难免有纸上谈兵之嫌。一旦碰到企业招聘要求现场写代码,或者考研复试要求上机写代码,往往力不从心。
相比大多数数据结构和算法教材,本书的最大特点就是高标准的实践性。除了少数几个特别复杂的数据结构,95%的数据结构和算法都给出了完整可运行的代码,并且这些代码大部分都出现在具体的例题中。
本书的例题和编程习题,都可以在北京大学在线程序评测平台OpenJudge上提交解题程序。平台广泛用于北京大学“计算概论”“程序设计实习”“数据结构与算法”等编程类课程的教学。要在OpenJudge上完成本书的编程习题,必须对相应的数据结构和算法知识的每个实现细节都清楚地掌握,且编写的程序不能有隐错,这样的要求,比自己写个程序随意测试一下没发现问题就算对了要高得多,更远非在纸上写写画画的笔试型习题可比。OpenJudge的具体使用方法见附录。
本书配套电子资料齐全,包括课程讲义以及130多个风格简洁优美的程序源码。
本书还另有Python语言版和Java语言版,均已经由清华大学出版社出版。
作者水平有限,书中难免存在一些不足及疏漏之处,恳请读者批评指正。读者可以通过guo_wei@pku.edu.cn与作者沟通、交流。
作者的女儿兼校友郭小美审阅并校对了全部书稿。一部分程序由她从作者编写的其他语言的程序改写为C++程序,特此感谢。
郭炜
于北京大学信息科学技术学院
2025年4月
郭炜,北京大学信息科学技术学院教师。曾担任北京大学ACM大学生程序设计竞赛队教练9年。在中国大学MOOC独立开设的《程序设计与算法》系列课程获评国家精品在线开放课程。在华文慕课和另一教师合开的《程序设计实习》获评国家精品在线开放课程。编著有《新标准C++程序设计》、《Python程序设计基础及实践(慕课版)》、《新标准C++程序设计》、《Python程序设计基础及实践(慕课版)》、《算法基础与在线实践》、《ACM国际大学生程序设计竞赛亚洲区预选赛真题题解》等教材。
目录
第1章绪论1
1.1算法和算法分析1
1.1.1什么是算法1
1.1.2算法的时间复杂度及其表示法3
1.2数据结构7
1.2.1数据的逻辑结构7
1.2.2数据的存储结构7
1.2.3数据结构上的操作8
小结8
习题9
第2章C++语言快速入门和温故知新10
2.1从C到C++10
2.1.1文件名和头文件10
2.1.2输入/输出11
2.1.3结构体名直接作为类型名12
2.1.4引用12
2.1.5const关键字13
2.1.6函数参数默认值14
2.1.7函数参数传引用14
2.1.8函数重载15
2.1.9auto类型15
2.1.10基于范围的for循环16
2.1.11动态内存分配16
2.1.12统一的初始化方式18
2.1.13Lambda表达式18
2.1.14小节习题19
2.2面向对象程序设计19
2.2.1类和对象的概念19
2.2.2this指针21
2.2.3类的静态成员21
2.2.4类成员的可访问范围22
2.2.5构造函数24
2.2.6析构函数25
2.2.7运算符的重载27
2.2.8函数对象28
2.2.9小节习题28
2.3模板与泛型程序设计29
2.3.1函数模板30
2.3.2类模板32
2.3.3标准模板库35
2.3.4小节习题42
第3章线性表44
3.1顺序表44
3.1.1顺序表的概念和操作44
3.1.2C++中的顺序表46
3.2链表47
3.2.1单链表47
3.2.2循环单链表51
3.2.3双链表51
3.2.4静态链表55
3.2.5C++中的链表56
3.3顺序表和链表的选择56
小结57
习题57
第4章枚举与二分法59
4.1枚举59
4.1.1案例: 八皇后问题(P0070)59
4.1.2案例: 奥数问题(P0100)60
4.1.3案例: 特殊密码锁(P0090)62
4.2二分法64
4.2.1案例: 网线主管(P0120)65
★4.2.2案例: 好斗的牛(P0130)66
小结68
习题68
第5章递归和分治70
5.1用递归进行枚举71
5.1.1案例: N皇后问题(P0230)71
5.1.2案例: 奥数问题(P0100)的递归解法73
5.1.3案例: 全排列(P0240)74
5.2解决用递归形式定义的问题76
5.2.1案例: 波兰表达式(P0250)76
★5.2.2案例: 分形盒(P0255)78
5.3用递归进行问题分解79
5.3.1案例: 上台阶(P0260)79
5.3.2案例: 算24(P0270)81
5.3.3案例: 放苹果(P0280)82
5.3.4案例: 7的倍数取法有多少种(P0290)83
5.4分治84
★5.4.1案例: 求排列的逆序数(P0300)84
5.4.2案例: 汉诺塔问题(P0310)86
5.4.3案例: 快速幂87
小结88
习题88
第6章栈和队列90
6.1栈90
6.1.1栈的概念和C++中的栈90
6.1.2案例: 括号配对(P0410)90
6.1.3案例: 后序表达式求值(P0420)92
★6.1.4案例: 中序表达式转后序表达式(P0430)93
★6.1.5案例: 四则运算表达式求值(P0440)96
6.1.6案例: 合法出栈序列(P0450)98
★★6.2栈和递归的关系99
6.3队列102
6.3.1基本实现102
6.3.2循环队列103
6.3.3C++中的队列105
★★6.3.4案例: 滑动窗口(P0460)106
★6.3.5案例: 用两个栈模拟一个队列109
6.4用链表实现栈和队列109
小结110
习题110
第7章二叉树113
7.1二叉树的概念113
7.2二叉树的性质115
7.3二叉树的表示117
7.3.1用类表示二叉树117
7.3.2完全二叉树的表示117
7.4二叉树的遍历118
7.4.1二叉树的前序、后序、中序和按层次遍历118
7.4.2案例: 根据二叉树前中序序列建树(P0570)121
★7.4.3案例: 求二叉树的宽度(P0630)123
★★★7.4.4案例: 根据后序表达式建立表达式树(P0580)124
★★7.4.5案例: 文本缩进二叉树(P0560)126
★7.4.6非递归方式遍历二叉树127
★★7.5线索二叉树129
7.6堆132
7.6.1堆的概念132
7.6.2堆的操作133
7.6.3建堆135
7.6.4堆的实现和优先队列135
7.7哈夫曼树138
7.7.1哈夫曼树的概念和构造138
7.7.2案例: 栅栏修补(P0590)139
7.7.3哈夫曼编码140
小结143
习题143
第8章树、森林和并查集146
8.1树的概念146
8.2树的实现147
8.2.1直观表示法147
8.2.2案例: 括号嵌套树(P0740)148
8.2.3儿子兄弟表示法149
8.2.4案例: 树转儿子兄弟树(P0750)150
8.2.5父结点表示法152
8.3森林152
8.4并查集153
8.4.1并查集的概念和用途153
8.4.2案例: The Suspects疑似病人(P0760)155
小结157
习题157
第9章字符串159
9.1字符串的编码159
9.2字符串的实现160
9.3字符串的匹配算法161
9.3.1匹配算法161
★★9.3.2KMP字符串匹配算法162
小结166
习题166
第10章动态规划168
10.1什么是动态规划168
10.2动态规划解题的一般思路172
10.3案例: 简单背包问题(P0880)174
★★10.4案例: 不太简单的出栈序列统计(P0890)176
★10.5案例: 最长上升子序列(P0900)177
★★10.6案例: 最长公共子序列(P0910)179
小结181
习题181
第11章图的遍历和搜索183
11.1图的定义和术语183
11.2图的表示185
11.2.1邻接矩阵185
11.2.2邻接表186
11.2.3邻接表和邻接矩阵的对比187
11.3图的遍历187
11.3.1深度优先遍历187
11.3.2案例: 深度优先遍历一个无向图(P1020)189
11.3.3案例: 城堡的房间(P1030)192
11.3.4案例: 判断无向图是否连通及是否有回路(P1040)194
11.3.5广度优先遍历196
11.4图的搜索198
11.4.1概述198
11.4.2深度优先搜索200
11.4.3案例: 走迷宫之一(P1050)204
11.4.4案例: 走迷宫之二(P1060)205
11.4.5案例: 走迷宫之三(P1070)205
11.4.6广度优先搜索206
11.4.7案例: 抓住那头牛(P1080)207
11.4.8案例: “走迷宫之三”的广搜解法(P1070)209
★★11.4.9案例: 拯救行动(P1100)210
11.5深搜和广搜的选择213
小结213
习题214
第12章图论基础应用算法217
12.1最短路217
12.1.1单源最短路问题的Dijkstra算法217
12.1.2案例: 简单的糖果分配(P1220)220
★12.1.3求每对顶点之间最短路的Floyd算法223
★12.1.4案例: 奶牛比赛(P1230)224
12.2最小生成树226
12.2.1概述226
12.2.2最小生成树的性质228
12.2.3Prim算法229
12.2.4Kruskal算法231
★12.2.5案例: 团结真的就是力量(P1235)232
★★12.2.6案例: 北极网络(P1240)235
12.3拓扑排序237
12.3.1拓扑排序的定义和算法237
12.3.2案例: 火星人家族树(P1250)238
★12.4关键路径240
12.4.1关键路径的定义和算法240
★★12.4.2案例: 火星大工程(P1260)242
小结245
习题245
第13章排序248
13.1插入排序249
13.1.1直接插入排序249
13.1.2折半插入排序251
13.1.3希尔排序252
13.2选择排序253
13.2.1简单选择排序253
13.2.2堆排序254
13.3归并排序256
13.4交换排序259
13.4.1冒泡排序260
13.4.2快速排序261
13.5分配排序264
13.5.1桶排序264
13.5.2计数排序266
13.5.3基数排序267
★13.6外排序269
13.6.1置换选择排序270
13.6.2多路归并和败者树274
小结278
习题279
第14章查找281
14.1线性表查找281
14.1.1顺序查找281
14.1.2二分查找282
14.1.3分块查找285
14.2树表查找286
14.2.1二叉查找树286
★14.2.2平衡二叉树(AVL树)294
★14.2.3红黑树302
★14.2.4外存查找: B树和B+树308
14.2.5C++中的二叉查找树317
14.3散列表321
14.3.1散列函数设计322
14.3.2散列表的插入和冲突消解324
14.3.3散列表的删除和查找327
14.3.4散列表的效率分析328
14.3.5C++中的散列表329
小结329
习题331
第15章贪心算法335
15.1案例: 圣诞老人的礼物(P1370)335
15.2案例: 电影节(P1380)336
小结338
习题338
第16章数组和矩阵340
16.1数组340
16.2特殊矩阵的压缩存储342
小结344
习题344
附录北京大学在线程序评测平台OpenJudge使用说明346
参考文献347