数据结构
0x00 概述
(1)数据结构
a. 定义
- 数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及他们之间的关系和操作等相关问题的学科
b. 分类
一般分为两大类:逻辑结构、物理结构
-
逻辑结构
逻辑结构是从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象中数据元素之间的相互关系分类
-
集合结构:元素除了属于同一集合外,之间没有任何其他关系
graph TB A B C D E F -
线性结构:元素之间存在一对一的关系
graph LR A-->B-->C-->D-->E-->F -
树形结构:元素之间存在一对多的关系
graph TB A-->B & C B-->D & E C-->F -
图形结构:元素之间存在多对多的关系
flowchart LR A-->B-->C-->A-->D B-->D-->C E-->F-->E
-
-
物理结构
逻辑结构在计算机中真正的表示方式(又称映像)成为物理结构,又称存储结构
-
顺序存储结构
数据元素在地址连续的存储单元里面,其数据间逻辑结构与物理结构一致,如数组
-
链式存储结构
数据元素在地址不连续的存储单元里面,通过在存储单元中引进一个指针来存放下一个元素的地址
-
(2)算法
- 算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,代表着用系统的方法解决问题的策略机制。
(3) 算法分析
- 算法的优劣可以通过各自运行时间与储存空间进行量化比较,量化的结果别为时间复杂度和空间复杂度
a. 大 \(O\) 阶
I. 函数渐进增长
- 概念:给定两个函数 \(f(n)\) 和 \(g(n)\),如果存在一个整数 \(N\),使得对于所有的 \(n \gt N\),\(f(n)\) 总是比 \(g(n)\) 大,那么我们说 \(f(n)\) 的增长渐进快于 \(g(n)\)。
- 重要结论
- 随着输入规模的增大,算法的常数操作可以忽略不计
- 随着输入规模的增大,与最高次项相乘的常数可以忽略
- 最高次项的指数大的,随着自变量 \(n\) 的增长,因变量 \(f(n)\) 也会增长的特别快
- 算法函数中 \(n\) 的最高次幂越小,算法效率越高
II. 算法的时间复杂度
- 大 \(O\) 记法:在进行算法分析时,语句执行总的次数 \(T(n)\) 是关于问题规模 \(n\) 的函数。算法的时间复杂度就是算法的时间量度,记作:\(T(n) = O(f(n))\),表示随着问题规模 \(n\) 的增大,算法执行时间的增长率和 \(f(n)\) 的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度
- 执行次数 = 执行时间
- 推导大 \(O\) 阶的表示法有以下几条规则
- 用常数 \(1\) 取代运行时间中所有的加法常数
- 在修改后的运行次数中,只保留高阶项
- 如果最高阶项存在,且常数因子不为 \(1\),则去除与这个项相乘的常数
III. 常见的大 \(O\) 阶
-
常数阶:一般不涉及循环的都是常数阶,不会随着输入规模的扩大而增加操作次数
sum = (n+1)*n/2;
上述算法的时间复杂度为 \(O(1)\)
-
线性阶:一般含有非嵌套循环涉及线性阶。线性阶就是随着输入规模的扩大,对应的计算次数呈直线增长
for (int i = 0; i < n; i++) { sum += i; }
上述循环的时间复杂度为 \(O(n)\)
-
乘方阶:一般嵌套循环涉及乘方阶,一般形式为 \(O(n^m)\),其中 \(m\) 为循环嵌套层数
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { for (int k = 0; k < n; k++) { sum += k; } } }
上述循环的时间复杂度为 \(O(n^3)\)
-
对数阶:
while (i < n) { i = i * 2; }
- 上述循环中,每次
i *= 2
后,i
都更加接近n
,如果有 \(x\) 个 2 相乘后会使i
大于n
,那么就会结束循环,此时 \(2^x = n\),即 \(x = \log_2n\) - 由于随着输入规模 \(n\) 的增大,无论底数为多少,其增长趋势不变,所有底数可以被忽略
综上,循环的时间复杂度为 \(O(\log n)\)
- 上述循环中,每次
汇总:从上到下复杂度逐级增高
描述 | 大\(O\)阶 | 说明 | 举例 |
---|---|---|---|
常数级 | \(O(1)\) | 普通语句 | 两数加法 |
对数级 | \(O(\log n)\) | 二分法 | 二分查找 |
线性级 | \(O(n)\) | 一层循环 | 找出数组中的最小值 |
线性对数级 | \(O(n\log n)\) | 分治思想 | 归并排序 |
平方级 | \(O(n^2)\) | 双层循环 | 遍历二维数组中所有元素对 |
立方级 | \(O(n^3)\) | 三层循环 | 遍历三维数组中所有元素对 |
指数级 | \(O(2^n)\) | 穷举查找 | 遍历所有子集 |
b. 时间复杂度
I. 概述
-
使用高级语言编写的程序在计算机上运行所消耗的时间取决以下因素
- 算法采用的策略和方案
- 编译产生的代码质量
- 问题的输入规模
- 机器执行指令的速度
由于第 2 和 4 条无法人为控制,因此算法时间复杂度仅须考虑算法方案和输入量即可
-
举例:从 1 加到 n
-
算法一:使用
for
循环模拟public static void main(String[] args) { int sum = 0; int n = 100; for (int i = 1; i <= n; i++) { // 执行 n+1 次 sum += i; // 执行 n 次 } System.out.println("sum = " + sum); }
-
算法二:使用数学方法直接计算
public static void main(String[] args) { int sum = 0; int n = 100; sum = (n+1)*n/2; System.out.println("sum = " + sum); }
此时,算法一执行了 \(1+1+(n+1)+n = 2n+3\) 次,算法二执行了 \(1+1+1 = 3\) 次。如果将算法一的循环体看作一个整体,并忽略结束的条件判断时,两种算法的差距为 \(O(n)\) 和 \(O(1)\)。
-
II. 情况分类
-
算法分析需要考虑到不同情况,包括最好情况、最坏情况、平均情况
-
举例:遍历查找出指定的数字
for (int i = 0; i < array.length; i++) { if (i == number) { return true; } } return false;
- 最好情况:查找到第一个元素就是期望结果,此时时间复杂度为 \(O(1)\)
- 最坏情况:查找到最后一个元素是期望结果,此时时间复杂度为 \(O(n)\)
- 平均情况:\(O(n/2)\)
c. 空间复杂度
I. Java 中常见内存占用
-
基本数据类型
数据类型 内存占用字节数 byte 1 short 2 int 4 long 8 float 4 double 8 boolean 1 char 2 -
计算机访问内存的方式是 1 字节/次
-
一个引用(机器地址)需要 8 字节
-
每个对象自身的开销为 16 字节,用来保存对象的头信息
-
一般内存的使用如果不够 8 字节,则会自动填充至 8 字节
-
一个原始数据类型的数组一般需要 24 字节的头信息(16 个自身对象的开销,4 个用于保存长度,4 个填充细节)
II. 算法的空间复杂度
-
计算公式:\(S(n) = O(f(n))\),其中 \(n\) 为输入规模,\(f(n)\) 为语句关于 \(n\) 所占存储空间的函数
-
举例:对指定的数组元素进行反转并返回反转的内容
-
算法一:数组内部交换位置
public static int[] reverse(int[] array) { int length = array.length; // 4 个字节 int temp; // 4 个字节 for (int start = 0, end = length - 1; start <= end; start++, end--) { temp = array[start]; array[start] = array[end]; array[end] = temp; } return array; }
-
算法二:使用临时数组接收
public static int[] reverse(int[] array) { int length = array.length; // 4 个字节 int[] newArray = new int[length]; // 24 + 4*n 个字节 for (int i = length - 1; i >= 0; i--) { newArray[length-1-i] = array[i]; } return newArray; }
忽略判断条件占用的内存,对各算法内存占用分析,发现算法一无论传入数组大小,始终需要申请 \(4 + 4 = 8\) 个字节;算法二需要申请 \(4 + 24 + 4n = 4n + 28\) 个字节
根据大 \(O\) 推导法则,算法一空间复杂度为 \(O(1)\),算法二空间复杂度为 \(O(n)\)
-
以下数据结构与算法的 Java 实现均通过下述 API 表示,以 ArrayList 类为例:
public class ArrayList { // 构造方法 public ArrayList() {} // 添加元素 public boolean add(E e) {} // 删除指定元素 E remove(int index) {} }
0x01 排序
-
Java 中提供了一个 Comparable 接口,用来定义排序规则
-
举例:
-
定义学生类,具有
name
和age
两个属性,并通过 Comparable 接口提供比较规则package com.example.example01; public class Student implements Comparable<Student> { String name; int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student other) { return this.getAge() - other.getAge(); } }
-
定义测试类,其中定义测试方法
Comparable getMax(Comparable c1, Comparable c2)
package com.example.example01; public class Test { public static void main(String[] args) { Student student1 = new Student(); student1.setName("Alex"); student1.setAge(18); Student student2 = new Student(); student2.setName("Bob"); student2.setAge(20); Comparable max = getMax(student1, student2); System.out.println(max); // 输出:Student{name='Bob', age=20} } public static Comparable getMax(Comparable c1, Comparable c2) { int result = c1.compareTo(c2); if (result >= 0) { return c1; } else { return c2; } } }
-
(1)简单排序
a. 冒泡排序
-
冒泡排序(Bubble Sort)
-
原理:
- 比较相邻元素,如果前一个元素比后一个元素大,则交换两者的位置
- 每一对相邻元素重复上述比较操作,当完成最后一对比较时,排序结束
-
特点:简单
-
举例:对数组
{4, 5, 6, 3, 2, 1}
从小到大排序-
表格演示
操作次数 排序结果 初始状态 4 5 6 3 2 1 第一次 4 5 3 2 1 6 第二次 4 3 2 1 5 6 第三次 3 2 1 4 5 6 第四次 2 1 3 4 5 6 第五次 1 2 3 4 5 6 第六次 1 2 3 4 5 6 -
代码实现
-
冒泡排序 API
package com.sort; public class Bubble { // 排序函数 public static void sort(Comparable[] array) { for (int i = array.length-1; i > 0; i--) { for (int j = 0; j < i; j++) { if (biggerOrNot(array[j], array[j+1])) { swap(array, j, j+1); } } } } // 比较函数 private static boolean biggerOrNot(Comparable c1, Comparable c2) { return c1.compareTo(c2) > 0; } // 交换函数 private static void swap(Comparable[] array, int i, int j) { Comparable temp; temp = array[i]; array[i] = array[j]; array[j] = temp; } }
-
在测试类中使用
package com.sort; import java.util.Arrays; public class Test { public static void main(String[] args) { Integer[] array = {4, 5, 6, 1, 2, 3}; Bubble.sort(array); System.out.println(Arrays.toString(array)); } }
-
-
-
时间复杂度分析
最坏情况(完全逆序)下,元素比较次数为:
\[(n-1)+(n-2)+(n-3)+\ldots+2+1=\frac 12(((n-1)+1)×(n-1))=\frac 12n^2-\frac 12n \]元素交换次数为:
\[(n-1)+(n-2)+(n-3)+\ldots+2+1=\frac 12(((n-1)+1)×(n-1))=\frac 12n^2-\frac 12n \]总执行次数为:
\[(\frac 12n^2-\frac 12n)+(\frac 12n^2-\frac 12n)=n^2-n \]此时时间复杂度为 \(O(n^2)\)
b. 选择排序
-
选择排序(Selection Sort)
-
原理:
- 每次遍历都假定第一个索引处元素为最小值,并和其他元素依次比较,直到找出最小值
- 将最小值与第一个索引处交换
-
特点:简单、直观
-
举例:对数组
{4, 6, 8, 7, 9, 2, 10, 1}
从小到大排序-
表格演示
操作次数 排序结果 初始状态 4 6 8 7 9 2 10 1 第一次(1,4) 1 6 8 7 9 2 10 4 第二次(2,6) 1 2 8 7 9 6 10 4 第三次(4,8) 1 2 4 7 9 6 10 8 第四次(6,7) 1 2 4 6 9 7 10 8 第五次(7,9) 1 2 4 6 7 9 10 8 第六次(8,9) 1 2 4 6 7 8 10 9 第七次(9,10) 1 2 4 6 7 8 9 10 -
代码实现
-
插入排序 API
package com.sort; public class Selection { // 排序函数 public static void sort(Comparable[] array) { for (int i = 0; i < array.length-1; i++) { int minIndex = i; for (int j = i+1; j < array.length; j++) { if (biggerOrNot(array[minIndex], array[j])) { minIndex = j; } } swap(array, i, minIndex); } } // 比较函数 private static boolean biggerOrNot(Comparable c1, Comparable c2) { return c1.compareTo(c2) > 0; } // 交换函数 private static void swap(Comparable[] array, int i, int j) { Comparable temp; temp = array[i]; array[i] = array[j]; array[j] = temp; } }
-
在测试类中使用
package com.sort; import java.util.Arrays; public class Test { public static void main(String[] args) { Integer[] array = {4, 6, 8, 7, 9, 2, 10, 1}; Selection.sort(array); System.out.println(Arrays.toString(array)); } }
-
-
-
时间复杂度分析
数据比较次数:
\[(n-1)+(n-2)+(n-3)+\ldots+2+1=\frac 12(((n-1)+1)×(n-1))=\frac 12n^2-\frac 12n \]数据交换次数:
\[n-1 \]总执行次数:
\[(\frac 12n^2-\frac 12n)+(n-1)=\frac 12n^2+\frac 12n-1 \]此时时间复杂度为 \(O(n^2)\)
c. 插入排序
-
插入排序(Insertion Sort)
-
原理:
- 将所有元素分为已排序和未排序两组
- 找到未排序中的第一个元素,向已排序中进行插入
- 倒序遍历已排序元素,依次与待插入元素比较,直到找到小于待插入元素的元素,将待插入元素放在该位置,其他元素向后移动一位
-
特点:简单、直观
-
举例:对数组
{4, 3, 2, 8, 9, 1, 5, 6}
从小到大排序-
表格演示
操作次数 排序结果 初始状态 4 3 2 8 9 1 5 6 第一次(3) 3 4 2 8 9 1 5 6 第二次(2) 2 3 4 8 9 1 5 6 第三次(8) 2 3 4 8 9 1 5 6 第四次(9) 2 3 4 8 9 1 5 6 第五次(1) 1 2 3 4 8 9 5 6 第六次(5) 1 2 3 4 5 8 9 6 第七次(6) 1 2 3 4 5 6 8 9 -
代码实现
-
排序 API
package com.sort; public class Insertion { // 排序函数 public static void sort(Comparable[] array) { for (int i = 1; i < array.length; i++) { for (int j = i; j > 0; j--) { if (biggerOrNot(array[j-1], array[j])) { swap(array, j-1, j); } else { break; } } } } // 比较函数 private static boolean biggerOrNot(Comparable c1, Comparable c2) { return c1.compareTo(c2) > 0; } // 交换函数 private static void swap(Comparable[] array, int i, int j) { Comparable temp; temp = array[i]; array[i] = array[j]; array[j] = temp; } }
-
在测试类中使用
package com.sort; import java.util.Arrays; public class Test { public static void main(String[] args) { Integer[] array = {4, 3, 2, 8, 9, 1, 5, 6}; Insertion.sort(array); System.out.println(Arrays.toString(array)); } }
-
-
-
时间复杂度分析
最坏情况(完全逆序)下,元素比较次数为:
\[(n-1)+(n-2)+(n-3)+\ldots+2+1=\frac 12(((n-1)+1)×(n-1))=\frac 12n^2-\frac 12n \]元素交换的次数为:
\[(n-1)+(n-2)+(n-3)+\ldots+2+1=\frac 12(((n-1)+1)×(n-1))=\frac 12n^2-\frac 12n \]总执行次数为:
\[(\frac 12n^2-\frac 12n)+(\frac 12n^2-\frac 12n)=n^2-n \]此时时间复杂度为 \(O(n^2)\)
(2)高级排序
a. 希尔排序
-
排序(Shell Sort)
-
原理:
希尔排序就是通过增设增量来减少交换次数
-
选定一个增量
h
,按照增量对数据进行分组增量的确定规则:
int h = 1; while (h < array.length/2) { h = h*2 + 1; }
-
对每个分组进行插入排序
-
减小增量,最小为 1,重复第二步
增量的减小规则:
h /= 2;
-
-
特点:插入排序的改进,高效
-
举例:对数组
{9, 1, 2, 5, 7, 4, 8, 6, 3, 5}
从小到大排序-
表格演示
操作次数 排序结果 初始状态 9 1 2 5 7 4 8 6 3 5 第一次(h=5) 4 1 2 3 5 9 8 6 5 7 第二次(h=2) 2 1 4 3 5 6 5 7 8 9 第三次(h=1) 1 2 3 4 5 5 6 7 8 9 -
代码实现
-
排序 API
package com.sort; public class Shell { // 排序函数 public static void sort(Comparable[] array) { int length = array.length; int h = 1; // 增量 while (h < length) { h = h * 2 + 1; } while (h >= 1) { for (int i = h; i < length; i++) { for (int j = i; j >= h; j-=h) { if (biggerOrNot(array[j-h], array[j])) { swap(array, j-h, j); } else { break; } } } h /= 2; } } // 比较函数 private static boolean biggerOrNot(Comparable c1, Comparable c2) { return c1.compareTo(c2) > 0; } // 交换函数 private static void swap(Comparable[] array, int i, int j) { Comparable temp; temp = array[i]; array[i] = array[j]; array[j] = temp; } }
-
在测试类中使用
package com.sort; import java.util.Arrays; public class Test { public static void main(String[] args) { Integer[] array = {9, 1, 2, 5, 7, 4, 8, 6, 3, 5}; Shell.sort(array); System.out.println(Arrays.toString(array)); } }
-
-
-
时间复杂度
时间复杂度为 \(O(n^\frac 23)\)
b. 归并排序
-
排序(Merge Sort)
-
原理:
- 将数组二分成元素个数相等的连续子数组,对子数组重复该二分法直至元素个数为 1
- 将两个相邻的子数组合并成一个有序的数组(二路归并)
- 重复第二步直至仅有一个数组
-
特点:分治,以空间换时间
-
举例:对数组
{8, 4, 5, 7, 1, 3, 6, 2}
从小到大排序-
图表演示
flowchart TB A(8 4 5 7 1 3 6 2)-->B(8 4 5 7) & C(1 3 6 2) B-->D(8 4) & E(5 7) C-->F(1 3) & G(6 2) D-->8 & 4-->H(4 8) E-->5 & 7-->I(5 7) F-->1 & 3-->J(1 3) G-->2 & 6-->K(2 6) H & I-->L(4 5 7 8) J & K-->M(1 2 3 6) L & M-->N(1 2 3 4 5 6 7 8) -
代码实现
-
排序 API
package com.sort; public class Merge { // 辅助数组 private static Comparable[] assist_array; // 全局排序函数 public static void sort(Comparable[] array) { assist_array = new Comparable[array.length]; int lowest = 0, highest = array.length - 1; sort(array, lowest, highest); } // 区域排序函数 private static void sort(Comparable[] array, int low, int high) { if (low >= high) { return; } int mid = low + (high - low) / 2; sort(array, low, mid); sort(array, mid+1, high); merge(array, low, mid, high); } // 归并函数 private static void merge(Comparable[] array,int low, int mid, int high) { int index = low, p1 = low, p2 = mid+1; // 遍历,移动p1和p2,比对应索引处的值,找出较小的放到辅助数组的对应索引处 while(p1 <= mid && p2 <= high) { if (smallerOrNot(array[p1], array[p2])) { assist_array[index++] = array[p1++]; } else { assist_array[index++] = array[p2++]; } } // 遍历,如果p1没有走完,那么顺序移动p1,把对应的元素放到辅助数组的对应索引处 while (p1 <= mid) { assist_array[index++] = array[p1++]; } // 遍历,如果p2没有走完,那么顺序移动p2,把对应的元素放到辅助数组的对应索引处 while (p2 <= high) { assist_array[index++] = array[p2++]; } // 把辅助数组中的元素拷贝到原数组中 for (int i = low; i <= high; i++) { array[i] = assist_array[i]; } } // 比较函数 private static boolean smallerOrNot(Comparable c1, Comparable c2) { return c1.compareTo(c2) < 0; } }
-
在测试类中使用
package com.sort; import java.util.Arrays; public class Test { public static void main(String[] args) { Integer[] array = {8, 4, 5, 7, 1, 3, 6, 2}; Merge.sort(array); System.out.println(Arrays.toString(array)); } }
-
-
-
时间复杂度分析
假定需要排序的元素个数为 \(n\),使用归并排序拆分的次数为 \(\log_2n\),总执行次数为:
\[\log_2n×2^{\log_2n} = \log_2n×n \]此时时间复杂度为 \(O(n\log n)\)
c. 快速排序
-
排序(Quick Sort)
-
原理:
- 设定分界值,将数组分为左右两部分
- 将大于等于分界值的元素交换的数组右侧,其他交换到数组左侧
- 将数组左侧和右侧分别设定分界值后,对左右两数组重复第二步
- 重复第三步,直至拆分后的数组元素个数为 2
-
特点:冒泡排序的改进,快速
-
举例:对数组
{6, 1, 2, 7, 9, 3, 4, 5, 8}
从小到大排序-
图表演示
flowchart TB A(6 1 2 7 9 3 4 5 8)--分界值: 6-->B(3 1 2 5 4 6 9 7 8)-->C(3 1 2 5 4) & D(9 7 8) C--分界值: 3-->E(2 1 3 5 4)-->F(1 2) & G(4 5)-->H(1 2 3 4 5) D--分界值: 9-->I(8 7 9)-->J(7 8)-->K(7 8 9) H & K-->L(1 2 3 4 5 6 7 8 9) -
代码实现
-
排序 API
package com.sort; public class Quick { // 全局排序函数 public static void sort(Comparable[] array) { int lowest = 0, highest = array.length - 1; sort(array, lowest, highest); } // 区域排序函数 private static void sort(Comparable[] array, int low, int high) { if (low >= high) { return; } // 获取分界值 int partition = partition(array, low, high); // 排序左子数组 sort(array, low, partition - 1); // 排序右子数组 sort(array, partition + 1, high); } // 分界值函数 private static int partition(Comparable[] array, int low, int high) { Comparable key = array[low]; int left = low, right = high + 1; while (true) { // 从右向左遍历,直至找到比分界值小的元素 while (smallerOrNot(key, array[--right])) { if (right == low) { break; } } // 从左向右遍历,直至找到比分界值大的元素 while (smallerOrNot(array[++left], key)) { if (left == high) { break; } } // 判断是否全部遍历完毕,否则交换元素 if (left >= right) { break; } else { swap(array, left, right); } } // 交换分界值 swap(array, low, right); return right; } // 比较函数 private static boolean smallerOrNot(Comparable c1, Comparable c2) { return c1.compareTo(c2) < 0; } // 交换函数 private static void swap(Comparable[] array, int i, int j) { Comparable temp; temp = array[i]; array[i] = array[j]; array[j] = temp; } }
-
在测试类中使用
package com.sort; import java.util.Arrays; public class Test { public static void main(String[] args) { Integer[] array = {8, 4, 5, 7, 1, 3, 6, 2}; Quick.sort(array); System.out.println(Arrays.toString(array)); } }
-
-
-
时间复杂度分析
最优情况:每次切分选择的分界值恰好为当前序列等分,共切分了 \(\log n\) 次,此时时间复杂度为 \(O(n\log n)\)
最坏情况:每次切分选择的分界值恰好为当前序列中最大值(最小值),共切分了 \(n\) 次,此时时间复杂度为 \(O(n^2)\)
平均情况下,使用数学归纳法得到时间复杂度为 \(O(n\log n)\)
(3)排序稳定性
-
定义
某确定数组中一若干元素,其中元素 A 与元素 B 相等,并且元素 A 在元素 B 之前,如果使用了某种排序算法后,元素 A 仍在元素 B 之前,则说明此排序算法稳定
-
意义
有意义的前提是需要多次排序,稳定性好的排序算法不仅可以保持第一次排序的原有意义,而且可以减小系统开销
-
上述算法中具备稳定性的算法:冒泡排序、插入排序、希尔排序(第一次排序时)、归并排序
0x02 线性表
- 定义:线性表是 n 个具有相同特性的数据元素的有限序列
- 元素位置关系:某线性表
{..., a, b, ...}
- 前驱元素:元素
a
是元素b
的前驱元素 - 后继元素:元素
b
是元素a
的后继元素
- 前驱元素:元素
- 特征 :元素之间存在一对一的关系
- 头结点:没有前驱元素的第一个元素
- 尾结点:没有后继元素的第 n 个元素
- 一般结点:既有前驱元素,也有后继元素
- 分类:根据数据的存储方式
- 顺序存储:顺序表
- 链式存储:链表
(1)顺序表
a. 概述
- 定义:在计算机内存中以数组形式保存的线性表
- 存储方式:用一组地址连续的存储单元,依次存储线性表中的各个元素,可以通过元素物理存储的相邻关系反映元素之间逻辑上的相邻关系
b. 基本实现
注:以下代码实现存在一定的问题,仅作为 Demo
-
顺序表 API
package com.linear.linklist; public class SequenceList<T> { // 存储元素的数组 private T[] elements; // 记录当前顺序表元素个数 private int size; // 构造方法 public SequenceList(int capacity) { // 初始化数组 this.elements = (T[]) new Object[capacity]; // 初始化元素个数 this.size = 0; } // 清空顺序表 public void clear() { this.size = 0; } // 判断是否空表 public boolean isEmpty() { return this.size == 0; } // 获取顺序表长度 public int length() { return this.size; } // 获取指定位置的元素 public T get(int index) { return this.elements[index]; } // 添加新元素 public void insert(T newElement) { // 在数组尾部添加新元素 this.elements[this.size++] = newElement; } // 在指定元素处添加新元素 public void insert(int index, T newElement) { // index 索引处及以后的元素向后移动一位,从最后一位开始 for (int i = this.size; i >= index; i--) { this.elements[i] = this.elements[i - 1]; } // 在 index 索引处添加新元素 this.elements[index] = newElement; this.size++; } // 删除指定位置的元素 public T remove(int index) { // 记录 index 索引处元素 T current = this.elements[index]; // index 索引处及以后的元素向前移动一位,从 index 索引处开始 for (int i = index; i < this.size - 1; i++) { this.elements[i] = this.elements[i + 1]; } this.size--; return current; } // 查找指定元素首次出现的索引 public int indexOf(T element) { for (int i = 0; i < this.size; i++) { if (this.elements[i].equals(element)) { return i; } } return -1; } // 输出表中所有元素 public void show() { if (this.size == 0) { System.out.println("None"); return; } for (int i = 0; i < this.size; i++) { System.out.print(this.elements[i] + " "); } System.out.println(); } }
-
测试
package com.linear.linklist; public class Test { public static void main(String[] args) { SequenceList<Integer> sl = new SequenceList<>(10); sl.insert(1); sl.insert(2); sl.insert(3); sl.show(); sl.insert(1, 0); sl.show(); System.out.println(sl.remove(2) + " has been removed."); sl.show(); sl.clear(); sl.show(); } }
c. 遍历
-
通过以下操作,使顺序表 API 支持
foreach
循环-
实现
Iterable
接口,重写iterator
方法package com.linear.linklist; import java.util.Iterator; public class SequenceList<T> implements Iterable<T> { // ... @Override public Iterator<T> iterator() { return new SIterator(); } }
-
提供内部类
SIterable
,实现Iterator
接口,重写hasNext
和next
方法package com.linear.linklist; import java.util.Iterator; public class SequenceList<T> implements Iterable<T> { // ... private class SIterator implements Iterator { private int cursor; public SIterator() { this.cursor = 0; } @Override public boolean hasNext() { return this.cursor < size; } @Override public Object next() { return elements[this.cursor++]; } } }
-
-
测试
package com.linear.linklist; public class Test { public static void main(String[] args) { SequenceList<Integer> sl = new SequenceList<>(10); sl.insert(1); sl.insert(2); sl.insert(3); for (Integer integer : sl) { System.out.println(integer); } } }
d. 可变容量
-
容量变化函数
public class SequenceList<T> implements Iterable<T> { // ... public void resize(int newSize) { T [] tempElements = this.elements; this.elements = (T[]) new Object[newSize]; for (int i = 0; i < this.size; i++) { this.elements[i] = tempElements[i]; } tempElements = null; } }
-
添加元素时,如果数组已满,则创建新的容量更大的数组
public void insert(T newElement) { // 扩容 if (this.size == elements.length) { this.resize(2 * elements.length); } // ... } public void insert(int index, T newElement) { // 扩容 if (this.size == elements.length) { this.resize(2 * elements.length); } // ... }
-
删除元素时,如果数组空闲较多,则创建新的容量更小的数组
public T remove(int index) { // ... if (this.size < this.elements.length / 4) { resize(this.elements.length / 2); } return current; }
-
-
测试
package com.linear.linklist; public class Test { public static void main(String[] args) { SequenceList<Integer> sl = new SequenceList<>(2); sl.insert(1); sl.insert(2); sl.insert(3); sl.insert(4); for (Integer integer : sl) { System.out.println(integer); } } }
e. 时间复杂度
get()
:\(O(1)\)insert()
:\(O(n)\)remove()
:\(O(n)\)
h. ArrayList
Java 中的 ArrayList 集合的底层也是一种顺序表,使用数组实现,提供了增删改查以及扩容等功能
(2)链表
a. 概述
-
定义:链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能直观地表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
-
链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成
-
链表的结点插入演示
flowchart LR X([原链表]) A(1)--->B(2)-->C(3) Y([插入结点]) D(1)-.->E(2)-->F(3) D-->G(0)-->E Z([删除结点]) H(1)-.->I(0)-.->J(2)-->K(3) H--->J
b. 结点实现
-
结点 API
public class Node<T> { // 储存元素 public T data; // 指向下一个结点 public Node next; public Node(T data, Node next) { this.data = data; this.next = next; } }
-
生成链表
public static void main(String[] args) { // 构建结点 Node<Integer> a = new Node<Integer>(1, null); Node<Integer> b = new Node<Integer>(2, null); Node<Integer> c = new Node<Integer>(3, null); Node<Integer> d = new Node<Integer>(4, null); Node<Integer> e = new Node<Integer>(5, null); // 生成链表 a.next = b; b.next = c; c.next = d; d.next = e; }
c. 单向链表
-
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点
-
单向链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点
flowchart LR 1-->2-->3 -
代码实现
-
API
package com.linear.linklist; import java.util.Iterator; public class LinkList<T> implements Iterable<T> { // 头结点 private final Node head; // 链表长度 private int size; // 结点类 private class Node { T data; Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } // 初始化 public LinkList() { this.head = new Node(null, null); this.size = 0; } // 清空链表 public void clear() { this.head.next = null; this.size = 0; } // 获取长度 public int length() { return this.size; } // 判空 public boolean empty() { return this.size == 0; } // 获取指定位置的元素 public T get(int index) { if (index >= this.size) { return null; } Node node = this.head.next; for (int i = 0; i <= index - 1; i++) { node = node.next; } return node.data; } // 添加新元素 public void insert(T data) { Node tail = this.head; while (tail.next != null) { tail = tail.next; } tail.next = new Node(data, null); this.size++; } // 在指定位置插入新元素 public void insert(int index, T data) { Node pre = this.head.next; for (int i = 0; i < index - 1; i++) { pre = pre.next; } pre.next = new Node(data, pre.next); this.size++; } // 删除指定位置元素 public T remove(int index) { Node pre = this.head.next; for (int i = 0; i < index - 1; i++) { pre = pre.next; } T result = pre.next.data; pre.next = pre.next.next; this.size--; return result; } // 查找指定元素首次出现的索引 public int indexOf(T data) { Node node = this.head.next; for (int i = 0; node.next != null; i++) { node = node.next; if (node.data.equals(data)) { return i; } } return -1; } @Override public Iterator<T> iterator() { return new LIterator(); } private class LIterator implements Iterator { private Node node; public LIterator() { this.node = head; } @Override public boolean hasNext() { return this.node.next != null; } @Override public Object next() { this.node = this.node.next; return this.node.data; } } }
-
测试
package com.linear.linklist; public class Test { public static void main(String[] args) { LinkList<Integer> ll = new LinkList<>(); ll.insert(1); ll.insert(2); ll.insert(3); for (Integer integer : ll) { System.out.print(integer + " "); } // 1 2 3 System.out.println(); ll.insert(1, 0); for (Integer integer : ll) { System.out.print(integer + " "); } // 1 0 2 3 System.out.println(); System.out.println(ll.get(1)); // 0 System.out.println(ll.remove(2)); // 2 } }
-
d. 双向链表
-
双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储教据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点
-
链表的头结点的数据域不存储数据,指向前驱结点的指针域值为 null,指向后继结点的指针域指向第一个真正存储数据的结点
flowchart LR A(null)<-->1<-->2<-->3<-->B(null) -
代码实现
-
API
package com.linear.linklist; import java.util.Iterator; public class BidirectionalLinkList<T> implements Iterable<T> { private final Node head; private Node tail; private int size; private class Node { Node pre; T data; Node next; public Node(Node pre, T data, Node next) { this.pre = pre; this.data = data; this.next = next; } } public BidirectionalLinkList() { this.head = new Node(null, null, null); this.tail = null; this.size = 0; } // 清空链表 public void clear() { this.head.pre = null; this.head.data = null; this.head.next = null; this.tail = null; this.size = 0; } // 获取长度 public int length() { return this.size; } // 判空 public boolean empty() { return this.size == 0; } // 获取指定位置的元素 public T get(int index) { Node node = this.head.next; for (int i = 0; i < index; i++) { node = node.next; } return node.data; } // 添加新元素 public void insert(T data) { if (empty()) { Node node = new Node(this.head, data, null); this.tail = node; this.head.next = node; } else { Node tmp = this.tail; Node node = new Node(tmp, data, null); tmp.next = node; this.tail = node; } this.size++; } // 在指定位置添加新元素 public void insert(int index, T data) { Node pre = this.head; for (int i = 0; i < index; i++) { pre = pre.next; } Node next = pre.next; Node node = new Node(pre, data, next); pre.next = node; next.pre = node; this.size++; } // 删除指定元素 public T remove(int index) { Node node = head.next; for (int i = 0; i < index; i++) { node = node.next; } node.pre.next = node.next; node.next.pre = node.pre; return node.data; } // 获取指定元素首次出现的索引 public int indexOf(T data) { Node node = head; for (int i = 0; node != null; i++) { node = node.next; if (node.next.data.equals(data)) { return i; } } return -1; } // 获取第一个元素 public T getHead() { return this.empty() ? null : this.head.next.data; } // 获取最后一个元素 public T getTail() { return this.empty() ? null : this.tail.data; } @Override public Iterator<T> iterator() { return new BIterator(); } private class BIterator implements Iterator { private Node node; public BIterator() { this.node = head; } @Override public boolean hasNext() { return this.node.next != null; } @Override public Object next() { this.node = this.node.next; return this.node.data; } } }
-
测试
package com.linear.linklist; public class Test { public static void main(String[] args) { BidirectionalLinkList<Integer> bll = new BidirectionalLinkList<>(); bll.insert(1); bll.insert(2); bll.insert(3); for (Integer integer : bll) { System.out.print(integer + " "); } // 1 2 3 System.out.println(); bll.insert(1, 0); for (Integer integer : bll) { System.out.print(integer + " "); } // 1 0 2 3 System.out.println(); System.out.println(bll.get(1)); // 0 System.out.println(bll.remove(2)); // 2 System.out.println(bll.getHead()); // 1 System.out.println(bll.getTail()); // 3 } }
-
e. 时间复杂度
get()
:\(O(n)\)insert()
:\(O(n)\)remove()
:\(O(n)\)- 相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换
- 相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表
f. 链表反转
-
举例
原单向链表:1 -> 2 -> 3 -> 4
反转后链表:4 -> 3 -> 2 -> 1
-
原理:采用递归方法,从原链表的第一个存数据的结点开始,依次递归调用反转每一个结点,直到把最后一个结点反转完毕
-
代码实现
-
API
package com.linear.linklist; import java.util.Iterator; public class LinkList<T> implements Iterable<T> { //... public void reverse() { if (this.empty()) { return; } reverse(this.head.next); } public Node reverse(Node node) { if (node.next == null) { this.head.next = node; return node; } Node pre = reverse(node.next); pre.next = node; node.next = null; return node; } }
-
测试
package com.linear.linklist; public class Test { public static void main(String[] args) { LinkList<Integer> ll = new LinkList<Integer>(); ll.insert(1); ll.insert(2); ll.insert(3); ll.insert(4); for (Integer integer : ll) { System.out.print(integer + " -> "); } System.out.println("null"); ll.reverse(); for (Integer integer : ll) { System.out.print(integer + " -> "); } System.out.println("null"); } }
-
g. 快慢指针
- 快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以然我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍
I. 中间值问题
-
求链表中间结点的值
flowchart LR 1-->2-->3-->4-->5-->6-->7 -
利用快慢指针,我们把一个链表看成一个跑道,假设指针 a 的速度是指针 b 的两倍,那么当指针 a 跑完全程后,指针 b 刚好跑一半,以此来达到找到中间节点的目的
public class Test { public static void main(String[] args) { Node<Integer> g = new Node<>(7, null); Node<Integer> f = new Node<>(6, g); Node<Integer> e = new Node<>(5, f); Node<Integer> d = new Node<>(4, e); Node<Integer> c = new Node<>(3, d); Node<Integer> b = new Node<>(2, c); Node<Integer> a = new Node<>(1, b); System.out.println(getMid(a).data); } // 求中间值结点 public static Node<Integer> getMid(Node<Integer> first) { Node fast = first; Node slow = first; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; } return slow; } // 结点类 private static class Node<T> { T data; Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } }
II. 单向链表是否有环问题
-
有环
flowchart LR 3 & 5 1-->2-->3-->4-->5-->3 -
无环
flowchart LR 1---2---3---4---5 -
判断单向链表是否存在环
-
使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环
package com.linear.linklist; public class Test { public static void main(String[] args) { Node<Integer> e = new Node<>(5, null); Node<Integer> d = new Node<>(4, e); Node<Integer> c = new Node<>(3, d); Node<Integer> b = new Node<>(2, c); Node<Integer> a = new Node<>(1, b); System.out.println(isCircle(a)); // false e.next = c; System.out.println(isCircle(a)); // true } // 判断环的存在 public static boolean isCircle(Node<Integer> first) { Node fast = first; Node slow = first; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if (fast.equals(slow)) { return true; } } return false; } // 结点类 private static class Node<T> { T data; Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } }
III. 有环链表入口问题
-
以下有环链表的入口结点值为 3
flowchart LR 3 & 5 1-->2-->3-->4-->5-->3 -
当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样,则慢指针与“新"指针相遇的地方就是环的入口(证明这一结论牵涉到数论的知识)
package com.linear.linklist; public class Test { public static void main(String[] args) { Node<Integer> e = new Node<>(5, null); Node<Integer> d = new Node<>(4, e); Node<Integer> c = new Node<>(3, d); Node<Integer> b = new Node<>(2, c); Node<Integer> a = new Node<>(1, b); e.next = c; System.out.println(getEntrance(a).data); } // 获取环入口结点函数 public static Node<Integer> getEntrance(Node<Integer> first) { Node fast = first; Node slow = first; Node<Integer> temp = null; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if (fast.equals(slow)) { temp = first; continue; } if (temp != null) { temp = temp.next; if (temp.equals(slow)) { break; } } } return temp; } // 结点类 private static class Node<T> { T data; Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } }
h. 循环链表
-
循环链表是一个整体形成圆环状的链表
-
在单向链表中,最后一个节点的指针为 null,不指向任何结点,因为没有下一个元素;要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可
flowchart LR 1-->2-->3-->4-->5-->1 -
代码实现
package com.linear.linklist; public class Test { public static void main(String[] args) { // 构建结点和单链表 Node<Integer> e = new Node<>(5, null); Node<Integer> d = new Node<>(4, e); Node<Integer> c = new Node<>(3, d); Node<Integer> b = new Node<>(2, c); Node<Integer> a = new Node<>(1, b); // 构成循环链表: 尾指头 e.next = a; Node node = a; for (int i = 0; i < 10; i++) { System.out.print(node.data + " "); node = node.next; } } // 结点类 private static class Node<T> { T data; Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } }
i. 约瑟夫问题
-
问题描述
传说有这样一个故事,在罗马人占领乔塔特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39 个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41 个人排成一个圆圈,第一个人从 1 开始报数,依次往后,如果有人报数到 3,那么这个人就必须自杀,然后再由他的下一个人重新从 1 开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第 16 个与第 31 个位置,从而逃过了这场死亡游戏
-
问题转换
41 个人坐一圈,第一个人编号为 1,第二个人编号为 2,第 n 个人编号为 n
- 编号为 1 的人开始从 1 报数,依次向后,报数为 3 的那个人退出圈
- 自退出那个人开始的下一个人再次从 1 开始报数,以此类推
- 求出最后退出的那个人的编号
-
解题思路
- 构建含有 41 个结点的单向循环链表,分别存储 1~41 的值,分别代表这 41 个人
- 使用计数器
counter
,记录当前报数的值 - 遍历链表,每循环一次,
counter++
- 判断
counter
的值,如果是 3,则从链表中删除这个结点并打印结点的值,把counter
重置为 0;
-
代码实现
package com.linear.linklist; public class Test { public static void main(String[] args) { Node<Integer> first = null; Node<Integer> pre = null; for (int i = 1; i <= 41; i++) { if (i == 1) { first = new Node<Integer>(i, null); pre = first; continue; } Node<Integer> node = new Node<Integer>(i, null); pre.next = node; pre = node; if (i == 41) { pre.next = first; } } int counter = 0; Node<Integer> node = first; pre = null; while (node != node.next) { counter++; if (counter == 3) { pre.next = node.next; System.out.print(node.data + " "); counter = 0; node = node.next; } else { pre = node; node = node.next; } } System.out.println(); System.out.println(node.data); } // 结点类 private static class Node<T> { T data; Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } }
(3)栈
a. 概述
- 栈是一种基于先进后出(First In Last Out,简称 FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表
- 按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)
- 进栈顺序:1 2 3 4
- 出栈顺序:4 3 2 1
- 数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈
b. 实现
-
API
package com.linear.stack; import java.util.Iterator; public class Stack<T> implements Iterable<T> { private Node head; private int size; private class Node { public T data; public Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } // 构造函数 public Stack() { this.head = new Node(null, null); this.size = 0; } // 判空 public boolean empty() { return this.size == 0; } // 返回长度 public int length() { return this.size; } // 压栈 public void push(T data) { this.head.next = new Node(data, this.head.next); this.size++; } // 弹栈 public T pop() { Node node = this.head.next; if (node != null) { return null; } this.head.next = node.next; this.size--; return node.data; } @Override public Iterator<T> iterator() { return null; } private class SIterator implements Iterator { private Node node; public SIterator() { this.node = head; } @Override public boolean hasNext() { return this.node.next != null; } @Override public Object next() { this.node = this.node.next; return this.node.data; } } }
-
测试
package com.linear.stack; public class Test { public static void main(String[] args) { Stack<Integer> stack = new Stack<>(); stack.push(1); stack.push(2); stack.push(3); stack.push(4); for (Integer integer : stack) { System.out.print(integer + " "); // 4 3 2 1 } System.out.println(); while (!stack.empty()) { System.out.print(stack.pop() + " "); // 4 3 2 1 } } }
c. 括号匹配问题
-
问题描述
给定一个字符串,里边可能包含
()
小括号和其他字符,请编写程序检查该字符串的中的小括号是否成对出现
例如:
(上海)(长安)
: 正确匹配
上海((长安))
:正确匹配
上海(长安(北京)(深圳)南京)
:正确匹配
上海(长安))
:错误匹配
((上海)长安
:错误匹配 -
解题思路
- 创建一个栈用来存储左括号
- 从左往右遍历字符串,拿到每一个字符
- 判断该字符是不是左括号,如果是,放入栈中存储
- 判断该字符是不是右括号,如果不是,继续下一次循环
- 如果该字符是右括号,则从栈中弹出一个元素
temp
- 判新元素
temp
是否为 null,如果是,则证明有对应的左括号,如果不是,则证明没有对应的左括号
- 如果该字符是右括号,则从栈中弹出一个元素
- 循环结束后,判断栈中还有没有剩余的左括号,如果有,则不匹配,如果没有,则匹配
-
代码实现
package com.linear.stack; public class Test { public static void main(String[] args) { String[] strs = {"(上海)(长安)", "上海((长安))", "上海(长安(北京)(深圳)南京)", "上海(长安))", "((上海)长安"}; for (int i = 0; i < 5; i++) { System.out.println(isMatch(strs[i])); } } public static boolean isMatch(String str) { Stack<String> chars = new Stack<String>(); for (int i = 0; i < str.length(); i++) { String c = str.charAt(i) + ""; if (c.equals("(")) { chars.push(c); } else if (c.equals(")")) { String p = chars.pop(); if (p == null) { return false; } } } return chars.length() == 0; } }
d. 逆波兰表达式求值问题
-
中缀表达式:二元运算符置于两个操作数之间
-
逆波兰表达式(后缀表达式):二元运算符置于两个操作数之后
中缀表达式 逆波兰表达式 \(a+b\) \(ab+\) \(a+(b-c)\) \(abc-+\) \(a+(b-c)*d\) \(abc-d*+\) \(a*(b-c)+d\) \(abc-*d+\) -
问题描述
给定一个只包含加减乘除四种运算的逆波兰表达式的数组表示方式,求出该逆波兰表达式的结果
-
解题思路
- 创建一个栈对象
operands
存储操作数 - 从左往右遍历逆波兰表达式,得到每一个字符串
- 判断该字符串是不是运算符,如果不是,把该该操作数压入
operands
栈中 - 如果是运算符,则从
operands
栈中弹出两个操作数o1
,o2
- 使用该运算符计算
o1
和o2
,得到结果result
- 把该结果压入
operands
栈中 - 遍历结束后,拿出栈中最终的结果返回
- 创建一个栈对象
-
代码实现
package com.linear.stack; public class Test { public static void main(String[] args) { // 3*(17-15)+18/6 = 9 String[] notation = {"3", "17", "15", "-", "*", "18", "6", "/", "+"}; System.out.println(calculate(notation)); } public static int calculate(String[] notation) { Stack<Integer> operands = new Stack<Integer>(); for (String str : notation) { Integer o1, o2, result; switch (str) { case "+" -> { o1 = operands.pop(); o2 = operands.pop(); result = o2 + o1; operands.push(result); } case "-" -> { o1 = operands.pop(); o2 = operands.pop(); result = o2 - o1; operands.push(result); } case "*" -> { o1 = operands.pop(); o2 = operands.pop(); result = o2 * o1; operands.push(result); } case "/" -> { o1 = operands.pop(); o2 = operands.pop(); result = o2 / o1; operands.push(result); } default -> operands.push(Integer.parseInt(str)); } } return operands.pop(); } }
(4)队列
a. 概述
- 队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表
- 按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来
- 进队顺序:1 2 3 4
- 出队顺序:1 2 3 4
b. 实现
-
API
package com.linear.queue; import java.util.Iterator; public class Queue<T> implements Iterable<T> { private Node head; private Node tail; private int size; private class Node { T data; Node next; public Node(T data, Node next) { this.data = data; this.next = next; } } public Queue() { this.head = new Node(null, null); this.tail = null; this.size = 0; } public boolean empty() { return this.size == 0; } public int length() { return this.size; } // 入队 public void enQueue(T data) { if (this.tail == null) { this.tail = new Node(data, null); this.head.next = this.tail; } else { Node node = new Node(data, null); this.tail.next = node; this.tail = node; } this.size++; } // 出队 public T deQueue() { if (this.empty()) { return null; } Node node = this.head.next; this.head.next = node.next; this.size--; if (empty()) { this.tail = null; } return node.data; } @Override public Iterator<T> iterator() { return new QIterator(); } private class QIterator implements Iterator { private Node node; public QIterator() { this.node = head; } @Override public boolean hasNext() { return this.node.next != null; } @Override public Object next() { this.node = this.node.next; return this.node.data; } } }
-
测试
package com.linear.queue; public class Test { public static void main(String[] args) { Queue<Integer> queue = new Queue<Integer>(); queue.enQueue(1); queue.enQueue(2); queue.enQueue(3); queue.enQueue(4); for (Integer integer : queue) { System.out.print(integer + " "); // 1 2 3 4 } System.out.println(); for (int i = 0; i < 5; i++) { System.out.print(queue.deQueue() + " "); // 1 2 3 4 null } } }
(5)符号表
a. 概述
-
符号表最主要的目的就是将一个键和一个值联系起来,符号表能够将存储的数据元素是一个键和一个值共同组成的键值对数据,我们可以根据键来查找对应的值
键 key 值 value 1 张三 2 李四 3 王五 4 赵六 -
符号表中,键具有唯一性
b. 无序符号表
-
不考虑键值对的顺序
-
代码实现
-
API
package com.linear.symbol; public class Symbol<Key, Value> { private Node head; private int size; private class Node { Key key; Value value; Node next; public Node(Key key, Value value, Node next) { this.key = key; this.value = value; this.next = next; } } public Symbol() { this.head = new Node(null, null, null); this.size = 0; } public boolean empty() { return this.size == 0; } public int length() { return this.size; } // 添加新键值对 public void insert(Key key, Value value) { Node node = this.head; while (node.next != null) { node = node.next; if (node.key.equals(key)) { node.value = value; this.size++; return; } } node = new Node(key, value, this.head.next); this.head.next = node; this.size++; } // 删除指定键的键值对 public void remove(Key key) { Node node = this.head; while (node.next != null) { if (node.next.key.equals(key)) { node.next = node.next.next; this.size--; return; } node = node.next; } } // 获取指定键对应的值 public Value get(Key key) { Node node = this.head; while (node.next != null) { node = node.next; if (node.key.equals(key)) { return node.value; } } return null; } }
-
测试
package com.linear.symbol; public class Test { public static void main(String[] args) { Symbol<Integer, String> symbol = new Symbol<>(); symbol.insert(1, "abc"); symbol.insert(2, "def"); symbol.insert(3, "ghi"); System.out.println(symbol.get(2)); symbol.insert(2, "jkl"); System.out.println(symbol.get(2)); symbol.remove(2); System.out.println(symbol.get(2)); } }
-
c. 有序符号表
-
键值对存在一定的顺序,如:根据键的大小进行排序
-
代码实现
-
API
package com.linear.symbol; public class OrderSymbol<Key extends Comparable<Key>, Value> { private Node head; private int size; private class Node { Key key; Value value; Node next; public Node(Key key, Value value, Node next) { this.key = key; this.value = value; this.next = next; } } public OrderSymbol() { this.head = new Node(null, null, null); this.size = 0; } public boolean empty() { return this.size == 0; } public int length() { return this.size; } // 添加新键值对 public void insert(Key key, Value value) { Node node = this.head.next; Node pre = this.head; while (node != null && key.compareTo(node.key) > 0) { pre = node; node = node.next; } if (node != null && key.compareTo(node.key) == 0) { node.value = value; this.size++; return; } pre.next = new Node(key, value, node); this.size++; } // 删除指定键的键值对 public void remove(Key key) { Node node = this.head; while (node.next != null) { if (node.next.key.equals(key)) { node.next = node.next.next; this.size--; return; } node = node.next; } } // 获取指定键对应的值 public Value get(Key key) { Node node = this.head; while (node.next != null) { node = node.next; if (node.key.equals(key)) { return node.value; } } return null; } // 测试用函数, 用于分析键值对相对关系 public void test() { Node node = this.head; while (node.next != null) { node = node.next; System.out.print(node.value + " "); } } }
-
测试
package com.linear.symbol; public class Test { public static void main(String[] args) { OrderSymbol<Integer, String> orderSymbol = new OrderSymbol<>(); orderSymbol.insert(2, "a"); orderSymbol.insert(1, "b"); orderSymbol.insert(4, "c"); orderSymbol.insert(5, "d"); orderSymbol.insert(3, "e"); orderSymbol.test(); // b a e c d } }
-
0x03 树
(1)概述
a. 基本定义
-
树是由 n(n>=1)个有限结点组成一个具有层次关系的集合
-
根朝上,叶朝下
graph TB A-->B & C B-->D & E C-->F -
特点
- 每个结点有零个或多个子结点
- 没有父结点的结点为根结点
- 每一个非根结点只有一个父结点
- 每个结点及其后代结点整体上可以看做是一棵树,称作当前结点的父结点的一个子树
b. 相关术语
-
结点的度:一个结点含有的子树的个数称为该结点的度
以上述树为例,B 的度为 2,C 的度为 1
-
叶结点:度为 0 的结点称为叶结点,也可以叫做终端结点
以上述树为例,叶结点为 D、E、F
-
分支结点:度不为 0 的结点称为分支结点,也可以叫做非终端结点
以上述树为例,分支结点为 B、C
-
结点的层次:从根结点开始,根结点的层次为 1,根的直接后继层次为 2,以此类推
以上述树为例,该树的层次为 3
-
结点的层序编号:将树中的结点,按照从上层到下层,同层从左到右的次序排成一个线性序列,把他们编成连续的自然数
graph TB 1-->2 & 3 2-->4 & 5 3-->6 -
树的度:树中所有结点的度的最大值
以上述树为例,该树的度为 2
-
树的高度(深度):树中结点的最大层次
以上述树为例,该树的深度为 3
-
森林:m(m>=0)个互不相交的树的集合,将一颗非空树的根结点删去,树就变成一个森林,给森林增加一个统一的根结点,森林就变成一棵树
graph TB B-->D & E C-->F -
孩子结点:一个结点的直接后继结点称为该结点的孩子结点
以上述树为例,D 是 B 的孩子结点
-
双亲结点(父结点):一个结点的直接前驱称为该结点的双亲结点
以上述树为例,B 是 D 的双亲结点
-
兄弟结点:同一双亲结点的孩子结点间互称兄弟结点
以上述树为例,D 和 E 互称兄弟结点
(2)二叉树
a. 基本定义
-
二叉树就是度不超过 2 的树(每个结点最多有两个子结点)
-
满二叉树:一个二叉树,如果每一个层的结点树都达到最大值,则这个二叉树就是满二叉树
graph TB A-->B & C B-->D & E C-->F & G -
完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若千位置的二叉树
graph TB A-->B & C B-->D & E C-->F & a((null))
b. 二叉查找树
-
二叉查找树的子结点的键与父结点的键遵循以下规则:
\[左子结点 \lt 父节点 \lt 右子结点 \]graph TB 5-->4 & 7 4-->3 & a((null)) 3-->2 & b((null)) 7-->c((null)) & 10 10-->9 & d((null))
I. 实现
-
API
package com.tree; public class BinaryTree<Key extends Comparable<Key>, Value> { private Node root; private int size; private class Node { Key key; Value value; Node left; Node right; public Node(Key key, Value value, Node left, Node right) { this.key = key; this.value = value; this.left = left; this.right = right; } } public boolean empty() { return this.size == 0; } public int length() { return this.size; } // 添加新结点 public void put(Key key, Value value) { this.root = this.put(this.root, key, value); } // 在指定的树中添加新结点并返回该树 private Node put(Node node, Key key, Value value) { if (node == null) { this.size++; return new Node(key, value, null, null); } int cmp = key.compareTo(node.key); if (cmp > 0) { node.right = this.put(node.right, key, value); } else if (cmp < 0) { node.left = this.put(node.left, key, value); } else { node.value = value; } return node; } // 获取指定键对应的值 public Value get(Key key) { return this.get(this.root, key); } // 获取指定树中指定键对应的值 private Value get(Node node, Key key) { if (node == null) { return null; } int cmp = key.compareTo(node.key); if (cmp > 0) { return this.get(node.right, key); } else if (cmp < 0) { return this.get(node.left, key); } else { return node.value; } } // 删除指定键对应的值 public void remove(Key key) { this.root = this.remove(this.root, key); } // 删除指定树中指定键对应的值 private Node remove(Node node, Key key) { if (node == null) { return null; } int cmp = key.compareTo(node.key); if (cmp > 0) { node.right = this.remove(node.right, key); } else if (cmp < 0) { node.left = this.remove(node.left, key); } else { // 获取右子数中最小结点 if (node.right == null) { return node.left; } if (node.left == null) { return node.right; } Node minNode = node.right; while (minNode.left != null) { minNode = minNode.left; } // 删除右子数中最小结点 Node tmpNode = node.right; while(tmpNode.left != null) { if (tmpNode.left.left == null) { tmpNode.left = null; } else { tmpNode = tmpNode.left; } } minNode.left = node.left; minNode.right = node.right; node = minNode; this.size--; } return node; } }
-
测试
package com.tree; public class Test { public static void main(String[] args) { BinaryTree<Integer, String> bt = new BinaryTree<>(); bt.put(1, "a"); bt.put(2, "b"); bt.put(3, "c"); System.out.println(bt.get(2)); // b bt.remove(2); System.out.println(bt.get(2)); // null } }
II. 查找二叉树中最小键
-
在 API 添加方法
public Key min() { return this.min(root).key; } private Node min(Node node) { if (node.left != null) { return this.min(node.left); } else { return node; } }
-
测试
public static void main(String[] args) { BinaryTree<Integer, String> bt = new BinaryTree<>(); bt.put(1, "a"); bt.put(2, "b"); bt.put(3, "c"); System.out.println(bt.min()); // 1 }
III. 查找二叉树中最大键
-
在 API 添加方法
public Key max() { return this.max(root).key; } private Node max(Node node) { if (node.right != null) { return this.max(node.right); } else { return node; } }
-
测试
public static void main(String[] args) { BinaryTree<Integer, String> bt = new BinaryTree<>(); bt.put(1, "a"); bt.put(2, "b"); bt.put(3, "c"); System.out.println(bt.max()); // 3 }
c. 遍历
有二叉树如下:
-
按照父结点什么时候被访问,我们可以把二叉树的遍历分为以下三种方式
-
前序遍历(先序遍历)
graph LR 父结点-->左子结点-->右子结点结果:ABDECF
-
中序遍历
graph LR 左子结点-->父结点-->右子结点结果:DBEAFC
-
后序遍历
graph LR 左子结点-->右子结点-->父结点结果:DEBFCA
-
-
层序遍历:从根节点(第一层)开始,依次向下,获取每一层所有结点的值,对于以下二叉树该二叉树的层序遍历结果为 ABCDEF
-
代码实现
-
API(在上述 “二叉查找树” API 的基础上添加方法)
package com.tree; // 引用之前实现过的 队列 数据结构 import com.linear.queue.Queue; public class BinaryTree<Key extends Comparable<Key>, Value> { // ... // 前序遍历 public void preOrder() { Queue<Value> queue = new Queue<>(); preOrder(this.root, queue); for (Value value : queue) { System.out.print(value + " "); } System.out.println(); } private void preOrder(Node node, Queue<Value> queue) { if (node == null) { return; } queue.enQueue(node.value); if (node.left != null) { preOrder(node.left, queue); } if (node.right != null) { preOrder(node.right, queue); } } // 中序遍历 public void inOrder() { Queue<Value> queue = new Queue<>(); inOrder(this.root, queue); for (Value value : queue) { System.out.print(value + " "); } System.out.println(); } private void inOrder(Node node, Queue<Value> queue) { if (node == null) { return; } if (node.left != null) { inOrder(node.left, queue); } queue.enQueue(node.value); if (node.right != null) { inOrder(node.right, queue); } } // 后序遍历 public void postOrder() { Queue<Value> queue = new Queue<>(); postOrder(this.root, queue); for (Value value : queue) { System.out.print(value + " "); } System.out.println(); } private void postOrder(Node node, Queue<Value> queue) { if (node == null) { return; } if (node.left != null) { postOrder(node.left, queue); } if (node.right != null) { postOrder(node.right, queue); } queue.enQueue(node.value); } // 层序遍历 public void levelOrder() { Queue<Value> queue = new Queue<>(); Queue<Node> nodes = new Queue<>(); nodes.enQueue(this.root); while (!nodes.empty()) { Node node = nodes.deQueue(); queue.enQueue(node.value); if (node.left != null) { nodes.enQueue(node.left); } if (node.right != null) { nodes.enQueue(node.right); } } for (Value value : queue) { System.out.print(value + " "); } System.out.println(); } }
-
测试
package com.tree; public class Test { public static void main(String[] args) { BinaryTree<Integer, String> bt = new BinaryTree<>(); bt.put(4, "A"); bt.put(2, "B"); bt.put(6, "C"); bt.put(1, "D"); bt.put(3, "E"); bt.put(5, "F"); bt.preOrder(); // A B D E C F bt.inOrder(); // D B E A F C bt.postOrder(); // D E B F C A bt.levelOrder(); // A B C D E F } }
-
d. 最大深度问题
-
问题描述
给定一棵树,请计算树的最大深度,即树的根节点到最远叶子结点的最长路径上的结点数
-
解题思路
- 如果根结点为空,则最大深度为 0
- 计算左子树的最大深度
- 计算右子树的最大深度
- 当前树的最大深度 = 左子树的最大深度和右子树的最大深度中的较大者 + 1
-
代码实现
-
API
package com.tree; public class BinaryTree<Key extends Comparable<Key>, Value> { // ... // 获取最大深度 public int maxDepth() { return maxDepth(this.root); } private int maxDepth(Node node) { if (node == null) { return 0; } // 左子树最大深度 int maxLeft = 0; // 右子数最大深度 int maxRight = 0; if (node.left != null) { maxLeft = maxDepth(node.left); } if (node.right != null) { maxRight = maxDepth(node.right); } return Math.max(maxLeft, maxRight) + 1; } }
-
测试
package com.tree; public class Test { public static void main(String[] args) { BinaryTree<Integer, String> bt = new BinaryTree<>(); bt.put(4, "A"); bt.put(2, "B"); bt.put(6, "C"); bt.put(1, "D"); bt.put(3, "E"); bt.put(5, "F"); System.out.println(bt.maxDepth()); // 3 } }
-
e. 折纸问题
-
问题描述
请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折 1 次,压出折痕后展开。此时 折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折 2 次,压出折痕后展开,此时有 3 条折痕,从上到下依次是下折痕、下折痕和上折痕
给定一个输入参数 N,代表纸条都从下边向上方连续对折 N 次,请从上到下打印所有折痕的方向
例如:N=1 时,打印:down;N=2 时,打印:down down up
-
解题思路
- 定义结点类
- 构建深度为 N 的折痕树
- 第一次对折,只有一条折痕,创建根结点
- 如果不是第一次对折,则使用队列保存根结点
- 循环遍历队列
- 从队列中拿出一个结点
- 如果这个结点的左子结点不为空,则把这个左子结点添加到队列中
- 如果这个结点的右子结点不为空,则把这个右子结点添加到队列中
- 判断当前结点的左子结点和右子结点都不为空,如果是,则需要为当前结点创建一个值为
down
的左子结点,一个值为up
的右子结点
- 使用中序遍历,打印出树中所有结点的内容
-
代码实现
package com.tree; import com.linear.queue.Queue; public class Test { public static void main(String[] args) { Node<String> tree = createTree(1); inOrderTree(tree); // down System.out.println(); tree = createTree(2); inOrderTree(tree); // down down up System.out.println(); } public static Node<String> createTree(int n) { Node<String> root = null; for (int i = 1; i <= n; i++) { if (i == 1) { root = new Node<>("down", null, null); continue; } Queue<Node> queue = new Queue<>(); queue.enQueue(root); while (!queue.empty()) { Node node = queue.deQueue(); if (node.left != null) { queue.enQueue(node.left); } if (node.right != null) { queue.enQueue(node.right); } if (node.left == null && node.right == null) { node.left = new Node<String>("down", null, null); node.right = new Node<String>("up", null, null); } } } return root; } public static void inOrderTree(Node<String> node) { if (node == null) { return; } if (node.left != null) { inOrderTree(node.left); } System.out.print(node.data + " "); if (node.right != null) { inOrderTree(node.right); } } private static class Node<T> { T data; Node left; Node right; public Node(T data, Node left, Node right) { this.data = data; this.left = left; this.right = right; } } }
(3)堆
a. 概述
-
定义:堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组对象
-
特性:
-
完全二叉树
-
用数组实现
如果一个结点的位置为 \(k\),则他的父结点的位置为 \(\frac k2\),子结点的位置分别为 \(2k\) 和 \(2k+1\)
-
每个结点都大于等于其两个子结点
-
b. 实现
-
API
package com.tree; public class Heap<T extends Comparable<T>> { private T[] elements; private int size; public Heap(int capacity) { this.elements = (T[]) new Comparable[capacity]; this.size = 0; } private boolean smallerOrNot(int i, int j) { return elements[i].compareTo(elements[j]) < 0; } private void swap(int i, int j) { T temp = elements[i]; elements[i] = elements[j]; elements[j] = temp; } public void insert(T element) { this.elements[++this.size] = element; swim(this.size); } // 上浮算法, 使索引 k 处的元素能在堆中处于一个正确的位置 private void swim(int k) { // 通过循环,不断的比较当前结点的值和其父结点的值,如果发现父结点的值比当前结点的值小,则交换位置 while (k > 1) { if (smallerOrNot(k / 2, k)) { swap(k / 2, k); } k = k / 2; } } // 删除堆中最大的元素 public T removeMax() { T max = elements[1]; swap(1, this.size); elements[this.size] = null; this.size--; sink(1); return max; } // 下沉算法,使索引 k 处的元素能在堆中处于一个正确的位置 private void sink(int k) { // 通过循环,不断的比较当前结点的值和其左右子结点的值,如果比当前结点的值小,则交换位置 while (2*k <= this.size) { int max = (2*k+1 <= this.size) ? (smallerOrNot(2*k, 2*k+1) ? 2*k+1 : 2*k ) : 2*k; if (!smallerOrNot(k, max)) { break; } swap(k, max); k = max; } } }
-
测试
package com.tree; public class Test { public static void main(String[] args) { Heap<String> heap = new Heap<String>(10); heap.insert("A"); heap.insert("B"); heap.insert("C"); heap.insert("D"); heap.insert("E"); heap.insert("F"); heap.insert("G"); String str = null; while ((str = heap.removeMax()) != null) { System.out.print(str + " "); } // G F E D C B A } }
c. 堆排序
-
实现步骤
- 构造堆
- 得到堆顶元素,这个值就是最大值
- 交换堆顶元素和数组中的最后一个元素,此时所有元素中的最大元素已经放到合适的位置
- 对堆进行调整,重新让除了最后一个元素的剩余元素中的最大值放到堆顶
- 重复 2~4 这个步骤,直到堆中剩一个元素为止
-
代码实现
-
API
package com.tree; public class HeapSort { // 从小到大排序 public static void sort(Comparable[] source) { Comparable[] heap = new Comparable[source.length + 1]; createHeap(source, heap); int index = heap.length - 1; while (index != 1) { swap(heap, 1, index); index--; sink(heap, 1, index); } System.arraycopy(heap, 1, source, 0, source.length); } // 构造堆 private static void createHeap(Comparable[] source, Comparable[] heap) { // 将 source 中的元素拷贝到 heap 中, 此时 heap 是无序的堆 System.arraycopy(source, 0, heap, 1, source.length); for (int i = (heap.length)/2; i > 0; i--) { sink(heap, i, heap.length-1); } } // 下沉算法 private static void sink(Comparable[] heap, int target, int range) { while (2*target <= range) { int max = (2*target+1 <= range) ? (smallerOrNot(heap, 2*target, 2*target+1) ? 2*target+1 : 2*target) : 2*target; if (!smallerOrNot(heap, target, max)) { break; } swap(heap, target, max); target = max; } } // 比较函数 private static boolean smallerOrNot(Comparable[] heap, int i, int j) { return heap[i].compareTo(heap[j]) < 0; } // 交换函数 private static void swap(Comparable[] heap, int i, int j) { Comparable temp = heap[i]; heap[i] = heap[j]; heap[j] = temp; } }
-
测试
package com.tree; import java.util.Arrays; public class Test { public static void main(String[] args) { String[] array = {"F", "D", "A", "C", "E", "B"}; HeapSort.sort(array); System.out.println(Arrays.toString(array)); // [A, B, C, D, E, F] } }
-
(4)优先队列
- 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除
- 在某些情况下,我们可能需要找出队列中的最大值或者最小值,普通的队列要完成这样的功能,需要每次遍历队列中的所有元素,比较并找出最大值,效率不是很高,这个时候,我们就可以使用一种特殊的队列来完成这种需求——优先队列
- 优先队列按照其作用不同,可以分为以下两种:
- 最大优先队列:可以获取并删除队列中最大的值
- 最小优先队列:可以获取并删除队列中最小的值
a. 最大优先队列
-
堆这种结构是可以方便的删除最大的值,可以基于堆实现最大优先队列
-
代码实现
-
API
package com.tree; public class MaxPriorityQueue<T extends Comparable<T>> { private T[] elements; private int size; public MaxPriorityQueue(int capacity) { this.elements = (T[]) new Comparable[capacity + 1]; this.size = 0; } public boolean empty() { return this.size == 0; } public int length() { return this.size; } public void insert(T element) { this.elements[++this.size] = element; this.swim(this.size); } public T remove() { T max = this.elements[1]; this.swap(1, this.size); this.size--; this.sink(1); return max; } private void swim(int k) { while (k > 1) { if (this.smallerOrNot(k / 2, k)) { this.swap(k / 2, k); } k = k / 2; } } private void sink(int k) { while (2*k <= this.size) { int max = (2*k+1 <= this.size) ? (this.smallerOrNot(2*k, 2*k+1) ? 2*k+1 : 2*k) : 2*k; if (!smallerOrNot(k, max)) { break; } this.swap(k, max); } } private boolean smallerOrNot(int i, int j) { return this.elements[i].compareTo(this.elements[j]) < 0; } private void swap(int i, int j) { T temp = this.elements[i]; this.elements[i] = this.elements[j]; this.elements[j] = temp; } }
-
测试
package com.tree; import java.util.Arrays; public class Test { public static void main(String[] args) { MaxPriorityQueue<String> queue = new MaxPriorityQueue<>(10); queue.insert("A"); queue.insert("B"); queue.insert("C"); queue.insert("D"); queue.insert("E"); queue.insert("F"); queue.insert("G"); while (!queue.empty()) { System.out.print(queue.remove() + " "); } // G F E D C B A } }
-
b. 最小优先队列
-
也可以基于堆实现最小优先队列
-
由于堆中存放数据元素的数组要满足都满足如下特性:
- 最大的元素放在数组的索引 1 处
- 每个结点的数据总是大于等于它的两个子结点的数据
此时该堆称为最大堆,可以用相反的思维实现最小堆,此时存在以下特性:
- 最小的元素放在数组的索引 1 处
- 每个结点的数据总是小于等于它的两个子结点的数据
-
代码实现
-
API
package com.tree; public class MinPriorityQueue<T extends Comparable<T>> { private T[] elements; private int size; public MinPriorityQueue(int capacity) { this.elements = (T[]) new Comparable[capacity + 1]; this.size = 0; } public boolean empty() { return this.size == 0; } public int length() { return this.size; } public void insert(T element) { this.elements[++this.size] = element; this.swim(this.size); } public T remove() { T min = this.elements[1]; this.swap(1, this.size); this.size--; this.sink(1); return min; } private void swim(int k) { while (k > 1) { if (this.smallerOrNot(k, k / 2)) { this.swap(k, k / 2); } k = k / 2; } } private void sink(int k) { while (2*k <= this.size) { int min = (2*k+1 <= this.size) ? (this.smallerOrNot(2*k, 2*k+1) ? 2*k : 2*k+1) : 2*k; if (smallerOrNot(k, min)) { break; } this.swap(k, min); k = min; } } private boolean smallerOrNot(int i, int j) { return this.elements[i].compareTo(this.elements[j]) < 0; } private void swap(int i, int j) { T temp = this.elements[i]; this.elements[i] = this.elements[j]; this.elements[j] = temp; } }
-
测试
package com.tree; import java.util.Arrays; public class Test { public static void main(String[] args) { MinPriorityQueue<String> queue = new MinPriorityQueue<>(10); queue.insert("A"); queue.insert("B"); queue.insert("C"); queue.insert("D"); queue.insert("E"); queue.insert("F"); queue.insert("G"); while (!queue.empty()) { System.out.print(queue.remove() + " "); } // A B C D E F G } }
-
c. 索引最小优先队列
-
索引优先队列:通过索引访问已存在于优先队列中的对象
-
索引优先队列实现思路
- 存储数据时,给每一个数据元素关联一个整数,例如
insert(int k ,T t)
,我们可以将k
看做是t
关联的整数,此时通过k
这个值即可快速获取到队列中t
这个元素,同时k
这个值需要具有唯一性 - 数组中的元素顺序是随机的,并不是堆有序的,所以可以增加一个数组
int[] array
,来保存每个元素在数组中的索引,数组需要堆有序,即array[1]
对应的数据元素要小于等于array[2]
和array[3]
对应的数据元素 - 通过上浮和下沉做堆调整的时候,其实调整的是
array
数组,当有了array
数组后,可以先通过索引,在array
数组中找到对应的索引,直接调整索引即可
- 存储数据时,给每一个数据元素关联一个整数,例如
-
代码实现
-
API
package com.tree; public class IndexMinPriorityQueue<T extends Comparable<T>> { private T[] elements; private int size; // index 保存每个元素在 elements 中的索引 private int[] index; // array 中 index 的索引为元素, index 的元素为索引 private int[] array; public IndexMinPriorityQueue(int capacity) { this.elements = (T[]) new Comparable[capacity+1]; this.size = 0; this.index = new int[capacity+1]; this.array = new int[capacity+1]; for (int i = 0; i < this.array.length; i++) { array[i] = -1; } } public boolean empty() { return this.size == 0; } public int length() { return this.size; } // 判断 k 对应的元素是否存在 public boolean exist(int k) { return this.array[k] != -1; } // 最小元素关联的索引 public int minIndex() { return this.index[1]; } public void insert(int i, T element) { if (this.exist(i)) { return; } this.size++; this.elements[i] = element; this.index[this.size] = i; this.array[i] = this.size; this.swim(this.size); } // 删除队列中最小元素并返回其关联的索引 public int remove() { int minIndex = this.index[1]; swap(1, this.size); this.array[this.index[this.size]] = -1; this.index[this.size] = -1; this.elements[minIndex] = null; this.size--; this.sink(1); return minIndex; } // 删除索引 idx 关联的元素 public void remove(int idx) { int k = this.array[idx]; this.swap(k, this.size); this.array[this.index[this.size]] = -1; this.index[this.size] = -1; this.elements[k] = null; this.size--; this.sink(k); this.swim(k); } // 修改索引 idx 关联的元素为 element public void change(int idx, T element) { this.elements[idx] = element; int k = this.array[idx]; this.sink(k); this.swim(k); } private void swim(int k) { while (k > 1) { if (this.smallerOrNot(k, k / 2)) { this.swap(k, k / 2); } k = k / 2; } } private void sink(int k) { while (2*k <= this.size) { int min = (2*k+1 <= this.size && this.smallerOrNot(2*k+1, 2*k)) ? 2*k+1 : 2*k; if (this.smallerOrNot(k, min)) { break; } this.swap(k, min); k = min; } } private boolean smallerOrNot(int i, int j) { return this.elements[this.index[i]].compareTo(this.elements[this.index[j]]) < 0; } private void swap(int i, int j) { // index 进行交换 int temp = this.index[i]; this.index[i] = this.index[j]; this.index[j] = temp; // 更新 array this.array[this.index[i]] = i; this.array[this.index[j]] = j; } }
-
测试
package com.tree; import java.util.Arrays; public class Test { public static void main(String[] args) { IndexMinPriorityQueue<String> queue = new IndexMinPriorityQueue<>(10); queue.insert(1, "A"); queue.insert(2, "B"); queue.insert(3, "C"); queue.change(2, "D"); while (!queue.empty()) { System.out.print(queue.remove() + " "); } // 1 3 2 } }
-
(5)2-3 查找树
平衡树:一种方法,能够使树不受插入数据的影响,让生成的树都像完全二叉树那样,那么即使在最坏情况下,查找的效率依旧会很好
a. 概述
-
为了保证查找树的平衡性,允许树中的一个结点保存多个键,将一棵标准的二叉查找树中的结点称为 2- 结点(含有一个键和两条链),现在需要引入 3- 结点(含有两个键和三条链),2- 结点和 3- 结点中的每条链都对应着其中保存的键所分割产生的一个区间
-
一棵 2-3 查找树在不为空的情况下满足以下要求:
- 2- 结点:含有一个键(及其对应的值)和两条链
- 左链接指向的 2-3 树中的键都小于该结点
- 右链接指向的 2-3 树中的键都大于该结点
- 3-结点:含有两个键(及其对应的值)和三条链
- 左链接指向的 2-3 树中的键都小于该结点
- 中链接指向的 2-3 树中的键都位于该结点的两个键之间
- 右链接指向的 2-3 树中的键都大于该结点
- 2- 结点:含有一个键(及其对应的值)和两条链
-
2-3 树举例
flowchart TB M((13))---EJ([5      10]) & R((18)) EJ---AC([1      3]) & H((8)) & L((12)) R---P((16)) & SX([19      24])18
:为 2- 结点5 10
:为 3- 结点- 叶子结点以下为空链接
b. 查找
- 要判断一个键是否在树中,我们先将它和根结点中的键比较:
- 如果它和其中任意一个相等,查找命中
- 否则我们就根据比较的结果找到指向相应区间的连接
- 如果这个是空链接,查找未命中
- 否则在其指向的子树中递归地继续查找,直至命中
c. 插入
-
首先要进行查找,然后将新结点挂到未找到的结点上
-
向含有 2- 结点的树中插入新结点
插入 11
2- 结点变为 3- 结点
-
原树
flowchart TB EJ([5      10])---H((8)) & L((12)) -
新树
flowchart TB EJ([5      10])---H((8)) & KL([11      12])
-
-
向只含有 3- 结点的树中插入新结点
插入 19
3- 结点变为 4- 结点,将 4- 结点的中间元素提升为一个 2- 结点
-
原树
flowchart TB AE([1      5]) -
中间树
flowchart TB AES([1      5      19]) -
新树
flowchart TB E((5))---A((1)) & S((19))
-
-
向父结点为 2- 结点的 3- 结点的树中插入
插入 26
3- 结点变为 4- 结点,将 4- 结点中的中间元素提升到父结点即 2- 结点中,使其父结点成为一个 3- 结点,将左右结点分别挂在这个 3- 结点的恰当位置-
原树
flowchart TB R((18))---P((16)) & SX([19      24]) -
中间树
flowchart TB R((18))---P((16)) & SXZ([19      24      26]) -
新树
flowchart TB RZ([18      26])---P((16)) & S((19)) & X((24))
-
-
向父结点为 3- 结点的 3- 结点的树中插入
插入 4
3- 结点变为 4- 结点,将 4- 结点中的中间元素提升到父结点即 3- 结点中,使其父结点成为一个 4- 结点,将左右结点分别挂在这个 4- 结点的恰当位置,再将父结点 4- 结点的中间元素继续向上提升直至 2- 结点变为 3- 结点-
原树
flowchart TB EJ([5      10])---AC([1      3]) & H((8)) & L((12)) -
中间树
flowchart TB EJ([5      10])---ACD([1      3      4]) & H((8)) & L((12))flowchart TB CEJ([3      5     10])---A((1)) & D((4)) & H((8)) & L((12)) -
新树
flowchart TB E((5))---C((3)) & J((10)) C---A((1)) & D((4)) J---H((8)) & L((12))
-
-
分解根结点
d. 性质
- 一棵完全平衡的 2-3 树具有以下性质
- 任意空链接到根结点的路径长度都是相等的
- 4- 结点变换为 3- 结点时,树的高度不会发生变化,只有当根结点是临时的 4- 结点,分解根结点时,树高 +1
- 普通的二叉查找树是自顶向下生长,而 2-3 树是自底向上生长
(6)红黑树
a. 定义
-
2-3 树思想的简单实现
-
红黑树主要是对 2-3 树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由 2- 结点构成)和一些额外的信息(替换 3- 结点)来表示 2-3 树
-
树中的链接分为两种类型:
- 红链接:将两个 2- 结点连接起来构成一个 3- 结点
- 黑链接:是 2-3 树中的普通链接
-
红黑树是含有红黑链接并满足下列条件的二叉查找树:
- 红链接均为左链接
- 没有任何一个结点同时和两条红链接相连
- 完美黑色平衡,即任意空链接到根结点的路径上的黑链接数量相等
-
红黑树举例
-
红黑树(虚线表示红链接)
flowchart TB M((13))---J((10)) & R((18)) E((5)) & L((12)) J-.-E J---L E---C((3)) & H((8)) C-.-A((1)) R---P((16)) & X((24)) X-.-S((19)) -
2-3 树
flowchart TB M((13))---EJ([5      10]) & R((18)) EJ---AC([1      3]) & H((8)) & L((12)) R---P((16)) & SX([19      24])
-
b. 结点 API
-
每个结点都只会有一条指向自己的链接(来自父结点),可以在之前的Node结点中添加一个布尔类型的变量
color
来表示链接的颜色- 如果是红链接,那么该变量的值为
true
- 如果是黑链接,那么该变量的值为
false
- 如果是红链接,那么该变量的值为
-
代码实现
private class Node<Key, Value> { private Key key; private Value value; private Node left; private Node right; private boolean color; public Node(Key key, Value value, Node left, Node right, boolean color) { this.key = key; this.value = value; this.left = left; this.right = right; this.color = color; } }
c. 平衡化
- 平衡化:在对红黑树进行一些增删改查的操作后,很有可能会出现红色的右链接或者两条连续红色的链接,而这些都不满足红黑树的定义,所以需要对这些情况通过旋转进行修复,让红黑树保持平衡
I. 左旋
-
当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋
-
举例:(虚线表示红链接)
flowchart TB a([x < 5]) & S([S: 19]) E([E: 5])---a E-.-S---b([5 < x < 19]) & c([x > 19])- 让 S 结点的左子结点变为 E 的右子结点
- 让 E 结点成为 S 结点的左子结点
- 让 S 结点的
color
属性值变为 E 结点的color
属性值 - 让 E 结点的
color
属性值变为true
flowchart TB E([E: 5]) & a([x > 19]) S([S: 19])---a S-.-E---b([x < 5]) & c([5 < x < 19])
II. 右旋
-
当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋
-
举例:(虚线表示红链接)
flowchart TB E([E: 5]) & a([x > 19]) S([S: 19])---a b([x < 5]) & c([5 < x < 19]) S-.-E-.-b E---c- 让 E 结点的右子结点变为 S 的左子结点
- 让 S 结点成为 E 结点的右子结点
- 让 E 结点的
color
属性值变为 S 结点的color
属性值 - 让 S 结点的
color
属性值变为true
flowchart TB E([E: 5])-.-a([x < 5]) & S([S: 19]) S---b([5 < x < 19]) & c([x > 19])
d. 颜色反转
-
当一个结点的左子结点和右子结点都是红链接时,即出现了临时的 4- 结点,此时只需把左子结点和右子结点黑链接,同时让当前结点变为红链接即可
-
原树
flowchart TB D((4))---B((2)) & n[null] B-.-A((1)) & C((3)) -
新树
flowchart TB B((2)) & n[null] D((4))-.-B D---n B---A((1)) & C((3))
-
-
由于根结点没有父结点,所有根结点永远是黑链接,因此在颜色反转后,需要确保根结点为黑链接
e. 插入
I. 向单个 2- 结点插入
-
如果新键小于当前结点的键,只需新增一个红色结点即可,新的红黑树和单个 3- 结点完全等价
-
原树
flowchart TB b((2))---c1[null] & c2[null] -
新树
flowchart TB a((1)) & c[null] b((2))-.-a b---c
-
-
如果新键大于当前结点的键,那么新增的红色结点将会产生一条红色的右链接,此时需要通过左旋,把红色右链接变成左链接,插入操作才算完成
- 形成的新的红黑树依然和 3- 结点等价,其中含有两个键,一条红色链接
-
原树
flowchart TB b((2))---a1[null] & a2[null] -
中间树
flowchart TB c((3)) & a[null] b((2))---a b-.-c -
新树
flowchart TB b((2)) & a[null] c((3))-.-b c---a
II. 向底部 2- 结点插入
-
在树的底部新增一个结点(可以保证有序性),用红链接将新结点与其父结点相连
- 如果它的父结点是一个 2- 结点,那么上述的向单个 2- 结点插入两种方式仍然适用
-
原树
flowchart TB R((18)) & n[null] E((5))---A((1)) & S((19)) S-.-R S---n -
中间树(插入结点)
flowchart TB R((18)) & n2[null] C((3)) & n1[null] E((5))---A((1)) & S((19)) A---n1 A-.-C S-.-R S---n2 -
新树(左旋)
flowchart TB R((18)) & n2[null] A((1)) & n1[null] E((5))---C((3)) & S((19)) C-.-A C---n1 S-.-R S---n2
III. 向单个 3- 结点插入
-
新键大于原树中的两个键
-
原树
flowchart TB A((1)) & n[null] B((2))-.-A B---n -
中间树(插入结点)
flowchart TB B((2))-.-A((1)) & C((3)) -
新树(颜色反转)
flowchart TB B((2))---A((1)) & C((3))
-
-
新键小于原树中的两个键
-
原树
flowchart TB B((2)) & n[null] C((3))-.-B C---n -
中间树(插入结点、右旋)
flowchart TB B((2)) & n1[null] A((1)) & n2[null] C((3))-.-B C---n1 B-.-A B---n2flowchart TB B((2))-.-A((1)) & C((3)) -
新树(颜色反转)
flowchart TB B((2))---A((1)) & C((3))
-
-
新键介于原数中两个键之间
-
原树
flowchart TB A((1)) & n[null] C((3))-.-A C---n -
中间树(插入结点、左旋、右旋)
flowchart TB A((1)) & n1[null] B((2)) & n2[null] C((3))-.-A C---n1 A---n2 A-.-Bflowchart TB B((2)) & n1[null] A((1)) & n2[null] C((3))-.-B C---n1 B-.-A B---n2flowchart TB B((2))-.-A((1)) & C((3)) -
新树(颜色反转)
flowchart TB B((2))---A((1)) & C((3))
-
IV. 向底部 3- 结点插入
-
假设在树的底部的一个3-结点下加入一个新的结点,指向新结点的链接可能是:
- 3- 结点的右链接(此时只需颜色反转)
- 3- 结点的左链接(此时需要进行右旋转后再反转)
- 3- 结点的中链接(此时需要先左旋转后再右旋转,最后颜色反转)
-
颜色反转会使中间结点变为红链接,相当于将它送入了父结点,即父结点中继续插入一个新键,只需使用相同的方法解决即可,直到遇到一个 2- 结点或者根结点为止
-
原树
flowchart TB E((5))---C((3)) & S((19)) A((1)) & n1[null] C-.-A C---n1 R((18)) & n2[null] S-.-R S---n2 -
中间树(插入结点、右旋、颜色反转)
flowchart TB E((5))---C((3)) & S((19)) A((1)) & n1[null] C-.-A C---n1 R((18)) & n2[null] S-.-R S---n2 H((8)) & n3[null] R-.-H R---n3flowchart TB E((5))---C((3)) & R((18)) A((1)) & n1[null] C-.-A C---n1 R-.-H((8)) & S((19))flowchart TB C((3)) & R((18)) E((5))---C E-.-R A((1)) & n1[null] C-.-A C---n1 R---H((8)) & S((19)) -
新树(左旋)
flowchart TB E((5)) & S((19)) R((18))-.-E R---S E---C((3)) & H((8)) A((1)) & n[null] C-.-A C---n
-
f. 实现
-
API
package com.tree; public class RedBlackTree<Key extends Comparable<Key>, Value> { private Node root; private int size; private static final boolean RED = true; private static final boolean BLACK = false; private class Node { private Key key; private Value value; private Node left; private Node right; private boolean color; public Node(Key key, Value value, Node left, Node right, boolean color) { this.key = key; this.value = value; this.left = left; this.right = right; this.color = color; } } public boolean empty() { return this.size == 0; } public int length() { return this.size; } public boolean red(Node node) { if (node == null) { return false; } return node.color == RED; } private Node rotateLeft(Node node1) { Node node2 = node1.right; node1.right = node2.left; node2.left = node1; node2.color = node1.color; node1.color = RED; return node2; } private Node rotateRight(Node node1) { Node node2 = node1.left; node1.left = node2.right; node2.right = node1; node2.color = node1.color; node1.color = RED; return node2; } private void flipColors(Node node) { node.color = RED; node.left.color = BLACK; node.right.color = BLACK; } public void insert(Key key, Value value) { this.root = this.insert(this.root, key, value); this.root.color = BLACK; } private Node insert(Node node, Key key, Value value) { if (node == null) { this.size++; return new Node(key, value, null, null, RED); } int cmp = key.compareTo(node.key); if (cmp < 0) { node.left = this.insert(node.left, key, value); } else if (cmp > 0) { node.right = this.insert(node.right, key, value); } else { node.value = value; } if (this.red(node.right) && !this.red(node.left)) { node = this.rotateLeft(node); } if (this.red(node.left) && this.red(node.left)) { node = this.rotateRight(node); } if (this.red(node.left) && this.red(node.right)) { this.flipColors(node); } return node; } public Value get(Key key) { return this.get(this.root, key); } private Value get(Node node, Key key) { if (node == null) { return null; } int cmp = key.compareTo(node.key); if (cmp < 0) { return this.get(node.left, key); } else if (cmp > 0) { return this.get(node.right, key); } else { return node.value; } } }
-
测试
package com.tree; import java.util.Arrays; public class Test { public static void main(String[] args) { RedBlackTree<Integer, String> tree = new RedBlackTree<>(); tree.insert(1, "a"); tree.insert(2, "b"); tree.insert(3, "c"); System.out.println(tree.get(1)); System.out.println(tree.get(2)); System.out.println(tree.get(3)); } }
(7)B- 树
-
B- 树即为 B 树
-
B 树中允许一个结点中包含多个键
-
假设选择一个参数 M,构造一个 B 树,该树称作 M 阶的 B 树,具有如下特点:
- 每个结点最多有 M-1 个键,并且以升序排列
- 每个结点最多能有 M 个子结点
- 根结点至少有两个子结点
-
举例:M 取值为 5,此时每个结点包含 4 个键值对,例子中忽略具体值,仅以键表示
-
插入 39
flowchart TB A[39 / null / null / null] -
依次插入 22、97、41
flowchart TB A[22 / 39 / 41 / 97] -
插入 53
flowchart TB A[41 / null / null / null]---B[22 / 39 / null / null] & C[53 / 97 / null / null] -
依次插入 13、21
flowchart TB A[41 / null / null / null]---B[13 / 21 / 22 / 39] & C[53 / 97 / null / null] -
插入 40
flowchart TB A[22 / 41 / null / null]---B[13 / 21 / null / null] & C[39 / 40 / null / null] & D[53 / 97 / null / null] -
依次插入 30、27、33、36、35、34、24、29
flowchart TB A[22 / 33 / 36 / 41]---B[13 / 21 / null / null] & C[24 / 27 / 29 / 30] & D[34 / 35 / null / null] & E[39 / 40 / null / null] & F[53 / 97 / null / null] -
插入 26
flowchart TB A[33 / null / null / null]---B[22 / 27 / null / null] & C[36 / 41 / null / null] B---D[13 / 21 / null / null] & E[24 / 26 / null / null] & F[29 / 30 / null / null] C---G[34 / 35 / null / null] & H[39 / 40 / null / null] & I[53 / 97 / null / null]
-
(8)B+ 树
-
B+ 树是 B 树的一种变形树,区别在于:
- 非叶子结点仅具有索引作用,即非叶子结点只存储键,不存储值
- 树的所有叶结点构成一个有序链表,可以按照键排序的次序遍历全部数据
-
举例:构建 5 阶的 B+ 树
-
插入 5
flowchart TB A[5:data / null / null / null] -
依次插入 8、10、15
flowchart TB A[5:data / 8:data / 10:data / 15:data] -
插入 16
flowchart TB A[10 / null / null / null]---B[5:data / 8:data / null / null] & C[10:data / 15:data / 16:data / null] -
插入 17
flowchart TB A[10 / null / null / null]---B[5:data / 8:data / null / null] & C[10:data / 15:data / 16:data / 17:data] -
插入 18
flowchart TB A[10 / 16 / null / null]---B[5:data / 8:data / null / null] & C[10:data / 15:data / null / null] & D[16:data / 17:data / 18:data / null] -
依次插入 6、9、19、20、21、22
flowchart TB A[10 / 16 / 18 / 20]---B[5:data / 6:data / 8:data / 9:data] & C[10:data / 15:data / null / null] & D[16:data / 17:data / null / null] & E[18:data / 19:data / null / null] & F[20:data / 21:data / 22:data / null] -
插入 7
flowchart TB A[16 / null / null / null]---B[7 / 10 / null / null] & C[18 / 20 / null / null] B---D[5:data / 6:data / null / null] & E[7:data / 8:data / 9:data / null] & F[10:data / 15:data / null / null] C---G[16:data / 17:data / null / null] & H[18:data / 19:data / null / null] & I[20:data / 21:data / 22:data / null]
-
(9)并查集
a. 概述
-
并查集是一种树形结构,可以高效地完成以下操作:
- 查询某两个元素是否同组
- 将某个元素合并到另一个元素所在的组
-
并查集树具有以下要求:
- 每个元素都对应唯一一个结点
- 每组数据中的多个元素都在同一棵树中
- “一个组中的数据对应的树” 和 “另外一个组中的苏剧对应的树” 之间没有任何联系
- 元素在树中没有父子关系的硬性要求
flowchart LR a[1   3   5] subgraph 第一组数据 a2((1))---a3((3)) & a4((5)) end a-->第一组数据 b[2] subgraph 第二组数据 b2((2)) end b-->第二组数据 c[4   6   7] subgraph 第三组数据 c2((6))---c3((4)) & n1[null] c3---c4((7)) & n2[null] end c-->第三组数据
b. 实现
I. 基本实现
-
API
package com.tree; public class UF { // 记录结点元素和该元素所在分组的标识 private int[] elementWithGroup; // 分组个数 private int count; public UF(int capacity) { this.count = capacity; this.elementWithGroup = new int[capacity]; for (int i = 0; i < capacity; i++) { this.elementWithGroup[i] = i; } } public int getCount() { return this.count; } // 找到元素 element 所在分组的标识符 public int find(int element) { return this.elementWithGroup[element]; } // 判断是否同一分组 public boolean connected(int element1, int element2) { return this.find(element1) == this.find(element2); } // 合并算法 public void merge(int element1, int element2) { // 如果两个元素同组则无需合并 if (this.connected(element1, element2)) { return; } // 如果两个元素不同组,需要将某个元素所在组的所有元素的组标识符修改为另一个元素所在组的标识符 int group1 = this.find(element1); int group2 = this.find(element2); // 合并 for (int i = 0; i < this.elementWithGroup.length; i++) { if (this.elementWithGroup[i] == group1) { elementWithGroup[i] = group2; } } // 分组数量减一 this.count--; } }
-
测试
package com.tree; import java.util.Scanner; public class Test { public static void main(String[] args) { UF uf = new UF(5); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("Enter the 1st element that needs to be merged: "); int element1 = scanner.nextInt(); System.out.print("Enter the 2nd element that needs to be merged: "); int element2 = scanner.nextInt(); if (uf.connected(element1, element2)) { System.out.println(element1 + " and " + element2 + " are already in the same group"); continue; } uf.merge(element1, element2); System.out.println("There are currently " + uf.getCount() + " groups left"); } } }
II. UF_Tree 算法优化
-
基本实现中,合并算法
merge
的时间复杂度为 \(O(n^2)\),不适合解决大规模问题,因此需要优化查询算法find
和合并算法merge
-
为进行优化,需要对数组
elementWithGroup
含义修改如下:- 数组的索引作为某个结点的元素
- 数组的值作为该结点的父结点的值
-
代码实现
-
API
package com.tree; public class UF_Tree { // 记录结点元素和该元素所在分组的标识 private int[] elementWithGroup; // 分组个数 private int count; public UF_Tree(int capacity) { this.count = capacity; this.elementWithGroup = new int[capacity]; for (int i = 0; i < capacity; i++) { this.elementWithGroup[i] = i; } } public int getCount() { return this.count; } // 找到元素 element 所在分组的标识符 public int find(int element) { while (true) { if (element == this.elementWithGroup[element]) { return element; } element = this.elementWithGroup[element]; } } // 判断是否同一分组 public boolean connected(int element1, int element2) { return this.find(element1) == this.find(element2); } // 合并算法 public void merge(int element1, int element2) { // 找到两个元素各自所在组的树的根结点 int root1 = this.find(element1); int root2 = this.find(element2); // 如果同组则不需合并 if (root1 == root2) { return; } // 使元素所在的树的根结点的父结点为另一个元素所在树的根结点 this.elementWithGroup[root1] = root2; // 组的数量减一 this.count--; } }
-
测试
package com.tree; import java.util.Scanner; public class Test { public static void main(String[] args) { UF_Tree ut = new UF_Tree(5); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("Enter the 1st element that needs to be merged: "); int element1 = scanner.nextInt(); System.out.print("Enter the 2nd element that needs to be merged: "); int element2 = scanner.nextInt(); if (ut.connected(element1, element2)) { System.out.println(element1 + " and " + element2 + " are already in the same group"); continue; } ut.merge(element1, element2); System.out.println("There are currently " + ut.getCount() + " groups left"); } } }
-
III. 优化:路径压缩
-
UF_Tree 中最坏情况下合并算法
merge
的时间复杂度为 \(O(n^2)\),其最主要的问顾在于最坏情况下,树的深度和数的大小一样,如果能够通过一些算法让合并时生成的树的深度尽可能的小,就可以优化查询算法find
-
如果把并查集中每棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度
-
代码实现
-
API
package com.tree; public class UF_Tree_Weighted { // 记录结点元素和该元素所在分组的标识 private int[] elementWithGroup; // 分组个数 private int count; // 用来存储每一个根结点对应的树中保存的结点的个数 private int[] layer; public UF_Tree_Weighted(int capacity) { this.count = capacity; this.elementWithGroup = new int[capacity]; this.layer = new int[capacity]; for (int i = 0; i < capacity; i++) { this.elementWithGroup[i] = i; } for (int i = 0; i < capacity; i++) { this.layer[i] = 1; } } public int getCount() { return this.count; } // 找到元素 element 所在分组的标识符 public int find(int element) { while (true) { if (element == this.elementWithGroup[element]) { return element; } element = this.elementWithGroup[element]; } } // 判断是否同一分组 public boolean connected(int element1, int element2) { return this.find(element1) == this.find(element2); } // 合并算法 public void merge(int element1, int element2) { // 找到两个元素各自所在组的树的根结点 int root1 = find(element1); int root2 = find(element2); // 如果同组则不需合并 if (root1 == root2) { return; } // 判断两个根结点各自对应的树的大小,将较小的树合并到较大的树中 if (this.layer[root1] < this.layer[root2]) { this.elementWithGroup[root1] = root2; this.layer[root2] += this.layer[root1]; } else { this.elementWithGroup[root2] = root1; this.layer[root1] += this.layer[root2]; } // 组的数量减一 this.count--; } }
-
测试
package com.tree; import java.util.Scanner; public class Test { public static void main(String[] args) { UF_Tree_Weighted utw = new UF_Tree_Weighted(5); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("Enter the 1st element that needs to be merged: "); int element1 = scanner.nextInt(); System.out.print("Enter the 2nd element that needs to be merged: "); int element2 = scanner.nextInt(); if (utw.connected(element1, element2)) { System.out.println(element1 + " and " + element2 + " are already in the same group"); continue; } utw.merge(element1, element2); System.out.println("There are currently " + utw.getCount() + " groups left"); } } }
-
c. 畅通工程问题
-
问题描述
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府"畅通工程"的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
-
输入说明
-
输入内容:
20 7 0 1 6 9 3 8 5 11 2 12 6 10 4 8
-
解释
第一个数字为“城市个数”
m
,城市编号从 0 到m-1
第二个数字为“已经修建好的道路数目”
n
接下来
n
行是道路连接的两座城市编号
-
-
解题思路
- 创建一个并查集
- 调用
merge
方法表示已经修建好的道路把对应的城市连接起来 - 如果城市全部连接起来,那么并查集中剩余的分组数目为 1,所有的城市都在一个树中,所以只需要获取当前并查集中剩余的数目并减去 1,就是问题的解
-
代码实现
package com.tree; import java.util.Scanner; public class Test { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int m = scanner.nextInt(); UF_Tree_Weighted utw = new UF_Tree_Weighted(m); int n = scanner.nextInt(); for (int i = 0; i < n; i++) { int a = scanner.nextInt(); int b = scanner.nextInt(); utw.merge(a, b); } System.out.println(utw.getCount() - 1); } }
0x04 图
(1)概述
-
图是由一组顶点和一组能够将两个顶点相连的边组成的
flowchart LR A-->B-->C-->A-->D B-->D-->C E-->F-->E -
特殊的图
-
自环
flowchart TB A---A -
平行边
flowchart LR A---B A---B A---B
-
-
图的分类
按照连接两顶点的边的不同,将图分为以下两类:
- 无向图:边仅连接两个顶点,没有其他含义(类比标量)
- 有向图:边不仅连接两个顶点,还具有方向(类比向量)
(2)无向图
a. 相关术语
- 相邻顶点:当两个顶点通过一条边相连时,我们称这两个顶点是相邻的,并且称这条边依附于这两个顶点
- 度:某个顶点的度就是依附于该顶点的边的个数
- 子图:一幅图的所有边的子集(包含这些边依附的顶点)组成的图
- 路径:由边顺序连接的一系列的顶点组成
- 环:一条至少含有一条边且终点和起点相同的路径
- 连通图:图中任意一个顶点都存在一条路径到达另外一个顶点
- 连通子图:一个非连通图中每一个连通的部分
b. 存储结构
要表示一幅图,只需要表示清楚以下两部分内容即可:
- 图中所有的顶点
- 所有连接顶点的边
I. 邻接矩阵(顺序)
- 使用一个 \(V×V\) 的二维数组
adj[v][V]
,将索引的值看做顶点编号 - 如果顶点 V 和顶点 W 相连,我们只需要将
adj[v][w]
和adj[w][v]
的值设置为 1,否则设置为 0
举例:
-
图:
flowchart LR A((0)) A---B((1)) & C((2)) B---C C---D((3))---D E((4))---F((5)) -
邻接矩阵:
V\W 0 1 2 3 4 5 0 0 1 1 0 0 0 1 1 0 1 0 0 0 2 1 1 0 1 0 0 3 0 0 1 1 0 0 4 0 0 0 0 0 1 5 0 0 0 0 1 0
II. 邻接表(链式)
- 使用一个大小为 \(V\) 的数组
adj[v]
,将索引的值看做顶点编号 - 每个索引处
adj[v]
存储了一个队列,该队列中存储的是所有与该顶点相邻的其他顶点
举例:
-
图:
flowchart LR A((0)) A---B((1)) & C((2)) B---C C---D((3))---D E((4))---F((5)) -
邻接表:
flowchart TB A((0))---B((1))---C((2))---D((3)) E((4))---F((5)) subgraph a a1[1]-->a2[2] end B & a A-->a subgraph b b1[0]-->b2[2] end B-->b subgraph c c1[0]-->c2[1]-->c3[3] end C-->c subgraph d d1[2]-->d2[3] end D-->d subgraph e e1[5] end E---e subgraph f f1[4] end F---f
c. 无向图的实现
-
API
package com.graph; import com.linear.queue.Queue; public class Graph { // 顶点数 private int V; // 边数 private int E; // 邻接表 private Queue<Integer>[] adj; public Graph(int V) { this.V = V; this.E = 0; this.adj = new Queue[V]; for (int i = 0; i < this.adj.length; i++) { this.adj[i] = new Queue<Integer>(); } } // 获取顶点数 public int getV() { return this.V; } // 获取边数 public int getE() { return this.E; } // 添加一条边 public void addEdge(int v, int w) { this.adj[v].enQueue(w); this.adj[w].enQueue(v); this.E++; } // 获取与顶点 v 相邻的所有顶点 public Queue<Integer> adj(int v) { return this.adj[v]; } }
-
测试
package com.graph; public class Test { public static void main(String[] args) { Graph graph = new Graph(6); System.out.println(graph.getV()); // 6 graph.addEdge(0, 1); graph.addEdge(1, 2); graph.addEdge(2, 3); graph.addEdge(0, 2); graph.addEdge(3, 3); graph.addEdge(4, 5); System.out.println(graph.getE()); // 6 for (Integer integer : graph.adj(2)) { System.out.print(integer + " "); } // 1 3 0 } }
d. 无向图的搜索
I. DFS:深度优先搜索
-
在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找子结点,然后找兄弟结点
-
代码实现
-
API
package com.graph; public class DepthFirstSearch { // 索引代表顶点,值代表当前顶点是否被搜索 private boolean[] marked; // 记录与当前顶点相通的顶点数 private int count; public DepthFirstSearch(Graph G, int point) { this.marked = new boolean[G.getV()]; this.count = 0; this.dfs(G, point); } // 使用 DFS 找出图 G 中 point 所有相邻的顶点 private void dfs(Graph G, int point) { this.marked[point] = true; for (Integer integer : G.adj(point)) { if (!this.marked[integer]) { this.dfs(G, integer); } } this.count++; } // 判断顶点 point 与当前顶点是否相通 public boolean marked(int point) { return this.marked[point]; } // 获取与当前顶点相通的顶点数 public int getCount() { return this.count; } }
-
测试
package com.graph; public class Test { public static void main(String[] args) { Graph graph = new Graph(6); graph.addEdge(0, 1); graph.addEdge(1, 2); graph.addEdge(2, 3); graph.addEdge(0, 2); graph.addEdge(3, 3); graph.addEdge(4, 5); DepthFirstSearch search = new DepthFirstSearch(graph, 0); System.out.println(search.getCount()); // 4 System.out.println(search.marked(2)); // true System.out.println(search.marked(4)); // false } }
-
II. BFS:广度优先搜索
-
在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找兄弟结点,然后找子结点
-
代码实现
-
API
package com.graph; import com.linear.queue.Queue; public class BreadthFirstSearch { // 索引代表顶点,值代表当前顶点是否被搜索 private boolean[] marked; // 记录与当前顶点相通的顶点数 private int count; // 存储待搜索邻接表的点 private Queue<Integer> waitSearch; public BreadthFirstSearch(Graph G, int point) { this.marked = new boolean[G.getV()]; this.count = 0; this.waitSearch = new Queue<Integer>(); this.bfs(G, point); } // 使用 BFS 找出图 G 中 point 所有相邻的顶点 private void bfs(Graph G, int point) { this.waitSearch.enQueue(point); while (!this.waitSearch.empty()) { Integer wait = this.waitSearch.deQueue(); if (this.marked[wait]) { continue; } this.marked[wait] = true; this.count++; for (Integer integer : G.adj(wait)) { this.waitSearch.enQueue(integer); } } } // 判断顶点 point 与当前顶点是否相通 public boolean marked(int point) { return this.marked[point]; } // 获取与当前顶点相通的顶点数 public int getCount() { return this.count; } }
-
测试
package com.graph; public class Test { public static void main(String[] args) { Graph graph = new Graph(6); graph.addEdge(0, 1); graph.addEdge(1, 2); graph.addEdge(2, 3); graph.addEdge(0, 2); graph.addEdge(3, 3); graph.addEdge(4, 5); BreadthFirstSearch search = new BreadthFirstSearch(graph, 0); System.out.println(search.getCount()); // 4 System.out.println(search.marked(2)); // true System.out.println(search.marked(4)); // false } }
-
e. 畅通工程问题 2
-
问题描述
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程"的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。目前的道路状况,9 号城市和 10 号城市是否相通?9 号城市和 8 号城市是否相通?
-
输入说明
-
输入内容:
20 7 0 1 6 9 3 8 5 11 2 12 6 10 4 8
-
解释
第一个数字为“城市个数”
m
,城市编号从 0 到m-1
第二个数字为“已经修建好的道路数目”
n
接下来
n
行是道路连接的两座城市编号
-
-
解题思路
- 创建一个图
Graph
对象,表示城市图 - 调用
addEdge
方法表示已经修建好的道路把对应的城市连接起来 - 通过
Graph
对象和顶点 9,构建DepthFirstSearch
对象或BreadthFirstSearch
对象 - 调用搜索对象的
marked
方法即可得到问题的解
- 创建一个图
-
代码实现
package com.graph; import java.util.Scanner; public class Test { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int m = scanner.nextInt(); int n = scanner.nextInt(); Graph graph = new Graph(m); for (int i = 0; i < n; i++) { int a = scanner.nextInt(); int b = scanner.nextInt(); graph.addEdge(a, b); } DepthFirstSearch dSearch = new DepthFirstSearch(graph, 9); System.out.println(dSearch.marked(10)); // true System.out.println(dSearch.marked(8)); // false BreadthFirstSearch bSearch = new BreadthFirstSearch(graph, 9); System.out.println(bSearch.marked(10)); // true System.out.println(bSearch.marked(8)); // false } }
f. 路径查找问题
-
问题描述
在实际生活中,地图是我们经常使用的一种工具,通常我们会用它进行导航,输入一个出发城市,输入一个目的地城市,就可以把路线规划好,而在规划好的这个路线上,会路过很多中间的城市。从 start 顶点到 target 顶点是否存在一条路径?如果存在,请找出这条路径。
-
输入说明
-
输入内容:
6 8 0 2 0 1 2 1 2 3 2 4 3 5 3 4 0 5
-
解释
第一个数字为“城市个数”
m
,城市编号从 0 到m-1
第二个数字为“两座城市存在的道路数目”
n
接下来
n
行是道路连接的两座城市编号
-
-
代码实现
-
API
package com.graph; import com.linear.stack.Stack; public class DepthFirstPaths { // 索引代表顶点,值代表当前顶点是否被搜索 private boolean[] marked; // 起始顶点 private int start; // 索引代表顶点,值代表从起始顶点到当前顶点的路径上最后一个顶点 private int[] edgeTo; public DepthFirstPaths(Graph G, int start) { this.marked = new boolean[G.getV()]; this.start = start; this.edgeTo = new int[G.getV()]; this.dfs(G, start); } // 使用深度优先搜索找出图 G 中 point 顶点的所有相邻顶点 private void dfs(Graph G, int point) { this.marked[point] = true; for (Integer integer : G.adj(point)) { if (!this.marked[integer]) { this.edgeTo[integer] = point; this.dfs(G, integer); } } } // 判断两顶点间是否存在路径 public boolean hasPathTo(int point) { return this.marked[point]; } // 找出从起始顶点到目标顶点的路径 public Stack<Integer> pathTo(int target) { if (!this.hasPathTo(target)) { return null; } Stack<Integer> path = new Stack<Integer>(); for (int i = target; i != start; i = this.edgeTo[i]) { path.push(i); } path.push(start); return path; } }
-
测试
package com.graph; import com.linear.stack.Stack; import java.util.Scanner; public class Test { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int m = scanner.nextInt(); int n = scanner.nextInt(); Graph graph = new Graph(m); for (int i = 0; i < n; i++) { int a = scanner.nextInt(); int b = scanner.nextInt(); graph.addEdge(a, b); } // 假定从城市 0 出发 DepthFirstPaths paths = new DepthFirstPaths(graph, 0); // 目标为城市 4 Stack<Integer> path = paths.pathTo(4); StringBuilder s = new StringBuilder(); for (Integer integer : path) { s.append(integer).append("→"); } s.deleteCharAt(s.length() - 1); System.out.println(s); } }
-
(3)有向图
- 有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点
- 一副有向图中两个顶点v和w可能存在以下四种关系:
- 没有边相连
- 存在从 v 到 w 的边 v->w
- 存在从 w 到 v 的边 w->v
- 既存在 w 到 v 的边,也存在 v 到 w 的边,即双向连接
a. 相关术语
- 出度:由某个顶点指出的边的个数称为该顶点的出度
- 入度:指向某个顶点的边的个数称为该顶点的入度
- 有向路径:由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点
- 有向环:一条至少含有一条边,且起点和终点相同的有向路径
b. 有向图的实现
-
API
package com.graph; import com.linear.queue.Queue; public class Digraph { private int V; private int E; private Queue<Integer>[] adj; public Digraph(int V) { this.V = V; this.E = 0; this.adj = new Queue[V]; for (int i = 0; i < adj.length; i++) { this.adj[i] = new Queue<Integer>(); } } public int getV() { return this.V; } public int getE() { return this.E; } public void addEdge(int v, int w) { this.adj[v].enQueue(w); this.E++; } public Queue<Integer> adj(int v) { return this.adj[v]; } // 返回该有向图的反向图 private Digraph reverse() { Digraph digraph = new Digraph(this.V); for (int v = 0; v < this.V; v++) { for (Integer w : this.adj[v]) { digraph.addEdge(w, v); } } return digraph; } }
-
测试
package com.graph; public class Test { public static void main(String[] args) { Digraph digraph = new Digraph(6); digraph.addEdge(0, 1); digraph.addEdge(1, 2); digraph.addEdge(2, 0); digraph.addEdge(0, 3); digraph.addEdge(1, 3); digraph.addEdge(3, 2); digraph.addEdge(4, 5); digraph.addEdge(5, 4); System.out.println(digraph.getV()); // 6 System.out.println(digraph.getE()); // 8 for (Integer w : digraph.adj(1)) { System.out.print(w + " "); } // 2 3 System.out.println(); for (Integer w : digraph.adj(4)) { System.out.print(w + " "); } // 5 } }
c. 拓扑排序
I. 概述
-
定义
给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素,此时就可以明确的表示出每个顶点的优先级
-
举例
-
原有向图
flowchart LR 0-->2 & 3-->4-->5 1-->3 -
排序后
flowchart TB subgraph a 1 end 1-->3 subgraph b 0-->3 end 0-->2 3-->4 subgraph c 2-->4-->5 end
-
II. 检测有向环
-
如果要使用拓扑排序解决优先级问题,首先得保证图中没有环的存在
-
有向环:
flowchart LR A-->B-->C-->A -
代码实现 API
package com.graph; public class DirectedCycle { // 索引代表顶点,值代表当前顶点是否被搜索过 private boolean[] marked; // 记录图中是否有环 private boolean hasCycle; // 索引代表顶点,记录当前顶点是否处于正在搜索的有向路径上 private boolean[] onStack; public DirectedCycle(Digraph G) { this.marked = new boolean[G.getV()]; this.hasCycle = false; this.onStack = new boolean[G.getV()]; for (int v = 0; v < G.getV(); v++) { if (!this.marked[v]) { this.dfs(G, v); } } } private void dfs(Digraph G, int v) { this.marked[v] = true; this.onStack[v] = true; for (Integer w : G.adj(v)) { if (!this.marked[w]) { this.dfs(G, w); } if (this.onStack[w]) { hasCycle = true; return; } } this.onStack[v] = false; } public boolean hasCycle() { return this.hasCycle; } }
III. 顶点排序
-
顶点排序基于深度优先实现
-
深度优先搜索在一个连通子图上,每个顶点只会被搜索一次,在此基础上,将搜索的顶点放入到线性序列的数据结构中即可完成排序
-
代码实现 API
package com.graph; import com.linear.stack.Stack; public class DepthFirstOrder { private boolean[] marked; private Stack<Integer> reversePost; public DepthFirstOrder(Digraph G) { this.marked = new boolean[G.getV()]; this.reversePost = new Stack<Integer>(); for (int v = 0; v < G.getV(); v++) { if (!this.marked[v]) { this.dfs(G, v); } } } private void dfs(Digraph G, int v) { this.marked[v] = true; for (Integer w : G.adj(v)) { if (!this.marked[w]) { this.dfs(G, w); } } reversePost.push(v); } public Stack<Integer> reversePost() { return this.reversePost; } }
IV. 实现
-
API
package com.graph; import com.linear.stack.Stack; public class TopoLogical { // 顶点的拓扑排序 private Stack<Integer> order; public TopoLogical(Digraph G) { DirectedCycle cycle = new DirectedCycle(G); if (!cycle.hasCycle()) { DepthFirstOrder DFO = new DepthFirstOrder(G); this.order = DFO.reversePost(); } } // 判断环 public boolean isCycle() { return this.order == null; } // 获取排序结果 public Stack<Integer> getOrder() { return this.order; } }
-
测试
package com.graph; public class Test { public static void main(String[] args) { Digraph digraph = new Digraph(6); digraph.addEdge(0, 2); digraph.addEdge(0, 3); digraph.addEdge(1, 3); digraph.addEdge(2, 4); digraph.addEdge(3, 4); digraph.addEdge(4, 5); TopoLogical topoLogical = new TopoLogical(digraph); StringBuilder path = new StringBuilder(); for (Integer integer : topoLogical.getOrder()) { path.append(integer).append("→"); } path.deleteCharAt(path.length() - 1); System.out.println(path); // 1→0→3→2→4→5 } }
(4)最小生成树
a. 加权无向图
-
加权无向图是一种为每条边关联一个权重值或是成本的图模型
graph LR A--2---B--1---C--1---D--3---B -
代码实现
-
边的 API
package com.graph; public class Edge implements Comparable<Edge> { // 两个顶点 private int v, w; // 边的权重值 private double weight; public Edge(int v, int w, double weight) { this.v = v; this.w = w; this.weight = weight; } // 获取权重值 public double getWeight() { return this.weight; } // 获取边上的一个顶点 public int either() { return this.v; } // 获取边的顶点中 point 的另外一个顶点 public int other(int v) { return (v == this.v) ? this.w : this.v; } @Override public int compareTo(Edge other) { return Double.compare(this.getWeight(), other.getWeight()); } }
-
图的 API
package com.graph; import com.linear.queue.Queue; public class WeightedGraph { private int V; private int E; private Queue<Edge>[] adj; public WeightedGraph(int V) { this.V = V; this.E = 0; this.adj = new Queue[V]; for (int i = 0; i < V; i++) { this.adj[i] = new Queue<Edge>(); } } public int getV() { return this.V; } public int getE() { return this.E; } public void addEdge(Edge edge) { int v = edge.either(); int w = edge.other(v); this.adj[v].enQueue(edge); this.adj[w].enQueue(edge); this.E++; } public Queue<Edge> adj(int v) { return this.adj[v]; } public Queue<Edge> edges() { Queue<Edge> allEdges = new Queue<>(); for (int v = 0; v < this.V; v++) { for (Edge edge : this.adj[v]) { if (edge.other(v) < v) { allEdges.enQueue(edge); } } } return allEdges; } }
-
b. 最小生成树概述
-
定义
图的生成树是它的一棵含有其所有顶点的无环连通子图,一幅加权无向图的最小生成树就是它的一棵权值(树中所有边的权重之和)最小的生成树
-
举例(虚线表示构成最小生成树)
graph LR 4-.0.35.-5 4--0.37---7 5-.0.28.-7 0-.0.16.-7 1--0.32---5 0--0.38---4 2-.0.17.-3 1-.0.19.-7 0-.0.26.-2 1--0.36---2 1--0.29---3 2--0.34---7 6-.0.40.-2 3--0.52---6 6--0.58---0 6--0.93---4 -
约定
只考虑连通图
最小生成树的定义说明它只能存在于连通图中,如果图不是连通的,那么分别计算每个连通图子图的最小生成树,合并到一起称为最小生成森林
-
性质
- 用一条边连接树中的任意两个顶点都会产生一个新的环
- 从树中删除任意一条边就会得到两棵独立的树
-
切分定理
要从一幅连通图中找出该图的最小生成树,需要通过切分定理完成
-
切分
将图的所有顶点按照某些规则分为两个非空且没有交集的集合
-
横切分
连接两个属于不同集合的顶点的边
-
切分定理
在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图中的最小生成树
但是,一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图的最小生成树的边
-
c. 贪心算法
- 贪心算法是计算图的最小生成树的基础算法,它的基本原理就是切分定理
- 使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边
- 如果图有 \(V\) 个顶点,那么需要找到 \(V-1\) 条边,就可以表示该图的最小生成树
- 以下算法都可以看做是贪心算法的一种特殊情况,不同之处在于保存切分和判定权重最小的横切边的方式
I. Prim 算法
-
一开始这棵树只有一个顶点,接下来每一步都会为一棵生成中的树添加一条边,最终会添加 \(V-1\) 条边,每次总是将下一条连接树中的顶点与不在树中的顶点目权重最小的边加入到树中
-
切分规则
把最小生成树中的顶点看做是一个集合,把不在最小生成树中的顶点看做是另外一个集合
-
输入说明
-
输入内容
8 16 4 5 0.35 4 7 0.37 5 7 0.28 0 7 0.16 1 5 0.32 0 4 0.38 2 3 0.17 1 7 0.19 0 2 0.26 1 2 0.36 1 3 0.29 2 7 0.34 6 2 0.40 3 6 0.52 6 0 0.58 6 4 0.93
-
解释
第一个数字为顶点个数
m
,编号从 0 到m-1
第二个数字为边的个数
n
接下来
n
行依次输入两个顶点以及连接这两顶点的边的权重
-
-
代码实现
-
API
package com.graph; import com.linear.queue.Queue; import com.tree.IndexMinPriorityQueue; public class PrimMST { // 索引代表顶点,值代表当前顶点和最小生成树之间的最短边 private Edge[] edgeTo; // 索引代表顶点,值代表当前顶点和最小生成树之间的最短边的权重 private double[] distTo; // 索引代表顶点,值代表当前顶点是否已经在最小生成树中 private boolean[] marked; // 存放树中顶点与非树中顶点之间的有效横切边 private IndexMinPriorityQueue<Double> pq; public PrimMST(WeightedGraph G) { this.edgeTo = new Edge[G.getV()]; this.distTo = new double[G.getV()]; for (int i = 0; i < distTo.length; i++) { this.distTo[i] = Double.POSITIVE_INFINITY; } this.marked = new boolean[G.getV()]; this.pq = new IndexMinPriorityQueue<Double>(G.getV()); this.distTo[0] = 0.0; this.pq.insert(0, 0.0); while (!this.pq.empty()) { this.visit(G, this.pq.remove()); } } // 将顶点 v 添加到最小生成树中 private void visit(WeightedGraph G, int v) { this.marked[v] = true; for (Edge edge : G.adj(v)) { int w = edge.other(v); if (this.marked[w]) { continue; } if (edge.getWeight() < this.distTo[w]) { this.edgeTo[w] = edge; this.distTo[w] = edge.getWeight(); if (this.pq.exist(w)) { this.pq.change(w, edge.getWeight()); } else { this.pq.insert(w, edge.getWeight()); } } } } // 获取最小生成树的所有边 public Queue<Edge> edges() { Queue<Edge> allEdges = new Queue<>(); for (int i = 0; i < this.edgeTo.length; i++) { if (this.edgeTo[i] != null) { allEdges.enQueue(this.edgeTo[i]); } } return allEdges; } }
-
测试
package com.graph; import com.linear.queue.Queue; import java.util.Scanner; public class Test { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int m = scanner.nextInt(); WeightedGraph WG = new WeightedGraph(m); int n = scanner.nextInt(); for (int i = 0; i < n; i++) { int v = scanner.nextInt(); int w = scanner.nextInt(); double weight = scanner.nextDouble(); Edge e = new Edge(v, w, weight); WG.addEdge(e); } PrimMST primMST = new PrimMST(WG); Queue<Edge> edges = primMST.edges(); for (Edge edge : edges) { int v = edge.either(); int w = edge.other(v); double weight = edge.getWeight(); System.out.println(v + " - " + w + " :: " + weight); } /* * 1 - 7 :: 0.19 * 0 - 2 :: 0.26 * 2 - 3 :: 0.17 * 4 - 5 :: 0.35 * 5 - 7 :: 0.28 * 6 - 2 :: 0.4 * 0 - 7 :: 0.16 */ } }
-
II. kruskal 算法
-
按照边的权重(从小到大)处理,将边加入最小生成树中,加入的边不会与已经加入最小生成树的边构成环,直到树中含有 \(V-1\) 条边为止
-
输入说明
-
输入内容
8 16 4 5 0.35 4 7 0.37 5 7 0.28 0 7 0.16 1 5 0.32 0 4 0.38 2 3 0.17 1 7 0.19 0 2 0.26 1 2 0.36 1 3 0.29 2 7 0.34 6 2 0.40 3 6 0.52 6 0 0.58 6 4 0.93
-
解释
第一个数字为顶点个数
m
,编号从 0 到m-1
第二个数字为边的个数
n
接下来
n
行依次输入两个顶点以及连接这两顶点的边的权重
-
-
代码实现
-
API
package com.graph; import com.linear.queue.Queue; import com.tree.MinPriorityQueue; import com.tree.UF_Tree_Weighted; public class KruskalMST { // 保存最小生成树的所有边 private Queue<Edge> edges; // 索引表示顶点 private UF_Tree_Weighted utw; // 存储图中所有的边,使用最小优先队列按权重排序 private MinPriorityQueue<Edge> pq; public KruskalMST(WeightedGraph G) { this.edges = new Queue<Edge>(); this.utw = new UF_Tree_Weighted(G.getV()); this.pq = new MinPriorityQueue<>(G.getE() + 1); for (Edge edge : G.edges()) { this.pq.insert(edge); } while (!this.pq.empty() && this.edges.length() < G.getV() - 1) { Edge edge = this.pq.remove(); int v = edge.either(); int w = edge.other(v); if (this.utw.connected(v, w)) { continue; } this.utw.merge(v, w); this.edges.enQueue(edge); } } public Queue<Edge> edges() { return this.edges; } }
-
测试
package com.graph; import com.linear.queue.Queue; import java.util.Scanner; public class Test { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int m = scanner.nextInt(); WeightedGraph WG = new WeightedGraph(m); int n = scanner.nextInt(); for (int i = 0; i < n; i++) { int v = scanner.nextInt(); int w = scanner.nextInt(); double weight = scanner.nextDouble(); Edge e = new Edge(v, w, weight); WG.addEdge(e); } KruskalMST kruskalMST = new KruskalMST(WG); Queue<Edge> edges = kruskalMST.edges(); for (Edge edge : edges) { int v = edge.either(); int w = edge.other(v); double weight = edge.getWeight(); System.out.println(v + " - " + w + " :: " + weight); } /* * 0 - 7 :: 0.16 * 2 - 3 :: 0.17 * 1 - 7 :: 0.19 * 0 - 2 :: 0.26 * 5 - 7 :: 0.28 * 4 - 5 :: 0.35 * 6 - 2 :: 0.4 */ } }
-
(5)最短路径
a. 加权有向图
-
代码实现
-
有向边的 API
package com.graph; public class DirectedEdge { private int v, w; private double weight; public DirectedEdge(int v, int w, double weight) { this.v = v; this.w = w; this.weight = weight; } public double getWeight() { return this.weight; } public int from() { return this.v; } public int to() { return this.w; } }
-
图的 API
package com.graph; import com.linear.queue.Queue; public class WeightedDigraph { private int V; private int E; private Queue<DirectedEdge>[] adj; public WeightedDigraph(int V) { this.V = V; this.E = 0; this.adj = new Queue[V]; for (int i = 0; i < this.adj.length; i++) { this.adj[i] = new Queue<>(); } } public int getV() { return this.V; } public int getE() { return this.E; } public void addEdge(DirectedEdge edge) { this.adj[edge.from()].enQueue(edge); this.E++; } public Queue<DirectedEdge> adj(int v) { return this.adj[v]; } public Queue<DirectedEdge> edges() { Queue<DirectedEdge> allEdges = new Queue<>(); for (int v = 0; v < this.V; v++) { for (DirectedEdge edge : this.adj[v]) { allEdges.enQueue(edge); } } return allEdges; } }
-
b. 最短路径概述
-
定义
在一幅加权有向图中,两顶点的最短路径是所有两顶点之间的路径中总权重最小的那条路径
-
举例:从 0 到 6 的最短路径,路径使用虚线表示
graph LR 4--0.35-->5 5--0.35-->4 4--0.37-->7 5--0.28-->7 7--0.28-->5 5--0.32-->1 0--0.38-->4 0-.-0.26-.->2 7-.-0.39-.->3 1--0.29-->3 2-.-0.34-.->7 6--0.40-->2 3-.-0.52-.->6 6--0.58-->0 6--0.93-->4 -
性质
- 路径具有方向性
- 权重不一定等价于距离(权重可以是距离、时间、花费等内容,权重最小指的是成本最低)
- 只考虑连通图
- 最短路径不一定是唯一的
-
最短路径树
- 给定一幅加权有向图和一个顶点,以该顶点为起点的一棵最短路径树是图的一幅子图,它包含该顶点以及从该顶点可达的所有顶点
- 这棵有向树的根结点为该顶点,树的每条路径都是有向图中的一条最短路径
c. 松弛技术
I. 松弛技术概述
-
“松弛” 说明
一条橡皮筋沿着两个顶点的某条路径紧紧展开,如果这两个顶点之间的路径不止一条,还有存在更短的路径,那么把皮筋转移到更短的路径上,皮筋就可以松弛了
-
边的松弛
松弛边
v->w
:检查从s
到w
的最短路径是否先从s
到v
,然后再从v
到w
- 如果是,则这条边需要加入到最短路径树中
- 如果不是,则忽略这条边。
-
顶点的松弛
顶点的松弛是基于边的松弛完成的,只需要把某个顶点指出的所有边松弛,那么该顶点就松弛完毕了
II. Dijstra 算法
Dijstra 算法使用了松弛技术
-
输入说明
-
输入内容
8 15 4 5 0.35 5 4 0.35 4 7 0.37 5 7 0.28 7 5 0.28 5 1 0.32 0 4 0.38 0 2 0.26 7 3 0.39 1 3 0.29 2 7 0.34 6 2 0.40 3 6 0.52 6 0 0.58 6 4 0.93
-
解释
第一个数字为顶点个数
m
,编号从 0 到m-1
第二个数字为边的个数
n
接下来
n
行依次输入起始顶点和指向顶点以及连接这两顶点的边的权重
-
-
代码实现
-
API
package com.graph; import com.linear.queue.Queue; import com.tree.IndexMinPriorityQueue; public class DijstraSP { // 索引代表顶点,值代表从 start 顶点到当前顶点的最短路径上的最后一条边 private DirectedEdge[] edgeTo; // 索引代表顶点,值代表从 start 顶点到当前顶点的最短路径的总权重 private double[] distTo; // 存放树中顶点与非树中顶点之间的有效横切边 private IndexMinPriorityQueue<Double> pq; public DijstraSP(WeightedDigraph G, int start) { this.edgeTo = new DirectedEdge[G.getV()]; this.distTo = new double[G.getV()]; for (int i = 0; i < this.distTo.length; i++) { this.distTo[i] = Double.POSITIVE_INFINITY; } this.pq = new IndexMinPriorityQueue<>(G.getV()); this.distTo[start] = 0.0; this.pq.insert(start, 0.0); while (!this.pq.empty()) { this.relax(G, this.pq.remove()); } } // 松弛指定顶点 private void relax(WeightedDigraph G, int v) { for (DirectedEdge edge : G.adj(v)) { int w = edge.to(); if (this.distTo(v) + edge.getWeight() < this.distTo(w)) { this.distTo[w] = this.distTo[v] + edge.getWeight(); this.edgeTo[w] = edge; if (this.pq.exist(w)) { this.pq.change(w, this.distTo(w)); } else { this.pq.insert(w, this.distTo(w)); } } } } // 获取总权重 public double distTo(int v) { return this.distTo[v]; } // 判断两顶点之间是否可达 public boolean hasPathTo(int v) { return this.distTo[v] < Double.POSITIVE_INFINITY; } // 查询最短路径中的所有边 public Queue<DirectedEdge> pathTo(int v) { if (!this.hasPathTo(v)) { return null; } Queue<DirectedEdge> allEdges = new Queue<>(); while (true) { DirectedEdge edge = this.edgeTo[v]; if (edge == null) { break; } allEdges.enQueue(edge); v = edge.from(); } return allEdges; } }
-
测试
package com.graph; import com.linear.queue.Queue; import java.util.Scanner; public class Test { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int m = scanner.nextInt(); WeightedDigraph WD = new WeightedDigraph(m); int n = scanner.nextInt(); for (int i = 0; i < n; i++) { int v = scanner.nextInt(); int w = scanner.nextInt(); double weight = scanner.nextDouble(); DirectedEdge e = new DirectedEdge(v, w, weight); WD.addEdge(e); } DijstraSP dijstraSP = new DijstraSP(WD, 0); Queue<DirectedEdge> edges = dijstraSP.pathTo(6); for (DirectedEdge edge : edges) { System.out.println(edge.from() + "→" + edge.to() + " :: " + edge.getWeight()); } /* * 3→6 :: 0.52 * 7→3 :: 0.39 * 2→7 :: 0.34 * 0→2 :: 0.26 */ } }
-
-End-