软考教材分享程序员考试考点分析与真题详解第4版.docx
《软考教材分享程序员考试考点分析与真题详解第4版.docx》由会员分享,可在线阅读,更多相关《软考教材分享程序员考试考点分析与真题详解第4版.docx(62页珍藏版)》请在冰豆网上搜索。
软考教材分享程序员考试考点分析与真题详解第4版
程序员考试考点分析与真题详解(第4版)
第 1 章 数据结构与算法
数据结构是指数据对象及其相互关系和构造方法,一个数据结构S可以用一个二元组表示为S=(D,R)。
其中,D是数据结构中的数据的非空有限集合,R是定义在D上的关系的非空有限集合。
在数据结构中,结点与结点间的相互关系称为数据的逻辑结构,数据在计算机中的存储形式称为数据的存储结构。
数据结构按逻辑结构不同分为线性结构和非线性结构两大类,其中非线性结构又可分为树形结构和图结构,而树形结构又可分为树结构和二叉树结构。
按照考试大纲的要求,在数据结构与算法方面,要求考生掌握以下知识点。
1.常用数据结构
数组(一维数组、二维数组、静态数组、动态数组)、线性表、链表(单向链表、双向链表、环形链表)、队列、栈、树(二叉树、查找树)和图(邻接矩阵、邻接表)等的定义、存储和操作。
2.常用算法
(1)排序算法、查找算法、数值计算算法、字符串处理算法、递归算法、最小生成树、拓扑排序和单源点最短路径求解算法、图的相关算法。
(2)算法与数据结构的关系、算法效率、算法设计、算法描述(流程图、伪代码、决策表)、算法的复杂性。
1.1 算法设计概述
算法是在有限步骤内求解某一问题所使用的一组定义明确的规则。
通俗地说,就是计算机解题的过程。
在这个过程中,无论是形成解题思路还是编写程序,都是在实施某种算法。
前者是推理实现的算法,后者是操作实现的算法。
一个算法应该具有以下5个重要的特征。
(1)有穷性:
一个算法(对任何合法的输入值)必须总是在执行有穷步之后结束,且每一步都可在有穷时间内完成。
(2)确定性:
算法中每一条指令必须有确切的含义,读者理解时不会产生二义性。
在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得出相同的输出。
(3)输入:
一个算法有零个或多个输入,以确定运算对象的初始情况。
所谓零个输入是指算法本身定出了初始条件。
这些输入取自于某个特定对象的集合。
(4)输出:
一个算法有一个或多个输出,以反映对输入数据加工后的结果。
没有输出的算法是毫无意义的。
(5)可行性:
一个算法是可行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的。
算法设计要求正确性、可读性、健壮性、高效率与低存储量需求。
效率指的是算法执行的时间。
对于解决同一问题的多个算法,执行时间短的算法效率高。
存储量需求指算法执行过程中所需要的最大存储空间。
两者都与问题的规模有关。
算法的复杂性是算法效率的度量,是算法运行所需要的计算机资源的量,是评价算法优劣的重要依据。
可以从一个算法的时间复杂度与空间复杂度来评价算法的优劣。
当将一个算法转换成程序并在计算机上执行时,其运行所需要的时间取决于下列因素。
(1)硬件的速度。
例如,使用486还是使用586.
(2)书写程序的语言。
实现语言的级别越高,其执行效率就越低。
(3)编译程序所生成目标代码的质量。
对于代码优化较好的编译程序其所生成的程序质量较高。
(4)问题的规模。
例如,求100以内的素数与求1000以内的素数其执行时间必然是不同的。
显然,在各种因素都不能确定的情况下,很难比较出算法的执行时间。
也就是说,使用执行算法的绝对时间来衡量算法的效率是不合适的。
为此,可以将上述的各种与计算机相关的软、硬件因素都确定下来,这样一个特定算法的运行工作量大小就只依赖于问题的规模(通常用正整数n表示),或者说它是问题规模的函数。
1.时间复杂度
一个程序的时间复杂度是指程序运行从开始到结束所需要的时间。
一个算法是由控制结构和原操作构成的,其执行时间取决于两者的综合效果。
为了便于比较同一问题的不同算法,通常的做法是:
从算法中选取一种对于所研究的问题来说是基本运算的原操作,以该操作重复执行的次数作为算法的时间度量。
在一般情况下,算法中原操作重复执行的次数是规模n的某个函数T(n)。
许多时候要精确地计算T(n)是困难的,我们引入渐近时间复杂度在数量上估计一个算法的执行时间,也能够达到分析算法的目的。
定义(大Ο记号):
如果存在两个正常数c和n0,对于所有的n,当n≥n0时有:
f(n)≤cg(n)
则有:
f(n)=Ο(g(n))
也就是说,随着n的增大,f(n)渐近的不大于g(n)。
例如,一个程序的实际执行时间为T(n)=2n3+n2+5,则T(n)=Ο(n3)。
T(n)和n3的值随n的增大渐近地靠拢。
使用大Ο记号表示的算法的时间复杂度,称为算法的渐近时间复杂度。
通常用Ο
(1)表示常数计算时间。
常见的渐近时间复杂度有:
Ο
(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<Ο(2n)
2.空间复杂度
一个程序的空间复杂度是指程序运行从开始到结束所需的存储量。
程序运行所需的存储空间包括以下两部分。
(1)固定部分:
这部分空间与所处理数据的大小和个数无关。
主要包括程序代码、常量、简单变量和定长成分的结构变量所占的空间。
(2)可变部分:
这部分空间大小与算法在某次执行中处理特定数据的大小和规模有关。
例如,100个数据元素的排序算法与1000个数据元素的排序算法所需的存储空间显然是不同的。
算法由数据结构来体现,所以看一个程序首先要搞懂程序实现中所使用的数据结构,如解决装箱问题就使用链表这种数据结构。
数据结构是算法的基础,数据结构支持算法,如果数据结构是递归的,算法也可以用递归来实现,如二叉树的遍历。
经常采用的算法有迭代法、递推法、递归法、穷举法、贪婪法、分治法和回溯法等,根据考试大纲的要求,对程序员级别的考试,需要考生掌握递归法。
1.2 线性表
线性表是最简单和最常用的一种数据结构,线性表是由相同类型的结点组成的有限序列。
一个由n个结点a0,a1,…,an-1组成的线性表可记为(a0,a1,…,an-1)。
线性表的结点个数为线性表的长度,长度为0的线性表称为空表。
对于非空线性表,a0是线性表的第一个结点,an-1是线性表的最后一个结点。
线性表的结点构成一个序列,对序列中两相邻结点ai和ai+1,称ai是ai+1的前驱结点,ai+1是ai的后继结点。
其中a0没有前驱结点,an-1没有后继结点。
线性表中结点之间的关系可由结点在线性表中的位置所确定,通常用(ai,ai+1)(0≤i≤n–2)表示两个结点之间的先后关系。
例如,如果两个线性表有相同的数据结点,但它们的结点在线性表中出现的顺序不同,则它们是两个不同的线性表。
线性表的结点可由若干个成分组成,其中能唯一标识该结点的成分称为关键字,或简称键。
为了讨论方便,往往只考虑结点的关键字,而忽略其他成分。
1.线性表的基本运算
线性表包含的结点个数可以动态地增加或减少,可以在任何位置上插入或删除结点。
线性表常用的运算可分成几类,每类有若干种运算,如下所示。
1)查找运算
在线性表中查找具有给定键值的结点。
2)插入运算
在线性表的第i(0≤i≤n–1)个结点的前面或后面插入一个新结点。
3)删除运算
删除线性表的第i(0≤i≤n–1)个结点。
4)其他运算
统计线性表中结点的个数;
输出线性表各结点的值;
复制线性表;
线性表分拆;
线性表合并;
线性表排序;
按某种规则整理线性表。
2.线性表的存储
线性表常用的存储方式有顺序存储和链接存储。
1)顺序存储
顺序存储是最简单的存储方式,通常用一个数组,从数组的第一个元素开始,将线性表的结点依次存储在数组中,即线性表的第i个结点存储在数组的第i(0≤i≤n–1)个元素中,用数组元素的顺序存储来体现线性表中结点的先后次序关系。
顺序存储线性表的最大优点就是能随机存取线性表中的任何一个结点。
缺点主要有两个:
一是数组的大小通常是固定的,不利于任意增加或减少线性表的结点个数;二是插入和删除线性表的结点时,要移动数组中的其他元素,操作复杂。
2)链接存储
链接存储是用链表存储线性表(链表),最简单的是用单向链表,即从链表的第一个结点开始,将线性表的结点依次存储在链表的各结点中。
链表的每个结点不但要存储线性表结点的信息,还要用一个域存储其后继结点的指针。
单向链表通过链接指针来体现线性表中结点的先后次序关系。
链表存储线性表的优点是线性表的每个结点的实际存储位置是任意的,这给线性表的插入和删除操作带来方便,只要改变链表有关结点的后继指针就能完成插入或删除的操作,不需移动任何表元。
链表存储方式的缺点主要有两个:
一是每个结点增加了一个后继指针成分,要花费更多的存储空间;二是不方便随机访问线性表的任一结点。
3.线性表上的查找
线性表上的查找运算是指在线性表中找某个键值的结点。
根据线性表中的存储形式和线性表本身的性质差异,有多种查找算法,例如顺序查找、二分法查找、分块查找、散列查找等。
其中二分法查找需要线性表是一个有序序列。
4.在线性表中插入新结点
1)顺序存储
设线性表结点的类型为整型,插入之前有n个结点,把值为x的新结点插在线性表的第i(0≤i≤n)个位置上。
完成插入主要有以下步骤。
检查插入要求的有关参数的合理性;
把原来的第n–1个结点至第i个结点依次往后移一个数组元素的位置;
把新结点放在第i个位置上;
修改线性表的结点个数。
在具有n个结点的线性表上插入新结点,其时间主要花费在移动结点的循环上。
若插入任一位置的概率相等,则在顺序存储线性表中插入一个新结点,平均移动次数为n/2.
2)链接存储
在链接存储线性表中插入一个键值为x的新结点,分以下4种情况。
在某指针p所指结点之后插入;
插在首结点之前,使待插入结点成为新的首结点;
接在线性表的末尾;
在有序链表中插入,使新的线性表仍然有序。
5.删除线性表的结点
1)顺序存储
在有n个结点的线性表中,删除第i(0≤i≤n–1)个结点。
删除时应将第i+1个表元至第n–1个结点依次向前移一个数组元素的位置,共移动n–i–1个结点。
完成删除主要有以下几个步骤。
检查删除要求的有关参数的合理性;
把原来第i+1个表元至第n–1个结点依次向前移一个数组元素的位置;
修改线性表表元个数。
在具有n个结点的线性表上删除结点,其时间主要花费在移动表元的循环上。
若删除任一表元的概率相等,则在顺序存储线性表中删除一个结点,平均移动次数为n/2.
2)链接存储
对于链表上删除指定值结点的删除运算,需考虑几种情况:
一是链表为空链表,不执行删除操作;二是要删除的结点恰为链表的首结点,应将链表头指针改为指向原首结点的后继结点;其他情况,先要在链表中寻找要删除的结点,从链表首结点开始顺序寻找。
若找到,执行删除操作,若直至链表末尾没有指定值的结点,则不执行删除操作。
完成删除由以下几个步骤组成。
如链表为空链表,则不执行删除操作;
若链表的首结点的值为指定值,更改链表的头指针为原首结点的后继结点;
在链表中找指定值的结点;
将找到的结点删除。
1.2.1 栈
栈是一种特殊的线性表,栈只允许在同一端进行插入和删除运算。
允许插入和删除的一端称为栈顶,另一端称为栈底。
称栈的结点插入为进栈,结点删除为出栈。
因为最后进栈的结点必定最先出栈,所以栈具有后进先出的特征。
1.顺序存储
可以用顺序存储线性表来表示栈,为了指明当前执行插入和删除运算的栈顶位置,需要一个地址变量top指出栈顶结点在数组中的下标。
2.链接存储
栈也可以用链表实现,用链表实现的栈称为链接栈。
链表的第一个结点为顶结点,链表的首结点就是栈顶指针top,top为NULL的链表是空栈。
1.2.2 队列
队列也是一种特殊的线性表,只允许在一端进行插入,另一端进行删除运算。
允许删除运算的那一端称为队首,允许插入运算的一端称为队尾。
称队列的结点插入为进队,结点删除为出队。
因最先进入队列的结点将最先出队,所以队列具有先进先出的特征。
1.顺序存储
可以用顺序存储线性表来表示队列,为了指明当前执行出队运算的队首位置,需要一个指针变量head(称为头指针),为了指明当前执行进队运算的队尾位置,也需要一个指针变量tail(称为尾指针)。
若用有N个元素的数组表示队列,随着一系列进队和出队运算,队列的结点移向存放队列数组的尾端,会出现数组的前端空着,而队列空间已用完的情况。
一种可行的解决办法是当发生这样的情况时,把队列中的结点移到数组的前端,修改头指针和尾指针。
另一种更好的解决办法是采用循环队列。
循环队列就是将实现队列的数组a[N]的第一个元素a[0]与最后一个元素a[N–1]连接起来。
一般地,用tail指向队尾元素的下一个位置(注意,并不是指向最后一个元素本身),用head指向队头元素。
队空的初态为head=tail=0.在循环队列中,当tail赶上head时,队列满。
反之,当head赶上tail时,队列变为空。
这样队空和队满的条件都同为head=tail,这会给程序判别队空或队满带来不便。
因此,可采用当队列只剩下一个空闲结点的空间时,就认为队列已满,用这种简单办法来区别队空和队满。
即对空的判别条件是head=tail,队满的判别条件是head=(tail+1)%N.
2.链接存储
队列也可以用链接存储线性表实现,用链表实现的队列称为链接队列。
链表的第一个结点是队列首结点,链表的末尾结点是队列的队尾结点,队尾结点的链接指针值为NULL.队列的头指针head指向链表的首结点,队列的尾指针tail指向链表的尾结点。
当队列的头指针head值为NULL时,队列为空。
1.2.3 数组
在计算机中存储一个矩阵时,可使用二维数组。
例如,M×N阶矩阵可用一个数组a[M][N]来存储(可按照行优先或列优先的顺序)。
如果一个矩阵的元素绝大部分为零,则称为稀疏矩阵。
若直接用一个二维数组表示稀疏矩阵,则会因存储太多的零元素而浪费大量的内存空间。
因此,通常采用三元组数组或十字链表两种方法来存储稀疏矩阵。
1.三元组数组
稀疏矩阵的每个非零元素用一个三元组来表示,即非零元素的行号、列号和它的值。
然后按某种顺序将全部非零元素的三元组存于一个数组中。
如果只对稀疏矩阵的某些单个元素进行处理,则宜用三元组表示。
2.十字链表
在十字链表中,矩阵的非零元素是一个结点,同一行的结点和同一列的结点分别顺序循环链接,每个结点既在它所在行的循环链表中,又在它所在列的循环链表中。
每个结点含5个域,分别为结点对应的矩阵元素的行号、列号、值,以及该结点所在行链表后继结点指针、所在列链表后继结点指针。
为了处理方便,通常对每个行链表和列链表分别设置一个表头结点,并使它们构成带表头结点的循环链表。
为了方便引用某行某列,全部行链表的表头结点和全部列链表的表头结点分别组成数组,这两个数组的首结点指针存于一个十字链表的头结点中,最后由一个指针指向该头结点,如图1-1所示。
图1-1 十字链表
如果对稀疏矩阵某行或某列整体进行某种处理,可能会使原来为零的元素变为非零,而原来为非零的元素可能变成零。
对于这种场合,稀疏矩阵宜用十字链表来表示。
1.2.4 字符串
字符串是由某字符集上的字符所组成的任何有限字符序列。
当一个字符串不包含任何字符时,称它为空字符串。
一个字符串所包含有效字符的个数称为这个字符串的长度。
一个字符串中任一连续的子序列称为该字符串的子串。
字符串通常存于足够大的字符数组中,每个字符串的最后一个有效字符之后有一个字符串结束标志,记为'\0'.通常,由系统提供的库函数形成的字符串末尾会自动添加'\0',但当由用户的应用程序来形成字符串时,必须由程序自行负责在最后一个有效字符之后添加'\0',以形成字符串。
对字符串的操作通常有:
统计字符串中有效字符的个数;
把一个字符串的内容复制到另一个字符串中;
把一个字符串的内容连接到另一个足够大的字符串的末尾;
在一个字符串中查找另一个字符串或字符;
按字典顺序比较两个字符串的大小。
1.3 树
1.3.1 二叉树
1.二叉树的基本概念
二叉树是一个有限的结点集合,该集合或者为空,或者是由一个根结点及其两棵互不相交的左、右叉子树所组成的。
二叉树的结点中有两棵子二叉树,分别称为左子树和右子树。
因二叉树可以为空,所以二叉树中的结点可能没有子结点,也可能只有一个左子结点(或右子结点),也可能同时有左右两个子结点。
如图1-3所示的是二叉树的4种可能形态(如果把空树计算在内,则共有5种形态)。
图1-3 二叉树的4种不同形态
与树相比,二叉树可以为空,空的二叉树没有结点(树至少有一个结点);在二叉树中,结点的子树是有序的,分左、右两棵子二叉树。
二叉树常采用类似树的标准存储结构来存储,其结点类型可以用C语言定义如下:
typedefstructBtnode{
chardata;/*数据*/
strucrtBtnode*lchild;/*左孩子*/
structBtnode*rchild;/*右孩子*/
}BTNODE;
2.二叉树的性质
二叉树具有下列重要性质(此处省略了推导过程,有兴趣的读者可自行推导)。
性质1:
在二叉树的第i层上最多有2i–1个结点(i≥1)。
性质2:
深度为k的二叉树最多有2k–1个结点(k≥1)。
性质3:
对任何一棵二叉树,如果其叶子结点数为n0,度为2的结点数为n2,则n0=n2+1.
一棵深度为k且有2k–1个结点的二叉树称为满二叉树。
如图1-4就是一棵满二叉树,对结点进行了顺序编号。
如果深度为k,有n个结点的二叉树中的结点能够与深度为k的顺序编号的满二叉树从1到n标号的结点相对应,则称这样的二叉树为完全二叉树。
例如图1-5(a)是一棵完全二叉树,而(b)、(c)是两棵非完全二叉树。
显然,满二叉树是完全二叉树的特例。
图1-4 满二叉树的例子 图1-5 完全二叉树、非完全二叉树的例子
根据完全二叉树的定义,显然,在一棵完全二叉树中,所有的叶子结点都出现在第k层或k-1层(最后两层)。
性质4:
具有n个结点的完全二叉树的深度为[log2n]+1(注:
[m]表示不大于m的最大整数,如[4]=4、[4.1]=4、[4.9]=4,下同)。
性质5:
如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到第[log2n]+1层,每层从左到右),则对任一结点i(1≤i≤n),有:
(1)如果i=1,则结点i无双亲,是二叉树的根;如果i>1,则其双亲是结点[i/2]。
(2)如果2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i。
(3)如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1。
3.二叉树的遍历
树的所有遍历方法也同样适用于二叉树,此外,由于二叉树自身的特点,还有中序遍历方法。
(1)前序遍历(先根遍历,先序遍历):
首先访问根结点,然后按前序遍历根结点的左子树,再按前序遍历根结点的右子树。
(2)中序遍历(中根遍历):
首先按中序遍历根结点的左子树,然后访问根结点,再按中序遍历根结点的右子树。
(3)后序遍历(后根遍历,后序遍历):
首先按后序遍历根结点的左子树,然后按后序遍历根结点的右子树,再访问根结点。
例如,图1-6所示的二叉树,其前序遍历、中序遍历和后序遍历结果分别如下。
(1)前序遍历:
1,2,4,5,7,8,3,6。
(2)中序遍历:
4,2,7,8,5,1,3,6。
(3)后序遍历:
4,8,7,5,2,6,3,1。
以上3种遍历方法都是递归定义的,可通过递归函数分别给以实现。
性质6:
一棵二叉树的前序序列和中序序列可以唯一地确定这棵二叉树。
根据性质6,给定一棵二叉树的前序遍历序列和中序遍历序列,我们可以写出该二叉树的后序遍历序列。
例如,某二叉树的前序遍历序列为ABHFDECKG、中序遍历序列为HBDFAEKCG,则构造二叉树的过程如图1-7所示。
图1-7 已知前序遍历序列和中序序列,求二叉树的过程
1.3.2 二叉排序树
二叉排序树又称为二叉查找树,其定义为:
二叉排序树或者是一棵空树,或者是具有如下性质(BST性质)的二叉树。
(1)若它的左子树非空,则左子树上所有结点的值均小于根结点;
(2)若它的右子树非空,则右子树上所有结点的值均大于根结点;
(3)左、右子树本身又各是一棵二叉排序树。
例如,图1-8就是一棵二叉排序树。
根据二叉排序树的定义可知,如果中序遍历二叉排序树,就能得到一个排好序的结点序列。
二叉排序树上有查找、插入和删除3种操作。
下面,我们假设二叉排序树的结点只存储结点的键值,其类型与前面的二叉树的结点类型相同。
1.静态查找
静态查找是在二叉排序树上查找键值为key的结点是否存在,可按以下步骤在二叉排序树ST上找值为key的结点:
(1)如果二叉排序树ST为空二叉树,则查找失败,结束查找;
(2)如果二叉排序树根结点的键值等于key,则查找成功,结束查找;
(3)如果key小于根结点的键值,则沿着根结点的左子树查找,即根结点的左子树作为新的二叉排序树ST继续查找;
(4)如果key大于根结点的键值,则沿着根结点的右子树查找,即将根结点的右子树作为新的二叉排序树ST继续查找。
2.动态查找
在二叉排序树上,为插入和删除操作而使用的查找称为动态查找,动态查找应得到两个指针,一个指向键值为key的结点,另一个指向该结点的父结点。
为此,查找函数可设4个参数:
查找树的根结点指针root、待查找值key、存储键值为key结点的父结点的指针pre,存储键值为key以及结点的指针p.但函数要考虑以下几种不同的情况:
(1)二叉排序树为空,查找失败,函数使*p=NULL,*pre=NULL;
(2)二叉排序树中没有键值为key的结点,函数一直寻找至查找路径的最后一个结点,*pre指向该结点,*p=NULL,如果插入键值为key的结点,就插在该结点下;
(3)查找成功,*p指向键值为key的结点,*per指向它的父结点。
3.插入结点
首先利用动态查找函数确定新结点的插入位置,然后分以下几种情况作为相应的处理:
(1)如果相同键值的结点已在二叉排序中,则不再插入;
(2)如果二叉排序为空树,则以新结点为二叉排序树;
(3)根据要插入结点的键值与插入后的父结点的键值比较,就能确定新结点是父结点的左子结点,还是右子结点,并进行相应的插入。
4.删除结点
删除二叉排序树上键值为key的结点的操作可按以下步骤进行。
(1)调用查找函数确定被删结点的位置;
(2)如果被删除结点不在二叉排序树上,则函数返回。
否则,按以下情况分别处理。
①如果被删除的结点是根结点,又可分以下两种情况。
l被删除结点无左子树,则以被删除结点的右子树作为删除后的二叉排序树;
l被删除结点有左子树,则用被删除结点的左子结点作为根结点,并把被删除结点的右子树作为被删除结点的左子树按中序遍历的最后一个结点的右子树。
②如果被删除结点不是根结