本文只梳理了基本概念(概念很重要),日后会更新练习题和算法题的归纳整理


绪论


1. 数据结构的定义:数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及它们之间关系和操作等的学科

2. 数据:对客观事物的符号表示,在计算机科学中指所有能输入到计算机中并被计算机程序处理的符号的总称如:数字、字符、声音、图形、图像等信息

3. 数据对象性质相同的数据元素的集合,是数据的一个子集

4. 数据元素(结点或记录)是数据的基本单位数据项(域)是数据的最小单位

5. 逻辑结构

  • 描述数据元素之间的逻辑关系
  • 与数据的存储无关,独立于计算机
  • 是从具体问题抽象出来的数学模型
  • 可分为 集合, 线性结构, 树形结构, 图形结构

6. 物理结构(存储结构)

  • 数据元素及其关系在计算机存储器中的存储方式

  • 数据结构在计算机中的表示

  • 可分为4大类:顺序、链式、索引、散列

7. 算法原地工作是指辅助空间不随着数据规模的增大而增大,不是说不需要辅助空间

8. 栈和队列属于逻辑结构而非存储结构,它们的实现才属于存储结构

9. 程序需要算法和数据结构结合在一起才能实现,仅仅把算法用某种计算机语言来描述不能称之为程序

10. 抽象数据类型的表示:数据对象 , 数据关系 ,基本操作

11. 算法的定义:对特定问题求解步骤的一种描述,是指令的有限序列

12. 算法的特性: 输入输出 , 有穷性 , 确定性 , 可行性

13. 评价算法优劣的基本标准

  • 正确性
    1、程序中不含语法错误;
    2、程序对于几组输入数据能够得出满足要求的结果;
    3、程序对于精心选择的、典型、苛刻且带有刁难性的几组输入数据能够得出满足要求的结果;
    4、程序对于一切合法的输入数据都能得出满足要求的结果。
    通常以第三层意义上的正确性作为衡量一个算法是否合格的标准
  • 可读性
    1、算法主要是为了人的阅读和交流,其次才是为计算机执行,因此算法应该易于人的理解;
    2、另一方面,晦涩难读的算法易于隐藏较多错误而难以调试。
  • 健壮性:
    1、指当输入数据 非法时,算法恰当的做出反应或进行相应处理,而不是产生莫名其妙的输出结果。
    2、处理出错的方法,不应是中断程序的执行,而应是返回一个表示错误或错误性质的值,以便在更高的抽象层次上进行处理
  • 高效性
    1.包括时间和空间两个方面;
    2.时间高效指算法设计合理,执行效率高,可以用时间复杂度来度量;
    3.空间高效是指算法占用存储容量合理,可以用空间复杂度来度量。
    4.两者都与问题的规模有关。

14. 算法的时间复杂度取决于问题的规模和待处理数据的状态;算法的时间复杂度是由嵌套最深层语句的频度决定的


线性表 栈和队列

线性表

1. 链表头节点的作用:如果没有头节点,那么在头部插入一个节点和在中间插入一个节点的操作是不一样的,如果加上头节点就可以去除这样的判断,代码更加简化了

2. 线性表最开始的节点没有前驱最后一个节点没有后继

3. 循环链表的优点:从任一节点出发都可以访问到链表中的每一个元素

4. 循环链表不同于循环队列,循环队列如果头指针和尾指针在最后重叠到了一起,无法判断队列已满,所以决定牺牲一个位置,让尾指针实际上为尾后指针

5. 顺序表和链表的比较
在这里插入图片描述


栈和队列

1.栈的定义限定仅在表尾进行插入和删除操作的线性表。允许插入和删除的一端称为栈顶,另一端称为栈底。空栈:不含任何数据元素的栈栈的操作特性:后进先出

2. 队列的定义只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。空队列:不含任何数据元素的队列

3. 链栈的表示:运算是受限的单链表,只能在链表头部进行操作,故没有必要附加头结点。栈顶指针就是链表的头指针

4. 涉及到表达式的题目,关键就在于考虑运算符的优先级,当前符号会把符号栈中优先级更高的元素全部执行完,然后自己进栈,这是不难理解的

5. 队列删除元素是在队首,添加元素是在队尾

6. 尾递归和单向递归可以使用循环来消除递归。尾递归就是递归语句在最后,当前环境的情况不需要保留。即使保留了也没什么用,所以不需要单独开辟堆栈来进行下一次递归,下一次的递归直接在本次递归的空间进行即可。注意这里说的是能够用循环消除递归的条件。不是说消除递归的条件,事实上任何递归都可以转化为非递归

7. 假溢出:头指针没有指向最开始的位置,尾指针指向最后位置,呈现出队满的假象,所以引入循环队列

8. 循环队列应该是用顺序存储机构,所以头尾指针的变化都是通过数的运算得来的,没有什么next指针

9. 循环队列中:

  • 队头指针表示真正的队头,队尾指针表示真正队尾的下一位置。
  • 初始化: front = rear = 0
  • 入队时,rear = (rear + 1) % Maxsize
  • 出队时,front = (front + 1) % Maxsize
  • 队空的条件:front = = rear
  • 队满的条件: front = = (rear + 1) % Maxsize

串,数组和广义表

1. 串的定义:即字符串,由零个或多个字符组成的有限序列,是数据元素为单个字符的特殊线性表串长: 串中字符个数(n ≥ 0)。 n = 0 时称为空串 ,  。空白串: 由一个或多个空格符组成的串

2. 串的术语子串: 串中任意个连续的字符组成的子序列。主串: 包含子串的串。字符位置: 字符在串中的序号。子串的位置: 子串的第一个字符在主串中的序号。串相等: 串长度相等,且对应位置上字符相等

3. 广义表常用术语长度: 广义表中元素的个数n 。n=0 时称为空表。深度: 定义为括号嵌套的最深层次。(a, (b,c,d))。表头: 对于任意一个非空广义表LS=(a1,a2,…,an),它的第一个数据元素a1被称为广义表的表头。表尾: 对于任意一个非空广义表,除表头外,由剩余数据元素构成的广义表(a2,a3,…,an)被称为广义表的表尾

4. 数组一旦被定义,它的维数和维界就不再改变。二维数组的行序优先表示: 行列索引下标为 i 和 j 对应的元素首地址为: LOC( i , j) = a + i * n + j


树和二叉树


1. 树中概念总结

  • 节点的度:一个节点含有的子树的个数成为该节点的度
  • 树的度:一棵树中,最大的节点度成为树的度
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点
  • 节点层次:根为1层,依次递增
  • 堂兄弟节点:父节点在同一层的节点互为堂兄弟
  • 节点祖先:从根到该节点所经分支上的所有节点,从根节点到此节点的路径中除了此节点本身,其余节点均为此节点的祖先节点,(儿子->父亲->父亲的父亲->…->根)
  • 子孙节点:以该节点为根的子树中的任一节点均为该节点的子孙节点
  • 深度:对于任意节点n,n的深度为从根到n的唯一路径长度,根的深度定为0
  • 高度:对于任意节点n,n的高度为从n到叶节点的最长路径长度,所有叶节点的高度为0
  • 森林:由m(m>=0)棵互不相交的树的集合成为森林
  • 有序树: 结点各子树从左至右有序,不能互换
  • 无序树: 结点各子树可互换位置

2. 树的定义: 树是由n个结点组成的有限集,且对于非空树:

  • 有且仅有一个特定的称为根的结点;
  • 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又是一棵树,称为根的子树。

3. 树的性质

  • 树有先根遍历和后根遍历两种方法,先根遍历:先访问树的根节点,再依次先根遍历子树;后根遍历:先依次后根遍历子树,再访问树的根节点。之所以没有中序遍历是因为树未必就是二叉树,子树有多个,可选择的序列也就有多个(比如根节点下有3个子树,分别为a,b,c,不考虑根节点的遍历顺序,仅这3个子树就是 3! 种遍历顺序),所以仅考虑是先遍历子树还是先遍历根节点这两种方式。而树的先根遍历与对应二叉树的中序遍历结果是一样的,理由我还没想懂。
  • 若二叉树采用二叉链表存储结构,要交换其所有分支结点左、右子树的位置,利用©遍历方法最合适。A.前序 B.中序 C.后序 D.按层次

4. 树的存储结构: 双亲表示法 , 孩子表示法 , 孩子兄弟表示法

二叉树

1. 二叉树的概念:n (n≥0)个结点的有限集合。当n=0时称为空树。
在一棵非空树T中:

  • 有一个特定的称为根的结点;
  • 除根结点之外的其余结点被分成两个互不相交的有限集合T1和T2,且T1和T2都是二叉树,并分别称之为根的左子树和右子树。

2. 二叉树或为空树;或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。

3. 二叉树的基本特点:

  • 结点的度小于等于2
  • 二叉树的子树有左右之分,其次序不能颠倒。如具有3个结点的二叉树有5种不同形态

4. 二叉树的性质:

  • 性质1在二叉树的第 i 层上至多有2i-1个结点, 第 i 层上至少有 1 个结点
  • 性质2: 深度为 k 的二叉树至多有2k - 1个结点, 深度为 k 的二叉树至少有k个结点
  • 性质3: 对于任何一棵二叉树T,如果其叶子结点数为n0,度为2的结点数为n2,则n0 = n2 + 1。
  • 性质4: 具有n个结点的完全二叉树的深度为 log2n下取整加一。
  • 性质5: 对于完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,右孩子编号必为2i+1,双亲的编号必为i / 2。

5. 特殊形态的二叉树

  • 满二叉树: 一棵深度为k 且有2k -1个结点的二叉树。
  • 完全二叉树: 深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满二叉树中编号从1至n的结点一一对应。(只有最后一层叶子不满,且叶子全部集中在左边)

6. 满二叉树的特点:

  • 叶子只能出现在最下一层
  • 只有度为0和度为2的结点
  • 在同样深度的二叉树中叶子结点(结点)个数最多

7. 完全二叉树的特点:

  • 叶子结点只能出现在最下两层,且最下层的叶子结点都集中在二叉树的左部;
  • 完全二叉树中如果有度为1的结点,只可能有一个,且该结点只有左孩子。
  • 深度为k的完全二叉树在k-1层上一定是满二叉树。

8. 满二叉树和完全二叉树的区别:

  • 满二叉树是叶子一个也不少的树,而完全二叉树虽然前n-1层是满的,但最底层却允许在右边缺少连续若干个结点。满二叉树是完全二叉树的一个特例。

9. 二叉树的遍历

  • 遍历定义 :
    • 指以一定的次序访问二叉树中的每个节点,并且每个节点仅被访问一次。
      • “访问”的含义:输出结点的信息
  • 遍历用途是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
  • 遍历方式 :
    • 先序遍历: 先根再左再右
    • 中序遍历: 先左再根再右
    • 后序遍历: 先左再右再根

10. 若二叉树中各结点的值均不相同,则:

  • 由二叉树的先序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树
  • 但由先序序列和后序序列却不一定能唯一地确定一棵二叉树。

11. 用二叉链表(l_child, r_child)存储包含n个结点的二叉树,结点的指针域中有 n + 1个空指针


二叉树,树,森林转化

1. 将树转换成二叉树:
  加线:在兄弟之间加一连线;
  抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系;
  旋转:将同一孩子的连线绕左孩子旋转45度角。

  树转换成的二叉树其右子树一定为空

2. 将二叉树转换成树:
  加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子,……沿分支找到的所有右孩子,都与p的双亲用线连起来;
  抹线:抹掉原二叉树中双亲与右孩子之间的连线;
  调整:将结点按层次排列,形成树结构

  操作要点:把右孩子变为兄弟
在这里插入图片描述
在这里插入图片描述
3. 二叉树转换成森林:
  抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树
  还原:将孤立的二叉树还原成树

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


哈夫曼树

1. 哈夫曼树的相关术语:

  • 路径从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
  • 路径长度:路径上的分支数目
  • 带权路径长度:结点到根的路径长度与结点上权的乘积
  • 树的带权路径长度:树中所有叶子结点的带权路径长度之和

2. 哈夫曼树性质
  一般所说的哈夫曼树只有度为0和2的节点度为m的哈夫曼树:只有度数为有0和m的节点
哈夫曼树不一定是完全二叉树

3. 哈夫曼树的构造过程:
  操作要点:对权值的合并、删除与替换,总是合并当前值最小的两个
在这里插入图片描述


小结

在这里插入图片描述


图的概念和性质

  • 图的定义:
    图G由两个集合V和E组成,记为G=(V,E),其中V是顶点的有穷非空集合,E是边的集合。
    注意: 在图中,顶点个数不能为零,但边数可以为零。
  • 完全图: 任意两个点都有一条边相连。
    无向完全图: n (n - 1) / 2 条边 ; 有向完全图 :n (n - 1) 条边
  • 路径长度: 路径上边或弧的数目。
    回路(Cycle) : 第一个顶点和最后一个顶点相同的路径。
    简单路径 : 序列中顶点不重复出现的路径。
    简单回路(简单环) : 除路径起点和终点相同外,其余顶点均不相同的路径。
  • 连通图
    • 在无向图中,如果从一个顶点vi到另一个顶点vj(i≠j)有路径,则称顶点vi和vj是连通的。
    • 如果图中任意两个顶点都是连通的,则称该图是连通图。
  • 连通分量
    • 无向图中的极大连通子图称为连通分量。
    • 极大连通子图
      • 子图是图 G的连通子图,将G 的任何不在该子图中的顶点加入,子图不再连通。
  • 强连通图
    在有向图中,对图中任意一对顶点vi和vj (i≠j),若从顶点vi到顶点vj和从顶点vj到顶点vi均有路径,则称该有向图是强连通图。
  • 极小连通子图
    • 子图是G 的连通子图,在该子图中删除任何一条边,子图不再连通。
  • 生成树
    • 包含无向图G 所有顶点的极小连通子图。
  • 生成森林
    • 在非连通图中,由每个连通分量都可以得到一棵生成树,这些连通分量的生成树就组成了一个非连通图的生成森林。

2. 邻接矩阵表示法的特点

  • 优点:

    • 容易实现以下操作
      • 求某顶点的度
      • 判断顶点之间是否有边
      • 找顶点的邻接点等等。
  • 缺点

    • 对稀疏图而言尤其浪费空间。
    • n个顶点需要n*n个单元存储边;空间效率为O(n2)。

3. 邻接表表示法的特点

  • 优点

    • 便于增加和删除顶点
    • 便于统计边的数目
    • 空间效率高,容易寻找顶点的邻接点;
      • 对于n个顶点e条边的无向图,邻接表中除了n个头结点外, 只有2e个表结点,空间效率为O(n+2e)。
  • 缺点

    • 判断两顶点间是否有边或弧,需搜索两结点对应的单链表,没有邻接矩阵方便。
    • 不便于计算各个顶点的度。

在这里插入图片描述
4. DFS
基本思想

  • 从图的某一顶点v出发,访问此顶点;
  • 然后依次从v的未被访问的邻接点出发,深度优先遍历图,直至图中所有和v相通的顶点都被访问到;
  • 若图中尚有顶点未被访问,则另选图中一个未被访问的顶点作起点,重复上述过程,直至图中所有顶点都被访问为止。
    在这里插入图片描述

4. 广度优先搜索
在这里插入图片描述
在这里插入图片描述

关键路径

关键路径不唯一时,一条关键路径上的活动提前完成只能使得这条关键路径变为非关键路径,其它关键路径没受到影响,所以整个工程未必就会提前完成。

事件 vj 的最早发生时间Ve(j)
 从ve(0)=0开始向前递推 :ve(j) = Max { ve(i) + dut(<i, j>) } <i, j>∈T
  事件 vi 的最迟发生时间 Vl(i):
  从vl(n-1)=ve(n-1)起向前递推: vl(i) = Min { vl(j) - dut(<i, j>) } <i, j> ∈S
  活动as的最早开始时间 e(s):
  若该活动在弧<vi, vj>上,则活动as的最早开始时间应等于事件vi的最早发生时间: e(s) = Ve(i)
  活动as的最迟开始时间 l(s):
   1)在不推迟整个工程完成的前提下,该活动最迟必须开始进行的时间。
   2)若as由弧<vi,vj>表示,则as的最晚开始时间要保证事件vj的最迟发生时间不拖后。因此有: l(s) = Vl(j) - dut (as)
  关键活动: l[i]=e[i]的活动

易错点

1. 强连通分量是有向图的概念;连通分量是无向图的概念

2. 邻接多重表是无向图的概念,它的引入就是为了解决邻接表存储无向图时的一条边的重复存储的问题

3. 十字链表是有向图的存储结构

4. 连通图才存在生成树,非连通图可以生成森林

5. 不同的求最小生成树的方法最后得到的生成树是相同的,这句话是错的,原因是如果存在相同权值的边,结果未必唯一。如果能保证权值均不相同则结果唯一

6. 求最小生成树的普里姆(Prim)算法中边上的权可正可负。这道题的结果存在异议。

7. AOV(A:Activity活动 O:on位于 V:Vertex顶点)AOV网中,结点表示活动,边表示活动间的优先关系,边无权值,即拓扑排序网络

8. AOV网特点:有且只有一个表示开始状态的初始点和一个表示结束的终点,始点入度为0,终点出度为0

9. AOE(E:edge边) 中,结点表示活动开始或结束的状态,边上的权表示活动进行所需时间,关键路径就是AOE网中的概念

查找

顺序查找

1. 顺序查找

  • 查找过程: 从表的一端开始逐个进行记录的关键字和给定值的比较
    • 应用范围:
    • 顺序表或线性链表表示的静态查找表
      • 表内元素之间无序

2. 顺序查找的性能分析

  • 空间复杂度: 一个辅助空间O(1)
  • 时间复杂度 : O(n)
  • 平均查找长度
  • 查找成功时的平均查找长度
    • 设表中各记录查找概率相等
    • ASL=(1+2+ … +n)/n =(n+1)/2
  • 查找不成功时的平均查找长度
    • ASL=n+1

3. 顺序查找算法的特点

  • 优点
    算法简单,对表结构无任何要求,即适用于顺序结构,也适用于链式结构,无论记录是否按关键字有序均可应用。
  • 缺点
    平均查找长度较大,查找效率较低
    当n很大时,不宜采用顺序查找

折半查找

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分块查找

分块查找性能评价

  • ASLbs=Lb+Lw
    在这里插入图片描述

哈希函数的方法

  • 直接定址法:例如使用一个简单的线性函数hash(key) = key - 1000,由于key是没有冲突的,按照这种方法计算处的hash(key)自然不会产生冲突,这也是它的优点,缺点是随着key范围的增大,hash(key)的增大不能满足现实需求
  • 数字分析法
  • 折叠法:适用范围是事先不知道关键字分布,且位数较多
  • 除留取余法:关键讨论的方法

冲突解决方法

  • 开放地址法 (Hash( Key )+ di) mod m
  • 线性探测 di=1,2,3…m−1
  • 二次探测 di=12,−12,22,−22,…+−k2(k<=m / 2)

易错点:1. 注意mod的是m(表长),而不是求哈希值mod的那个东西了 2. 公式是基于hash(key)进行变化的,不是对原数进行变化

  • 再哈希法:就是对出现的重复的哈希值再进行一次运算

  • 链地址法:类似邻接表的形式,把所有映射值相同的元素都放到内部一个链表中

  • 公共溢出区法:把出现冲突的数据直接放在另一个空间中存储起来
    在这里插入图片描述

概念

  • AVL树:自平衡二叉查找树,一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
    节点的平衡因子:节点的左子树的高度减去它的右子树的高度

  • 装填因子:α=填入表中记录个数 / 哈希表长度,α越大,产生冲突的可能性越大,平均查找长度也就越大

  • 二叉排序树:若左子树不为空,则左子树上节点的值均小于根节点,若右子树不为空,则右子树上节点的值均大于根节点,左右子树均为二叉排序树(这是一个递归的定义)
    在这里插入图片描述

排序

排序算法分类

插入排序: 直接插入;希尔排序; 折半插入排序
选择排序: 直接选择;堆排序;
交换排序: 冒泡排序;快速排序;
归并排序:2-路归并排序
基数排序

不稳定排序:一 堆 希尔 快 选

时间复杂度总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

与数组初始状态无关的算法

1. 时间复杂度和初始状态无关:堆,归并,选择,基数

2. 比较次数和初始状态无关:选择,基数

3. 移动次数和初始状态无关:归并,基数

情况2、3是必定包含在情况1中间的

  • 堆排序

    • 思想:首先对初始数组建立最小堆,然后取堆顶元素与堆尾交换,再此堆元素(不含堆尾)再重新构建最小堆,依次循环。
    • 分析:由于建立最小堆其实就是将初始元素按照规定的准则进行一系列排序(包括层级向下比较、交换),所以如果元素一开始就已经是最小堆则不需要此时的交换且大大减少向下比较次数,
      所以堆排序不属于情况二也不属情况三
  • 归并排序

    • 思想:将初试数组划分成N个子数组,两两进行合并排序,然后结果再和其他同级合并后的数组合并知道合并完所有。
    • 分析:外层递归与初始无关,主要思考合并排序中的比较和交换即可。合并排序思想:将数组A第一个与数组B第一个比较,较小的那一个直接进入result数组并且指针向下移动再与对面数组第一个比较,依次类推,然后将还有剩余的数组内元素全放入result,最后用result将原数组中对应的值一一替换。因此,假设初始数组就是有序的,那么每次合并排序的时的比较次数都仅仅是一个待合并的数组的长度,因此比较次数与初始状态有关,归并排序不属于情况二   
      然而,不论一开始的状态如何,最后都是两个数组进入result,移动次数都为两个待合并数组的长度和,然后再将result内元素全部移动到原来数组进行替换。所以元素移动次数与初始状态无关,归并排序属于情况三
  • 选择排序

    • 思想:i 从头开始,每次遍历之后所有的元素,k 从 i 开始,向后标记最小的元素,循环后如果大于 i ,则与 i 位置元素交换,一直到最后。
    • 分析:比较次数都是N-1的阶乘,与初始状态无关,所以选择排序属于情况二
      交换次数当全部已经排序好时则不发生交换,所以选择排序不属于情况三
  • 基数排序

    • 思想:将数组从低位到高位,每到一位对应分入10个桶(0-9)中,依次到最高位,由于每上升一位,处于“0号桶”中的数据都会将此位之前的数字排好,以此达到排序效果。
    • 分析:基数排序中并不发生任何元素之间的比较,所以基数排序属于情况二
      不论初始数组如何排列,都是从个位开始,各自进入自己个位对应的位置,之后也都是一样,所以元素移动次数一样,所以基数排序属于情况三

相关算法实现

堆排序

public static void heapSort(int []arr,int len) {
		for(int i =len/2;i>0;i--) {
			heapAdjust(arr,i,len);
		}
		
		for(int i=len;i>0;i--)//需要交换几次位置的次数 
		{
			//下面三行的代码是把堆顶最大的元素和堆尾最后一个元素换位置
			//这样一来,最大元素就在数组尾部了,
			//因此大顶堆 是用来从小 到大排序的 
			int temp=arr[1];
			arr[1]=arr[i];
			arr[i]=temp;
			
			//对堆剩下的元素继续排序。 
			heapAdjust(arr,1,i-1);
		}
		
	}

	private static void heapAdjust(int[] arr, int first, int last) {
			int temp=arr[first];//暂存“根”结点 
			int j;//子结点 
			for(j=2*first;j<=last;j=j*2)
			{
				//下面if语句的作用是找出子结点中比较大的那个 
				//j是左节点,j+1是右节点,
				//如果右节点大,那j+1就可以了,如果左节点大那就不用+1
				//执行完下面的语句,j下标是较大的那个子结点的下标 
				if(j<last &&arr[j]<arr[j+1])
					j++;
				
				//下面if语句的作用是如果“根”结点大于子结点,
				//结束查找即可
				if(temp>arr[j])
					break; 
				
				//理解下面两条语句可以类比插入排序,
				//还记得插入排序中的元素后移吗? 这里是“下移” 
				arr[first]=arr[j];
				first=j; //如果下移,记录对应的下标,方便下次下移 
			}
			//同样类比插入排序,把要插入的元素,放到合适的位置 
			arr[first]=temp; 
	}

快速排序

public static void quickSort(int[] arr, int l, int r) {
		if (l >= r)
			return;
		int i = l - 1, j = r + 1, x = arr[l + r >> 1];
		while (i < j) {
			do {
				i++;
			} while (arr[i] < x); // 在左边大于分界点的数停下
			do {
				j--;
			} while (arr[j] > x); // 在右边小于分界点的数停下
			if (i < j) { // 交换两个数
				int temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}
		quickSort(arr, l, j); // 递归处理左边
		quickSort(arr, j + 1, r); // 递归处理右边
	}

希尔排序

/*
     * 算法实现思路:
     * 在插入排序的基础上增加变动增量:increment = increment/3+1(初始是len);直到增量等于零时算法结束
     * 每次从增量开始循环到len;
     * 若当前位置减去增量后的位置有更大的值则一定会交换;
     * 临时变量记录当前位置值; 再记录当前位置减去增量的位置index;
     * 往前延伸到大于等于零或者前面还有更大的数为止;
     * 交换时要有回退和前沿只用index增减增量即可。
     */
	public static void shellSort(int []a,int len) {
		int increment = len; //增量
		int temp;
		do {
			increment = increment/3+1;  //增量表达式求解
			for(int i=increment;i<len;i++) {
				if(a[i-increment]>a[increment]) { //如果当前位置的前面有更大的值则一定会交换
					temp = a[i];
					int index = i-increment; //类似插入排序
					while(index>=0 && a[index]>temp) {
						a[index+increment] = a[index];
						index-=increment;
					}
					a[index+increment] = temp;
				}
			}	
		}while(increment>1);//最后一轮是增量等于一的时候
	}

归并排序

public static void merge(int[] arr,int low,int mid,int high,int[] tmp){
		int index = 0;
		int j = low,k = mid+1;  //左边序列和右边序列起始索引
		while(j <= mid && k <= high){
			if(arr[j] < arr[k]){
				tmp[index++] = arr[j++];
			}else{
				tmp[index++] = arr[k++];
			}
		}
		//若左边序列还有剩余,则将其全部拷贝进tmp[]中
		while(j <= mid){    
			tmp[index++] = arr[j++];
		}
		
		while(k <= high){
			tmp[index++] = arr[k++];
		}
		
		for(int t=0;t<index;t++){
			arr[low + t] = tmp[t]; //标记其在原数组中的相对位置
		}
	}
	public static void mergeSort(int[] arr,int low,int high,int[] tmp){
		if(low<high){
			int mid = low+high >> 1;
			mergeSort(arr,low,mid,tmp); //对左边序列进行归并排序
			mergeSort(arr,mid+1,high,tmp);  //对右边序列进行归并排序
			merge(arr,low,mid,high,tmp);    //合并两个有序序列
		}
	}

基数排序

public static void countingSort(int []a,int len) {
		
		//找出最大值和最小值
		for(int i=0;i<len;i++) {
			if(a[i]>=MAX) {
				MAX = a[i];
			}
			if(a[i]<=MIN) {
				MIN = a[i];
			}
		}
		
		int l = MAX-MIN+1; //变量l是要开辟数组的长度
		int []count = new int[l];
		Arrays.fill(count, 0);
		
		//统计某个值出现的次数,统计时要减去数组最小值,方便存储
		for(int i=0;i<len;i++) {
			count[a[i]-MIN]++;
		}
		
		//累加
		for(int i=0;i<l;i++) {
			count[i]+=count[i-1];
		}
		
		int []temp = new int[len]; //用来排序好的数组
		Arrays.fill(temp, 0);
		
		for(int i=len-1;i>=0;i--)//逆序遍历。 
		{
			temp[count[a[i]-MIN]-1]=a[i];
			count[a[i]-MIN]--;
		}
		//经过上面的逆序遍历,现在temp数组就是排序好的成绩数组 
		
		//把排序好的数组放到arr数组中 ,方便后面的打印 
		for(int i=0;i<len;i++)
			a[i]=temp[i];
	}

迪杰斯特拉算法

public static void dijkstra(int k) { // k起始节点

		for (int i = 1; i <= n; i++) { // 初始化dis数组 值为k到各节点的距离。
			Dijkstra.dis[i] = Dijkstra.map1[k][i];
		}
		// 单源节点只需遍历n-1遍
		for (int i = 1; i <= n - 1; i++) { // 找n-1轮
			int minx = INF, u = 0;
			for (int j = 1; j <= n; j++) { // 找与上一个点最近的点
				if (Dijkstra.book[j]  false && Dijkstra.dis[j] < minx) {
					minx = Dijkstra.dis[j];
					u = j;
				}
			}

			Dijkstra.book[u] = true;
			for (int k1 = 1; k1 <= n; k1++) { // 找着最近的点后通过这个最近的点更新其余的点到起点的距离
				if (Dijkstra.book[k1]  false && Dijkstra.dis[u] + Dijkstra.map1[u][k1] < Dijkstra.dis[k1]) {
					Dijkstra.dis[k1] = Dijkstra.dis[u] + Dijkstra.map1[u][k1];
				}
			}
		}
		for(int i=1;i<=n;i++) {
			if(dis[i]  INF)
				System.out.print("∞ ");
			else
				System.out.print(dis[i]+" ");
		}
		System.out.println();
	}

哈夫曼树

// 选择权值最小的两颗树
void Select(HuffmanTree H,int n,int &s1,int &s2)
{
    s1 = s2 = 0;
    int t1 = MAX,t2 = MAX;
    for(int i=1;i<n;i++)
    {
        if(!H[i].parent){
            if(H[i].weight<t1){
                t1 = H[i].weight;
                s1 = i;
            }
        }
    }
    for(int i=1;i<n;i++)
    {
        if(!H[i].parent){
            if(H[i].weight  t1)
                continue;
            else if(H[i].weight<t2){
                t2 = H[i].weight;
                s2 = i;
            }
        }
    }
}

// 构造有n个权值(叶子节点)的哈夫曼树
void CreateHufmanTree(HuffmanTree &H)
{
    int n, m;
    cin >> n;
    m = 2*n - 1;

    H = new HNode[m + 1];    // 0号节点不使用
    for(int i = 1; i <= m; ++ i)
    {
        H[i].parent = H[i].lChild = H[i].rChild = 0;
    }
    for(int i = 1; i <= n; ++ i)
    {
        cin >> H[i].weight;    // 输入权值
    }
    H[0].weight = m;    // 用0号节点保存节点数量

    /****** 初始化完毕, 创建哈夫曼树 ******/
    for(int i = n + 1; i <= m; ++ i)
    {
        int s1, s2;
        Select(H, i, s1, s2);
        H[s1].parent = H[s2].parent = i;
        H[i].lChild = s1;
        H[i].rChild = s2;    // 作为新节点的孩子
        H[i].weight = H[s1].weight + H[s2].weight;    // 新节点为左右孩子节点权值之和
    }
    cout<<H[1].weight;
    for(int i=2; i<=m; i++)
        cout<<" "<<H[i].weight;
    cout<<endl;
}

// 构造根据哈夫曼树来哈夫曼编码
void CreateHuffmanCode(HuffmanTree H, HuffmanCode &hC)
{
    hC.n = (H[0].weight+1)/2;
    hC.code = new char*[hC.n + 1];    // 0位置不使用
    char *cd = new char[hC.n+1];  // 临时存放每个编码

    for(int i = 1; i <= hC.n; ++ i)
    {

        // 每次从叶子节点向上回溯构造编码
        int len = 0, child = i, parent = H[i].parent;
        while(parent != 0)
        {
            if(H[parent].lChild  child)
            {
                cd[len ++] = '0';
            }
            else
            {
                cd[len ++] = '1';
            }
            child = parent;
            parent = H[parent].parent;
        }
        reverse(cd, cd + len);    // 将序列翻转
        hC.code[i] = new char[len];
        strcpy(hC.code[i], cd);
    }
    delete[] cd;
}
//递归实现
public static void main(String[] args) {
		sc = new Scanner(System.in);
		int n = sc.nextInt();
		for (int i = 0; i < n; i++) {
			a[i] = sc.nextInt();
		}
		Arrays.sort(a, 0, n);
		a[1] += a[0];
		ans = a[1];
		if (n > 2) {
			dfs(1, n);
		}
		System.out.println(ans);
	}

	public static void dfs(int i, int n) {
		if (n - i  2) {
			a[n - 1] += a[n - 2];
			ans += a[n - 1];
			return;
		}
		Arrays.sort(a, i, n);
		a[i + 1] += a[i];
		ans += a[i + 1];
		dfs(i + 1, n);
	}

图论代码部分

(下标从一开始)

邻接表

// idx = 0; e,ne 空间均开总边数
void add(int a, int b, int c)
{   // h 表示头节点 e 表示下一个节点  ne 表示下一个结点的下一个节点(尾节点)
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;  // idx 为索引下标 作为指针联接纽带
}
// memset(h, -1, sizeof h); // 表头的初始化

最短路径

  • 朴素版迪杰斯特拉算法(On^2)(无负权边)

算法步骤:

  1. 初始化距离: dis[i] = Integer.Max_Value,dis[1] = 0;

  2. for i : 1 ~ n // 枚举 n 个节点,每次找到距源点 s 距离最短的节点

​     for v : 1 ~ n

​        t <= 不在 s 中的距离源点最近的点

​     if (t == n) break; // 提前到 n 节点

​      st <= t

​      for v : 1 ~ n

​        用 t 更新其它点的距离:dis[j] = min(dis[j], dis[t] + g[t][j])

// g 邻接矩阵存图 
// dis 表示 单源点到各个节点的最短距离
// st 表示 到该节点的最短路 是否已确定
int dijkstra()
{   // 初始化距离
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;  // 单源节点到自身距离为0

    for (int i = 1; i <= n; i++)
    {
        int t = -1; // 表示还没有确定
        for (int j = 1; j <= n; j++)
            if (!st[j] && (t == -1 || dis[t] > dis[j]))  // st[j] 未确定 下 t 也未确定 或 比dis[j]大
                t = j;  // 更新为较小的节点
        if(t == n) break;  // 提前找到了
        st[t] = true;  // 每轮循环确定一个状态 t 并加入集合 st 中
        for (int j = 1; j <= n; j++)  
            dis[j] = min(dis[j], dis[t] + g[t][j]);
    }

    if (dis[n] == 0x3f3f3f3f)
        return -1;
    return dis[n];
}
// memset(g, 0x3f, sizeof g);
// g[a][b] = min(g[a][b], c); // 去除重边
  • 堆优化版迪杰斯特拉算法(O mlogn)
// 带有边权的邻接表加边模板(稀疏图)
public static void add(int a, int b, int c)
{
    e[idx] = b;  // 存放表头索引指向的下一个节点
    w[idx] = c;  // 对应的边权
    ne[idx] = h[a]; // ne 存的也是索引值
    h[a] = idx++;  // 表头存的是索引
}
// 堆优化版迪杰斯特拉算法
public static int dijkstra(int s)
{   // 初始化距离
    Arrays.fill(dis, 0x3f);
    dis[s] = 0;  //   dis 表示各个节点到源点的最短距离
    // 构造小顶堆(精简版)
    PriorityQueue<int[]> heap = new PriorityQueue<int[]>((o1,o2)->(o1-o2));
    heap.offer(new int[]{s,0});
    while(!heap.isEmpty()){
        int[] node = heap.poll();
        int ver = node[0];
        int dist = node[1];  // dist 表示源点到 ver 节点的最短距离
        if(st[ver]) continue;
        st[ver] = true;
        // 遍历邻接表
        for(int i = h[ver]; i != -1; i = ne[i]){
            int j = e[i];
            if(dis[j] > dist + w[i]){  // w[i] 即为g[ver][j] 边权值
                dis[j] = dist + w[i];
                heap.offer(new int[]{j,dis[j]});            
            }                    
        }                                        
    }
    if (dis[n] == 0x3f3f3f3f)
        return -1;
    return dis[n];
}
  • SPFA算法(一般Om,最坏Onm)
  1. 初始化距离数组dis为INF(0x3f),并置dis[s] = 0。

  2. 将源点加入队列,并标记该节点在队列中。

  3. 当队列不空时,取队头元素进行拓展(遍历邻接表),将从源点拓展出去的距离更小的节点加入队列(首先拓展节点不在队列中)。

public static int SPFA(int s){
    Arrays.fill(dis, 0x3f);
    dis[s] = 0;
    Queue<Integer> queue = new LinkedList<Integer>();
	queue.offer(s);
	st[s] = true; // 当前的节点是否在队列中

	while (!queue.isEmpty()) {
		int ver = queue.poll();
		st[ver] = false;

		for (int i = h[ver]; i != -1; i = ne[i]) {
			int j = e[i];
			if (dis[j] > dis[ver] + w[i]) {
				dis[j] = dis[ver] + w[i];
				if (!st[j]) {
					queue.offer(j);
					st[j] = true;
				}
			}
		}
	}
	if (dis[n] == 0x3f)
		return -1;
	return dis[n];
}
  • SPFA判断负环

    dis[x] 表示 单源点s 到 x 的最短距离 dis[x] = dis[t] + w[i]

    cnt[x] 表示 从 s 到 x 经过的边数 cnt[x] = cnt[t] + 1

    若cnt[x] >= n 则表示存在负环 n 表示总节点数

public static boolean SPFA(){
    Queue<Integer> queue = new LinkedList<Integer>();
    for(int i = 1; i <= n; i++){
        queue.offer(i);
	    st[i] = true; // 当前的节点是否在队列中    
    }
	while (!queue.isEmpty()) {
		int ver = queue.poll();
		st[ver] = false;

		for (int i = h[ver]; i != -1; i = ne[i]) {
			int j = e[i];
			if (dis[j] > dis[ver] + w[i]) {
				dis[j] = dis[ver] + w[i];
                cnt[j] = cnt[ver] + 1;
                if(cnt[j] >= n) return true;
				if (!st[j]) {
					queue.offer(j);
					st[j] = true;
				}
			}
		}
	}
	
	return false;
}
  • Floyd算法(多源最短路径)
void floyd(){
    for(int k = 1; k <= n; k++) // 过渡阶段的 k 必须在外层
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);  // 松弛
}
int main(){
    // 初始化邻接矩阵
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++){
            if(i == j) g[i][j] = 0;
            else g[i][j] = INF;                        
        }    
    }
    // 读入边数
    while(m--){
        cin>>a>>b>>w;
        g[a][b] = min(g[a][b], w); // 去除重边    
    }
    floyd();
}
  • Bellman-Ford算法(可带负权边,解决有边数限制的最短路 Onm)(了解即可)

    for 所有节点(或者题目中所要求的限制次数)

     ​ //备份dis

    ​   for 所有边 a,b,w

    ​     dis[b] = min(dis[b], dis[a] + w); // 松弛操作

有边数限制的负权边最短路问题

int dis[N], backup[N];//backup用来备份dis数组防止出现串联问题
struct Edge{   int x, y, w;  }e[M];
void bellman_ford()
{
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    //最多经过k条边的最短路
    for (int i = 0; i < k; i++)
    {
        memcpy(backup, dis, sizeof dis);//备份上一次的最短距离
        for (int j = 0; j < m; j++)//遍历每条边,更新最短距离
            dis[e[j].y] = min(dis[e[j].y], backup[e[j].x] + e[j].w);
    }
}
int main()
{
    cin >> n >> m >> k;  // n 个节点 m 条边 走 k 步
    for (int i = 0; i < m; i++)
        cin >> e[i].x >> e[i].y >> e[i].w;
    bellman_ford();
    dis[n] > 0x3f / 2 ? cout << "impossible\n" : cout << dis[n] << endl;
    return 0;
}

最小生成树

  • 朴素版Prim算法( 稠密图 O n^2)
  1. 初始化 dis 为INF

  2. for i : 0 - n

​     t <= 找到集合外距离最近的点(该点与集合内所有有连通边中最短的边)

​     用 t 更新其他点到集合的距离

​     st[t] = true // 把 t 加到集合中去

注意:因为初始化未加入任意节点,所以需要遍历 n 次。

const int N = 510, INF = 0x3f3f3f3f;
int g[N][N], dis[N]; // dis[t] 表示 t 节点到集合的最短距离
bool st[N];
int prim()
{
    memset(dis, INF, sizeof dis); // 初始化 dis 为 无穷
    int res = 0; // 记录每次加到集合里的最短边权值
    for (int i = 0; i < n; i++) // 从 0 开始便于判断是否是第一个节点
    {
        int t = -1; // 因为距离最近节点状态未知,所以 t 初值为 -1
        for (int j = 1; j <= n; j++)
            if (!st[j] && (t == -1 || dis[t] > dis[j])) // 找到集合外距离最近的节点
                t = j;
        if (i && dis[t] == INF) // 最小生成树:所有节点均连通 dis[t] = INF 表示该点 独立
            return INF;
        if (i)
            res += dis[t]; // 先累加再更新 避免自环
        for (int j = 1; j <= n; j++)
            dis[j] = min(dis[j], g[t][j]); // 用 t 更新其他节点到集合的距离
        st[t] = true;  // 把 t 加到集合中
    }
    return res;
}
while (m--)
{
     cin >> a >> b >> c;
     g[a][b] = g[b][a] = min(g[a][b], c); // 取重边最小值
}
  • Kruskal算法(稀疏图 O mlogm)

    将所有边按权重从小到大排序(快排)

    枚举每条边 a,b,权重 c

    ​   if a,b不连通

    ​    将这条边加入集合中

public class Kruskal {
	private static Scanner in;
	private static int[] p;
	private static int[] wight;

	public static void main(String[] args) {
		in = new Scanner(System.in);
		int n = in.nextInt(), m = in.nextInt();
		p = new int[n + 5];
		wight = new int[n + 5];
		Edge[] edge = new Edge[m + 5];
		for (int i = 0; i < m; i++) {
			int a = in.nextInt(), b = in.nextInt(), c = in.nextInt();
			edge[i] = new Edge(a, b, c);
		}

		Arrays.sort(edge, 0, m);
		for (int i = 0; i < n; i++) {
			p[i] = i;
			wight[i] = 1;
		}

		int res = 0, cnt = 0;

		for (int i = 0; i < m; i++) {
			int a = edge[i].getA(), b = edge[i].getB(), c = edge[i].getC();
			a = find(a);
    b = find(b);
			if (a != b) { // 并查集的连接操作
				unionElements(a, b);
				res += c;
				cnt++;
			}
		}

		if (cnt < n - 1)
			System.out.println("impossible");
		else
			System.out.println(res);
	}

	public static int find(int x) {
		while (x != p[x]) {
			p[x] = p[p[x]]; // 路径压缩
			x = p[x];
		}
		return x;
	}

	public static void unionElements(int a, int b) {
		int leftRoot = find(a), rightRoot = find(b);
		if (leftRoot == rightRoot)
			return;
		if (wight[leftRoot] > wight[rightRoot]) {
			p[rightRoot] = leftRoot;
			wight[leftRoot] += wight[rightRoot];
		} else {
			p[leftRoot] = rightRoot;
			wight[rightRoot] += wight[leftRoot];
		}
	}
}

// 自定义排序规则
class Edge implements Comparable<Edge> {
	private int a, b, c;

	public Edge(int a, int b, int c) {
		this.a = a;
		this.b = b;
		this.c = c;
	}

	public int getA() {return a;}

	public int getB() {return b;}

	public int getC() {return c;}

	@Override
	public int compareTo(Edge o) {
		return c - o.c; // 从小到大排序
	}
}

二分图

(当且仅当图中不含奇数环(自己到自己的边数量为奇数))

  • 染色法(O n + m)
bool dfs(int u, int c)
{
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!color[j])  // 未染色
            if (!dfs(j, 3 - c)) // 递归染色
                return false;
        else if(color[j] == c) return false;  // 一条边的两边不能一个色
    }
    return true;
}
// main: 
 bool flag = true;
 for(int i=1;i<=n;i++){
     if(!color[i])
        if(!dfs(i,1)){
            flag = false;
            break;
       }
 }
  • 匈牙利算法(O mn)(二分图最大匹配)
int h[N], e[M], ne[M], idx = 0;
int match[N]; // 右边的点
bool st[N];   // 不重复搜同一个点

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])  // 还没考虑过
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))  // 没有匹配 / 匹配了但能找到下家
            {
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

int main(void)
{
    cin >> n1 >> n2 >> m; 
    memset(h, -1, sizeof h);
    while (m--)
    {        int a, b, ;     cin >> a >> b;    add(a, b);    }

    int res = 0;
    for (int i = 1; i <= n1; i++) // 从前往后分析左边的节点
    {
        memset(st, false, sizeof st);  // 清空右边的节点,表示还没有考虑过
        if (find(i))
            res++;
    }
    cout << res << endl; 
}

拓扑排序(数组模拟队列)

bool topSort()
{
    int hh = 0, tt = -1;  // hh 表示 头指针  tt 表示尾指针
    for (int i = 1; i <= n; i++)
    {
        if (!d[i])
            q[++tt] = i;  // 入度为零的节点入队
    }
    while (hh <= tt)  // 队列非空
    {
        int t = q[hh++];  // 取队头元素 队头指针右移
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i]; // 下一个节点
            d[j]--;    // 队头的下一个节点入度减一
            if (!d[j])
                q[++tt] = j;
        }
    }
    return tt = n - 1;
}

树与图的遍历

// 此代码可以获取当前节点及其孩子节点的总数
int dfs(int u){
    st[u] = true; // 标记当前节点已经被访问过
    int sum = 1, res = 0;
    for(int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        if(!st[j]){
            int s = dfs(j);
            // rea = Math.max(res, s); // 取左孩子或右孩子节点数最大值
            sum += s;        
        }            
    }
    // res = Math.max(n - sum, res);
    // ans = Math.min(ans, res);
    return sum;
}

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐