复杂度
什么是算法
算法是用于解决特定问题一系列执行步骤
如果单从执行效率上进行评估,可能会想到这么一种方案比较不同算法对同一组输入的执行处理时间,这种叫事后统计法
评估算法优劣
时间复杂度:程序指令执行次数
空间复杂度:估算所需占用的存储空间
大O表示法
表示数据规模n对应的复杂度
9 >> O(1)
2n+3 >> O(n^2)
n^2^
9 >> O(1)
2n+3 >> O(n2)
n2 +2n +6 >> O(n2)
n3 +2n +6 >> O(n3)
对数阶的细节
所以 log2n 、log9n 统称为 logn
log2n = log29 ∗ log9n
算法优化方向
尽量少的存储空间
尽量少的执行步骤
空间换时间
时间换空间
多个数据规模
public static void test(int n, int k ){
for (int i = 0; i < n; i++) {
System.out.println("test");
}
for (int i = 0; i < k; i++) {
System.out.println("test");
}
}
补充
一般O(n)计算方法:
用常数1取代运行时间中所有加法常数。
在修改后的运行次数函数中,只保留最高阶项。
如果最高阶项系数存在且不是1,则去除与这个项相乘的常数。
空间复杂度:函数中创建对象的个数关于问题规模函数表达式,一般情况下也用O渐进表示法表示。
递归算法的空间复杂度:递归的深度*递归创建空间的大小。
什么是数据结构
数据结构使计算机存储,组织数据的方式
线性结构:行星表,数组链表栈队列,哈希表
树形结构:AVL树,红黑树,B树,堆,Trie,哈夫曼树,并查集
图形结构:邻接矩阵,邻接表
数组
数组是一种顺序存储线性表,所有元素内存地址是连续的
int [] array = new int[] {11,22,33}
动态数组接口设计
◼ int size(); // 元素的数量
◼ boolean isEmpty(); // 是否为空
◼ boolean contains(E element); // 是否包含某个元素
◼ void add(E element); // 添加元素到最后面
◼ E get(int index); // 返回index位置对应的元素
◼ E set(int index, E element); // 设置index位置的元素
◼ void add(int index, E element); // 往index位置添加元素
◼ E remove(int index); // 删除index位置对应的元素
◼ int indexOf(E element); // 查看元素的位置
◼ void clear(); // 清除所有元素
动态数组设计
成员变量会自动初始化
int 类型自动初始化 0
对象初始化null
添加元素add
打印数组
重写toString
在toString 方法中将元素拼接成字符串
字符串拼接建议使用StringBuilder
删除元素
添加元素
数组扩容
import java.util.Arrays;
public class CopyArrayDemo {
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
// 数组扩容(一)
// int[] arr = {1,2,3,4,5}; //数组arr的下标分别为:0 1 2 3 4
int[] arr_new = new int[6]; // 数组arr_new的下标分别为:0 1 2 3 4 5
for (int i = 0; i < arr.length; i++) { //遍历一遍,将数组arr的值传入数组arr_new中
arr_new[i] = arr[i];
}
// 为新数组赋值
arr_new[arr_new.length - 1] = 6;
for (int a : arr_new) {
System.out.print(a + " ");
}
System.out.println(); //换行
// 数组扩容(二)
// 第一个参数是拷贝的数组,第二个参数扩容长度,并且返回一个新的数组
int[] copyOf = Arrays.copyOf(arr, arr.length * 2);
for (int a : copyOf) {
System.out.print(a + " ");
}
System.out.println(); //换行
// 数组扩容(三)
int arr2[] = { 10, 20, 30, 40, 50, 60, 70, 80, 90 };
/*
* src:表示复制的源数组
* srcPos:代表从源数组哪一个元素开始复制(下标)
* dest:目标数组
* destPos:从目标数组的哪一个元素开始粘贴源数组的数据
* length:复制源数组的长度是多少
*/
System.arraycopy(arr, 1, arr2, 3, arr.length - 1);
for (int a : arr2) {
System.out.print(a + " ");
}
}
}
泛型
使用泛型技术让动态数组更加通用,可以存放任何数据类型
public class ArrayList<E>{
private int size;
private E[] elements;
}
elements = (E[]) new Object[capcity];
ArrayList<Interger> list = new ArrayList<>();
对象数组
链表
动态数组有个明显的缺点
可能会造成内存空间大量浪费
链表可以办到用多少就申请多少内存
链表是一个链式存储线性表,所有元素内存地址不一定连续
接口设计
清空元素
添加元素
node方法用于获取index位置节点
private Node<E> node(int index){
rangeCheck(index)
Node<E>node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node
}
添加元素
public void add(int index,E element){
rangeCheck(index)
if(index==0){
first = new Node<element,first>;
}else{
Node<E>prev = node(index-1);
prev.next = new Node<>(element,prev.next)
}
}
删除元素
反转一个列表
队列底层也可以使用动态数组实现,并且各项接口也可以优化到O(1)的时间复杂度,这个用数组实现并且优化之后的队列叫,循环队列
向下取整:只取前面的整数
4.1 4
4
向上取整:如果小数不为0,前面的整数加1,小数为0,只取整数
4.5 5
4.0 4
数组随机访问
随机访问速度快
elements[n]的效率与n是多少无关
动态数组链表复杂度分析
add
最好 O(1)
最坏O(n)
平均O(1)
均摊O(1)
问:什么情况下适合使用均摊复杂度
经过连续多次复杂度比较低的情况后,出现个别复杂度比较高
动态数组缩容
如果内存比较紧张,动态数组比较多的剩余空间可以考虑进行缩容操作
如果扩容倍数,缩容时机设计不当,有可能导致复杂度振荡
双向链表
使用双向链表可以提升链表综合性能
双向链表node法
双向链表add
remove
双向链表 vs 动态数组
◼ 动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)
◼ 双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费
◼ 如果频繁在尾部进行添加、删除操作,动态数组、双向链表均可选择
◼ 如果频繁在头部进行添加、删除操作,建议选择使用双向链表
◼ 如果有频繁的(在任意位置)添加、删除操作,建议选择使用双向链表
◼ 如果有频繁的查询操作(随机访问操作),建议选择使用动态数组
◼ 有了双向链表,单向链表是否就没有任何用处了?
并非如此,在哈希表的设计中就用到了单链表
至于原因,在哈希表章节中会讲到
单向循环列表
单向add
单向remove
双向循环列表
如何发挥循环列表最大威力
可以考虑1个成员变量,3个方法
current :用于指向某个节点
void reset() 让current 指向头节点 first
E next 让 current 往后走一步,也就是 current = current.next
E remove() :删除 current 指向的节点,删除成功后让 current 指向下一个节点
静态列表
前面所学习的链表,是依赖于指针(引用)实现的
◼ 有些编程语言是没有指针的,比如早期的 BASIC、FORTRAN 语言
◼ 没有指针的情况下,如何实现链表?
可以通过数组来模拟链表,称为静态链表
数组的每个元素存放 2 个数据:值、下个元素的索引
数组 0 位置存放的是头结点信息
思考:如果数组的每个元素只能存放 1 个数据呢?
那就使用 2 个数组,1 个数组存放索引关系,1 个数组存放值
栈
栈是一种特殊的线性表,只能在一端进行操作
往栈中添加元素操作,push 入栈
从栈中移除元素操作
pop 出栈,移除栈顶元素
后进先出
栈接口
int size() 元素数量
boolean isEmpty()是否是空
void push(E element)入栈
E top();获取栈顶元素
void clear() 清空
栈 括号面试题
判断括号的有效性可以使用「栈」这一数据结构来解决。
我们遍历给定的字符串 s。当我们遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。
当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回 False。为了快速判断括号的类型,我们可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。
在遍历结束后,如果栈中没有左括号,说明我们将字符串 s 中的所有左括号闭合,返回 True,否则返回 False。
注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 \text{False}False,省去后续的遍历判断过程。
class Solution {
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1) {
return false;
}
Map<Character, Character> pairs = new HashMap<Character, Character>() {{
put(')', '(');
put(']', '[');
put('}', '{');
}};
Deque<Character> stack = new LinkedList<Character>();
for (int i = 0; i < n; i++) {
char ch = s.charAt(i);
if (pairs.containsKey(ch)) {
if (stack.isEmpty() || stack.peek() != pairs.get(ch)) {
return false;
}
stack.pop();
} else {
stack.push(ch);
}
}
return stack.isEmpty();
}
}
时间复杂度:O(n)O(n),其中 nn 是字符串 ss 的长度。
空间复杂度:O(n+∣Σ∣),其中 Σ 表示字符集,本题中字符串只包含 66 种括号,∣Σ∣=6。栈中的字符数量为 O(n),而哈希表使用的空间为 O((∣Σ∣),相加即可得到总空间复杂度。
有效的括号
遇见左字符,将左字符入栈
如果栈是空的,说明括号无效
如果栈不是空的将栈顶字符出栈,和右字符匹配
如果左右字符不配,说明括号无效
如果左右字符匹配,继续扫描下一个字符
所有字符扫描完毕后
栈是空,说明括号有效
栈不是空,说明括号无效
队列
队列是特殊线性表,只能在头尾两端进行操作
队尾(rear) :只能从队尾添加元素,一般叫 enQueue 入队
对头(front) 只能从队头移除元素,一般叫 deQueue 出队
先进先出原则
队列接口设计
int size() 元素数量
boolean isEmpty()
void clear()
void enQueue (E element)
E deQueue() 出队
E front() 获取队列头元素
动态数组、链表
优先使用双向链表,因为队列主要是往头尾操作元素
用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
private int front;
public void push(int x) {
if (s1.empty())
front = x;
while (!s1.isEmpty())
s2.push(s1.pop());
s2.push(x);
while (!s2.isEmpty())
s1.push(s2.pop());
}
复杂度分析
时间复杂度:O(n)O(n)
对于除了新元素之外的所有元素,它们都会被压入两次,弹出两次。新元素只被压入一次,弹出一次。这个过程产生了 4n + 24n+2 次操作,其中 nn 是队列的大小。由于 压入 操作和 弹出 操作的时间复杂度为 O(1)O(1), 所以时间复杂度为 O(n)O(n)。
空间复杂度:O(n)O(n)
需要额外的内存来存储队列中的元素。
双端队列
双端队列是在头尾两端添加,删除队列
int size()
boolean isEmpty()
◼ void clear(); // 清空
◼ void enQueueRear(E element); // 从队尾入队
◼ E deQueueFront(); // 从队头出队
◼ void enQueueFront(E element); // 从队头入队
◼ E deQueueRear(); // 从队尾出队
◼ E front(); // 获取队列的头元素
◼ E rear(); // 获取队列的尾元素
循环队列底层使用动态数组实现,并且各项接口也可以优化到O(1)
时间复杂度
◼ 这个用数组实现并且优化之后的队列也叫做:循环队列
循环队列
循环双端队列
%运算符优化
尽量避免使用乘*、除/、模%、浮点数运算,效率低下
二叉树
节点根节点,父节点,子节点,兄弟节点
节点的度:子树的个数
树的度:所有节点度中最大值
叶子节点:度是0节点
非叶子节点:度不为0的节点
层数:根节点在1层,根节点子节点在2层
节点深度:从根节点到当前节点唯一路径上节点总数
节点的高度:从当前节点到最远叶子节点路径上节点总数
树的深度:所有节点深度最大值
树的高度:所有节点高度最大值
树的深度等于树的高度
有序树,无序树,森林
有序树
树中任意节点的子节点之间有顺序关系
无序树
树中任意节点的子节点之间没有顺序关系
也称为“自由树
森林
由 m(m ≥ 0)棵互不相交的树组成的集合
二叉树特点
每个节点度最大是2
左子树和右子树有顺序
即使是某节点只有一个子树,也要区分左右子树
非空二叉树的第 i 层,最多有 2i-1 个节点( i ≥ 1 )
在高度为 h 的二叉树上最多有 2h-1个结点( h ≥ 1 )
对于任何一棵非空二叉树,如果叶子节点个数为 n0,度为 2 的节点个数为 n2,则有: n0 = n2 + 1 假设度为 1 的节点个数为 n1,那么二叉树的节点总数 n = n0 + n1 + n2
二叉树的边数 T = n1 + 2 * n2 = n – 1 = n0 + n1 + n2 – 1
因此 n0 = n2 + 1
真二叉树
所有节点度都要么是0,要么是2
满二叉树
满二叉树:所有节点的度都要么为 0,要么为 2。且所有的叶子节点都在最后一层
假设满二叉树的高度为 h( h ≥ 1 ),那么 第 i 层的节点数量 2i-1
叶子节点数量2h-1
✓n = 2 h − 1 = 2 0 + 2 1 + 2 2 + ⋯ + 2 h−1
h = log2(n + 1)
完全二叉树
叶子节点只会出现最后2层,最后1层叶子节点都靠左对齐
完全二叉树从根节点到倒数第二层是一个满二叉树
满二叉树一定时完全二叉树,完全二叉树不一定是满二叉树
性质
度为1的节点只有左子树
度是1的节点要么是1个要么是0个
同样是节点数量的二叉树,完全二叉树高度最小
一棵有 n 个节点的完全二叉树(n > 0),从上到下、从左到右对节点从 1 开始进行编号,对任意第 i 个节点
如果 i = 1 ,它是根节点
如果 i > 1 ,它的父节点编号为 floor( i / 2 )
如果 2i ≤ n ,它的左子节点编号为 2i
如果 2i > n ,它无左子节点
如果 2i + 1 ≤ n ,它的右子节点编号为 2i + 1
如果 2i + 1 > n ,它无右子节点
一棵有 n 个节点的完全二叉树(n > 0),从上到下、从左到右对节点从 0 开始进行编号,对任意第 i 个节点
如果 i = 0 ,它是根节点
如果 i > 0 ,它的父节点编号为 floor( (i – 1) / 2 )
如果 2i + 1 ≤ n – 1 ,它的左子节点编号为 2i + 1
如果 2i + 1 > n – 1 ,它无左子节点
如果 2i + 2 ≤ n – 1 ,它的右子节点编号为 2i + 2
如果 2i + 2 > n – 1 ,它无右子节点
◼ 如果一棵完全二叉树有 768 个节点,求叶子节点的个数
假设叶子节点个数为 n0,度为 1 的节点个数为 n1,度为 2 的节点个数为 n2
总结点个数 n = n0 + n1 + n2,而且 n0 = n2 + 1
✓ n = 2n0 + n1 – 1
完全二叉树的 n1 要么为 0,要么为 1
✓ n1为1时,n = 2n0,n 必然是偶数
➢ 叶子节点个数 n0 = n / 2,非叶子节点个数 n1 + n2 = n / 2
✓ n1为0时,n = 2n0 – 1,n 必然是奇数
➢ 叶子节点个数 n0 = (n + 1) / 2,非叶子节点个数 n1 + n2 = (n – 1) / 2
叶子节点个数 n0 = floor( (n + 1) / 2 ) = ceiling( n / 2 )
非叶子节点个数 n1 + n2 = floor( n / 2 ) = ceiling( (n – 1) / 2 )
因此叶子节点个数为 384
二叉树遍历
把所有元素都访问一遍
线性数据结构遍历比较简单
正序遍历
逆序遍历
前序
中序
后序
层序
前序遍历
先访问根节点,前序遍历左子树,前序遍历右子树
中序遍历
左子树 ,根节点,右子树
后序遍历
左子树,右子树,根节点
层序遍历
从上到下,从左到右依次遍历
◼ 实现思路:使用队列
1. 将根节点入队
2. 循环执行以下操作,直到队列为空
将队头节点 A 出队,进行访问
将 A 的左子节点入队
将 A 的右子节点入队
/**
* 前序遍历
*/
public void preorderTraversal() {
preorderTraversal(root);
}
private void preorderTraversal(Node<E> node) {
if (node == null) return;
System.out.println(node.element);
preorderTraversal(node.left);
preorderTraversal(node.right);
}
/**
* 中序遍历
*/
public void inorderTraversal() {
inorderTraversal(root);
}
private void inorderTraversal(Node<E> node) {
if (node == null) return;
inorderTraversal(node.left);
System.out.println(node.element);
inorderTraversal(node.right);
}
/**
* 后序遍历
*/
public void postorderTraversal() {
postorderTraversal(root);
}
private void postorderTraversal(Node<E> node) {
if (node == null) return;
postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.println(node.element);
}
/**
* 层序遍历
*/
public void levelOrderTraversal() {
if (root == null) return;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
System.out.println(node.element);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
public static abstract class Visitor<E> {
boolean stop;
/**
* @return 如果返回true,就代表停止遍历
*/
public abstract boolean visit(E element);
}
public void preorder(Visitor<E> visitor) {
if (visitor == null) return;
preorder(root, visitor);
}
private void preorder(Node<E> node, Visitor<E> visitor) {
if (node == null || visitor.stop) return;
visitor.stop = visitor.visit(node.element);
preorder(node.left, visitor);
preorder(node.right, visitor);
}
public void inorder(Visitor<E> visitor) {
if (visitor == null) return;
inorder(root, visitor);
}
private void inorder(Node<E> node, Visitor<E> visitor) {
if (node == null || visitor.stop) return;
inorder(node.left, visitor);
if (visitor.stop) return;
visitor.stop = visitor.visit(node.element);
inorder(node.right, visitor);
}
public void postorder(Visitor<E> visitor) {
if (visitor == null) return;
postorder(root, visitor);
}
private void postorder(Node<E> node, Visitor<E> visitor) {
if (node == null || visitor.stop) return;
postorder(node.left, visitor);
postorder(node.right, visitor);
if (visitor.stop) return;
visitor.stop = visitor.visit(node.element);
}
优化
public void levelOrder(Visitor<E> visitor) {
if (root == null || visitor == null) return;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
if (visitor.visit(node.element)) return;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
树状打印二叉树
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
toString(root, sb, "");
return sb.toString();
}
private void toString(Node<E> node, StringBuilder sb, String prefix) {
if (node == null) return;
toString(node.left, sb, prefix + "L---");
sb.append(prefix).append(node.element).append("\n");
toString(node.right, sb, prefix + "R---");
}
完全二叉树判断
public boolean isComplete() {
if (root == null) return false;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
if (leaf && !node.isLeaf()) return false;
if (node.left != null) {
queue.offer(node.left);
} else if (node.right != null) { // node.left == null && node.right != null
return false;
}
if (node.right != null) {
queue.offer(node.right);
} else { // node.right == null
leaf = true;
}
}
return true;
}
计算二叉树的高度
public int height() {
if (root == null) return 0;
// 树的高度
int height = 0;
// 存储着每一层的元素数量
int levelSize = 1;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
levelSize--;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
if (levelSize == 0) { // 意味着即将要访问下一层
levelSize = queue.size();
height++;
}
}
return height;
}
public int height2() {
return height(root);
}
private int height(Node<E> node) {
if (node == null) return 0;
return 1 + Math.max(height(node.left), height(node.right));
}
反转二叉树
package datastruct.tree.翻转二叉树;
import java.util.LinkedList;
import java.util.Queue;
public class invertTree {
// 首先我们考虑的不是用什么方式遍历的问题,而是考虑题目想要干什么
public TreeNode invertTree1(TreeNode root){
if (root == null) return root;
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree1(root.left);
invertTree1(root.right);
return root;
}
// public TreeNode invertTree(TreeNode root) {
// if (root == null) return root;
//
// TreeNode tmp = root.left;
// root.left = root.right;
// root.right = tmp;
//
// invertTree(root.left);
// invertTree(root.right);
//
// return root;
// }
// public TreeNode invertTree(TreeNode root) {
// if (root == null) return root;
//
// invertTree(root.left);
// invertTree(root.right);
//
// TreeNode tmp = root.left;
// root.left = root.right;
// root.right = tmp;
//
// return root;
// }
// public TreeNode invertTree(TreeNode root) {
// if (root == null) return root;
//
// invertTree(root.left);
//
// TreeNode tmp = root.left;
// root.left = root.right;
// root.right = tmp;
//
// invertTree(root.left);
//
// return root;
// }
// 层序遍历
public TreeNode invertTree(TreeNode root) {
if (root == null) return root;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return root;
}
}
遍历应用
前序遍历
树状结构展示
中序遍历
二叉搜索树中序遍历按升序或者降序处理节点
后序遍历
先子后父
层序遍历
计算二叉树的高度
根据遍历结果重构二叉树
前序遍历 + 中序遍历
后序遍历 + 中序遍历
前序遍历 + 后序遍历
前+中
◼ 前序遍历:4 2 1 3 6 5 ◼ 中序遍历:1 2 3 4 5 6
前驱节点和后继节点
private Node<E> predecessor(Node<E> node) {
if (node == null) return null;
// 前驱节点在左子树当中(left.right.right.right....)
Node<E> p = node.left;
if (p != null) {
while (p.right != null) {
p = p.right;
}
return p;
}
// 从父节点、祖父节点中寻找前驱节点
while (node.parent != null && node == node.parent.left) {
node = node.parent;
}
// node.parent == null
// node == node.parent.right
return node.parent;
}
private Node<E> successor(Node<E> node) {
if (node == null) return null;
// 前驱节点在左子树当中(right.left.left.left....)
Node<E> p = node.right;
if (p != null) {
while (p.left != null) {
p = p.left;
}
return p;
}
// 从父节点、祖父节点中寻找前驱节点
while (node.parent != null && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
二叉搜索树
在 n 个动态的整数中搜索某个整数?(查看其是否存在)
◼ 针对这个需求,有没有更好的方案?
使用二叉搜索树,添加、删除、搜索的最坏时间复杂度均可优化至:O(logn)
0 1 2 3 4 5 6 7 8 9
31 66 17 15 28 20 59 88 45 56
◼ 如果维护一个有序的动态数组,使用二分搜索,最坏时间复杂度:O(logn)
但是添加、删除的平均时间复杂度是 O(n)
0 1 2 3 4 5 6 7 8 9
15 17 20 28 31 45 56 59 66 88
◼ 假设使用动态数组存放元素,从第 0 个位置开始遍历搜索,平均时间复杂度:O(n)
删除节点
删除度为0的节点,删除叶子节点
node == node.parent.left
✓ node.parent.left = null
node == node.parent.right
✓ node.parent.right = null
node.parent == null
✓ root = null
删除度为1的节点
用子节点替代源节点的位置
child 是 node.left 或者 child 是 node.right
用 child 替代 node 的位置
✓ 如果 node 是左子节点
➢ child.parent = node.parent
➢ node.parent.left = child
✓ 如果 node 是右子节点
➢ child.parent = node.parent
➢ node.parent.right = child
✓ 如果 node 是根节点
➢ root = child
➢ child.parent = null
删除度为2的节点(删除根节点)
◼ 举例:先删除 5、再删除 4
◼ 先用前驱或者后继节点的值覆盖原节点的值
◼ 然后删除相应的前驱或者后继节点
◼ 如果一个节点的度为 2,那么
它的前驱、后继节点的度只可能是 1 和 0
private void remove(Node<E> node) {
if (node == null) return;
size--;
if (node.hasTwoChildren()) { // 度为2的节点
// 找到后继节点
Node<E> s = successor(node);
// 用后继节点的值覆盖度为2的节点的值
node.element = s.element;
// 删除后继节点
node = s;
}
// 删除node节点(node的度必然是1或者0)
Node<E> replacement = node.left != null ? node.left : node.right;
if (replacement != null) { // node是度为1的节点
// 更改parent
replacement.parent = node.parent;
// 更改parent的left、right的指向
if (node.parent == null) { // node是度为1的节点并且是根节点
root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else { // node == node.parent.right
node.parent.right = replacement;
}
} else if (node.parent == null) { // node是叶子节点并且是根节点
root = null;
} else { // node是叶子节点,但不是根节点
if (node == node.parent.left) {
node.parent.left = null;
} else { // node == node.parent.right
node.parent.right = null;
}
}
}
------------------------------
找后继节点代码
private Node<E> successor(Node<E> node) {
if (node == null) return null;
// 前驱节点在左子树当中(right.left.left.left....)
Node<E> p = node.right;
if (p != null) {
while (p.left != null) {
p = p.left;
}
return p;
}
// 从父节点、祖父节点中寻找前驱节点
while (node.parent != null && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
--------------
node
private Node<E> node(E element) {
Node<E> node = root;
while (node != null) {
int cmp = compare(element, node.element);
if (cmp == 0) return node;
if (cmp > 0) {
node = node.right;
} else { // cmp < 0
node = node.left;
}
}
return null;
}
平衡二叉搜索树
二叉树复杂度分析
添加,删除节点会导致二叉搜索树退化成链表
平衡:当节点数量固定时,左右子树高度越接近,二叉树越平衡,最理想的平衡是满二叉树,高度差最小
改进二叉搜索树
首先,节点的添加、删除顺序是无法限制的,可以认为是随机的
◼ 所以,改进方案是:在节点的添加、删除操作之后,想办法让二叉搜索树恢复平衡(减小树的高度)
如果接着继续调整节点的位置,完全可以达到理想平衡,但是付出的代价可能会比较大
比如调整的次数会比较多,反而增加了时间复杂度
◼ 总结来说,比较合理的改进方案是:用尽量少的调整次数达到适度平衡即可
◼ 一棵达到适度平衡的二叉搜索树,可以称之为:平衡二叉搜索树
经典常见的平衡二叉搜索树有
AVL树
✓ Windows NT 内核中广泛使用
红黑树
✓ C++ STL(比如 map、set )
✓ Java 的 TreeMap、TreeSet、HashMap、HashSet
✓ Linux 的进程调度
✓ Ngix 的 timer 管理
一般也称它们为:自平衡的二叉搜索树(Self-balancing Binary Search Tree)
AVL树
平衡因子:某节点左右子树高度差
特点
每个节点平衡因子只可能是1,0,-1,绝对值不超过1
每个节点左右子树高度差不超过1
搜索添加删除时间复杂度是O(logn)
添加导致失衡
◼ 示例:往下面这棵子树中添加 13
◼ 最坏情况:可能会导致所有祖先节点都失衡
◼ 父节点、非祖先节点,都不可能失衡
LL – 右旋转(单旋)
◼ g.left = p.right
◼ p.right = g
◼ 让p成为这棵子树的根节点
◼ 仍然是一棵二叉搜索树:T0 < n < T1 < p < T2 < g < T3
◼ 整棵树都达到平衡
◼ 还需要注意维护的内容
T2、p、g 的 parent 属性
先后更新 g、p 的高度
RR – 左旋转(单旋)
◼ g.right = p.left
◼ p.left = g
◼ 让p成为这棵子树的根节点
◼ 仍然是一棵二叉搜索树:T0 < g < T1 < p < T2 < n < T3
◼ 整棵树都达到平衡
◼ 还需要注意维护的内容
T1、p、g 的 parent 属性
先后更新 g、p 的高度
LR – RR左旋转,LL右旋转(双旋)
RL – LL右旋转,RR左旋转(双旋)
package com.mj.tree;
import java.util.Comparator;
public class AVLTree<E> extends BST<E> {
public AVLTree() {
this(null);
}
public AVLTree(Comparator<E> comparator) {
super(comparator);
}
@Override
protected void afterAdd(Node<E> node) {
while ((node = node.parent) != null) {
if (isBalanced(node)) {
// 更新高度
updateHeight(node);
} else {
// 恢复平衡
rebalance(node);
// 整棵树恢复平衡
break;
}
}
}
@Override
protected void afterRemove(Node<E> node) {
while ((node = node.parent) != null) {
if (isBalanced(node)) {
// 更新高度
updateHeight(node);
} else {
// 恢复平衡
rebalance(node);
}
}
}
@Override
protected Node<E> createNode(E element, Node<E> parent) {
return new AVLNode<>(element, parent);
}
/**
* 恢复平衡
* @param grand 高度最低的那个不平衡节点
*/
@SuppressWarnings("unused")
private void rebalance2(Node<E> grand) {
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
Node<E> node = ((AVLNode<E>)parent).tallerChild();
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
rotateRight(grand);
} else { // LR
rotateLeft(parent);
rotateRight(grand);
}
} else { // R
if (node.isLeftChild()) { // RL
rotateRight(parent);
rotateLeft(grand);
} else { // RR
rotateLeft(grand);
}
}
}
/**
* 恢复平衡
* @param grand 高度最低的那个不平衡节点
*/
private void rebalance(Node<E> grand) {
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
Node<E> node = ((AVLNode<E>)parent).tallerChild();
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
rotate(grand, node, node.right, parent, parent.right, grand);
} else { // LR
rotate(grand, parent, node.left, node, node.right, grand);
}
} else { // R
if (node.isLeftChild()) { // RL
rotate(grand, grand, node.left, node, node.right, parent);
} else { // RR
rotate(grand, grand, parent.left, parent, node.left, node);
}
}
}
private void rotate(
Node<E> r, // 子树的根节点
Node<E> b, Node<E> c,
Node<E> d,
Node<E> e, Node<E> f) {
// 让d成为这棵子树的根节点
d.parent = r.parent;
if (r.isLeftChild()) {
r.parent.left = d;
} else if (r.isRightChild()) {
r.parent.right = d;
} else {
root = d;
}
//b-c
b.right = c;
if (c != null) {
c.parent = b;
}
updateHeight(b);
// e-f
f.left = e;
if (e != null) {
e.parent = f;
}
updateHeight(f);
// b-d-f
d.left = b;
d.right = f;
b.parent = d;
f.parent = d;
updateHeight(d);
}
private void rotateLeft(Node<E> grand) {
Node<E> parent = grand.right;
Node<E> child = parent.left;
grand.right = child;
parent.left = grand;
afterRotate(grand, parent, child);
}
private void rotateRight(Node<E> grand) {
Node<E> parent = grand.left;
Node<E> child = parent.right;
grand.left = child;
parent.right = grand;
afterRotate(grand, parent, child);
}
private void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) {
// 让parent称为子树的根节点
parent.parent = grand.parent;
if (grand.isLeftChild()) {
grand.parent.left = parent;
} else if (grand.isRightChild()) {
grand.parent.right = parent;
} else { // grand是root节点
root = parent;
}
// 更新child的parent
if (child != null) {
child.parent = grand;
}
// 更新grand的parent
grand.parent = parent;
// 更新高度
updateHeight(grand);
updateHeight(parent);
}
private boolean isBalanced(Node<E> node) {
return Math.abs(((AVLNode<E>)node).balanceFactor()) <= 1;
}
private void updateHeight(Node<E> node) {
((AVLNode<E>)node).updateHeight();
}
private static class AVLNode<E> extends Node<E> {
int height = 1;
public AVLNode(E element, Node<E> parent) {
super(element, parent);
}
public int balanceFactor() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
return leftHeight - rightHeight;
}
public void updateHeight() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
height = 1 + Math.max(leftHeight, rightHeight);
}
public Node<E> tallerChild() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
if (leftHeight > rightHeight) return left;
if (leftHeight < rightHeight) return right;
return isLeftChild() ? left : right;
}
@Override
public String toString() {
String parentString = "null";
if (parent != null) {
parentString = parent.element.toString();
}
return element + "_p(" + parentString + ")_h(" + height + ")";
}
}
}
统一所有旋转操作
删除导致失衡
可能会导致父节点或祖父节点失衡,其他节点,都不可能失衡
◼ 添加
可能会导致所有祖先节点都失衡
只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡【仅需 O(1) 次调整】
◼ 删除
可能会导致父节点或祖先节点失衡(只有1个节点会失衡)
恢复平衡后,可能会导致更高层的祖先节点失衡【最多需要 O(logn) 次调整】
◼ 平均时间复杂度
搜索:O(logn)
添加:O(logn),仅需 O(1) 次的旋转操作
删除:O(logn),最多需要 O(logn) 次的旋转操作
LL-右旋转
如果绿色节点不存在,更高层的祖先节点可能也会失衡,需要再次恢复平衡,然后又可能导致更高层的祖先节点失衡...
◼ 极端情况下,所有祖先节点都需要进行恢复平衡的操作,共 O(logn) 次调整
RR-左旋转
LR – RR左旋转,LL右旋转(双旋)
RL – LL右旋转,RR左旋转(双旋)
B树
B树是一种平衡多路搜索树,用于文件系统数据库
◼ 仔细观察B树,有什么眼前一亮的特点?
1 个节点可以存储超过 2 个元素、可以拥有超过 2 个子节点
拥有二叉搜索树的一些性质
平衡,每个节点的所有子树高度一致
比较矮
m阶B树性质
◼ 假设一个节点存储的元素个数为 x
根节点:1 ≤ x ≤ m − 1
非根节点:┌ m/2 ┐ − 1 ≤ x ≤ m − 1
如果有子节点,子节点个数 y = x + 1
✓ 根节点:2 ≤ y ≤ m
✓ 非根节点:┌ m/2 ┐ ≤ y ≤ m
➢ 比如 m = 3,2 ≤ y ≤ 3,因此可以称为(2, 3)树、2-3树
➢ 比如 m = 4,2 ≤ y ≤ 4,因此可以称为(2, 4)树、2-3-4树
➢ 比如 m = 5,3 ≤ y ≤ 5,因此可以称为(3, 5)树
➢ 比如 m = 6,3 ≤ y ≤ 6,因此可以称为(3, 6)树
➢ 比如 m = 7,4 ≤ y ≤ 7,因此可以称为(4, 7)树
B树和二叉搜索树逻辑上等价的
◼ 多代节点合并,可以获得一个超级节点
2代合并的超级节点,最多拥有 4 个子节点(至少是 4阶B树)
3代合并的超级节点,最多拥有 8 个子节点(至少是 8阶B树)
n代合并的超级节点,最多拥有 2
n个子节点( 至少是 2
n阶B树)
m阶B树,最多需要 log2m 代合并
搜索
- 现在节点内部从小到大开始搜索元素
- 如果命中,搜索结束
- 如果未命中,再去对应的子节点中搜索元素,重复步骤1
添加
新添加的元素必定是添加到叶子节点上
添加 – 上溢的解决(假设5阶)
上溢节点元素个数必然是m
假设上溢节点中间元素位置是k,将k位置元素向上与父节点合并
将 [0, k-1] 和 [k + 1, m - 1] 位置的元素分裂成 2 个子节点
✓ 这 2 个子节点的元素个数,必然都不会低于最低限制(┌ m/2 ┐ − 1)
◼ 一次分裂完毕后,有可能导致父节点上溢,依然按照上述方法解决
最极端的情况,有可能一直分裂到根节点
删除叶子节点
假如入需要删除叶子节点,直接删
删除非叶子节点
假如需要删除元素在非叶子节点中
- 先找到前驱或后继元素,覆盖所需要删除元素的值
- 再把前驱或后继元素删除
非叶子节点前驱或后继元素,必定在叶子节点中
真正删除的元素都是发生在叶子节点中
下溢
◼ 删除 22 ?(假设这是一棵 5阶B树)
叶子节点被删掉一个元素后,元素个数可能会低于最低限制( ≥ ┌ m/2 ┐ − 1 )
这种现象称为:下溢(underflow)
下溢解决
◼ 下溢节点的元素数量必然等于 ┌ m/2 ┐ − 2
◼ 如果下溢节点临近的兄弟节点,有至少 ┌ m/2 ┐ 个元素,可以向其借一个元素
将父节点的元素 b 插入到下溢节点的 0 位置(最小位置)
用兄弟节点的元素 a(最大的元素)替代父节点的元素 b
这种操作其实就是:旋转
◼ 如果下溢节点临近的兄弟节点,只有 ┌ m/2 ┐ − 1 个元素
将父节点的元素 b 挪下来跟左右子节点进行合并
合并后的节点元素个数等于┌ m/2 ┐ + ┌ m/2 ┐ − 2,不超过 m − 1
这个操作可能会导致父节点下溢,依然按照上述方法解决,下溢现象可能会一直往上传播
删除
4阶B树
◼ 4阶B树的性质
所有节点能存储的元素个数 x :1 ≤ x ≤ 3
所有非叶子节点的子节点个数 y :2 ≤ y ≤ 4
◼ 添加
从 1 添加到 22
◼ 删除
从 1 删除到 22
红黑树
红黑树是一种自平衡二叉搜索树
五条性质
- 节点是红色
- 根节点是黑色
- 叶子节点(外部节点,空节点)都是黑色
- 红色节点的子节点都是黑色
- 红色节点的父节点都是黑色
- 从根节点到叶子节点所有路径上不能有两个连续的红色节点
- 从任一节点到叶子节点所有路径上都包含相同数目的黑色节点
红黑树的等价交换
红黑树和4阶B树具有等价性
黑色节点和红色子节点融合在一起,形成一个B树节点
红黑树的黑色节点个数和4阶B树节点个数相等
◼ parent:父节点
◼ sibling:兄弟节点
◼ uncle:叔父节点( parent 的兄弟节点)
◼ grand:祖父节点( parent 的父节点)
添加
已知
B树中,新元素必定是添加到叶子节点中
4阶B树所有节点的元素个数 x 都符合 1 ≤ x ≤ 3
◼ 建议新添加的节点默认为 RED,这样能够让红黑树的性质尽快满足(性质 1、2、3、5 都满足,性质 4 不一定)
添加根节点,直接染成黑色
所有情况
◼ 有 4 种情况满足红黑树的性质 4 :parent 为 BLACK
同样也满足 4阶B树 的性质
因此不用做任何额外处理
◼ 有 8 种情况不满足红黑树的性质 4 :parent 为 RED( Double Red ) 其中前 4 种属于B树节点上溢的情况
添加 – 修复性质4 – LL\RR
◼ 判定条件:uncle 不是 RED
1. parent 染成 BLACK,grand 染成 RED
2. grand 进行单旋操作
LL:右旋转
RR:左旋转
添加 – 修复性质4 – LR\RL
◼ 判定条件:uncle 不是 RED
1. 自己染成 BLACK,grand 染成 RED
2. 进行双旋操作
LR:parent 左旋转, grand 右旋转
RL:parent 右旋转, grand 左旋转
添加 – 修复性质4 – 上溢 – LL
◼ 判定条件:uncle 是 RED
1. parent、uncle 染成 BLACK
2. grand 向上合并
染成 RED,当做是新添加的节点进行处理
◼ grand 向上合并时,可能继续发生上溢
◼ 若上溢持续到根节点,只需将根节点染成 BLACK
添加 – 修复性质4 – 上溢 – RR
◼ 判定条件:uncle 是 RED
1. parent、uncle 染成 BLACK
2. grand 向上合并
染成 RED,当做是新添加的节点进行处理
添加 – 修复性质4 – 上溢 – LR
◼ 判定条件:uncle 是 RED
1. parent、uncle 染成 BLACK
2. grand 向上合并
染成 RED,当做是新添加的节点进行处理
添加 – 修复性质4 – 上溢 – RL
◼ 判定条件:uncle 是 RED
1. parent、uncle 染成 BLACK
2. grand 向上合并
染成 RED,当做是新添加的节点进行处理
删除:最后被删除的元素都在叶子节点
删除 – RED节点
直接删除,不用作任何调整
删除 – BLACK节点
拥有 2 个 RED 子节点的 BLACK 节点
不可能被直接删除,因为会找到它的子节点替代删除
不用考虑
拥有 1 个 RED 子节点的 BLACK 节点
BLACK 叶子节点
删除 – 拥有1个RED子节点的BLACK节点
判定条件:所以替代的子节点是RED
将替代的子节点染成黑色既可以保持红黑树的性质
删除 – BLACK叶子节点 – sibling为BLACK
BLACK 叶子节点被删除后,会导致B树节点下溢
如果sibling至少有一个RED子节点
进行旋转操作
旋转之后中心节点继承parent颜色
旋转之后左右节点染成黑色
删除 – BLACK叶子节点 – sibling为BLACK
判定条件:sibling 没有 1 个 RED 子节点
◼ 将 sibling 染成 RED、parent 染成 BLACK 即可修复红黑树性质
如果 parent 是 BLACK
会导致 parent 也下溢
这时只需要把 parent 当做被删除的节点处理即可
删除 – BLACK叶子节点 – sibling为RED
◼ 如果 sibling 是 RED
sibling 染成 BLACK,parent 染成 RED,进行旋转
于是又回到 sibling 是 BLACK 的情况
红黑树平衡
最初遗留的困惑:为何那5条性质,就能保证红黑树是平衡的?
那5条性质,可以保证 红黑树 等价于 4阶B树
没有一条路径会大于其他路径2倍
◼ 相比AVL树,红黑树的平衡标准比较宽松:没有一条路径会大于其他路径的2倍
◼ 是一种弱平衡、黑高度平衡
◼ 红黑树的最大高度是 2 ∗ log2(n + 1) ,依然是 O(logn) 级别
平均时间复杂度
◼ 搜索:O(logn)
◼ 添加:O(logn),O(1) 次的旋转操作
◼ 删除:O(logn),O(1) 次的旋转操作
了解————
◼ AVL树
平衡标准比较严格:每个左右子树的高度差不超过1
最大高度是 1.44 ∗ log2 n + 2 − 1.328(100W个节点,AVL树最大树高28)
搜索、添加、删除都是 O(logn) 复杂度,其中添加仅需 O(1) 次旋转调整、删除最多需要 O(logn) 次旋转调整
◼ 红黑树
平衡标准比较宽松:没有一条路径会大于其他路径的2倍
最大高度是 2 ∗ log2(n + 1)( 100W个节点,红黑树最大树高40)
搜索、添加、删除都是 O(logn) 复杂度,其中添加、删除都仅需 O(1) 次旋转调整
◼ 搜索的次数远远大于插入和删除,选择AVL树;搜索、插入、删除次数几乎差不多,选择红黑树
◼ 相对于AVL树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树
◼ 红黑树的平均统计性能优于AVL树,实际应用中更多选择使用红黑树