程序 =
数据结构
+算法
本篇文章只介绍 数据结构
基础内容。
算法
是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令标识一个或多个操作。
简而言之,算法是描述解决问题的方法。
好的算法,应该具有正确性、可读性、健壮性、高效率和低存储量的特征。
可以忽略加法常数:O(2n + 3) = O(2n)
与最高次项相乘的常数可忽略:O(2n^2) = O(n^2)
最高次项的指数大的,函数随着 n 的增长,结果也会变得增长更快:O(n^3) > O(n^2)
判断一个算法时间效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数:
O(2n^2) = O(n^2 + 3n + 1)
O(n^3) > O(n^2)
时间复杂度
:所消耗的时间即基本操作执行次数。
用常数1取代运行时间中的所有加法常数。
在修改后的运行次数函数中,只保留最高阶项。
如果最高阶项存在且不是 1,则去除与这个项相乘的常数,得到的结果就是大 O 阶。
O(2n+1) = O(2n)
O(2n^2+1) = O(n^2)
计算下面程序的时间复杂度:
int i, j;
for (i = 0; i < n; i++) {
for (j = i; j < n; j++) {
System.out.println("hello");
}
}
分析:
对于外循环,其时间复杂度为 O(n);
对于内循环,总执行次数为:
根据大 O 阶推导法,上述代码的时间复杂度为:
O ( n 2 ) O(n^{2}) O(n2)
执行次数函数 | 阶 | 非正式术语 |
---|---|---|
12 12 12 | O ( 1 ) O(1) O(1) | 常数阶 |
2 n + 3 2n+3 2n+3 | O ( n ) O(n) O(n) | 线性阶 |
3 n 2 + 2 n + 1 3n^{2}+2n+1 3n2+2n+1 | O ( n 2 ) O(n^{2}) O(n2) | 平方阶 |
5 l o g 2 n + 20 5log_{2}n+20 5log2n+20 | O ( l o g n ) O(logn) O(logn) | 对数阶 |
2 n + 3 n l o g 2 n + 19 2n+3nlog_{2}n+19 2n+3nlog2n+19 | O ( n l o g n ) O(nlogn) O(nlogn) | n l o g n 阶 nlogn阶 nlogn阶 |
6 n 3 + 2 n 2 + 3 n + 4 6n^{3}+2n^{2}+3n+4 6n3+2n2+3n+4 | O ( n 3 ) O(n^{3}) O(n3) | 立方阶 |
2 n 2^{n} 2n | O ( 2 n ) O(2^{n}) O(2n) | 指数阶 |
常用的时间复杂度所耗费的时间从小到大依次是:
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(logn) < O(n) < O(nlogn) < O(n^{2}) < O(n^{3}) < O(2^{n}) < O(n!) < O(n^{n}) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
算法的 空间复杂度
是指算法所需的存储空间,即运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序运行所需要的内存有个预先估计。
算法的时间复杂度和空间复杂度是可以相互转化的。
O(1)
。int a = 0;
int b = 0;
System.out.println(a + b);
例如下面这段代码,通过递归实现,每次调用 fun 函数,都会创建 1 个变量 k。调用 n 次,空间复杂度为:O(n*1) = O(n)
。
对于单线程来说,递归有运行时的堆栈。空间复杂度求得是递归最深一次所耗费得空间个数,保证空间足够容纳它得所有递归过程。
public int fun(int n) {
int k = 10;
if (n == k) {
return n;
} else {
return fun(n + 1);
}
}
存储空间包括两部分:
静态空间
:这部分空间的大小与输入/输出数据的个数多少、数值大小无关,主要包括:指令空间(代码空间)、数据空间(常量、简单变量)等所占空间。可变空间
:这部分空间大小与算法有关,主要包括:动态分配的空间,以及递归栈所需空间等。类别 | 排序法 | 最差时间 | 最好时间 | 平均时间复杂度 | 稳定性 | 空间复杂度 | 复杂性 | 性能 |
---|---|---|---|---|---|---|---|---|
交换 | 冒泡排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^{2}) O(n2) | 稳定 | O ( 1 ) O(1) O(1) | 简单 | $$$$ |
- | 快速排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | 不稳定 | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | 较复杂 | $$$$ |
插入 | 插入排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^{2}) O(n2) | 稳定 | O ( 1 ) O(1) O(1) | 简单 | n 小时较好 n小时较好 n小时较好 |
- | 希尔排序 | 依赖于-> | 增量序列 | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | 不稳定 | O ( 1 ) O(1) O(1) | 较复杂 | n 大时较好 n大时较好 n大时较好 |
选择 | 选择排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n 2 ) O(n^{2}) O(n2) | O ( n 2 ) O(n^{2}) O(n2) | 不稳定 | O ( 1 ) O(1) O(1) | 简单 | 大部分已排序时较好 |
- | 堆排序 | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | 不稳定 | $ O ( 1 ) O(1) O(1) | 较复杂 | |
=> | 二叉树排序 | O ( n 2 ) O(n^{2}) O(n2) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | 稳定 | O ( n ) O(n) O(n) | n 小时较好 n小时较好 n小时较好 | |
=> | 归并排序 | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(n∗log2n) | 稳定 | O ( n ) O(n) O(n) | 较复杂 | n 大时较好 n大时较好 n大时较好 |
=> | 基数排序 | 稳定 | 较复杂 |
线性表
:零个或多个数据元素的有限序列。
顺序存储结构
:用一段地址连续的存储单元依次存储线性表的数据元。
顺序存储示意图如下所示:
存储器中的每个存储单元都有自己的编号,这个编号称为 地址
。
每个数据元素,不管它是整型、实型还是字符型,都是需要占用一定的存储单元空间的。假设占用的是 c 个存储单元,那么对于线性表的第 i 个数据元素 ai 的存储位置都可以由 a1 推导算出:
L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) ∗ c LOC(a_{i}) = LOC(a_{1}) + (i - 1) * c LOC(ai)=LOC(a1)+(i−1)∗c
存储位置如下所示:
通过存储位置公式,就可以随时算出线性表中任意位置的地址。不管是第一个还是最后一个,都是相同的时间,即:对于计算来说,线性表中每个位置的存入或者取出数据都是相等的时间,也就是一个常数时间。因此,线性表的存取操作时间复杂度为:O(1)
。
我们通常将存取操作具备常数性能(时间复杂度为 O(1))的存储结构称为随机存储结构
。
O(1)
。O(n)
。总结: 线性表顺序存储结构比较 适用于存取操作较多,增删操作较少的场景。
一个或多个结点组合而成的数据结构称为链表
。
结点,一般由两部分内容构成:
数据域
:存储真实数据元素。指针域
:存储下一个结点的地址(指针)。头指针
,用于存储链表的第一个数据元素。头结点的数据域可以不存储任何信息,其指针域存储指向第一个结点的指针,即指向头指针。
在线性表的顺序存储结构(即数组)中,其任意一个元素的存储位置可以通过计算得到,因此其数据读取的时间复杂度为 O(1)
。
O(n)
。O(1)
;O(n)
。总结: 单链表 适用于频繁插入或删除数据的场景。
将单链表的终端结点的指针端由空指针改为指向头节点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称 循环链表(circular linked list)
。
循环链表不一定需要头结点。
单链表和循环链表的区别:
(主要差异在循环的判断条件上)
p -> next = NULL
)。p -> next = head
)。双向链表(double linked list)
在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
栈
是限定尽在表尾(栈顶)进行插入和删除操作的线性表。所以栈又称为后进先出(Last In First Out
)的线性表,简称 LIFO 结构。
内部实现原理:
栈顶
,另一端称为 栈底
。空栈
。栈,是特殊的线性表,具备后进先出(LIFO)的特性。
顺序栈
。链栈
。顺序栈和链栈对比:
队列
是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out
)的线性表。和线性表一样,队列也存在顺序存储和链式存储两种存储方式。
队头
。队尾
。数组
是一种特殊的线性表存储结构。其特殊性在于表中的元素本身也是一种线性表,内存连续,根据下标在 O(1) 时间读/写任何元素。
数组的顺序存储:
行优先顺序
:每一行的第一个元素位于低地址,最后一个元素位于高地址。因此,访问同一行中的元素时,其地址是连续的。列优先顺序
:每一列的第一个元素位于低地址,最后一个元素位于高地址。因此,访问同一列中的元素时,其地址是连续的。数组中的任一元素可以在相同的时间存取,即顺序存储的数组是一个随机存取结构。
数组的优点是访问速度快,缺点是插入和删除元素效率较低。
广义表
,又称列表,也是一种线性存储结构。同数组类似,广义表中既可以存储不可再分的元素,也可以存储广义表。
记作:LS=(a1, a2, ..., an)
。
LS
代表广义表的名称;an
代表广义表存储的数据。ai
既可以代表单个元素,也可以代表另一个广义表。广义表中存储的每个元素称为 原子
,而存储的广义表称为 子表
。
广义表的存储结构:
深度优先顺序
:广义表的每个元素都会被递归地处理,直到到达叶子结点。广度优先顺序
:广义表的每个元素都会被逐层处理,每一层的元素都按照其在该层中的顺序进行处理。总结: 广义表的优点是具有较好的灵活性,可以存储任意类型的数据,缺点是访问速度较慢。
串(String)
是由零个或多个字符组成的有限序列,又叫字符串。
串的逻辑结构和线性表很相似,不同之处在于串针对的是字符集,也就是串中的元素都是字符。因此,对于串的基本操作与线性表是由很大差别的。线性表更关注的是单个元素的操作,比如查、插入或删除一个元素,但串中更关注的是查找子串为止,得到指定位置字串,替换字串等操作。
串的顺序存储结构
是用一组地址连续的存储单元来存储串中的字符序列。
一般是用定长数组来定义。由于是定长数组,因此就会存在一个预定义的最大串长度。一般可以将实际的串长度值保存在数组 0 下标位置,也可以放在数组最后一个下标位置。
也有些语言使用在串值后面加一个不计入串长度的结束标记符(比如 \0
),来表示串值的终结,这样就无需使用数字进行记录。
串的链式存储结构
,与线性表相似。
由于串结构的特殊性(结构中的每个元素数据都是一个字符),如果也简单地将每个链结点存储一个字符,就会存在很大的空间浪费。因此,一个结点可以考虑存放多个字符。如果最后一个结点未被占满时,可以使用 #
或其他非串值字符补全。
串的链式存储结构除了在链接串与串时有一定的方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。
树
是 n 个结点的有限集合(n >= 0)。
空树
。子树(SubTree)
。正确的树结构:
下图所示的结构就不符合树的定义,因为它们都有相交的子树。
线性结构 | 树结构 |
---|---|
- 第一个数据元素:无前驱 - 随后一个数据元素:无后继 - 中间元素:一个前驱一个后继 |
- 根结点:无双亲,唯一 - 叶结点:无孩子,可以多个 - 中间结点:一个双亲多个孩子 |
二叉树(Binary Tree)
:是一个包含 n 个结点的有限集合(n >= 0)。该集合或者为空集(称为空二叉树
)或者由一个根节点和两棵互不相交的二叉树(左子树
、右子树
)组成。
二叉树的遍历
是指从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问且仅被访问一次。
总结: 根节点 -> 左子树 -> 右子树
如下图所示,遍历的顺序为:ABDGHCEIF。
总结: 左子树 -> 根节点 -> 右子树
如下图所示,遍历的顺序为:GDHBAEICF
总结: 从左到右访问叶子结点 -> 根节点
如下图所示,遍历的顺序为:GHDBIEFCA
总结: 第一层 -> 第二层(从左到右访问结点)-> …… -> 最后一层(从左到右访问结点)
如下图所示,遍历的顺序为:ABCDEFGHI
树,森林看似复杂,其实它们都可以转化为简单的二叉树来处理。这样就使得面对树和森林的数据结构时,编码实现成为了可能。最基本的压缩编码方法:赫夫曼编码。
赫夫曼编码
:给定 n 个权限作为 n 个叶子结点,构造一棵二叉树,若 树的带权路径长度
达到最小,则这棵树被称为哈夫曼树。
下图这棵树就是哈夫曼树:
哈夫曼树指的是树的带权路径长度达到最小,那什么是树的带权路径长度呢?我们需要先了解一下路径和路径长度的概念。
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径
。通路中分支的数目称为路径长度
。若规定根节点的层数为1,则从根节点到 L 层结点的路径长度为 L - 1。
例如:
了解了路径和路径长度的概念之后,我们还是不能理解什么是树的带权路径长度,下面我们来看一下带权路径长度的概念吧。
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的 权
。
结点的 带权路径
长度为:从根节点到该结点之间的路径长度与该结点的权的乘积。
例如:
了解了权路径长度的概念之后,那么树的带权路径长度是指什么呢?
树的带权路径长度
规定为所有叶子结点的带权路径长度之和,记为 WPL
。
例如:
所以,赫夫曼树作为树的带权路径长度达到最小的树,就是指树上所有带权路径的总和最小。
我们再比较下面两棵树:
上面的两棵树都是以 {10, 20, 50, 100} 为叶子节点的树。
左边的树 WPL > 右边的树 WPL
也可以尝试计算除上面两种示例之外的情况,试过之后就会发现,实际上右边的树就是 {10, 20, 50, 100} 对应的哈夫曼树。
在线性表中:
数据元素之间是被串起来的,仅有线性关系;
每个元素只有一个直接前驱和一个直接后驱。
在树形结构中:
图
是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的。途中任意两个数据元素之间都可能相关。
图是由顶点的有穷非空集合和顶点之间边的集合组成。
通常表示为:G(V, E)
G
:表示一个图。V
:表示图 G 中顶点的集合。E
:表示图 G 中边的集合。元素
;结点
;顶点(Vertex)
。空表
;空树
;若顶点 Vi 到 Vj 之间的边没有方向,则称这条边为 无向边(Edge)
,用无序偶对 (Vi, Vj)
来表示。
如果图中任意两个顶点之间的边都是无向边,则称该图为 无向图
。
无向图顶点的边数叫做 度
。
下图所示即为无向图 G1:
结合 G(V, E) 表达式介绍:
(A, D)
,也可以写成 (D, A)
。G1 = (V1, {E1})
。若从顶点 Vi 到 Vj 的边 有方向,则称这条边为 有向边
,也成为 弧(Arc)
,用有序偶
来表示,Vi 称为 弧尾(Tail)
,Vj 称为 弧头(Head)
。
如果图种任意两个顶点之间的边都是有向边,则称该图为 有向图(Directed graphs)
。
有向图的顶点分为 入度
(箭头朝自己)和 出度
(箭头朝外)。
如下图所示即为一个有向图 G2:
结合 G(V, E) 表达式介绍:
弧
,其中顶点 A 是 弧尾
,顶点 D 是 弧头
,
表示 弧
。(注意:不能写成 G2 = (V2, {E2})
。V2 = {A, B, C, D}
。E2 = {, , , }
。在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则成这样的图为 简单图
。
如下图所示的两个图就不属于简单图:
在无向图中,如果 任意两个顶点之间都存在边,则称该图为 无向完全图
。
如图所示即为一个无向完全图:
在有向图中,如果 任意两个顶点之间都存在方向互为相反的两条弧,则称该图为 有向完全图
。
如图所示即为一个有向完全图:
有些图的边或弧具有与它相关的数字,这种 与图的边或弧相关的数 叫做 权(Weight)
。
这些权可以表示从一个顶点到另一个顶点的距离或耗费,这种 带权的图 通常称为 网(Network)
。
上图就是一张带权的图,即标识中国四大城市的直线距离的网。此图中的权就是两地的距离。
图结构中,路径的长度是路径上的边或弧的数据。第一个顶点到最后一个顶点相同的路径 称为 回环
或 环(Cycle)
,序列中顶点不重复出现的路径 称为 简单路径
。
除了一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称之为 简单回路
或 简单环
。
举个例子:
如图所示,两个图粗线都构成环。
连通
:图中两顶点间存在路径,则说明是连通的。简单路径
:如果路径最终回到起始点则称为环,当中不重复的叫做简单路径。强连通图
:若任意两顶点都是连通的,则该图就是连通图,有向则称为强连通图。强连通分量
:图中能够连通的极大子图就是连通分量,有向则称为强连通分量。生成树
:无向图中连通且 n 个顶点 n-1 条边叫生成树。有向树
:有向图中一顶点入度为 0,其余顶点入度为 1 的叫有向树。森林
:一个有向图由若干棵有向树构成森林。图的遍历和树的遍历类似。我们希望 从图中某一顶点触发,遍历图中其余顶点。且使每一个顶点仅被访问一次,这一过程就叫做 图的遍历(Traversing Graph)
。
深度优先搜索(Depth-First-Search)
,简称 DFS。最直观的例子就是 “走迷宫”。假设你站在迷宫的某个岔路口,要找到出口就需要先选择一个岔路口,走不通之后再回来重新选择一条路。这种走法就是一种深度优先搜索。
二叉树的前序、中序、后序遍历,本质上也可以认为是深度优先搜索,深度优先搜索是先序遍历的推广。
深度优先搜索的核心思想:
知识回顾: 若任意两顶点都是连通的,则该图就是
连通图
。
只需要对它的连通分量分别进行深度优先遍历。
知识回顾:
- 图中两顶点间存在路径,则说明是
连通
的。- 图中能够连通的极大子图就是
连通分量
。
对上无向图进行深度优先遍历,从 A 开始:
第1步: 访问A
。
第2步: 访问B
(A的邻接点)。 在第1步访问A之后,接下来应该访问的是A的邻接点,即"B,D,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"D和F"的前面,因此,先访问B。
第3步: 访问G
(B的邻接点)。 和B相连只有"G"(A已经访问过了)
第4步: 访问E
(G的邻接点)。 在第3步访问了B的邻接点G之后,接下来应该访问G的邻接点,即"E和H"中一个(B已经被访问过,就不算在内)。而由于E在H之前,先访问E。
第5步: 访问C
(E的邻接点)。 和E相连只有"C"(G已经访问过了)。
第6步: 访问D
(C的邻接点)。
第7步: 访问H
。因为D没有未被访问的邻接点;因此,一直回溯到访问G的另一个邻接点H。
第8步: 访问(H的邻接点)F。
因此,访问顺序是: A -> B -> G -> E -> C -> D -> H -> F
对上有向图进行深度优先遍历,从A开始:
第1步: 访问 A
。
第2步: 访问 B
(A的出度对应的字母)。 在第1步访问A之后,接下来应该访问的是A的出度对应字母,即"B,C,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"C和F"的前面,因此,先访问B。
第3步: 访问 F
(B的出度对应的字母)。 B的出度对应字母只有F。
第4步: 访问 H
(F的出度对应的字母)。 F的出度对应字母只有H。
第5步: 访问 G
(H的出度对应的字母)。
第6步: 访问 E
(G的出度对应字母)。 在第5步访问G之后,接下来应该访问的是G的出度对应字母,即"B,C,E"中的一个。但在本文的实现中,顶点B已经访问了,由于C在E前面,所以先访问C。
第7步: 访问 D
(C的出度对应的字母)。
第8步: 访问 D
(C的出度对应字母)。 在第7步访问C之后,接下来应该访问的是C的出度对应字母,即"B,D"中的一个。但在本文的实现中,顶点B已经访问了,所以访问D。
第9步: 访问 E
。D无出度,所以一直回溯到G对应的另一个出度E。
因此,访问顺序是:A -> B -> F -> H -> G -> C -> D -> E
广度优先搜索(Breadth-First-Search)
,简称 BFS。
类似于树的层序遍历,广度优先搜索是按层来处理顶点,距离开始点最近的顶点首先被访问,而距离最远的顶点则最后被访问。
广度优先搜索的核心思想:
A
开始,有 4 个邻接点,“B,C,D,F”,这是第二层;因此,访问顺序是:A -> B -> C -> D -> F -> G -> E -> H。
因此,访问顺序是:A -> B -> C -> F -> D -> H -> G -> E
相同点:
不同点:
对顶点访问的顺序不同:
把构造连通网的最小代价生成树称为 最小生成树
。
如图所示,假设 V0 到 V8 表示 9 个村庄,现在需要在这 9 个村庄架设通信网络,村庄之间的数组代表村庄之间的直线距离,求用最小成本完成这 9 个村庄的通信网络建设。
找连通网的最小生成树,经典的算法有两种:普利姆算法(Prim)、克鲁斯卡尔算法(Kruskal)。
时间复杂度:
记顶点数为 v,边数为 e,邻接矩阵的时间复杂度为 O(v2)
,邻接表的时间复杂度为 O(elog2v)
。
Kruskal 算法可以称为 “加边法”,初始最小生成树边数为 0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
时间复杂度:
记边数为 e,克鲁斯卡尔算法(Kruskal算法)的时间复杂度为 elog2e
。
补充: Kruskal 算法和 Prim 算法、Boruvka 算法一样,都是贪婪算法的应用。不同点在于 Kruskal 算法在图中存在同样权值的边时也有效。
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为 AOV网(Activity On Vertext Network)
。
AOV 网中的弧表示活动之间存在的某种制约关系,同时 AOV 网中不能存在回路。
设 G=(V, E) 是一个具有 n 个顶点的有向图,V 中的顶点序列 V1,V2,…,Vn 满足若从顶点序列 Vi 到 Vj 有一条路径,则在顶点序列中顶点 Vi 必在顶点 Vj 之前,则我们将这样的顶点序列成为一个 拓扑序列
。
所谓 拓扑排序
,其实就是对一个有向图构造拓扑序列的过程。
对 AOV 网进行拓扑排序的基本思路是:
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间。这种有向图的边表示活动的网,我们称之为 AOE 网(Activity On Edge Network)
。
始点
或 源点
。终点
或 汇点
。由于一个工程,总有一个开始,一个结束,所以正常情况下,AOE 网只有一个源点一个汇点。路径长度
。关键路径
。关键活动
。二叉排序树(Binary Sort Tree)
,又称为 二叉查找树
,它或者是一棵空树,或者是具有下列性质的二叉树:
构造一棵二叉排序树的目的,其实并不是为了排序,而是 为了提高查找和插入删除关键字的速度。
不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集。
而二叉排序树这种非线性的结构,也有利于插入和删除的实现。
二叉排序树是以 链接 的方式存储。
log_2*n + 1
。O(logn)
。平衡二叉树
是一种二叉排序树,近似二分查找,其中 每一个结点的左子树和右子树的高度差至多等于 1。
平衡因子BF
。-1
、0
和 1
。距离插入结点最近的,且平衡因子的绝对值大于 1 的结点为根的子树,我们称为 最小不平衡子树
。
对于树来说,一个结点只能存储一个元素。那么在元素非常多的时候,就会使得:
这就使得内存存取外存次数非常多,这显然成了时间效率上的平静,这迫使我们要打破每一个结点只存储一个元素的限制,为此引入了 多路查找树
的概念:
常见的多路查找树有四种:2-3树、2-3-4树、B树、B+树。
2-3树
:每个结点都具有两个孩子(我们称它为 2 结点)或三个孩子(我们称它为 3 结点)的树。
2 结点
包含:一个元素、两个孩子(或没有孩子);
3 结点
包含:一小一大两个元素、三个孩子(或没有孩子);
2-3-4树
是 2-3 树的扩展,在其基础上增加一个 4 结点的使用。
4 结点
包含:小、中、大三个元素、四个孩子(或没有孩子)。
B 树
是一种平衡的多路查找树。2-3 树和 2-3-4 树都是 B 树的特例。
结点最大的孩子数目称为 B 树的 阶(order)
。
3阶B树
;4阶B树
。B+ 树
是一种应文件系统所需而出的一种 B 树的变形树。
散列表(Hash table,也叫哈希表)
是一种根据关键码值(Key value)而直接进行访问的数据结构。
散列函数
,存放记录的数组叫做 散列表
。散列地址
。散列函数设计原则:
构造散列函数的六种方法: 直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法。
处理散列冲突的四种方法: 开放定址法、再散列函数法、链地址法、公共溢出区法。
整理完毕,完结撒花~
参考地址:
1.数据结构——学习笔记——入门必看【建议收藏】,https://blog.csdn.net/liu17234050/article/details/104237990
2.常见排序算法及对应的时间复杂度和空间复杂度,https://blog.csdn.net/gane_cheng/article/details/52652705
3.连通分量,https://www.ultipa.cn/document/ultipa-graph-analytics-algorithms/connected-component/v4.0
4.最小生成树(Kruskal算法),https://blog.csdn.net/qq_36932169/article/details/81236147
5.B+树看这一篇就够了(B+树查找、插入、删除全上),https://zhuanlan.zhihu.com/p/149287061