数据结构
类型
线性结构
-
线性结构是最常用的数据结构
-
特点 数据之间是***一对一***的线性关系
-
线性结构的存储结构分为
-
顺序存储结构(如数组、栈、队列)
顺序存储的线性表称为顺序表
顺序表中存储的元素是连续的
优点
可以通过下标来查找元素 速度快
(对于有序数组 还可以使用二分查找等算法来提高检索速度) 缺点
当要检索更改某个具体的值/插入删除某个值(需要整体移动)时的效率较低 -
链式存储结构(如链表)
链式存储的线性表称为链表
链表中存储的元素不一定是连续的 但元素节点中存放着数据以及相邻元素的地址信息 优点
当要插入删除某个值时的效率较高 缺点
当要进行检索时的效率较低(需要从头开始遍历)
-
非线性结构
- 特点 数据之间是***一对多(二维数组、多维数组、广义表、树)、多对多***(图)的非线性关系
栈
-
栈是一个***先入后出、后入先出的有序列表***
-
栈是一种限制线性表中元素的插入与删除只能在线性表的同一端进行的特殊线性表
-
允许插入与删除的一端为变化的一端 称为栈顶(Top) 另一端为固定的一段 称为栈底(Bottom)
-
在栈中 最先放入的元素在栈底 而最后放入的元素在栈顶
-
且最后放入的元素最先删除 最先放入的元素最后删除
-
元素的插入又称为入栈(push) 元素的删除又称为出栈(pop)!
-
{逆波兰计算器}
链表
-
链表是有序列表 是以节点的方式来进行存储的 是链式存储
-
每个节点都包含data域(存储数据)与next域(存储下一个节点的物理地址信息)
-
对于链表的物理结构(即真实结构 与逻辑结构相对)来说 链表中的各个节点不一定是连续存储的
-
链表分为带头节点的链表与不带头节点的链表
-
类型
-
单(向)链表
查找/遍历的方向只能是从前往后的一个固定方向
-
双(向)链表
可以向前或向后查找/遍历
-
{关于链表的面试题}
-
{约瑟夫环}
队列
-
队列是一个***先入先出、后入后出的有序列表***
-
{环形队列}
稀疏数组
-
当一个数组中的元素大部分为0/同一个值时 此时可以使用稀疏数组来***节省所占用的内存空间***
-
{棋盘}
哈希表
-
又称为散列表
-
是根据关键码值来直接进行访问的一种数据结构
-
即通过把关键码值映射到表中的某一个位置来访问记录以***加快查找的速度***
-
此映射函数叫做散列函数 存放记录的数组叫做散列表
-
{哈希表}
树
-
能提高数据存储、读取的效率
即与顺序存储结构比较 树能提高增删改的效率
而与链式存储结构比较 树能提高查的效率
-
常用术语
-
节点
-
根节点/root
-
父节点
-
子节点
-
叶子节点
即没有子节点的节点
-
路径
即从root开始到此节点的路线
-
路径长度
即通路中分支的数目(若root层为1 则从根节点到第L层节点的路径长度为L-1) -
节点的权
即节点的值 -
节点的带权路径长度
即从root到该节点的路径长度与该节点的权的乘积 -
树的带权路径长度/WPL(Weighted Path Length)
即所有的叶子节点的带权路径长度之和 -
层
root所处的层数为第1层 -
树的高度
即树的最大层数 -
子树
由树的其中一个节点以及其下面的所有节点构成 -
森林
多棵子树即为森林 -
节点的度
即此节点的分支数 -
树的度
即树的各个节点的度的最大值
-
二叉树
即每个节点最多只能有两个子节点的树
-
所有叶子节点都在最后一层/倒数第二层
-
且对于L层从左开始叶子节点得一直连续
-
若途中断开则另一部分一定得为L-1且一直连续到尽头
-
且若右分支的最大层数为L则左分支的最大层数一定为L/L+1的树叫做完全二叉树
-
所有叶子节点都在最后一层的树叫做满二叉树(满二叉树是特殊的完全二叉树)
-
且***满二叉树的节点总数为2n-1(n为层数)***
-
缺点
二叉树是需要加载到内存中的 若二叉树的节点很多 就会出现问题
构建二叉树时的速度慢 因为需要多次进行IO读写操作
(海量的数据通常都存放在数据库/文件中)
而节点海量 二叉树的高度就会变得很大 这样同样会降低操作速度(如增删改查)
顺序存储二叉树
从数据存储的结构来看 数组的存储方式与树的存储方式之间可以相互转换
即树可以转换成数组(如[1, 2, 3, 4, 5, 6, 7]) 而数组也可以转换成树
-
且顺序存储二叉树通常为完全二叉树
-
特点
在数组中下标为n的数据所代表的节点的左子节点为2n+1 右子节点为2n+2 父节点为(int)(n-1)/2
线索二叉树
n个节点的二叉树有2n-(n-1)即n+1个空指针域
而当这些空指针域存放着指向该节点在某种遍历次序下的前驱与后继节点的指针时
此二叉树就叫做线索二叉树 空指针域中存放着的指针又叫做线索
-
而根据线索性质的不同 线索二叉树又可以分为前序/中序/后序线索二叉树
哈夫曼树
-
又称为最优二叉树
-
即WPL最小的树即***权值越大的节点离root越近的二叉树***
二叉排序树
对于二叉树的任何一个非叶子节点来说
若左子节点的值比当前节点的值小 且右子节点的值比当前节点的值大
则此二叉树就叫做二叉排序树/二叉搜索树/BST(binary search/sort tree)
-
二叉排序树可以是空树
-
缺点
如{1, 2, 3, 4, 5, 6} 若转化成二叉排序树的话 则左子树全部为空
且看起来更像是一个单链表 且查询速度明显降低甚至比单链表的查询速度还要慢
因为每次都需要比较左子树(即便左子树不存在)(插入速度不变)
即不能完全发挥出BST的优势
平衡二叉树
若二叉排序树的左右两棵子树的高度差的绝对值<=1且它的左右两棵子树也是如此
则此二叉排序树就叫做平衡二叉树/平衡二叉搜索树(Self-balancing binary search tree)/AVL
-
平衡二叉树也可以是空树
-
平衡二叉树又分为AVL、红黑树、替罪羊树、伸展树等
-
想要将BST转化成AVL可以进行单旋转(左旋转/右旋转)/双旋转
多叉树
若允许树中的每个节点可以有更多的数据项与更多的子节点
则此树就叫做多叉树/多路查找树/multiway tree
-
多叉树通过重新组织节点以减少树的高度来对二叉树进行优化
B树
所有叶子节点都在同一层且满足二叉排序树的规则的多叉树就叫做B树/B-tree/B-树
-
2-3树、2-3-4树等都是B树 (2-3树是最简单的B树结构)
-
特点
关键字分布在整棵树中 即叶子节点与非叶子节点都存放着数据
搜索有可能在非叶子节点就结束
B树的搜索性能等价于在整棵树中做一次二分查找 -
常用术语
-
二节点/三节点/…
有两个子节点的节点叫做二节点 二节点要么没有子节点 要么有两个子节点
有三个子节点的节点叫做三节点 三节点要么没有子节点 要么有三个子节点
… -
B树的阶
即节点的最多子节点的个数
如2-3树的阶为3 2-3-4树的阶为4
-
B树的搜索
从root开始 对节点内的关键字进行二分查找
若命中则结束 否则进入查询关键字所属范围的子节点 重复
直到所对应的子指针为空 或到达叶子节点
-
B+树
是B树的变体之一
-
与B树的区别
B+树的搜索只有到达叶子节点才命中而不可能在非叶子节点命中
所有关键字都出现在叶子节点的链表中
(即数据只能在叶子节点中 且此叶子节点叫做稠密索引)
链表中的关键字/数据也都恰好是有序的
非叶子节点相当于是叶子节点的索引 且此非叶子节点叫做稀疏索引
而叶子节点相当于是存储关键字/数据的数据层
B+树更适合用于文件索引系统
(B树与B+树都各有自己的应用场景 因此不能说B+树完全比B树好)
B*树
也是B树的变体之一
-
与B+树的区别
在B+树的非根与非叶子节点中增加指向兄弟的指针
且较B+树来说 B*树的块的最低使用率比B+树高 且分配新节点的概率比B+树低
即空间利用率更高
图
-
图的节点可以有0个/多个相邻的节点 且图可以表示多对多的关系
-
常用术语
-
顶点
即节点 -
边
即两个节点之间的连接 -
路径
-
无向图
即边是没有规定方向的 如可以A-B 也可以B-A -
有向图
即边规定了方向 如只能A-B -
带权图
即边带权值的图 而这种图也叫做网
-
-
可以使用二维数组(邻接矩阵)/数组+链表(邻接表)来表示图
- 邻接矩阵中有很多边都是不存在的 会造成空间的浪费
- 邻接表只关心存在的边 不会造成空间的浪费
-
图的遍历
即使用特定的策略来访问图中的许多节点
-
类型
{图的深度优先遍历/搜索} {图的广度优先遍历/搜索}
-
算法
递归
- 即方法自己调用自己 但每次调用时传入的变量不同
- 递归有助于解决各种复杂的问题 同时还可以让代码变得简洁
- 递归必须向退出递归的条件逐渐逼近 否则就会出现***StackOverflowError***
- {阶乘}
- {八皇后问题}
排序算法
-
类型
-
内部排序
即将所有要排序的数据都加载到内存中进行排序
-
外部排序
即因为数据量的过大导致数据无法全部加载到内存中 因此排序时不仅需要内部存储 还需要借助外部存储
-
-
时间复杂度
-
一个算法中的语句执行次数称为时间频度/语句频度 记为T(n)
-
即为执行一个算法所耗费的时长
-
时间复杂度记为O(f(n))
-
时间复杂度忽略了T(n)的常数项、低次项、系数
-
因此时间频度不同 但时间复杂度可能相同
-
如T(n1) = n2 + 7n + 6与T(n2) = 3n2 + 2n + 2 它们的时间复杂度都为O(n2)
-
当n -> ∞时 时间复杂度越大 算法的执行效率越低
-
常见的时间复杂度
-
常数阶/O(1)
即无论代码有多少行 只要没有循环等复杂结构 那么这个代码的时间复杂度就是O(1)
-
对数阶/O(log2n)
即耗时是根据代码的变化而变化的
假设n -> ∞ 则i经过x次的while循环后>=n 即2x=n 即x=log2n
(因此 若为i = i * 3 则为O(log3n))
-
线性阶/O(n)
即耗时是随着n的变化而变化的
-
线性对数阶/O(nlog2n)
-
平方阶/O(n2)
即双层n循环的嵌套
(若将其中一层循环的n改成m 则为O(n * m))
-
立方阶/O(n3)
即三层n循环的嵌套
-
k次方阶/O(nk)
即k层n循环的嵌套
-
-
当n -> ∞时 O(1) < O(log2n) < O(n) < O(nlog2n) < O(n2) < O(n3) < O(nk) < O(2n)
-
平均时间复杂度
即为所有可能的输入实例均以等概率出现的情况下的该算法的运行时长
-
最坏时间复杂度
即最坏情况下的时间复杂度
一般的 时间复杂度为最坏时间复杂度
-
-
空间复杂度
即为执行一个算法所需的内存
主要讨论时间复杂度 因为用户最看重的是执行速度 因此可以用空间换时间 如Redis
-
稳定性
假定在待排序的序列中 存在多个相同的记录 若经过排序 这些记录的相对次序保持不变
则称这种排序算法是稳定的 否则是不稳定的
即在原序列中 a=b且a在b的前面 而在排序后的序列中a可能会在b的后面 此为不稳定
冒泡排序
- 即从下标为0开始 依次比较相邻元素的值 若逆序则交换
- 每一次都会把数组中剩余的数据中最大的数挑出来并放到数组的最右边
选择排序
-
与冒泡排序相似
-
每一次都会把数组中剩余的数据中最小的数挑出来并放到数组的最左边
插入排序
-
把数组看成是一个有序表与一个无序表 开始时有序表中只有一个元素 而无序表中有(n-1)个元素 排序时每次都会从无序表中取出第一个元素 并将它的值依次与有序表中元素的值进行比较 然后将它插入到有序表中的适当位置 最后会产生一个有序表
希尔排序
-
是插入排序的升级版 也称缩小增量排序
快速排序
-
是冒泡排序的升级版
归并排序
-
采用的是分治思想
基数排序
-
是对传统桶排序的扩展
-
是经典的***以空间换时间的算法 占用内存大 因此对大量数据进行排序时容易出现OutOfMemoryError***
-
一般不适合于有负数的数据的排序
堆排序
-
类型
- 每个节点的值都大于等于其左右子节点的值的完全二叉树为***大顶堆***
-
大顶堆的特点
arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]
-
每个节点的值都小于等于其左右子节点的值的完全二叉树为***小顶堆***
-
小顶堆的特点
arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]
-
一般的 若想要将数组排序成升序则用大顶堆排序 若想要将数组排序成降序则用小顶堆排序
-
构造大顶堆的原理(小顶堆同理)
查找算法
顺序查找
- 又称为线性查找
二分查找(递归)
- 又称为折半查找
- mid为***固定mid***
- 当数据量过大时可能会出现***OutOfMemoryError***
二分查找(非递归)
- 与二分查找(递归)类似
- mid也为***固定mid***
- 因为为非递归 因此当数据量过大时不会出现OutOfMemoryError
插值查找
-
与二分查找(递归)类似 将mid修改为***自适应mid***即可
-
即将mid由(left + right) / 2改成***left + (right - left) * ((target - arr[left]) / (arr[right] - arr[left]))***
-
对于关键字分布比较均匀的数组(如{1, 2, 3, 4, …, n})来说 插值查找的速度比二分查找(递归)的速度要快
-
但在关键字分布不均匀/target在arr中不存在时的情况下可能会出现***OutOfMemoryError/StackOverflowError***
斐波那契查找
-
又称为黄金分割法
-
与二分查找(递归)与插值查找类似 将mid修改为***黄金分割mid***即可
-
其中F[k]为斐波那契数列({1, 1, 2, 3, 5, 8, 13, 21, 34, 55, …}) 有F[k] = F[k - 1] + F[k - 2]
-
转化后为(F[k] - 1) = (F[k - 1] - 1) + (F[k - 2] - 1) + 1
常用10种算法
二分查找(非递归)
- 同上
分治算法
- 原理为分而治之
- 即把一个复杂的问题分成两个或多个相同或相似的子问题 再把子问题分成更小的子问题… (直到子问题可以简单的直接求解) 则原问题的解即为子问题的解的合并
- 因此分治算法通常用递归来进行
- {汉诺塔}
动态规划算法
-
与分治类似
-
即也把大问题划分成小问题 但小问题通常不是相互独立的
-
即***下一个子问题的求解是建立在上一个子问题的解的基础上***
-
{背包问题}
-
零一背包
即每个物品只有一个
-
完全背包
即每个物品都有无数个
-
KMP算法
- 比BF算法快
- 重点为取得子串所对应的next[]
贪心算法
-
又称为贪婪算法
-
即***在每一步的选择中都采取最优解***使得结果可能最优
-
贪心算法所得到的结果不一定是最优解(有时是最优解) 但结果一定是接近最优解
-
{集合覆盖}
普里姆(Prim)算法
-
最小生成树/Minimum Spanning Tree(MST)
- 即对于带权无向连通图 设计一棵图的所有边的权值总和最小的生成树
-
Prim算法为求连通网的最小生成树的一种
-
即在含有n个顶点的连通图中找出(n - 1)条边就可连通n个顶点的连通子图 此图又称为***极小连通子图***
-
每次都找已被访问过的顶点的权值最小的邻接边
克鲁斯卡尔(Kruskal)算法
-
Kruskal算法为求连通网的最小生成树的一种
-
在保证已连通的顶点之间不会构成回路的情况下每次都找权值最小的边
迪杰斯特拉(Dijkstra)算法
-
Dijkstra算法为求连通网的某顶点到其它顶点的最短路径的一种
-
使用的是***图的广度优先搜索思想***
弗洛伊德(Floyd)算法
-
Floyd算法为求连通网的各个顶点之间的最短路径的一种
马踏棋盘算法
-
又被称为骑士周游问题
-
即在8 * 8的棋盘上 有一棋子马位于某一格子中且按照马走日的方式移动 问怎样走才能走完所有的格子(每个格子只能走一次)
-
使用的是***图的深度优先搜索思想***(但其实是BF算法)
-
一定要使用其它算法如贪心算法来优化马踏棋盘算法 否则算不出来
哈夫曼编码
-
是可变字长编码的一种
-
又叫做最佳编码
-
被广泛的用于数据/文件的压缩
-
原理
定长编码
哈夫曼编码
-
且哈夫曼编码属于前缀编码 即任意一个字符的编码都不能是其他字符的编码的前缀
-
即哈夫曼编码***不会造成匹配时的多义性*** 因此哈夫曼编码是无损压缩
-
当然 构建的哈夫曼树不同 对应的哈夫曼编码可能会不同 但***WPL都是一样的 都是最小的***
-
若某文件本身已经过压缩处理 则再使用哈夫曼编码进行压缩的效果不会有太大的变化 如PPT、视频等
-
若某文件的内容中重复的数据并不多 则使用哈夫曼编码进行压缩的效果不会很明显
-
使用哈夫曼编码的压缩是对字节来进行处理的 因此***可以压缩任何类型的文件***