第 1 章 数据结构绪论
-
程序 = 数据结构 + 算法
-
数据结构:是相互之间存在的一种或多种特定关系的数据元素的集合。
-
为编写出一个“好”的程序,必须分析待处理对象的特性及各处理对象之间存在的关系。这也就是研究数据结构的意义所在。
-
按照视点的不同,可以把数据结构分为 逻辑结构 和 物理结构:
☛ 逻辑结构:是指数据对象中数据元素之间的相互关系。其可具体分为以下四种关系:
1. 集合结构:数据元素除了同属于一个集合外,它们之间没有其他关系。如下图所示:
2. 线性结构:数据元素之间是一对一关系。如下图所示:
3. 树形结构:数据元素之间呈现一对多关系。如下图所示:
4. 图形结构:数据元素是多对多关系。如下图所示:
图形结构 ☛ 物理结构:是指数据的逻辑结构在计算机中的存储形式,因此也称为 存储结构。
数据是数据元素的集合,那么根据物理结构的定义,实际上就是如何把数据元素存储到计算机的存储器中。存储器主要是针对内存而言的,像硬盘,软盘,光盘等外部存储器的数据组织通常用文件结构来描述。
数据的存储结构应正确反映数据元素之间的逻辑关系。
数据元素的存储结构形式有两种:顺序存储 和 链式存储:
1. 顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。如下图所示:
2. 链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。如下图所示:
链式存储结构- 逻辑结构是面向问题的,而物理结构是面向计算机的,其基本的目标就是将数据及其逻辑关系存储到计算机的内存中。
第 2 章:算法
-
算法:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。 简而言之,算法是描述解决问题的方法。
-
算法的特性:输入、输出、有穷性、确定性和可行性。
-
好的算法,应该具有正确性,可读性,健壮性,高效率和低存储量的特征。
-
算法时间效率度量:
☛ 可以忽略加法常数:
☛ 与最高次项相乘的常数可忽略:
☛ 最高次项的指数大的,函数随着 n 的增长,结果也会变得增长得更快:
☛ 判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数: -
时间复杂度:大 O 阶推导方法:
1.用常数 1 取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是 1,则去除与这个项相乘的常数(即 ),得到的结果就是大 O 阶。
举个例子,如下代码,请用大 O 推导式求出函数的时间复杂度:
int i,j;
for ( i = 0; i < n; ++i){
for(j = i; j < n; ++j){
/*时间复杂度为 O(1) 的程序步骤序列 */
}
}
分析:对于外循环,其时间复杂度为 O(n);对于内循环环,当 i=0 时,内循环执行了 n 次,当 i=1 时,执行了 n-1 次,······当 i=n-1 时,执行了 1 次。因此内循环总的执行次数为:
根据大 O 阶推导方法,最终上述代码的时间复杂度为 。
常见的时间复杂度及其耗时排序-
空间复杂度:算法的空间复杂度通过计算算法所需的存储空间实现。
一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令,常数,变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样就只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为
第 3 章:线性表
-
线性表(List):零个或多个数据元素的有限序列。
-
线性表的顺序存储结构:指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表()的顺序存储示意图如下所示:
- 存储器中的每个存储单元都有自己的编号,这个编号称为地址。每个数据元素,不管它是整型,实型还是字符型,它都是需要占用一定的存储单元空间的,假设占用的是 c 个存储单元,那么对于线性表的第 i 个数据元素 的存储位置都可以由 推导算出:
上述推导公式具体内容如下图所示:
通过该公式,就可以随时算出线性表中任意位置的地址,不管是第一个还是最后一个,都是相同的时间。也即对于线性表每个位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数时间,因此,线性表的存取操作时间性能为 。我们通常将存取操作具备常数性能()的存储结构称为 随机存储结构。
-
线性表的顺序存储结构,对于存取操作,其时间复杂度为 (因为元素位置可以直接计算得到);对于插入和删除操作,其时间复杂度为(因为插入或删除后,需要移动其余元素)。因此,线性表顺序存储结构比较适用于元素存取操作较多,增删操作较少的场景。
-
链表:一个或多个 结点 组合而成的数据结构称为 链表。其中,结点 一般由两部分内容构成:
☛ 数据域:存储真实数据元素。
☛ 指针域:存储下一个结点的地址(指针)。
- 一般把链表中的第一个结点称为 头指针,其存储链表的第一个数据元素。有时,为了能更加方便地对链表进行操作,会在单链表的第一个结点(即头指针)前附设一个结点,称为 头结点。头结点的数据域可以不存储任何信息,其指针域存储指向第一个结点的指针(即指向头指针)。如下图所示:
-
在线性表的顺序存储结构(即数组)中,其任意一个元素的存储位置可以通过计算得到,因此其数据读取的时间复杂度为 ;而对于单链表结构,假设需要获取第 i 个元素,则必须从第一个结点开始依次进行遍历,直到达到第 i 个结点。因此,对于单链表结构而言,其数据元素读取的时间复杂度为 。
-
在线性表的顺序存储结构(即数组)中,对任意一个元素的增删操作,其时间复杂度为 (因为存在移动操作,对尾元素进行增删操作其时间复杂度为 );而对单链表结构来说,对其任意一个位置进行增删操作,其时间复杂度为 (因为需要先进行遍历找到目标元素,对头指针的增删操作其时间复杂度为 )。因此,如果只对一个元素进行增删操作,两种结构并不存在优劣之分,但如果针对多个数据进行增删,由于线性表每一次增删都需要移动 n-i 个元素,即每个元素的操作都为 ;而单链表只在第一次遍历定位目标元素时为 ,对后续元素的增删只需简单地赋值移动指针即可,其时间复杂度为 。因此,对于插入或删除数据越频繁的操作,单链表的效率就越明显。
-
将单链表中的终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称 循环链表(circular linked list)。
-
为了使空链表与非空链表处理一致,我们通常设一个头结点(循环链表不一定需要头结点)。其实循环链表和单链表的主要差异就在于循环的判断条件上,单链表判断结束的条件为尾结点是否指向空:
p->next = NULL
,而循环链表判定的条件为当前结点是否指向头结点:p->next = head
,是则当前结点为尾结点。 -
双向链表(double linked list):在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
第 4 章:栈与队列
-
栈(stack):是限定仅在表尾(栈顶)进行插入和删除操作的线性表。
-
我们把允许插入和删除的一端称为 栈顶(top),另一端称为 栈底(bottom),不含任何任何数据元素的栈称为 空栈。栈又称为 后进先出(Last In First Out) 的线性表,简称 LIFO 结构。
-
栈 是线性表的特例,其具备先进后出 FILO 特性。可以使用线性表的顺序存储结构(即数组)实现栈,将之称之为 顺序栈;可以使用单链表结构实现栈,将之称之为 链栈。两者示意图如下所示:
-
顺序栈和链栈的时间复杂度均为。对于空间性能,顺序栈需要事先确定一个固定的长度(数组长度),可能存在内存空间浪费问题,但它的优势是存取时定位很方便;而链栈则要求每个元素都要配套一个指向下个结点的指针域,增大了内存开销,但好处是栈的长度无限。因此,如果栈的使用过程中元素变化不可预料,有时很小,有时很大,那么最好使用链栈;反之,如果它的变化在可控范围内,则建议使用顺序栈。
-
栈的内部实现原理其实就是数组或链表的操作,而之所以引入 栈 这个概念,是为了将程序设计问题模型化,用高层的模块指导特定行为(栈的先进后出特性),划分了不同关注层次,使得思考范围缩小,更加聚焦于我们致力解决的问题核心,简化了程序设计的问题。
-
栈 有一个很重要的应用:递归。每个递归定义必须至少有一个条件,使得当满足条件时,递归不再进行。
-
递归 的一个经典例子为:斐波那契数列(Fibonacci),指的是这样一个数列:1、1、2、3、5、8、13、21、……,即当前位置的值为前面两项之和。用数学表达式表达如下:
如果我们直接将按上面的公式用代码进行翻译,如下所示:
int fbi(const int n){
int i;
int ret;
int last1 = 0;
int last2 = 1;
if (n == 0){
ret = 0;
}else if (n == 1){
ret = 1;
}else{
for(i = 2; i <= n; ++i){
ret = last1 + last2;
last1 = last2;
last2 = ret;
}
}
return ret;
}
但是如果我们使用递归方式实现,则会更加简洁:
int fbi(const int n){
if (n < 2){
return n == 0 ? 0 : 1;
}
return fbi(n-1) + fbi(n-2);
}
使用递归,简洁之余,还更加契合其数学公式的定义。
-
栈 的另一个常见的应用为:数学表达式的求值。其原理是通过将 中缀表达式(即标准的四则运算表达式) 以特定操作进行进栈出栈操作,得到一个对应的 后缀表达式(也称为逆波兰(Reverse Polish Notation,PRN)表示);然后将该后缀表达式再次通过特定操作进行出栈入栈操作,即可得到运算结果。
-
队列(queue):是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
-
队列 是一种 先进先出(First In First Out) 的线性表。允许插入的一端称为队尾,允许删除的一端称为对头。
-
线性表有顺序存储和链式存储,栈是线性表,所以有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。
第 5 章:串
-
串(string) 是由零个或多个字符组成的有限序列,又名叫 字符串。
-
串 的逻辑结构和线性表很相似,不同之处在于串针对的是字符集,也就是串中的元素都是字符。因此,对于串的基本操作与线性表是有很大差别的。线性表更关注的是单个元素的操作,比如查找一个元素,插入或删除一个元素,但串中更多的是查找子串位置,得到指定位置子串,替换子串等操作。
-
串 的存储结构与线性表相同,分为两种:
☛ 串的顺序存储结构:串的顺序存储结构是用 一组地址连续的存储单元 来存储串中的字符序列。一般是用定长数组来定义。
由于是定长数组,因此就会存在一个预定义的最大串长度,一般可以将实际的串长度值保存在数组 0 下标位置,也可以放在数组最后一个下标位置,也有些语言使用在串值后面加一个不计入串长度的结束标记符(比如\0
)来表示串值得终结,这样就无需使用数字进行记录。
☛ 串的链式存储结构:对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性(结构中的每个元素数据都是一个字符),如果也简单地将每个链结点存储一个字符,就会存在很大的空间浪费。因此,一个结点可以考虑存放多个字符,如果最后一个结点未被占满时,可以使用 "#" 或其他非串值字符补全,如下图所示:
- 串的链式存储结构除了在链接串与串操作时有一定的方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。
第 6 章:树
-
树(Tree): 树是 个结点的有限集。当 时称为空树。
-
在任意一棵非空树中:
☛ 有且仅有一个特定的结点:根结点(Root);
☛ 当 时,其余结点可分为 个互不相交的有限集 ,其中每一个集合本身又是一棵树,并且称为根的 子树(SubTree)。其图示如下所示:
- 对于 树 的定义需要强调两点:
☛ 结点大于 0 ()时根结点是唯一的,不可能同时存在多个根结点。
☛ 子结点大于 0 ()时,子树的个数没有限制,但它们一定是互不相交的。下图所示的结构就不符合树的定义,因为它们都有相交的子树:
-
树 其实也是一种递归的实现,即树的定义之中还用到了树的概念。
-
对比线性表与树的结构,它们有很大的不同:
-
单独使用顺序存储结构(即数组)无法很好地实现树的存储概念,不过如果充分利用顺序存储和链式存储结构的特点,则完全可以实现对数的存储结构的表示。
-
二叉树(Binary Tree):是 个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的,分别称为根结点的左子树和右子树的二叉树组成。如下图所示:
-
二叉树的特点:
☛ 每个结点最多只能有两棵子树。
☛ 左子树和右子树是有顺序的,次序不能任意颠倒。
☛ 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。 -
二叉树具有以下五种基本形态:
☛ 空二叉树
☛ 只有一个跟结点
☛ 根结点只有左子树
☛ 根结点只有右子树
☛ 根结点既有左子树又有右子树 -
二叉树的特性:(理解并牢记二叉树的以下特性,可以帮助我们更好地使用它)
☛ 在二叉树的第 层上至多有 个结点()。比如第一层是根结点,只有一个;第二层有两个:根结点的左子树和右子树···
☛ 深度为 的二叉树至多有 个结点()。比如深度为 1,则至多只有 1 个结点,即根结点;深度为 2,则至多只有 3 个结点:根结点,根结点的左子树,根结点的右子树···
☛ 对任何一棵二叉树 ,如果其叶子结点点数为 ,度(即子结点数)为 2 的结点数为 ,则 。
☛ 具有 个结点的完全二叉树的深度为 ( 表示不大于 的最大整数)。
☛ 如果对一棵树有 个结点的完全二叉树(其深度为 )的结点按层序编号(从第 1 层到第 层,每层从左到右),对任一结点 有:
1).如果 ,则结点 是二叉树的根,无双亲;如果 ,则其双亲是结点 。
2).如果 ,则结点 无左孩子(结点 为叶子结点);否则其左孩子是结点 .
3).如果 ,则结点 无右孩子;否则其右孩子是结点 。 -
前面提及到顺序存储对数这种一对多的关系结构实现起来是比较困难的,但是对于二叉树,由于它的特殊性,使得用顺序存储结构也可以实现。
-
二叉树的遍历(traversing binary tree):是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
-
二叉树的遍历方式有很多,如果我们限制了从左到右的习惯方式,那么主要就分为四种:
☛ 前序遍历:规则是先访问根结点,然后前序遍历左子树,再前序遍历右子树(总结:根结点 -> 左子树 -> 右子树)。如下图所示,遍历的顺序为:ABDGHCEIF。
前序遍历☛ 中序遍历:从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后再访问根结点,最后中序遍历右子树(总结:左子树 -> 根结点 -> 右子树)。如下图所示,遍历的顺序为:GDHBAEICF。
中序遍历☛ 后序遍历:从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点(总结:**从左到右访问叶子结点 -> 根结点)。如下图所示,遍历的顺序为:GHDBIEFCA。
后序遍历☛ 层序遍历:从树的第一层,即根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问(总结:第一层 -> 第二层(从左到右访问结点)-> ··· -> 最后一层(从左到右访问结点)。如下图所示,遍历的顺序为:ABCDEFGHI。
层序遍历-
树,森林看似复杂,其实它们都可以转化为简单的二叉树来处理,这样就使得面对树和森林的数据结构时,编码实现成为了可能。
-
最基本的压缩编码方法:赫夫曼编码
第 7 章:图
- 图(Graph):由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:,其中, 表示一个图, 是图 中的顶点的集合, 是图 中边的集合。
-
在线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后驱。
在树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。
图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。 -
对于图的定义,有以下几个地方需要明确注意:
☛ 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中的数据元素,我们称之为顶点(Vertex)。
☛ 线性表可以没有数据元素,称为空表;树中可以没有结点,称为空树;而在图结构中,不允许没有顶点。在定义中,若 是顶点的集合,即强调了顶点集合 有穷非空。
☛ 线性表中,相邻的数据元素之间具有线性关系;树结构中,相邻两层的结点具有层次关系;而 图中,任意两个顶点之间都可能存在关系,顶点之间的逻辑关系用边进行表示,边集可以是空的。 -
图的种类:
☛ 无向图(Undirected graphs):若顶点 到 之间的边没有方向,则称这条边为 无向边(Edge),用无序偶对 来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为 无向图。下图所示即为无向图:
无向图由于无向图是无方向的,连接顶点 与 的边,可以表示成无序对 ,也可以写成 。
对于上图中的无向图 来说,,其中顶点集合 ;边集合
☛ 有向图(Directed graphs):若从顶点 到 的边有方向,则称这条边为 有向边,也称为 弧(Arc)。用有序偶 来表示, 称为弧尾(Tail), 称为弧头(Head)。如果图中任意两个顶点之间的边都是有向边,则称该图为 有向图(Directed graphs)。如下图所示即为一个有向图:
有向图连接到顶点 到 的有向边就是弧, 是弧尾, 是弧头, 表示弧,注意不能写成 。
对于上图的有向图 来说,,其中顶点集合 ;弧集合 。
注:看清楚了,无向边用小括号 表示,而有向边则是使用尖括号 表示。
☛ 简单图:在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。如下所示的两个图就不属于简单图:
非简单图☛ 完全无向图:在无向图中,如果任意两个顶点之间都存在边,则称该图为 无向完全图。如下图所示即为一个无向完全图:
无向完全图☛ 有向完全图:在有向图中,如果任意两个顶点之间都存在 方向互为相反 的两条弧,则称该图为 有向完全图。如下图所示即为一个有向完全图:
有向完全图-
无向图顶点的边数叫做 度,有向图顶点分为 入度(箭头朝自己) 和 出度(箭头朝外)。
-
有些图的边或弧具有与它相关的数字,这种 与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网(Network)。下图就是一张带权的图,即标识中国四大城市的直线距离的网,此图中的权就是两地的距离。
-
图结构中,路径的长度是路径上的边或弧的数据。
-
第一个顶点到最后一个顶点相同的路径称为 回环 或 环(Cycle)。
序列中顶点不重复出现的路径称为 简单路径。
除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为 简单回路 或 简单环。
下图所示两个图粗线都构成环,左侧的环只有第一个顶点和最后一个顶点都是 B,其余顶点没有重复出现,因此其是一个简单环。而右侧的环,由于顶点 C 的重复,因此它就不是简单环了。
-
图中顶点间存在 路径,两顶点存在路径则说明是 连通 的,如果路径最终回到起始点则成为 环,当中不重复叫 简单路径。若任意两顶点都是连通的,则图就是 连通图,有向则称为 强连通图。图中有子图,若子图极大连通则就是 连通分量,有向的则称为 强连通分量。
-
无向图中连通且 个顶点 条边叫 生成树。有向图中一顶点入度为 其余顶点入度为 的叫 有向树。一个有向图由若干棵有向树构成生成 森林。
-
由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构(即数组)来表示。而多重链表尽管可以实现图结构(即以一个数据域和多个指针域组成的结点表示图中的一个顶点),但是却存在内存浪费或操作不便的问题。因此,图存储结构最终还是得通过结合顺序存储和链式存储才能做到比较好地实现。
-
图的遍历和树的遍历类似,我们希望 从图中某一顶点触发,遍历图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历(Traversing Graph)。
-
图的遍历通常有两种方案:深度优先遍历 和 广度优先遍历。
☛ 深度优先遍历(Depth First Search):也称为 深度优先搜索,简称 DFS。对于连通图,它从图中某个顶点 触发,访问此顶点,然后从 的未被访问的邻接点出发深度优先遍历图,直至图中所有和 有路径相通的顶点都被访问到。而对于非连通图,只需要对它的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后,若图中尚未有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点,重复上述过程,直至图中所有顶点都被访问到为止。(所谓深度优先搜索,就是选择一个顶点开始走,期间对于走过的顶点就不在访问,走其他未被访问的,一直走到无路可走。若此时还有顶点未走过,选择一个,重复上述过程。)。
☛ 广度优先遍历(Breadth First Search):又称为 广度优先搜索,简称 BFS。 -
深度优先遍历与广度优先遍历算法在时间复杂度上是一样的,不同之处仅仅在于对顶点访问的顺序不同。
-
深度优先更适合目标比较明确,以找到目标为主要目的的情况;而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
-
把构造连通网的最小代价生成树称为 最小生成树(Minimum Cost Spanning Tree)。
-
找连通网的最小生成树,经典的算法有两种:普里姆(Prim)算法 和 克鲁斯卡尔(Kruskal)算法。
-
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们成为 AOV 网(Activity On Vertext Network)。AOV 网中的弧表示活动之间存在的某种制约关系,同时 AOV 网中不能存在回路。
-
拓扑序列:设 是一个具有 个顶点的有向图, 中的顶点序列 ,满足若从顶点 到 有一条路径,则在顶点序列中顶点 必在顶点 之前,则我们将这样的顶点序列称为一个 拓扑序列。
-
所谓 拓扑排序,其实就是对一个有向图构造拓扑序列的过程。
-
对 AOV 网进行拓扑排序的基本思路是:从 AOV 网中选择一个入度为 0 的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者 AOV 网中不存在入度为 0 的顶点为止。
-
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为 AOE 网(Activity On Edge Network)。
-
我们把 AOE 网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点。由于一个工程,总有一个开始,一个结束,所以正常情况下,AOE 网只有一个源点一个汇点。
-
我们把路径上各个活动所持续的时间之和称为 路径长度,从源点到汇点具有最大长度的路径叫 关键路径,在关键路径上的活动叫关键活动。
网友评论