1.时间复杂度
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度。记作:T(n)=O(f(n))。它表示随问题n的增大,算法执行的时间增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度。
常见的时间复杂度的对比
O(1)<O(logN)<O(N)<O(NlogN)<O(n^2)<O(2^n)<O(N!)
- 常数时间复杂度 O(1):无论输入规模大小,算法的执行时间都是常数。
- 对数时间复杂度 O(log n):算法的执行时间随着输入规模的增长而增长,但呈对数增长。
- 线性时间复杂度 O(n):算法的执行时间与输入规模成线性关系。
- 线性对数时间复杂度 O(n log n):算法的执行时间随输入规模的增长而增长,但呈线性对数增长。
- 平方时间复杂度 O(n^2):算法的执行时间与输入规模的平方成正比。
- 指数时间复杂度 O(2^n):算法的执行时间随着输入规模的增长指数增长。
- 阶乘时间复杂度 O(n!):算法的执行时间随着输入规模的增长阶乘增长。
2.空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题规模,f(n)为 语句关于n所占存储空间的函数
O(1)<O(logN)<O(N)<O(n^2)<O(2^n)
-
O(1):表示算法所需的额外空间是一个常量,不随输入规模的增加而增加,即算法的辅助空间是固定的。
-
O(n):表示算法所需的额外空间随输入规模的增加而线性增加。例如,一个数组的存储空间复杂度就是O(n),因为数组的长度与输入规模n相关。
-
O(n^2):表示算法所需的额外空间随输入规模的增加而平方增加。这种情况通常出现在需要创建二维数组或矩阵的算法中。
-
O(log n):表示算法所需的额外空间随输入规模的增加而对数增加。这种情况通常出现在递归算法中,每次递归调用都会压入栈中,因此空间复杂度呈对数增长。
3.基础结构
数组
这是最常见的数据结构,数组在内存中顺序存储,所以只要给出一个数组下标,就可以读取到对应的数组元素;比如:上图给定下标3就可以读取到对应的5元素结果
对于链表,在进行删除和修改的时候,元素会往前或者往后移动,如上图就是给链表添加元素其元素往后移动。
链表
单向链表
双向链表
对于链表特点就是存在多个节点,节点data存放数据,next指向下一个节点的指针,而prev则是指向前一个节点的指针(双向链表);
对于链表查找节点就需要从头部开始(如果是双向链可以计算从头到尾查找节点那个性能好);对于元素的更新或者删除元素的时候最关键的细节点在于引用,变化元素前后需要根据情况指向对应元素的引用
对比
链表和数组是数据结构中最基础的结构,基本上其他的结构都是以他们为基础实现的,数据结构种类有很多,它们存在的目的无非是在不同应用场景下尽可能高效增、删、改、查
| 查找 | 更新 | 插入 | 删除 |
数组 | O(1) | O(1) | O(n) | O(n) |
链表 | O(n) | O(1) | O(1) | O(1) |
在上述对比中,链表删除,更新,插入复杂度为O(1)是在不考虑查找元素的过程情况下分析的:
->在读多写少的情况下,数组会比较合适
->在写多读少的情况下,特别是需要在尾部频繁插入、删除元素,用链表更合适一些
在内存存储上:
数组:是连续的内存块,查找元素上会很快
链表:是随机存储,在内存块上是随机分配存储的
4.快速查找结构体(针对整体)
对于这一类的结构体在整体上分为两类,以“树”的形式为一类,以“一致性计算”为一类;
- 树:通过构建类似与树的结构可以从上根节点开始查找,往下一层层匹对元素,在实现层面需要考虑新增元素需要做好树结构的维护;
- 一致性计算:提前创建一个足够大的空间,然后利用算法(哈希,取模等)计算元素存储位置并进行存储,该方法元素查找很快,但是也存在的问题就在于多个元素之间可能都落在同一个位置,这个时候需要利用一定的方法对数据冲突进行处理;
上两种类型的结果其目的在于数据的查找
4.1 树类
实现的原理上,采用的是链表实现,也可以链表加数组实现
堆
需要注意,堆的实现可以使用数组或者链表。在数组中,堆的元素按照层次顺序存储,可以通过下标计算元素之间的关系。在链表中,每个节点包含元素值以及指向左右子节点的指针。
规则:
- 元素比较行
- 节点最大/最小性
- 堆是一颗完全二叉树
堆的存储和结构:
1.根节点位置:根节点的数据总是在数组的位置[0]
2.节点的父节点位置:假设一个非根节点的数据在数组中的位置[i],那么它的父节点总是在位置[(i-1)/2]
3.节点的孩子节点位置:假设一个节点的数据在数组中的位置为[i],那么它的孩子(如果有)总是在下面的这两个位置:左孩子在[2*i+1],右孩子在[2*i+2]
二叉树/平衡二叉树
平衡二叉树属于二叉树,二叉树特点在每个节点上具有左右两个节点,二叉树会因为插入的元素顺序导致二叉树趋向线性链表,而平衡二叉树则是针对二叉树因为插入趋向线性链表而做的设计或者说要求
平衡二叉树(
Balanced Binary Tree
)又被称为
AVL
树(有别于
AVL
算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1
,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)
。但是频繁旋转会使插入和删除牺牲掉O(logN)
左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
->
遍历方式:对于二叉树本身的遍历采用的方式分为两种
“
深度优化遍历
”
与
“
广度优化遍历
”
- 深度优先遍历 : 思路很简单,就是“一头扎到底”的访问方式,从根节点8开始,然后选择一个方向,比如选择从根节点的左节点遍历
然后继续遍历
4
节点的左节点
2
;当
2
的左节点没有元素的时候就会回溯到
2
节点,遍历右节点;发现没有右节点继续遍历4
的右节点及
8
的右节点
9
根据
9
节点先遍历左节点
7
,发现
7
无子节点就回溯到
9
节点遍历右节点
10
- 广度优先遍历 : 思路就是一层层的往下走,先访问第一层的数据再访问第二层的数据
比如在访问第一层的时候根节点为
8
,子节点为
4,9
可以先记录到结构中,然后从栈中读取分别访问
4,9
下的子节点
2+3
树
/
红黑树
/btree/b+tree
->这里是重点部分
从
2+3
到
b+tree
都已经不属于
(
红黑树属于二叉树
)
这是在平衡二叉树的查询效率基础上,针对数据新增需要较高的维护成本而进行的优化设计的;
- 2+3树: 设计上相对节点,就是在一个节点上同时可以存在两个数据,当一个节点存入第三个元素的时候会进行裂变,重新调整结构
新来元素,不是新增叶子节点,而是和最后找到的节点融合,把两个节点合并起来
end
else
融合之后是
4
节点
(
即有
4
个孩子,存
3
个元素的节点
):
把
4
节点拆成
3
个节点,形成一棵二叉树
拆出来的
3
个节点中的父亲节点与上一个节点融合
if
融合之后是
3
节点
:
end
else
融合之后是
4
节点
把
4
节点拆成
3
个节点,形成一棵二叉树
拆出来的
3
个节点中的父亲节点与上一个节点融合
总结一下,就是先融合融合后是4节点就拆后继续向上融合,直到根节点融合后是3节点就结束
(
1
)每个节点或者是黑色,或者是红色。
(
2
)根节点是黑色。
(
3
)每个叶子节点(
NIL
)是黑色。
\[
注意:这里叶子节点,是指为空
(NIL
或
NULL)
的叶子节点!
]
(
4)如果一个节点是红色的,则它的子节点必须是黑色的
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
当新添加一个节点到树中后,将其颜色置为
red
,遵循以下原则对整个树进行调整:
1
、当插入的节点的父节点为
null
,则将该节点颜色置为
black
。
2
、当插入节点的父节点颜色为
black
,不需要调整。
3
、当插入节点的父节点为
red
,其叔父节点亦为红色,则将其父亲节点和叔父节点置为
black
,同时将祖父节点置为
red
,将祖父节
点设置为当前新增节点,重新按照从规则
1
开始判断。
4
、但插入节点的父亲节点为
red
,其叔父节点为
black
或
null
,则需要分多钟情况考虑。
a)
新增节点为父亲节点右孩子同时父亲节点是祖父节点的左孩子,则进行左旋,将父节点置为新节点,重新按照规则
1
进行判断;
b)
新增节点为父亲节点左孩子,同时父亲节点是祖父节点的右孩子,则进行右旋,将父节点置为新节点,重新按照规则
1
进行判断;
5
、不满足上述所有条件,将父节点置为
black
,同时,将祖父节点置为
red
,进行以下两种情况判断。
a)
如果新增节点是父亲节点的左孩子,同时,父亲节点是祖父孩子的左孩子,则对祖父节点进行右旋
b)
其他情况,对祖父节点左旋。
- btree: B-tree,即B树,而不要读成B减树,它是一种多路搜索树(并不是二叉的):
1. 定义任意非叶子结点最多只有M个儿子;且M>2;
2. 根结点的儿子数为[2, M];
3. 除根结点以外的非叶子结点的儿子数为[M/2, M];
4.
每个结点存放至少
M/2-1
(取上整)和至多
M-1
个关键字;(至少
2
个关键字)
5.
非叶子结点的关键字个数
=
指向儿子的指针个数
-1
;
6.
非叶子结点的关键字:
K[1], K[2], …, K[M-1]
;且
K[i] < K[i+1]
;
7.
非叶子结点的指针:
P[1], P[2], …, P[M]
;其中
P[1]
指向关键字小于
K[1]
的子树,
P[M]
指向关键字大于
K[M-1]
的子树,其它
P[i]
指向关键字属于
(K[i-1], K[i])
的子树;
8. 所有叶子结点位于同一层;
- b+tree: B+树是B-树的变体,也是一种多路搜索树:其定义基本与B-树相同
除了:
1.
非叶子结点的子树指针与关键字个数相同;
2.
非叶子结点的子树指针
P[i]
,指向关键字值属于
[K[i], K[i+1])
的子树(
B-
树是
开区间);(
B+
是闭区间,也就是包含区间的两端值)
3.
为所有叶子结点增加一个链指针;
4.
所有关键字都在叶子结点出现;
btree
与
b+tree
的对比
b+tree
的优势:
- 单一节点存储更多的元素,使得查询的IO次数更少;
- 所有查询都要查找到叶子节点,查询性能稳定;
- 所有叶子节点形成有序链表,便于范围查询;
- 对于范围查找,B+树通过二分查找找到下限,然后顺着叶子结点的链表找到上限;B树依赖中序遍历,二分查找找到上限,在中序遍历找到范围的下限
btree
的优势:
- 主要优势点在于数据和键是绑在一起,因此在查询上会不固定,但是比较适合聚合型数据库的查询
跳跃表
->应用Redis的zset
跳跃表实际上就是在链表的基础上进行优化;针对链表的基础上建立一层层的索引,然后通过索引的方式查询
比如在上建立索引:
而跳跃表的索引也是有层数关系,层数的建立在实现的时候不好确定,因此往往会根据
“
抛硬币
”
的策略去进行计算是50%的概率分配到
level1,25%
的概率被分配到
level2,12.5%
概率分配到
level3
,以此类推
2^-63
的概率被分配到最顶层,每次一层的晋升率是50%
跳跃表与树的区别
跳跃表的优点是维持结构平衡的成本比较低,完全依靠随机。而二叉查找树在多次插入删除后,需要
Rebalance
来重新调整结构平衡。
4.2 一致性计算类
实现的原理上可以基于数组
+
链表实现;
散列表
首先会创建好一个数组,每新增元素的时候通过对
key
进行计算确定
key
的下标位置并最终将存入到数组中,如果说存储的元素存在冲突这个时候就会在数组的基础上构建链表存储
5.关系结构
图
图是一种比较有用的算法结构,可以帮助我们求得最短距离;比如存在这
“
长沙西站,西湖公园,橘子洲,五一广场,梅溪湖,桃子湖,岳麓山”7
个关键地方;目前存在的需求就是查找从
“
长沙西站
-
》五一广场
”
的最短路径
我们可以针对性的构建如下线路;
可以发现从
“
长沙西站
-
西湖公园
-
橘子洲
-
五一广场
”
这一条路线是最短的;如上的方案就是利用了
“
图
”
进行实现,那么在程序中如何利用图连进行最短距离的计算呢?(
广度搜索法查询最短距离
)
对于程序来说,会选择其中一个元素为开始的根节点,然后寻找可以访问的下一个元素;默认所有距离都一样;
然后再继续同步同样的过程
最终确定从
“
长沙西站
-
》西湖公园
-
》橘子洲
-
》五一
”
;这条路线最短
邻接表
我们将上面的图话题转化为连接表的方式展示
邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。 对于无向图来说,使用邻接表进行存储也会出现数据冗余,表头结点A
所指链表中存在一个指向
C
的表结点的同时,表头结点
C
所指链表也会存在一个指向A
的表结点。
6. 运用结构
队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(
front
)进行删除操作,而在表的后端(
rear
)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
栈
栈
(stack)
是一种线性数据结构,特点就是先放入的元素最后取出,只能先入后出
(FILO),
最后进入的元素存放的位置叫作栈顶(top)
,在实现的方式上可以基于数组或者链表的方式来实现;