目录
设无向图 G 中有 n 个顶点 e 条边,则用邻接矩阵作为图的存储结构进行深度优先或广度优先遍历时的时间复杂度为 1 ;用邻接表作为图的存储结构进行深度优先或广度优先遍历的时间复杂度为 2 。
使用邻接矩阵作为存储结构,对于每个结点都要探测一遍所有的结点与自己有没有连接,所以时间复杂度为,使用邻接表只需要找到每个节点邻接的结点就可以,所以时间复杂度为
树
树的前中后序遍历
前序遍历 根左右 每次左右可以展开时进行替换
中序遍历 左根右
后序遍历 左右根
前中后序转化
前中后序转化看遍历方式决定的顺序结构
例如前序遍历的第一个元素一定是根节点 中序遍历的根节点的两边就是左右子树
树与二叉树的转化
树要变成二叉树,那就将树中的所有兄弟结点进行链接,然后每一层与上一层的连接只留下第一个结点的连接
二叉树要变成树,那就反方向来一次,将除了第一个结点的其他结点与根节点连接上,然后将兄弟结点连接,这时候二叉树就变回了原来的树
森林与二叉树的转化
森林转化为二叉树,森林是由若干个树组成的,可以理解为森林中的每棵树都是兄弟,我们先把森林中的每棵树转化成二叉树,然后将从第二个树起的每个结点作为上一个结点的右孩子
二叉树想转化成森林,先要看他可不可以转化,直接看根节点有没有右孩子,有就可以转化,先递归的将每个拥有右节点的根节点都断开 然后将二叉树再转化成树就成了森林
树与森林的遍历
树的遍历
树的遍历很简单,分为先根遍历与后根遍历
森林的遍历
森林的遍历也分为两种,分别是前序遍历与后序遍历,森林的前序遍历与二叉树的中序遍历相同,森林的后序遍历与二叉树的中序遍历相同
图
图的表示
邻接矩阵
typedef struct { Vertextype vexs[MAXVEX]; EdgeType arc[MAXVEX][MAXVEX]; int numNodes, numEdges; }MGraph;
邻接表
int EdgeType; typedef struct { int adjvex; EdgeType info; struct EdgeNode *next; }EdgeNode; typedef struct { VertexType data; EdgeNode *firstedge; }VertexNode,AdjList[MAXVEX]; typedef struct { Adjlist adjList; int numNodes,numEdges; }AdjList;
深度优先搜索
深度优先搜索也叫DFS,这种搜索如其名,深度优先,在走之前先确定一个方向,比如先访问最左边的,那就持续往前走,在未遇到过的结点的路中选择最左边的即可
//邻接矩阵 #define MAXVEX 9 Boolean visited[MAXVEX] void DFS(MGraph G,int i) { int j; visited[i] = True; printf("%c",G.vexs[i]) for(j = 0;j < MAXVEX;j++) { if(G.arc[i][j] == 1 && visited[j] == False) { DFS(G,j); } } } void DFSTraverse(MGraph G) { int i; for(i = 0;i < G.numvertexes;i++) { visited[i] = False; } for(i = 0;i < G.numvertexes;i++) { if(!visited(i)) { DFS(G,i); } } }
//邻接表 void DFS(GraphAdjList GL,int i) { EdgeNode *p; visited[i] = True; printf("%c",GL->adjlist[i].data); p = GL->adjlist[i].firstedge; while(p) { if(!visted[p->adjvex]) { DFS(GL,p->adjvex); } p = p->next; } } void DFSTraverse(GraphAdjlist GL) { int i; for(i = 0;i < GL->numvexes;i++) { visited[i] = False; } for(i = 0;i < GL->numvexes;i++) { if(!visited[i]) { DFS(GL,i); } } }
广度优先搜索
广度优先搜索,也叫BFS
核心思想是一层一层访问结点,使用的是一个栈来作为辅助存储,从入栈第一个节点开始,每次出栈一个结点,就将这个结点邻接的所有未访问顶点入栈,由此来遍历所有顶点
void BFSTraverse(MGraph G) { int i; Queue Q; for(i = 0;i < G->numvexes;i++) { visited[i] = False; } InitQueue(Q); for(i = 0;i < G->numvexes;i++) { if(!visited[i]) { visited[i] = True; printf("%c",G->vex[i]) EnQueue(&Q,i); while(!EmptyQueue(Q)) { DeQueue(&Q,&i); for(j = 0;j < G->numvexes;j++) { if(G.arc[i][j]&&!visited[j]) { printf("%c",G->vex[j]); EnQueue(&Q,j); } } } } } }
最小生成树
prim算法
简介
prim算法的核心就是迭代,从一个顶点开始构建生成树,每次讲代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
实现思想
创建两个数组,一个是标记是否加入的数组isjoin,一个是计算各节点加入最小生成树的最低代价的数组lowcost
在此之前先选取第一个结点,对此结点的相邻边进行遍历,将有权边加入到lowcost中供选择
第一轮循环遍历各个结点,找出lowcost最低的并且未加入树的顶点,将相邻结点加入isjoin数组中并开启下一轮遍历,更新还未加入的各个顶点的lowcost值
不断循环直至所有顶点都纳入为止
因为要进行n-1轮的循环,每次循环2n次
总时间复杂度是O(n^2)即O(|V|^2)
kruskal算法
简介
kruskal算法的核心就是全局里去找,每次选择一条权值最小的边,使这条边的两头连通(若原本已经连通则不选)直到所有顶点都连通
实现思想
初始时先将各条边按照权值进行排序
然后使用并查集方法检查是否已连通,若未连通则将新节点加入,一共要执行e轮,美伦判断两结点是否属于同一集合,需要O(log_2e)
最短路径
BFS求无权图的单源最短路径
简介
直接进行广度优先遍历
使用两个数组,一个记录最短路径值,一个记录到这个顶点的直接前驱
只能用无权图
迪杰斯特拉算法
简介
dijkstra算法是一种一步一步找出最短路径的方法,核心思路就是从初始点开始,一步一步从已确定路径中选取最短的路径作为新的最短路径,并加入新已确定顶点,然后执行多次
实现
我们选用三个数组,分别是标记各顶点是否已找到最短路径的finals,最短路径长度的dist,以及记录路径上的前驱的path
也就是我们每次将可到达的结点找出来,从可获取路径中找到最短路径,并将其前驱记录,标记出结点
时间复杂度为O(n^2)即O(|V|^2)
如果用于负权值带权图,则迪杰斯特拉算法可能会失效
弗洛伊德算法
简介
Floyd算法是求出每一对顶点之间的最短路径 使用动态规划思想,将问题的求解分为多个阶段
对于个顶点的图G,求任意一对顶点Vi一>Vj之间的最短路径可分为如下几个阶段: #初始:不允许在其他顶点中转,最短路径是? #0:若允许在Vo中转,最短路径是? #1:若允许在Vo、V1中转,最短路径是? #2:若允许在Vo、V1、V2中转,最短路径是? ...... #n-1:若允许在Vo、V1、V2.Vn-1中转,最短路径是?
例如这样,左边的矩阵就是初始时,不中转获得的个顶点建最短路径长度,右边的矩阵是初始时中转点的记录,因为不中转,所以是-1
若允许在V0中转,则新加一
如此经历n轮递推
woc,大道至简,本身以为是只有一个节点做中转的情况,但是仔细一想,它并不是单源的算法,而是点到点的算法,并且也从来不是每次加一个这么简单,他是考虑了所有的结点
就好比是需要经过 0 2 4 6才能到这个点,在查找时0->2是最小值不需要中转,0->4是经过2的中转,0到6是经过4的中转,但是到4的中转前已经中转过2了,所以这种算法已经考虑了所有的情况
DAG
简介
有向无环图简称DAG 图
DAG描述表达式
相同部分可以合并
节省存储空间
顶点中不能出现重复的操作数,标出来各个运算符的生效顺序,注意分层
拓扑排序
简介
拓扑排序就是找到做事的先后顺序
每个AOV网可能有一个或者多个拓扑排序
实现
①从AOV网中选择一个没有前驱(入度为0)的顶点并输出。 ②从网中删除该顶点和所有以它为起点的有向边。 ③重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
使用三个数组进行实现
分别是 记录当前顶点入度的数组indegree 记录拓扑序列的数组print 保存度为零的顶点栈s
逆拓扑排序
将拓扑排序中的入度更换成出度即可,使用邻接表不适合实现逆拓扑排序,应该使用逆邻接表或者邻接矩阵
查找
查找的衡量方法为平均查找长度
顺序查找
基本思想
是从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置。若查找到表的另一端,仍未找到符合给定条件的元素,则返回查找失败的信息。
折半查找
基本思想
就是二分法
散列表
基本概念
散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这些情况为冲突,这些发生碰撞的不同关键字称为同义词。 散列表建立了关键字和存储地址之间的一种直接映射关系。
散列函数不同,散列表不同
直接定址法
就是直接使用线性函数确定地址,一般不常见
除留余数法
确定一个数m,所有的数对设定的数m进行取余,取余后进行散列表的排序
解决冲突办法
如果有重复则将其使用向后推移并记录查找次数的方法进行储存,成为开放定址法,也可使用链表进行存储,称为拉链法
查找长度
散列表的查找长度取决于三个因素:散列函数、处理冲突的方法和装填因子。 装填因子a=表中记录数n/散列表长度m。 散列表的平均查找长度依赖于散列表的装填因子α,不直接依赖于n或m。 α越大,表示装填的记录越“满”,发生冲突的可能性就越大。
排序
插入排序
直接排序实际上就是进行比较后一步步替换
空间复杂度为O(1)
时间复杂度为O(n^2)-->两个嵌套for循环(平均)
稳定性 稳定 (遇到相同数字,相对位置保持不变)
希尔排序
希尔排序是通过一个常数d作为增量,然后对于相隔d个增量的记录作为子表进行排序,经过几次排序,使得整个表格基本有序后,对全体进行一次排序即可
因为同样使用常数个辅助单元,所以空间复杂度为o(1)
时间复杂度依赖于增量d,一般来说是不确定的,所以一般我们不去考虑
最后两个相同数字的相对位置也不能保证,所以稳定性也是不稳定的
交换排序
冒泡排序
不做解释,一一交换
空间 O(1)
时间O(n^2)
快速排序
快速排序是选取一个固定数,通常为第一个数,将小于这个数的跟不小于这个数的值进行排序,然后依次进行,是一个递归的过程
最坏时间复杂度是,相当于每次只能分出来一个数,需要都进行一遍
所以
空间复杂度是O(log_2n)
时1间复杂度是O(nlog_2n)
稳定性为不稳定
快速排序是所有内部排序算法中平均性能最优的算法
但是并不适用于本身就已经有了一定顺序的序列进行排序
选择排序
简单选择排序
就是遍历每个元素,在遍历到第i个元素时,选择从i到n的所有元素中最小的一个,将其与第i个元素进行交换
空间复杂度为O(1)
时间复杂度为O(n^2)
稳定性为不稳定
堆排序
对于二叉树的排序结果,我们可以根据根节点存放的是最大结点还是最小结点将堆分为大根堆与小根堆
对于大根堆小根堆的构造,都是从n/2开始进行的
对于堆的删除操作,就是将栈顶元素输出后再次进行构造
对于堆的插入操作,我们将其放入栈尾,再次进行构造
空间复杂度O(1)
时间复杂度O(nlog_2n)
稳定性 不稳定
归并排序
归并排序是将两个或两个以上的有序表组成一个新的有序表
例如二路归并排序,就是将元素两两组合并进行排序
空间复杂度是O(n)
时间复杂度是O(nlog_2n)
稳定性 为稳定
基数排序
不基于比较与移动进行排序,而是基于关键字各位的大小进行排序
空间效率 O(r)
时间复杂度O(d(n+r))
稳定性 稳定
各种排序算法的比较
问题
树的度为树中最大的度,例如二叉树的度为2

树中的指针域,看图理解即可
前后缀表达式
前缀表达式
首先先看,前缀表达式是从后往前算,遇到数字一个个放入栈中,遇到符号则拿出栈顶的元素进行计算,后进先算
后缀表达式
先入先出,从前往后进行计算,同样使用栈进行实现
二叉搜索树
(BST,Binary Search Tree),也称二叉排序树或二叉查找树。 二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:
-
非空左子树的所有键值小于其根结点的键值。
-
非空右子树的所有键值大于其根结点的键值。
-
左、右子树都是二叉搜索树。
二叉链表
用二叉链表存储哈夫曼树,有m个叶子结点,问哈夫曼树中总共多少个空指针域:
2m,叶子节点数*2
数据的物理存储结构
主要包括链式存储与顺序存储
二叉排序树插入新节点
时间复杂度为O(n),因为最差情况为单链
以链表为栈的存储结构出栈时
以链表为栈的存储结构出栈时必须判空,不需要判定满栈
顺序线性表插入

数据的最小单位
是数据项
归并排序落单
丢掉
substr(str,int,int)
意思是str的第int开始的int个字符
层次遍历初始堆
无法保证得到一个有序的序列,因为堆的兄弟结点之间无序
创建邻接表的时间复杂度
无向图中有n个结点e条边,建立该图邻接表的平均时间复杂度为O(n+e)
深度为k的完全二叉树中最少有2^{k-1}个结点
如上
一趟排序结束后不一定能选出一个元素在其最终位置上的排序算法
希尔排序,可能没有元素在最终位置上
连通图是无向图
连通图一定是无向图,所以深度优先遍历连通图一定能够访问到所有的顶点
链式栈的栈顶元素删除
删除栈顶元素操作序列 top = top->next
初始化堆
筛选法建初始堆必须从第个元素开始进行筛选,因为第
个元素都有孩子结点(对于所有的完全二叉树来讲都是这样)
新建元素
x = (类型)malloc(sizeof(元素))
散列函数p的选择
散列表中有m个元素,散列函数,对于p的选择采用小于等于m的最大素数
分块查找的平均查找长度
设顺序线性表的长度为30,分成5块,每块6个元素,如果采用分块查找,则其平均查找长度为()。
找块:(1+2+3+4+5)/5=3
在块内找元素: (1+2+3+4+5+6)/6=3.5
合计:3+3.5=6.5
单向链表在结点前插入新结点
在已知结点A前插入结点X,只有两个结点,故不能直接插入,应当将X插入A结点后,将两结点值进行互换,如p指向结点A,s指向结点X:
s->next = p->next;
p->next = s;
t = p->data;
p->data = s->data;
s->data = t;
顺序循环队列元素个数
设某顺序循环队列中有 m 个元素,且规定队头指针 F 指向队头元素的前一个位置,队尾指针 R 指向队尾元素的当前位置,则该循环队列中最多存储 _ 个队列元素。
存储m-1个,因为要额外留一个存储单元位置存储队头队尾指针
顺序表查找
指的是在顺序存储结构上进行查找(×)
顺序表查找是指从第一个元素开始查找一直到最后一个元素为止,所以它在链表上也可以进行
判断单链表
进行单链表相关函数时应当检测链表是否存在两个以上元素
LinkList * head = lklist;
if (head == 0 || head->next == 0)
{
return 1;
}
else
{
}
哈夫曼树带权路径长度
将哈夫曼树画出来后从0 1 2 3 4 层进行划分,将结点上的权值都加上去得到带权路径长度
线性探测次数
设有n个关键字具有相同的Hash函数值,则用线性探测法把这n个关键字映射到Hash表中需要做几次线性探测?
答案:(n*(n-1)/2)
线性探测是解决冲突的处理
第一次不算线性探测,那就是0+1+2+......n-1
如果算探测次数,那就是1+2+3+....n
排序次数最多最少
设需要对5个不同的记录关键字进行冒泡排序,则至少需要比较___4 ___次,至多需要比较_______10 ______次
最少就是插入排序或者冒泡排序,基本有序的情况下只需要比较四次
最多就是冒泡全逆序情况或者有序情况快速排序
深度/广度优先遍历使用邻接矩阵/邻接表的时间复杂度
设无向图 G 中有 n 个顶点 e 条边,则用邻接矩阵作为图的存储结构进行深度优先或广度优先遍历时的时间复杂度为 1 ;用邻接表作为图的存储结构进行深度优先或广度优先遍历的时间复杂度为 2 。
使用邻接矩阵作为存储结构,对于每个结点都要探测一遍所有的结点与自己有没有连接,所以时间复杂度为
,使用邻接表只需要找到每个节点邻接的结点就可以,所以时间复杂度为
![]()
堆排序要点
使用数组进行堆排序,记住当前结点/2为父节点
插入新结点重新进行堆排序时,可以将其储存在一个变量中,到最后再进行赋值,可以简化流程