Appearance
部分数据结构和算法笔记
哈希表与数组的对比
- 数组进行插入操作时效率比较低
- 数组进行查找操作的效率
- 如果是基于索引进行查找操作效率非常高
- 基于内容查找效率非常低
- 删除操作效率也不高 因为删除了之后需要将后续的移动位置
哈希表扩容的思想
为什么要扩容
- 目前我们是将所有的数据项放在长度为 7 的数组中的
- 使用的是链地址法,loadFactor可以大于 1,所以这个哈希表可以无限制的进行插入新数据
- 随着数据量的增多,每一个 index 对应的 bucket 会越来越长,也就造成效率的降低
- 在合适的情况对数组进行扩容
如何进行扩容
- 可以将容量增大两倍
- 但是这种情况下,所有的数据项一定要同时进行修改(重新调用哈希函数,)
- 这是一个耗时的过程,但是如果数组需要扩容,那么这个过程是必要的
什么情况下扩容
- 比较常见的是loadfactor>0.75的时候进行扩容
判断一个数是否是质数
jsx
const p = (num) => {
for (let i = 2; i < num - 1; i++) {
if (num % i === 0) {
return false
}
}
return true
}
为什么要使用树结构来保存数据呢?
树结构和数组/链表/哈希表对比起来有什么有点?
数组 | 链表 |
---|---|
优点 | |
根据下标值访问的效率很高 | 插入和删除操作的效率很高 |
但是如果我们希望根据元素来查找对应的位置呢? | |
比较好的方法是先对数组进行排序,再进行二分查找 | |
缺点 | |
需要先对数组进行排序,生成有序数组,才能提高查找效率 | 查找效率很低 |
对数组进行插入和删除时,有大量位移操作,效率很低 | 即使插入和删除效率很高,但是如果要插入的是再中间位置,还是需要从头先找到对应的数据 |
哈希表
优点
- 插入/查询/删除的效率很高 缺点
- 空间利用率不高,底层使用的是数组,并且某些单元也是没有被利用的
- 哈希表中的元素是无序的,不能按照固定的顺序来遍历哈希表中的元素
- 不能快速找出哈希表中的最大值或者最小值
树结构
- 不能说树结构比其他结构都要号,因为每种数据结构都有自己特定的应用场景
- 但是树结构确实综合了上面的结构的优点 而且为了模拟某些场景,我们使用树结构会更方便
- 因为树结构的非线性,可以表示一对多的关系
- 比如文件的目录结构
树的术语
- 树:n >= 0 个节点构成的有限集合 当 n = 0 时,称为空树
- 对于任何一棵非空树 n > 0 ,具备以下性质
- 树中有一个称为 根的特殊节点,用 r 表示
- 其余节点可分为 m(m>0)个互不相交的有限集合 t1,t2,每个集合本身又是一棵树,称为原来的树的子树
- 节点的度:节点的子树个数
- 树的度:树的所有节点中最大的度数
- 叶节点:度为 0 度节点
- 父节点
- 子节点
- 兄弟节点
- 路径和路径长度
- 节点的层次:规定根节点在 1 层,其它任一节点的层数是其父节点的层数加 1
- 树的深度:树中所有节点中最大层次
二叉树的特性
二叉树有几个重要的特性,在笔试题中较为常见:
- 一个二叉树第 i 层的最大节点数为:2^(i-1),i>=1
- 深度为 k 的二叉树有最大节点总数为:2^k-1,k>=1
- 对于任何非空二叉树 t,若 n0 表示叶节点的个数,n2 树度为 2 的非叶节点个数,那么两只满足关系 n0 = n2+1
了解
- 完美二叉树,也称为满二叉树
- 在二叉树中,除了最下层的叶节点外,每层节点都有 2 个子节点;
- 完全二叉树
- 除二叉树最后一层外,其它各层的节点数都达到最大个数
- 且最后一层从左向右的叶节点连续存在,只缺右侧若干节点
- 完美二叉树是特殊的完全二叉树
二叉树的存储
二叉树的存储方式是数组和链表; 如果是完全二叉树,使用数组是可以的,但是如果是非完全二叉树,使用数组,则会造成资源的浪费; 二叉树最常见的方式还是使用链表存储
- 每个节点封装成一个 Node,Node 中包含存储的数据,左节点的引用,右节点的引用。
二叉搜索树
- 二叉搜索树是一棵二叉树,可以为空
- 如果不为空,满足以下性质
- 非空左子树的所有健值小于其根节点的键值
- 非空右子树的所有键值大于其根节点的键值
- 左、右子树本身也是二叉搜索树
二叉搜索树的特点
- 相对较小的值总是保存在左节点上,相对较大的值保存在右节点上
树的遍历
- 遍历一棵树是指访问树的每个节点,也可以对每个节点进行操作,例如进行简答的打印
- 树和线性结构不同,线性结构通常是从前到后的顺序遍历
- 树的遍历主要是下面几种
- 先序
- 中序
- 后续
- 树的递归遍历 就是函数栈的调用
先序遍历
- 访问根节点
- 先遍历左子树
- 再遍历其右子树
中序遍历
- 遍历其左子树
- 访问根节点
- 遍历其右子树
后续遍历
- 遍历其左子树
- 遍历其右子树
- 访问根节点
二叉搜索树搜索
- 获取节点
- 循环搜索 key
js
search(key) {
// 1. 获取跟节点
let node = this.root
// 2. 循环搜索key
while (node !== null) {
// 如果要搜索的节点比当node小
if (key < node.key) {
// 那就将node.left赋值给node
node = node.left
} else if (key > node.key) {
// 反之亦然
node = node.right
} else {
// 如果相等,就说明搜索成功
console.log(node)
return true
}
}
// 3. 没有找到
return false
}
二叉搜索树的删除
删除节点要从查找要删除的节点开始,找到节点后,需要考虑三种情况,
- 该节点上叶节点(没有子节点,比较简单)
- 该节点有一个子节点(也相对简单)
- 该节点有两个子节点(情况比较复杂)
从删除入手:
- 查找要删除的节点,如果没有找到,不需要删除
- 找到要删除的节点,
- 删除叶子节点
- 删除只有一个子节点的节点
- 删除有两个子节点的节点
删除两个子节点的节点的寻找规律 如果要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下,我们需要从下面的子节点中找到一个节点,来替换当前节点;
- 比 current小一点的节点,一定是 current左子树的最大值
- 比 current大一点的节点,一定是 current右子树的最小值
在二叉搜索树中,这两个特别的节点,有两个特别的名字:
- 比 current小一点的节点,称为 current 节点的前驱
- 比 current大一点的节点,称为 current 节点的后继
二叉搜索树的删除总结
二叉搜索树的删除非常棘手
因为它非常复杂,一些程序员都尝试着避开删除操作
- 部分人的做法是在 Node 类中添加要给布尔值,
- 要删除一个节点就让此字段设置为true
- 进行其他操作之前,会判断这个节点是否标记删除
- 这样相对简单,并且删除不会改变原有的结构
- 但是在二叉搜索树的存储中,还保留着那些本该被删除的节点
- 但是这个方法会造成很大的空间浪费,特别是针对数据量较大的情况
- 而且作为程序员要学会通过这些复杂的操作,锻炼自己的逻辑
二叉搜索树的缺陷
- 如果插入的数据是有序数据,例如 9-8-7-6-5-4-3,就会使得所有的数据分布在节点的左侧,
非平衡树
- 比较好的二叉搜索树,应该是左右分布均匀的;
- 但是插入连续数据后,分布的不均匀,称这种树为非平衡树
- 对于一颗平衡二叉树来说,插入/查找等操作的效率是O(logN)
- 对于一颗非平衡二叉树,相当于编写了一个链表,查找的效率就变成了O(N)
红黑树的规则
- 节点上红色或黑色
- 根节点上黑色
- 每个叶子节点都是黑色的空节点
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子到所有路径都包含相同数目的黑色节点
变色
旋转
图的术语
顶点:图中的一个节点
边:顶点和顶点之间的连线
相邻顶点:由一条边连接在一起的顶点称为相邻顶点
度:一个顶点的度是相邻顶点的数量
路径:是顶点 v1,v2...vn 的一个连续序列
简单路径:不包含重复顶点的路径
回路:第一个顶点和最后一个顶点相同的路径
无向图:所有的边都没有方向
有向图:所有的边都有方向
无权图:边是没有权重的
有权图:边是有权重的
图的表示方式
一种比较常见的表示图的方式:邻接矩阵
- 让每个节点和一个整数相关联,改整数作为数组的下标值
- 使用一个二维数组来表示顶点之间的连接
常用排序算法
简单排序:冒泡-选择-插入 高级排序:希尔-快速
冒泡排序的思路
效率相对其他的效率较低,但是较为简单
思路:
- 对未排序各元素从头到尾依次比较相邻的两个元素大小关系
- 如果左边的队员高,则两队员交换位置
- 向右移动一个位置,继续比较下面的的两个队员
- 当走到最右端时,最高的队员一定在最右边
- 按照这个思路,从最左端重新开始,这次排到倒数第二个位置即可
- 依此类推,就可以完成数据的排序
选择排序的思路
选择排序对冒泡排序进行改进
- 将交换的次数由 O(N^2)减少到 O(N)
- 但是比较多次数依然是 O(N^2)
思路:
- 选定第一个索引位置,然后和后面元素依次比较
- 如果后面的位置,小于第一个索引位置,则交换位置
- 经过一轮的比较后,可以确定第一个位置是最小的
- 然后使用同样的方法把剩下的元素逐个比较
- 可以看出,选择排序第一轮会选出最小值,第二轮会选出第二小的值,以此类推
插入排序的思路
插入排序是简单排序中效率最好的的一种
插入排序的和核心是局部有序,那局部有序是? 例如在一个队列中,选择其中一个作为标记的队员 这个被标记的队员左边的所有队员已经是局部有序的 这意味着,一部分人是按顺序排列好的,有一部分还没有顺序
希尔排序
希尔排序相当于是插入排序的升级版
快速排序
快速排序其实是冒泡排序的升级版