目录
- 绪论
- 链表
- 栈和队列
- 串、广义表
- 树和二叉树
- 图
绪论
- 什么是数据结构
- 数据结构:相互之间存在一种或多种特定关系的数据元素的集合。
- 数据、数据元素和数据项的定义
- 数据:客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。
- 数据元素:是数据的基本单位,可分为若干。
- 数据项:组成数据元素、有独立含义的、不可再分的最小单位。
- 数据结构涵盖的内容
- 数据的逻辑结构: ——与计算机无关
- 集合(仅同属一个集合)
- 线性结构(1:1)
- 树结构(1:n)
- 图结构(m:n)
- 数据的物理(存储)结构:——与计算机有关
- 顺序结构
- 链式结构
- 索引结构
- 散列结构
- 数据的运算包含的内容:插入、删除、修改、查找、排序
- 算法的概念和特性
- 定义:算法是解决某一特定类型问题的有限运算序列,是一系列输入转换为输出的计算步骤。
- 基本特性:有穷性、确定性、可行性、输入和输出
- 算法设计的要求: 正确性、可读性、健壮性、效率与低存储量需求
- 算法效率的度量:
- 时间复杂度的度量
- 频度——即该语句重复执行的次数。
- 算法的运行时间由算法中所有语句的频度之和构成。
- 空间复杂度的度量
- 时间复杂度的度量
线性表
- 线性表的逻辑结构
- 线性结构:数据元素之间呈线性
- 线性表的定义和逻辑结构
- 定义:由n(n>=0)个数据特征相同的元素构成的有限序列
- 逻辑结构:数据元素虽然不同,但同一线性表中的元素必定具有相同的特征,即属于同一数据对象,相邻数据元素之间存在着序偶关系。数据之间存在线性关系
- 存在唯一的首、尾
- 首无前驱,尾无后继,其余均有且只有一个前驱和后继
- 线性表的顺序存储结构
- 表示方法:地址连续的存储单元依次存储线性表的元素
存储位置关系:第i个数据元素 a i a_i ai(l是元素占用存储单元)
L O C ( a i ) = L O C ( a 0 ) + ( i − 1 ) x l LOC(a_i) = LOC(a_0) + (i-1)xl LOC(ai)=LOC(a0)+(i−1)xl - 特点:可称为随机存储结构,可借助数组下表存取相应元素
- 基本操作要点:
- 存取查询注意输入位置的合法性
- 插入时表空间是否已满
- 插入和删除的关键逻辑:
// insert:移动留出空位
for(int j=L.length-1;j>=i-1;j--) L.elem[j+1] = L.elem[j];
// delete:移动向前覆盖
for(int j=i;j<=L.length-1;j++) L.elem[j-1] = L.elem[j];
利用关键的顺序存储和链式存储对比连接上下文:
3. 线性表链式存储结构
- 链式存储表示和特点:可连续也可不连续的任意存储单元存储线性表的数据元素
由于没有索引,每次取值只能遍历链表
-
头指值(反正指向第链表第一个节点)-> 头结点(可以存储也可以不存储的节点)-> 首元结点(链表第一个存储数据元素的节点)
-
其他链表形式
- 循环链表:终止条件为
p!=L
或者p->next!=L
- 双向链表:多了一个指向上一个元素的指针
prior
- 循环链表:终止条件为
-
练习题踩坑:创建表是不是有序的?有序还要单独考虑其排序过程的时间复杂度
-
算法设计题
栈和队列
- 栈
-
逻辑结构
- 定义:限定仅在表尾进行插入或者删除操作的线性表;先进后出
-
顺序存储:base始终指向栈底位置,初始化的时候也是给base动态分配空间,但是移动的时候是移动top指针
- 基本操作要点:
// 判断栈满
//1. 栈中元素个数
n = S.top - Sbase
//2. 是否满
S.top - S.base == S.stacksize;
// *S.top 指的是S.top指针指向的存储空间,可直接赋值
e = *--S.top;//出栈时返回栈顶元素
//栈空
S.top == S.base
-
链式存储
- 定义:链式存储,没必要设置头结点,只修改首元结点和尾节点
-
递归与迭代:前者是自身的不断调用,多为重复调用自己直到达成递归条件;后者多为循环体,每次循环的结果可作为下一次循环的输入。而递归的时间复杂度往往可以达到 O ( n 2 ) O(n^2) O(n2)
- 队列
- 循环队列:关于队头和对尾的操作都需要模运算(放止假溢出)
顺序存储
- 基本操作:Q.base是队列存储空间的基地址,取值都靠它
//判空,初始条件设置队头队尾均为0
Q.front = Q.rear
//满
(Q.rear+1)%Q.MAXSIZE == Q.front
//长度
(Q.rear - Q.front + Q.MAXSIZE) % Q.MAXSIZE
//入队
Q.base[Q.rear] = e; Q.rear = (Q.rear + 1)%Q.MAXSIZE;
//出队
e = Q.base[Q.front]; Q.front = (Q.front + 1)%Q.MAXSIZE;
链式存储
与顺序存储不同:出队时如果队列为空,需要将尾指针指向头指针,因此在进行出队/删除操作时,可能需要操作头/尾指针
- 解题技巧:对于递归函数或者求下表/次数,可以带入特殊值进行检验
串、数组与广义表
- 串术语:
- 字符串:即串,由0或多个有限字符组成地序列
- 串长:字符的数目
- 空白串/空串
- 子串:串中的任意连续字符组成的子序列
- 子串位置:以子串第一个字符在主串中的位置
- 串相等:当且仅当串的长度、各个位置对应字符都相等时
顺序存储:地址连续的存储单元存储串的字符串序列
链式存储:并不拘束于每个节点存储一个字符,通常用#
作为非字符
模式匹配算法
- BF算法(古典算法):循环执行对比操作
- 最好O(n+m)子串m和主串长度n?第一次成功
- 最差O(nxm)两个嵌套循环
- KMP算法
- 由子串求
- next
- 由子串求
* nextval:
* 第一个固定为0
* 第二个与第一个比较:相等为0,不等为1
* 后面:第n个与其next值对应位置的比较
* 相等,向前直到不等
* 不等时,比较的前一位的nextval值即为所求
* 尽头即为尽头的nextval
* 不相等:自己的next值
- 数组
- 定义:同类型的数据元素构成的有序集合
- 数组的地址计算
- 二维:默认为行,LOC(0,0)+(nxi+j)L(其实就是以哪个为参考就不让哪个乘以对应行/列数),坑:注意所求的L(n,m),与下表的展示范围有关系,实在理解不了还是动手画图,或者更有效的方法是特殊值法
- 多维
L O C ( j 1 , j 2 , . . . , j n ) = L O C ( 0 , 0 , . . . , 0 ) + ( ∑ i = 1 ( n − 1 ) j i ∏ k = i + 1 n b k + j n ) x L LOC(j_1,j_2,...,j_n) = LOC(0,0,...,0)+(\sum_{i=1}^(n-1)j_i\prod_{k=i+1}^nb_k + j_n)xL LOC(j1,j2,...,jn)=LOC(0,0,...,0)+(∑i=1(n−1)ji∏k=i+1nbk+jn)xL
- 特殊计算
- 对称矩阵:
- i>=j,k=i(i-1)/2 + j-1;
- j>i, k=j(j-1)/2 + i-1;
- 谁大谁先累加和
- 三角矩阵
- 对角矩阵
- 对称矩阵:
- 广义表
- 注意求表头表尾的区别
- 表头:非空广义表第一个元素(可以是元素也可以是广义表)
- 表尾:除了表头以外的所有元素构成的广义表
- 深度:括号的层数,递归广义表深度为无穷
树和二叉树
- 树
- 定义:非线性数据结构
- 术语:
- 结点
- 结点的度
- 树的度:各结点度的最大值
- 叶子
- 非终端结点/内部结点
- 双亲和孩子
- 兄弟
- 祖先
- 子孙
- 层次
- 堂兄弟
- 树的深度:节点的最大层次,高度是从下至上数
- 有序树:从左至右有序,不能互换
- 无序树
- 森林,互不相交的树的集合
- 二叉树
- 提示:二叉树可以为空
- 性质:
- 第i层至多 2 ( i − 1 ) 2_(i-1) 2(i−1)结点
- 深度为k的树最多有 2 k − 1 2^k-1 2k−1个结点(各层求和)
- n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1(本式无多少意义,因为是推导式)
完全二叉树:满二叉树从后往前少叶子结点
-
深度为 [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1:2^k-1 = n
-
除根结点外的结点双亲为i/2,2i>n时无左孩子否则左孩子为2i
-
2i+1>n说明没有右孩子,否则右孩子为2i+1(可以由左孩子性质推到)
-
顺序存储结构可以存,不过最好使用链式存储结构
-
遍历方式:注意法则,将法则下放到每个子树上更容易理解
- 前序:根左右
- 中序:左根右
- 后序:左右根
- 层序:按层次依次遍历
-
算法
- 遍历:前三种遍历方法(时间复杂度均为 O ( n ) O(n) O(n))使用递归方便理解;非递归需要借助栈
- 层次遍历非递归需要依靠队列来完成
- 复制:递归复制
- 深度:递归求和再比较
- 结点个数:
NodeCount(T->lchild) + NodeCount(T->rchild) + 1
- 线索二叉树(为了加快查找结点前驱或后驱的速度):lchild+LTag+data+RTag+rchild
- LTag和RTag取值决定了左右指针指向的是左右孩子(0)还是前驱以及后继(0)
- 树的存储结构
-
双亲表示:每一个存储其父结点,所以需要找到一个结点的子结点就需要遍历整个表
-
孩子表示法:一个结点存储的元素个数就是度数,会造成操作不易,而且所占空间较大
-
孩子链表法:先存储所有结点,然后给每个结点链接单链表,单链表为其所有子节点(从左到右),还可以增加双亲的位置
-
孩子兄弟表示法(又称为二叉树表示法/二叉链表表示法):左边指向左孩子,右边指向有兄弟,和森林转换二叉树有点像
-
森林与二叉树转换:
- 森林转换为二叉树:结点右孩子为兄弟结点,左孩子为左孩子。森林转换为二叉树时,森林中非终端结点个数之和+1=二叉树的右指针域为空的结点数之和
- 二叉树转换为森林:上述的逆方法
-
遍历方法
- 树:树的结点度数之和=树的结点个数-1
- 先根遍历:从上到下根优先(即二叉树的先序遍历)
- 后根遍历:从下到上(相当于中序遍历)
- 森林
- 先序遍历:对每棵树均依次进行先根遍历
- 中序遍历:对每棵树均依次进行后根遍历
- 树:树的结点度数之和=树的结点个数-1
- 哈弗曼树
- 哈弗曼树:最优树
- 路径
- 路径长度
- 树的路径长度
- 权
- 结点的带权路径长度
- 哈弗曼树
- 构造方法:集合中的元素组合,可以在每一次合成以后对新的元素集合排序,防止混淆
- 构造过程都是每次选取权值最小的树作为左右子树构成一棵新的二叉树,所以树中一定没有度数为1的结点
- 哈夫曼编码:在构成的哈夫曼树从根节点出发,左右子树编码不同,某结点编码即为路径上的编码总和
- 哈夫曼编码是前缀编码:任意构成的哈夫曼编码都不会与其他部分完全重叠;哈夫曼是最优前缀编码,带权路径之和最小
- 算法
- 存储结构:权值,双亲,左右孩子下标
- 解题思路
- 完全二叉树的结点度为1的只有0/1个
- 计算个数,遇事不决,使用特例验证结论
图
- 基本概念:G(V,E),V是有限非空点集,E是有限V的边集
- 术语:
-
完全图:任意两点之间都有边/弧,有向:(n)(n-1),无向:(n)(n-1)/2
-
稠密图:nlog2n>e,否则为稀疏图
-
权和图即带权图即网
-
邻接点,有边相连的点
-
度:入度和出度
-
路径和路径长度
-
回路/环
-
简单路径:顶点不重复出现
-
简单回路/简单环:除了顶点不重复出现的回路
-
连通、连通图和连通分量:无向图的任意两个顶点都连通即连通图,非连通图中的各个连通子图称为连通分量
-
强连通图和强连通分量:任意两个顶点都存在相互路径的有向图
-
连通图的生成树
-
有向图和生成森林
-
度与边的关系:
- n顶点,e边无向图:度数之和=2e
- n顶点,e边有向图:入度之和=出度之和=e
- 图的存储
- 邻接矩阵
- 有向图:y方向上顶点指出至x方向,有边则1,所以每行之和为顶点出度,列之和是该顶点入度
- 相对于无向图,不需要进行对称处理
- 无向图:对称矩阵,对角线上全为0,每行每列之和均为顶点的度数之和
- 构造流程:
- 输入点数、边数并根据顶点数初始化邻接矩阵
- 根据点数依次输入顶点信息
- 根据边数输入边的两个顶点(以及权值)
- 查找顶点在顶点表中的位置并返回作为索引给邻接矩阵赋值
- 同时对称赋值
- 构造流程:
- 有向图:y方向上顶点指出至x方向,有边则1,所以每行之和为顶点出度,列之和是该顶点入度
//查找位置方法
int LocateVex(G,v) {
int i;
for(i=0;i<G.vexnum && G.vexs[i]!=v;i++)
continue;
return i;
}
- 邻接表
- 顶点表+链结点:指针指向的是顶点所在的头表的索引
- 度数
- 无向图:每个顶点对应的:所有的链接点个数之和
- 有向图
- 出边:
- 入边(逆邻接表)
查找算法
int LocateVex(G,v) {
int i;
for(i=0;i<G.vexnum && G.vertices[i].data!=v;i++)
continue;
return i;
}
构造方法关键
//1. 找到输入顶点的位置i,j,生成边结点存入点序号,改变点表的头指针位置
p = new ArcNode;
p -> adjvex = j; //思考一下这里为什么是j
p -> nextarc = G.vertices[i].firstarc;
G.vertices[i].firstarc = p;
- 图的遍历:一定要注意是有向图还是无向图,有向图有方向
- DFS:深度优先遍历:某个结点出发一直一个方向遍历直至无,然后从另一个方向继续开始
- 邻接矩阵O(n^2)
- 非递归算法需要借助栈
- 辅助一维数组visited[n]:不断访问直到visited中的每个顶点都被标记
- 由访问的路径构成的树即为深度优先生成树
- 邻接矩阵O(n^2)
//关键条件:如果表内有值,而且visited数组并未修改,遍历访问
if((G.arcs[v][w]!=0)&&(!visited[w])) DFS_AM(G,w);
* 邻接表:O(n+e)
* 当不是连通图的时候,可能生成深度优先生成森林
* 时间复杂度+空间复杂度=>稀疏图用邻接表,稠密图用邻接矩阵
- BFS:广度优先遍历:一层一层的遍历
- 邻接矩阵
- 非递归算法需要借助队列
- 过程
- 打印顶点后顶点入队
- 循环判断队列是否为空,空已经遍历结束,否:元素出队,求其未被访问过的邻接点
- 输出,出队,visited置true
- 邻接表
- 先遍历第一个节点的所有链结点,再依次访问链结点对应顶点的链接点
- 邻接矩阵
- 最小生成树:生成树各边权值之和最小
- 克鲁斯卡尔算法(Kruskal):O(eln)
- 边出发:邻接表,适合稀疏图
- 从最小的边出发,相关的顶点的所有相关边中再循环找最小的边,但是不能出现环
- 普里姆(Prim):O(n^2)
- 顶点出发:邻接矩阵,适合稠密图
- 过程
- 最短路径
- Dijkstra算法:O(n^3)
- 新增顶点,新的出发点,新的边长,比较循环
- Floy算法
- 过程
- 所有的顶点之间建立二维表
- 每个顶点循环考虑顶点加入对路径的影响,更短的路径被记录
- 加入某点,则某点的行和列以及对角线的路径不用考虑
- 需要借助两个二维数组,两个数组元素要一一对应
- dist:存储路径长度
- path:存储路径
- 过程
- 拓扑排序
- AOV网:顶点表示活动,弧表示活动的优先关系的有向图
- 拓扑序列:AOV网所有顶点组成的线性序列,按照序列顺序安排(简言之,各个元素的执行有前提条件,必须在前提条件结束以后才能执行)
- 过程:(包括环的有向图是无法输出的)
- 找图中入度为0的结点,多个就随机弄一个。没有就无法。
- 输出这个结点以及删除所有与它相关边
- 再找入度为0的结点,循环输出的结点序列即为所求
- 算法思想
- 邻接表存储,顶点表包含入度、顶点、firstedge(指向该顶点的出度顶点)
- 入度为0的入栈,然后出栈,该顶点的firstedge指向的所有顶点的入度值减一
- 循环操作直到每个元素的入度为零
- 过程:(包括环的有向图是无法输出的)
- 关键路径
-
是整个工程的最短时间
- Vj的最早发生时间:Vj的前驱们的最长路线(可以理解为:前驱必须都完成,所以必须是最晚(路径最大)的那个)
- Vi的最迟发生时间:从后往前推推导(可以理解为:后继的完成的最优情况,即路径最小值)
- 关键路径的顶点:该顶点的最早和最晚发生时间都相同
-
题坑
- n个顶点的连通图至少(n-1)边,所以邻接矩阵至少有2(n-1)个非零元素
- 连通图是针对无向图,强连通图是针对有向图
- n个顶点的连通图至少(n-1)边,所以邻接矩阵至少有2(n-1)个非零元素