算法与数据结构

第一章

算法复杂度分析

性质:

消常

消非主导项

幂集问题

时间复杂度:O(2^n)

链表与数组(公式化描述)

1. 公式化描述(顺序存储 / 数组)

  • 所有元素连续存放在内存中。
  • 假设每个元素占 sizeof(int) 空间(比如 4 字节),共 9 个元素。
  • 总空间 = n × sizeof(element) = 9 × 4 = 36 字节

2. 链表描述(链式存储 / 单链表)

  • 每个节点包含:数据域 + 指针域
  • 数据域:存一个整数(4 字节)
  • 指针域:存下一个节点的地址(通常 8 字节,64位系统)

总空间 = 9×12=108 字节

链表反转

【3分钟搞懂链表反转 | 链表面试题】 https://www.bilibili.com/video/BV1SCtBzyESd/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

三指针法

Pre先指向none,先将Next(指针)标记为下一个节点,然后把当前节点的next(属性)设置为前一个节点Pre(此时已经断开连接),然后推进Cur到Next,因为虽然断了连接,但是Next却保存着下一个节点的信息,因此仍然可以正常推进Cur和Pre迁移

时间复杂度O(n) 每个节点都会被访问一次

第二章

二分查找

必须是数组 必须有序

时间复杂度:O(logn)

ASL------平均查找长度

ASL成功=每一层层数*每一层个数/n

ASL失败=Σ第n层失败节点数*(n-1)

冒泡排序

时间复杂度:O(n^2)

插入排序

将数组分为已排序和未排序两部分

依次从未排序中取出一个元素,放入已排序中

就是打牌

时间复杂度:O(n)~O(n^2)

空间复杂度:O(1)

选择排序

从未排序区找最小元素,和当前元素交换(双指针,当min≠i时即替换并i++)

时间复杂度:O(n^2) 扫描n n轮

空间复杂度:O(1)

堆排序

建堆

排序:将最大元素取出,将最小元素替换上去(利用删除的性质)

算法复杂度:

空间复杂度O(1)

前后缀表达式计算

中缀转前后缀表达式

1.加括号,对每一次运算都加括号

2.将运算符挪到括号左边

第六章

链表插入

第八章

汉诺塔的递归解决

第十章

散列 哈希表

构建下标和关键字的关系,比如一一对应,此时的查找效率O(1)

H(key)=key

但这个散列函数不固定

直接定址法------关键字连续的

可用通过调整下标b来把映射地址调整为从0开始

除留余数法------分散列

让关键字对某个数p求余数,使得不连续的关键字映射到连续的哈希表上

p一般去小于等于表长的最大质数

散列表的冲突 与 效率

同义词------两个映射到同一块散列表地址上了

1.散列函数的选取

直接定制法:浪费但不冲突

除留余数法:不浪费但冲突

2.装填因子/散列表的空间利用率α

越大则碰撞概率越大

3.处理冲突的方式(重点)

开放定址法:找新的空间存放同义词

线性探测法:依次查找后一个位置

存储/插入阶段

空位置和带有DEL的位置都可以插入

查找阶段

先找对应下标的位置,如果不一致就线性向后探测

ASL------平均查找长度

由于冲突存在,在查找中进行了多次比较,可用使用ASL 平均查找长度 来衡量散列表的效率

对于单个查找数字,其查找长度=真实位置-映射位置

ASL成功=Σ所有关键字的查找长度/n (关键字数量)

ASL失败=Σ所有关键字到空位置的距离(映射到这里直到查找失败比较了几次)(mod数)

切记任何关键字都不可能映射到下标11的,因为在mod数外

删除阶段

使用DEL=-1进行占位,不能直接删除 否则线性搜索会被截断!!!

平方探测法

拉链法

计算性能

ASL成功=第一层*1 + 第二层*2 + 第n层*n /n

ASL失败=11(关键字个数)/13(链表长度)

可以直接删除

第十二章 堆------用于找最大最小值

用于动态保存当前可用选择的边,最好是取出最大最下作为下一次使用的情况,比如Prim算法每次都需要从最小堆中招找权重最小的边进行添加

堆的定义

基于完全二叉树,除了最后一层都是满的,最后一层做连续排列

堆序性:最大最小堆

堆的特性

插入 删除 排序都是logn 建堆是n

堆的高度:logn

堆的数组表示与索引关系

堆的插入

在已经建好的堆中插入一个节点

  1. 将新元素插入到堆的末尾(插入)

2.从新元素上浮交换,直到满足堆的性质(调整)

创建heap数组,往最后一个位置插入新元素

确认新节点的位置i=heap.size()-1

当i没有到根节点前都执行循环,

找到父节点p=(i-1)/2,比较二者大小

比p大就交换,更新新位置i=p

比p小就跳出break

时间复杂度:要把末尾元素上浮要执行高度次 即O(logn)

堆的删除

1.移除堆顶元素,用堆中最后一个元素替代堆顶

2.通过下沉,对比当前节点与子节点大小,交换

创建队列heap

首先保存堆顶元素heap.get(0),这是作为返回值的

然后交换last和0元素

接着从头i=0开始下沉:

如果有左节点就设左节点更小,如果还有右节点就要对比左右节点谁更小

如果有小的就swap,同时设置i为小节点的idx

直到左右节点小的那个值比当前get(i)的值更大,跳出break

时间复杂度:同理O(logn)

堆的构建

将一个无序数组转为符合堆的数组结构

从最后一个非叶子节点开始调整

最后一个非叶子节点的下标是:len/2-1

从最后一个非叶子节点向前堆化

对于当前i节点,对比其左右子节点,2i+1/2,如果存且比当前i更小则设置min=l/r

如果min不是i则证明被替换了,执行swap

然后递归调用堆化

时间复杂度:O(n)

哈夫曼树与哈夫曼编码

非歧义且最短

1.统计出现的次数

2.定义节点和值,值是其出现的次数

3.每次合并最小的两个

4.左0右1

让出现次数越多的编码越短。由于都是叶子节点,因此不在另一个节点的路上,不会重复

带权路径长度=路径长度*叶子节点权重

左高树/左偏树

【C16【模板】左偏树(可并堆)】 https://www.bilibili.com/video/BV1Hz4y177gn/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

左偏树(Leftist Tree,又称左偏堆)是一种可合并堆(Mergeable Heap) ,主要用于高效地合并两个优先队列 。它与二叉搜索树(BST)、AVL 树、红黑树虽然都是树形数据结构,但设计目标、性质和应用场景完全不同

性质

合并

先拆解比较 到空为止 再合并,合并过程中如果左小于右则交换左右子树

时间复杂度:

第十四 十五章------树

堆只管"上下级"(父子),
BST 还管"左右派"(左右子树顺序)。

因此堆的调整只需要通过上浮和下沉即可,而二叉搜索树、红黑树、AVL都需要旋转才行!!!

二叉搜索树

任何节点都有当前值、左右节点三个属性

二叉搜索树则拥有这个节点属性,同时也有增删改查几个方法

如果没有根就是根,通过new创建新TreeNOde

如果有则判断当前值与左右节点node.value

如果大于则insert右子树,小于则insert左子树

查找类似,如果当前值比node.value小则在左子树查,直到找到或查到叶子节点node==null的情况

插入 查找 删除的时间复杂度:O(logn)

极端情况下退化为O(n)

二叉搜索树的删除

删除叶子节点------直接让父节点的左右指针指向null

删除只有一个叶子节点的父节点------让父节点的单侧指针指向这个节点唯一连着的那个节点

删除有两个叶子节点的父节点

先找到要删除的节点key,递归调用

找到后执行删除

如果左子树或右子树不存在或都不存在,就直接返回另一侧即可

如果两个自述都存在,就寻找中序后继,即右子树的最小左节点

调用在Node.right中找最小值的函数,最后删除中序后继节点

前中后序列的遍历与生成与知二推一

AVL树

普通二叉搜索树的结构会由于插入顺序的不同退化为链表

AVL通过严格平衡条件,使得树高不高,具有自平衡性

平衡因子=左子树高-右子树高

对于平衡二叉树,平衡因子的范围为[-1,0,1]也就是说,左右子树高差不超过1

可以通过旋转操作平衡

增删改查的时间复杂度:O(logn) n是节点数量

但是旋转操作非常频繁

红黑树通过堆红色节点的约束,保证最大树高不超过最小数高的两倍

因此AVL树更使用于查询密集的场景,红黑树适用于插入删除密集的场景

需要节点值、节点高度、左右子节点,节点高度用于计算平衡因子

AVL树的插入

注意子树的归属!!!

AVL树的删除

和二叉搜索树一致,但是每次删除后都需要重新平衡一次

已知前序中序求后序 已知中序后续求前序

【【有挂!!】二叉树遍历秒解,前序遍历、中序遍历、后序遍历-哔哩哔哩】 https://b23.tv/IBUkkdf

【无脑秒解!已知先/后序遍历与中序遍历,求后/先序遍历。-哔哩哔哩】 https://b23.tv/Cpvk3YR

红黑树(不考)

调整旋转次数少

约束红黑树:

保证从根节点到叶子节点的最长路径不超过最短路径的两倍

时间复杂度:

左旋的过程【红黑树的旋转操作】 https://www.bilibili.com/video/BV1ufNqeoE9b/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca![](https://i-blog.csdnimg.cn/direct/0fe2dd45781040669b6d0fd56975a67b.png)

旋转的时间复杂度是:O(1)只涉及指针的变化

红黑树的插入

1.按照二叉搜索树插入新的红色节点

2.若规则被破坏(红红冲突),通过重新着色和旋转解决

【红黑树的插入演示】 https://www.bilibili.com/video/BV1i5P5eeEMh/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

时间复杂度:查找(logn)+调整(logn)=O(logn)

B-树 多叉平衡搜索树(类似并查集的路径压缩后)

【B树(B-树) - 来由, 定义, 插入, 构建】 https://www.bilibili.com/video/BV1tJ4m1w7yR/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

用于急需要快速查找且需要保持数据的顺序

每一层都需要访问一次 因此压缩高度后访问次数就少了

读取多个字节和读取单个字节的耗时基本一样,因此一个节点多个数据是可以的

n阶B树------最大n叉

性质:

平衡:所有的叶子在同一层

有序:节点内有序 任意元素的左子树都小于他 右子树大于他

多路:m叉树

最多m个分支 节点内最多m-1个元素

根节点最少两个分支 一个元素

其他节点至少m/2个分支 m/2-1个元素

查找

插入(满了上移中间)

插入都是在叶子节点层操作的,发送上溢出才会触发上层

插入目标节点14:

先插入到指定位置,超过m叉树定义,发生分裂:

第m/2个元素上移,其左右两个元素分裂

构建

删除

对于5阶B树

删除非叶子节点都会转换为删除叶子节点

类似AVL的删除,或堆的删除,将其直接前驱 直接后继替换后删除原指定节点

对于出现下溢出的节点(删除后节点元素少于m/2个)

则遵循父下来 兄上去,向左右节点借一个元素,因此要改变父节点,把父节点拉下来,让兄弟节点上去

第十六章

邻接矩阵

方便找到是否有边 顶点的度

但是空间复杂度高 对于统计边的数量只能枚举

入度:竖 出度:横

邻接表

深度优先搜索 DFS

基于栈的访问

算法复杂度

时间复杂度:O(n) 每个节点都被访问

空间复杂度:最坏O(n)。AVL树O(logn)

用于生成括号-子集枚举-N皇后问题

搜索所有可能路径

广度优先搜索 BFS

基于队列的访问

先进先出-离得近的先访问

算法复杂度

空间复杂度:对于平衡二叉树,一般存入队列的是一层的内容,不超过树高logn;对于普通搜索树则是O(N)

第十九章 贪心

迪杰斯特拉算法

由于要探索最短代价的节点,每次需要从最小堆中取出堆顶元素,因此要用优先队列(堆)

每次出队列(提取堆)要进行重拍,重拍开销logn,对于所有节点都要删除一次,因此nlogn;
每条边可能引发一次"松弛操作",从而导致一个顶点被重新插入堆中!

BellmanFord(不考)

依次遍历所有节点的所有边,即A->B A->C A->D,然后B->E C->B C->E。。。
重复V-1轮一定收敛,可能提前收敛

算法复杂度:O(V*E)

A*搜索算法

复杂度在O(E)~O(E+VlogV)之间

Floyd-Warshall 动态规划

D法是单源算法,如果要全源则需要n次

FW能一次性计算出所有点之间的最短路径

通过一张加权表和一张前驱表不断更新得到

distx是邻接矩阵,表达任意两点之间的最短距离

nextx是前驱表,记录下一跳

通过三层循环,选择kij,k是中继节点,i是起始,j是终点

不断更新k,相当于在任意两个节点之间插入另一个节点,看看有没有更短通路

更新n轮后一定会收敛

只要对角线为正,就证明没有负权环

对于最终表,从2->3,先看next4,由0,为下一条,那么下一步就是0->3,是1,然后是1->3,找到最短路径2-0-1-3. 距离是dist4 2->3=4

算法复杂度:

第十六章

并查集

用于处理元素分组问题的数据结构,用于快速判断两个元素是否属于同1个集合,用于快速合并两个集合

make_set()------每个元素独立为集合

union(x,y)------合并两个元素,使得第一个元素的牢大为第二个元素的牢大,把这两个涉及的集合合并为一个,直接修改数组

find(x)------找x的最终父节点是谁

按秩合并

当树很深时,find效率会变低,因此使用两种方式,将节点多的挂载到节点少的 或将树高低的挂载在树高高的上

多加一个rank属性,初始化0

合并的时候先判断将rank矮的挂载到rank高的上

路径压缩------在查找的同时将路径上经过的节点全部挂载到根节点上

find的时候顺便将当前的根节点迭代挂载到根节点的根节点上,即将数组内的值更改为下一次找到的根节点的idx

时间复杂度O(4~5)

克鲁斯卡尔最小生成树ElogE

创建并查集类

包括find check两个函数,check通过find找到两个连通分支各自的代表元

如果一致则返回false,否则true

在主函数中,创建目标队列 与 uf对象

对每一条边遍历,根据权重排序,每次提取最小的边,

对其顶点进行判断,调用uf对象的check函数,如果返回true则证明为两个分支,可以选择,否则continue

直到n-1

这就设计了连通性判断,因此需要上面并查集的内容

这样的操作包括:

1.判断是否成环------是否联通 root_x=self.find(x) root_y=self.find(y) root_x == root_y?

2.将一条边加入生成树中------union()

先创建UF集合,会自动拆为最小节点

然后堆边进行sort 复杂度logE,然后对于每条边进行判断:

如果uf.union能够执行,也就是UF输出True,不成环的情况下,就往mst中添加这条边,同时累加权重

直到mst的len=n-1

时间复杂度:

Prim最小生成树算法ElogV

创建邻接表、最小堆 目标队列 已访问 原始节点队列

创建结构体 包括起点 终点 权重

遍历所有边将其连接的点存入邻接表

初始时选择任意一个节点,探索其边,选择最小权重边,查看邻接表其终点是否在已访问列表中

如果没有则添加,并累计权重

同时将邻居节点的相邻边放入最小堆

开辟最小堆,维护当前可用探索的边,由于每次探索最小的,因此使用最小堆

开辟结果列表维护选取的边

开辟以访问集保存已经访问的顶点

开辟nodes集保存未访问的节点

'

nodes用于存放当前可用访问的节点

visited存放已经访问的顶点

mst保存结果边

min_heap维护当前可选边

对于传入的每一条边,对于第一个顶点graph[u]添加权重为w,目的为v

对于第二个顶点graph[v]添加权重为w,目的为u

随机选一个起点。

将其放入已访问列表visited,然后把start顶点可连出去的所有边放入最小堆,堆会自动按权重从小到大排序

对于每一轮,取出堆中最小的边,如果其目标顶点没有访问过,就将其添加到visied中,然后将这条边放进mst目标列表中,同时累加权重

对于这条新加入顶点考虑,对于新顶点的其他边连接的节点,如果没有访问过,就全部放入堆中

直到n-1条边都被访问了

时间复杂度:

对于堆的处理复杂度都是logV 每条边最多被加入堆一次,因此堆操作数不超过E次,因此复杂度是O(ElogV)

Kahn拓扑排序算法(入度为零即取出队列) V+E

入度 零度 原始 临界 目标 管理访问的队列

添加邻接表 对于每一条边存储始终点,并为终点在入度数组中加一

初始将0度节点加入目标队列,搜索其邻接节点,将其度减一

如果为0则加入零度队列

检查目标队列长度是否等于原始节点队列

解决依赖问题,找到合理的安装顺序

只要入度为零即可加入,加入后其指向的节点的入度减一

创建两个字典,一个字典用于记录每个节点的入度,一个用于构建邻接表(建图)

创建一个topo_order保存最终结果

遍历每条边,对于每对起点u终点v:将v加入u的邻接表,同时给v的入度加一

初始时将入度为0的节点全加入队列

进入主循环后,每次从队列zero中取出一个节点current,将其放入最终结果topo_order中

访问器所有邻接点:for neighbor in graph

将邻接节点入度减一,如果邻接节点入度为0,则加入zero访问列表

最后检查len(topo_order)是否等于nodes的长度,如果不等于说明由环

时间复杂度:O(V+E)每个顶点和边添加一次

第十八章------分治

归并排序

一路拆到一组只有一个元素,然后开始归并

算法复杂度:

时间复杂度:O(nlogn) 递归深度是logn,然后每次合并需要n的复杂度

空间复杂度O(n)

快速排序

写一个快排和分区函数

快排首先调用分区函数进行分区

分区函数先设置基准值为最后一个,初始化指针指向low-1

对于low到high-1如果有比基准值大的放右侧,小的放左侧

接着递归调用快排,对分区的左右继续分区+快排

1.选择基准值

2.双指针分区(左边元素比基准值小 右边比基准值大)

3.递归排序 对左右分区执行选基准值并分区

j用于探索i用于标记,当j的值比基准值小则让i++然后交换ij的值,最后

i的值就是已经根据基准值分区的位置了,然后把基准值和i的位置交换,接着分治

选择

算法设计的小技巧:

保护顺序的动态维护使用栈或队列,不保护顺序但要求按增减序排序的动态维护用最大最小堆

比如DFS BFS,DFS使用队栈 BFS 使用队列

克鲁斯卡尔和普利姆是使用最小堆

树结构用于急需要快速查找且需要保持数据的顺序

并查集会破坏前中后序 因此不能平替树

相关推荐
风清扬_jd1 天前
libtorrent-rasterbar-2.0.11编译说明
c++·windows·p2p
u0109272711 天前
C++中的RAII技术深入
开发语言·c++·算法
青桔柠薯片1 天前
数据结构:顺序表与链表
数据结构·链表
彷徨而立1 天前
【C/C++】strerror、GetLastError 和 errno 的含义和区别?
c语言·c++
誰能久伴不乏1 天前
【Qt实战】工业级多线程串口通信:从底层协议设计到完美收发闭环
linux·c++·qt
2401_832131951 天前
模板错误消息优化
开发语言·c++·算法
金枪不摆鳍1 天前
算法--二叉搜索树
数据结构·c++·算法
liu****1 天前
4.Qt窗口开发全解析:菜单栏、工具栏、状态栏及对话框实战
数据库·c++·qt·系统架构
近津薪荼1 天前
优选算法——双指针6(单调性)
c++·学习·算法
向哆哆1 天前
画栈 · 跨端画师接稿平台:基于 Flutter × OpenHarmony 的整体设计与数据结构解析
数据结构·flutter·开源·鸿蒙·openharmony·开源鸿蒙