复杂度
(通过复杂度可以评判出算法的性能)
1. 时间复杂度:
估算程序指令的执行次数(执行时间)
2. 空间复杂度:
估算程序执行所占用的存储空间
3. 大O表示法:
用该方法可以估算出算法的复杂度
方法:忽略常数,系数,低阶
9 -----> O(1)
2n + 3 -----> O(n)
n^2 + 2n + 6 -----> O(n^2)
4n^3 + 3n^2 + 22n + 5 -----> O(n^3)
复杂度大小比较:
O(1) < O(logn) < O(n) < O(n logn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
例如:
public static int fib2(int n) {
if (n <= 1) return n;
int first = 0;
int second = 1;
for (int i = 0; i < n - 1; i++) {
int sum = first + second;
first = second;
second = sum;
}
return second;
}
这段算法的时间复杂度为:O(n)
空间复杂度为:4
链表(LinkedList)
1. 链表设计:
- 概念:链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的,链表的每个节点(Node)中包含两个组成部分:元素:element,下一个Node的地址:next;
链表的尾结点指向null,头结点被“LinkedList(虚拟头节点)”的first指向:
- 优点:
- 节省了内存:链表不像动态数组,size不足够的时候还需要扩容,链表则为添加一个元素,随时添加一个节点,删除一个元素,便删除一个节点;
- 增删的速度很快:链表的增删只需要改变该位置前后节点的指向,而数组则需要整体挪动;
- 缺点:
链表查找元素的速度相对较慢,需要从头节点开始依次按顺序遍历;不像数组可以进行随机查找,因为jvm会根据数组的索引找打数据的内存地址;
2. 动态数组的缩容:
如果对数组的数据进行删除,当剩余的容量过大时,为了节省内存,就产生了缩容这一操作;根据实际需求,可自行调节缩容时机;
例如当size < 容量的一半时进行缩容;
但要掌握好扩容缩容的时机,不然可能会导致复杂度震荡。
3. 双向链表:
双向链表的性能比单向链表更高,它的虚拟节点中包含了头节点(first)和尾节点(last),且每个节点都包含:元素,下一节点地址,上一节点地址;其中头节点的上一节点(prev)指向null,尾节点的下一节点(next)指向null;
栈(Stack)
栈是一种特殊的线性表,只能在一端进行操作,也就是只能对栈顶的数据进行操作,栈中的数据对外界是封闭的;
- 入栈(push):往栈中添加元素;
- 出栈(pop):移除栈顶元素;
都遵循后进先出的原则
队列(Queue)
队列是一种特殊的线性表,只能在头尾两端进行操作;
队尾(rear):只能从队尾添加元素,叫做enQueue,入队;
对头(front):只能从对头移除元素,叫做deQueue,出队;
1. 双端队列(Deque)
双端队列可以再头尾两端进行添加和删除;
2. 循环队列(Circle Queue)
头尾之间连接在一起,形成了循环;
循环双端队列:可以进行头尾两端的增删操作;
二叉树(Binary Tree)
基本概念:
- 空树:没有任何节点的树
- 只有一个节点的二叉树即只有根节点;
- 节点的度:子树的个数
- 树的度:所有节点度中的最大值;
- 叶子节点:度为0的节点;
- 非叶子节点:度部位零的节点;
- 层数(level):根节点在第一层,根节点的子节点在第二层,以此类推;
- 节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数;
- 节点的高度:从当前节点到最远叶子节点的路径上的节点总数;
- 树的深度:所有节点深度中的最大值;
- 树的高度:同上;
- 树的深度:等于树的高度;
- 有序树:树中任意节点之间有顺序关系;
- 无序树:树中任意子节点之间没有顺序关系(自由树)
- 森林:由m(m>=0)颗互不相交的树组成的集合;
二叉树:
- 特点:
每个节点的度最大为2(最多拥有两颗子树)
左子树和右子树是有顺序的
即使某节点只有一颗子树,也要区分左右子树 - 性质:
- 非空二叉树的第i层,最多有2^i-1个节点(i >= 1)
- 在高度为h的二叉树上最多有(2^h)-1个节点(h >= 1);
1. 真二叉树(Proper Binary Tree)
所有节点的度要么为0,要么为2;
2. 满二叉树(Full Binary Tree)
最后一层节点的度都为0,其他节点度都为2
3. 完全二叉树(Complete Binary Tree)
对节点从上至下,从左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应;
叶子节点只会出现在最后两层,最后一层的叶子节点都靠左对齐;
完全二叉树从根节点至倒数第二层是一颗满二叉树;
满二叉树一定是完全二叉树,反之不易然;
- 性质:
- 度为1的节点只有左子树;
- 度为1的节点要么是一个,要么是0个;
- 同样节点数量的二叉树,完全二叉树的高度最小;
- 假设完全二叉树的高度为h(h>=1),那么:
至少有2 ^(h-1)个节点
最多有(2^h)-1个节点;
二叉树的遍历:
1. 前序遍历(Preorder Traversal):
- 访问顺序:根节点,前序遍历左子树,前序遍历右子树;
2. 中序遍历(Inorder Traversal):
访问顺序:中序遍历左子树,根节点,中序遍历右子树;
3. 后序遍历(Postorder Traversal):
访问顺序:后序遍历左子树,后序遍历右子树,根节点;
4. 层序遍历(Level Order Traversal):
访问顺序:从上到下,从左到右依次访问每一个节点;
二叉搜索树(Binary Search Tree):
- 性质:
- 任意一个节点的值都大于其左子树所有节点的值;
- 任意一个节点的值都小于其右子树所有节点的值;
- 他的左右子树也是一颗二叉搜索树;
平衡二叉搜索树( Balanced Binary Search Tree):
- 平衡:
当节点数量固定时,左右子树的高度越接近,这颗二叉树越平衡;
最理想的平衡就是想像完全二叉树,满二叉树那样,高度是最小的;
常见的BBST有:AVL树,红黑树;
AVL树:
AVL树:
AVL树是最早发明的自平衡二叉搜索树之一;
- 平衡因子(Balance Factor):某节点左右子树的高度差;
- AVL树的特点:
- 每个节点的平衡因子只可能是1,0,-1(即绝对值 <= 1,如果不在该范围内称之为失衡);
- 搜索,添加,删除的时间复杂度是O(log n);
简单的继承结构:
添加导致失衡:
往以下的AVL树中添加13,会导致失衡;
最坏情况:可能导致所有的祖先节点都失衡;
父节点,非祖先节点都不可能失衡;
删除导致失衡:
可能会导致父节点或祖先节点失衡(只有一个节点会失衡),其他节点都不会影响;
总结:
- 添加:
- 可能会导致所有祖先节点都失衡;
- 只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡(仅需O(1)次调整);
- 删除:
- 可能会导致父节点或祖先节点失衡(只有一个节点会失衡);
- 恢复平衡后,可能会导致更高层的祖先节点失衡(最多需要O(log n)次调整);
B树:
B树是一种平衡的多路搜索树,多用于文件系统,数据库的实现;
m阶B树的性质(m >= 2):
令一个节点存储的元素个数为x:
根节点:1 <= x <= m - 1
非根节点:「m / 2⌉ - 1 <= x <= m - 1 (向上取整)
如果有子节点,子节点个数 y = x + 1;
根节点:2 <= y <= m
非根节点:「m / 2⌉ <= y <= m
搜索:
跟二叉树的搜索类似:
- 先在节点内部从小到大开始搜索元素;
- 如果命中,搜索结束;
- 如果未命中,再去对应的子节点中搜索元素,重复步骤1;
添加:
新添加的元素必定是添加到叶子节点
添加 - 上溢:
假设m = 5阶;
假设上溢节点中最中间元素的位置为k;
上溢节点的元素个数必然等于5;
- 将k位置的元素向上与父节点合并;
- 将【0,k-1】和【k + 1,m - 1】位置的元素分裂成两个子节点;
一次分裂完毕后,有可能导致父节点上溢,依然按照上述方法解决;最极端的情可能一直分裂到根节点;
删除:
- 删除的如果为叶子节点,那么直接删除即可;
- 删除非叶子节点:
先找到前驱或后继元素,覆盖所需删除的元素的值,再把前驱或后继元素删除;
非叶子节点的前驱或后继元素必定在叶子节点中;
删除 - 下溢:
叶子节点被删掉一个元素后,元素个数可能会低于最低限制(>= 「m / 2⌉ - 1)
下溢节点的元素数量必然等于「m / 2⌉ - 2;
- 如果下溢节点的临近兄弟节点有至少「m / 2⌉ 个元素,可以向其借一个元素;
- 将父节点的元素b插入到下溢节点的0位置(最小位置)
- 用兄弟节点的元素a(最大元素)代替父节点元素b;
- 如果下溢节点临近兄弟节点只有「m / 2⌉ - 1 个元素;
- 将父节点的元素b挪下来跟左右子节点进行合并;
- 合并后的节点元素个数等于「m / 2⌉ + 「m / 2⌉ - 2,不超过m - 1;
- 这个操作可能会导致父节点下溢,依然重复上述解决方法;
红黑树(RBTree):
红黑树也是一种自平衡二叉搜索树,由AVL树优化而来;
红黑树的5条性质:
- 节点是RED或者BLACK;
- 根节点是BLACK;
- 叶子节点(外部节点,空节点)都是BLACK;
- RED节点的子节点都是BLACK,RED节点的父节点都是BLACK,从根节点到叶子节点的所有路径上不能有两个连续的RED节点;
- 从任一节点到叶子节点的所有路径都包含相同数目的BLACK节点;
红黑树与四阶B树:
红黑树与四阶B树具有等价性,BLACK和他的RED子节点融合在一起,形成一个B树节点,红黑树的BLACK节点个数4阶B树的节点总数相等;
添加:
红黑树将继承一个艾薇儿树(AVLT)也继承的类(BBST),这个类中封装了旋转方法,而BBST继承二叉搜索树(BST),BST又继承二叉树(BT),所以红黑树的添加方法直接来自于二叉搜索树BST,添加后所需要的有关红黑树独特的特性的调整再进行处理(afteradd);BST中主要封装了添加,删除,比较器方法;BT中主要封装了前序遍历,中序遍历,后序遍历,层次遍历,树高度等方法;
- 已知条件:
B树中新元素必定添加到叶子节点中;
4阶B树的所有节点元素个数x都符合 1 <= x <= 3; - 添加的所有情况(共12种):
前4种(满足4阶B树的性质)parent为BLACK:
1 ~ 4. 不需要做任何处理;
后8种(不满足4阶B树的性质)parent为RED:
5 ~ 8. 后8种的前4种属于B树节点上溢;
判定条件:uncle不是RED:(LL/RR)
parent染成BLACK,grand染成RED,grand进行单旋操作(LL右旋/RR左旋);
6. 8.
- 判定条件:uncle不是RED:(LR/RL)
自己染成BLACK,grand染成RED,进行双旋操作;(LR:parent 左旋,grand右旋 / RL:parent右旋,grand左旋)
7. 10. 11. 12.
- 判定条件:uncle是RED《B树上溢》:(LL/RR/LR/RL)
parent,uncle染成BLACK,grand向上合并;
grand向上合并时可能继续发生上溢,若上溢持续到根节点,只需将根节点染成BLACK即可;
LL
RR
LR
RL
删除:
红黑树的删除和添加类似,删除的具体方法都封装在BST(二叉搜索树)中,且在B树中,最后真正被删除的元素都在叶子节点中,所以删除红黑树元素时,调用的BST中的删除方法只会处理叶子节点的元素(非叶子节点的删除会找到该节点的前驱节点或后继节点,然后用前驱或后继的值去覆盖要删除节点的值,再删除前驱或后继节点,所以在红黑树封装的类中,afterremove只需要处理被删除的叶子节点中的元素);
- 删除RED节点:
直接删除后不做任何处理;
- 删除BLACK节点(1)拥有1个RED子节点的BLACK节点:
- 判定条件:用以替代的子节点是RED
将替代的子节点染成BLACK即可保持红黑树的性质;
- 删除BLACK节点(2)sibling(兄弟节点)为BLACK,且sibling至少有1个RED子节点:
BLACK叶子节点被删除后,会导致B树节点下溢;
进行旋转操作,旋转后的中心节点继承parent的颜色,旋转后的左右节点染成BLACK;
- 删除BLACK节点(3)sibling(兄弟节点)为BLACK,且sibling没有一个RED子节点:
若parent为RED,将sibling染成RED,parent染成BLACK即可;
若parent为BLACK,会导致parent也下溢,这是只需要把parent也当做被删除的节点再次传给afterRemove即可;
- 删除BLACK节点(4)sibling(兄弟节点)为RED:
sibling染成BLACK,parent染成RED,进行旋转,于是变成了sibling是BLACK的情况;
红黑树的平衡:
红黑树的五条性质保证了红黑树等价于4阶B树,
相比于AVL树,红黑树的平衡标准比较宽松:没有一条路径的长度会大于其他路径的2倍;
红黑树的最大高度是2 * log2(n + 1),依然是O(log n)级别;
集合(Set)
- 集合的特点:
不存放重复的元素;
常用于去重:存放新增IP,统计新增IP。存放词汇,统计词汇量;
集合的底层可以用红黑树实现,效率相当高;
映射(Map)
- Map中的每一个key都是唯一的,同样不存放重复元素;
- Map的所有key组合在一起,其实就是一个Set;
哈希表(Hash)
哈希表(HashTable)
添加,删除,搜索都需要利用哈希函数生成key对应的index,然后在根据index操作指定数组元素;
哈希冲突(Hash Collision)
也叫哈希碰撞:两个不同的key,经过哈希函数计算出相同的结果,也就是index相同,如下图:
处理哈希冲突的常见方法:
- 开放地址法:按照一定的规则向其他地址探测,直到遇到空桶;
- 再哈希法:设计多个哈希函数;
- 链地址法:比如通过链表将同一index的元素串起来;
JDK1.8的哈希冲突解决方法:
- 默认使用单向链表将元素串起来;
- 在添加元素时,可能会由单向链表转化为红黑树;(比如官方为哈希表容量 >= 64,且单向链表的节点数量大于8时);
- 当红黑树的节点数量少到一定程度,又会变回单向链表;
哈希函数:
-
哈希表中的哈希函数实现步骤:
先生成key的哈希值(必须是整数)
再让key的哈希值跟数组的大小进行相关运算,生成一个索引值 -
生成key的哈希值的方法:
尽量让每个key的哈希值是唯一的;
尽量让key的所有信息参与运算;
在java中,HashMap的key必须实现hashCode,equals方法,也允许key为null;
装填因子:
Load Factor: = 节点总数量/哈希表桶数组长度
在JDK1.8中,若装填因子超过0.75,就扩容为原来的2倍;