数据结构是指数据对象及其相互关系和构造方法。在软件设计过程中,选用不同的数据结构对系统最终效果的影响极大。因此,该知识点是软件设计师核心考点。
本章我们需要掌握数组、图、广义表、树与二叉树、线性表、排序与查找、算法基础及常见的算法等相关知识。
本章主要梳理笔记常见数据结构的逻辑结构特性及存储的相关内容。
目录
一、数组与线性表
按数据的逻辑结构来划分,常见的数据结构包括:数组(静态数组、动态数组)、线性表(顺
序表、链表、队列、栈)、树(二叉树、查找树、平衡树、线索树、堆)、图。
1. 数组
数组是一种常见的数据结构,根据数组下标的个数,可以把数组分为一维、二维、…、多维数组,如表(数组类型)所示。维度是指下标的个数。一维数组只有一个下标;二维数组则有两个下标,第一个称为行下标,第二个称为列下标。通常根据数组的定义来计算存储地址。
数组类型 | 存储地址计算 |
---|---|
一维数组 a[n] | a[i] 的存储地址为: a+i*len |
二维数组 a[m][n] | a[i][j] 的存储地址(按行存储)为:a+(i*n+j)*len a[i][j] 的存储地址(按列存储)为:a+(j*m+i)*len |
三维数组 a[m][n][o] | a[i][j][k] 的存储地址为:a+(i*n+j*o+k)*len |
注:表中的计算公式的a为数组首地址,len为每个数据对象的长度,i 与j 的下标默认从0开始。
2. 稀疏矩阵
在计算机中存储一个矩阵时,可使用二维数组。例如,M×N阶矩阵可用一个数组a[ M] [ N] 来存储(可按照行优先或列优先的顺序)。如果一个矩阵的元素绝大部分为零,则称为稀疏矩阵。若直接用一个二维数组表示稀疏矩阵,则会因存储太多的零元素而浪费大量的内存空间。在稀疏矩阵中,有一种情况非常常见,即稀疏矩阵内部存在对称性。这样,我们可以采用一维数组来表示它们,这也常称为压缩存储。
考试的时候选择代入法:把数组下标带入公式进行计算,看是否满足结果。
由于二维数组的行和列是相等的,求存储在某个元素之前的元素个数,如果在这个元素的行等于列的情况下,那么存储元素的个数是与存储方式无关的。
3. 线性表
线性表是用来表示数据对象之间的线性结构,通俗地说,线性结构是指所有结点按“一个接着一个排列”的方式相互关联而组成一个整体“。
线性结构是n个结点的有穷序列。通常表示为(a1,a2,…,an),a1称为起始结点,an称为结束结点,i 称为ai 在线性表中的序号或位置,线性表所含结点的个数称为线性表的长度,长度为0的线性表称为空表。
线性表主要的存储结构有两种:顺序存储结构和链式存储结构。采用顺序存储结构,就称为顺序表(常用数组实现);采用链式存储结构则称为线性链表(即链表)。
(1)顺序表
顺序存储是最简单的存储方式,通常用一个数组,从数组的第一个元素开始,将线性表的结点依次存储在数组中,即线性表的第i 个结点存储在数组的第i (0≤i ≤n–1)个元素中,用数组元素的顺序存储来体现线性表中结点的先后次序关系。
顺序存储线性表的最大优点就是能随机存取线性表中的任何一个结点,缺点主要有两个,一是数组的大小通常是固定的,不利于任意增加或减少线性表的结点个数;二是插入和删除线性表的结点时,要移动数组中的其他元素,操作复杂。
(2)链表
链表就是采用链式存储实现的线性表。它是动态分配链表结点,通过链接指针,将各个节点按逻辑顺序连接起来。根据其存储结构的不同,可以分为单链表、循环链表和双链表三种,软件设计师考试中目前主要考查前两种。
单链表
单链表又分为:存在头结点和不存在头结点
循环链表
循环链表与单链表的区别仅仅在于其尾结点的指针域值不是nul l ,而是指向头结点的指针。这样做的好处是,从表中的任一结点出发都能够通过后移操作扫描整个循环链表。
(3)顺序表与链表的比较
在实际应用中,应该如何在顺序实现和链式实现中进行选择呢?通常是从时间和空间性能的角度来进行判断
性能类别 | 具体项目 | 顺序存储 | 链式存储 |
---|---|---|---|
空间性能 | 存储密度 | =1 ,更优 | < 1 |
容量分配 | 事先确定 | 动态改变,更优 | |
时间性能 | 定位运算 | O(n) | O(n) |
读运算 | O(1),更优 | O([n-1]/2),最好情况为 n | |
插入运算 | O(n/2),最好情况为0,最坏情况为n | O(1),更优 | |
删除运算 | O([n-1]/2) | O(1),更优 |
采用单向循环链表存储的特点之一是从表中任意结点出发都能遍历整个链表,另外便于元素的元素节点的删除与插入。如需要对表中的任意节点进行随机访问需采用顺序存储结构。
(4)队列
队列也是一种特殊的线性表,只允许在一端进行插入,另一端进行删除运算。允许删除运算的那一端称为队首,允许插入运算的一端称为队尾。称队列的结点插入为进队,结点删除为出队。因最先进入队列的结点将最先出队,所以队列具有先进先出的特征。
实现队列,可以使用顺序存储(如:数组方式)也可以用链表。
顺序存储用顺序存储线性表来表示队列,为了指明当前执行出队运算的队首位置,需要一个指针变量head(称为头指针),为了指明当前执行进队运算的队尾位置,也需要一个指针变量tai l (称为尾指针)。
若用有N个元素的数组表示队列,随着一系列进队和出队运算,队列的结点移向存放队列的数组的尾端,会出现数组的前端空着,而队列空间已用完的情况。一种可行的解决办法是当发生这样的情况时,把队列中的结点移到数组的前端,修改头指针和尾指针。另一种更好的解决办法是采用循环队列。
循环队列就是将实现队列的数组a[ N] 的第一个元素a[ 0] 与最后一个元素a[ N–1] 连接起来。队空的初态为 head=tai l =0。在循环队列中,当tail赶上head时,队列满。反之,当head赶上tail 时,队列变为空。这样队空和队满的条件都同为head=tail ,这会给程序判别队空或队满带来不便。因此,可采用当队列只剩下一个空闲结点的空间时,就认为队列已满的简单办法,以区别队空和队满。即队空的判别条件是head=tail ,队满的判别条件是head=tail +1。
链式存储
队列也可以用链接存储线性表实现,用链表实现的队列称为链接队列。链表的第一个结点是队列首结点,链表的末尾结点是队列的队尾结点,队尾结点的链接指针值为NULL。队列的头指针head 指向链表的首结点,队列的尾指针t ai l 指向链表的尾结点。当队列的头指针head值为NULL时,队列为空。
(5)栈
栈是另一种特殊的线性表,栈只允许在同一端进行插入和删除运算。允许插入和删除的一端称为栈顶,另一端为栈底。称栈的结点插入为进栈,结点删除为出栈。因为最后进栈的结点必定最先出栈,所以栈具有后进先出的特征。
顺序存储
采用顺序实现的栈中,初始化运算负责将栈顶变量top初始化为“1”,使栈为空;在进栈操作时,需要判断栈是否满(top=Null说明栈满),如果未满,则将新元素插入栈,并将top的值加1;在出栈操作时,需要判断栈是否空(top=1说明栈空),如果非空,则取出栈顶元素,将top的值减1。顺序栈的缺点在于为了避免栈满时发生溢出,需要预先为栈设立足够大的空间,但太大会造成空间浪费,不过太小又容易引发溢出。
链式存储
栈可以用链表实现,用链表实现的栈称为链接栈。链表的第一个结点为顶结点,链表的首结点是栈顶指针top,top为NULL 的链表是空栈。
队列具有先进先出的特点,而栈具有后进先出的特点。因此我们可以知道入队序列与出队序列一定相同,但入栈序列与出栈序列不一定相同。比如a,b,c这样一个序列,那么按照a,b,c 的顺序入队列,那么其出队列的次序一定是a,b,c。而按照a,b,c的顺序入栈,那么可能是a入栈后就出栈,然后b入栈又出栈,然后C入栈出栈。也可能是等a,b,c都入栈后再出栈,那么出栈序列就是c,b,a。
(6)字符串
字符串是由某字符集上的字符所组成的任何有限字符序列。当一个字符串不包含任何字符时,称它为空字符串。一个字符串所包含的有效字符个数称为这个字符串的长度。一个字符串中任一连续的子序列称为该字符串的子串。
字符串通常存于足够大的字符数组中,每个字符串的最后一个有效字符之后有一个字符串结束标志,记为“\ 0”。通常由系统提供的库函数形成的字符串的末尾会自动添加“\ 0”,当由用户的应用程序来形成字符串时,必须由程序自行负责在最后一个有效字符之后添加“\ 0”,以形成字符串。
对字符串的操作通常有:
● 统计字符串中有效字符的个数;
● 把一个字符串的内容复制到另一个字符串中;
● 把一个字符串的内容连接到另一个足够大的字符串的末尾;
● 在一个字符串中查找另一个字符串或字符;
● 按字典顺序比较两个字符串的大小。
二、树
1.数据逻辑结构
我们通常把数据的逻辑结构分为:线性结构和非线性结构
线性结构:第一个是线性结构,常用的线性结构有:线性表,栈,队列,双队列,数组,串。
非线性结构:第一个是树、第三个是图。
2. 树的概念
树状图是一种数据结构,它是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
结点:1、2、3、4都称为结点
结点的度:一个结点所拥有的孩子结点数(比如结点1有孩子结点2、3,因此结点1的度为2)
树的度:一棵树中所有结点最高的度为树的度,这棵树的度为2
叶子结点:一个结点若没有孩子结点则称为叶子结点(比如4、5、7、8都为叶子结点)
分支结点:一个结点有相应的分支则称为分支结点(比如结点2、3、6)
内部结点:不是叶子结点、也不是根节点则可称为内部结点(比如结点2、3、6)
兄弟结点:属于同一个父节点的平级关系(比如结点4、5)
堂兄弟结点:如果二叉树的两个节点深度相同,但父节点不同,则它们是一对堂兄弟节点。
树是一种典型的非线性数据结构,它能够很好地应用于描述分支和层次特性的数据集合。树是由一个或多个结点组成的有限集合T,它满足以下两个条件:
(1)有一个特定的结点,称为根结点;
(2)其余的结点分成m(m≥0)个互不相交的有限集合。其中每个集合又都是一棵树,称
T 1, T 2, …, T m–1为根结点的子树。
显然,以上定义是递归的,即一棵树由子树构成,子树又由更小的子树构成。由条件(1)可知,一棵树至少有一个结点(根结点)。一个结点的子树数目称为该结点的度(次数),树中各结点的度的最大值称为树的度(树的次数)。度为0的结点称为叶子结点(树叶),除叶子结点外的所有结点称为分支结点,根以外的分支结点称为内部结点。例如,在如图所示的树中,根结点的度数为3,结点2的度数为4,结点4的度数为1,结点9的度数为2,其他结点的度数为0,该树的度数4。
在用图形表示的树中,对两个用线段连接的相关联的结点而言,称位于上端的结点是位于下端的结点的父结点或双亲结点,称位于下端的结点是位于上端的结点的(孩)子结点,称同一父结点的多个子结点为兄弟结点,称处于同一层次上、不同父结点的子结点为堂兄弟结点。例如在示例-树图中,结点1是结点2, 3, 4的父结点。反之,结点2, 3, 4都是结点1的子结点。结点2, 3, 4是兄弟结点,而结点5, 6, 7, 8, 9是堂兄弟结点。
定义一棵树的根结点所在的层次为1,其他结点所在的层次等于它的父结点所在的层次加1。树中各结点的层次的最大值称为树的层次。
3. 树的遍历
另外一个重点则是树的遍历问题,也就是根据某种顺序逐个获得树中全部结点的信息,常见的遍历方法有三种:
前序遍历:“根左右”,即先访问根结点,然后再从左到右按前序遍历各棵子树。以图示例-树为例,
前序遍历结果为:1,2,5,6,7,8,3,4,9,a,b。
后序遍历:“左右根”,即从左到右遍历根结点的各棵子树,最后访问根结点。以图示例-树为例,
后序遍历结果为:5,6,7,8,2,3,a,b,9,4,1。
层次遍历(即从上到下,从左到右依次访问):首先访问处于0层的根结点,然后从左到右访问1层上的结点,以此类推,层层向下访问。以图示例-树为例,层次遍历结果为:1,2,3,4,5,6,7,8,9,a,b
4. 二叉树的概念
二叉树是一个有限的结点集合,该集合或者为空,或者由一个根结点及其两棵互不相交的左、右二叉子树所组成。
二叉树的结点中有两棵子二叉树,分别称为左子树和右子树。因为二叉树可以为空,所以二叉树中的结点可能没有子结点,也可能只有一个左子结点(右子结点),也可能同时有左右两个子结点。如图所示是二叉树的4种可能形态(如果把空树计算在内,则共有5种形态)。
在二叉树中,有两种表现极为特殊,即满二叉树和完全二叉树,如下图所示
满二叉树:一棵深度为k且有2^k-1(k≥1)个结点的二叉树就称为满二叉树。
完全二叉树:如果深度为k、有n个结点的二叉树中各结点能够与深度k的顺序编号的满二叉树从1到n标号的结点相对应即为完全二叉树。
二叉树具有以下几个重要性质,经常成为出题的依据。
在二叉树的第i 层上最多有2^(i-1)个结点(i ≥1);
(满二叉树为例:第三层的结点=2^(3-1)=4)
深度为k的二叉树最多有2^k-1个结点(k≥1);
(满二叉树为例:一棵树最多的结点数=2^3-1=7)
对任何一棵二叉树,如果其叶子结点数为n₀,度为2的结点数为n₂,则n₀= n₂+1;
(完全二叉树为例:叶子结点的数量=度为2的结点数+1=2+1=3)
具有n个结点的完全二叉树的深度为 ⌊log₂n v⌋ ,( ⌊m⌋ 运算是表示向下取整,比如⌊1.5⌋=1);
如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到 ⌊log₂n⌋+1层,每层从左到右),则对任一结点i (1≤i ≤n),有:
如果i =1,则结点i 无父结点,是二叉树的根;如果i >1,则父结点是 ;
(完全二叉树为例:结点3的父节点=⌊3/2⌋=⌊1.5⌋=1)
如果2i >n,则结点i 为叶子结点,无左子结点;否则,其左子结点是结点2i ;
(n表示一棵树的结点数,满二叉树结点4为例:2*4>7无左子结点;满二叉树结点3为例:2*3<7,左子结点为6)
如果2i +1>n,则结点i 无右子结点,否则,其右子结点是结点2i +1;
(n表示一棵树的结点数,满二叉树结点4为例:2*4+1>7无右子结点;满二叉树结点2为例:2*2+1<7,右子结点为5)
在完全二叉树中,任意一个结点的左、右子树的高度之差的绝对值不超过1。
在软件设计师考试中,对这几个特性的灵活应用是十分关键的,因此应熟练的掌握它们。
5. 二叉树的遍历
树的遍历方法也同样适用于二叉树(如图 示例-二叉树遍历 所示),不过由于二叉树的自身特点,还有一种中序遍历法。
前序遍历(根左右,先访问根结点,然后分别用前序分别遍历左、右子树,也称为前根遍历)。图 示例-二叉树遍历 的
前序遍历结果是:1,2,4,5,7,8,3,6。
中序遍历(左根右,先按中序遍历左子树,再访问根结点,然后再按中序遍历右子树,也称为中根遍历)。图 示例-二叉树遍历 的
中序遍历的结果是:4,2,7,8,5,1,3,6。
后序遍历(左右根,分别按后序遍历要左、右子树,然后再访问根结点,也称为后根遍历)。图 示例-二叉树遍历 的
后序遍历的结果是:4,8,7,5,2,6,3,1。
层次遍历(即从上到下,从左到右依次访问)(首先访问处于0层的根结点,然后从左到右访问1层上的结点,以此类推,层层向下访问)。图 示例-二叉树遍历 的
层次遍历的结果是:1,2,3,4,5,6,7,8。
6.反向构造二叉树
前序遍历:根左右 ABHFDECG中推出A为根
中序遍历:左根右 HBEDFAGC 中已知A为根,则HBEDF为根的左子结点,GC为根的右子结点
前序遍历:根左右 ABHFDECG中推出B为HEDF的父结点,C为G的父结点
中序遍历:左根右 HBEDFAGC 中已知B为HEDF的父结点,则H为B的左子结点,EDF为B的右子结点
前序遍历:根左右 ABHFDECG中推出F为ED的父节点
中序遍历:左根右 HBEDFAGC 中已知F为ED的父节点,C为G的父结点,则G为C的左子结点
前序遍历:根左右 ABHFDECG中推出D为E的父节点
中序遍历:左根右 HBEDFAGC 中已知D为E的父节点,则E为D的左子结点
7.树转二叉树
连线法:把各个结点的兄弟结点连接起来,然后只保留第一个孩子结点
根据上面的定义和描述,我们可以发现遍历是递归定义的,最适合使用递归函数来实现。在学习遍历时,最重要的是结合其概念来灵活应用。
8. 二叉查找树
二叉查找树,它或者是一棵空树;或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉查找树;
二叉排序树、左孩子小于根、右孩子大于根
当对这样的二叉树进行中序遍历,就可以得到一个排好序的结点序列,因此,二叉查找树也称为二叉排序树。二叉查找树主要考查其遍历。
插入结点:
1)若该键值结点已存在,则不再插入,如:48;
2)若查找二叉树为空树,则以新结点为查找二叉树;
3)将要插入结点键值与插入后父结点键值比较,就能确定新结点是父结点的左子结点,还是右子结点。
删除结点:
1)若待删除结点是叶子结点,则直接删除;
2)若待删除结点只有一个子结点,则将这个子结点与待删除结点的父结点直接连接,如:56;
3)若待删除的结点p有两个子结点,则在其左子树上,用中序遍历寻找关键值最大的结点s,用结点s的值代替结点p的值,然后删除结点s,结点s比属于上述1)、2)情况之一,如89。
9. 平衡二叉树
平衡二叉树又被称为AVL 树,它具有以下性质:它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。满二叉树就是一种平衡二叉树。
10. 线索二叉树
二叉树在通常情况下是无法直接找到某结点在某种遍历序列中的前驱和后继结点的。而线索二叉树则通过利用二叉树上空的指针域来存放这些“线索”信息,通常其采用以下做法:
若前驱结点不为空,而且其右指针域为空,则将根结点的地址赋给前驱结点的右指针域,并将前驱结点的右线索标志置1;
若根结点的左指针域为空,则把前驱结点的地址赋给根结点的左指针域,同时将根结点的左线索标志置1;
将根结点地址赋给保存前驱结点指针的变量,以便当访问下一个结点时,此根结点成为前驱结点。
11. 哈夫曼树(最优二叉树)
它是一种工具,用于哈夫曼编码,压缩编码方式,属于无损压缩
在理解哈夫曼树之前,必须了解一些最基本的概念。
树的路径长度:是从树根到树中每一结点的路径长度之和,在结点数目相同的二叉树中,完全二叉树的路径长度最短;
权:在一些应用中会赋予树中结点一个有意义的实数,这个数字称为权,某个叶子结点的数值代表某一种字符出现的频度;
带权路径长度:结点到树根之间的路径长度与该结点上权的乘积,称为结点的带权路径长度;
树的带树路径长度(树的代价):所有叶结点的带树路径长度之和。
12.总结
特殊二叉树的性质
- 若二叉树中最多只有最下面两层的结点度数可以小于2,并且最下面一层的叶子结点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树,因此在完全二叉树中,任意一个结点的左、右子树的高度之差的绝对值不超过1。
- 二叉排序树的递归定义如下:二叉排序树或者是一棵空树;或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于根结点的值;
(3)左右子树也都是二叉排序树。
- 在n个结点的二叉树链式存储中存在n+1个空指针,造成了巨大的空间浪费,为了充分利用存储资源,可以将这些空链域存放指向结点在遍历过程中的直接前驱或直接后继的指针,这种空链域就称为线索,含有线索的二叉树就是线索二叉树。
- 最优二叉树即哈夫曼树。
- 哈夫曼树中权值最小的两个结点互为兄弟结点。
二、图
(图的考查频率较低)
图是一种比树更复杂的非线性结构,它是由顶点集合V和边集合E组成的,通常记做G=( V,E) 。它可以用来模拟现实世界中许多图状结构的事物与问题。
1. 图的相关概念
有向图:若一个图中的每条边都是有方向的,则称为有向图。在有向图中,<Vi ,Vj >表示一条有向边,Vi 是始点(起点),Vj 是终点。<Vi ,Vj >和<Vj ,Vi >表示的是两条不同的边。有向边也称为弧,边的始点称为弧头,终点称为弧尾。
无向图:若一个图中的每条边都是无方向的,则称为无向图。无向图的边是顶点的无序对,通常使用(Vi ,Vj )来表示一条边,无向图的边没有起点和终点,(Vi ,Vj )和(Vj ,Vi )表示的是同一条边。
无向完全图:如果限定任何一条边的两个顶点都不相同,则有n个顶点的无向图至多有n( n-1) / 2条边,这样的无向图称为无向完全图。
有向完全图:恰好有n( n-1) 条边的有向图称为有向完全图。
连通图:如果图中两个顶点间存在路径,则称它们是连通的;而如果图中任意两个顶点间都是连通的,则称该图为连通图。
2. 图的存储结构
图有两种主要的存储结构,它们是邻接矩阵表示法和邻接表表示法,如表 图的存储结构 所示。
1)邻接矩阵
用一个n阶方阵R来存放图中各结点的关联信息,其矩阵元素Rij={1 若顶点i到顶点j有邻接边;0 若顶点i到顶点j无邻接边}
R1具有对称性,节省存储空间,可以只存上三角或下三角
2)邻接表
首先把每个顶点的邻接顶点用链表示出来,然后用一个一维数组来顺序存储上面每个链表的头指针。
如果一个图有E条边,那么其邻接矩阵中的非零元素数目应该为2E。
在无向图中,一条边连接两个顶点,即如果存在一条边,那么与这条边相关的两个顶点的度都为加1,那么总的度就应该加2,因此,如果图中有n条边,那么所有顶点的度数之和就应该为2e。
从存储空间的利用率角度来看,完全图适合采用邻接矩阵存储。
3. 图的遍历
图的遍历也是从某个顶点出发,沿着某条搜索路径对图中每个顶点各做一次且仅做一次访问,
常用的遍历算法包括以下深度优先和广度优先两种,如表 图的深度优先和广度优先 所示
4. 拓扑排序
我们把用有向边表示活动之间开始的先后关系。这种有向图称为用顶点表示活动网络,简称AOV网络。
用一个序列表示图中哪些事件可以先执行,哪些事件可以后执行,用不同的顺序来完成任务
上图的拓扑序列有:02143567、02143657、01243657、01243567
5. 最小生成树
如果连通图G的一个子图是一棵包含G所有顶点的树,则该子图称为G的生成树。生成树是含有该连通图全部顶点的一个极小连通子图,它并不是唯一的,从不同的顶点出发可以得到不同的子树。含有n个顶点的连通图的生成树有n个顶点和n-1条边。
要求一个连通图的生成树很简单,只需从任何一个顶点出发,作一次深度优先或广度优先的搜索,将所经过的n个顶点和n1条边连接起来,就形成了极小连通子图,也就是一棵生成树。
对一个带权的图,在一棵生成树中,各条边的权植之和为这棵生成树的代价,其中代价最小的生成树称为最小生成树。普里姆算法(Prim算法)和克鲁斯卡尔算法(Kruskal 算法)是求连通的带权无向图的最小代价树的常用算法。
注:带权的图是指每条边带上权值的图,常用于表示通路的代价
树不具备环路,当树构成环路时我们称之为图。
一个图的最小生成树的边数=图的顶点数-1,选择树的边时选路径最短的边