我真不明白为什么同一个课程,同样的内容,就只是因为开课的学院不同就不能互认,搞得我一门课程要学两次🙄
Chapter 1
- 什么是数据结构?
研究如何在计算机中有效的存储和组织数据的方法
- 如何表示数据结构?
抽象数据类型(ADT):是一种描述数据结构的程序设计语言无关的模型。ADT定义为一组数值,和一组相关的操作。
- 什么是算法?
算法:用于解决一个问题的方法或步骤。
- 算法的属性:
- 确定性:它必须是正确的
- 可行性:它必须包含一系列具体的步骤,执行的过程无二义性
- 有穷性:它必须包含有限的步骤,它必须是可以结束的
- 数据结构 + 算法 = 程序
- 数据结构主要研究如何有效的存储和组织应用程序的数据。
- 算法主要研究如何对数据进行处理从而解决某个问题
Chapter 2
Chapter 3
增长率分析:分析代价(时间、空间)如何随着输入数据规模大小的增长而增长
- $T(n)$:运行时间关于输入数据规模大小 $n$ 的函数。
- $S(n)$:消耗空间关于输入数据规模大小 $n$ 的函数。
渐进分析:渐进分析是对T(n)的一种简化分析,该分析保留了 $T(n)$ 的渐进增长趋势
- 大 $O$ 分析(上界分析)
对于非负函数 $T(n)$,如果存在正常数 $c$ 和 $n_0$ 使得当 $n>n_0$ 时 $T(n)\leq cf(n)$,则称 $T(n)$ 属于集合 $O(f(n))$,或记为 $T(n)=O(f(n))$
- 大 $\Omega$ 分析(下界分析)
对于非负函数 $T(n)$,如果存在正常数 $c$ 和 $n_0$ 使得当 $n>n_0$ 时 $T(n)\geq cg(n)$,则称 $T(n)$ 属于集合 $\Omega(g(n))$,或记为 $T(n)=\Omega(g(n))$
- 大 $\Theta$ 分析
若 $T(n)=O(h(n))$ 且 $T(n)=\Omega(h(n))$,则称 $T(n)=\Theta(h(n))$
$T(n)$ 存在多个上界和下界。
简化规则:
- 常数规则:若 $f(n)=O(kg(n))$,$k>0$,则 $f(n)=O(g(n))$
- 该规则同样适用于 $\Omega$ 和 $\Theta$
- 加法规则:若 $f_1(n)=O(g_1(n))$ 且 $f_2(n)=O(g_2(n))$,则 $f_1(n)+f_2(n)=O(\max(g_1(n),g_2(n)))$
- 乘法规则:若 $f_1(n)=O(g_1(n))$ 且 $f_2(n)=O(g_2(n))$,则 $f_1(n)f_2(n)=O(g_1(n)g_2(n))$
常见的渐进复杂度:
$$ O(1)线性表
概念:
- 线性表:由有限的数据元素构成的序列
- 当前位置:在线性表中插入、删除、或读去数据的位置。当前位置通过**栅栏 (fence)**来定义。栅栏将线性表划分为左右两个部分。插入、删除、读取都在栅栏所在的位置执行(栅栏后面的位置)
- 一个包含0个数据元素的线性表称为空表
- 线性表的长度指的是线性表中数据元素的个数
- 线性表的开始端称为表头(head), 线性表的结束端称为表尾(tail)
- 有序表:表中的所有的数据元素是根据其数值的大小来进行先后排列
- 无序表:表中数据元素的位置和该元素的数值没有必然联系
线性表ADT
|
|
线性表的实现:
- 数组
- 链表
比较 | 数组 | 链表 |
---|---|---|
插入与删除 | $\Theta(n)$ | $\Theta(1)$ |
求前驱和随机访问 | $\Theta(1)$ | $\Theta(n)$ |
特点 | 需要提前为数组分配空间 | 消耗的空间随数据元素个数的增长而增长 |
空间 $E$ 每个数据元素所占空间大小 $P$:每个指针所占空间大小 $D$:数组最多能存放的数据元素的个数 $n$:当前数据元素的个数 | $DE$ | $n(P+E)$ |
栈
概念:
- 栈:插入、删除只在一端进行的线性表
- LIFO 性质: Last In, First Out. (后进先出)
- 入栈(PUSH):插入元素到栈中
- 出栈(POP):从栈中取出元素
- 栈顶(TOP):栈中插入、删除的位置
栈的ADT
|
|
栈的实现:
- 数组
- 链表
队列
概念:
- 队列: 插入和删除分别在表的两端进行的线性表
- FIFO 性质: First in, First Out (先进后出)
- 插入: enqueue(入队)
- 删除: dequeue (出队)
- 第一个元素: front(队头)
- 最后一个元素: rear(队尾)
队列ADT
|
|
栈的实现:
- 数组
- 链表
Chapter 5 二叉树
概念:
二叉树是一个包含有限个结点的集合,有以下两种情况
- 空集,空树
- 包含一个称为树根(root)的结点,以及两个互不相交的二叉树,它们分别称为左子树和右子树
对于树上的每个结点,和它有连边的下层结点称为它的孩子结点。在二叉树中,每个结点最多会有两个孩子,最少没有孩子
对于二叉树上的每个结点,和它有连边的上层结点称为它的父亲结点。二叉树中,除了树根外,每个结点都有一个父亲结点。
一个结点的父亲,父亲的父亲,父亲的父亲的父亲,等等都是它的祖先结点。反过来,一个结点的孩子,孩子的孩子,孩子的孩子的孩子,等等都是它的后辈结点
叶子结点:没有孩子结点的结点为叶子结点
路径:一个结点序列 $n_1,n_2,\cdot,n_k$,如果其中的每个结点 $n_i$ 是 $n_{i+1}$ 的父亲($1\leq i \leq k$),我们就称这个序列是从 $n_1$ 到 $n_k$ 的路径。这条路径的长度等于 $k-1$
深度:一个结点的深度等于从树根到这个结点的路径的长度
层次:根结点为第0层,往下逐个增加。(貌似等于深度)
高度:一棵树的高度等于其中最大结点深度加一
满二叉树:二叉树的每个结点都是叶子或拥有两个孩子的中间结点。
- 非空满二叉树中叶子的数目比中间结点多一个,这个很容易证明:如果只有根结点,则只有一个叶子结点,没有中间结点。每增加两个叶子,则原有的叶子结点会变为中间结点,故叶子始终比中间结点多一个。
完全二叉树: 除了最深的层次,二叉树的所有层次都填满结点,且最深层的叶子结点集中在该层的最左边的位置
二叉树结点ADT
|
|
二叉树的实现:
- 基于链接的方式
- 基于数组的方式
哈夫曼编码:
- 一种可变长度的编码
- 高频字符对应较短的编码
哈夫曼编码的编码过程需要用到一种称为哈夫曼树的满二叉树结构。
平均编码长度取决于所有叶子的加权路径长度的和
定理: 对于给定的一组字符以及出现的频率,以它们作为叶子节点构建的满二叉树中,哈夫曼树具有最小的加权路径长度的和。在预先知道每个字符的出现的频率的前提下,哈夫曼编码是具有最小的平均编码长度的非前缀编码。
构建哈夫曼编码的算法:
Chapter 6 一般树
概念:
- 树 T 是一个包含一个或多个节点的有限集合,其中有一个称为 树根 的节点r,余下的节点(T –{r}) 可以被划分为k个( k≥0)互不想交的子集 T1, T2, …, Tk, 每个子集都是一棵树,它们的树根 r1, r2, …, rk,都是r的孩子
- 森林:是包含有一棵或多棵树的集合。
树结点 ADT
|
|
树 ADT
|
|
Chapter7 内部排序
概念:
排序:给定一个序列的记录 $R_1 , R_2 , … , R_n$,它们对应的关键字依次是 $k_1 , k_2 ,… , k_n$ , 调整记录在序列中的次序得到新的记录序列 $R_{s1},R_{s2},…,R_{sn}$,使得记录的关键字满足以下性质: $k_{s1} ≤ k_{s2} ≤ …≤ k_{sn}$.
稳定的排序算法:排序的过程不会改变具有相等关键字的记录的相对先后次序。
内部排序:指的是待排序记录存放在计算机随机存储器中进行的排序过程。
外部排序:指的是待排序记录的数量很大,以致内存一次不能容纳全部记录, 在排序过程中尚需对外存进行访问的排序过程。
插入排序
记忆:像洗牌,将每张牌插入到合适的位置
实现:在第i次迭代,将第i个元素与前面 i-1 个元素进行比较,使其插入到前面适当的位置(1 <= i <= n-1)
|
|
代价分析:最好 $\Theta(n)$,最坏 $\Theta(n^2)$,平均 $\Theta(n^2)$
插入排序是一种稳定的排序算法。
冒泡排序
实现:由后到前,相邻两个元素比较,逆序则交换。一次冒泡过程结束后,最小的元素会到达最前面。
|
|
代价分析:最好、最坏、平均都是 $\Theta(n^2)$。
冒泡排序算法是一种稳定的排序算法。
选择排序
实现:第i趟排序就是从位置属于[i,n-1]的元素中,选出最小的元素,并将其和第i位置上的元素交换
|
|
代价分析:最好、最坏、平均都是 $\Theta(n^2)$。
选择排序是一种不稳定的排序算法。
希尔排序(Shellsort)
思路(分组):每隔一定间距取一个元素放入同一组,将所有元素分成多组,并在组内进行插入排序。然后减小间距重新分组,重新排序。最后当间距为 1 时,退化为一般的插入排序。
分析:由于最后一次排序是一般的插入排序,所以这种方法是可行的。而前面几次排序可以使元素一次性移动多格,所以速度会快。
代码:
|
|
当增量序列为 $∆[k] = 2^{t-k+1}-1$ 时,算法复杂度为 $O(n^{3/2})$。 其中 t 是排序的趟数,$1\leq k\leq t$。例如(15,7,3,1)。证明过程
希尔排序是一种不稳定排序。
快速排序
思想(分而治之):选择一个记录的值作为支点(pivot)。将小于支点的记录安排在支点的左侧,将大于等于支点的记录安排在支点的右侧。从而将原序列划分为左右两个子序列。然后在子序列中重复该过程。
|
|
代价分析:类似二叉树(层数代表次数)
|
|
平均情况:
$$ T(n)=cn+\frac{1}{n} \sum_{k=0}^{n-1} (T(k)+T(n-1-k))=\Theta(n \log n) $$改进方法:
- 选取更好的支点
- 选择以下三个记录中的中间值作为支点 : 序列的第一个,中间,最后一个记录
- 采用非递归的算法来实现快速排序
归并排序
思路(分而治之):将一个序列划分为两个长度相等的子序列,然后递归的对每个子序列进行归并排序。将排好序的两个子序列归并为一个有序的序列。
|
|
代价分析:最好、最坏、平均都是 $\Theta(n \log n)$。
堆排序
思路:堆顶结点的值总保持是最大值/最小值,故每次取堆顶,然后用剩下元素建堆,再重复该过程。
代价分析:建堆 $\Theta(n)$,移除最大值 $\Theta(\log n)$,n 次移除最大值 $\Theta(n\log n)$。故总代价为 $\Theta(n\log n)$
详细分析见我的博客。
箱排序和基数排序
箱排序(Binsort):设置若干个箱子,依次扫描待排序的记录 R[0],R[1],…,R[n-1],把关键字等于 k 的记录分配到第 k 个箱子里,然后按序号依次将各非空的箱子中的记录收集起来。
示例:3,8,1,8,4,放入下面这组箱子,允许多个记录具有相同的关键字(链表)
8
---------------------
| |1| |3|4| | | |8| |
---------------------
0 1 2 3 4 5 6 7 8 9
然后从左到右逐个扫描箱子,如果有关键字,则取出:1,3,4,8,8
代价:对于Key取值范围较大的记录进行Binsort,较为低效。放进箱子需要 $n$ 次,然后要扫描所有箱子,所以代价为:$\Theta(n + m)$,$n$ 是记录个数,$m$ 是箱子个数。
箱排序是稳定的。
基数排序(Radix Sort):对箱排序的改进。将任何一个 $r$ 进制的整数 key $\overline{K_{d-1}K_{d-2}\cdots K_0}$ 看成是由 $d$ 个分量组成,$K_{d-1},K_{d-2},\cdots ,K_0$,对每个分量进行箱排序。
示例:十进制,27,91,1,97,17,23,84,28,72,5,67,25,先按个位放入箱子
0
1->91->1
2->72
3->23
4->84
5->5->25
6
7->27->97->17->67
8->28
9
然后取出来:91,1,72,23,84,5,25,27,97,17,67,28
再按十位放入箱子
0->1->5
1->17
2->23->25->27->28
3
4
5
6->67
7->72
8->84
9->91->97
再取出来:1,5,17,23,25,27,28,67,72,84,01,07
代价:每次放入全部 $n$ 个元素,$r$ 进制则需要扫描 $r$ 个箱子,需要重复位数 $d$ 次。故代价为 $\Theta((n+r)d)$,$n$ 是记录个数,$d$ 是Key的位数,$r$ 是基数。
基数排序是稳定的排序。
排序算法性能比较
排序算法的理论下界
排序问题的上界是 $n \log n$。因为我们能找到的最好排序算法的平均情况下的算法复杂度是 $O(n \log n)$
我们将证明排序问题的理论下界也是 $n \log n$。
我们考虑排序的决策树(每个结点处进行一次比较,根据结果分为左右子树),其叶子数量为 $N!$(因为有 $N!$ 种可能结果),深度为 $d$ 的二叉树最多有 $2^{d+1}-1$ 个叶子,那反过来,$N!$ 个叶子的二叉树的深度至少是 $\lceil \log(N!+1) \rceil-1$,即 $\Omega (\log n!)=\Omega(n \log n)$
$$ 斯特林公式:n!\approx \sqrt{2\pi n} \left( \frac{n}{e} \right)^n $$所以排序所需的时间 $T=\Omega(n \log n)$
Chapter9 查找
定义:
Chapter10 索引
原文件在硬盘中太大,不方便直接查找,所以建立索引文件。
索引文件:
- 索引文件中记录了关键字和对应的数据记录存储在硬盘中的位置
- 索引文件比数据记录小的多,查找时被加载到内存中进行查找
- 针对不同关键字的索引文件,支持多关键字查找
线性索引
线性索引: 索引文件组织成二元组(关键字, 指向记录的指针) 的序列。
如果索引太大无法加载到内存,则建立二级索引。分段,取每一段的最小值重新构成序列。
不足之处:插入和删除操作会导致大量数据移动 $\Theta(n)$
二叉查找树 BST
二叉查找数:对每个结点,左子结点比它小,右子结点比它大
二叉平衡树:左右子树的高度差不超过1
二叉平衡树查找复杂度 $\Theta(\log n)$
不足之处:让BST保持平衡需要付出较高的代价,插入一个结点需要调整大量结点。
2-3树
2-3树的性质:
B+树
Chapter11 图
概念
图的定义:$G=(V,E)$
$V$ Vertex 顶点的非空有限集合(也就是一组顶点)
$E$ 无序集 $V\&V$ 的子集,其元素是图的弧(Arc)(也就是顶点对)
弧:表示两个顶点 $v$ 和 $w$ 之间存在一个关系,用顶点偶对 $
$ 表示 有向图:若图G的关系集合E(G)中,顶点偶对<v,w>的v和w之间是有序的,称图G是有向图。
- 称为弧尾(tail)或始点(initial node),w称为弧头(head)或终点(terminal node) ,就像向量。
无向图:若图G的关系集合E(G)中,顶点偶对<v,w>的v和w之间是无序的,称图G是无向图。
权(Weight):与图的边和弧相关的数。权可以表示从一个顶点到另一个顶点的距离或耗费。
路径:顶点序列 $v_1, v_2, …, v_n$ 被称为长度为 n-1的路径,其中 $v_i$ 到 $v_{i+1}$ $(1 ≤ i < n)$ 之间有连边
简单路径:路径中的所有顶点都是唯一的
环:长度不小于3的首尾相接的路径
简单环:除了第一个和最后一个顶点外,其余顶点都唯一的环是简单环
连通图 : 图中任何两个顶点之间都存在路径
连通分量:图中的极大连通子图(也就是让点形成的连通图最大)
参考:图的基本概念和遍历
图的实现
图的遍历
图的遍历:按照某特定顺序,依次访问图中的顶点,且每个顶点只访问一次。
- 深度优先搜索(Depth First Search ,DFS)
- 广度优先搜索(Breadth First Search, BFS)
- 拓扑排序(Topological Sort )
图的最短路径
常见求解问题:
- 单源最短路径: 求从某个顶点S到图中其他顶点的最短路径
- 所有点对最短路径: 求任意两个顶点之间的最短路径
定义:
- d(A, B) 从A到B的最短距离(最短路径长度)
- w(A, B) 是A到B的连边的权重。如果A到B没有连边,则定义 w(A, B) = ∞.
单源最短路径:Dijkstra算法
所有点对最短路径:Floyd算法
最小代价生成树
最小代价生成树(Minimum-Cost Spanning Tree ,MST) 问题:
- 输入: 一个无向连通图 G
- 输出: 满足以下要求的 G 的子图
- 该子图是包含所有顶点的连通图
- 该子图具有最小的代价(代价指的是图中的所有连边上的权重的总和)
有两种算法:
- Prim 最小代价生成树算法
- Kruskal 最小代价生成树算法