# 一、Java数据结构和算法
## 1、数据结构和算法内容介绍
### 1.1、几个经典的算法面试题
* 1、字符串匹配问题:
* 有一个字符串 str1= ""硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好"",和一个子串str2="尚硅谷你尚硅你"
* 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
* 要求用最快的速度来完成匹配
* 你的思路是什么?
* 若是没学过算法,第一思路就是暴力匹配(同样可以完成需求,但效率很低)
* 若是我会算法,这个题目我就会使用KMP算法(我会建立一个部分匹配表,通过匹配表的搜索词进行匹配,提高效率)
* 2、汉诺塔游戏
* 请完成汉诺塔游戏的代码: 要求:1) 将 A 塔的所有圆盘移动到 C 塔。并且规定,在2) 小圆盘上不能放大圆盘,3)在三根柱子之间一次只能移动一个圆盘
![image-20221018210612097](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018210612097.png)
* 3、八皇后问题
* 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848 年提出:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。【92】=> 分治算法
![image-20221018213743371](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018213743371.png)
* 马踏棋盘算法介绍和游戏演示
* 马踏棋盘算法也被称为骑士周游问题
* 将马随机放在国际象棋的 8×8 棋盘 Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部 64 个方格
* 游戏演示: http://www.4399.com/flash/146267_2.htm
* 会使用到图的深度优化遍历算法(DFS) + 贪心算法优化
![image-20221018213830010](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018213830010.png)
### 1.2、数据结构和算法的重要性
* 1) **算法是程序的灵魂**,优秀的程序可以在海量数据计算时,依然保持高速计算
* 2) 一般来讲 程序会使用了内存计算框架(比如 Spark)和缓存技术(比如 Redis 等)来优化程序,再深入的思考一下,这些计算框架和缓存技术, 它的核心功能是哪个部分呢?
* 3) 拿实际工作经历来说, 在 Unix 下开发服务器程序,功能是要支持上千万人同时在线,在上线前,做内测,一切OK,可上线后,服务器就支撑不住了, 公司的 CTO 对代码进行优化,再次上线,坚如磐石。你就能感受到程序是有灵魂的,就是算法。
* 4) 目前程序员面试的门槛越来越高,**很多一线 IT 公司(大厂)**,都会有**数据结构和算法面试题**(负责的告诉你,肯定有的)
* 5) 如果你不想永远都是代码工人,那就花时间来研究下数据结构和算法
# 二、数据结构和算法概述
### 2.1.1、数据结构和算法的关系
* 1) 数据 data 结构(structure)是一门**研究组织数据方式**的学科,有了编程语言也就有了数据结构.学好数据结构可以编写出更加漂亮,更加有效率的代码。
* 2) 要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决.
* **3) 程序 = 数据结构 + 算法**
* 4) **数据结构是算法的基础**, 换言之,想要学好算法,需要把数据结构学到位。
## 2.2、看几个实际编程中遇到的问题
### 2.2.1、问题一:字符串替换问题
* Java代码:
```JAVA
public static void main(String[] args){
String str = "Java,Java,hello,world!"
String new str = str.replaceAll("Java","胡桃~");
System.out.println("new Str = " + new Str);
}
```
* 问:试写出用单链表表示的字符串类及字符串节点类的定义,并依次实现它的构造函数、以及计算串长度、串赋值、判断两串相等、求字串、两串连接、求字串在串中位置等7个成员函数。
* 小结:需要使用到单链表数据结构
### 2.2.2、一个五子棋程序
![image-20221018222948328](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018222948328.png)
* 如何判断游戏的输赢,并可以完成存盘退出和继续上局的功能
* 1) 棋盘 **二维数组**=>(**稀疏数组**)-> 写入文件 【存档功能】
* 2) 读取文件-> 稀疏数组-> 二维数组 -> 棋盘 【接上局】
### 2.2.3、约瑟夫(Josephu)问题(丢手帕问题)
* 1) Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
* 2) 提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表(**单向环形链表**),然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。
* 3) 小结:完成约瑟夫问题,需要使用到单向环形链表 这个数据结构
![image-20221018223310899](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018223310899.png)
### 2.2.4、其它常见算法问题:
![image-20221018223514746](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018223514746.png)
* 1) 修路问题 => 最小生成**树(加权值)**【**数据结构**】+ 普利姆算法
* 2) 最短路径问题 => 图+弗洛伊德算法
* 3) 汉诺塔 => 分支算法
* 4) 八皇后问题 => 回溯法
## 2.3 、线性结构和非线性结构
* 数据结构包括:**线性结构**和**非线性结构**
### 2.3.1、线性结构
* 1) 线性结构作为**最常用的数据结构**,其特点是**数据元素之间存在一对一的线性关系**
* 2) 线性结构有两种不同的存储结构,即**顺序存储结构**(数组)**和链式存储结构**(链表)。顺序存储的线性表称为顺序表,顺序表中的**存储元素是连续的**
* 3) 链式存储的线性表称为链表,链表中的**存储元素不一定是连续的**,元素节点中存放数据元素以及相邻元素的地址信息
* 4) 线性结构常见的有:**数组、队列、链表和栈**,后面我们会详细讲解
### 2.3.2、非线性结构
* 非线性结构包括:二维数组,多维数组,广义表,**树结构**,**图结构**
# 三、稀疏数组和队列
## 3.1、稀疏 sparsearray 数组
### 3.1.1、**先看一个实际的需求**
* 编写的五子棋程序中,有**存盘退出**和**续上盘**的功能。
![image-20221018225256135](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018225256135.png)
* 分析问题:
* 因为该二维数组的很多值是默认值 0, 因此记录了**很多没有意义的数据.->稀疏数组**
### 3.1.2、基本介绍
* 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
* 稀疏数组的处理方法是:
* 1) 记录数组**一共有几行几列,有多少个不同**的值
* 2) 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而**缩小程序**的规模
* 稀疏数组举例说明:
![image-20221018230534155](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018230534155.png)
### 3.1.3、应用实例
* 1) 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
* 2) 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
![image-20221018232110250](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018232110250.png)
* 3) 整体思路分析
```BASIC
二维数组转稀疏数组的思路
1. 遍历 原始的二维数组,得到有效数据的个数 sum
2. 根据sum 就可以创建 稀疏数组 sparseArr int[sum + 1] [3]
3. 将二维数组的有效数据存入到稀疏数组
稀疏数组转原始的二维数组的思路
1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int [11][11]
2. 在读取稀疏数组后几行的数据,并赋给 原始的二维数组 即可.
```
* 4) 代码实现,按照思路首先先把原始的二维数组进行遍历
```JAVA
public class SparseArray {
public static void main(String[] args) {
//首先创建一个原始的五子棋棋盘(即二维数组) 大小为 11 * 11,0表示没有棋子,1表示黑子,2表示篮子
int[][] chessArr1 = new int[11][11];
//给黑子赋值,即代表黑子的位置是在第二行的第三列,数组默认下标是从0开始的
chessArr1[1][2] = 1;
//给篮子赋值,即代表篮子的位置是在第三行的第四列,数组默认下标是从0开始的
chessArr1[2][3] = 2;
//赋值完毕,使用for循环输出原始的二维数组,即遍历到一维数组,数据到每一行,即原始的二维数组
for(int[] row : chessArr1){
//再次循环具体的数据
for(int data: row){
//格式化输出显得更美观
System.out.printf("%d\t",data);
}
//每打印一行换一行
System.out.println();
}
}
}
```
* 输出结果如图所示:
![image-20221018234846966](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221018234846966.png)
* 遍历二维数组过后就可以使用变量int sum去记录并得到有效数据的个数
```JAVA
//遍历二维数组过后得到非0数据的个数,并使用int sum记录非0的值(默认为0)
int sum = 0;
//使用for循环遍历chessArr1这个数组,意为此数组原先有多少数据就遍历多少数据
for (int i = 0; i <11; i++) {
//再遍历列数
for (int j = 0; j < 11; j++) {
//再做一个判断,若二维数组chessArr1中的i和j不为0,就让sum自增(即记录非0的数据个数)
if (chessArr1[i][j]!=0){
sum++;
}
}
}
//验证数组中非0的数据个数
System.out.println("sum =" + sum);
}
```
* 输出结果如图所示:也可以得出结论:非0的有效数据个数为2
![image-20221019000424253](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019000424253.png)
* 接下来创建对应的稀疏数组int sparseArr初始化稀疏数组后遍历并输出
```JAVA
/**
* 接下来创建对应的稀疏数组int sparseArr [][],由于sum是存储具体数值,
* 所以sum+1,压缩成稀疏数组后具体的列是4,所以行为4-1
*/
int [][] SparseArr = new int[sum+1][3];
//根据原始数组几行几列以及有多少分离数据,给稀疏数组赋值
//第一行的第一列有11个数据
SparseArr[0][0] = 11;
//第一行的第二列也有11个数据
SparseArr[0][1] = 11;
//第一行的有效数据sum个,至此稀疏数组初始化完毕
SparseArr[0][2] = sum;
int count = 0; //使用count用于记录稀疏数组中的非0数据
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if (chessArr1[i][j]!=0){
count++;
SparseArr[count][0] = i; //第一列是i,由于count会递增,所以用于记录非0数据
SparseArr[count][1] = j; //第二列是j
SparseArr[count][2] = chessArr1[i][j]; //第三列是数组本身
}
}
}
//赋值完毕后输出稀疏数组的形式
System.out.println("得到的稀疏数组为如下形式~~");
//遍历输出稀疏数组SparseAr
for (int i = 0; i < SparseArr.length; i++) {
//格式化输出稀疏数组%d 代表第一行 \t代表第一列,依次递增,稀疏数组所在行的1-3列输出即可,\n代表换行
System.out.printf("%d\t%d\t%d\t\n",SparseArr[i][0],SparseArr[i][1],SparseArr[i][2]);
}
//换行
System.out.println();
}
```
* 输出结果如图所示:
![image-20221019093947763](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019093947763.png)
* 图解:第一行记录了原始数组有几行几列以及数组中的非0数据个数
* 第二行和第三行分别记录非零数据的行、列、值,前面以及提及,1代表黑子,2代表篮子
* 再给一个黑子赋值,即代表黑子的位置是再第五行的第六列
```JAVA
chessArr1[4][5] = 1;
```
* 而后稀疏数组的输出结果会随着有效数字的变化而变化
![image-20221019100142743](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019100142743.png)
* 而后再将稀疏数恢复成原始的二维数组,这是没有经过任何处理的稀疏数组转原始二维数组
```java
/**
* 稀疏数组转原始的二维数组的思路
* 1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int [11][11]
* 2. 在读取稀疏数组后几行的数据,并赋给 原始的二维数组 即可.
* 稀疏数组的第一行第一列就是二维数组chessArr2的行,稀疏数组的第一行第二列就是二维数组chessArr2的的列
*/
int[][] chessArr2 = new int[SparseArr[0][0]][SparseArr[0][1]];
//现在我们没有经过任何处理,直接输出看看效果
System.out.println("没经处理恢复后的二维数组");
for (int[] ints : chessArr2) {
for (int data : ints){
System.out.printf("%d\t",data);
}
//给数组加上换行
System.out.println();
}
}
```
* 输出结果如图:
![image-20221019103320379](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019103320379.png)
* 得出结论,稀疏数组恢复为原始的二维数组之前需要先进行读取,所以进行操作读取并恢复二维数组
```JAVA
//在读取稀疏数组后几行的数据(即从第二行开始遍历),并赋给原始的二维数组 即可.
for(int i = 1; i < SparseArr.length; i++) {
/**
* 由于稀疏数组内都是非0数据,所以不需要再进行非0判断,直接赋值即可,恢复二维数组的行和列可直接从稀疏数组中获取
* chessArr2是恢复后的原始二维数组,二维数组的行和列就直接从稀疏数组里取值即可,即i里的第一、第二、第三列,分别为 行、列、值
*/
chessArr2[SparseArr[i][0]][SparseArr[i][1]] = SparseArr[i][2];
}
//输出恢复过后的二维数组
System.out.println("恢复过后的二维数组");
for (int[] ints : chessArr2) {
for(int data : ints){
System.out.printf("%d\t",data);
}
//换行
System.out.println();
}
}
```
* 恢复后的二维数组如下:
![image-20221019120253956](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019120253956.png)
## 3.2 、队列
### 3.2.1队列的一个使用场景
* 银行排队的案例:
![image-20221019151115762](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019151115762.png)
### 3.2.2、队列介绍
1) 队列是一个有序列表,可以用**数组**或是**链表**来实现。
2) 遵循**先入先出**的原则。即:**先存入队列的数据,要先取出。后存入的要后取出**
3) 示意图:(使用数组模拟队列示意图)
![image-20221019151633478](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019151633478.png)
### **3.2.3、数组模拟队列思路**
* 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中maxSize 是该队列的最大容量。
* 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear 则是随着数据输入而改变,如图所示:
![image-20221019151823720](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019151823720.png)
* 当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:**思路分析**
* 1) 将尾指针往后移:rear+1 , 当 front == rear 【空】
* 2) 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。rear == maxSize - 1[队列满]
* 以下是代码实现
```JAVA
/**
* description
* 使用数组模拟队列--->编写一个ArrayQueue类
* @author
* @since 2022/10/19 15:30
*/
public class ArrayQueued {
private int maxSize; //表示数组的最大容量
private int front; //队列头
private int rear; //队列尾
private int[] arr; //该数组用于存放数据,模拟队列
public ArrayQueued(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize];
front = -1; //指向队列头部,分析出front是指向队列头的前一个位置
rear = -1; //指向队列尾部,分析出rear是指向队列尾部的具体数据(即队列最后一个数据)
}
//判断队列是否满
public boolean isFull() {
return rear == maxSize - 1;
}
//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}
//添加数据到队列
public void addQueue(int n) {
//判断队列是否满
if (isFull()) {
System.out.println("队列已满,请耐心等待");
}
rear++; //让rear后移
arr[rear] = n;
}
//获取队列数据
public int getQueue() {
//判断队列是否为空
if (isEmpty()) {
//通过抛出异常来处理
throw new RuntimeException("队列为空");
}
front++; //取数据前让front后移
return arr[front];
}
//显示队列的所有数据
public void showQueue() {
//遍历之前判断数组是否为空
if (isEmpty()) {
System.out.println("队列为空,无法获取");
} else {
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}
}
//显示队列的头数据,注意:这里不是取出数据
public int headQueue() {
//显示之前判断队列是否为空
if (isEmpty()) {
throw new RuntimeException("队列为空,无法显示");
}
return arr[front + 1];
}
}
```
* 测试队列的代码
```JAVA
/**
* description
* 使用数组模拟队列--->编写一个ArrayQueue类
* @author
* @since 2022/10/19 15:30
*/
public class ArrayQueued {
private int maxSize; //表示数组的最大容量
private int front; //队列头
private int rear; //队列尾
private int[] arr; //该数组用于存放数据,模拟队列
public ArrayQueued() {
}
public ArrayQueued(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize];
front = -1; //指向队列头部,分析出front是指向队列头的前一个位置
rear = -1; //指向队列尾部,分析出rear是指向队列尾部的具体数据(即队列最后一个数据)
}
//判断队列是否满
public boolean isFull() {
return rear == maxSize - 1;
}
//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}
//添加数据到队列
public void addQueue(int n) {
//判断队列是否满
if (isFull()) {
System.out.println("队列已满,请耐心等待");
}
//让rear后移
rear++;
arr[rear] = n;
}
//获取队列数据
public int getQueue() {
//判断队列是否为空
if (isEmpty()) {
//通过抛出异常来处理
throw new RuntimeException("队列为空");
}
front++; //取数据前让front后移
return arr[front];
}
//显示队列的所有数据
public void showQueue() {
//遍历之前判断数组是否为空
if (isEmpty()) {
System.out.println("队列为空,无法获取");
} else {
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}
}
//显示队列的头数据,注意:这里不是取出数据
public int headQueue() {
//显示之前判断队列是否为空
if (isEmpty()) {
throw new RuntimeException("队列为空,无法显示");
}
return arr[front + 1];
}
}
```
* 运行过程中发现问题:
* 即当添加三个数据后并依次取出后队列已经为空的情况却无法再次使用
![image-20221019164915334](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019164915334.png)
* 如图,当前队列为空,但是再次添加数据时控制台会显示队列已满并抛出异常
![image-20221019165109530](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019165109530.png)
* 由此得出结论:队列中的数组在使用过之后就不能再使用了,所以这一块就可以进行优化
* 问题分析并优化
* 目前数组使用一次就不能用, 没有达到复用的效果
* 解决办法:将这个数组使用算法,改进成一个**环形的队列** 取模:%
### 3.2.4、数组模拟环形队列
* 对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过**取模的方式来实现**即可)
* 分析说明:
* 1) 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的时候需要注意 (rear + 1) % maxSize == front 满]
* 2) rear == front [空]
* 3) 分析示意图:
![image-20221019204759241](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019204759241.png)
* 思路如下:
* front 变量的含义做一个调整: front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素,front 的初始值 = 0
* rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定.
* rear 的初始值 = 0
* 当队列满时,条件是 (rear + 1) % maxSize == front 【满】
* 对队列为空的条件, rear == front 空
* 当我们这样分析, 队列中有效的数据的个数 **(rear + maxSize - front) % maxSize** / 我们就可以在原来的队列上修改得到,一个环形队列
* 思路清晰,代码实现如下,首先把模拟队列的类写好
```JAVA
/**
* description
* 环形数组队列
* @author
* @since 2022/10/19 21:09
*/
public class CircleArrayQueued {
private int maxSize; //表示数组的最大容量
/*front 变量的含义做一个调整:
front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素,front 的初始值 = 0*/
private int front; //队列头
//rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定.
private int rear; //队列尾
private int[] arr; //该数组用于存放数据,模拟队列
public CircleArrayQueued(int arrayMaxSize) {
maxSize = arrayMaxSize;
arr = new int[maxSize];
}
//判断队列是否满
public boolean isFull() {
return (rear+1) % maxSize == front;
}
//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}
//添加数据到队列
public void addQueue(int n) {
//添加前判断队列是否满
if(isFull()){
System.out.println("队列满,暂时不能加入数据,请耐心等待");
}
//直接将数据加入即可
arr[rear] = n;
//将rear的指针后移,这里必须考虑取模
rear = (rear+1)% maxSize;
}
//从队列取出数据
public int getQueue() {
//取出数据前首先判断队列是否为空
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("队列空,不能取出数据");
}
//这里需要分析出front是指向队列的第一个元素,首先先把front对应的值保存到一个临时变量,再将front后移,再将临时变量返回
int value = arr[front];
//后移过程中要考虑取模
front = (front+1) % maxSize;
return value;
}
//显示队列的所有数据
public void showQueue() {
//遍历所有数据,首先判断队列是否为空
if (isEmpty()){
System.out.println("队列为空,没有数据");
}
//思路:从front开始遍历,遍历多少个元素
for (int i = front; i < front + size(); i++) {
System.out.printf("arr[%d]=%d\n",i % maxSize,arr[i % maxSize]);
}
}
//求出当前队列有效数据的个数,否则无法遍历
public int size() {
return (rear + maxSize - front) % maxSize;
}
//显示队列的头数据,注意不是取出数据
public int headQueue() {
//首先判断队列的数据是否为空
if(isEmpty()){
throw new RuntimeException("队列是空的,没有数据");
}
return arr[front];
}
}
```
* 环形队列的测试
```JAVA
/**
* description
* 测试环形的数组队列
* @author
* @since 2022/10/19 21:04
*/
public class CircleArrayQueueDemo {
public static void main(String[] args) {
//测试队列,首先创建一个队列对象
CircleArrayQueued circleArrayQueued = new CircleArrayQueued(4); //说明设置是4,其队列有效数据是3
//创建一个key模拟用户输入
char key =' ';
Scanner scanner = new Scanner(System.in);
boolean loop = true;
//输出一个菜单
while (loop) {
System.out.println("s(show) :显示队列");
System.out.println("e(exit) :退出程序");
System.out.println("a(add) :添加数据到队列");
System.out.println("g(get) :从队列取出数据");
System.out.println("h(head) :查看队列头的数据");
key = scanner.next().charAt(0); //接收一个字符
switch (key) {
//输入s显示队列
case 's':
//调用显示队列的方法
circleArrayQueued.showQueue();
break;
//输入a添加数据
case 'a':
System.out.println("请输入一个数字");
int value = scanner.nextInt();
circleArrayQueued.addQueue(value);
break;
//输入g取出数据
case 'g':
try {
int result = circleArrayQueued.getQueue();
System.out.printf("取出的数据是%d\n",result);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
//查看队列头的数据
case 'h':
try {
int result = circleArrayQueued.headQueue();
System.out.printf("队列头的数据是%d\n",result);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
//代表退出程序
case 'e':
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
```
### 3.2.5、集合
> 集合一般被定义为:由**一个或多个确定的元素**所构成的整体。
>
> 通俗来讲,集合就是将一组事物组合在一起。你可以将力扣的题库看作一个集合:
>
> 也可以将商店里的礼品当成一个集合
>
> 甚至可以将桌面上的物品当作一个集合。
>
> 集合有什么特性呢?
>
> 首先,**集合里的元素类型不一定相同**。 你可以将商品看作一个集合,也可以将整个商店看作一个集合,这个商店中有人或者其他物品也没有关系。
>
> 其次,**集合里的元素没有顺序**。 我们不会这样讲:我想要集合中的第三个元素,因为集合是没有顺序的。
>
> 事实上,这样的集合并不直接存在于编程语言中。然而,实际编程语言中的很多数据结构,就是在集合的基础上添加了一些规则形成的。
### 3.2.6、列表
> **列表**(又称线性列表)的定义为:是一种数据项构成的有限序列,即按照一定的线性顺序,排列而成的数据项的集合。
>
> 列表的概念是在集合的特征上形成的,它具有顺序,且长度是可变的。你可以把它看作一张购物清单:
>
> ![image-20221125234515710](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125234515710.png)
>
> 在这张清单中:
>
> - 购物清单中的条目代表的类型可能不同,但是按照一定顺序进行了排列;
> - 购物清单的长度是可变的,你可以向购物清单中增加、删除条目。
>
> 在编程语言中,列表最常见的表现形式有数组和链表,而我们熟悉的栈和队列则是两种特殊类型的列表。除此之外,向列表中添加、删除元素的具体实现方式会根据编程语言的不同而有所区分。
### 3.2.7、数组
> 数组是列表的实现方式之一,也是面试中经常涉及到的数据结构。
>
> 正如前面提到的,数组是列表的实现方式,它具有列表的特征,同时也具有自己的一些特征。然而,在具体的编程语言中,数组这个数据结构的实现方式具有一定差别。比如 C++ 和 Java 中,数组中的元素类型必须保持一致,而 Python 中则可以不同。Python 中的数组叫做 list,具有更多的高级功能。
>
> 那么如何从宏观上区分列表和数组呢?这里有一个重要的概念:**索引**。
>
> 首先,数组会用一些名为 索引 的数字来标识每项数据在数组中的位置,且在大多数编程语言中,索引是从 0 算起的。我们可以根据数组中的索引,快速访问数组中的元素
>
> ![image-20221125234620380](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125234620380.png)
>
> 而列表中没有索引,这是数组与列表最大的不同点。
>
> 其次,数组中的元素在内存中是连续存储的,且每个元素占用相同大小的内存。要理解这一点,我们需要了解数组在内存中的存储方式。
>
> ![image-20221125234656191](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125234656191.png)
>
> 相反,列表中的元素在内存中可能彼此相邻,也可能不相邻。比如列表的另一种实现方式——链表,它的元素在内存中则不一定是连续的。
>
> ![image-20221125234748052](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125234748052.png)
## 3.3、数组的四种操作
### 3.3.1、读取元素
> 读取数组中的元素,是通过访问索引的方式来读取的,索引一般从 0 开始。
>
> 在计算机中,内存可以看成一些已经排列好的格子,每个格子对应一个内存地址。一般情况下,数据会分散地存储在不同的格子中。
>
> ![image-20221125235043911](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125235043911.png)
>
> 而对于数组,计算机会在内存中为其申请一段 连续 的空间,并且会记下索引为 0 处的内存地址。以数组 ["C", "O", "D", "E", "R"] 为例,它的各元素对应的索引及内存地址如下图所示
>
> ![image-20221125235944800](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125235944800.png)
>
> 假如我们想要访问索引为 2 处的元素 "D" 时,计算机会进行以下计算:
>
> 找到该数组的索引 0 的内存地址: 2008;
> 将内存地址加上索引值,作为目标元素的地址,即 2008 + 2 = 2010,对应的元素为 "D",这时便找到了目标元素。
> 我们知道,计算内存地址这个过程是很快的,而我们一旦知道了内存地址就可以立即访问到该元素,因此它的时间复杂度是常数级别,为 O(1)。
### 3.3.2、查找元素
> 假如我们对数组中包含哪些元素并不了解,只是想知道其中是否含有元素 "E",数组会如何查找元素 "E" 呢?
>
> 与读取元素类似,由于我们只保存了索引为 0 处的内存地址,因此在查找元素时,只需从数组开头逐步向后查找就可以了。如果数组中的某个元素为目标元素,则停止查找;否则继续搜索直到到达数组的末尾。
>
> ![image-20221126000040069](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221126000040069.png)
>
> 我们发现,最坏情况下,搜索的元素为 "R",或者数组中不包含目标元素时,我们需要查找 n 次,n 为数组的长度,因此查找元素的时间复杂度为 O(N),N。
### 3.3.3、插入元素
> 假如我们想在原有的数组中再插入一个元素 "S" 呢?
>
> 如果要将该元素插入到数组的末尾,只需要一步。即计算机通过数组的长度和位置计算出即将插入元素的内存地址,然后将该元素插入到指定位置即可。
>
> ![6.gif](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora2b53523dc1a745d89fbc11ba776eaa2d0f220acf4c232b1a83f939c973141280-6.gif)
>
> 然而,如果要将该元素插入到数组中的其他位置,则会有所区别,这时我们首先需要为该元素所要插入的位置 **腾出** 空间,然后进行插入操作。比如,我们想要在索引 2 处插入 "S"。
>
> ![7.gif](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora22ce7dbf8cd441fd7425499cd8154d1c4211a6a42ec3f3995520ee76ce7183c7-7.gif)
>
> 我们发现,如果需要频繁地对数组元素进行插入操作,会造成时间的浪费。事实上,另一种数据结构,即链表可以有效解决这个问题。
### 3.3.4、删除元素
> 删除元素与插入元素的操作类似,当我们删除掉数组中的某个元素后,数组中会留下 **空缺** 的位置,而数组中的元素在内存中是连续的,这就使得后面的元素需对该位置进行 **填补** 操作。
>
> 以删除索引 `1` 中的元素 `"O"` 为例,具体过程如图所示
>
> ![6.gif](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora4df7a5a75e5f76b6e7e4540f9403c7c2fee5197a1f30421b4f5d32fdca2cf360-8.gif)
>
> 当数组的长度为 n 时,最坏情况下,我们删除第一个元素,共需要的步骤数为 1 + (n - 1) = n 步,其中,1 为删除操作,n - 1 为移动其余元素的步骤数。删除操作具有线性时间复杂度,即时间复杂度为 O(N)O(N)O(N),NNN 为数组的长度。
>
> ![数组的操作.jpg](file://D:\002-java\java-study-documentj\liuruohan\04_学习笔记\0405_数据结构与算法\040608_力扣里的数据结构学习笔记.assets\1617108025-oQRoDO-数组的操作.jpg?lastModify=1669474214)
### 3.3.5、数组的力扣练习题——寻找数组的中心索引
> **寻找数组的中心索引**
> 给你一个整数数组 nums ,请计算数组的 **中心下标** 。
>
> 数组 **中心下标** 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
>
> 如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
>
> 如果数组有多个中心下标,应该返回 **最靠近左边** 的那一个。如果数组不存在中心下标,返回 -1
>
> 示例 1:
>
> ```BASIC
> 输入:nums = [1, 7, 3, 6, 5, 6]
> 输出:3
> 解释:
> 中心下标是 3 。
> 左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
> 右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。
> ```
>
> 思路分析:
>
> * 先求得数组中所有元素之和sum;
> * 遍历数组,取当前下标左边的元素之和leftSum,同时sum减去已遍历元素,比较二者是否相等,相等则返回当前下标;
> * 遍历结束,代表没有中心索引,返回-1;
>
> 代码实现
>
> ```JAVA
> /**
> * description
> * 寻找数组的中心索引
> * 思路分析:
> * 1、先求出数组的总和sum
> * 2、遍历数组,取出下标左边的元素之和leftSum,同时sum减去已遍历元素,并比较二者是否相等,相等则返回当前下标
> * 3、遍历结束,代表没有中心索引,返回-1
> *
> * @author
> * @since 2022/11/26 9:36
> */
> public class Solution {
> public static void main(String[] args) {
> int[] arr = {1, 7, 3, 6, 5, 6};
> int i = pivotIndex(arr);
> System.out.println(i);
> }
>
> /**
> * 寻找数组中心索引的方法
> *
> * @param nums 需要寻找中心索引的数组
> * @return
> */
> public static int pivotIndex(int[] nums) {
> int sum = 0; //临时变量,用于记录数组中所有的总和
> for (int i = 0; i < nums.length; i++) {
> sum += nums[i]; //在遍历此数组中使用变量接收总和
> }
> int leftSum = 0; //用于记录下标左边的元素之和
> for (int i = 0; i < nums.length; i++) {
> sum -= nums[i]; //总和减去已遍历元素
> if (leftSum == sum) {
> //比较二者是否相等,相等则返回当前下标
> return i;
> }
> leftSum += nums[i];
> }
> return -1; //遍历结束,代表中心没有索引,返回-1
> }
> }
> ```
### 3.3.6、数组的力扣练习题——搜索插入位置
> 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
>
> 请必须使用时间复杂度为 O(log n) 的算法。
>
> **示例 1:**
>
> ```bash
> 输入: nums = [1,3,5,6], target = 5
> 输出: 2
> ```
>
> 思路分析:
>
> * 如果数组中的值大于或者等于target,直接return
> * 如果全部遍历完证明target是最大的数,直接插入末尾
>
> 代码实现:
>
> ```JAVA
> /**
> * description
> * 数组的力扣练习题——搜索插入位置
> * 如果数组中的值大于或者等于target,直接return
> * 如果全部遍历完证明target是最大的数,直接插入末尾
> *
> * @author
> * @since 2022/11/26 23:30
> */
> public class Answer {
> public static void main(String[] args) {
> int target = 2;
> int[] arr = {1, 3, 5, 6};
> int i = searchInsert(arr, target);
> System.out.println(i);
> }
>
> /**
> * 搜索插入的位置的方法
> *
> * @param nums 需要操作的数组
> * @param target 需要插入的数字
> * @return
> */
> public static int searchInsert(int[] nums, int target) {
> for (int i = 0; i < nums.length; i++) {
> //数组中的值大于或者等于target,直接return
> if (nums[i] >= target) {
> return i;
> }
> }
> //全部遍历完证明target是最大的数,直接插入数组末尾
> return nums.length;
> }
> }
> ```
## 3.4、二维数组
### 3.4.1、二维数组简介
> 二维数组是一种结构较为特殊的数组,只是将数组中的每个元素变成了一维数组。
>
> ![1.png](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typorae64116dc9c9c8f9f8ad2a5c251c0e76a677ba874a3bab0e22ce164384237a55c-1.png)
>
> 所以二维数组的本质上仍然是一个一维数组,内部的一维数组仍然从索引 `0` 开始,我们可以将它看作一个矩阵,并处理矩阵的相关问题。
>
> * 示例
> 类似一维数组,对于一个二维数组 A = [[1, 2, 3, 4],[2, 4, 5, 6],[1, 4, 6, 8]],计算机同样会在内存中申请一段 连续 的空间,并记录第一行数组的索引位置,即 A[0][0] 的内存地址,它的索引与内存地址的关系如下图所示。
>
> ![2.png](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora1600741130-xzcLML-WechatIMG2.png)
>
> 注意,实际数组中的元素由于类型的不同会占用不同的字节数,因此每个方格地址之间的差值可能不为 1。
>
> 实际题目中,往往使用二维数组处理矩阵类相关问题,包括矩阵旋转、对角线遍历,以及对子矩阵的操作等
### 3.4.2、二维数组的力扣练习题——合并区间
> 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
>
> 示例 1:
>
> ```BASIC
> 输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
> 输出:[[1,6],[8,10],[15,18]]
> 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]
> ```
>
> 思路分析:合并 2 个区间
>
> * 对二维数组进行排序,按照第一列升序列排列
> * 借用临时空间,判断是否需要合并集合当前值,当前集合是否放入结果集触发点
>
> 代码实现:
>
> ```JAVA
> /**
> * description
> * 二维数组的练习题——合并区间的解法
> *
> * @author
> * @since 2022/11/27 10:14
> */
> public class practice {
> public static void main(String[] args) {
> int [][] arr = new int[3][3];
> int[][] merge = merge(arr);
> System.out.println(Arrays.toString(merge));
> }
> /**
> * 合并所有重叠的区间的方法
> *
> * @param intervals 若干个区间的集合
> * @return 一个不重叠的区间数组
> */
> public static int[][] merge(int[][] intervals) {
> if (intervals.length == 0) {
> return intervals;
> }
> Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));//按每行的第0列升序排序
> Vector<int[]> integerVector;//由于我们事先不知道数组大小,所以用Vector类实现动态数组。
> integerVector = new Vector<>();
> int[] ints = intervals[0];//定义一个Int类型数组用于作比较,默认值为第一组二维数组的值。
> for (int i = 1; i < intervals.length; i++) {//循环这个二维数组
> if (ints[1] >= intervals[i][0]) {//如果第一个数组的右端点大于等于下一个数组的左端点,做说明两个数组有所交集。
> ints[1] = Math.max(ints[1], intervals[i][1]);//int类型数组的右端点等于两个数组中右端点大的那个值。
> } else {
> integerVector.add(ints);//把int类型一维数组ints添加到我们创建的vector类里面。
> ints = intervals[i];//给一维数组重新赋值。
> }
> }
> integerVector.add(ints);//把最后一个区间添加到Vector里面
> return integerVector.toArray(new int[integerVector.size()][2]);//把vector转换成二维数组返回。
> }
> }
> ```
### 3.4.3、二维数组的力扣练习题——旋转矩阵
> 给你一幅由 `N × N` 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。
>
> 不占用额外内存空间能否做到?
>
> 例 1:
>
> ```BASIC
> 给定 matrix =
> [
> [1,2,3],
> [4,5,6],
> [7,8,9]
> ],
>
> 原地旋转输入矩阵,使其变为:
> [
> [7,4,1],
> [8,5,2],
> [9,6,3]
> ]
> ```
>
> 思路分析:
>
> * 如下图,先由对角线 `[1, 5, 9]` 为轴进行翻转:
>
> ![image.png](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora7c85f9932eeae5a454cd0e825106c972c719ed4401f7ab62bc4092c7239ff41b-image.png)
>
> 翻转后的数组变成了
>
> ```JAVA
> [1,4,7]
> [2,5,8]
> [3,6,9]
> ```
>
> 再对每一行以中点进行翻转,就得到了
>
> ```JAVA
> [7,4,1]
> [8,5,2]
> [9,6,3]
> ```
>
> 代码实现如下:
>
> ```JAVA
> /**
> * description
> * 二维数组的力扣练习题——旋转矩阵
> *
> * @author
> * @since 2022/11/27 12:54
> */
> public class Matrix {
> public static void main(String[] args) {
> int[][] arr = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
> //矩阵旋转前
> System.out.println("矩阵旋转前的数组");
> for (int i = 0; i < arr.length; i++) {
> for (int j = 0; j < arr[i].length; j++) {
> System.out.print(arr[i][j] + " ");
> }
> System.out.println(); //换行
> }
> rotate(arr);
> System.out.println("矩阵旋转后的数组");
> for (int i = 0; i < arr.length; i++) {
> for (int j = 0; j < arr[i].length; j++) {
> System.out.print(arr[i][j] + " ");
> }
> System.out.println(); //换行
> }
> }
>
> //旋转矩阵的方法
> public static void rotate(int[][] matrix) {
> int n = matrix.length; //使用变量平替二维数组matrix
> for (int i = 0; i < n - 1; i++) {
> //先以对角线(左上-右下)为轴进行翻转
> for (int j = i + 1; j < n; j++) {
> int tmp = matrix[i][j]; //tmp就是轴
> matrix[i][j] = matrix[j][i];
> matrix[j][i] = tmp;
> }
> }
> //再对每一行以中点进行翻转
> int mid = n >> 1;
> for (int i = 0; i < n; i++) {
> for (int j = 0; j < mid; j++) {
> int tmp = matrix[i][j];
> matrix[i][j] = matrix[i][n - 1 - j];
> matrix[i][n - 1 - j] = tmp;
> }
> }
> }
> }
> ```
>
>
# 四、链表
## 4.1 、链表(Linked List)介绍
链表是有序的列表,但是它在内存中是存储如下
![image-20221019224531271](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019224531271.png)
小结上图:
* 1) 链表是以节点的方式来存储,是**链式存储**
* 2) 每个节点包含 data 域, next 域:指向下一个节点
* 3) 如图:发现**链表的各个节点不一定是连续存储**
* 4) 链表分**带头节点的链表和没有头节点的链表**,根据实际的需求来确定
* 单链表(带头结点) **逻辑结构**示意图如下
![image-20221019224609218](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221019224609218.png)
## 4.2、 单链表的应用实例
使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作,注: 删除和修改,查找
* 1) 第一种方法在添加英雄时,直接添加到链表的尾部 思路分析示意图:
![image-20221020234346581](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020234346581.png)
* 2) 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示) 思路的分析示意图:
![image-20221020145412935](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020145412935.png)
* 3) 修改节点功能 思路(1) 先找到该节点,通过遍历,(2) temp.name = newHeroNode.name ; temp.nickname= newHeroNode.nickname
* 4) 删除节点 思路分析的示意图:
![image-20221020234818141](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020234818141.png)
* 5) 完成的代码演示:
* 首先创建定义英雄的对象,定义英雄的属性,再生成构造器和toString方法
```JAVA
/**
* description
* 每个HeroNode对象就是一个节点
* @author
* @since 2022/10/20 15:06
*/
public class SingleLinkedListHeroNode {
public int no;
public String name;
public String nickname;
public SingleLinkedListHeroNode next; //指向下一个节点
public SingleLinkedListHeroNode(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "SingleLinkedListHeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
```
* 再定义一个链表来管理英雄的对象
```JAVA
/**
* description
* 定义SingleLinkedList来管理我们的英雄
* @author
* @since 2022/10/20 15:10
*/
public class SingleLinkedList {
//先初始化一个头节点,头节点一般不动,表示数据中的头节点,并不存放具体数据
private SingleLinkedListHeroNode head = new SingleLinkedListHeroNode(0,"","");
/*定义一个添加的方法,用于添加节点到单向链表
思路:当不考虑编号顺序时:1、找到当前链表的最后这个节点;2、将最后这个节点的next域指向新的节点
*/
public void add(SingleLinkedListHeroNode heroNode){
//因为head(即头节点)不能动,因此我们需要一个辅助变量temp
SingleLinkedListHeroNode temp = head;
//遍历链表找到最后节点
while (true){
//当变量temp找到最后一个节点为空时,就代表找到了最后节点的位置
if (temp.next == null){
break;
}
//如果没有找到最后,就让temp向后移动即可
temp = temp.next;
}
//当退出了这个循环后说明了temp已经指向了链表的最后,那么这个时候只需要将temp连上新的节点即可
temp.next = heroNode;
}
//显示链表的方法(即遍历)
public void list(){
//首先判断链表是否为空,若为空就不用遍历了
if (head.next == null){
System.out.println("链表为空");
}
//因为头节点不能动,因此,需要通过一个辅助变量来遍历整个链表
SingleLinkedListHeroNode temp = head.next;
//链表不为空,开始循环
while (true){
//判断是否到达链表最后
if (temp == null){
break;
}
//若链表不为空就输出整个节点的信息
System.out.println(temp);
//将temp后移,不后移会导致链表死循环
temp = temp.next;
}
}
}
```
* 定义好之后再进行测试
```JAVA
/**
* description
* 单链表的案例实现
* @author
* @since 2022/10/20 15:05
*/
public class SingleLinkedListDemo {
public static void main(String[] args) {
//进行测试,先创建节点
SingleLinkedListHeroNode heroNode1 = new SingleLinkedListHeroNode(1, "宋江", "及时雨");
SingleLinkedListHeroNode heroNode2 = new SingleLinkedListHeroNode(2, "卢俊义", "玉麒麟");
SingleLinkedListHeroNode heroNode3 = new SingleLinkedListHeroNode(3, "吴用", "智多星");
SingleLinkedListHeroNode heroNode4 = new SingleLinkedListHeroNode(4, "林冲", "豹子头");
//创建一个链表用于存放数据
SingleLinkedList singleLinkedList = new SingleLinkedList();
//将节点加入
singleLinkedList.add(heroNode1);
singleLinkedList.add(heroNode2);
singleLinkedList.add(heroNode3);
singleLinkedList.add(heroNode4);
//显示链表,测试是否能正常显示
singleLinkedList.list();
}
}
```
* 经测试,输出结果如下
![image-20221020162116043](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020162116043.png)
* 但同时我们发现了问题,即在新加入节点时输出结果会根据添加节点的顺序进行排序
* 即当我们加入的顺序打乱,输出的结果就会根据添加的顺序排列
![image-20221020162743014](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020162743014.png)
* 输出结果如下
![image-20221020162854787](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020162854787.png)
* 所以要进行链表有序排列,需要再次进行优化
* 思路分析
![image-20221020164200299](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020164200299.png)
* 将原有的代码进行优化,新增另一种添加英雄的方式,即根据排名将英雄插入到指定位置
```JAVA
//新增第二种添加英雄的方式,根据排名将英雄插入到指定位置,若有此排名,则添加失败并返回错误信息
public void addByOrder(SingleLinkedListHeroNode heroNode){
/*因为头节点不能动,因此我们仍然通过一个辅助变量来帮助找到添加的位置
又因为这是单链表,辅助变量temp是位于添加位置的前一个节点,否则插入不了*/
SingleLinkedListHeroNode temp = head;
//标识添加的变量是否存在,默认为false
boolean flag = false;
while (true){
if (temp.next == null){ //说明temp已经到了链表的最后
break;
}
if (temp.next.no > heroNode.no){ //位置找到,就在temp的后边插入
break;
}else if (temp.next.no == heroNode.no){ //说明希望添加的heroNode的编号已经存在
flag = true; //说明编号存在
break;
}
temp = temp.next; //若是以上三个条件都不满足,就让temp后移,遍历当前链表
}
//判断flag的值
if (flag){//若flag的值为true,说明不能添加,编号已经存在
System.out.println("准备的插入的英雄编号" + heroNode.no + "已经存在了,无法重复添加");
}else {
//插入到链表种,即temp的后一位
heroNode.next = temp.next;
temp.next = heroNode;
}
}
```
* 打乱添加顺序,重新测试
```JAVA
singleLinkedList.addByOrder(heroNode2);
singleLinkedList.addByOrder(heroNode4);
singleLinkedList.addByOrder(heroNode1);
singleLinkedList.addByOrder(heroNode5);
singleLinkedList.addByOrder(heroNode3);
```
* 输出结果如下:添加顺序混乱,但链表内的节点仍能自动排序
![image-20221020170922158](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020170922158.png)
* 当加入一个重复添加的编号控制台会报错
![image-20221020171350193](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020171350193.png)
* 至此代码优化完毕
* 接下来是进行修改操作
* 加入修改的方法
```JAVA
/*修改节点的信息,根据编号来修改,即编号不能进行修改
说明:根据newHeroNode的no来修改即可
*/
public void update(SingleLinkedListHeroNode newHeroNode){
//首先判断链表是否为空
if(head.next == null ){
System.out.println("链表为空,无法修改");
}
//找到需要修改的节点,根据no编号来找,定义辅助节点方便查询
SingleLinkedListHeroNode temp = head.next;
boolean flag = false; //表示是否找到该节点
while (true){
if (temp == null){
break; //若temp为空表示链表已经遍历结束了
}
if (temp.no == newHeroNode.no){
//temp找到当前需要修改的节点了
flag = true;
break; //找到就结束循环,否则会一直往后
}
temp = temp.next;
}
//根据flag判断是否找到要修改的节点
if(flag = true){
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else { //没有找到此节点
System.out.println("没有找到编号为" + newHeroNode.no + "的节点");
}
}
```
* 测试修改功能是否正常
```java
//修改前的链表显示
singleLinkedList.list();
//测试修改节点的代码
SingleLinkedListHeroNode newHeroNode = new SingleLinkedListHeroNode(2,"小卢","麒麟");
singleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
//显示链表,测试是否能正常显示
singleLinkedList.list();
```
* 修改效果如图
* 经测试修改功能正常
![image-20221020222858697](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020222858697.png)
* 接下来是删除功能
* 思路如下
![image-20221020224811233](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020224811233.png)
* 代码实现
```JAVA
/*删除节点,思路:1、head节点不能动,因此我们需要temp辅助节点找到待删除节点的前一个节点
2、我们在比较时,是temp.next.no 和需要删除的节点的no进行比较 */
public void delete(int no){
SingleLinkedListHeroNode temp = head;
boolean flag = false; //标识是否找到待删除节点的前一个节点
while (true){
if (temp.next == null){ //说明已经到了链表的最后
break;
}
if (temp.next.no == no){
//说明找到了待删除节点的前一个节点temp
flag = true;
break;
}
temp = temp.next; //temp后移实现链表遍历
}
//判断flag
if (flag){ //找到节点
//进行删除操作
temp.next = temp.next.next;
}else {
System.out.println("要删除的节点" + no + "不存在");
}
}
```
* 进行测试删除的操作
* 结果如图所示
![image-20221020225813600](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221020225813600.png)
## 4.3 、单链表面试题(新浪、百度、腾讯)
- 单链表的常见面试题有如下:
- 1) 求单链表中有效节点的个数
- 思路:直接遍历即可(即直接在链表内加入方法即可)
- 代码实现入下:
```java
//添加方法:获取到单链表的个数(如果是带头节点的链表要求不统计头节点)
/**
*
* @param head 链表的头节点
* @return 返回的就是有效节点的个数
*/
public static int getLength(SingleLinkedListHeroNode head){
//首先判断链表是否为空
if (head.next == null){
return 0; //若为空说明这是一个带头节点的空链表,直接返回0即可
}
int length = 0;
//定义一个辅助变量,标识没有统计头节点
SingleLinkedListHeroNode cur = head.next;
while (cur!=null){
length++;
cur = cur.next; //结束遍历
}
return length;
}
```
* 在头节点种加入Getter方法并在主方法体中测试
```JAVA
public SingleLinkedListHeroNode getHead() {
return head;
}
//测试以下求单链表种有效节点的个数
System.out.println("有效的节点个数= " + getLength(singleLinkedList.getHead()));
```
* 输出结果如下:
![image-20221021174040797](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221021174040797.png)
- 2) 查找单链表中的倒数第 k 个结点 【新浪面试题】
- 思路分析入下:编写一个方法,接收head节点,同时接收一个index
- index表示倒数第index个节点
- 由于此题数据结构是单链表,不能倒序遍历,因此先把链表从头到尾遍历,得到链表的总长度 getLength
- 得到size (节点长度)后,我们从链表的第一个节点开始遍历(size-index)个,就可以得到
- 如果找到该节点,则返回该节点,找不到节点返回null即可
- 代码演示如下
```JAVA
public static SingleLinkedListHeroNode findLastIndexNode(SingleLinkedListHeroNode head,int index){
//首先判断链表是否为空,若链表为空返回null
if (head.next == null){
return null; //没有找到该节点
}
//第一次遍历得到链表的长度(节点的个数)
int size = getLength(head);
/*第二次遍历 size-index 位置,即倒数的第k个节点,在此之前先给index做一个校验,即保证遍历的节点位置不能为负数
也不能超过链表中总节点的数量,超过了就返回null*/
if (index <=0 || index > size){
return null;
}
//接下来开始遍历,定义一个辅助变量指向第一个有效的节点cut,找到之后使用for循环定位到倒数的index
SingleLinkedListHeroNode cur = head.next;
for(int i=0; i<size -index; i++){ //循环的主要是为了往下移动
cur = cur.next;
}
return cur; //循环过后就找到了数据,直接返回即可
}
```
- 接下来进行测试:k表示链表内任意一个有效节点,测试代码中获取的是倒数第一个
```JAVA
//测试以下是否得到了倒数第k个节点
SingleLinkedListHeroNode result = findLastIndexNode(singleLinkedList.getHead(),1);
System.out.println("result= " +result);
}
```
- 输出结果如下:
![image-20221021211829916](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221021211829916.png)
- 根据结果得出结论代码无误
* 3) 单链表的反转,即把节点的顺序进行反转
* 思路分析图解如下
![image-20221022082740198](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221022082740198.png)
* 具体思路分析如下
* 1、先定义一个节点reserseHead = new HeroNode();
* 2、从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reserseHead 的最前端
* 3、原链表的head.next = reverseHead.next
![image-20221022163849558](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221022163849558.png)
* 代码实现
```JAVA
//将单链表进行反转
public static void reversetList(SingleLinkedListHeroNode head){
//如果当前链表为空,或者只有一个节点,就无需反转,直接返回即可
if (head.next == null || head.next.next == null){
return;
}
//定义一个辅助的变量帮助我们遍历原来的链表
SingleLinkedListHeroNode cur = head.next;
//定义当前指向当前节点(cur)的下一个节点
SingleLinkedListHeroNode next = null;
//再定义一个reverseHead节点,作用就是表示单链表头next
SingleLinkedListHeroNode reverseHead = new SingleLinkedListHeroNode(0,"","");
//先遍历原来的链表,并从头遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端
while (cur !=null){ //cur不能为空,若为空说明已经遍历结束了
next = cur.next; //先暂时保存当前节点的下一个节点,后面需要使用
cur.next = reverseHead.next; //将cur的下一个节点指向新的链表的最前端
reverseHead.next = cur; //将cut连接到新的链表上
cur = next; //让cur后移
}
//将head.next指向reverseHead.next,实现单链表的反转
head.next = reverseHead.next;
}
```
* 接下来进行测试
```JAVA
//测试一下单链表的反转功能
System.out.println("原链表的情况");
singleLinkedList.list();
//反转
System.out.println("反转单链表之后");
reversetList(singleLinkedList.getHead());
singleLinkedList.list();
}
```
* 结果如下:
![image-20221022174239643](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221022174239643.png)
* 4) 从尾到头打印单链表 【百度,要求方式 1:反向遍历 。 方式 2:Stack 栈】
* 思路分析
* 题目要求就是逆向打印单链表
* 方式一:先将单链表进行反转操作,然后再遍历即可(这样做的问题是会破坏原来的单链表的结果,不建议)
* 方式二:可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点就实现了逆序打印的效果
* 由于方式一会破坏链表的结构,所以这题直接使用方式二:代码演示如下:
```JAVA
//使用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
public static void reversePrint(SingleLinkedListHeroNode head){
//首先判断链表是否为空
if (head.next == null) {
return; //链表为空,无法打印
}
//若不是空链表,创建一个栈,将各个节点压入栈中
Stack<SingleLinkedListHeroNode> stack = new Stack();
//首先将此节点保存,用于稍后遍历
SingleLinkedListHeroNode cur = head.next;
//将链表的所有节点压入栈中
while (cur != null){
stack.push(cur);
cur = cur.next; //让cut后移,这样就可以压入下一个节点
}
//将栈中的节点进行遍历即可
while (stack.size() > 0){
System.out.println(stack.pop()); //由于栈的特性是先进后出,直接调用stack的pop方法就可以实现倒序输出
}
}
```
* 进行测试
```JAVA
//测试一下单链表的反转功能
System.out.println("原链表的情况");
singleLinkedList.list();
System.out.println("测试逆序打印的情况,没有改变链表的本身结构");
reversePrint(singleLinkedList.getHead());
```
* 测试结果如下
![image-20221022204015345](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221022204015345.png)
## 4.4 、双向链表应用实例
### 4.4.1、双向链表的操作分析和实现
* 使用带 head 头的双向链表实现 –水浒英雄排行榜
* 管理单向链表的缺点分析:
* 1) 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
* 2) 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到 temp,temp 是待删除节点的前一个节点(认真体会).
* 3) 分析了双向链表如何完成遍历,添加,修改和删除的思路
![image-20221023084714027](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221023084714027.png)
对上图的说明:
* 分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现
* 1) 遍历 方和 单链表一样,只是可以向前,也可以向后查找
* 2) 添加 (默认添加到双向链表的最后)
* (1) 先找到双向链表的最后这个节点
* (2) temp.next = newHeroNode
* (3) newHeroNode.pre = temp;
* 3) 修改 思路和 原来的单向链表一样.
* 4) 删除
* (1) 因为是双向链表,因此,我们可以实现自我删除某个节点
* (2) 直接找到要删除的这个节点,比如 temp
* (3) temp.pre.next = temp.next
* (4) temp.next.pre = temp.pre;
* 双向链表增删改查功能的代码实现:首先定义增删改查的四个方法:
```JAVA
public class DoubleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体的数据
private DoubleLinkedListHeroNode head = new DoubleLinkedListHeroNode(0,"","");
//返回头节点
public DoubleLinkedListHeroNode getHead(){
return head;
}
//遍历双向链表的方法
public void list(){
//判断链表是否为空
if (head.next == null){
System.out.println("链表为空");
}
//因为头节点,不能动,因此我们需要一个辅助变量来遍历
DoubleLinkedListHeroNode temp = head.next;
while (true){
//判断是否到链表最后
if (temp == null){
break;
}
//输出节点的信息
System.out.println(temp);
//将temp后移
temp = temp.next;
}
}
//添加一个节点到双向链表的最后
public void add(DoubleLinkedListHeroNode heroNode){
//因为head(即头节点)不能动,因此我们需要一个辅助变量temp
DoubleLinkedListHeroNode temp = head;
//遍历链表找到最后节点
while (true){
//当变量temp找到最后一个节点为空时,就代表找到了最后节点的位置
if (temp.next == null){
break;
}
//如果没有找到最后,就让temp向后移动即可
temp = temp.next;
}
//当退出了这个循环后说明了temp已经指向了链表的最后
//形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
//修改一个节点的内容
/*修改节点的信息,根据编号来修改,即编号不能进行修改
说明:根据newHeroNode的no来修改即可
可以双向列表的内容修改和前面的单向链表一模一样,唯一的区别就是节点的类型不同
*/
public void update(DoubleLinkedListHeroNode newHeroNode){
//首先判断链表是否为空
if(head.next == null ){
System.out.println("链表为空,无法修改");
}
//找到需要修改的节点,根据no编号来找,定义辅助节点方便查询
DoubleLinkedListHeroNode temp = head.next;
boolean flag = false; //表示是否找到该节点
while (true){
if (temp == null){
break; //若temp为空表示链表已经遍历结束了
}
if (temp.no == newHeroNode.no){
//temp找到当前需要修改的节点了
flag = true;
break; //找到就结束循环,否则会一直往后
}
temp = temp.next;
}
//根据flag判断是否找到要修改的节点
if(flag==true){
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else { //没有找到此节点
System.out.println("没有找到编号为" + newHeroNode.no + "的节点");
}
}
/*从双线链表中删除一个节点,说明:
1、对于双向链表,我们可以直接找到要删除的这个节点
2、找到后自我删除即可*/
public void delete(int no){
//判断当前链表是否为空
if(head.next==null){ //空链表
System.out.println("链表为空,无法删除");
}
//辅助遍历
DoubleLinkedListHeroNode temp = head.next;
boolean flag = false; //标识是否找到待删除节点
while (true){
if (temp == null){ //说明已经到了链表的最后
break;
}
if (temp.no == no){
//说明找到了待删除节点的前一个节点temp
flag = true;
break;
}
temp = temp.next; //temp后移实现链表遍历
}
//判断flag
if (flag){ //找到节点
//进行删除操作
temp.pre.next = temp.next;
//如果是最后一个节点,就不需要执行 temp.next.pre = temp.pre;,否则会出现空指针异常
if (temp.next!=null){
temp.next.pre = temp.pre;
}
}else {
System.out.println("要删除的节点" + no + "不存在");
}
}
}
```
* 接下来是对双向链表的增删改查进行测试
```JAVA
public class DoubleLinkedListDemo {
public static void main(String[] args) {
//测试双向链表的增删改查功能是否正常
System.out.println("双向链表的测试");
//先创建节点
DoubleLinkedListHeroNode heroNode1 = new DoubleLinkedListHeroNode(1,"宋江","及时雨");
DoubleLinkedListHeroNode heroNode2 = new DoubleLinkedListHeroNode(2,"卢俊义","玉麒麟");
DoubleLinkedListHeroNode heroNode3 = new DoubleLinkedListHeroNode(3,"吴用","智多星");
DoubleLinkedListHeroNode heroNode4 = new DoubleLinkedListHeroNode(4,"林冲","豹子头");
DoubleLinkedListHeroNode heroNode5 = new DoubleLinkedListHeroNode(5,"武松","武都头");
//再创建一个双向链表对象
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(heroNode5);
doubleLinkedList.add(heroNode3);
doubleLinkedList.add(heroNode2);
doubleLinkedList.add(heroNode4);
doubleLinkedList.add(heroNode1);
//输出查看结果是否正确
doubleLinkedList.list();
//修改测试
DoubleLinkedListHeroNode newHeroNode = new DoubleLinkedListHeroNode(4,"公孙胜","入云龙");
doubleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
doubleLinkedList.list();
//删除
doubleLinkedList.delete(5);
System.out.println("删除后的链表情况");
doubleLinkedList.list();
}
}
```
* 输出结果如图所示
![image-20221023152946981](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221023152946981.png)
* 通过测试结果我们发现链表的节点添加编号是根据添加,即非顺序排序,所以我们参考单链表的顺序添加第二种方式进行优化
```JAVA
//新增第二种添加英雄的方式,根据排名将英雄插入到指定位置,若有此排名,则添加失败并返回错误信息
public void addByOrder(DoubleLinkedListHeroNode heroNode){
//因为是双链表,因此temp可以在插入的位置 (区别于单链表)
DoubleLinkedListHeroNode temp = head;
//标识添加的变量是否存在,默认为false
boolean flag = false;
while (true){
if (temp.next == null){ //说明temp已经到了链表的最后
break;
}
if (temp.next.no > heroNode.no){ //位置找到,就在temp的后边插入
break;
}else if (temp.next.no == heroNode.no){ //说明希望添加的heroNode的编号已经存在
flag = true; //说明编号存在
break;
}
temp = temp.next; //若是以上三个条件都不满足,就让temp后移,遍历当前链表
}
//判断flag的值
if (flag){//若flag的值为true,说明不能添加,编号已经存在
System.out.println("准备的插入的英雄编号" + heroNode.no + "已经存在了,无法重复添加");
}else {
//插入到链表种,即temp的后一位插入到链表,temp的后面,这里的顺序很有讲究,看一下
heroNode.pre = temp;
heroNode.next = temp.next;
temp.next = heroNode;
temp.next.pre = heroNode;
}
}
```
* 测试:
```JAVA
doubleLinkedList.addByOrder(heroNode5);
doubleLinkedList.addByOrder(heroNode3);
doubleLinkedList.addByOrder(heroNode2);
doubleLinkedList.addByOrder(heroNode4);
doubleLinkedList.addByOrder(heroNode1);
//测试结果
doubleLinkedList.list();
```
* 结果如下:
![image-20221023161956536](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221023161956536.png)
* 总结:双链表排序大致与单链表一致,重点是代码的这一部分
```JAVA
node.pre = temp;
node.next = temp.next;
temp.next = node;
temp.next.pre = node;
```
* 当这部分的顺序发生改变时,代码会报错或者发生运行不完的情形
* 也就是说,我们需要按照下面这种顺序进行赋值
![在这里插入图片描述](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora5dc3721710cd4f9787e6473fa74cd8e8.png)
* 这是因为:
* 我们所做的1、2步,是把要添加的node节点2变完整,是它既有next指针,又有pre指针。这样再将其他位置的指针指向node2,也会是一个完整的。而不能先指向node2,再将node2进行改变。
* 因此上图的顺序可以进行小调整,那就是将1、2顺序互换,但是,3、4位置不能互换
![在这里插入图片描述](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora883ab7be042243dab9aae14566c62b2f.png)
## 4.5、单向环形链表应用场景
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由k结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1 开始计数,直到最后一个结点从链表中删除算法结束。
![image-20221024083337267](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221024083337267.png)
## 4.6、 单向环形链表介绍
![image-20221024084105760](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221024084105760.png)
## 4.7、 Josephu (约瑟夫)问题
- 约瑟夫问题的示意图
![image-20221024084838963](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221024084838963.png)
* Josephu 问题
* Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从1 开始报数,数到m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
* 提示
* 用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由k 结点起从1开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1 开始计数,直到最后一个结点从链表中删除算法结束
* 约瑟夫问题-创建环形链表的思路图解
![image-20221024093630104](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221024093630104.png)
* 环形链表的创建及其解决约瑟夫问题的代码实现
* 首先创建一个实体类Boy,表示节点,创建对应属性、构造器和Get和Set方法
```JAVA
public class Boy {
private int no; //编号
private Boy next; //指向下一个节点,默认为null
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
```
* 接下来创建一个环形的单向链表,以及遍历环形链表的方法
```JAVA
public class CircleSingleLinkedList{
//创建一个节点
BoyNode first = null;
public void setFirst(BoyNode first) {
this.first = first;
}
//添加小孩节点
public void addNode(int nums){
//nums做一个数据校验
if(nums < 1){
System.out.println("nums的值不正确!");
return;
}
BoyNode curBoy = null; //辅助指针,来帮助构成环形链表
//使用for循环来创建我们的环形链表
for(int i = 1; i <= nums; i++){
BoyNode boy = new BoyNode(i);
//如果是第一个小孩节点
if(i == 1){
first = boy;
first.setNext(first); //构成一个环
curBoy = first; //让curBoy指向第一个小孩
}else{
curBoy.setNext(boy);
boy.setNext(first);
curBoy = boy;
}
}
}
//遍历当前环形链表
public void showBoy(){
if(first == null){
System.out.println("链表为空!");
return;
}
//因为first不能动,因此我们需要一个辅助指针完成遍历
BoyNode curBoy = first;
while (true){
System.out.println("小孩的编号"+curBoy.getNo());
if(curBoy.getNext() == first ){ //说明已经遍历完毕
break;
}
curBoy = curBoy.getNext(); //让curBoy后移
}
}
```
* 接下来进行测试,查看功能是否正常,如下图所示结果正常
```JAVA
public class Josepfu {
public static void main(String[] args) {
//测试构建环形链表和遍历是否正常显示
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5); //加入五个节点
circleSingleLinkedList.showBoy();
}
}
```
![image-20221024110633923](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221024110633923.png)
## 4.8、环形链表出圈问题的代码实现
* 约瑟夫问题-小孩出圈的思路分析图
![image-20221024114107338](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221024114107338.png)
* 代码实现如下:
```JAVA
/**
* 根据用户的输入,计算出出圈的顺序
* @param startNo 表示从第几个节点开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少节点在圈中
*/
public void countBoy(int startNo,int countNum,int nums){
//先对数据进行校验
if(first == null || startNo < 1 || startNo > nums){
System.out.println("参数输入有误!");
return;
}
//创建一个辅助指针,帮助完成小孩出圈
BoyNode temp = first;
//遍历让temp指向最后一个节点
while(true){
if(temp.getNext() == first){
break; //找到最后一个节点
}
temp = temp.getNext();
}
//小孩报数前,先让first和temp移动k-1次
for(int j = 0; j < startNo-1; j++){
first = first.getNext();
temp = temp.getNext();
}
//当一个小孩报数时,让first和temp同时的移动 m-1 次
//这里是一个循环操作,直到最后一个小孩
while(true){
if(temp == first){
break; //还剩一个小孩了
}
//让first和temp同时移动countNum-1
for(int j = 0; j < countNum-1; j++){
first = first.getNext();
temp = temp.getNext();
}
//这时first指向的节点,就是要出圈的小孩的节点
System.out.println("小孩"+first.getNo()+"出圈");
//这时将first指向的小孩节点出圈
first = first.getNext();
temp.setNext(first);
}
System.out.println("最后留在圈中的小孩编号为:"+first.getNo());
}
```
* 测试
```JAVA
//测试节点出圈是否正确
circleSingleLinkedList.countBoy(1,2,5); //2-->4-->1-->5-->3
```
* 测试结果如下:
![image-20221024131846103](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221024131846103.png)
# 第五章、栈
## 5.1、栈的一个实际需求
请输入一个表达式
* 计算式:[7*2*2-5+1-5+3-3] 点击计算【如下图】
![image-20221025091328778](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221025091328778.png)
请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式7* 2 * 2 - 5 + 1 - 5 + 3 + 3
但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。--->**栈**
## 5.2、栈的介绍
* 1) 栈的英文为(stack)
* 2) 栈是一个先入后出(FILO-First In Last Out)的有序列表。
* 3)栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
* 4) 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
* 5) 图解方式说明出栈(pop)和入栈(push)的概念
![image-20221025091849789](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221025091849789.png)
## 5.3、栈的应用场景
* 1) 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
* 2) 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
* 3) 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
* 4) 二叉树的遍历
* 5) 图形的深度优先(depth 一 first)搜索法。
## 5.4、栈的快速入门
* 1) 用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈,入栈等操作。
* 2) 实现思路分析,并画出示意图
![image-20221025092104422](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221025092104422.png)
* 使用数组模拟实现栈的思路分析
* 1、使用数组来模拟
* 2、定义一个 top 来表示栈顶,初始化 为 -1
* 3、入栈的操作,当有数据加入到栈时, top++; stack[top] = data;
* 4、出栈的操作, int value = stack[top]; top--, return value
* 接下来的代码实现,首先先定义一个类,使用数组模拟
* 分别把栈空、栈满、入栈、出栈和遍历栈的方法
```JAVA
/**
* description
* 定义一个类,使用数组模拟栈结构
* @author
* @since 2022/10/25 10:01
*/
public class ArrayStack {
private int maxSize; //栈的大小
private int [] stack; //数组,数组模拟栈,数据就放在该数组中
private int top = -1; //top表示栈顶,初始值为-1
//构造器
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//模拟栈满
public boolean isFull() {
return top == maxSize - 1;
}
//模拟栈空
public boolean isEmpty() {
return top == -1;
}
//模拟入栈
public void push(int value) {
//先判断栈是否满
if(isFull()){
System.out.println("栈满");
}
top++;
stack[top] = value;
}
//模拟出栈,将栈顶的数据返回
public int pop() {
//先判断栈是否空
if (isEmpty()){
//抛出异常
throw new RuntimeException("栈空,没有数据");
}
//先取得栈顶的值
int value = stack[top];
top--;
return value;
}
//显示栈的情况(遍历栈),由于栈是先入后出,所以要从栈底开始遍历
public void list(){
if (isEmpty()){
System.out.println("栈空,没有数据");
}
//需要从栈顶开始显示数据
for (int i = top; i>0; i--){
System.out.printf("stack[%d]=%d\n",stack[i]);
}
}
}
```
* 接下来进行测试,测试各项功能是否正常
```JAVA
public class ArrayStackDemo {
public static void main(String[] args) {
//测试ArrayStack是否正确,先创建一个ArrayStack的对象
ArrayStack arrayStack = new ArrayStack(4);
String key = "";
boolean loop = true; //此变量用于是否退出菜单
Scanner scanner = new Scanner(System.in); //拿到一个扫描器
//使用while循环模拟输入菜单
while (loop){
System.out.println("show:表示显示栈");
System.out.println("exit:表示退出程序");
System.out.println("push:表示添加数据到栈(入栈)");
System.out.println("pop:表示从栈取出数据(出栈)");
System.out.println("请输入你的选择");
key = scanner.next(); //使用key来快速接收扫描器
switch (key){
case "show":
arrayStack.list();
break;
case "push":
System.out.println("请输入一个数");
int value = scanner.nextInt(); //使用一个变量用于接收
arrayStack.push(value);
break;
case "pop":
try { //由于调用pop时可能会出现异常,使用try/catch进行捕获
int res = arrayStack.pop();
System.out.printf("出栈的数据是%d\n", res);
} catch (Exception e){
System.out.println(e.getMessage()); //若有异常输出信息即可
}
break;
case "exit":
scanner.close(); //关闭接收器资源流
loop = false; //让loop = false即可停止循环
break;
default:
break;
}
}
System.out.println("程序已退出");
}
}
```
* 经测试功能正常
* 接下来是使用链表实现,即使用链表模拟栈的思路分析:
* 1、使用链表来模拟
* 2、定义一个top来表示栈顶,初始化为-1
* 3、入栈的操作,当有数据加入到栈时,top++;stack[top] = data;
* 4、出战的操作,int = value = stack[top]; top--, return value
* 接下来的代码实现,定义一个类使用数组模拟
* 分别把栈空、栈满、入栈、出战和遍历输出栈的方法(提示:链表有倒序输出的方法)
* 代码实现入下:先定义出节点类,该类包含三个属性:节点编号、节点数据以及next指针,并重写toString方法。
```JAVA
/**
* description
* 定义一个类,使用链表模拟栈结构
* @author
* @since 2022/10/25 15:25
*/
public class HeroStack {
private int id; //节点编号
private int value; //节点数据
private HeroStack next; //指向下一个节点
//get and set
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public HeroStack getNext() {
return next;
}
public void setNext(HeroStack next) {
this.next = next;
}
//构造器
public HeroStack(int id, int value) {
this.id = id;
this.value = value;
}
@Override
public String toString() {
return "LinkedListStack{" +
"id=" + id +
", value=" + value +
'}';
}
}
```
* 定义一个LinkedListStack栈。在构造方法中传入maxLength属性,对栈的最大容量进行定义。定义入栈、出栈以及显示栈的方法
```JAVA
package com.xujicheng.stack;
/**
* description
* 栈的最大容量进行定义。定义入栈、出栈以及显示栈的方法
* @author
* @since 2022/10/25 15:39
*/
public class LinkedListStack {
//首先初始化一个头节点,头节点不要动,不用于存放具体的数据
private HeroStack head = new HeroStack(-1,0);
private int top = -1; //定义栈顶指针
int maxSize; //定义最大长度
//在构造方法内传入栈的最大容量
public LinkedListStack(int maxSize) {
this.maxSize = maxSize;
}
//判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
//判断栈是否满
public boolean isFull() {
return top == maxSize-1;
}
//模拟入栈的方法
public void push(int value){
HeroStack heroStack = new HeroStack(top,value); //新建一个top节点
//判断栈是否满了
if (isFull()){
System.out.println("栈已满,无法添加");
}
top++;
if (head.getNext() == null){ //说明暂时没有节点,可以直接添加
head.setNext(heroStack);//将新建的节点添加到头节点的后面
}else {
//如果链表已有节点,则在头节点和原来的第一个节点中间插入新的节点(倒插)
heroStack.setNext(head.getNext());
head.setNext(heroStack);
}
}
//模拟出栈
public int pop(){
//先判断栈是否为空
if (isEmpty()){
//抛出异常
throw new RuntimeException("栈空,没有数据");
}
//不为空,取出栈顶的值
int value = head.getNext().getValue();
head.setNext(head.getNext().getNext());
top--;
return value;
}
//显示栈
public void show(){
//判断栈是否为空
if (isEmpty()){
System.out.println("栈空,无法显示数据");
}
HeroStack temp = head.getNext(); //不能写成HeroNode temp = head;否则会显示头节点
//遍历链表
while (true){
System.out.println(temp);
/*
如果push方法中写成heroNode = head.getNext(),则head的下一个节点仍是null而不是heroNode,
即把null赋给了temp,这会导致下面的if语句中的temp.getNext()出现空指针异常*/
if (temp.getNext() == null){
break;
}
//如果没有找到最后就将temp后移
temp = temp.getNext();
}
}
}
```
* 定义测试类
```JAVA
package com.xujicheng.stack;
import java.util.Scanner;
/**
* description
* 课堂练习,将老师写的程序改成使用链表来模拟栈。
* 使用倒插法添加节点
* @author
* @since 2022/10/25 16:25
*/
public class LinkedListStackDemo {
public static void main(String[] args) {
//测试一下LinkedListStack是否正确
//先创建一个LinkedListStack对象表示栈
LinkedListStack stack = new LinkedListStack(3);
String key = "";
boolean loop = true;//控制是否退出菜单
Scanner scanner = new Scanner(System.in);
System.out.println("show:表示显示栈");
System.out.println("exit:表示退出程序");
System.out.println("push:表示添加数据到栈");
System.out.println("pop:表示从栈中取出数据");
while (loop) {
System.out.print("\n请输入你的选择:");
key = scanner.next();
switch (key) {
case "show":
stack.show();
break;
case "push":
System.out.print("请输入一个数:");
int value = scanner.nextInt();
stack.push(value);
break;
case "pop":
try {
int res = stack.pop();
System.out.println("出栈的数据是:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "exit":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
```
## 5.5 、栈实现综合计算器(中缀表达式)
> 使用栈来实现综合计算器
![image-20221028123053510](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221028123053510.png)
> 思路分析(图解)
![image-20221028125037591](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221028125037591.png)
> * 思路分析:如何使用栈完成计算一个表达式的结果?(由于原表达式过长,演示需要时间所以换成第二个表达式)
> * 1、通过一个index值(索引),来遍历我们的表达式
> * 2、如果发现是一个数字,就直接入数栈
> * 3、如果发现扫描到的是一个符号,就分成以下两种情况
> * 3.1、如果发现当前的符号栈为空,就直接入栈
> * 3.2、如果符号栈有操作符时,就与栈中的操作符进行比较,**如果当前的操作符的优先级小于或者等于栈中的操作符**,这时就需要从数栈中pop中两个数,再重符号栈中pop中符号(即运算符)进行运算,将得到的运行结果,入数栈,然后将当前的操作符入符号栈,**如果当前的操作符的优先级大于栈中的操作符**,就直接入栈(符号栈)。
> * 4、当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运算
> * 5、最后数栈只有一个数组就是这个表达式的结果
> * 验证表达式:3+2*6-2 = ?
> 代码实现:[1. 先实现一位数的运算, 2. 扩展到多位数的运算]
>
> * 这里复用前面使用数组模拟栈的代码,并在原功能上进行加强
> * 即加上运算符优先级、判断是不是运算符和计算的方法
```JAVA
public class ArrayStack2 {
private int maxSize; //栈的大小
private int[] stack; //数组,数组模拟栈,数据就放在该数组中
private int top = -1; //top表示栈顶,初始值为-1
//构造器
public ArrayStack2(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//模拟栈满
public boolean isFull() {
return top == maxSize - 1;
}
//模拟栈空
public boolean isEmpty() {
return top == -1;
}
//模拟入栈
public void push(int value) {
//先判断栈是否满
if (isFull()) {
System.out.println("栈满");
}
top++;
stack[top] = value;
}
//模拟出栈,将栈顶的数据返回
public int pop() {
//先判断栈是否空
if (isEmpty()) {
//抛出异常
throw new RuntimeException("栈空,没有数据");
}
//先取得栈顶的值
int value = stack[top];
top--;
return value;
}
//显示栈的情况(遍历栈),由于栈是先入后出,所以要从栈底开始遍历
public void list() {
if (isEmpty()) {
System.out.println("栈空,没有数据");
}
//需要从栈顶开始显示数据
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
//返回运算符的优先级,优先级是程序员来确定的,优先级使用数字表示,数字越大,优先级越高
public int priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1; //假定目前表达式只有加减乘除四种运算符
}
}
//判断是不是一个运算符的方法
public boolean isOper(char val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}
//计算方法
public int cal(int num1,int num2,int oper){
int result = 0; //用于存放计算的结果
switch (oper){
case '+':
result = num1 + num2;
break;
case '-':
result = num2 - num1;
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num2 / num1;
break;
}
return result;
}
//可以返回当前栈顶的值,但是不是真正的出栈
public int peek(){
return stack[top];
}
}
```
> 接下来就开始测试了
```JAVA
public class Calculator {
public static void main(String[] args) {
//根据思路,完成表达式的运算
String expression = "3+2*6-2";
//创建两个栈,一个数栈一个符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
//定义需要的相关变量
int index = 0; //用于扫描表达式中的数字和运算符
int num1 = 0; //计算的数字1
int num2 = 0; //计算的数字2
int oper = 0; //运算符
int result = 0; //接收结果
char ch = ' '; //将每次扫描得到的char保存到ch中
//开始使用while循环扫描expression
while (true){
//一次得到expression里的每一个字符
ch = expression.substring(index,index+1).charAt(0);
//判断ch是什么然后做相应的处理
if (operStack.isOper(ch)){ //如果是运算符
//判断当前符号栈是否为空
if (!operStack.isEmpty()){
//如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符
if (operStack.priority(ch)<= operStack.priority(operStack.peek())){
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
result = numStack.cal(num1, num2,oper);
//把运算的结果入数栈
numStack.push(result);
//把当前的操作符入符号栈
operStack.push(ch);
} else {
//如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈
operStack.push(ch);
}
}else {
//如果为空直接入符号栈
operStack.push(ch);
}
}else { //如果是数,则直接入数栈
numStack.push(ch-48);
}
//让index+1,并判断是否扫描到expression最后
index++;
if (index>=expression.length()){
break;
}
}
//当表达式扫描完毕,就顺序从数栈和符号栈中pop出相应的数和符号,并运行
while (true){
//如果符号栈为空,则计算到最后的结果,数栈中只有一个数字[结果]
if (operStack.isEmpty()){
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
result = numStack.cal(num1, num2,oper);
numStack.push(result); //入栈
}
//将数栈的最后数pop出来,就是结果
int result2 = numStack.pop();
System.out.printf("表达式%s = %d",expression,result2);
}
}
```
> 经测试,完成了单位数运算
![image-20221028152718823](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221028152718823.png)
> 但是又有一个新问题,即双位数参与运算就会导致运算结果不正确
![image-20221028152828440](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221028152828440.png)
> 所以要对代码进行再次优化,即在扫描数时加上逻辑
```JAVA
else { //如果是数,则直接入数栈
/*
numStack.push(ch-48);
分析思路
1、当处理多位数时,不能发现是一个数就立即入栈,因为它可能是多位数
2、在处理数时,需要向expression的表达式的后面index后再看一位,如果是数就继续扫描,如果是符号才入栈
3、因此我们需要定义一个字符串变量,用于拼接
处理多位数
*/
keepNum += ch;
//如果ch以及是expression的最后一位,就直接入栈
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepNum));
} else {
//判断下一个字符是不是数字,如果是数字就继续扫描,如果是运算符则入栈,注意是看后一位,不是index++
if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) {
//如果后一位是运算符则入栈
numStack.push(Integer.parseInt(keepNum));
//重要的清空,keepNum清空
keepNum = "";
}
}
}
```
## 5.6 、逆波兰计算器
> 我们完成一个逆波兰计算器,要求完成如下任务:
>
> 1) 输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果
>
> 2) 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
>
> 3) 思路分析
>
> * 例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
> * 1.从左至右扫描,将 3 和 4 压入堆栈;
> * 2.遇到+运算符,因此弹出 4 和 3(4 为栈顶元素,3 为次顶元素),计算出 3+4 的值,得7,再将7 入栈;
> * 3.将 5 入栈;
> * 4.接下来是×运算符,因此弹出 5 和 7,计算出 7×5=35,将 35 入栈;
> * 5.将 6 入栈;
> * 6.最后是-运算符,计算出 35-6 的值,即 29,由此得出最终结果
>
> 4) 代码实现:
```JAVA
public class PolandNotationStack {
public static void main(String[] args) {
//1、先定义一个逆波兰表达式, (3+4) * 5 -6 --> 3 4 + 5 * 6 - (使用空格隔开)
String suffixExpression = "3 4 + 5 * 6 -";
//测试结果
List<String> list = getListString(suffixExpression);
int result = calculate(list);
System.out.println("计算的结果=" + result);
}
//2、定义一个方法,依次将数据和运算符放入 ArrayList中
public static List<String> getListString(String suffixExpression) {
//3、将逆波兰表达式进行分隔(即使用空格进行分隔)
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<String>();
//4、使用for循环对数组split进行遍历,每循环一次就取出字符串中的一个元素
for (String ele : split) {
list.add(ele);
}
//4.1、遍历完成后再将数组返回
return list;
}
//4.2、创建一个栈
public static int calculate(List<String> list) {
Stack<String> stack = new Stack<>();
//4.3、遍历
for (String item : list) {
//5.1、从左至右扫描,使用正则表达式取出数
if (item.matches("\\d+")) { //代表匹配的是多位数
//5.2、扫描到元素内有数直接入栈
stack.push(item);
} else {
//6、遇到运算符,因此pop栈顶元素和为次顶元素,计算出值
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int result = 0; //6.1、定义一个存放结果的值
if (item.equals("+")) { //加法
result = num1 + num2;
} else if (item.equals("-")) { //减法
result = num1 - num2;
} else if (item.equals("*")) { //乘法
result = num1 * num2;
} else if (item.equals("/")) { //除法
result = num1 / num2;
} else {
//若不是加减乘除就抛出异常
throw new RuntimeException("运算符有误");
}
//6.1、再将计算结果入栈
stack.push(result + "");
}
}
//7、最后留在栈中的数就是结果
return Integer.parseInt(stack.pop());
}
}
```
## 5.7 、中缀表达式转换为后缀表达式
> 后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式。
### 5.7.1、具体步骤如下
> * 1) 初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2;
> * 2) 从左至右扫描中缀表达式;
> * 3) 遇到操作数时,将其压 s2;
> * 4) 遇到运算符时,比较其与 s1 栈顶运算符的优先级:
> * 1.如果 s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
> * 2.否则,若优先级比栈顶运算符的高,也将运算符压入 s1;
> * 3.否则,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4-1)与 s1 中新的栈顶运算符相比较;
> * 5) 遇到括号时:
> * (1) 如果是左括号“(”,则直接压入 s1
> * (2) 如果是右括号“)”,则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到左括号为止,此时将这一对括号丢弃
> * 6) 重复步骤 2 至 5,直到表达式的最右边
> * 7) 将 s1 中剩余的运算符依次弹出并压入 s2 8) 依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
> * 8) 依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
### 5.7.2、举例说明:
> 将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下 因此结果为 :"1 2 3 + 4 × + 5 –"
| 扫描到的元素 | s2(栈底->栈顶) | s1(栈底->栈顶) | 说明 |
| :----------: | :--------------: | :--------------: | :-----------------------------------: |
| 1 | 1 | 空 | 数组,直接入栈 |
| + | 1 | + | s1为空,运算符直接入栈 |
| ( | 1 | +( | 左括号,直接入栈 |
| ( | 1 | +(( | 左括号,直接入栈 |
| 2 | 1 2 | +(( | 数字 |
| + | 1 2 | +((+ | s1栈顶为左括号,运算符直接入栈 |
| 3 | 1 2 3 | +((+ | 数字 |
| ) | 1 2 3 + | +( | 右括号,弹出运算符直至遇到左括号 |
| * | 1 2 3 + | +( * | s1栈顶为左括号,运算符直接入栈 |
| 4 | 1 2 3 + 4 | +( * | 数字 |
| ) | 1 2 3 + 4 * | + | 右括号,弹出运算符直至遇到左括号 |
| - | 1 2 3 + 4 * + | - | - 于+ 优先级相同,因此弹出+,再压入 - |
| 5 | 1 2 3 + 4 * +5 | - | 数字 |
| 到达最右端 | 1 2 3 + 4 * +5 - | 空 | s1中剩余的运算符 |
### 5.7.3、代码实现中缀表达式转为后缀表达式
> 思路分析:
>
> **中缀表达式** **1 + ( ( 2 + 3 )× 4) - 5** **=》** **后缀表达式**
>
> **将** **s2** **出栈** **- 5 + \* 4 + 3 2 1 =>** **1 2 3 + 4** ***** **+ 5 -**
>
> **中缀表达式** **转 后缀表达式的 思路步骤分析**
>
> **打比方 : 降龙十八掌 :学习** **-》** **应用** **[**层次**]**
>
> **算法** **-》第一个层面: 理解算法-》灵活运用算法**
>
> **第二层: 设计算法**-》 **运用** **【】**
>
> 1) 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
>
> 2) 从左至右扫描中缀表达式;
>
> 3) 遇到操作数时,将其压s2;
>
> 4) 遇到运算符时,比较其与s1栈顶运算符的优先级:
>
> 1.如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
>
> 2.否则,若优先级比栈顶运算符的高,也将运算符压入s1;
>
> 3.否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较;
>
> 5) 遇到括号时:
> (1) 如果是左括号“(”,则直接压入s1
> (2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
>
> 6) 重复步骤2至5,直到表达式的最右边
>
> 7) 将s1中剩余的运算符依次弹出并压入s2
>
> 8) 依次弹出s2中的元素并输出,**结果的逆序即为中缀表达式对应的后缀表达式**
>
> 代码实现入下:
```JAVA
/**
* 完成一个将中缀表达式转成后缀表达式的功能
* 说明:1、1 +((2+3)* 4)-5 --> 转成 1 2 3 + 4 * + 5 -
* 2、直接对字符串操作不方便,因此先将"1 +((2+3)* 4)-5"-->中缀表达式转换成对应的List
* 即 "1 +((2+3)* 4)-5" --> ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
*/
public class PolandNotationStackTwo {
public static void main(String[] args) {
String expression = "1+((2+3)*4)-5";
//测试功能是否正常
List<String> list = toInfixExpressionList(expression);
System.out.println(list); //[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
}
//1、首先编写一个方法,将中缀表达式转成对应的List
public static List<String> toInfixExpressionList(String s) {
//2、定义一个List,存放中缀表达式对应的内容
List<String> list = new ArrayList<>();
int i = 0; //用于遍历中缀表达式字符串
String str; //用于对多位数的拼接
char c; //每遍历到一个字符,就放入到c中
do {
//如果c是非数字,就需要加入到list中
if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
list.add("" + c);
i++; //i需要后移
} else { //如果是一个数,需要考虑多位数的问题
str = ""; //先将str置空
while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) {
str += c; //拼接
i++;
}
list.add(str);
}
} while (i < s.length());
return list; //返回
}
}
/**
* description
* 可以返回一个运算符对应的优先级
*
* @author
* @since 2022/11/18 10:04
*/
public class Operation {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
//编写一个方法,返回对应的优先级数字
public static int getValue(String operation) {
int result = 0;
switch (operation){
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
System.out.println("不存在该运算符");
}
return result;
}
}
/**
* 完成一个将中缀表达式转成后缀表达式的功能
* 说明:1、1 +((2+3)* 4)-5 --> 转成 1 2 3 + 4 * + 5 -
* 2、直接对字符串操作不方便,因此先将"1 +((2+3)* 4)-5"-->中缀表达式转换成对应的List
* 即 "1 +((2+3)* 4)-5" --> ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
*/
public class PolandNotationStackTwo {
public static void main(String[] args) {
String expression = "1+((2+3)*4)-5";
//测试功能是否正常
List<String> list = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List=" +list); //[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
List<String> parseSuffixExpressionList = parseSuffixExpressionList(list);
System.out.println("后缀表达式对应的List= "+ parseSuffixExpressionList);
}
//1、首先编写一个方法,将中缀表达式转成对应的List
public static List<String> toInfixExpressionList(String s) {
//2、定义一个List,存放中缀表达式对应的内容
List<String> list = new ArrayList<>();
int i = 0; //用于遍历中缀表达式字符串
String str; //用于对多位数的拼接
char c; //每遍历到一个字符,就放入到c中
do {
//如果c是非数字,就需要加入到list中
if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
list.add("" + c);
i++; //i需要后移
} else { //如果是一个数,需要考虑多位数的问题
str = ""; //先将str置空
while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) {
str += c; //拼接
i++;
}
list.add(str);
}
} while (i < s.length());
return list; //返回
}
//3、将得到的中缀表达式对应的List转成后缀表达式对应的List
public static List<String> parseSuffixExpressionList(List<String> list) {
//4、定义两个栈
Stack<String> s1 = new Stack<>(); //符号栈
//4.1、说明:因为s2在整个转换过程中没有pop操作,而且最后还需要逆序输出,因此为了简化使用List替代
List<String> s2 = new ArrayList<String>(); //存放中间结果
//5、遍历中缀表达式对应的List
for (String item : list) {
//6、如果是一个数,加入到s2,使用正则表达式判断是否是数
if (item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
//6.1如果是左括号,直接入s1符号栈
s1.push(item);
} else if (item.equals(")")) {
//6.2、 如果是右括号,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop(); //将( 弹出 s1这个栈,消除小括号
} else {
//6.3、当item的优先级小于等于s1栈顶运算符的优先级,将s1栈顶的运算符弹出并加入到s2中,再次转到与s1中新的栈顶运算符相比较
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
//6.4、还需要将item压入栈中
s1.push(item);
}
}
//7、将s1剩余的运算符依次弹出并加入s2中
while (s1.size() != 0){
s2.add(s1.pop());
}
return s2; //注意,因为是存放到List中,因此按顺序输出就是后缀表达式对应的List
}
}
```
# 第6 章、递归
## 6.1 、递归应用场景
> 看个实际应用场景,迷宫问题(回溯), 递归(Recursion)
>
> ![image-20221118104450996](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221118104450996.png)
## 6.2、 递归的概念
> * 简单的说: **递归就是方法自己调用自己**,每次调用时**传入不同的变量**.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
## 6.3 、递归调用机制
> 列举两个小案例来理解递归
>
> * 1) 打印问题
> * 2) 阶乘问题
> * 3) 使用图解方式说明了递归的调用机制
>
> 打印问题的代码实现:
```JAVA
/**
* description
* 递归案例测试--打印问题
*
* @author
* @since 2022/11/18 11:12
*/
public class RecursionTest {
public static void main(String[] args) {
//通过打印的案例了解递归的调用机制
test(4);
}
public static void test(int n){
if (n> 2){
test(n -1);
}
System.out.println("n=" + n);
}
}
```
> 代码分析:程序执行时会先进入主方法
>
> * 根据Java虚拟机的结构分为三个部分 ---栈、堆、代码区(常量也是放在这的)
>
> * 当程序执行到主方法时,首先会在栈里开辟一个独立的空间
>
> * 由此得出递归调用规则
>
> * 1、当程序执行到一个方法时,就会开辟一个独立的空间(栈)
> * 2、 每个空间的数据(局部变量)是独立的
>
> ![image-20221118112600408](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221118112600408.png)
>
> 图解:首先当程序走到main方会在栈内开辟相应的独立空间执行到相应的独立空间时都会执行一条打印语句直至退出
> 2)通过阶乘问题来了解递归机制
>
> 以下是代码实现
```JAVA
/**
* description
* 递归案例测试--阶乘问题
*
* @author
* @since 2022/11/18 11:12
*/
public class RecursionTest {
public static void main(String[] args) {
int result = factorial(3);
System.out.println("result=" +result);
}
public static int factorial(int n) {
if (n == 1) {
return 1;
} else {
return factorial(n - 1) * n;
}
}
}
```
> 代码分析:当程序执行到主方法时,首先会在栈里开辟一个独立的空间,且每个空间的数据(局部变量)是独立的
>
> * 那么我们可以结合案例得出结论
> * 代码中n = 3显然是大于1的,所以只有2和3进入到else,进入后变成了两个个整体,即factorial(2-1) * factorial(3-1)* n ,得出表达式就是 1 * 2 * 3 ,结果自然是六
> * 大白话理解就是,栈都还是那个栈,但是局部变量都是独立的
## 6.4 、递归能解决什么样的问题
> 递归用于解决什么样的问题
>
> 1) 各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
>
> 2) 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
>
> 3) 将用栈解决的问题-->第归代码比较简洁
6.5 、递归需要遵守的重要规则
> 递归需要遵守的重要规则
>
> * 1) 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
> * 2) 方法的局部变量是独立的,不会相互影响, 比如 n 变量
> * 3) 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
> * 4) 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError,死龟了:)
> * 5) 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
## 6.6 、递归-迷宫问题
### 6.6.1、迷宫问题
![image-20221119081431332](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221119081431332.png)
### 6.6.2、代码实现:
```JAVA
/**
* description
* 使用递归解决迷宫回溯问题
*
* @author
* @since 2022/11/19 8:35
*/
public class Maze {
public static void main(String[] args) {
//先创建一个二维数组,模拟迷宫
int[][] map = new int[8][7];
//使用1表示墙,上下全部置为1
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
//把左右的墙也置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
//设置相应的挡板
map[3][1] = 1;
map[3][2] = 1;
//输出这个地图
System.out.println("当前地图情况");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
//使用递归回溯给小球找路
setWay(map,1,1);
//输出新的地图,小球走过并标示过的地图
System.out.println("小球走过并标示过的地图情况");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
/**
* 说明:
* 1、map表示地图
* 2、i,j 表示地图的哪个位置开始出发(1,1)
* 3、如果小球能到 map[6][5]位置,则说明通路找到
* 4、约定:当map[i][j] 为0时表示还没有走过,当为1时表示墙,若为2表示通路可以走;若为3表示走过的路
* 5、在走迷宫之前需要提前确定路线:依照顺序依次走 下→右→上→左,如果该点走不通再回溯
* 使用递归回溯来给小球找路
*
* @param map 表示地图
* @param i 从哪个位置开始找
* @param j 同上
* @return 如果找到路了返回true,反之返回false
*/
public static boolean setWay(int[][] map, int i, int j) {
if (map[6][5] == 2) { //通路已经找到
return true;
} else {
if (map[i][j] == 0) { //如果当前点还没有走过
//按照策略走下→右→上→左
map[i][j] = 2; //假定该点是可以走通的
if (setWay(map, i + 1, j)) { //向下走
return true;
} else if (setWay(map, i, j + 1)) {
return true;
} else if (setWay(map, i - 1, j)) {
return true;
} else if (setWay(map, i, j - 1)) {
return true;
} else {
//说明该点是死路,走不通
map[i][j] = 3;
return false;
}
} else { //如果map[i][j]!=0 ,可能是1,2,3
return false;
}
}
}
}
```
> 代码说明:
>
> * 1、map表示地图
> * 2、i,j 表示地图的哪个位置开始出发(1,1)
> * 3、如果小球能到 map [ 6 ] [ 5 ] 位置,则说明通路找到
> * 4、约定:当map [ i ] [ j ] 为0时表示还没有走过,当为1时表示墙,若为2表示通路可以走;若为3表示走过的路
> * 5、在走迷宫之前需要提前确定路线:依照顺序依次走 下→右→上→左,如果该点走不通再回溯
> * 6、小球的走的路径与定制的策略有关
## 6.7 、递归-八皇后问题(回溯算法)
### 6.7.1、八皇后问题介绍
> 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848 年提出:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即:**任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)。**
![image-20221120090856757](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120090856757.png)
### 6.7.2、八皇后问题算法思路分析
> 1) 第一个皇后先放第一行第一列
>
> 2) 第二个皇后放在第二行第一列、然后判断是否 OK, 如果不 OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适
>
> 3) 继续第三个皇后,还是第一列、第二列……直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确解
>
> 4) 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到. 5) 然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4 的步骤
>
> 6) 思路分析
>
> * 1、 第一个皇后先放第一行第一列
> * 2、第二个皇后放在第二行第一列、然后判断是否OK[即判断是冲突], 如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适
> * 3、继续第三个皇后,还是第一列、第二列……直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解
> * 4、当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到
> * 5、然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4的步骤
* 说明:
* 理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] ={0 , 4, 7, 5, 2, 6, 1, 3} //对应 arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第i+1 个皇后,放在第i+1行的第 val+1 列
### 6.7.3、八皇后问题算法代码实现
```JAVA
/**
* description
* 使用回溯算法解决八皇后问题
*
* @author
* @since 2022/11/20 9:35
*/
public class Queue {
//1、先定义一个max表示共有多少个皇后
int max = 8;
//2、再定义一个数组array,用于保存放置皇后位置的结果
int[] array = new int[max];
//3、定义一个变量count来同居共有多少种写法
static int count = 0;
public static void main(String[] args) {
//测试一把
Queue queue = new Queue();
queue.check(0);
System.out.printf("一共有%d种解法", count);
}
//3、编写一个方法,放置第n个皇后
private void check(int n) {
if (n == max) { //n =8
point();
return;
}
//3.1、依次放入皇后并判断是否冲突
for (int i = 0; i < max; i++) {
//3.2、先把当前皇后 n ,放到该行的第一列
array[n] = i;
//3.3、判断当放置第n个皇后到i列时是否冲突
if (judge(n)) { //不冲突
//3.3、接着放n+1个皇后,即开始递归
check(n + 1);
}
//3.4、如果冲突就继续执行array[n] = i,即将第n个皇后放置在本行的后移的一个位置
}
}
/**
* 4、定义一个方法查看当我们放置第n个皇后,就去检测该皇后是否和前面已经摆放的皇后冲突
*
* @param n 表示第n个皇后
* @return
*/
private boolean judge(int n) {
for (int i = 0; i < n; i++) {
/**
* 4.1、array[i] == array[n] 表示判断第n个皇后是否和前面的n-1个皇后在同一列
* 4.2、Math.abs(n-i) == Math.abs(array[n] - array[i] 表示判断第n个皇后是否和第i个皇后是否在同一斜线
*/
if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) {
return false;
}
}
return true;
}
//4、定义一个方法,将皇后摆放的位置输出
private void point() {
count++;
for (int j : array) {
System.out.print(j + " ");
}
System.out.println();
}
}
```
# 七、排序算法
## 7.1 、排序算法的介绍
* 排序也称排序算法(Sort Algorithm),排序是将**一组数据**,依**指定的顺序**进行**排列的过程**
## 7.2 、排序的分类:
1) 内部排序: 指将需要处理的所有数据都加载到**内部存储器(内存)**中进行排序。
2) 外部排序法: **数据量过大**,无法全部加载到内存中,需要借助**外部存储(文件等)**进行排序。
3) 常见的排序算法分类(见右图):
![image-20221120134417433](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120134417433.png)
## 7.3 、算法的时间复杂度
### 7.3.1、度量一个程序(算法)执行时间的两种方法
> * 1) 事后统计的方法 这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较哪个算法速度更快
> * 2) 事前估算的方法:通过分析某个算法的**时间复杂度**来判断哪个算法更优
### 7.3.2、时间频度
> * 基本介绍
>
> 时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。**一个算法中的语句执行次数**称为语句频度或时间频度。记为 T(n)。
>
> * 比如计算 1-100 所有数字之和, 我们设计两种算法:
>
> ![image-20221120132147338](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132147338.png)
>
> * 忽略常数项
>
> ![image-20221120132118782](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132118782.png)
>
> 结论:
>
> 1) 2n+20 和 2n 随着 n 变大,执行曲线无限接近, 20 可以忽略
>
> 2) 3n+10 和 3n 随着 n 变大,执行曲线无限接近, 10 可以忽略
>
> * 忽略低次项
>
> ![image-20221120132211692](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132211692.png)
>
> 结论:
>
> 1) 2n^2+3n+10 和 2n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 3n+10
>
> 2) n^2+5n+20 和 n^2 随着 n 变大,执行曲线无限接近, 可以忽略 5n+20
>
> * 忽略系数
>
> ![image-20221120132229380](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132229380.png)
>
> 结论:
>
> 1) 随着 n 值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明 这种情况下, 5 和3 可以忽略。
>
> 2) 而 n^3+5n 和 6n^3+4n ,执行曲线分离,说明多少次方式关键
### 7.3.3、时间复杂度
> 1) 一般情况下,**算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数**,用T(n)表示,若有某个辅助函数 f(n),使得当 n 趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 **T(n)=O( f(n) )**,称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
>
> 2) T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为 **O(n²)**。
>
> 3) 计算时间复杂度的方法:
>
> * 用常数 1 代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1
> * 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²
> * 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)
### 7.3.4、常见的时间复杂度
> 1) 常数阶 O(1)
>
> 2) 对数阶 O(log2n)
>
> 3) 线性阶 O(n)
>
> 4) 线性对数阶 O(nlog2n)
>
> 5) 平方阶 O(n^2)
>
> 6) 立方阶 O(n^3)
>
> 7) k 次方阶 O(n^k)
>
> 8) 指数阶 O(2^n)
>
>
>
> **常见的时间复杂度对应的图:**
>
> ![image-20221120131937606](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120131937606.png)
>
> **说明:**
>
> 1) 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<Ο(nk) <Ο(2n) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低
>
> 2) 从图中可见,我们应该尽可能避免使用指数阶的算法
>
> * 1、常数阶 O(1)
> * 无论代码执行力多少行,只要是没有循环等复杂结构,那么走过代码的时间复杂度就都是O(1)
>
> ![image-20221120131926441](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120131926441.png)
>
> 上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万行甚至几十万行,都可以用O(1)来表示它的时间复杂度
>
> * 2、 对数阶 O(log2n)
>
> ![image-20221120132332267](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132332267.png)
>
> **说明**:在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = **log**2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log**2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(log3n**) .
>
> 如果N=a(a>0,a*1),即a的x次方等于N (a>0,且af1),那么数x叫做以a为底N的对数(lgarithm),记作log。 。其中,a叫做对数的底数,N叫做真数,x叫做"以a为底N的对数”。
>
> * 3、**线性阶**O(n)
>
> ![image-20221120132513982](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132513982.png)
>
> **说明**:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度
>
> * 4、**线性对数阶**O(**nlogN**)
>
> ![image-20221120132633874](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132633874.png)
>
> **说明**:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)
>
> * 5)**平方阶**O(n²)
>
> ![image-20221120132706908](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221120132706908.png)
>
> **说明**:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n)
>
> * 6、**立方阶*O(n³)、*K次方阶O(n^k)**
>
> **说明**:参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似
### 7.3.5、平均时间复杂度和最坏时间复杂度
****
> 1) 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
>
> 2) 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
>
> 3) 平均时间复杂度和最坏时间复杂度是否一致,和算法有关(如表所示:)。
>
> | 排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
> | :----: | :------: | :-----------: | :----: | :------: | :--------------------------: |
> | 冒泡 | O(n²) | O(n²) | 稳定 | O(1) | n小时较好 |
> | 交换 | O(n²) | O(n²) | 不稳定 | O(1) | n小时较好 |
> | 选择 | O(n²) | O(n²) | 不稳定 | O(1) | n小时较好 |
> | 插入 | O(n²) | O(n²) | 稳定 | O(1) | 大部分已排序时较好 |
> | 基数 | O(logRB) | O(logRB) | 稳定 | O(n) | B是真数(0-9),R是基数(个十百) |
> | Shell | O(nlogn) | O(n²) 1< s <2 | 不稳定 | O(1) | s是所选分组 |
> | 快速 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
> | 归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
> | 堆 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
## 7.4 、算法的空间复杂度简介
### 7.4.1、基本介绍
> 1) 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。
>
> 2) 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模 n 有关,它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元,例如**快速排序和归并排序算法**, 基数排序就属于这种情况
>
> 3) 在做算法分析时,主要讨论的是时间复杂度。**从用户使用体验上看,更看重的程序执行的速度**。一些缓存产品(redis, memcache)和算法(基数排序)**本质就是用空间换时间**.
## 7.5 、冒泡排序
### 7.5.1、基本介绍
> 冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),**依次比较相邻元素的值,若发现逆序则交换**,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒
>
> 优化: 因为排序的过程中,各元素不断接近自己的位置,**如果一趟比较下来没有进行过交换,就说明序列有序**,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)
### 7.5.2、演示冒泡过程的例子(图解)
![image-20221121113353890](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221121113353890.png)
> 小结:
>
> * (1) 一共进行 数组的大小-1 次 大的循环
> * (2)每一趟排序的次数在逐渐的减少
> * (3) 如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序。这个就是优化
>
> 举一个具体的案例来说明冒泡法。我们将五个无序的数:3, 9, -1, 10, -2 使用冒泡排序法将其排成一个从小到大的有序数列。
### 7.5.3、冒泡排序应用实例
* 将五个无序的数:3, 9, -1, 10, -2 使用冒泡排序法将其排成一个从小到大的有序数列。 代码实现如下
```JAVA
/**
* description
* 冒泡排序的案例——将五个无序数组进行排序
*
* @author
* @since 2022/11/21 12:31
*/
public class BubbleSort {
public static void main(String[] args) {
//首先定义一个一维数组,并定义五个无序数字
int[] arr = {3, 9, -1, 10, -2};
int temp = 0; //临时变量
for (int i = 0; i < arr.length - 1; i++) {
//第一趟排序,将最大的数排在最后
for (int j = 0; j < arr.length -1 - i; j++) {
//如果前面的数比后面的数大,则进行交换,否则不进行处理
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第"+ (i+1) + "排序后的数组");
System.out.println(Arrays.toString(arr));
}
}
```
> 代码执行结果:
>
> ![image-20221121125426541](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221121125426541.png)
>
> 冒泡排序执行完毕,还可以进行优化,优化思路如下:
>
> * 优化: 因为排序的过程中,各元素不断接近自己的位置,**如果一趟比较下来没有进行过交换,就说明序列有序**,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)
> * 代码实现如下:
```JAVA
/**
* description
* 冒泡排序的案例——将五个无序数组进行排序
*
* @author
* @since 2022/11/21 12:31
*/
public class BubbleSort {
public static void main(String[] args) {
//首先定义一个一维数组,并定义五个无序数字
int[] arr = {3, 9, -1, 10, -2};
System.out.println("排序前=" + Arrays.toString(arr));
//测试冒泡排序
bubbleSort(arr);
System.out.println("排序后=" + Arrays.toString(arr));
}
//将冒泡排序封装成一个方法
public static void bubbleSort(int[] arr) {
int temp = 0; //临时变量
boolean flag = false; //标识变量,标识是否进行过交换
for (int i = 0; i < arr.length - 1; i++) {
//第一趟排序,将最大的数排在最后
for (int j = 0; j < arr.length - 1 - i; j++) {
//如果前面的数比后面的数大,则进行交换,否则不进行处理
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (!flag) { //在一趟排序中,一次交换都没有发生过
break;
} else {
flag = false; //重置flag,进行下一次判断
}
}
}
}
```
### 7.5.4、冒泡排序的力扣练习题——移动零
> 给定一个数组 `nums`,编写一个函数将所有 `0` 移动到数组的末尾,同时保持非零元素的相对顺序。
>
> **请注意** ,必须在不复制数组的情况下原地对数组进行操作。
>
> **示例 1:**
>
> ```bash
> 输入: nums = [0,1,0,3,12]
> 输出: [1,3,12,0,0]
> ```
>
> 思路分析
>
> * 这里参考了快速排序的思想,快速排序首先要确定一个待分割的元素做中间点x,然后把所有小于等于x的元素放到x的左边,大于x的元素放到其右边。
> * 这里我们可以用0当做这个中间点,把不等于0(注意题目没说不能有负数)的放到中间点的左边,等于0的放到其右边。
> 这的中间点就是0本身,所以实现起来比快速排序简单很多,我们使用两个指针i和j,只要nums[i]!=0,我们就交换nums[i]和nums[j]
> * 请对照动态图来理解:
>
> ![283_2.gif](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora36d1ac5d689101cbf9947465e94753c626eab7fcb736ae2175f5d87ebc85fdf0-283_2.gif)
>
> 代码实现
>
> ```java
> /**
> * 力扣练习题:移动零
> * 思路参考笔记...
> */
> public class BubbleSort {
> public void moveZeroes(int[] nums) {
> if (nums == null) {
> return;
> }
> //两个指针i和j
> int j = 0;
> for (int i = 0; i < nums.length; i++) {
> //当前元素!=0,就把其交换到左边,等于0的交换到右边
> if (nums[i] != 0) {
> int tmp = nums[i];
> nums[i] = nums[j];
> nums[j++] = tmp;
> }
> }
> }
> }
> ```
>
>
## 7.6 、选择排序
### 7.6.1、基本介绍
> 选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
### 7.6.2、选择排序思想:
> 选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从arr[0]~arr[n-1]中选取最小值,与 arr[0]交换,第二次从 arr[1]~arr[n-1]中选取最小值,与 arr[1]交换,第三次从 arr[2]~arr[n-1]中选取最小值,与arr[2]交换,…,第 i 次从 arr[i-1]~arr[n-1]中选取最小值,与 arr[i-1]交换,…, 第 n-1 次从 arr[n-2]~arr[n-1]中选取最小值,与 arr[n-2]交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。
### 7.6.3、选择排序思路分析图:
![image-20221122102502405](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221122102502405.png)
> 对一个数组的选择排序再进行讲解
>
> * 原始的数组 : 101, 34, 119, 1
>
> * 第一轮排序 : **1**, 34, 119, 101
>
> * 第二轮排序 : **1**, 34, 119, 101
>
> * 第三轮排序 : 1, 34, 101, 119
>
> 说明:
>
> 1. 选择排序一共有 数组大小 - 1 轮排序
>
> 2. 每1轮排序,又是一个循环, 循环的规则(代码)
>
> * 2.1先假定当前这个数是最小数
> * 2.2 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
> * 2.3 当遍历到数组的最后时,就得到本轮最小数和下标
### 7.6.4、选择排序应用实例
> 有一群牛 , 颜值分别是 101, 34, 119, 1 请使用选择排序从低到高进行排序 [101, 34, 119, 1]
>
> * 代码实现
```JAVA
/**
* description
* 选择排序的案例
*
* @author
* @since 2022/11/22 10:56
*/
public class SelectSort {
public static void main(String[] args) {
int[] arr = {101, 34, 119, 1};
System.out.println("排序前" + Arrays.toString(arr));
selectSort(arr);
System.out.println("排序后" + Arrays.toString(arr));
}
//选择排序
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) { //让最小数和下标为一的数进行比较
if (min > arr[j]) { //说明假定的最小值并部署最小的
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//将最小值放在arr[0],即交换
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
}
```
> 代码说明:算法思想:先简单--》 再复杂, 就是可以把一个复杂的算法,拆分成简单的问题-》逐步解决
>
> * 写代码和设计程序也是同理
## 7.7 、插入排序
### 7.7.1、插入排序法介绍
> 插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
### 7.7.2、插入排序法思想
> 插入排序(Insertion Sorting)的基本思想是:**把 n 个待排序的元素看成为一个有序表和一个无序表**,开始时**有序表中只包含一个元素**,无序表中包含有 **n-1 个元素**,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
### 7.7.3、插入排序思路图
![image-20221122114119807](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221122114119807.png)
### 7.7.4、插入排序法应用实例:
> 有一群小牛, 考试成绩分别是 101, 34, 119, 1 请从小到大排序
>
> 代码实现如下:
```JAVA
/**
* description
* 插入排序法的案例
*
* @author
* @since 2022/11/22 13:00
*/
public class InsertSoft {
public static void main(String[] args) {
int[] arr = {101, 34, 119, 1};
System.out.println("排序插入前" + Arrays.toString(arr));
insert(arr);
}
/**
* 插入排序,进行逐步推导
*
* @param arr 定义的一位数组,用于存放数据
*/
public static void insert(int[] arr) {
//使用for循环把代码简化
for (int i = 1; i < arr.length; i++) {
//首先定义待插入的数,第一轮排序{101, 34, 119, 1} --> {34, 101, 119, 1}
int insertVal = arr[i];
int insertIndex = i - 1; //即arr[1]的前面数的下标
/*给insertVal找到插入的位置, insertIndex >= 0保证在找插入位置时不越界
* insertVal < arr[insertIndex]说明待插入的数还没找到适当的插入位置
* 就需要将 arr[insertIndex] 后移 */
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex]; //将 arr[insertIndex]后移
insertIndex--;
}
//当退出while循环时,说明插入的位置找到,insertIndex +1
arr[insertIndex + 1] = insertVal;
System.out.println("第" + i + "轮插入后" + Arrays.toString(arr));
}
}
}
```
## 7.8 、希尔排序
### 7.8.1、简单插入排序存在的问题
> 我们看简单的插入排序可能存在的问题
>
> 数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(**最小**), 这样的过程是:
>
> {2,3,4,5,6,6} --> {2,3,4,5,5,6} --> {2,3,4,4,5,6} --> {2,3,3,4,5,6} --> {2,2,3,4,5,6} --> {1,2,3,4,5,6}
>
> **结论**: 当**需要插入的数是较小的数时**,**后移的次数明显增多**,对**效率**有影响.
### 7.8.2、希尔排序法介绍
> 希尔排序是希尔(Donald Shell)于 1959 年提出的一种排序算法。希尔排序也是一种**插入排序,**它是简单插入排序经过改进之后的一个**更高效的版本**,也称为**缩小增量排序**
### 7.8.3、希尔排序法基本思想
> 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,**当增量减至 1 时**,整个文件恰被分成一组,算法便终止
### 7.8.4、希尔排序法的示意图
![image-20221123090129548](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221123090129548.png)
![image-20221123090143544](040607_数据结构与算法的学习笔记.assets/image-20221123090143544.png)
### 7.8.5、希尔排序法应用实例
> 有一群小牛, 考试成绩分别是 {8,9,1,7,2,3,5,4,6,0} 请从小到大排序. 请分别使用
>
> 1) 希尔排序时, 对有序序列在插入时采用**交换法**, 并测试排序速度
>
> 2) 希尔排序时, 对有序序列在插入时采用**移动法**, 并测试排序速度
>
> 3) 代码实现
```JAVA
/**
* description
* 希尔排序中交换法的案例
*
* @author
* @since 2022/11/23 9:34
*/
public class ShellSort {
public static void main(String[] args) {
//首先定义需要排序的数组
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSort(arr);
}
/**
* 使用逐步推导的方式编写希尔排序
*
* @param arr 需要排序的数组
*/
public static void shellSort(int[] arr) {
int temp;
int count = 0; //计数器
//根据逐步分析得到规律
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
//遍历各组中所有的元素(共gap组,每组gap/2个元素),步长gap
for (int j = i - gap; j >= 0; j -= gap) {
//如果当前元素大于加上步长后的那个元素,说明需要交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
System.out.println("希尔排序第" + (++count) + "轮后数组的情况=" + Arrays.toString(arr));
}
}
}
/**
* description
* 希尔排序中移动法的案例
*
* @author
* @since 2022/11/23 10:26
*/
public class ShellSortTwo {
public static void main(String[] args) {
//首先定义需要排序的数组
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSort(arr);
}
//对交换式的希尔排序进行优化 -->移动法
public static void shellSort(int[] arr) {
//增量gap,并逐步地缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素开始,逐个对其所在的组进行直接插入
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
//移动
arr[j] = arr[j - gap];
j -= gap;
}
//当退出while循环后,就给temp找到插入的位置
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
```
## 7.9 、快速排序
### 7.9.1、快速排序法介绍
> 快速排序(Quicksort)是对**冒泡排序**的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,**整个排序过程可以递归进行,**以此达到整个数据变成有序序列
### 7.9.2、快速排序法示意图:
![image-20221124090452656](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221124090452656.png)
![image-20221124090439999](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221124090439999.png)
### 7.9.3、快速排序法应用实例:
> 要求: 对 [-9,78,0,23,-567,70] 进行从小到大的排序,要求使用快速排序法。【测试8w 和800w】说明[验证分析]:
>
> 1) 如果取消左右递归,结果是 -9 -567 0 23 78 70
>
> 2) 如果取消右递归,结果是 -567 -9 0 23 78 70
>
> 3) 如果取消左递归,结果是 -9 -567 0 23 70 78
>
> 4) 代码实现
```JAVA
/**
* description
* 快速排序的案例
*
* @author
* @since 2022/11/24 10:56
*/
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9, 78, 0, 23, -567, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println("arr=" + Arrays.toString(arr));
}
/**
* 快速排序
*
* @param arr 需要排序的数组
* @param left 左边的索引
* @param right 右边的索引
*/
public static void quickSort(int[] arr, int left, int right) {
int l = left; //左下标
int r = right; //右下标
//中轴值
int pivot = arr[(left + right) / 2];
int temp = 0; //临时变量,作为交换时使用
while (l < r) { //while循环的目的是让比pivot值小的放到左边,比pivot值大的放右边
while (arr[l] < pivot) { //在pivot左边一直找,找到大于等于pivot的值才退出
l += 1;
}
while (arr[r] > pivot) { //在pivot右边一直找,找到大于等于pivot的值才退出
r -= 1;
}
//如果 l >= r 成立,说明pivot左右的值已经按照左边全是小于等于pivot值,右边大于等于pivot值
if (l >= r) {
break;
}
//若以上条件没满足,进行交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交换完后发现arr[l] == pivot值相等r--,前移
if (arr[l] == pivot) {
r -= 1;
}
//如果交换完后发现arr[r] == pivot值相等l++,后移
if (arr[r] == pivot) {
l += 1;
}
}
//如果l == r ,必须l++,r--,否则会出现栈溢出
if (l == r) {
l += 1;
r -= 1;
}
//向左递归
if (left < r) {
quickSort(arr, left, r);
}
//向右递归
if (right > l) {
quickSort(arr, l, right);
}
}
}
```
## 7.10 、归并排序
### 7.10.1 、归并排序介绍
> 归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的**分治(divide-and-conquer)**策略(分治法将问题分(divide)成一些**小的问题然后递归求解**,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
### 7.10.2、 归并排序思想示意图 1-基本思想:
![image-20221125083510795](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125083510795.png)
### 7.10.3、 归并排序思想示意图 2-合并相邻有序子序列:
> 再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
![image-20221125084505888](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221125084505888.png)
### 7.10.4、归并排序的应用实例:
> 给你一个数组, val arr = Array(8, 4, 5, 7, 1, 3, 6, 2 ), 请使用归并排序完成排序。
>
> 代码实现
```JAVA
/**
* description
* 归并排序的案例
*
* @author
* @since 2022/11/25 8:52
*/
public class MergeSort {
public static void main(String[] args) {
int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
int[] temp = new int[arr.length]; //归并排序需要额外的空间
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println(Arrays.toString(arr));
}
/**
* 分 + 合并方法
*
* @param arr 待排序的数组
* @param left 左边有序序列的初始索引
* @param right 右边索引
* @param temp 临时数组
*/
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2; //中间的索引
//向左递归进行分解
mergeSort(arr, left, mid, temp);
//向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
//到合并
merge(arr, left, mid, right, temp);
}
}
/**
* 合并的方法
*
* @param arr 待排序的数组
* @param left 左边有序序列的初始索引
* @param right 右边索引
* @param mid 中间索引
* @param temp 临时数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; //初始化i,左边有序序列的初始索引
int j = mid + 1; //初始化j,右边有序序列的初始索引
int t = 0; //指向temp数组的当前索引
//1、先把左右两边的数据按照规则填充到temp数组,知道左右两边的有序序列有一方处理完毕为主
while (i <= mid && j <= right) {//继续
// 如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素,即将左边的元素拷贝到temp,然后t后移
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else { //反之,将右边有序序列的当前元素填充到temp数组里去
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//2、把有剩余数据的一边全部依次填充到temp
while (i <= mid) { //说明左边的有序序列还有剩余元素,就全部填充到temp里去
temp[t] = arr[i];
t += 1;
i += 1;
}
while (j <= right) { //说明右边的有序序列还有剩余元素,就全部填充到temp里去
temp[t] = arr[j];
t += 1;
j += 1;
}
//3、将temp数组的元素拷贝到arr,但并不是每一次都拷贝所有的元素
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
```
## 7.11、 基数排序
### 7.11.1、 基数排序(桶排序)介绍:
> 1) 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
>
> 2) 基数排序法是属于稳定性的排序,基数排序法的是效率高的**稳定性**排序法
>
> 3) 基数排序(Radix Sort)是桶排序的扩展
>
> 4) 基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
### 7.11.2 、基数排序基本思想
> 1) 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
>
> 2)图文解释,理解基数排序的步骤
### 7.11.3 、基数排序图文说明
> ![image-20221126104705424](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221126104705424.png)
>
> ![image-20221126104728633](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221126104728633.png)
### 7.11.4、 基数排序代码实现
> 要求:将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序
>
> 代码实现:
```JAVA
/**
* description
* 基数排序的代码实现,即将无需数组转成有序
*
* @author
* @since 2022/11/26 16:21
*/
public class RadixSort {
public static void main(String[] args) {
int[] arr = {53, 3, 542, 748, 14, 214};
radixSort(arr);
}
/**
* 基数排序的方法
*
* @param arr 需要进行排序的数组
*/
public static void radixSort(int[] arr) {
//根据推导过程,得到最终的基数排序代码,得到数组中最大的数的位数
int max = arr[0]; //假设第一个数就是最大数
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//得到最大数是几位数
int maxLength = (max + "").length();
/*
第一轮排序(针对每个元素的个位进行排序处理)
定义一个二维数组表示10个桶,每个桶就是一个一维数组
说明:1、二维数组包含十个一位数组
2、为了防止在放入数时数据溢出,则每个一维数组(桶),大小为arr.length
3、由此很明显地发现,基数排序是使用空间换时间的经典算法*/
int[][] bucket = new int[10][arr.length];
/* 为了记录每个桶中实际存放了多少个数据,我们定义一个一维数组来记录各个桶每次放入的数据个数
int[] bucketElementCounts 可以理解为,记录的就是bucket[] 桶放入的数据个数*/
int[] bucketElementCounts = new int[10];
//这里使用循环将代码处理
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
//(针对每个元素的对应的个位进行排序处理),第一次是个位,第二次是十位第三次是百位
for (int j = 0; j < arr.length; j++) {
//取出每个元素的个位的值
int digitOfElement = arr[j] / n % 10;
//放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
int index = 0; //下标
//遍历每一个桶,并将桶中的数据放入到原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
//循环第k个桶(即第k个一维数组),放入数据即可
for (int l = 0; l < bucketElementCounts[k]; l++) {
//取出元素放入到arr中
arr[index++] = bucket[k][l];
}
}
//第i+1轮处理后,需要将每个bucketElementCounts[k] = 0!
bucketElementCounts[k] = 0;
}
System.out.println("第" + (i + 1) + "轮,对个位的排序处理arr=" + Arrays.toString(arr));
}
}
}
```
### 7.11.5 、基数排序的说明:
> 1) 基数排序是对传统桶排序的扩展,速度很快.
>
> 2) 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成OutOfMemoryError 。
>
> 3) 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i]在 r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
>
> 4) 有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: https://code.i-harness.com/zh-CN/q/e98fa9
## 7.12 、常用排序算法总结和对比
### 7.12.1 、一张排序算法的比较图
![image-20221126211218351](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221126211218351.png)
### 7.12.2 、相关术语解释
> 1) 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
>
> 2) 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
>
> 3) 内排序:所有排序操作都在内存中完成;
>
> 4) 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
>
> 5) 时间复杂度: 一个算法执行所耗费的时间。
>
> 6) 空间复杂度:运行完一个程序所需内存的大小。
>
> 7) n: 数据规模
>
> 8) k: “桶”的个数
>
> 9) In-place: 不占用额外内存
>
> 10) Out-place: 占用额外内存
# 八、查找算法
## 8.1 、查找算法介绍 在 java 中,我们常用的查找有四种:
> 1) 顺序(线性)查找
>
> 2) 二分查找/折半查找
>
> 3) 插值查找
>
> 4) 斐波那契查找
## 8.2 、线性查找算法
> 有一个数列: {1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称【顺序查找】要求: 如果找到了,就提示找到,并给出下标值。
>
> 代码实现如下:
```JAVA
/**
* description
* 线性查找算法的代码实现
*
* @author
* @since 2022/11/27 16:25
*/
public class SeqSearch {
public static void main(String[] args) {
int[] arr = {1, 9, 11, -1, 34, 89}; //没有顺序的数列
int index = search(arr,-11);
if (index == -1 ){
System.out.println("没有查找到你要找的这个值");
}else {
System.out.println("找到,下标为=" + index);
}
}
/**
* 这里实现的线性查找的方法是找到满足一个条件的值就返回
*
* @param arr 需要查找的数组
* @param value 需要查找的值
* @return 查找数的下标
*/
public static int search(int[] arr, int value) {
//线性查找是逐一比对,发现有相同值,就返回下标
for (int i = 0; i < arr.length; i++) {
if (arr[i] == value){
return i;
}
}
return -1;
}
}
```
## 8.3、 二分查找算法
### 8.3.1、二分查找:
> 请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
### 8.3.2、二分查找算法的思路
![image-20221127163509876](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221127163509876.png)
> 说明:如果需要二分查找的数组是倒序,2.1和2.2的递归条件则相反
### 8.3.3、二分查找的代码实现
> 代码实现
```JAVA
/**
* description
* 二分查找的案例
*
* @author
* @since 2022/11/27 16:51
*/
public class BinarySearch {
//注意:使用二分查找的前提是:该数组是有序的
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
int resultIndex = binarySearch(arr, 0, arr.length - 1, 1); //结果索引
System.out.println("resultIndex=" + resultIndex);
}
/**
* 二分查找的算法
*
* @param arr 需要查找的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 需要查找的值
* @return 如果找到就返回值的下标,没有找到就返回-1
*/
public static int binarySearch(int[] arr, int left, int right, int findVal) {
//当left > right 时,说明递归整个数组,但是也没有找到
if (left < right) {
return -1;
}
int mid = (left + right) / 2; //中间数的下标
int midVal = arr[mid]; //中间数的值
//如果查找的值大于中间的值
if (findVal > midVal) { //向右递归
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) { //向左递归
return binarySearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
}
```
### 8.3.4、二分查找的优化
> 说明:增加了找到所有的满足条件的元素下标:
>
> 课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000
>
> 代码实现:
```JAVA
/**
* 二分查找中有多个相同的值,如何将所有数值都找到
* 思路分析
* 1、在找到mid索引值,不要马上返回
* 2、向mid索引值的左边扫描,将所有满足查找值的元素下标加入到一个集合中
* 3、向mid索引值的右边扫描,将所有满足查找值的元素下标加入到一个集合中
* 4、将集合返回即可
*
* @param arr 需要查找的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 需要查找的值
* @return 如果找到就返回值的下标,没有找到就返回-1,找到相同的数时将他们存入一个集合中
*/
public static ArrayList<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
//当left > right 时,说明递归整个数组,但是也没有找到
if (left > right) {
return new ArrayList<Integer>();
}
int mid = (left + right) / 2; //中间数的下标
int midVal = arr[mid]; //中间数的值
//如果查找的值大于中间的值
if (findVal > midVal) { //向右递归
return binarySearch2(arr, mid + 1, right, findVal);
} else if (findVal < midVal) { //向左递归
return binarySearch2(arr, left, mid - 1, findVal);
} else {
//创建一个集合,用于存放备查找的数值
ArrayList<Integer> resultIndexList = new ArrayList<Integer>();
//向mid索引值的左边扫描,将所有满足查找值的元素下标加入到一个集合中
int temp = mid - 1;
while (temp >= 0 && arr[temp] == findVal) {
//否则就把temp放到集合中
resultIndexList.add(temp);
temp -= 1; //temp左移
}
resultIndexList.add(mid);
//向mid索引值的右边扫描,将所有满足查找值的元素下标加入到一个集合中
temp = mid + 1;
while (temp <= arr.length - 1 && arr[temp] == findVal) {
//否则就把temp放到集合中
resultIndexList.add(temp);
temp += 1; //temp右移
}
return resultIndexList;
}
}
```
### 8.3.5、二分查找的力扣练习题
> 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
>
> **示例 1:**
>
> ```bash
> 输入: nums = [-1,0,3,5,9,12], target = 9
> 输出: 4
> 解释: 9 出现在 nums 中并且下标为 4
> ```
>
> 思路分析
>
> * 首先确定该数组的中间的下标
> * mid = (right - left) / 2 + left
> * 然后让需要查找的数 findVal 和 arr[mid] 比较
> * target > arr[mid] , 说明你要查找的数在mid 的右边, 因此需要递归的向右查找
> * target < arr[mid], 说明你要查找的数在mid 的左边, 因此需要递归的向左查找
> * target == arr[mid] 说明找到,就返回
> * 什么时候我们需要结束递归.
> * 找到就结束递归
> * 递归完整个数组,仍然没有找到target ,也需要结束递归 当 left <= right 就需要退出
>
> 代码实现
>
> ```JAVA
> class Solution {
> public int search(int[] nums, int target) {
> int left = 0; //左边索引
> int right = nums.length - 1; //右边索引
> while (left <= right) { //若左边索引大于等于右边,就是死递归
> int mid = (right - left) / 2 + left; //中间数的下标
> int midVal = nums[mid]; //中间数的值
> //如果寻找值等于中间值,直接返回
> if (midVal == target) {
> return mid;
> } else if (midVal > target) { //大于中间值,向左递归
> right = mid - 1;
> } else { //小于中间值,向右递归
> left = mid + 1;
> }
> }
> return -1;
> }
> }
> ```
## 8.4 、插值查找算法
> 1) 插值查找原理介绍: 插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。
>
> 2) 将折半查找中的求 mid 索引的公式 , low 表示左边索引 left, high 表示右边索引 right. key 就是前面我们讲的 findVal
>
> ![image-20221128100421295](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221128100421295.png)
>
> 3) int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;插值索引 对应前面的代码公式: int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])
>
> 4) **举例说明插值查找算法** 1-100 的数组
>
> ![image-20221128095827516](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221128095827516.png)
### 8.4.1、插值查找应用案例:
> 请对一个有序数组进行插值查找 {1,2,3,4,...,100} ,输入一个数看看该数组是否存在此数,并且求出下标
>
> 代码实现:
```JAVA
/**
* description
* 插值查找的算法案例实现——对一个有序数组进行插值查找 {1,2,3,4,...,100}
*
* @author
* @since 2022/11/28 10:21
*/
public class InsertValueSearch {
public static void main(String[] args) {
int[] arr = new int[100];
for (int i = 0; i < 100; i++) { //生成1-100的数组
arr[i] = i + 1;
}
int resultIndexValue = insertValueSearch(arr, 0, arr.length - 1, 100);
System.out.println("resultIndexValue=" + resultIndexValue);
}
/**
* 编写插值插值查找算法,思路大致跟二分查找一致,要求数组是有序的
*
* @param arr 传入的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findValue 查找的值
* @return 如果找到就返回查找数值的下标,如果没有找到返回-1
*/
public static int insertValueSearch(int[] arr, int left, int right, int findValue) {
System.out.println("查找次数");
//这些条件必须需要,否则数组可能会越界
if (left > right || findValue < arr[0] || findValue > arr[arr.length - 1]) {
return -1;
}
//找出中间索引
int mid = left + (right - left) * (findValue - arr[left]) / (arr[right] - arr[left]);
//找到中间值
int midValue = arr[mid];
if (findValue == midValue) {
return mid;
} else if (findValue > midValue) { //向左递归
return insertValueSearch(arr, mid + 1, right, findValue);
} else { //向右递归
return insertValueSearch(arr, left, mid - 1, findValue);
}
}
}
```
### 8.4.2、插值查找注意事项
> 1) 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
>
> 2) 关键字分布不均匀的情况下,该方法不一定比折半查找要好
## 8.5 、斐波那契(黄金分割法)查找算法
### 8.5.1、斐波那契(黄金分割法)查找基本介绍:
> 1) 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是 0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
>
> 2) 斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 发现斐波那契数列的两个相邻数 的比例,无限接近黄金分割值0.618
### 8.5.2、斐波那契(黄金分割法)原理:
> 斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid 不再是中间或插值得到,而是位于黄金分割点附近,即 mid=low+F(k-1)-1(F 代表斐波那契数列),如下图所示
>
> ![image-20221129093949016](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221129093949016.png)
>
> **对 F(k-1)-1 的理解:**
>
> 1) 由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。该式说明:只要顺序表的长度为 F[k]-1,则可以将该表分成长度为 F[k-1]-1 和 F[k-2]-1 的两段,即如上图所示。从而中间位置为 mid=low+F(k-1)-1
>
> 2) 类似的,每一子段也可以用相同的方式分割
>
> 3) 但顺序表长度 n 不一定刚好等于 F[k]-1,所以需要将原来的顺序表长度 n 增加至F[k]-1。这里的k 值只要能使得 F[k]-1 恰好大于或等于 n 即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1 到F[k]-1 位置),都赋为 n 位置的值即可。 while(n>fib(k)-1) k++;
### 8.5.3、斐波那契查找应用案例
> 请对一个**有序数组**进行斐波那契查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
>
> 代码实现如下:
```JAVA
/**
* description
* 斐波那契查找的案例
*
* @author
* @since 2022/11/29 9:54
*/
public class FibonacciSearch {
public static int maxSize = 20; //初始化变量
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
System.out.println(fibSearch(arr,8));
}
/**
* 因为我们要使用斐波那契算法,因此需要先获取斐波那契数列
* 使用非递归的方式得到一个斐波那契数列
* 公式为 F[k]=F[k-1]+F[k-2]--> (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1
*/
public static int[] fib() {
int[] f = new int[maxSize];
f[0] = 0;
f[1] = 1;
for (int i = 2; i < maxSize; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
/**
* 编写斐波那契数列查找算法
* 使用非递归的方式编写
*
* @param arr 数组
* @param key 需要查找的关键值
* @return 返回对于下标,如果没有就返回-1
*/
public static int fibSearch(int[] arr, int key) {
int low = 0;
int high = arr.length - 1;
int k = 0; //表示斐波那契分割数值的下标
int mid = 0; //存放mid值
int[] f = fib(); //获取到斐波那契数列
//获取到斐波那契数值的下标,也就是k
while (high > f[k] - 1) {
k++;
}
//因为f[k] 值可能大于数组的arr的长度,因此我们需要使用一个Arrays构造一个新的数组并指向arr
int[] temp = Arrays.copyOf(arr, f[k]);
//实际上需要使用数组最后一个元素填充temp
for (int i = high + 1; i < temp.length; i++) {
temp[i] = arr[high];
}
//使用while来循环处理,找到需要的找到的值(k)
while (low <= high) { //只要这个条件满足就可以一直找
mid = low + f[k - 1] - 1;
if (key < temp[mid]) { //说明我们应该继续向数组的前部分查找(左边)
high = mid - 1;
/* 为什么是k--,说明:
1、全部元素= 前面的元素 + 后面的元素
2、f[k] = f[k-1] + f[k-2]
3、因为前面有f[k-1]个元素,所以可以继续拆分f[k-1] = f[k-2] + f[k-3]
4、即在f[k-1]的前面继续查找k--,即下次循环mid = f[k-1-1]-1*/
k--;
} else if (key > temp[mid]) { //说明我们应该继续向数组的前部分查找(右边)
low = mid + 1;
/* 为什么是k-=2 ,说明:
1、全部元素= 前面的元素 + 后面的元素
2、f[k] = f[k-1] + f[k-2]
3、因为后面我们有f[k-2],所以可以继续拆分f[k-1] = f[k-3] +f[k-4]
4、即在f[k-2]的前面进行查找 k-=2
5、即下次循环 mid = f[k-1-2]-1*/
k -= 2;
} else { //找到了
//需要确定返回的是哪个下标
if (mid <= high){
return mid;
}else {
return high;
}
}
}
return -1;
}
}
```
# 九、哈希表
## 9.1 、哈希表(散列)-Google 上机题
> 1) 看一个实际需求,google 公司的一个上机题:
>
> 2) 有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工的id 时,要求查找到该员工的 所有信息.
>
> 3) 要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
## 9.2、 哈希表的基本介绍
> 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
>
> ![image-20221130090101835](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221130090101835.png)
>
> ![image-20221130090212934](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221130090212934.png)
## 9.3 、google 公司的一个上机题:
> **要求:**
>
> 1) 不使用数据库,,速度越快越好=>哈希表(散列)
>
> 2) 添加时,保证按照 id 从低到高插入 [课后思考:**如果 id 不是从低到高插入**,但要求各条链表仍是从低到高,怎么解决?] 3) 使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
>
> 4) 思路分析并画出示意图
>
> ![image-20221130090346633](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221130090346633.png)
>
> 5)代码实现
```JAVA
/**
* description
* 哈希表案例的测试
*
* @author xujicheng
* @date 2022年11月30日 10:18
*/
public class HashTableDemo {
public static void main(String[] args) {
//创建哈希表
HashTable hashTable = new HashTable(7);
//写一个简单的菜单来测试
String key = "";
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("add:添加雇员");
System.out.println("list:显示雇员");
System.out.println("find: 查找雇员");
System.out.println("delete: 删除雇员");
System.out.println("exit:退出系统");
key = scanner.next();
switch (key) {
case "add":
System.out.println("输入id");
int id = scanner.nextInt();
System.out.println("输入名字");
String name = scanner.next();
//创建雇员
Emp emp = new Emp(id, name);
hashTable.add(emp);
break;
case "list":
hashTable.list();
break;
case "find":
System.out.println("请输入要查找的id");
id = scanner.nextInt();
hashTable.findEmpById(id);
break;
case "delete":
System.out.println("请输入要删除的id");
id = scanner.nextInt();
hashTable.deleteEmpById(id);
break;
case "exit":
scanner.close();
System.exit(0);
break;
default:
break;
}
}
}
}
/**
* description
* 哈希表,管理多条链表
*
* @author xujicheng
* @date 2022年11月30日 10:45
*/
public class HashTable {
private EmpLinkedList[] empLinkedListArray;
private int size; //表示共有多少条链表
/**
* 构造器
*
* @param size 数组大小
*/
public HashTable(int size) {
this.size = size;
//初始化empLinkedListArray
empLinkedListArray = new EmpLinkedList[size];
//不要忘了初始化每个链表
for (int i = 0; i < size; i++) {
empLinkedListArray[i] = new EmpLinkedList();
}
}
/**
* 添加雇员
*
* @param emp 雇员
*/
public void add(Emp emp) {
//根据雇员的id得到该员工应当添加到哪条链表
int empLinkedListNo = hashFun(emp.id);
//将emp 添加到对应的链表中
empLinkedListArray[empLinkedListNo].add(emp);
}
//遍历所有的链表,遍历哈希表
public void list() {
for (int i = 0; i < size; i++) {
empLinkedListArray[i].List(i);
}
}
//编写散列函数,使用取模法
public int hashFun(int id) {
return id % size;
}
//根据输入的id查找雇员
public void findEmpById(int id) {
//使用散列函数确定到哪条链表查找
int empLinkedListNO = hashFun(id);
Emp emp = empLinkedListArray[empLinkedListNO].findEmpById(id);
if (emp != null) { //找到
System.out.printf("在第%d 条链表中找到 雇员 id = %d\n", (empLinkedListNO + 1), id);
} else {
System.out.println("在哈希表中没有找到该雇员");
}
}
public void deleteEmpById(int id) {
//使用散列函数确定到哪条链表查找
int empLinkedListNO = hashFun(id);
//从对应的链表中删除
empLinkedListArray[empLinkedListNO].deleteEmpById(id);
}
}
/**
* description
* 表示一个雇员
*
* @author xujicheng
* @date 2022年11月30日 10:21
*/
public class Emp {
public int id; //id
public String name; //名字
public Emp next; //指向下一个节点的下一个引用,默认为空
public Emp(int id, String name) {
this.id = id;
this.name = name;
}
}
/**
* description
* 创建一个EmpLinkedList,表示链表
*
* @author xujicheng
* @date 2022年11月30日 10:28
*/
public class EmpLinkedList {
//头指针,指向第一个Emp,因此我们这个链表的head是直接指向第一个Emp,默认为空
private Emp head;
/**
* 添加雇员到链表
* 说明:
* 1、假定当添加雇员时,id是自增的,即id的分配是从小到大
* 2、因此我们将雇员直接加入到本链表的最后即可
*
* @param emp 雇员
*/
public void add(Emp emp) {
//如果是添加第一个雇员
if (head == null) {
head = emp;
return;
}
//如果不是添加第一个雇员,则使用一个辅助的指针帮助我们定位到最后
Emp curEmp = head;
while (true) {
if (curEmp.next == null) { //说明到链表最后
break;
}
curEmp = curEmp.next; //后移,直到到最后
}
//退出时直接将emp加到最后即可
curEmp.next = emp;
}
//遍历链表的雇员信息
public void List(int no) {
if (head == null) { //说明链表为空
System.out.println("第 " + (no + 1) + " 链表为空");
return;
}
System.out.print("第 " + (no + 1) + " 链表的信息为");
Emp curEmp = head; //辅助指针
while (true) {
System.out.printf("=> id=%d name=%s\t", curEmp.id, curEmp.name);
if (curEmp.next == null) { //说明链表已经到最后了
break;
}
curEmp = curEmp.next; //让curEmp后移
}
System.out.println(); //换行
}
/**
* 根据id查找雇员
*
* @param id id
* @return 找到就返回雇员对象,找不到就返回空
*/
public Emp findEmpById(int id) {
//先判断链表是否为空
if (head == null) { //说明链表为空
System.out.println("链表为空");
return null;
}
//辅助指针
Emp curEmp = head;
while (true) {
if (curEmp.id == id) { //找到
return curEmp; //这时curEmp就指向要查找的雇员
}
//退出条件
if (curEmp.next == null) { //说明遍历当前链表没有找到该雇员
curEmp = null;
break;
}
curEmp = curEmp.next; //后移,继续判断
}
return curEmp;
}
/**
* 根据雇员id删除雇员
* 删除思路:
* 1、head节点不能动,因此我们需要辅助节点找到待删除节点的前一个节点
* 2、我们在比较时,是head.next.id 和需要删除的节点的id进行比较
*
* @param id 需要删除的雇员id
* @return
*/
public void deleteEmpById(int id) {
//先判断链表是否为空
if (head == null) {
System.out.println("没有找到第" + id + "号员工");
return;
}
if (head.id == id) {
//如果头节点的id等于要删除的id,则头节点指向下一个
head = head.next;
return;
}
Emp curEmp = head;
boolean flag = false; //标识
while (true) {
if (curEmp.next == null) { //说明已经到了链表的最后
break;
}
if (curEmp.next.id == id) {
//说明找到了待删除节点的前一个节点
flag = true;
break;
}
curEmp = curEmp.next; //后移,实现遍历
}
//判断flag
if (flag) {
//进行删除操作
curEmp.next = curEmp.next.next;
}
}
}
```
> 代码总结:使用哈希表管理链表的方式模拟数据库进行了增删查功能思路都写在代码里了,相当于复习了链表的知识
# 十、树结构的基础部分
## 10.1、 二叉树
### 10.1.1 、为什么需要树这种数据结构
> 1) 数组存储方式的分析
>
> **优点**:通过**下标方式访问元素**,速度快。对于有序数组,还可使用**二分查找**提高检索速度。
>
> **缺点:**如果要检索具体某个值,或者**插入值(按一定顺序)会整体移动**,效率较低[示意图]
>
> **画出操作示意图:**
>
> ![image-20221201100253053](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201100253053.png)
>
> 2) 链式存储方式的分析
>
> **优点:**在一定程度上对数组存储方式有优化(比如:**插入**一个数值节点,只需要将插入节点,链接到链表中即可,**删除**效率也很好)。
>
> **缺点**:在进行**检索**时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历) 【示意图】
>
> **操作示意图:**
>
> ![image-20221201100351381](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201100351381.png)
>
> 3) 树存储方式的分析
>
> 能提高数据**存储,读取**的效率, 比如利用 **二叉排序树**(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的**插入,删除,修改**的速度
>
> 案例: [7, 3, 10, 1, 5, 9, 12]
>
> ![image-20221201100508981](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201100508981.png)
>
> 分析以二叉排序树来存储数据的效率:
>
> 1、查找12,经过两次比较就找到12节点
>
> 2、添加13,速度很快
>
> 3、删除节点,速度也很快
### 10.1.2、 树示意图
![image-20221201141252550](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201141252550.png)
> 树的常用术语(结合示意图理解):
>
> 1) 节点
>
> 2) 根节点
>
> 3) 父节点
>
> 4) 子节点
>
> 5) 叶子节点 (没有子节点的节点)
>
> 6) 节点的权(节点值)
>
> 7) 路径(从 root 节点找到该节点的路线)
>
> 8) 层
>
> 9) 子树
>
> 10) 树的高度(最大层数)
>
> 11) 森林 :多颗子树构成森林
### 10.1.3、 二叉树的概念
> 1) 树有很多种,每个节点**最多只能有两个子节点**的一种形式称为二叉树。
>
> 2) 二叉树的子节点分为左节点和右节点
>
> 3) 示意图
>
> ![image-20221201100749556](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201100749556.png)
>
> 4) 如果该二叉树的所有**叶子节点**都在**最后一层**,并且结点总数**= 2^n -1** , n 为层数,则我们称为满二叉树
>
> ![image-20221201100824533](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201100824533.png)
>
> 5) 如果该二叉树的所有**叶子节点**都在**最后一层**或者**倒数第二层**,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
>
> ![image-20221201100855873](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201100855873.png)
### 10.1.4、 二叉树遍历的说明
> 使用**前序,中序和后序**对下面的二叉树进行遍历.
>
> 1) 前序遍历: 先输出父节点,再遍历左子树和右子树
>
> 2) 中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
>
> 3) 后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
>
> 4) 小结: 看输出父节点的顺序,就确定是前序,中序还是后序
### 10.1.5 、二叉树遍历应用实例(前序,中序,后序)
> 二叉树遍历的要求如下:
>
> 1)使用前序、中序、后序遍历,请写出各组的输出顺序是什么
>
> 2)在三号节点 "卢俊义",添加一个左子节点 [5, 关胜],再使用前序、中序、后序输出的顺序是什么
>
> 应用实例的说明和思路
>
> <img src="https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201101535124.png" alt="image-20221201101535124" style="zoom:50%;" />
>
> 分析二叉树的前序、中序、后序的遍历步骤
>
> * 1、创建一颗二叉树
> * 2、前序遍历
> * 2.1、先输出当前节点(初始的时候是root节点)
> * 2.2、如果左子节点不为空,则递归继续前序遍历
> * 2.3、如果右子节点不为空,则递归继续前序遍历
> * 3、中序遍历
> * 3.1、如果当前节点的左子节点不为空,则递归中序遍历
> * 3.2、输出当前节点
> * 3.3、如果当前节点的右子节点不为空,则递归中序遍历
> * 4、后序遍历
> * 4.1、如果左子节点不为空,则递归后序遍历
> * 4.2、如果右子节点不为空,则递归后序遍历
> * 4.3、输出当前节点
>
> 代码实现如下:
```JAVA
/**
* description
* 创建HeroNode节点——英雄类
*
* @author xujicheng
* @since 2022年12月01日 15:21
*/
public class HeroNode {
private int no; //编号
private String name; //名字
private HeroNode left; //指向左边的节点的指针,默认为null
private HeroNode right; //指向右边的节点的指针,默认为null
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public void setRight(HeroNode right) {
this.right = right;
}
//编写前序遍历的方法
public void preOrder() {
System.out.println(this); //先输出父节点
//递归向左子树前序遍历
if (this.left != null) { //左子树不为空的情况下才能遍历
this.left.preOrder();
}
//递归向右子树前序遍历
if (this.right != null) { //右子树不为空的情况下才能遍历
this.right.preOrder();
}
}
//编写中序遍历的方法
public void infixOrder() {
//递归向左子树中序遍历
if (this.left != null) {
this.left.infixOrder();
}
//输出当前节点(父节点)
System.out.println(this);
//递归向右子树中后序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
//编写后序遍历的方法
public void postOrder() {
//递归向左子树的后序遍历
if (this.left != null) {
this.left.postOrder();
}
if (this.right != null) {
this.right.postOrder();
}
//输出当前节点(父节点)
System.out.println(this);
}
}
/**
* description
* 定义二叉树
*
* @author xujicheng
* @since 2022年12月01日 16:46
*/
public class BinaryTree {
//二叉树最重要的是根节点
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("当前二叉树为空,无法遍历");
}
}
//中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("当前二叉树为空,无法遍历");
}
}
//后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("当前二叉树为空,无法遍历");
}
}
}
/**
* description
* 实现二叉树前序、中序、后序的遍历
*
* @author xujicheng
* @since 2022年12月01日 15:20
*/
public class BinaryTreeDemo {
public static void main(String[] args) {
//测试之前首先需要创建一颗二叉树
BinaryTree binaryTree = new BinaryTree();
//创建需要的节点
HeroNode root = new HeroNode(1,"宋江");
HeroNode heroNode2 = new HeroNode(2,"吴用");
HeroNode heroNode3 = new HeroNode(3,"卢俊义");
HeroNode heroNode4 = new HeroNode(4,"林冲");
//说明:先手动创建该二叉树,以后用递归方式创建
root.setLeft(heroNode2); //root这个节点左边挂上了heroNode2
root.setRight(heroNode3); //root这个节点右边边挂上了heroNode3
heroNode3.setRight(heroNode4); //heroNode3这个节点右边挂上了heroNode4
binaryTree.setRoot(root); //把root节点给到二叉树
//测试前序遍历
System.out.println("前序遍历");
binaryTree.preOrder();
//测试中序遍历
System.out.println("中序遍历");
binaryTree.infixOrder();
//测试后序遍历
System.out.println("后序遍历");
binaryTree.postOrder();
}
```
> 代码执行结果如下:
>
> 1)第一个要求:
>
> * 完成了前序中序后序遍历二叉树的测试,前序、中序、后序的输出顺序如下图
>
> ![image-20221201172337550](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201172337550.png)
>
> 2)第二个要求,添加节点后输出顺序
>
> ![image-20221201173803391](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201173803391.png)
>
> 添加后的输出结果为:
>
> ![image-20221201173833854](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201173833854.png)
>
> 得出结论:
>
> * 二叉树最重要的是根节点,我们定义了跟节点之后可以根据构造器去进行完成二叉树遍历的操作
### 10.1.6、 二叉树-查找指定节点
> 1) 请编写前序查找,中序查找和后序查找的方法。
>
> 2) 并分别使用三种查找方式,查找 heroNO = 5 的节点
>
> 3) 并分析各种查找方式,分别比较了多少次
>
> 4) 思路分析图解
>
> ![image-20221201195529028](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201195529028.png)
>
> 使用前序、中序、后序的方式来查询指定的节点
>
> **前序查找思路:**
>
> * 1、先判断当前节点的no是否等于要查找的
> * 2、如果是相等,则返回当前节点
> * 3、如果不相等,**则判断当前节点的左子节点是否为空**,如果不为空,则递归前序擦或者
> * 4、如果左递归前序查找,找到节点,则返回,否则继续判断,当前节点的右子节点是否为空,如果不为空,则继续向右递归前序查找
>
> **中序查找思路:**
>
> * 1、先判断当前节点的左子节点是否为空,如果不为空,则进行左递归的中序查找
> * 2、如果找到,则返回,如果没有找到,就和当前节点比较,如果是则返回当前节点,否则继续进行右递归的中序查找
> * 3、如果右递归中序查找,找到就返回,否则返回null
>
> **后序查找思路:**
>
> * 1、判断当前节点的左节点是否为空,如果不为空,则进行左递归后序查找
> * 2、如果找到,就返回,如果没有找到,就判断当前节点的右子节点是否为空,如果不为空,则右递归进行后序查找,如果找到,就返回;
> * 3、就和当前节点进行比较,如果是则返回,否则返回null
>
> 5) 代码实现
>
> * 在HeroNode类中添加前序、中序、后序的查找方法的具体逻辑
>
> ```JAVA
> /**
> * 前序遍历查找的方法
> *
> * @param no 需要查找的编号
> * @return 如果找到返回该节点,没有找到返回null即可
> */
> public HeroNode preOrderSearch(int no) {
> System.out.println("进入前序遍历"); //用于测试前序遍历一共比较了几次
> //比较当前节点是否为要寻找的节点
> if (this.no == no) {
> return this;
> }
>
> HeroNode result = null; //初始化结果节点
> //如果不是则判断当前节点的左子节点是否为空,如果不为空,则左递归前序查找
> if (this.left != null) {
> result = this.left.preOrderSearch(no);
> }
> if (result != null) { //返回的结果若不为空说明左子树上找到了
> return result;
> }
> //如果左递归前序查找没有找到节点,则继续判断,判断当前节点的右子节点是否为空,不为空继续向右递归
> if (this.right != null) {
> result = this.right.preOrderSearch(no);
> }
> return result;
> }
>
> /**
> * 中序遍历查找的方法
> *
> * @param no 需要查找的编号
> * @return 如果找到返回该节点,没有找到返回null即可
> */
> public HeroNode infixOrderSearch(int no) {
> HeroNode result = null; //初始化结果节点
> //先判断当前节点的左子节点是否为空,如果不为空,则进行左递归中序查找
> if (this.left != null) {
> result = this.left.infixOrderSearch(no);
> }
> //判断向左递归进行中序查找的节点是否为空,找到则返回当前对象
> if (result != null) {
> return result;
> }
> System.out.println("进入中序遍历"); //用于测试中序遍历一共比较了几次
> //若没有找到,就比较当前节点是否为要寻找的节点,如果是则返回当前对象
> if (this.no == no) {
> return this;
> }
> //若比较不为需要寻找的节点,就进行右递归中序查找,找到就返回,否则返回此对象即可
> if (this.right != null) {
> result = this.right.infixOrderSearch(no);
> }
> return result;
> }
>
> /**
> * 后序遍历查找的方法
> *
> * @param no 需要查找的编号
> * @return 如果找到返回该节点,没有找到返回null即可2
> */
> public HeroNode postOrderSearch(int no) {
> HeroNode result = null; //初始化结果节点
> //判断当前节点的左节点是否为空,如果不为空,则进行左递归后序查找
> if (this.left != null) {
> result = this.left.postOrderSearch(no);
> }
> //若向左递归已经找到了就返回当前对象即可
> if (result != null) {
> return result;
> }
> //如果左子树没有找到,则向右子树进行后序遍历查找
> if (this.right != null) {
> result = this.right.postOrderSearch(no);
> }
> //若向右递归已经找到了就返回当前对象即可
> if (result != null) {
> return result;
> }
> System.out.println("进入后序遍历"); //用于测试后序遍历一共比较了几次
> //若左右递归子树都没有找到,就比较当前节点是不是需要找的节点
> if (this.no == no) {
> return this;
> }
> return result;
> }
> ```
>
>
>
> * 在BinaryTree类中去调用
>
> ```JAVA
> /**
> * 前序遍历查找的方法
> *
> * @param no 需要查找的编号
> * @return 如果找到返回该节点,没有找到返回null即可
> */
> public HeroNode preOrderSearch(int no) {
> //如果头节点不等于空,就调用前序遍历查找的方法即可
> if (root != null) {
> return root.preOrderSearch(no);
> } else { //若为空,说明这是空子树,直接返回null即可
> return null;
> }
> }
>
> /**
> * 中序遍历查找的方法
> *
> * @param no 需要查找的编号
> * @return 如果找到返回该节点,没有找到返回null即可
> */
> public HeroNode inOrderSearch(int no) {
> //如果头节点不等于空,就调用中序遍历查找的方法即可
> if (root != null) {
> return root.infixOrderSearch(no);
> } else { //若为空,说明这是空子树,直接返回null即可
> return null;
> }
> }
>
> /**
> * 后序遍历查找的方法
> *
> * @param no 需要查找的编号
> * @return 如果找到返回该节点,没有找到返回null即可2
> */
> public HeroNode postOrderSearch(int no) {
> //如果头节点不等于空,就调用后序遍历查找的方法即可
> if (root != null) {
> return root.postOrderSearch(no);
> } else { //若为空,说明这是空子树,直接返回null即可
> return null;
> }
> }
> ```
>
> * 分别使用三种查找方式,查找 heroNO = 5 的节点 ,并分析各种查找方式,分别比较了多少次
>
> ```JAVA
> System.out.println("前序遍历查找方式");
> HeroNode result = binaryTree.preOrderSearch(5);
> if (result != null) {
> System.out.printf("找到了,信息为no = %d name=%s", result.getNo(), result.getName());
> } else {
> System.out.printf("没有找到为no = %d 的英雄", 5);
> }
>
> System.out.println("中序遍历查找方式");
> HeroNode result2 = binaryTree.inOrderSearch(5);
> if (result2 != null) {
> System.out.printf("找到了,信息为no = %d name=%s", result2.getNo(), result2.getName());
> } else {
> System.out.printf("没有找到为no = %d 的英雄", 5);
> }
>
> System.out.println("后序遍历查找方式");
> HeroNode result3 = binaryTree.postOrderSearch(5);
> if (result3 != null) {
> System.out.printf("找到了,信息为no = %d name=%s", result3.getNo(), result3.getName());
> } else {
> System.out.printf("没有找到为no = %d 的英雄", 5);
> }
> }
> ```
>
> 输出结果如下:
>
> * 以下是前序查找的比较次数,是四次
>
> ![image-20221201223832743](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201223832743.png)
>
> * 以下是中序查找的比较次数,是三次
>
> ![image-20221201223924251](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201223924251.png)
>
> * 以下是后序查找的比较次数,是两次
>
> ![image-20221201224010737](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221201224010737.png)
### 10.1.7、 二叉树-删除节点
> 1) 如果删除的节点是叶子节点,则删除该节点
>
> 2) 如果删除的节点是非叶子节点,则删除该子树.
>
> 3) 测试,删除掉 5 号叶子节点 和 3 号子树.
>
> 4) 完成删除思路分析
>
> ![image-20221202083232418](040607_数据结构与算法的学习笔记.assets/image-20221202083232418.png)
>
> 思路分析
>
> 完成删除节点的操作
>
> 规定:
>
> * 1)如果删除的节点是叶子节点,则删除该节点
> * 2)如果删除的节点是非叶子节点,则删除该子树
>
> 思路:
>
> 首先先处理:
>
> 考虑如果树是空树 root ,如果只有一个root节点,则等价将二叉树置空
>
> 然后进行下面步骤:
>
> * 1、因为我们的二叉树是单向的,所以我们是**判断当前节点的子节点**是需要删除节点,而不能去判断当前这个节点是不是需要删除的节点
> * 2、如果当前节点的左子节点不为空,并且左子节点就是要删除节点就将this.left = null;并且就返回(结束递归删除)
> * 3、如果当前节点的右子节点不为空,并且右子节点就是要删除节点就将this.right= null;并且就返回(结束递归删除)
> * 4、如果第二步和第三步没有删除节点,那么我们就需要向左子树进行递归删除
> * 5、如果第四步也没有删除节点,则应当向右子树进行递归删除
>
> 5) 代码实现
>
> * 在HeroNode类中添加前序、中序、后序的删除方法的具体逻辑
>
> ```JAVA
> /**
> * 递归删除节点
> * 1、说明:如果删除的节点是叶子节点,则删除该节点
> * 2、如果删除的节点是非叶子节点,则删除该子树
> *
> * @param no 需要删除的节点编号
> */
> public void deleteNode(int no) {
> //首先判断左子节点是否等于要删除的节点
> if (this.left != null && this.left.no == no) {
> this.left = null;
> return;
> }
> //如果右子节点不为空,并且右子节点就是要删除的节点,就将this.right = null;并且返回结束
> if (this.right != null && this.right.no == no) {
> this.right = null;
> return;
> }
> //以上两步都没有删除节点,我们就需要向左子树进行递归删除
> if (this.left != null) {
> this.left.deleteNode(no);
> }
> //向左子树递归也没有删除节点,那么就向右子树递归删除
> if (this.right != null) {
> this.right.deleteNode(no);
> }
> }
> ```
>
> * 在BinaryTree类中去调用
>
> ```JAVA
> /**
> * 删除节点的方法
> *
> * @param no 需要进行删除的节点
> */
> public void deleteNode(int no) {
> if (root != null){
> //如果只有一个root节点,需要立即判断root是不是就是要删除的节点
> if (root.getNo() == no){
> root = null;
> }else {
> //若root不是要删除的节点,就进行递归删除即可
> root.deleteNode(no);
> }
> }else {
> System.out.println("空树,不能删除");
> }
> }
> ```
>
> * 测试,删除掉 5 号叶子节点 和 3 号子树.
>
> ```JAVA
> //测试删除节点的代码
> System.out.println("删除前,前序遍历");
> binaryTree.preOrder();
> binaryTree.deleteNode(5);
> System.out.println("删除后,前序遍历");
> binaryTree.preOrder();
> ```
>
> * 删除叶子节点5,输出结果如下
>
> ![image-20221202092035012](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221202092035012.png)
>
> * 删除三号子树,输出结果如下
>
> ![image-20221202094533743](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221202094533743.png)
### 10.1.8 二叉树-删除节点——课后思考题
> 思考题(课后练习)
>
> 1) 如果要删除的节点是非叶子节点,现在我们不希望将该非叶子节点为根节点的子树删除,需要指定规则, 假如规定如下:
>
> 2) 如果该非叶子节点 A 只有一个子节点 B,则子节点 B 替代节点 A
>
> 3) 如果该非叶子节点 A 有左子节点 B 和右子节点 C,则让左子节点 B 替代节点 A。
>
> 4) 思考,如何完成该删除功能
>
> ![image-20221202095915177](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221202095915177.png)
>
> 思路分析:
>
> 在删除节点前做一个判断,查看被删除节点下是否只有一个子节点,再判断子节点下也没有叶子节点,经过判断之后再使用辅助指针将需要删除的节点删除,再将被删除子节点下的叶子节点上提提即可。
>
> 代码实现:
```JAVA
/**
* 递归删除节点
* 1、说明:如果删除的节点是叶子节点,则删除该节点
* 2、如果删除的节点是非叶子节点,则删除该子树
*
* @param no 需要删除的节点编号
*/
public void deleteNode(int no) {
//首先判断左子节点是否为空并且判断是否等于要删除的节点
if (this.left != null && this.left.no == no) {
HeroNode tempNode = this.left.right; //定义一个辅助指针用于将被删除的子节点底下的叶子节点上提
//若被删除的节点的左子节点下的左子节点不为空,则将左子节点下的左叶子节点直接返回
if (this.left.left != null) {
this.setLeft(this.left.left); //当前节点的左节点下的左节点
this.left.setRight(tempNode); //当前左节点下的右节点重新指向指针
return;
}
//若被删除的节点的左子节点下的右子节点不为空,则将右子节点下的右叶子节点直接返回
if (this.left.right != null) {
this.setLeft(this.left.right); //当前节点的左节点下的右叶子节点
return;
}
this.left = null; //若以上都没有找到要删除的节点就将当前节点的左节点置空
return;
}
//判断右子节点是否为空并且判断是否等于要删除的节点
if (this.right != null && this.right.no == no) {
HeroNode tempNode1 = this.right.right; //定义一个辅助指针用于将被删除的子节点底下的叶子节点上提
//若被删除的节点的右子节点下的左子节点不为空,则将右子节点下的左叶子节点直接返回
if (this.right.left != null) {
this.setRight(this.right.left); //当前节点的右节点下的左节点
this.right.setRight(tempNode1); //当前右节点下的右节点重新指向指针
return;
}
//若被删除的节点的右子节点下的右子节点不为空,则将右子节点下的右叶子节点直接返回
if (this.right.right != null) {
this.setRight(this.right.right); //当前节点的右节点下的右叶子节点
return;
}
this.right = null; //若以上都没有找到要删除的节点就将当前节点的右节点置空
return;
}
//以上步骤都没有删除节点,我们就需要向左子树进行递归删除
if (this.left != null) {
this.left.deleteNode(no);
}
//以上步骤都没有删除节点,我们就需要向右子树进行递归删除
if (this.right != null) {
this.right.deleteNode(no);
}
}
```
* 输出结果如下:
![image-20221202103218565](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221202103218565.png)
## 10.2 、顺序存储二叉树
### 10.2.1、 顺序存储二叉树的概念
> 基本说明
>
> 从数据存储来看,数组存储方式和树的存储方式可以相互转换,即**数组可以转换成树,树也可以转换成数组**,看右面的示意图。
>
> ![image-20221203095131189](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203095131189.png)
>
> 要求:
>
> 1) 右图的二叉树的结点,要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 6]
>
> 2) 要求在遍历数组 arr 时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历
>
>
>
> 顺序存储二叉树的特点:
>
> 1) 顺序二叉树通常只考虑完全二叉树
>
> 2) 第 n 个元素的左子节点为 2 * n + 1
>
> 3) 第 n 个元素的右子节点为 2 * n + 2
>
> 4) 第 n 个元素的父节点为 (n-1) / 2
>
> 5) n : 表示二叉树中的第几个元素(按 0 开始编号如图所示)
### 10.2.2、 顺序存储二叉树遍历
#### 前序遍历完成顺序存储的应用实例
> 需求: 给你一个数组 {1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。前序遍历的结果应当为1,2,4,5,3,6,7
>
> 思路分析如下:
>
> * 1、首先定义一个方法用于完成顺序存储二叉树的前序遍历
> * 2、对需要排序的数组进行非空判断,若数组为空或数据为空无需向下执行
> * 3、对数组下标做出逻辑判断,防止下标越界的问题,左右递归都需要进行逻辑判断,防止下标越界
> * 4、再根据顺序二叉树左节点的公式去完成向左递归遍历 公式 = 2 * n + 1
> * 5、再根据顺序二叉树左节点的公式去完成向右递归遍历 公式 = 2 * n + 2
>
> **代码实现:**
>
> ```JAVA
> /**
> * description
> * 编写一个 ArrayBinaryTree ,实现顺序存储二叉树遍历
> *
> * @author xujicheng
> * @since 2022年12月03日 10:30
> */
> public class ArrayBinaryTree {
>
> private int[] arr; //存储数据节点的数组
>
> //构造器,用于传递数组以进行排序
> public ArrayBinaryTree(int[] arr) {
> this.arr = arr;
> }
>
> //重载preOrder方法
> public void preOrder() {
> this.preOrder(0);
> }
>
> /**
> * 编写一个方法完成顺序存储二叉树的一个前序遍历
> *
> * @param index 表示数组的下标
> */
> public void preOrder(int index) {
> //如果数组为空或者arr.length=0,即非空判断,没有数据无需遍历
> if (arr == null || arr.length == 0) {
> System.out.println("数组为空,无法进行二叉树前序遍历");
> }
> //输出当前元素
> System.out.print(" " + arr[index]);
> //向左递归遍历,做一个判断防止index越界的问题
> if ((index * 2 + 1) < arr.length) {
> preOrder(2 * index + 1);
> }
> //向右递归遍历,也要做一个判断防止index越界
> if ((2 * index + 2) < arr.length) {
> preOrder(2 * index + 2);
> }
> }
> }
>
> /**
> * description
> * 以数组的方式存储二叉树并且完成前序遍历
> *
> * @author xujicheng
> * @since 2022年12月03日 10:27
> */
> public class ArrBinaryTreeDemo {
> public static void main(String[] args) {
> //首先定义好需要排序的数组
> int[] arr = {1, 2, 3, 4, 5, 6, 7};
> //创建一个ArrayBinaryTree
> ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(arr);
> System.out.println("前序遍历输出结果为:");
> arrayBinaryTree.preOrder();
> }
> }
> ```
>
> * 输出结果如下:
>
> ![image-20221203105530370](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203105530370.png)
#### 中序遍历完成顺序存储的应用实例
> 需求: 给你一个数组 {1,2,3,4,5,6,7},要求以二叉树中序遍历的方式进行遍历。中序遍历的结果应当为[2,4,5,1,3,6,7]
>
> 思路分析:
>
> * 1、首先定义一个方法用于完成顺序存储二叉树的中序遍历
> * 2、对需要排序的数组进行非空判断,若数组为空或数据为空无需向下执行
> * 3、对数组下标做出逻辑判断,防止下标越界的问题,左右递归都需要进行逻辑判断,防止下标越界
> * 4、再根据顺序二叉树左节点的公式去完成向左递归遍历 公式 = 2 * n + 1
> * 5、再根据顺序二叉树左节点的公式去完成向右递归遍历 公式 = 2 * n + 2
>
> 顺序存储中序遍历的代码实现如下
>
> ```JAVA
> /**
> * description
> * 编写一个 ArrayBinaryTree ,实现顺序存储二叉树遍历
> *
> * @author xujicheng
> * @since 2022年12月03日 10:30
> */
> public class ArrayBinaryTree {
>
> private int[] arr; //存储数据节点的数组
>
> //构造器,用于传递数组以进行排序
> public ArrayBinaryTree(int[] arr) {
> this.arr = arr;
> }
>
> //重载infixOrder方法
> public void infixOrder() {
> this.infixOrder(0);
> }
>
> /**
> * 编写一个方法完成顺序存储二叉树的一个中序遍历
> *
> * @param index 表示数组的下标
> */
> public void infixOrder(int index) {
> //首先对需要排序数组进行非空判断,若数组为空无需向下执行
> if (arr == null || arr.length == 0) {
> System.out.println("数组为空,无法进行二叉树中序遍历");
> }
> //向左递归遍历,做一个判断防止index越界的问题
> if ((2 * index + 1) < arr.length) {
> infixOrder(2 * index + 1);
> }
>
> //输出当前元素
> System.out.print(" " + arr[index]);
>
> //向右递归遍历,做一个判断防止index越界的问题
> if ((2 * index + 2) < arr.length) {
> infixOrder(2 * index + 2);
> }
> }
> }
>
> /**
> * description
> * 以数组的方式存储二叉树并且完成遍历
> *
> * @author xujicheng
> * @since 2022年12月03日 10:27
> */
> public class ArrBinaryTreeDemo {
> public static void main(String[] args) {
> //首先定义好需要排序的数组
> int[] arr = {1, 2, 3, 4, 5, 6, 7};
> //创建一个ArrayBinaryTree
> ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(arr);
> System.out.println("中序遍历输出结果为:");
> arrayBinaryTree.infixOrder();
> }
> }
> ```
>
> 输出结果如下:
>
> ![image-20221203123546548](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203123546548.png)
#### 后序遍历完成顺序存储的应用实例
> 需求: 给你一个数组 {1,2,3,4,5,6,7},要求以二叉树后序遍历的方式进行遍历。后序遍历的结果应当为[4 2 5 1 6 3 7]
>
> 思路分析:
>
> * 1、首先定义一个方法用于完成顺序存储二叉树的中序遍历
> * 2、对需要排序的数组进行非空判断,若数组为空或数据为空无需向下执行
> * 3、对数组下标做出逻辑判断,防止下标越界的问题,左右递归都需要进行逻辑判断,防止下标越界
> * 4、再根据顺序二叉树左节点的公式去完成向左递归遍历 公式 = 2 * n + 1
> * 5、再根据顺序二叉树左节点的公式去完成向右递归遍历 公式 = 2 * n + 2
>
> 顺序存储后序遍历的代码实现如下
>
> ```JAVA
> /**
> * description
> * 编写一个 ArrayBinaryTree ,实现顺序存储二叉树遍历
> *
> * @author xujicheng
> * @since 2022年12月03日 10:30
> */
> public class ArrayBinaryTree {
>
> private int[] arr; //存储数据节点的数组
>
> //构造器,用于传递数组以进行排序
> public ArrayBinaryTree(int[] arr) {
> this.arr = arr;
> }
>
> //重载postOrder方法
> public void postOrder(){
> this.postOrder(0);
> }
>
> /**
> * 编写一个方法完成顺序存储二叉树的一个后序遍历
> *
> * @param index 表示数组的下标
> */
> public void postOrder(int index) {
> //首先对需要排序数组进行非空判断,若数组为空无需向下执行
> if (arr == null || arr.length == 0) {
> System.out.println("数组为空,无法进行二叉树后序遍历");
> }
> //向左递归遍历,做一个判断防止index越界的问题
> if ((2 * index + 1) < arr.length) {
> postOrder(2 * index + 1);
> }
> //向右递归遍历,做一个判断防止index越界的问题
> if ((2 * index + 2) < arr.length) {
> postOrder(2 * index + 2);
> }
> //输出当前元素
> System.out.print(" " + arr[index]);
> }
> }
>
> /**
> * description
> * 以数组的方式存储二叉树并且完成遍历
> *
> * @author xujicheng
> * @since 2022年12月03日 10:27
> */
> public class ArrBinaryTreeDemo {
> public static void main(String[] args) {
> //首先定义好需要排序的数组
> int[] arr = {1, 2, 3, 4, 5, 6, 7};
> //创建一个ArrayBinaryTree
> ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(arr);
> System.out.println("后序遍历的输出结果为:");
> arrayBinaryTree.postOrder();
> }
> }
> ```
>
> 输出结果如下:
>
> ![image-20221203124307565](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203124307565.png)
## 10.3 、线索化二叉树
### 10.3.1 、先看一个问题
> 将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树. n+1=7
>
> ![image-20221203161638213](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203161638213.png)
>
> 问题分析:
>
> 1) 当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
>
> 2) 但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上.
>
> 3) 如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
>
> 4) 解决方案-**线索二叉树**
### 10.3.2 、线索二叉树基本介绍
> 1) n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
>
> 2) 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
>
> 3) 一个结点的前一个结点,称为**前驱**结点
>
> 4) 一个结点的后一个结点,称为**后继**结点
### 10.3.3 、线索二叉树应用案例
#### 前序线索化二叉树
![在这里插入图片描述](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typora20210227211830765.png)
```JAVA
/**
* description
* 创建HeroNode节点——英雄类
*
* @author xujicheng
* @since 2022年12月01日 15:21
*/
public class HeroNode {
private int no; //编号
private String name; //名字
private HeroNode left; //指向左边的节点的指针,默认为null
private HeroNode right; //指向右边的节点的指针,默认为null
//规定如果leftType ==0 表示指向的是左子树,如果为1则表示指向的是前驱节点
private int leftType;
//规定如果leftType ==0 表示指向的是右子树,如果为1则表示指向的是后继节点
private int rightType;
getter、setter、toString方法、构造器...
}
/**
* description
* 定义ThreadedBinaryTree ,实现了线索化功能的二叉树
*
* @author xujicheng
* @since 2022年12月01日 16:46
*/
public class ThreadedBinaryTree {
//二叉树最重要的是根节点
private HeroNode root;
//为了实现线索化,需要创建要给指向当前节点的前序节点的指针,在递归进行线索化是总保留前一个节点
private HeroNode pre = null;
/**
* 编写对二叉树进行前序线索化的方法
*
* @param node 需要线索化的节点
*/
public void preOrderThreadedNodes(HeroNode node) {
//如果节点为空则不需要线索化
if (node == null) {
return;
}
//1、线索化当前节点,处理当前节点的前驱节点
if (node.getLeft() == null) {
//让当前节点的左指针指向前驱节点
node.setLeft(pre);
//修改当前节点的左指针的类型,指向的是前驱节点
node.setLeftType(1);
}
//处理后继节点
if (pre != null && pre.getRight() == null) {
//让前驱节点的右指针指向当前节点
pre.setRight(node);
//修改前驱节点的右指针类型
pre.setRightType(1);
}
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
//2、向左线索化二叉树
if (node.getLeftType() == 0) {
preOrderThreadedNodes(node.getLeft());
}
//3、线索化右子树
if (node.getRightType() == 0) {
preOrderThreadedNodes(node.getRight());
}
}
```
#### 中序线索化二叉树
> 应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}
>
> ![image-20221203154610318](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203154610318.png)
>
> **思路分析:** 中序遍历的结果:{8, 3, 10, 1, 14, 6}
>
> ![image-20221203163255207](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203163255207.png)
>
> 说明: 当线索化二叉树后,Node 节点的 属性 left 和 right ,有如下情况:
>
> 1) left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而⑩节点的left 指向的就是前驱节点.
>
> 2) right 指向的是右子树,也可能是指向后继节点,比如 ① 节点 right 指向的是右子树,而⑩节点的right 指向的是后继节点.
>
> 代码实现
```java
/**
* description
* 创建HeroNode节点——英雄类
*
* @author xujicheng
* @since 2022年12月01日 15:21
*/
public class HeroNode {
private int no; //编号
private String name; //名字
private HeroNode left; //指向左边的节点的指针,默认为null
private HeroNode right; //指向右边的节点的指针,默认为null
private HeroNode parent;//默认null,父节点的指针(为了后序线索化使用)
//规定如果leftType ==0 表示指向的是左子树,如果为1则表示指向的是前驱节点
private int leftType;
//规定如果leftType ==0 表示指向的是右子树,如果为1则表示指向的是后继节点
private int rightType;
getter、setter、toString方法、构造器...
}
/**
* 后序线索化二叉树
*
* @param node 就是当前需要线索化的结点
*/
public void postThreadedNodes(HeroNode node) {
if (node == null) {
return;
}
//设置父节点,后序线索化遍历时需要
if (node.getLeft() != null) {
node.getLeft().setParent(node);
} else if (node.getRight() != null) {
node.getRight().setParent(node);
}
postThreadedNodes(node.getLeft());
postThreadedNodes(node.getRight());
if (node.getLeft() == null) {
node.setLeft(pre);
node.setLeftType(1);
}
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightType(1);
}
pre = node;
}
```
### 10.3.4 、遍历线索化二叉树
#### 前序遍历线索化的二叉树
> 代码实现如下:
```JAVA
/**
* 遍历前序线索化二叉树
*/
public void preThreadedList() {
HeroNode node = root;
while (node != null) {
System.out.print(node + " ");
//如果存在左子节点就往左走,否则往右走,此时右指针一定是前序遍历的下一个节点
if (node.getLeftType() == 0) {
node = node.getLeft();
} else {
node = node.getRight();
}
}
}
```
* 输出结果
![image-20221203233319944](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203233319944.png)
#### 中序遍历线索化的二叉树
> 1) 说明:对前面的中序线索化的二叉树, 进行遍历
>
> 2) 分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。遍历的次序应当和中序遍历保持一致。
>
> 3) 代码:
>
> ```JAVA
> //遍历线索化二叉树的方法
> public void threadedList(){
> //定义一个变量用于存储临时当前遍历的节点,从root开始
> HeroNode node = root;
> while (node != null){
> //循环的找 leftType == 1的节点,第一个找到的就是8节点,会随着遍历变化和变化
> while (node.getLeftType() == 0){
> node = node.getLeft();
> }
> //打印当前节点
> System.out.println(node);
> //如果当前节点的右指针指向的是后继节点,就一直输出
> while (node.getRightType() == 1){
> //获取到当前节点的后继节点
> node = node.getRight();
> System.out.println(node);
> }
> //替换这个遍历的节点
> node = node.getRight();
> }
> }
> ```
>
> 输出结果为:
>
> ![image-20221203211332075](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221203211332075.png)
# 十一、树结构实际应用
## 11.1 、堆排序
### 11.1.1 、堆排序基本介绍
> 1) 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。
>
> 2) 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意: 没有要求结点的左孩子的值和右孩子的值的大小关系。
>
> 3) 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
>
> 4) 大顶堆举例说明
>
> ![image-20221204143213151](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204143213151.png)
>
> 5) 小顶堆举例说明
>
> ![image-20221204143228731](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204143228731.png)
>
> 6) 一般升序采用大顶堆,降序采用小顶堆
### 11.1.2 、堆排序基本思想
> 堆排序的基本思想是:
>
> 1) 将待排序序列构造成一个大顶堆
>
> 2) 此时,整个序列的最大值就是堆顶的根节点。
>
> 3) 将其与末尾元素进行交换,此时末尾就为最大值。
>
> 4) 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
>
> 可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.
### 11.1.3 、堆排序步骤图解说明
> 要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升
>
> **步骤一 **
>
> **构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。原始的数组 [4, 6, 8, 5, 9]**
>
> * 1) .假设给定无序序列结构如下
>
> ![image-20221204144754639](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144754639.png)
>
> 2) .此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,也就是下面的 6 结点),从左至右,从下至上进行调整
>
> ![image-20221204144807379](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144807379.png)
>
> 3) .找到第二个非叶节点 4,由于[4,9,8]中 9 元素最大,4 和 9 交换。
>
> ![image-20221204144816909](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144816909.png)
>
> 4) 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换4 和6。
>
> ![image-20221204144825508](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144825508.png)
>
> 此时,我们就将一个无序序列构造成了一个大顶堆
>
> **步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换**
>
> * 1) .将堆顶元素 9 和末尾元素 4 进行交换
>
> ![image-20221204144852873](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144852873.png)
>
> 2) .重新调整结构,使其继续满足堆定义
>
> ![image-20221204144903210](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144903210.png)
>
> 3) .再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8.
>
> ![image-20221204144914319](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144914319.png)
>
> 4) 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
>
> ![image-20221204144926058](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221204144926058.png)
>
> **再简单总结下堆排序的基本思路:**
>
> **1).将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;**
>
> **2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;**
>
> **3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。**
### 11.1.4 、堆排序代码实现
> 要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
>
> 1) 堆排序不是很好理解,通过 Debug 帮助理解堆排序
>
> 2) 堆排序的速度非常快,在我的机器上 8 百万数据 3 秒左右。O(nlogn)
>
> 3) 代码实现
```JAVA
/**
* description
* 堆排序——树的实际应用
*
* @author xujicheng
* @since 2022年12月04日 15:35
*/
public class HeapSort {
public static void main(String[] args) {
//要求将数组进行升序排列
int[] arr = {4, 6, 8, 5, 9};
headSort(arr);
}
/**
* 编写一个堆排序的方法
*
* @param arr 需要进行堆排序的数组
*/
public static void headSort(int[] arr) {
int temp = 0; //临时变量,用于交换
System.out.println("堆排序");
//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或者小顶堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
//将栈顶元素于末尾元素交换,将最大元素"沉"到数组末端
//重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整,直到有序
for (int j = arr.length - 1; j > 0; j--) {
//交换
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
System.out.println(Arrays.toString(arr));
}
/**
* 将一个数组(二叉树),调整成一个大顶堆
* 功能:完成将以i 对应的非叶子节点的数调整成大顶堆
* 举例: int[] arr = {4, 6, 8, 5, 9}; --> i = 1 => adjustHeap --> 得到 {4, 9, 8, 5, 6}
* 如果我们再次调用adjustHeap 传入的是 i = 0 --> 得到 {9, 6, 8, 5, 4}
*
* @param arr 待调整的数组
* @param i 表示非叶子节点在数组中的索引
* @param length 表示对多少个元素进行调整,length是在逐渐的减少
*/
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i]; //先取出当前元素的值并保存在当前变量
//开始调整,说明:k = i * 2 + 1是i这个节点的左子节点
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) {
//说明这个左子节点值小于右子节点的值
k++; //k就指向右子节点
}
if (arr[k] > temp) { //如果子节点大于父节点
arr[i] = arr[k]; //把较大的值赋给当前节点
i = k; //i 指向k,继续循环比较
} else {
break;
}
}
//当for循环结束后,我们已经将以i 为父节点的树的最大值,放在了最顶上
arr[i] = temp; //将temp值放到调整后的位置
}
}
```
## 11.2 、赫夫曼树
### 11.2.1 、基本介绍
> 1) 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,**若该树的带权路径长度(wpl)达到最小**,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
>
> 2) 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
### 11.2.2 、赫夫曼树几个重要概念和举例说明
> 1) **路径和路径长度**:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通**路中分支的数目称为路径长度**。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为L-1
>
> 2) **结点的权及带权路径长度**:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。**结点的带权路径长度为**:从根结点到该结点之间的路径长度与该结点的权的乘积
>
> 3) 树的带权路径长度:树的带权路径长度规定为**所有叶子结点的带权路径长度之和**,记为WPL(weightedpathlength) ,权值越大的结点离根结点越近的二叉树才是最优二叉树
>
> 4) WPL 最小的就是赫夫曼树
>
> ![image-20221206084653981](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221206084653981.png)
### 11.2.3 、赫夫曼树创建思路图解
> 给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树
>
> 思路分析(示意图): {13, 7, 8, 3, 29, 6, 1}
>
> **构成赫夫曼树的步骤:**
>
> 1) 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
>
> 2) 取出根节点权值最小的两颗二叉树
>
> 3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
>
> 4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
>
> 5) 图解:
>
> ![image-20221206084716123](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221206084716123.png)
### 11.2.4、 赫夫曼树的代码实现
```JAVA
/**
* description
* 创建节点类
* 为了让Node对象支持排序,让Node继承Comparable接口
*
* @author xujicheng
* @since 2022年12月06日 10:29
*/
public class Node implements Comparable<Node> {
int value; //节点权值
Node left; //指向左子节点
Node right; //指向右子节点
//构造器,用于初始化权值
public Node(int value) {
this.value = value;
}
//写一个前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
return this.value - o.value; //表示从小到大进行排序
}
}
/**
* description
* 赫夫曼树的代码实现
*
* @author xujicheng
* @since 2022年12月06日 10:11
*/
public class HuffManTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
Node root = createHuffmanTree(arr);
preOrder(root);
}
/**
* 前序遍历的方法
*
* @param root 赫夫曼树的根节点
*/
public static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("该霍夫曼树是空树");
}
}
/**
* 创建赫夫曼树的方法
*
* @param arr 需要进行霍夫曼树排序的数组
*/
public static Node createHuffmanTree(int[] arr) {
//遍历arr这个数组
ArrayList<Node> nodes = new ArrayList<>();
for (int value : arr) {
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
//从小到大进行排序
Collections.sort(nodes);
//取出权值最小的二叉树
Node leftNode = nodes.get(0);
//取出权值此小的二叉树
Node rightNode = nodes.get(1);
//构建一颗新的二叉树
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//从ArrayList中删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//将parent加入到nodes
nodes.add(parent);
}
//返回最后这个节点
return nodes.get(0);
}
}
```
## 11.3 、赫夫曼编码
### 11.3.1 、基本介绍
> 1) 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
>
> 2) 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
>
> 3) 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间
>
> 4) 赫夫曼码是可变字长编码(VLC)的一种。Huffman 于 1952 年提出一种编码方法,称之为最佳编码
### 11.3.2 、原理剖析
> 通信领域中信息的处理方式 1-定长编码
>
> ![image-20221206152713272](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221206152713272.png)
>
> 通信领域中信息的处理方式 2-变长编码
>
> ![image-20221206152729812](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221206152729812.png)
>
> 通信领域中信息的处理方式 3-赫夫曼编码
>
> 步骤如下:
>
> 传输的 字符串
>
> * 1) i like like like java do you like a java
> * 2) d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
> * 3) 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
> * 步骤: 构成赫夫曼树的步骤:
> * 1) 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
> * 2) 取出根节点权值最小的两颗二叉树
> * 3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
> * 4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
>
> ![image-20221206152911245](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221206152911245.png)
>
> 4) 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为0 向右的路径为1,编码如下:
>
> * o: 1000 u: 10010 d: 100110 y: 100111 i: 101
> * a : 110 k: 1110 e: 1111 j: 0000 v: 0001
> * l: 001 : 01
>
> 5) 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为(注意这里我们使用的无损压缩)
>
> * 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 通过赫夫曼编码处理长度为133
>
> 6) 长度为 : 133
>
> * 说明: 原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
>
> 此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性赫夫曼编码是无损处理方案
>
> 注意事项
>
> * 注意, 这个赫夫曼树根据**排序方法不同**,也可能不太一样,这样对应的**赫夫曼编码也不完全一样**,但是**wpl** 是一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为
>
> ![image-20221206163310738](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221206163310738.png)
### 11.3.3、 最佳实践-数据压缩(创建赫夫曼树)
> 将给出的一段文本,比如 "i like like like java do you like a java" , 根据前面的讲的赫夫曼编码原理,对其进行数据 压 缩 理处理形式:
>
> 如"1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 "
>
> **步骤 1**:根据赫夫曼编码压缩数据的原理,需要创建 "i like like like java do you like a java" 对应的赫夫曼树
>
> 思路:
>
> * 1、构建一个新的节点Node ,Node的属性 Node{ data(用于存放数据), weight(权值) , left(指向左子节点) , right}
> * 2、得到 "i like like like java do you like a java" 对应的byte[]数组
> * 3、编写一个方法,将准备构建赫夫曼树的节点放到 List中,形式:[Node[date=97,weight = 5 ],Node[data = 32 weight = 9]]体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
> * 4、可以通过List创建对应的赫夫曼树
>
> ```JAVA
> /**
> * description
> * 创建Node,带数据和权值
> *
> * @author xujicheng
> * @since 2022年12月06日 17:15
> */
> public class Node implements Comparable<Node> {
>
> Byte data; //用于存放数据本身,比如'a' --> 97
> int weight; //权值,表示数据出现的次数
> Node left; //指向左边的节点
> Node right; //指向右边的节点
>
> //构造器,用于初始化权值和存放数据的data
> public Node(Byte data, int weight) {
> this.data = data;
> this.weight = weight;
> }
>
> @Override
> public int compareTo(Node o) {
> return this.weight - o.weight; //代表将来比较的时候从小到大排序
> }
>
> @Override
> public String toString() {
> return "Node{" +
> "data=" + data +
> ", weight=" + weight +
> '}';
> }
>
> //前序遍历
> public void preOrder() {
> System.out.println(this);
> if (this.left != null){
> this.left.preOrder();
> }
> if (this.right != null){
> this.right.preOrder();
> }
> }
> }
>
> /**
> * description
> * 赫夫曼编码的代码是实现
> *
> * @author xujicheng
> * @since 2022年12月06日 17:14
> */
> public class HuffManCode {
> public static void main(String[] args) {
> String content = "i like like like java do you like a java";
> byte[] contentBytes = content.getBytes();
> System.out.println(contentBytes.length);
> List<Node> nodes = getNodes(contentBytes);
> System.out.println(nodes);
> //测试创建二叉树
> System.out.println("赫夫曼树");
> Node huffmanTreeRoot = createHuffmanTree(nodes);
> System.out.println("前序遍历");
> huffmanTreeRoot.preOrder();
> }
>
> //前序遍历的方法
> private static void preorder(Node root) {
> if (root != null) {
> root.preOrder();
> } else {
> System.out.println("赫夫曼树为空");
> }
> }
>
> /**
> * 将左边构建赫夫曼树的Node 节点放到List
> *
> * @param bytes 需要构建的字节数
> * @return 返回的List形式 -->[Node[date=97,weight = 5 ],Node[data = 32 weight = 9]]
> */
> private static List<Node> getNodes(byte[] bytes) {
> //创建一个ArrayList
> ArrayList<Node> nodes = new ArrayList<Node>();
> //存储每个byte出现的次数,使用map来统计,key为数据本身,Integer是次数
> Map<Byte, Integer> counts = new HashMap<>();
> for (byte b : bytes) {
> Integer count = counts.get(b);
> if (count == null) { //说明Map还没有这个字符数据,是第一次放入
> counts.put(b, 1);
> } else {
> counts.put(b, count + 1);
> }
> }
> //把每个键值对转成Node对象并加入到nodes集合
> for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
> nodes.add(new Node(entry.getKey(), entry.getValue()));
> }
> return nodes;
> }
>
> //通过一个List创建对应的赫夫曼树
> private static Node createHuffmanTree(List<Node> nodes) {
> while (nodes.size() > 1) {
> //排序,从小到大
> Collections.sort(nodes);
> //取出第一颗最小的二叉树
> Node leftNode = nodes.get(0);
> //取出第二颗最小的二叉树
> Node rightNode = nodes.get(1);
> //创建一颗新的二叉树,它的根节点没有data,只有权值
> Node parent = new Node(null, leftNode.weight + rightNode.weight);
> parent.left = leftNode;
> parent.right = rightNode;
> //将已经处理的两棵二叉树从nodes中删除
> nodes.remove(leftNode);
> nodes.remove(rightNode);
> //将新的二叉树加入到nodes
> nodes.add(parent);
> }
> //返回的nodes最后的节点,就是赫夫曼树的root节点
> return nodes.get(0);
> }
> }
> ```
>
>
### 11.3.4 、最佳实践-数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)
> 我们已经生成了 赫夫曼树, 下面我们继续完成任务
>
> 1) 生成赫夫曼树对应的赫夫曼编码 , 如下表: =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
>
> 2) 使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like ajava"字符串生成对应的编码数据, 形式如下.
>
> * 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
>
> 3) 思路:
>
> * 1、将赫夫曼编码表存放在Map中<Byte ,String>
> * 2、在生成赫夫曼编码表时需要去拼接路径,定义一个StringBuilder 存储某个叶子节点的路径
>
> 4)代码实现
>
> ```JAVA
> //为了调用方便重载getCodes方法
> private static Map<Byte,String>getCodes(Node root){
> if (root == null){
> return null;
> }
> //处理root的左子树
> getCodes(root.left,"0",stringBuffer);
> //处理root的右子树
> getCodes(root.right,"1",stringBuffer);
> return huffManCodes;
> }
>
> //将赫夫曼编码表存放在Map中<Byte ,String>
> static Map<Byte, String> huffManCodes = new HashMap<>();
> //在生成赫夫曼编码表时需要去拼接路径,定义一个StringBuilder 存储某个叶子节点的路径
> static StringBuffer stringBuffer = new StringBuffer();
>
> /**
> * 将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中
> *
> * @param node 传入节点
> * @param code 路径:左子节点是0,右子节点是1
> * @param stringBuffer 用于拼接路径
> */
> private static void getCodes(Node node, String code, StringBuffer stringBuffer) {
> StringBuffer stringBuffer2 = new StringBuffer(stringBuffer);
> //将传入的code加入stringBuffer2
> stringBuffer2.append(code);
> if (node != null){ //如果node等于空不处理
> //判断当前node是叶子节点还是非叶子节点
> if (node.data == null){ //说明是非叶子节点
> //向左递归处理
> getCodes(node.left,"0",stringBuffer2);
> //向右递归
> getCodes(node.right,"1",stringBuffer2);
> } else { //说明是叶子节点
> //就表示找到了某个叶子节点的最后
> huffManCode.put(node.data,stringBuffer2.toString());
> }
> }
> }
>
> /**
> * 编写一个方法,将一个字符串对应的byte[]数组,通过生成的赫夫曼编码,返回一个压缩后的byte[]
> *
> * @param bytes 原始的字符串对应的byte数组
> * @param huffmanCodes 生成的赫夫曼编码map
> * @return 返回赫夫曼编码处理后的byte[]
> */
> private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
> //利用huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
> StringBuffer stringBuffer = new StringBuffer();
> //遍历bytes 数组
> for (byte b : bytes) {
> stringBuffer.append(huffmanCodes.get(b));
> }
> //将对应的字符串转成byte[]
>
> //统计返回的byte[] huffManCodesBytes 长度
> int length;
> if (stringBuffer.length() % 8 == 0) {
> length = stringBuffer.length() / 8;
> } else {
> length = stringBuffer.length() / 8 + 1;
> }
> //创建存储压缩后的byte数组
> int index = 0; //记录是第几个byte
> byte[] huffManCodesBytes = new byte[length];
> for (int i = 0; i < stringBuffer.length(); i += 8) { //因为是每八位对应一个byte,步长+8
> String strByte;
> if (i + 8 > stringBuffer.length()) { //不够八位
> strByte = stringBuffer.substring(i);
> } else {
> strByte = stringBuffer.substring(i, i + 8);
> }
> //将strByte转成一个byte数组,放入到huffManCodesBytes
> huffManCodesBytes[index] = (byte) Integer.parseInt(strByte, 2);
> index++;
> }
> return huffManCodesBytes;
> }
>
> /**
> * 使用一个方法,将全面的方法封装起来便于我们调用
> *
> * @param bytes 原始的字符串对应的字节数组
> * @return 经过赫夫曼编码处理后的字节数组,即压缩后的数组
> */
> private static byte[] huffmanZip(byte[] bytes) {
> //1、将需要构建成赫夫曼树的节点放入List数组中
> List<Node> nodes = getNodes(bytes);
> //2、根据传入的节点创建赫夫曼树
> Node huffmanTree = createHuffmanTree(nodes);
> //3、根据赫夫曼树生成对应的赫夫曼编码
> Map<Byte, String> huffmanCodes = getCodes(huffmanTree);
> //4、根据生成的赫夫曼编码压缩,压缩后得到的赫夫曼编码字节数组
> byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
> //5、将压缩后的编码字节返回即可
> return huffmanCodeBytes;
> }
> ```
>
>
### 11.3.5 、最佳实践-数据解压(使用赫夫曼编码解码)
> 使用赫夫曼编码来解码数据,具体要求是
>
> 1) 前面我们得到了赫夫曼编码和对应的编码 byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77 , -57, 6, -24, -14, -117, -4, -60, -90, 28]
>
> 2) 现在要求使用赫夫曼编码, 进行解码,又 重新得到原来的字符串"i like like like java do you like a java"
>
> 3) 思路:解码过程,就是编码的一个逆向操作
>
> * 1、先将赫夫曼的数组重新转成赫夫曼编码对应的二进制字符串
> * 2、将赫夫曼编码对应的二进制字符串--->对照赫夫曼编码--->转成最初的字符串
>
> 4)代码实现
>
> ```JAVA
> /**
> * 将一个byte转成一个二进制的字符串
> *
> * @param bytes 需要转换的byte
> * @param flag 标志位,用于判断是否需要补高位
> * @return byte对应的二进制字符串,按补码返回的
> */
> private static String byteToBitString(boolean flag, byte bytes) {
> //使用一个变量保存bytes
> int temp = bytes; //将bytes转成int类型
> //如果是正数还存在一个补高位
> if (flag) {
> temp |= 256;
> }
> String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
> if (flag) {
> return str.substring(str.length() - 8);
> } else {
> return str;
> }
> }
>
> /**
> * 编写一个方法,完成对压缩数据的解码
> *
> * @param huffManCodes 赫夫曼编码map
> * @param huffmanBytes 赫夫曼编码得到的字节数组
> * @return 原来字符串对应的数组
> */
> private static byte[] decode(Map<Byte, String> huffManCodes, byte[] huffmanBytes) {
> //1、先得到huffmanBytes对应的二进制的字符串
> StringBuilder stringBuilder = new StringBuilder();
> //2、将byte[]转成二进制的字符串
> for (int i = 0; i < huffmanBytes.length; i++) {
> byte b = huffmanBytes[i];
> boolean flag = (i == huffmanBytes.length - 1);
> stringBuilder.append(byteToBitString(!flag, b));
> }
> //把字符串按照指定的赫夫曼编码进行解码,即把赫夫曼编码表进行调换,反向查询
> Map<String, Byte> map = new HashMap<>();
> for (Map.Entry<Byte, String> entry : huffManCodes.entrySet()) {
> map.put(entry.getValue(), entry.getKey());
> }
> //创建一个集合存放byte
> List<Byte> list = new LinkedList<>();
> //i可以理解成一个索引,在扫描二进制对应的字符串
> for (int i = 0; i < stringBuilder.length();) {
> int count = 0; //计数器
> boolean flag = true;
> Byte b = null;
> while (flag) {
> //递增取出key
> String key = stringBuilder.substring(i, i + count); //i不动,让count移动
> b = map.get(key);
> if (b == null) { //说明还没有匹配到
> count++;
> } else {
> flag = false;
> }
> }
> list.add(b);
> i += count; //让i直接移动到count的位置
> }
> //当for循环结束后,list中就存放了所有的字符,把list中放入一个byte[]并返回
> byte[] b = new byte[list.size()];
> for (int i = 0; i < b.length; i++) {
> b[i] = list.get(i);
> }
> return b;
> }
>
> ```
>
>
### 11.3.6 、最佳实践-文件压缩
> 我们学习了通过赫夫曼编码对一个字符串进行编码和解码, 下面我们来完成对文件的压缩和解压,具体要求:给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
>
> 1) 思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩
>
> 2) 代码实现:
>
> ```JAVA
> /**
> * 编写一个方法,将一个文件进行压缩
> *
> * @param srcFile 传入的希望压缩文件的完整的路径
> * @param destFile 压缩后将压缩文件放到哪个目录下
> */
> public static void zipFile(String srcFile, String destFile) {
> //创建输出流
> OutputStream os = null;
> //创建一个文件的输入流,准备读取文件
> FileInputStream is = null;
> //创建一个和文件输出流关联的ObjectOutputStream
> ObjectOutputStream oos = null;
> try {
> is = new FileInputStream(srcFile);
> //创建一个和源文件大小一致的byte[]
> byte[] bytes = new byte[is.available()];
> //读取文件
> is.read(bytes);
> //直接对源文件进行压缩,得到赫夫曼对应的字节数组
> byte[] huffmanBytes = huffmanZip(bytes);
> //创建文件的输出流准备存放压缩文件
> os = new FileOutputStream(destFile);
> //创建一个和文件输出流关联的ObjectOutputStream
> oos = new ObjectOutputStream(os);
> //把赫夫曼编码后的字节数组写入压缩文件
> oos.writeObject(huffmanBytes);
> //这里我们以对象流的方式写入赫夫曼的编码,目的是为了恢复源文件时使用
> oos.writeObject(huffManCodes);
> } catch (Exception e) {
> System.out.println(e.getMessage());
> } finally {
> try {
> is.close();
> os.close();
> oos.close();
> } catch (IOException e) {
> System.out.println(e.getMessage());
> }
> }
> }
> ```
>
>
### 11.3.7 、最佳实践-文件解压(文件恢复)
> 具体要求:将前面压缩的文件,重新恢复成原来的文件。
>
> 1) 思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)
>
> 2) 代码实现:
>
> ```JAVA
> /**
> * 编写一个方法,完成对压缩文件的解压
> *
> * @param zipFile 准备解压的文件
> * @param dstFile 将文件解压到哪个位置
> */
> public static void unZipFile(String zipFile, String dstFile) {
> //定义文件输入流
> InputStream is = null;
> //定义一个对象输入流
> ObjectInputStream ois = null;
> //定义文件的输出流
> OutputStream os = null;
> try {
> //创建文件输入流
> is = new FileInputStream(zipFile);
> //创建一个和is关联的对象输入流
> ois = new ObjectInputStream(is);
> //读取byte[] huffmanBytes
> byte[] huffmanBytes = (byte[]) ois.readObject();
> //读取赫夫曼编码表
> Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
> //解码
> byte[] bytes = decode(huffmanCodes, huffmanBytes);
> //将bytes数组写入到目标文件
> os = new FileOutputStream(dstFile);
> //写数据到dstFile文件中
> os.write(bytes);
> } catch (Exception e) {
> System.out.println(e.getMessage());
> } finally {
> try {
> os.close();
> ois.close();
> is.close();
> } catch (IOException e) {
> System.out.println(e.getMessage());
> }
> }
> }
> ```
>
>
### 11.3.8 、代码汇总,把前面所有的方法放在一起
```JAVA
/**
* description
* 赫夫曼编码的代码是实现
*
* @author xujicheng
* @since 2022年12月06日 17:14
*/
public class HuffManCode {
public static void main(String[] args) {
String zipFile = "D://001-file//dst.zip";
String dstFile = "D://001-file";
unZipFile(zipFile, dstFile);
System.out.println("解压成功");
}
/**
* 编写一个方法,将一个文件进行压缩
*
* @param srcFile 传入的希望压缩文件的完整的路径
* @param destFile 压缩后将压缩文件放到哪个目录下
*/
public static void zipFile(String srcFile, String destFile) {
//创建输出流
OutputStream os = null;
//创建一个文件的输入流,准备读取文件
FileInputStream is = null;
//创建一个和文件输出流关联的ObjectOutputStream
ObjectOutputStream oos = null;
try {
is = new FileInputStream(srcFile);
//创建一个和源文件大小一致的byte[]
byte[] bytes = new byte[is.available()];
//读取文件
is.read(bytes);
//直接对源文件进行压缩,得到赫夫曼对应的字节数组
byte[] huffmanBytes = huffmanZip(bytes);
//创建文件的输出流准备存放压缩文件
os = new FileOutputStream(destFile);
//创建一个和文件输出流关联的ObjectOutputStream
oos = new ObjectOutputStream(os);
//把赫夫曼编码后的字节数组写入压缩文件
oos.writeObject(huffmanBytes);
//这里我们以对象流的方式写入赫夫曼的编码,目的是为了恢复源文件时使用
oos.writeObject(huffManCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
is.close();
os.close();
oos.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
/**
* 编写一个方法,完成对压缩文件的解压
*
* @param zipFile 准备解压的文件
* @param dstFile 将文件解压到哪个位置
*/
public static void unZipFile(String zipFile, String dstFile) {
//定义文件输入流
InputStream is = null;
//定义一个对象输入流
ObjectInputStream ois = null;
//定义文件的输出流
OutputStream os = null;
try {
//创建文件输入流
is = new FileInputStream(zipFile);
//创建一个和is关联的对象输入流
ois = new ObjectInputStream(is);
//读取byte[] huffmanBytes
byte[] huffmanBytes = (byte[]) ois.readObject();
//读取赫夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
//解码
byte[] bytes = decode(huffmanCodes, huffmanBytes);
//将bytes数组写入到目标文件
os = new FileOutputStream(dstFile);
//写数据到dstFile文件中
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
os.close();
ois.close();
is.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
/**
* 编写一个方法,完成对压缩数据的解码
*
* @param huffManCodes 赫夫曼编码map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 原来字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffManCodes, byte[] huffmanBytes) {
//1、先得到huffmanBytes对应的二进制的字符串
StringBuilder stringBuilder = new StringBuilder();
//2、将byte[]转成二进制的字符串
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
//把字符串按照指定的赫夫曼编码进行解码,即把赫夫曼编码表进行调换,反向查询
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffManCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建一个集合存放byte
List<Byte> list = new LinkedList<>();
//i可以理解成一个索引,在扫描二进制对应的字符串
for (int i = 0; i < stringBuilder.length(); ) {
int count = 0; //计数器
boolean flag = true;
Byte b = null;
while (flag) {
//递增取出key
String key = stringBuilder.substring(i, i + count); //i不动,让count移动
b = map.get(key);
if (b == null) { //说明还没有匹配到
count++;
} else {
flag = false;
}
}
list.add(b);
i += count; //让i直接移动到count的位置
}
//当for循环结束后,list中就存放了所有的字符,把list中放入一个byte[]并返回
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 将一个byte转成一个二进制的字符串
*
* @param bytes 需要转换的byte
* @param flag 标志位,用于判断是否需要补高位,若是最后一个字节,无需补高位
* @return byte对应的二进制字符串,按补码返回的
*/
private static String byteToBitString(boolean flag, byte bytes) {
//使用一个变量保存bytes
int temp = bytes; //将bytes转成int类型
//如果是正数还存在一个补高位
if (flag) {
temp |= 256;
}
String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
if (flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
/**
* 使用一个方法,将全面的方法封装起来便于我们调用
*
* @param bytes 原始的字符串对应的字节数组
* @return 经过赫夫曼编码处理后的字节数组,即压缩后的数组
*/
private static byte[] huffmanZip(byte[] bytes) {
//1、将需要构建成赫夫曼树的节点放入List数组中
List<Node> nodes = getNodes(bytes);
//2、根据传入的节点创建赫夫曼树
Node huffmanTree = createHuffmanTree(nodes);
//3、根据赫夫曼树生成对应的赫夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTree);
//4、根据生成的赫夫曼编码压缩,压缩后得到的赫夫曼编码字节数组
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
//5、将压缩后的编码字节返回即可
return huffmanCodeBytes;
}
/**
* 编写一个方法,将一个字符串对应的byte[]数组,通过生成的赫夫曼编码,返回一个压缩后的byte[]
*
* @param bytes 原始的字符串对应的byte数组
* @param huffmanCodes 生成的赫夫曼编码map
* @return 返回赫夫曼编码处理后的byte[]
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//利用huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
StringBuffer stringBuffer = new StringBuffer();
//遍历bytes 数组
for (byte b : bytes) {
stringBuffer.append(huffmanCodes.get(b));
}
//将对应的字符串转成byte[]
//统计返回的byte[] huffManCodesBytes 长度
int length;
if (stringBuffer.length() % 8 == 0) {
length = stringBuffer.length() / 8;
} else {
length = stringBuffer.length() / 8 + 1;
}
//创建存储压缩后的byte数组
int index = 0; //记录是第几个byte
byte[] huffManCodesBytes = new byte[length];
for (int i = 0; i < stringBuffer.length(); i += 8) { //因为是每八位对应一个byte,步长+8
String strByte;
if (i + 8 > stringBuffer.length()) { //不够八位
strByte = stringBuffer.substring(i);
} else {
strByte = stringBuffer.substring(i, i + 8);
}
//将strByte转成一个byte数组,放入到huffManCodesBytes
huffManCodesBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffManCodesBytes;
}
//前序遍历的方法
private static void preorder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("赫夫曼树为空");
}
}
//为了调用方便重载getCodes方法
private static Map<Byte, String> getCodes(Node root) {
if (root == null) {
return null;
}
//处理root的左子树
getCodes(root.left, "0", stringBuffer);
//处理root的右子树
getCodes(root.right, "1", stringBuffer);
return huffManCodes;
}
//将赫夫曼编码表存放在Map中<Byte ,String>
static Map<Byte, String> huffManCodes = new HashMap<>();
//在生成赫夫曼编码表时需要去拼接路径,定义一个StringBuilder 存储某个叶子节点的路径
static StringBuffer stringBuffer = new StringBuffer();
/**
* 将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中
*
* @param node 传入节点
* @param code 路径:左子节点是0,右子节点是1
* @param stringBuffer 用于拼接路径
*/
private static void getCodes(Node node, String code, StringBuffer stringBuffer) {
StringBuffer stringBuffer2 = new StringBuffer(stringBuffer);
//将传入的code加入stringBuffer2
stringBuffer2.append(code);
if (node != null) { //如果node等于空不处理
//判断当前node是叶子节点还是非叶子节点
if (node.data == null) { //说明是非叶子节点
//向左递归处理
getCodes(node.left, "0", stringBuffer2);
//向右递归
getCodes(node.right, "1", stringBuffer2);
} else { //说明是叶子节点
//就表示找到了某个叶子节点的最后
huffManCodes.put(node.data, stringBuffer2.toString());
}
}
}
/**
* 将左边构建赫夫曼树的Node 节点放到List
*
* @param bytes 需要构建的字节数
* @return 返回的List形式 -->[Node[date=97,weight = 5 ],Node[data = 32 weight = 9]]
*/
private static List<Node> getNodes(byte[] bytes) {
//创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
//存储每个byte出现的次数,使用map来统计,key为数据本身,Integer是次数
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) { //说明Map还没有这个字符数据,是第一次放入
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
//把每个键值对转成Node对象并加入到nodes集合
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
//通过一个List创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
//排序,从小到大
Collections.sort(nodes);
//取出第一颗最小的二叉树
Node leftNode = nodes.get(0);
//取出第二颗最小的二叉树
Node rightNode = nodes.get(1);
//创建一颗新的二叉树,它的根节点没有data,只有权值
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//将已经处理的两棵二叉树从nodes中删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将新的二叉树加入到nodes
nodes.add(parent);
}
//返回的nodes最后的节点,就是赫夫曼树的root节点
return nodes.get(0);
}
}
```
### 11.3.9、 赫夫曼编码压缩文件注意事项
* 1) 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件[举例压一个.ppt]
* 2) 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml 文件]
* 3) 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显
## 11.4、二叉排序树
### 11.4.1、 先看一个需求
给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
### 11.4.2 、解决方案分析
> 使用数组 数组未排序
>
> * 优点:直接在数组尾添加,速度快。 缺点:查找速度慢. [示意图] 数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。
> 使用链式存储-链表
>
> * 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动
>
> 使用二叉排序树
### 11.4.3 、二叉排序树介绍
> **二叉排序树:**BST: (Binary Sort(Search) Tree), 对于二叉排序树的**任何一个非叶子节点**,**要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大**
>
> **特别说明**:如果有相同的值,可以将该节点放在左子节点或右子节点
>
> 比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
>
> ![image-20221208090734374](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221208090734374.png)
### 11.4.4 、二叉排序树创建和遍历
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) ,创建成对应的二叉排序树为 :
![image-20221208091340941](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221208091340941.png)
代码实现如下:
```JAVA
/**
* description
* 节点
*
* @author xujicheng
* @since 2022年12月08日 9:28
*/
public class Node {
int value; //权值
Node left; //左子节点
Node right; //右子节点
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
/**
* 添加节点的方法,使用递归的方式添加节点,需要满足二叉排序树的要求
*
* @param node 需要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
//判断传入节点的值和当前子树的根节点的值的关系
if (node.value < this.value) {
if (this.left == null) { //如果当前节点左子节点为空,直接挂在左子节点即可
this.left = node;
} else {
this.left.add(node); //递归的向左子树添加
}
} else { //添加的节点的值大于当前节点的值
if (this.right == null) {
this.right = node;
} else {
this.right.add(node); //递归的向右子树添加
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
/**
* description
* 二叉排序树
*
* @author xujicheng
* @since 2022年12月08日 9:39
*/
public class BinarySortTree {
private Node root; //根节点
/**
* 添加节点的方法
*
* @param node 需要加入的节点
*/
public void add(Node node) {
if (root == null){
root = node; //如果root为空则直接让root指向node
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder(){
if (root != null){
root.infixOrder();
}else {
System.out.println("当前二叉排序树为空,不能遍历");
}
}
}
/**
* description
* 二叉排序数代码实现
*
* @author xujicheng
* @since 2022年12月08日 9:27
*/
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9};
BinarySortTree binarySortTree = new BinarySortTree();
//循环的添加节点到二叉排序树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历二叉排序树
System.out.println("中序遍历二叉排序树");
binarySortTree.infixOrder();
}
}
```
### 11.4.5 、二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
* 1) 删除叶子节点 (比如:2, 5, 9, 12)
* 2) 删除只有一颗子树的节点 (比如:1)
* 3) 删除有两颗子树的节点. (比如:7, 3,10 )
* 4) 操作的思路分析
![image-20221208090842262](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221208090842262.png)
对删除节点的各种情况的思路分析:
* 第一种情况:删除叶子节点
* 思路
* 1、需要先去找到要删除的节点 targetNode
* 2、找到targetNode 的父节点 parent
* 3、确定 targetNode 是 parent 的左子结点 还是右子结点
* 4、 根据前面的情况来对应删除
* 左子结点 parent.left = null 右子结点 parent.right = null;
* 第二种情况: 删除只有一颗子树的节点
* 思路
* 1、(1) 需求先去找到要删除的结点 targetNode
* 2、找到 targetNode 的 父结点 parent
* 3、确定 targetNode 的子结点是左子结点还是右子结点
* 4、targetNode 是 parent 的左子结点还是右子结点
* 5、如果 targetNode 有左子结点
* 5.1、如果 targetNode 是 parent 的左子结点
parent.left = targetNode.left;
* 5.2、如果 targetNode 是 parent 的右子结点
parent.right = targetNode.left;
* 6、如果 targetNode 有右子结点
* 6.1、如果 targetNode 是 parent 的左子结点
parent.left = targetNode.right;
* 6.2、如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right;
* 情况三 : 删除有两颗子树的节点.
* 思路:
* 1、需要先去找到要删除的结点 targetNode
* 2、找到 targetNode 的 父结点 parent
* 3、从 targetNode 的右子树找到最小的结点
* 4、用一个临时变量,将 最小结点的值保存 temp
* 5、删除该最小结点
* 6、targetNode.value = temp
### 11.4.6 、二叉排序树删除结点的代码实现
```JAVA
在Node节点中加入以下两个方法:
/**
* 查找要删除的节点
*
* @param value 希望删除的结点的值
* @return 如果找到返回该结点,否则返回 null
*/
public Node search(int value) {
if (value == this.value) { //找到就是该结点
return this;
} else if (value < this.value) {//如果查找的值小于当前结点,向左子树递归查找//如果左子结点为空
if (this.left == null) {
return null;
}
return this.left.search(value);
} else { //如果查找的值不小于当前结点,向右子树递归查找
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除节点的父节点
*
* @param value 需要查找的值
* @return 要删除的节点的父节点,如果没有就返回null
*/
public Node searchParent(int value) {
//如果当前结点就是要删除的结点的父结点,就返回
if ((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else {
//如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); //向左子树递归查找
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); //向右子树递归查找
} else {
return null; // 没有找到父结点
}
}
}
在BinarySortTree中加入对应的具体实现方法
/**
* 查找要删除的节点
*
* @param value 希望删除的节点的值
* @return 如果找到返回该节点,否则返回null
*/
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
/**
* 查找父节点
*
* @param value 需要查找的值
* @return 要删除的节点的父节点,如果没有就返回null
*/
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 删除节点
*
* @param value 需要删除的节点的值
*/
public void delNode(int value) {
if (root == null) {
return;
} else {
//需要先找到要删除的节点 targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点就无需向下执行
if (targetNode == null) {
return;
}
//如果我们发现当前这颗二叉树排序树只有一个节点
if (root.left == null && root.right == null) {
root = null;
return;
}
//去找到 targetNode 的父结点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.left == null && targetNode.right == null) {
//判断 targetNode 是父结点的左子结点,还是右子结点
if (parent.left != null && parent.left.value == value) { //是左子结点
parent.left = null;
} else if (parent.right != null && parent.right.value == value) {//是由子结点parent.right = null;
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) { //删除有两颗子树的节点int minVal = delRightTreeMin(targetNode.right);
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
} else { // 删除只有一颗子树的结点
//如果要删除的结点有左子结点
if (targetNode.left != null) {
if (parent != null) {
//如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.left;
} else { // targetNode 是 parent 的右子结点
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else { //如果要删除的结点有右子结点
if (parent != null) {
//如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.right;
} else { //如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
```
### 11.4.7 、课后练习:完成老师代码,并使用第二种方式来解决
* 如果我们从左子树找到最大的结点,然后前面的思路完成
```JAVA
//作业
//编写方法
//1.返回以node为根节点的二叉排序树的最大节点的值
//2.删除以node为根节点的二叉排序树的最大节点
public int delLeftTreeMin(Node node){
Node target = node;
//循环的查找右节点,就会找到最大值
while (target.right != null){
target = target.right;
}
//这时,target就指向了最小节点
//删除最小节点
delNode(target.value);
return target.value;
}
//这一段在delNode方法里
else if(targetNode.left != null && targetNode.right != null){//删除左右两边都有子树的节点
//int minValue = delRightTreeMin(targetNode.right);
int minValue = delLeftTreeMin(targetNode.left);//这里解释一下,目标节点的右子树的最小节点比目标节点的左子树里的所有节点都大,所以向右找,就找一个最小的(老师代码的含义),向左找左子树那就要找最大的,说的通俗一点就是,矮个子里挑高个,高个子里挑矮个
targetNode.value = minValue;
}
```
### 11.4.8、完整代码整合
```JAVA
/**
* description
* 节点
*
* @author xujicheng
* @since 2022年12月08日 9:28
*/
public class Node {
int value; //权值
Node left; //左子节点
Node right; //右子节点
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
/**
* 查找要删除的节点
*
* @param value 希望删除的结点的值
* @return 如果找到返回该结点,否则返回 null
*/
public Node search(int value) {
if (value == this.value) { //找到就是该结点
return this;
} else if (value < this.value) {//如果查找的值小于当前结点,向左子树递归查找//如果左子结点为空
if (this.left == null) {
return null;
}
return this.left.search(value);
} else { //如果查找的值不小于当前结点,向右子树递归查找
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除节点的父节点
*
* @param value 需要查找的值
* @return 要删除的节点的父节点,如果没有就返回null
*/
public Node searchParent(int value) {
//如果当前结点就是要删除的结点的父结点,就返回
if ((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else {
//如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); //向左子树递归查找
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); //向右子树递归查找
} else {
return null; // 没有找到父结点
}
}
}
/**
* 添加节点的方法,使用递归的方式添加节点,需要满足二叉排序树的要求
*
* @param node 需要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
//判断传入节点的值和当前子树的根节点的值的关系
if (node.value < this.value) {
if (this.left == null) { //如果当前节点左子节点为空,直接挂在左子节点即可
this.left = node;
} else {
this.left.add(node); //递归的向左子树添加
}
} else { //添加的节点的值大于当前节点的值
if (this.right == null) {
this.right = node;
} else {
this.right.add(node); //递归的向右子树添加
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
/**
* description
* 二叉排序树
*
* @author xujicheng
* @since 2022年12月08日 9:39
*/
public class BinarySortTree {
private Node root; //根节点
/**
* 查找要删除的节点
*
* @param value 希望删除的节点的值
* @return 如果找到返回该节点,否则返回null
*/
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
/**
* 查找父节点
*
* @param value 需要查找的值
* @return 要删除的节点的父节点,如果没有就返回null
*/
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 课后作业
*
* @param node 传入的结点(当做二叉排序树的根结点)
* @return 返回的 以 node 为根结点的二叉排序树的最小结点的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
//循环的查找左子节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
//这时 target 就指向了最小结点,删除最小结点
delNode(target.value);
return target.value;
}
/**
* 删除节点
*
* @param value 需要删除的节点的值
*/
public void delNode(int value) {
if (root == null) {
return;
} else {
//需要先找到要删除的节点 targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点就无需向下执行
if (targetNode == null) {
return;
}
//如果我们发现当前这颗二叉树排序树只有一个节点
if (root.left == null && root.right == null) {
root = null;
return;
}
//去找到 targetNode 的父结点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.left == null && targetNode.right == null) {
//判断 targetNode 是父结点的左子结点,还是右子结点
if (parent.left != null && parent.left.value == value) { //是左子结点
parent.left = null;
} else if (parent.right != null && parent.right.value == value) {//是由子结点parent.right = null;
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) { //删除有两颗子树的节点int minVal = delRightTreeMin(targetNode.right);
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
} else { // 删除只有一颗子树的结点
//如果要删除的结点有左子结点
if (targetNode.left != null) {
if (parent != null) {
//如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.left;
} else { // targetNode 是 parent 的右子结点
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else { //如果要删除的结点有右子结点
if (parent != null) {
//如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.right;
} else { //如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
/**
* 添加节点的方法
*
* @param node 需要加入的节点
*/
public void add(Node node) {
if (root == null) {
root = node; //如果root为空则直接让root指向node
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("当前二叉排序树为空,不能遍历");
}
}
}
//测试部分省略
```
## 11.5 、平衡二叉树(AVL 树)
### 11.5.1、 看一个案例(说明二叉排序树可能的问题)
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.
![image-20221209091411087](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209091411087.png)
* 左边 BST 存在的问题分析:
* 1) 左子树全部为空,从形式上看,更像一个单链表.
* 2) 插入速度没有影响
* 3) 查询速度明显降低(因为需要依次比较), 不能发挥 BST的优势,因为每次还需要比较左子树,其查询速度比 单链表还慢
* 的优势,因为每次还需要比较左子树,其查询速度比 单链表还慢
### 11.5.2、 基本介绍
1) 平衡二叉树也叫平衡**二叉搜索树**(Self-balancing binary search tree)又被称为 AVL 树,可以保证查询效率较高。
2) 具有以下特点:它是**一 棵空树**或**它的左右两个子树的高度差的绝对值不超过 1**,并且**左右两个子树都是一棵平衡二叉树**。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
3) 举例说明, 看看下面哪些 AVL 树
![image-20221209091516371](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209091516371.png)
## 11.5.3、二平衡树的高度求解
```JAVA
在Node节点中加入以下方法
//返回右子树的高度
public int rightHeight() {
if (right == null){
return 0;
}
return right.height();
}
//返回以该节点为根节点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
/**
* description
* 二叉平衡树的高度求解测试
*
* @author xujicheng
* @since 2022年12月09日 9:47
*/
public class AvlTreeDemo {
public static void main(String[] args) {
int[] arr = {4, 3, 6, 5, 7, 8};
//创建一个AVLTree对象
AvlTree avlTree = new AvlTree();
//添加节点
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
System.out.println("中序遍历");
avlTree.infixOrder();
System.out.println("在没有做平衡处理前");
System.out.println("树的高度" + avlTree.getRoot().height());
System.out.println("树的左子树的高度" + avlTree.getRoot().leftHeight());
System.out.println("树的右子树的高度" + avlTree.getRoot().rightHeight());
}
}
```
> 代码说明:Node节点和二叉排序树的代码我可以复用,直接添加相应求高度解的方法即可
>
> 这里我们经过测试可以得出在二叉平衡树之前左子树和右子树的高度为
>
> ![image-20221209104243571](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209104243571.png)
### 11.5.4、应用案例-单旋转(左旋转)
1) 要求: 给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}
2) 思路分析(示意图)
![image-20221209093227182](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209093227182.png)
3) 代码实现
```JAVA
在Node节点类中加入左旋转的方法,并在加入节点的方法中加入左旋转的判断逻辑
//左旋转的方法
private void leftRotate() {
//创建新的节点,以当前根节点的值创建的
Node node = new Node(value);
//把新的节点的左子树,设置成当前节点的左子树
node.left = left;
//把新的节点的右子树,设置成当前节点的右子树的左子树
node.right = right.left;
//把当前节点的值换成右子节点的值
value = right.value;
//把当前节点的右子树设置成右子树的右子树
right = right.right;
//把当前节点的左子树设置成新的节点
left = node;
}
/**
* 添加节点的方法,使用递归的方式添加节点,需要满足二叉排序树的要求
*
* @param node 需要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
//判断传入节点的值和当前子树的根节点的值的关系
if (node.value < this.value) {
if (this.left == null) { //如果当前节点左子节点为空,直接挂在左子节点即可
this.left = node;
} else {
this.left.add(node); //递归的向左子树添加
}
} else { //添加的节点的值大于当前节点的值
if (this.right == null) {
this.right = node;
} else {
this.right.add(node); //递归的向右子树添加
}
}
//当添加完一个节点后,如果 (右子树的高度 - 左子树的高度) > 1,就将该树左旋转
if (rightHeight() - leftHeight() > 1) {
leftRotate(); //左旋转
}
}
```
> 在经过左旋转之后的二叉树高度为
>
> ![image-20221209110345025](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209110345025.png)
### 11.5.5、 应用案例-单旋转(右旋转)
1) 要求: 给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}
2)右旋转的思路分析
![image-20221209110352768](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209110352768.png)
3)代码实现
```JAVA
在Node节点类中加入右旋转的方法,并在加入节点的方法中加入左旋转的判断逻辑
//右旋转的方法
private void rightRotate() {
//创建新的节点,以当前根节点的值创建的
Node node = new Node(value);
//把新的节点的右子树,设置成当前节点的右子树
node.right = right;
//把新的节点的左子树,设置成当前节点的左子树的右子树
node.left = left.right;
//把当前节点的值换成左子节点的值
value = left.value;
//把当前节点的做指数设置成左子树的左子树
left = left.left;
//把当前节点的右子树设置成新的节点
right = node;
}
/**
* 添加节点的方法,使用递归的方式添加节点,需要满足二叉排序树的要求
*
* @param node 需要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
//判断传入节点的值和当前子树的根节点的值的关系
if (node.value < this.value) {
if (this.left == null) { //如果当前节点左子节点为空,直接挂在左子节点即可
this.left = node;
} else {
this.left.add(node); //递归的向左子树添加
}
} else { //添加的节点的值大于当前节点的值
if (this.right == null) {
this.right = node;
} else {
this.right.add(node); //递归的向右子树添加
}
}
//当添加完一个节点后,如果 (右子树的高度 - 左子树的高度) > 1,就将该树左旋转
if (rightHeight() - leftHeight() > 1) {
leftRotate(); //左旋转
}
//当添加完一个节点后,如果 (左子树的高度 - 右子树的高度) > 1,就将该树右旋转
if (leftHeight() - rightHeight() > 1) {
rightRotate();
}
}
/**
* description
* 测试右旋转
*
* @author xujicheng
* @since 2022年12月09日 9:47
*/
public class AvlTreeDemo {
public static void main(String[] args) {
int[] arr = {10, 12, 8, 9, 7, 6};
//创建一个AVLTree对象
AvlTree avlTree = new AvlTree();
//添加节点
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
System.out.println("中序遍历");
avlTree.infixOrder();
System.out.println("在做平衡处理后");
System.out.println("树的高度" + avlTree.getRoot().height());
System.out.println("树的左子树的高度" + avlTree.getRoot().leftHeight());
System.out.println("树的右子树的高度" + avlTree.getRoot().rightHeight());
}
}
```
> 在经过右旋转之后的二叉树高度为
>
> ![image-20221209111846955](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209111846955.png)
### 11.5.6、应用案例-双旋转
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列 int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL 树. int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL 树
1) 问题分析
![image-20221209112032701](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209112032701.png)
2) 解决思路分析
1. 当符号右旋转的条件时
2. 如果它的左子树的右子树高度大于它的左子树的高度
3. 先对当前这个结点的左节点进行左旋转
4. 在对当前结点进行右旋转的操作即可
3) 代码实现[AVL 树的汇总代码(完整代码)]
```JAVA
/**
* description
* 节点复用
*
* @author xujicheng
* @since 2022年12月08日 9:28
*/
public class Node {
int value; //权值
Node left; //左子节点
Node right; //右子节点
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
//返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
//返回以该节点为根节点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
//左旋转的方法
private void leftRotate() {
//创建新的节点,以当前根节点的值创建的
Node node = new Node(value);
//把新的节点的左子树,设置成当前节点的左子树
node.left = left;
//把新的节点的右子树,设置成当前节点的右子树的左子树
node.right = right.left;
//把当前节点的值换成右子节点的值
value = right.value;
//把当前节点的右子树设置成右子树的右子树
right = right.right;
//把当前节点的左子树设置成新的节点
left = node;
}
//右旋转的方法
private void rightRotate() {
//创建新的节点,以当前根节点的值创建的
Node node = new Node(value);
//把新的节点的右子树,设置成当前节点的右子树
node.right = right;
//把新的节点的左子树,设置成当前节点的左子树的右子树
node.left = left.right;
//把当前节点的值换成左子节点的值
value = left.value;
//把当前节点的做指数设置成左子树的左子树
left = left.left;
//把当前节点的右子树设置成新的节点
right = node;
}
/**
* 查找要删除的节点
*
* @param value 希望删除的结点的值
* @return 如果找到返回该结点,否则返回 null
*/
public Node search(int value) {
if (value == this.value) { //找到就是该结点
return this;
} else if (value < this.value) {//如果查找的值小于当前结点,向左子树递归查找//如果左子结点为空
if (this.left == null) {
return null;
}
return this.left.search(value);
} else { //如果查找的值不小于当前结点,向右子树递归查找
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除节点的父节点
*
* @param value 需要查找的值
* @return 要删除的节点的父节点,如果没有就返回null
*/
public Node searchParent(int value) {
//如果当前结点就是要删除的结点的父结点,就返回
if ((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else {
//如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); //向左子树递归查找
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); //向右子树递归查找
} else {
return null; // 没有找到父结点
}
}
}
/**
* 添加节点的方法,使用递归的方式添加节点,需要满足二叉排序树的要求
*
* @param node 需要添加的节点
*/
public void add(Node node) {
if (node == null) {
return;
}
//判断传入节点的值和当前子树的根节点的值的关系
if (node.value < this.value) {
if (this.left == null) { //如果当前节点左子节点为空,直接挂在左子节点即可
this.left = node;
} else {
this.left.add(node); //递归的向左子树添加
}
} else { //添加的节点的值大于当前节点的值
if (this.right == null) {
this.right = node;
} else {
this.right.add(node); //递归的向右子树添加
}
}
//当添加完一个节点后,如果 (右子树的高度 - 左子树的高度) > 1,就将该树左旋转
if (rightHeight() - leftHeight() > 1) {
//如果它的右子树的左子树的高度大于它的右子树的高度
if (right != null && right.leftHeight() > right.rightHeight()) {
//先进行右旋转,在进行左旋转
right.rightRotate();
//再对当前节点进行左旋转
leftRotate();
} else {
//直接进行左旋转
leftRotate();
}
return;
}
//当添加完一个节点后,如果 (左子树的高度 - 右子树的高度) > 1,就将该树右旋转
if (leftHeight() - rightHeight() > 1) {
//如果它的左子树的右子树的高度大于它的左子树的高度
if (left != null && left.rightHeight() > left.leftHeight()) {
//先进行左旋转,在进行右旋转
left.leftRotate();
//再对当前节点进行右旋转
rightRotate();
} else {
//直接进行右旋转
rightRotate();
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
/**
* description
* 二叉平衡树,二叉排序树的代码复用
*
* @author xujicheng
* @since 2022年12月09日 9:56
*/
public class AvlTree {
private Node root;
public Node getRoot() {
return root;
}
public void setRoot(Node root) {
this.root = root;
}
/**
* 查找要删除的节点
*
* @param value 希望删除的节点的值
* @return 如果找到返回该节点,否则返回null
*/
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
/**
* 查找父节点
*
* @param value 需要查找的值
* @return 要删除的节点的父节点,如果没有就返回null
*/
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 课后作业
*
* @param node 传入的结点(当做二叉排序树的根结点)
* @return 返回的 以 node 为根结点的二叉排序树的最小结点的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
//循环的查找左子节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
//这时 target 就指向了最小结点,删除最小结点
delNode(target.value);
return target.value;
}
/**
* 删除节点
*
* @param value 需要删除的节点的值
*/
public void delNode(int value) {
if (root == null) {
return;
} else {
//需要先找到要删除的节点 targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点就无需向下执行
if (targetNode == null) {
return;
}
//如果我们发现当前这颗二叉树排序树只有一个节点
if (root.left == null && root.right == null) {
root = null;
return;
}
//去找到 targetNode 的父结点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.left == null && targetNode.right == null) {
//判断 targetNode 是父结点的左子结点,还是右子结点
if (parent.left != null && parent.left.value == value) { //是左子结点
parent.left = null;
} else if (parent.right != null && parent.right.value == value) {//是由子结点parent.right = null;
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) { //删除有两颗子树的节点int minVal = delRightTreeMin(targetNode.right);
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
} else { // 删除只有一颗子树的结点
//如果要删除的结点有左子结点
if (targetNode.left != null) {
if (parent != null) {
//如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.left;
} else { // targetNode 是 parent 的右子结点
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else { //如果要删除的结点有右子结点
if (parent != null) {
//如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.right;
} else { //如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
/**
* 添加节点的方法
*
* @param node 需要加入的节点
*/
public void add(Node node) {
if (root == null) {
root = node; //如果root为空则直接让root指向node
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("当前二叉排序树为空,不能遍历");
}
}
}
/**
* description
* 二叉平衡树的左旋转代码实现
*
* @author xujicheng
* @since 2022年12月09日 9:47
*/
public class AvlTreeDemo {
public static void main(String[] args) {
int[] arr = {10, 11, 7,6,8,9};
//创建一个AVLTree对象
AvlTree avlTree = new AvlTree();
//添加节点
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
System.out.println("中序遍历");
avlTree.infixOrder();
System.out.println("在做平衡处理后");
System.out.println("树的高度" + avlTree.getRoot().height());
System.out.println("树的左子树的高度" + avlTree.getRoot().leftHeight());
System.out.println("树的右子树的高度" + avlTree.getRoot().rightHeight());
System.out.println("当前的根节点=" + avlTree.getRoot());
System.out.println("根节点的左子节点" + avlTree.getRoot().left);
System.out.println("根节点的右子节点" + avlTree.getRoot().right);
}
}
```
> 经过双旋转的处理后的二叉树就变成了平衡二叉树
>
> ![image-20221209115829805](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221209115829805.png)
# 十二、多路查找树
## 12.1、二叉树与 B 树
### 12.1.1、二叉树的问题分析
二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树
![image-20221210113030592](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210113030592.png)
1) 二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1 亿),就存在如下问题:
2) 问题 1:在构建二叉树时,需要多次进行 i/o 操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
3) 问题 2:节点海量,也会造成二叉树的高度很大,会降低操作速度
### 12.1.2 、多叉树
1) 在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
2) 后面我们讲解的 2-3 树,2-3-4 树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
3) 举例说明(下面 2-3 树就是一颗多叉树)
![image-20221210113211761](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210113211761.png)
### 12.1.3 、B 树的基本介绍
B 树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数来提升效率
![image-20221210113355083](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210113355083.png)
1) 如图 B 树通过重新组织节点, 降低了树的高度.
2) 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次 I/O 就可以完全载入
3) 将树的度 M 设置为 1024,在 600 亿个元素中最多只需要 4 次 I/O 操作就可以读取到想要的元素, B树(B+)广泛应用于文件存储系统以及数据库系统中
## 12.2 、2-3 树
### 12.2.1 、2-3 树是最简单的 B 树结构, 具有如下特点:
1) 2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
2) 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
3) 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.
4) 2-3 树是由二节点和三节点构成的树
### 12.2.2、 2-3 树应用案例
将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成 2-3 树,并保证数据插入的大小顺序。(演示一下构建2-3树的过程.)
![image-20221210114654657](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210114654657.png)
插入规则:
1) 2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
2) 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
3) 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
4) 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面 3 个条件。
5) 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
### 12.2.3 、其它说明
除了 23 树,还有 234 树等,概念和 23 树类似,也是一种 B 树。 如图:
![image-20221210121520986](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210121520986.png)
## 12.3 、B 树、B+树和 B*树
### 12.3.1 、B 树的介绍
B-tree 树即 B 树,B 即 Balanced,平衡的意思。有人把 B-tree 翻译成 B-树,容易让人产生误解。会以为B-树是一种树,而 B 树又是另一种树。实际上,B-tree 就是指的 B 树
### 12.3.2、 B 树的介绍
前面已经介绍了 2-3 树和 2-3-4 树,他们就是 B 树(英语:B-tree 也写成 B-树),这里我们再做一个说明,我们在学习 Mysql 时,经常听到说某种类型的索引是基于 B 树或者 B+树的,如图:
![image-20221210121537724](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210121537724.png)
对上图的说明:
* 1) B 树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4
* 2) B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
* 3) 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据.
* 4) 搜索有可能在非叶子结点结束
* 5) 其搜索性能等价于在关键字全集内做一次二分查找
### 12.3.3 、B+树的介绍
B+树是 B 树的变体,也是一种多路搜索树
![image-20221210121556698](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210121556698.png)
对上图的说明:
1) B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
2) 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。 3) 不可能在非叶子结点命中
4) 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
5) 更适合文件索引系统
6) B 树和 B+树各有自己的应用场景,不能说 B+树完全比 B 树好,反之亦然.
### 12.3.4 、B*树的介绍
B*树是 B+树的变体,在 B+树的**非根和非叶子结点**再增加**指向兄弟的指针**
![image-20221210102202184](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210102202184.png)
B*树的说明: *
*1) B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为 2/3,而B+树的块的最低使用率为的1/2。 *
*2) 从第 1 个特点我们可以看出,B*树分配新结点的概率比 B+树要低,空间使用率更高
# 十三、图
## 13.1 、图基本介绍
### 13.1.1 、为什么要有图
1) 前面我们学了线性表和树
2) 线性表局限于一个直接前驱和一个直接后继的关系
3) 树也只能有一个直接前驱也就是父节点
4) 当我们需要表示**多对多**的关系时, 这里我们就用到了**图**。
### 13.1.2、 图的举例说明
图是一种**数据结构**,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。结点也可以称为顶点。如图:
![image-20221210125108621](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210125108621.png)
### 13.1.3 、图的常用概念
1) 顶点(vertex)
2) 边(edge)
3) 路径
4) 无向图(右图)
![image-20221210125548523](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210125548523.png)
5) 有向图
6) 带权图
![image-20221210125740883](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210125740883.png)
## 13.2 、图的表示方式
图的表示方式有两种:
* 二维数组表示(邻接矩阵);链表表示(邻接表)
### 13.2.1 、邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于 n 个顶点的图而言,矩阵是的row 和col 表示的是1....n个点
![image-20221210125849200](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210125849200.png)
### 13.2.2 、邻接表
1) 邻接矩阵需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在,会造成空间的一定损失.
2) 邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
3) 举例说明
![image-20221210123916925](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210123916925.png)
## 13.3 、图的快速入门案例
1) 要求: 代码实现如下图结构
![image-20221210130312247](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210130312247.png)
2) 思路分析
* (1) 存储顶点 String 使用 ArrayList
* (2) 保存矩阵 int [ ] [ ] edges
3) 代码实现
```JAVA
/**
* description
* 数据结构——图的快速入门案例
*
* @author xujicheng
* @since 2022年12月10日 13:11
*/
public class Graph {
private ArrayList<String> vertexList; //存储顶点的集合
private int[][] edges; //存储图对应的邻接矩阵
private int numOfEdges; //表示边的个数
public static void main(String[] args) {
//测试图是否创建完成
int n = 5; //节点的个数
String[] Vertex = {"A", "B", "C", "D", "E"};
//创建图对象
Graph graph = new Graph(n);
//循环的添加顶点
for(String vertex : Vertex){
graph.insertVertex(vertex);
}
//添加边 A -->B A -->C B-->C B --> D B --> E
graph.insertEdge(0,1,1);
graph.insertEdge(0,2,1);
graph.insertEdge(1,2,1);
graph.insertEdge(1,3,1);
graph.insertEdge(1,4,1);
//显示邻接矩阵
graph.showGraph();
}
/**
* 构造器,用于初始化对象
*
* @param n 表示顶点的个数
*/
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
}
/**
* 数据结构——图中常用的方法——返回节点的个数
*
* @return 返回vertexList中节点的总树
*/
public int getNumOfVertex() {
return vertexList.size();
}
/**
* 得到边的数目
*
* @return 返回边的总数
*/
public int getNumOfEdges() {
return numOfEdges;
}
/**
* 返回节点i(下标)对应的数据"0" -> "A" ,"1" -> "B"
*
* @param i 节点对应的下标
* @return 返回下标对应的数据
*/
public String getValueByIndex(int i) {
return vertexList.get(i);
}
//显示矩阵的方法
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
/**
* 返回v1和v2的权值
*
* @param v1 顶点的下标
* @param v2 另一个顶点的下标
* @return 顶点的权值
*/
public int getWeight(int v1, int v2) {
return edges[v1][v2];
}
/**
* 插入顶点的方法
*
* @param vertex 需要插入的顶点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/**
* 添加边的方法
*
* @param v1 表示第一个顶点的下标,即是第几个顶点
* @param v2 表示第二个顶点的下标
* @param weight 矩阵边对应的值,要么是0要么是1,默认值为0
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
}
```
## 13.4 、图的深度优先遍历介绍
### 13.4.1 、图遍历介绍
所谓图的遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略:
(1)深度优先遍历
(2)广度优先遍历
### 13.4.2 、深度优先遍历基本思想
图的深度优先搜索(Depth First Search) 。
* 1) 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点,可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
* 2) 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
* 3) 显然,深度优先搜索是一个递归的过程
### 13.4.3 、深度优先遍历算法步骤
1) 访问初始结点 v,并标记结点 v 为已访问。
2) 查找结点 v 的第一个邻接结点 w。
3) 若 w 存在,则继续执行 4,如果 w 不存在,则回到第 1 步,将从 v 的下一个结点继续。
4) 若 w 未被访问,对 w 进行深度优先遍历递归(即把 w 当做另一个 v,然后进行步骤123)。
5) 查找结点 v 的 w 邻接结点的下一个邻接结点,转到步骤 3。
6) 分析图
![image-20221210155150907](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210155150907.png)
### 13.4.4 、深度优先算法的代码实现
```JAVA
只加入核心方法,测试部分和其他无关紧要的代码不写入,后面有汇总
/**
* 得到第一个邻接节点的下标w
*
* @param index 当前节点的下标
* @return 如果存在返回当前节点的第一个邻接节点的下标,否则返回-1
*/
public int getFirstNeighbor(int index) {
for (int j = 0; j < vertexList.size(); j++) {
if (edges[index][j] > 0) {
return j;
}
}
return -1;
}
/**
* 根据前一个邻接节点的下标来获取下一个邻接节点
*
* @param v1 前一个邻接节点的下标
* @param v2 当前邻接节点
* @return 如果存在返回当前邻接节点的下一个邻接节点的下标,否则返回-1
*/
public int getNextNeighbor(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (edges[v1][j] > 0) {
return j;
}
}
return -1;
}
//对dfs方法进行一个重载,遍历我们所有的节点,并进行dfs
public void dfs() {
isVisited = new boolean[vertexList.size()];
//遍历所有的节点,进行dfs[回溯]
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]) { //增加节点是否被访问的逻辑提高遍历效率
dfs(isVisited, i);
}
}
}
/**
* 深度优先遍历算法
*
* @param isVisited 记录某个节点是否被访问
* @param i 是节点的初始下标,第一次是0
*/
private void dfs(boolean[] isVisited, int i) {
//首先访问该节点(输出)
System.out.print(getValueByIndex(i) + "-->");
//访问后将该节点设置成已访问
isVisited[i] = true;
//查找节点i的第一个邻接点w
int w = getFirstNeighbor(i);
while (w != -1) { //说明有邻接点
//还需要判断当前节点是否被访问过
if (!isVisited[w]) {
dfs(isVisited, w);
}
//如果w这个节点已经被访问,就去查找w的邻接点的下一个邻接节点
w = getNextNeighbor(i, w);
}
}
```
## 13.5 、图的广度优先遍历
### 13.5.1 、广度优先遍历基本思想
1) 图的广度优先搜索(Broad First Search) 。
2) 类似于一个**分层搜索**的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
### 13.5.2 、广度优先遍历算法步骤
1) 访问初始结点 v 并标记结点 v 为已访问。
2) 结点 v 入队列
3) 当队列非空时,继续执行,否则算法结束。
4) 出队列,取得队头结点 u。
5) 查找结点 u 的第一个邻接结点 w。
6) 若结点 u 的邻接结点 w 不存在,则转到步骤 3;否则循环执行以下三个步骤:
* 6.1 若结点 w 尚未被访问,则访问结点 w 并标记为已访问。
* 6.2 结点 w 入队列
* 6.3 查找结点 u 的继 w 邻接结点后的下一个邻接结点 w,转到步骤 6。
### 13.5.3 、广度优先算法的图示
![image-20221210124655387](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210124655387.png)
## 13.6、 广度优先算法的代码实现
```JAVA
/**
* 广度优先遍历算法——以一个节点而言
*
* @param isVisited 记录某个节点是否被访问
* @param i 是节点的初始下标,第一次是0
*/
private void bfs(boolean[] isVisited, int i) {
int u; //表示队列的头节点对应的下标
int w; //表示邻接节点的下标
//需要队列来记录节点访问的顺序
LinkedList<Integer> queue = new LinkedList<Integer>();
//访问这个节点(即输出这个节点的信息)
System.out.print(getValueByIndex(i) + "-->");
//访问后把此节点标记为已访问
isVisited[i] = true;
//将节点加入队列,根据队列的特点,加入节点是从尾部加,取出是从头部取
queue.addLast(i);
//只要队列不为空,继续执行,否则算法结束
while (!queue.isEmpty()) {
//取出队列的头节点下标
u = queue.removeFirst();
//得到第一个邻接点的下标w
w = getFirstNeighbor(u);
//判断w是否存在
while (w != -1) { //存在
//判断节点是否被访问过
if (!isVisited[w]) { //未访问
//未访问,访问节点w
System.out.print(getValueByIndex(w) + "-->");
//标记为已访问
isVisited[w] = true;
//访问后入队列,记录访问顺序
queue.addLast(w);
}
//以u为起点,找w后面的后的下一个邻接点,即w被访问,还以u为起点继续向下访问
w = getNextNeighbor(u, w); //体现出广度优先
}
}
}
//遍历所有的节点,都进行广度优先搜索,对广度优先的方法进行重载
public void bfs() {
isVisited = new boolean[5];
for (int i = 0; i < getNumOfVertex(); i++) {
//增加判断是否访问过,以此增加遍历的效率
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
}
```
## 13.7 、图的代码汇总
```JAVA
/**
* description
* 数据结构——图的快速入门案例
*
* @author xujicheng
* @since 2022年12月10日 13:11
*/
public class Graph {
private ArrayList<String> vertexList; //存储顶点的集合
private int[][] edges; //存储图对应的邻接矩阵
private int numOfEdges; //表示边的个数
//定义个数组boolean[],记录某个节点是否被访问
private boolean[] isVisited;
public static void main(String[] args) {
//测试图是否创建完成
int n = 5; //节点的个数
String[] Vertex = {"A", "B", "C", "D", "E"};
//创建图对象
Graph graph = new Graph(n);
//循环的添加顶点
for (String vertex : Vertex) {
graph.insertVertex(vertex);
}
//添加边 A -->B A -->C B-->C B --> D B --> E
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
//显示邻接矩阵
graph.showGraph();
//测试dfs遍历是否正常
System.out.println("深度遍历");
graph.dfs();
System.out.println(); //换行
System.out.println("广度优先遍历");
graph.bfs();
}
/**
* 构造器,用于初始化对象
*
* @param n 表示顶点的个数
*/
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
}
/**
* 得到第一个邻接节点的下标w
*
* @param index 当前节点的下标
* @return 如果存在返回当前节点的第一个邻接节点的下标,否则返回-1
*/
public int getFirstNeighbor(int index) {
for (int j = 0; j < vertexList.size(); j++) {
if (edges[index][j] > 0) {
return j;
}
}
return -1;
}
/**
* 根据前一个邻接节点的下标来获取下一个邻接节点
*
* @param v1 前一个节点的下标
* @param v2 下一个邻接节点
* @return 如果存在返回当前邻接节点的下一个邻接节点的下标,否则返回-1
*/
public int getNextNeighbor(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (edges[v1][j] > 0) {
return j;
}
}
return -1;
}
//对dfs方法进行一个重载,遍历我们所有的节点,并进行dfs
public void dfs() {
isVisited = new boolean[vertexList.size()];
//遍历所有的节点,进行dfs[回溯]
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]) { //增加节点是否被访问的逻辑提高遍历效率
dfs(isVisited, i);
}
}
}
/**
* 深度优先遍历算法
*
* @param isVisited 记录某个节点是否被访问
* @param i 是节点的初始下标,第一次是0
*/
private void dfs(boolean[] isVisited, int i) {
//首先访问该节点(输出)
System.out.print(getValueByIndex(i) + "-->");
//访问后将该节点设置成已访问
isVisited[i] = true;
//查找节点i的第一个邻接点w
int w = getFirstNeighbor(i);
while (w != -1) { //说明有邻接点
//还需要判断当前节点是否被访问过
if (!isVisited[w]) {
dfs(isVisited, w);
}
//如果w这个节点已经被访问,就去查找w的邻接点的下一个邻接节点
w = getNextNeighbor(i, w);
}
}
/**
* 广度优先遍历算法——以一个节点而言
*
* @param isVisited 记录某个节点是否被访问
* @param i 是节点的初始下标,第一次是0
*/
private void bfs(boolean[] isVisited, int i) {
int u; //表示队列的头节点对应的下标
int w; //表示邻接节点的下标
//需要队列来记录节点访问的顺序
LinkedList<Integer> queue = new LinkedList<Integer>();
//访问这个节点(即输出这个节点的信息)
System.out.print(getValueByIndex(i) + "-->");
//访问后把此节点标记为已访问
isVisited[i] = true;
//将节点加入队列,根据队列的特点,加入节点是从尾部加,取出是从头部取
queue.addLast(i);
//只要队列不为空,继续执行,否则算法结束
while (!queue.isEmpty()) {
//取出队列的头节点下标
u = queue.removeFirst();
//得到第一个邻接点的下标w
w = getFirstNeighbor(u);
//判断w是否存在
while (w != -1) { //存在
//判断节点是否被访问过
if (!isVisited[w]) { //未访问
//未访问,访问节点w
System.out.print(getValueByIndex(w) + "-->");
//标记为已访问
isVisited[w] = true;
//访问后入队列,记录访问顺序
queue.addLast(w);
}
//以u为起点,找w后面的后的下一个邻接点,即w被访问,还以u为起点继续向下访问
w = getNextNeighbor(u, w); //体现出广度优先
}
}
}
//遍历所有的节点,都进行广度优先搜索,对广度优先的方法进行重载
public void bfs() {
isVisited = new boolean[5];
for (int i = 0; i < getNumOfVertex(); i++) {
//增加判断是否访问过,以此增加遍历的效率
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
}
/**
* 数据结构——图中常用的方法——返回节点的个数
*
* @return 返回vertexList中节点的总树
*/
public int getNumOfVertex() {
return vertexList.size();
}
/**
* 得到边的数目
*
* @return 返回边的总数
*/
public int getNumOfEdges() {
return numOfEdges;
}
/**
* 返回节点i(下标)对应的数据"0" -> "A" ,"1" -> "B"
*
* @param i 节点对应的下标
* @return 返回下标对应的数据
*/
public String getValueByIndex(int i) {
return vertexList.get(i);
}
//显示矩阵的方法
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
/**
* 返回v1和v2的权值
*
* @param v1 顶点的下标
* @param v2 另一个顶点的下标
* @return 顶点的权值
*/
public int getWeight(int v1, int v2) {
return edges[v1][v2];
}
/**
* 插入顶点的方法
*
* @param vertex 需要插入的顶点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/**
* 添加边的方法
*
* @param v1 表示第一个顶点的下标,即是第几个顶点
* @param v2 表示第二个顶点的下标
* @param weight 矩阵边对应的值,要么是0要么是1,默认值为0
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
}
```
## 13.8 、图的深度优先 VS 广度优先
![image-20221210181106622](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221210181106622.png)
# 十四、程序员常用10 种算法
## 14.1、二分查找算法(非递归)
### 14.1.1、二分查找算法(非递归)介绍
1) 前面我们讲过了二分查找算法,是使用递归的方式,下面我们讲解二分查找算法的非递归方式
2) 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
3) 二分查找法的运行时间为对数时间 O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n 步,假设从[0,99]的队列(100 个数,即 n=100)中寻到目标数 30,则需要查找步数为㏒₂100 , 即最多需要查找7 次( 2^6 <100<2^7)
### 14.1.2、二分查找算法(非递归)代码实现
数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成
1) 思路分析:
* 1、收起确定该数组中间的下标 ,即mid = (left + right) /2
* 2、然后让需要查找的书findVal 和arr [mid] 比较
* 2.1、findVal > arr[mid],说明你要查找的数在mid 的右边
* 2.2、findVal < arr[mid],说明要查找的数在mid的左边
* 2.3、findVal == arr[mid] 说明找到,就返回
2) 代码实现:
```JAVA
/**
* description
* 二分查找算法的非递归方式实现
*
* @author xujicheng
* @since 2022年12月11日 16:49
*/
public class BinarySearchRecursion {
public static void main(String[] args) {
int[] arr = {1, 3, 8, 10, 11, 67, 100};
int index = binarySearch(arr, 8);
System.out.println(index);
}
/**
* 二分查找算法的非递归方式实现
*
* @param arr 待查找的数组
* @param target 需要查找的数
* @return 找到返回对应下标,没找到返回-1即可
*/
public static int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) { //说明可以继续查找
int mid = (left + right) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] > target) {
right = mid - 1; //需要向左边查找
} else if (arr[mid] < target) {
left = mid + 1; //向右边查找
}
}
return -1;
}
}
```
## 14.2、分治算法
### 14.2.1、分治算法介绍
1) 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
2) 分治算法可以求解的一些经典问题
* 二分搜索
* 大整数乘法
* 棋盘覆盖
* 合并排序
* 快速排序
* 线性时间选择
* 最接近点对问题
* 循环赛日程表
* 汉诺塔
### 14.2.2 、分治算法的基本步骤
分治法在每一层递归上都有三个步骤:
1) 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
2) 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3) 合并:将各个子问题的解合并为原问题的解。
### 14.2.3、分治(Divide-and-Conquer(P))算法设计模式如下:
![image-20221211164006773](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221211164006773.png)
### 14.2.4、分治算法最佳实践-汉诺塔
汉诺塔的传说 汉诺塔:
汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着 64 片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
假如每秒钟一次,共需多长时间呢?移完这些金片需要 5845.54 亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了 5845.54 亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭
汉诺塔游戏的演示和思路分析:
* 1) 如果是有一个盘, A->C
* 如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的盘 2. 上面的盘
* 2) 先把 最上面的盘 A->B
* 3) 把最下边的盘 A->C
* 4) 把 B 塔的所有盘 从 B->C
汉诺塔游戏的代码实现:
```JAVA
/**
* description
* 汉洛塔问题——使用分治算法实现
*
* @author xujicheng
* @since 2022年12月11日 17:47
*/
public class GlottalTower {
public static void main(String[] args) {
glottalTower(2,'A','B','C');
}
/**
* 汉洛塔移动的方法——分治算法解决
*
* @param num 盘子的数量
* @param a 柱子A
* @param b 柱子B
* @param c 柱子C
*/
public static void glottalTower(int num, char a, char b, char c) {
//如果只有一个盘,直接移到柱子C即可
if (num == 1) {
System.out.println("第1个盘子从" + a + "->" + c);
} else {
//如果盘子的数量满足公式n >=2的情况下,我们可以把看成是两个盘子,分别是最上面的一个盘子和下面的所有盘子
glottalTower(num - 1, a, c, b); //先把最上面的所有盘子从A->B,移动过程会使用到C
System.out.println("第" + num + "个盘从 " + a + "->" + c); //把最下边的盘从A->C
glottalTower(num - 1, b, a, c); //把B塔的所有盘从B->C,移动过程使用到了A
}
}
}
```
## 14.3、动态规划算法
### 14.3.1、应用场景-背包问题
背包问题:有一个背包,容量为 4 磅 , 现有如下物品
![image-20221212143148365](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212143148365.png)
1) 要求达到的目标为装入的背包的总价值最大,并且重量不超出
2) 要求装入的物品不能重复
### 14.3.2 、动态规划算法介绍
1) 动态规划(Dynamic Programming)算法的核心思想是:将大**问题划分为小问题进行解决**,从而一步步获取最优解的处理算法
2) 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
3) 与分治法不同的是,适合于用动态规划求解的问题,经分解得到**子问题往往不是互相独立的**。( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
4) 动态规划可以通过填表的方式来逐步推进,得到最优解
### 14.3.3 、动态规划算法最佳实践-背包问题解决
思路分析和图解
1)背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分 **01 背包和完全背包**(完全背包指的是:每种物品都有无限件可用)
2)这里的问题属于 **01 背包**,即每个物品最多放一个。而无限背包可以转化为 01 背包
3)算法的主要思想,利用动态规划来解决。每次遍历到的第 i 个物品,根据 w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的 n 个物品,设 v[i]、w[i]分别为第 i 个物品的价值和重量,C 为背包的容量。再令v[i][j]表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值。则我们有下面的结果:
```BASH
(1) v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是 0
(2) 当 w[i]> j 时:v[i][j]=v[i-1][j] // 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略
(3) 当 j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}
// 当 准备加入的新增的商品的容量小于等于当前背包的容量, // 装入的方式:
v[i-1][j]: 就是上一个单元格的装入的最大值
v[i] : 表示当前商品的价值
v[i-1][j-w[i]] : 装入 i-1 商品,到剩余空间 j-w[i]的最大值
当 j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :
```
4) 图解的分析
![image-20221212141740219](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212141740219.png)
### 14.3.4、动态规划-背包问题的代码实现
```JAVA
/**
* description
* 使用动态规划算法解决背包问题
*
* @author xujicheng
* @since 2022年12月12日 15:50
*/
public class KnapsackProblem {
public static void main(String[] args) {
int[] w = {1, 4, 3}; //物品的重量
int[] value = {1500, 3000, 2000}; //物品的价值
int m = 4; //背包的容量
int n = value.length; //物品的个数
//创建二维数组,v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值
int[][] v = new int[n + 1][m + 1];
//为了记录放入商品的情况,定义一个二维数组
int[][] path = new int[n + 1][m + 1];
//初始化第一行和第一列
for (int i = 0; i < v.length; i++) {
v[i][0] = 0; //将第一列初始化为0
}
for (int i = 0; i < v[0].length; i++) {
v[0][i] = 0; //将第一行初始化为0
}
//根据全面得到的公式来动态规划处理
for (int i = 1; i < v.length; i++) { //不处理第一行
for (int j = 1; j < v[0].length; j++) { //不处理第一列
if (w[i - 1] > j) { //因为程序中的i是从1开始的,因此原来公式中的w[i]修改成w[i-1]
v[i][j] = v[i - 1][j];
//为了记录商品存放到背包的情况,我们不能直接使用公式,需要用if-else来体现
} else if (v[i - 1][j] < value[i - 1] + v[i - 1][j - w[i - 1]]) {
//说明:因为我们的i是从1开始的,因此公式需要调整成以下形式
v[i][j] = value[i - 1] + v[i - 1][j - w[i - 1]];
//把当前的情况记录到path
path[i][j] = 1;
} else {
v[i][j] = v[i - 1][j];
}
}
}
//输出一下二维数组v查看目前情况
for (int i = 0; i < v.length; i++) {
for (int j = 0; j < v[i].length; j++) {
System.out.print(v[i][j] + " ");
}
System.out.println();
}
System.out.println("==============================");
//输出最后放入的是哪些商品
int i = path.length - 1; //行的最大下标
int j = path[0].length - 1; //列的最大下标
while (i > 0 && j > 0) { //从path的最后开始找
if (path[i][j] == 1) {
System.out.printf("第%d个商品放入到背包\n", i);
j -= w[i - 1];
}
i--;
}
}
}
```
## 14.4 、KMP 算法
### 14.4.1、应用场景-字符串匹配问题
字符串匹配问题:
1) 有一个字符串 str1= ""硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好"",和一个子串str2="尚硅谷你尚硅你"
2) 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
### 14.4.2、暴力匹配算法
如果用暴力匹配的思路,并假设现在 str1 匹配到 i 位置,子串 str2 匹配到 j 位置,则有:
* 1) 如果当前字符匹配成功(即 str1[i] == str2[j]),则 i++,j++,继续匹配下一个字符
* 2) 如果失配(即 str1[i]! = str2[j]),令 i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
* 3) 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可行!)
* 4) 暴力匹配算法实现.
* 5) 代码实现如下
```JAVA
/**
* description
* 字符串匹配问题——暴力解法
*
* @author xujicheng
* @since 2022年12月12日 17:12
*/
public class ViolenceMatch {
public static void main(String[] args) {
//测试暴力匹配算法
String str1 = "硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好";
String str2 = "尚硅谷你尚硅你";
int index = violenceMatch(str1, str2);
System.out.println("index=" + index);
}
/**
* 暴力匹配解决字符串匹配问题
*
* @param str1 第一个字符串,和字符串二进行匹配
* @param str2 第二个字符串,与字符串一进行匹配
* @return 若匹配则返回第一次出现的位置,不匹配则返回-1
*/
public static int violenceMatch(String str1, String str2) {
char[] s1 = str1.toCharArray(); //将字符串一转成字符数组
char[] s2 = str2.toCharArray(); //将字符串二转成字符数组
int s1Length = s1.length; //得到字符数组s1的长度
int s2Length = s2.length; //得到字符数组s2的长度
//需要两个索引 i 和j分别指向s1和s2,初始化为0
int i = 0;
int j = 0;
while (i < s1Length && j < s2Length) { //保证匹配时,索引不越界
if (s1[i] == s2[j]) { //匹配成功
i++; //索引后移
j++;
} else { //没有匹配成功
//如果没匹配(即str[i]!=str2[j]),令i = i - (j-1),j=0.
i = i - (j - 1);
j = 0;
}
}
//判断是否匹配成功
if (j == s2Length) {
return i - j;
} else {
return -1;
}
}
}
```
### 14.4.3 、KMP 算法介绍
1) KMP 是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
2) Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP 算法”,常用于在一个文本串S 内查找一个模式串P的出现位置,这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这3人的姓氏命名此算法.
3) KMP 方法算法就利用之前判断过信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间
字符串匹配问题:
* 1) 有一个字符串 str1= "BBC ABCDAB ABCDABCDABDE",和一个子串 str2="ABCDABD"
* 2) 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
* 3) 要求:**使用 KMP 算法完成**判断,不能使用简单的暴力匹配算法
思路分析图解
> 举例来说,有一个字符串 Str1 = “BBC ABCDAB ABCDABCDABDE”,判断,里面是否包含另一个字符串Str2=“ABCDABD”?
>
> 1、首先,用 Str1 的第一个字符和 Str2 的第一个字符去比较,不符合,关键词向后移动一位
>
> ![image-20221212142356060](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142356060.png)
>
> 2、重复第一步,还是不符合,再后移
>
> ![image-20221212142447686](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142447686.png)
>
> 3、一直重复,直到 Str1 有一个字符与 Str2 的第一个字符符合为止
>
> ![image-20221212142533630](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142533630.png)
>
> 4、接着比较字符串和搜索词的下一个字符,还是符合。
>
> ![image-20221212142544792](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142544792.png)
>
> 5、遇到 Str1 有一个字符与 Str2 对应的字符不符合
>
> ![image-20221212142607190](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142607190.png)
>
> 6.这时候,想到的是继续遍历 Str1 的下一个字符,重复第 1 步。(其实是很不明智的,因为此时BCD已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与 D 不匹配时,你其实知道前面六个字符是”ABCDAB”。KMP 算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。)
>
> ![image-20221212142621218](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142621218.png)
>
> 7.怎么做到把刚刚重复的步骤省略掉?可以对 Str2 计算出一张《部分匹配表》
>
> ![image-20221212142635029](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142635029.png)
>
> 8.已知空格与 D 不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符B对应的”部分匹配值”为 2,因此按照下面的公式算出向后移动的位数: 移动位数 = 已匹配的字符数 - 对应的部分匹配值 因为 6 - 2 等于 4,所以将搜索词向后移动 4 位。
>
> 9.因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为 2(”AB”),对应的”部分匹配值”为 0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移 2 位。
>
> ![image-20221212142651515](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142651515.png)
>
> 10.因为空格与 A 不匹配,继续后移一位
>
> ![image-20221212142701981](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142701981.png)
>
> 11.逐位比较,直到发现 C 与 D 不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4 位
>
> ![image-20221212142713679](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142713679.png)
>
> 12.逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动 7 位,这里就不再重复了
>
> ![image-20221212142722938](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142722938.png)
>
> 13.介绍《部分匹配表》怎么产生的 先介绍前缀,后缀是什么
>
> ![image-20221212142732807](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142732807.png)
>
> “部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例,
>
> -”A”的前缀和后缀都为空集,共有元素的长度为 0;
>
> -”AB”的前缀为[A],后缀为[B],共有元素的长度为 0;
>
> -”ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度 0;
>
> -”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为 0;
>
> -”ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;
>
> -”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为 2; -”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为 0。
>
>
>
> 14.”部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是 2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动4 位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置。
>
> ![image-20221212142823195](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221212142823195.png)
### 14.4.4、KMP算法解决字符串问题——代码实现
```JAVA
/**
* description
* 使用KMP算法解决字符串匹配问题
*
* @author xujicheng
* @since 2022年12月12日 22:32
*/
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmpNext("ABCDABD");
System.out.println("next=" + Arrays.toString(next));
int index = kmpSearch(str1, str2, next);
System.out.println("index=" + index);
}
/**
* 写出我们的kmp搜索算法
*
* @param str1 原字符串
* @param str2 字串
* @param next 部分匹配表,字串对应的部分匹配表
* @return 如果是-1就是没有匹配到,否则返回第一个匹配的位置
*/
public static int kmpSearch(String str1, String str2, int[] next) {
//遍历
for (int i = 0, j = 0; i < str1.length(); i++) {
//需要从str1.charAt(i) != str2.charAt(j)的情况KMP算法的核心点
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) { //找到了
return i - j + 1;
}
}
return -1;
}
/**
* 获取到一个字符串(字串)的部分匹配值表
*
* @param dest 字符串(字串)的部分匹配值表
* @return 匹配值表
*/
public static int[] kmpNext(String dest) {
//创建一个next数组保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0; //如果字符串长度为1部分匹配值就是0
for (int i = 1, j = 0; i < dest.length(); i++) {
//当dest.charAt(i) != dest.charAt(j)满足时,我们需要从next[j-1]获取新的j
//直到我们发现有dest.charAt(i) == dest.charAt(j)才退出
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1];
}
//当dest.charAt(i) == dest.charAt(j)满足时,部分匹配值就是要+1
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
```
## 14.5 、贪心算法
### 14.5.1、应用场景-集合覆盖问题
假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
![image-20221213093908780](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213093908780.png)
### 14.5.2 、贪心算法介绍
1) 贪婪算法(贪心算法)是指在对问题进行求解时,**在每一步选择中都采取最好或者最优(即最有利)的选择**,从而希望能够导致结果是最好或者最优的算法
2) 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
### 14.5.3、 贪心算法最佳应用-集合覆盖
1) 假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
2) 思路分析:
* 如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。假设总的有 n 个广播台,则广播台的组合总共有 2ⁿ -1 个,假设每秒可以计算 10 个子集, 如图:
![image-20221213094133849](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213094133849.png)
使用贪婪算法,效率高:
1) 目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
2) 遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
3) 将这个电台加入到一个集合中(比如 ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。
4) 重复第 1 步直到覆盖了全部的地区
![image-20221213091718732](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213091718732.png)
3) 代码实现
```JAVA
/**
* description
* 使用贪心算法解决集合覆盖问题
*
* @author xujicheng
* @since 2022年12月13日 10:35
*/
public class GreedyAlgorithm {
public static void main(String[] args) {
//创建广播电台,放入Map中
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
//将各个电台放入到broadcasts
HashSet<String> hashSet1 = new HashSet<>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<>();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<>();
hashSet5.add("杭州");
hashSet5.add("大连");
//加入到map
broadcasts.put("K1", hashSet1);
broadcasts.put("K2", hashSet2);
broadcasts.put("K3", hashSet3);
broadcasts.put("K4", hashSet4);
broadcasts.put("K5", hashSet5);
//创建一个allAreas,将电台都放入到此集合中
HashSet<String> allAreas = new HashSet<>();
for (String s :broadcasts.keySet()){
allAreas.addAll(broadcasts.get(s));
}
//创建ArrayList,存放选择的电台集合
ArrayList<String> selects = new ArrayList<>();
//定义一个临时的集合,用于保存遍历过程中的电台覆盖的地区和当前还没有覆盖地区的交集
HashSet<String> tempSet = new HashSet<>();
//定义一个maxKey,保存在一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的key
//如果maxKey不为空,则会加入到selects
String maxKey = null;
while (allAreas.size()!= 0) { //如果allAreas不为0 则表示还没有覆盖到所有的地区
//每进行一次while循环需要将maxKey置空
maxKey = null;
//遍历broadcasts,取出对应的key
for (String key : broadcasts.keySet()) {
//每进行一次for循环都要把tempSet清空
tempSet.clear();
//表示当前key能够覆盖的地区
HashSet<String> areas = broadcasts.get(key);
tempSet.addAll(areas);
//求出tempSet和 allAreas 集合的交集,交集会赋给tempSet
tempSet.retainAll(allAreas);
//如果当前这个集合包含 未覆盖的区域数量比maxKey指向的集合的地区还多,就需要重置maxKey
if (tempSet.size() > 0 &&
(maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())) {
maxKey = key;
}
}
//maxKey !=null,就应该将maxKey加入到selects
if (maxKey != null) {
selects.add(maxKey);
//将maxKey指向的广播电台覆盖的地区,从allAreas去掉
allAreas.removeAll(broadcasts.get(maxKey));
}
}
System.out.println("得到的选择结果=" + selects);
}
}
```
### 14.5.4 、贪心算法注意事项和细节
1) 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
2) 比如上题的算法选出的是 K1, K2, K3, K5,符合覆盖了全部的地区
3) 但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区,如果 K2 的使用成本低于 K1,那么我们上题的K1, K2, K3, K5 虽然是满足条件,但是并不是最优的
## 14.6、 普里姆算法
### 14.6.1、应用场景-修路问题
看一个应用场景和问题:
![image-20221213140956398](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213140956398.png)
1) 有胜利乡有 7 个村庄(A, B, C, D, E, F, G) ,现在需要修路把 7 个村庄连通
2) 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
3) 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短? 思路: 将 10 条边,连接即可,但是总的里程数不是最小.
**正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少**.
### 14.6.2 、最小生成树
修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST。给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
1) N 个顶点,一定有 N-1 条边
2) 包含全部顶点
3) N-1 条边都在图中
4) 举例说明(如图:)
5) 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法
![image-20221213141341331](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213141341331.png)
### 14.6.3 、普里姆算法介绍
普利姆(Prim)算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有(n-1)条边包含所有n 个顶点的连通子图,也就是所谓的极小连通子图 普利姆的算法如下:
1) 设 G=(V,E)是连通网,T=(U,D)是最小生成树,V,U 是顶点集合,E,D 是边的集合
2) 若从顶点 u 开始构造最小生成树,则从集合 V 中取出顶点 u 放入集合 U 中,标记顶点v 的visited[u]=1
3) 若集合 U 中顶点 ui 与集合 V-U 中的顶点 vj 之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点 vj 加入集合 U 中,将边(ui,vj)加入集合 D 中,标记 visited[vj]=1
4) 重复步骤②,直到 U 与 V 相等,即所有顶点都被标记为访问过,此时 D 中有 n-1 条边
5) 提示: 单独看步骤很难理解,我们通过代码来讲解,比较好理解.
6) 图解普利姆算法
![image-20221213142424305](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213142424305.png)
### 14.6.4、普利姆算法的最佳实践——修路问题
![image-20221213142559744](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213142559744.png)
1) 有胜利乡有 7 个村庄(A, B, C, D, E, F, G) ,现在需要修路把 7 个村庄连通
2) 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
3) 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
4)代码实现:
```JAVA
/**
* description
* 使用邻接矩阵创建图
*
* @author xujicheng
* @since 2022年12月13日 14:33
*/
public class MGraph {
int vertx; //表示图的节点个数
char[] data; //用于存放节点数据
int[][] weight; //用于存放边,就是我们的邻接矩阵
//构造器,用于初始化矩阵图中的各个属性
public MGraph(int vertx) {
this.vertx = vertx;
data = new char[vertx];
weight = new int[vertx][vertx];
}
}
/**
* description
* 创建最小生成树-->即村庄的图
*
* @author xujicheng
* @since 2022年12月13日 14:37
*/
public class MinTree {
/**
* 创建图的邻接矩阵
*
* @param mGraph 传入的图对象
* @param vertx 图对应的顶点个数
* @param data 图的各个顶点的值
* @param weight 图的邻接矩阵
*/
public void createGraph(MGraph mGraph, int vertx, char[] data, int[][] weight) {
for (int i = 0; i < vertx; i++) { //遍历顶点
mGraph.data[i] = data[i];
for (int j = 0; j < vertx; j++) {
//初始化邻接矩阵
mGraph.weight[i][j] = weight[i][j];
}
}
}
/**
* 显示图的邻接矩阵
*
* @param mGraph 图的对象
*/
public void showGraph(MGraph mGraph) {
for (int[] link : mGraph.weight) {
System.out.println(Arrays.toString(link));
}
}
/**
* 编写Prim算法,得到最小生成树
*
* @param mGraph 图
* @param v 表示从图的第几个顶点开始生成
*/
public void prim(MGraph mGraph, int v) {
//visited这个数组标记顶点是否被访问过
int[] visited = new int[mGraph.vertx];
//把当前这个节点标记为已访问
visited[v] = 1;
//用h1和h2记录两个顶点的下标
int h1 = -1;
int h2 = -1;
int minWeight = 10000; //将minWeight初始化成一个较大值,后面再遍历过程中会被替换
for (int k = 1; k < mGraph.vertx; k++) {//因为有mGraph.vertx个顶点,普利姆算法结束有mGraph.vertx-1条边
//在确定每次生成的子图和哪一个节点和这一次遍历的节点距离最近
for (int i = 0; i < mGraph.vertx; i++) { //i节点表示被访问过的顶点
for (int j = 0; j < mGraph.vertx; j++) { //j节点表示没有被访问过的顶点
if (visited[i] == 1 && visited[j] == 0 && mGraph.weight[i][j] < minWeight) {
//替换minWeight位置(寻找已经访问过的节点和未访问过的节点间权值最小的边)
minWeight = mGraph.weight[i][j];
h1 = i;
h2 = j;
}
}
}
//找到了一条边最小
System.out.println("边<" + mGraph.data[h1] + "," + mGraph.data[h2] + "> 权值:" + minWeight);
//将当前找到的节点标记为已经访问
visited[h2] = 1;
//minWeight重新设置为最大值10000
minWeight = 10000;
}
}
}
/**
* description
* 使用普利姆算法解决修路问题
*
* @author xujicheng
* @since 2022年12月13日 14:27
*/
public class PrimAlgorithm {
public static void main(String[] args) {
//测试图是否创建成功
char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int vertx = data.length;
//邻接矩阵的关系使用二维数组表示,10000表示较大的数,表示两个点不连通
int[][] weight = new int[][]{
{10000, 5, 7, 10000, 10000, 10000, 2},
{5, 10000, 10000, 9, 10000, 10000, 3},
{7, 10000, 10000, 10000, 8, 10000, 10000},
{10000, 9, 10000, 10000, 10000, 4, 10000},
{10000, 10000, 8, 10000, 10000, 5, 4},
{10000, 10000, 10000, 4, 5, 10000, 6},
{2, 3, 10000, 10000, 4, 6, 10000},};
//创建一个MGraph对象
MGraph mGraph = new MGraph(vertx);
//创建一个最小生成树对象
MinTree minTree = new MinTree();
minTree.createGraph(mGraph,vertx,data,weight);
//输出
minTree.showGraph(mGraph);
//测试普利姆算法
minTree.prim(mGraph,0);
}
}
```
## 14.7、克鲁斯卡尔算法
### 14.7.1、 应用场景-公交站问题
看一个应用场景和问题:
![image-20221213155001212](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213155001212.png)
1) 某城市新增 7 个站点(A, B, C, D, E, F, G) ,现在需要修路把 7 个站点连通
2) 各个站点的距离用边线表示(权) ,比如 A – B 距离 12 公里
3) 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
### 14.7.2、 克鲁斯卡尔算法介绍
1) 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
2) 基本思想:按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路
3) 具体做法:首先构造一个只含 n 个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止
### 14.7.3、 克鲁斯卡尔算法图解说明
以城市公交站问题来图解说明 克鲁斯卡尔算法的原理和步骤:
> 在含有 n 个顶点的连通图中选择 n-1 条边,构成一棵极小连通子图,并使该连通子图中n-1 条边上权值之和达到最小,则称其为连通网的最小生成树。
>
> ![image-20221213092247201](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213092247201.png)
>
> 例如,对于如上图 G4 所示的连通网可以有多棵权值总和不相同的生成树。
>
> ![image-20221213092256837](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213092256837.png)
>
> 以上图 G4 为例,来对克鲁斯卡尔进行演示(假设,用数组 R 保存最小生成树结果)。
>
> 克 鲁 斯 卡 尔 算 法 图 解
>
> ![image-20221213092339652](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213092339652.png)
>
> ![image-20221213092350694](040607_数据结构与算法的学习笔记.assets/image-20221213092350694.png)
>
> 第 1 步:将边<E,F>加入 R 中。 边<E,F>的权值最小,因此将它加入到最小生成树结果 R 中。
>
> 第 2 步:将边<C,D>加入 R 中。 上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果 R 中。
>
> 第 3 步:将边<D,E>加入 R 中。 上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果 R 中。
>
> 第 4 步:将边<B,F>加入 R 中。 上一步操作之后,边<B,F>的权值最小,但会和已有的边构成回路;因此,跳过边。同理,跳过边。将边加入到最小生成树结果 R 中。
>
> 第 5 步:将边<E,G>加入 R 中。 上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果 R 中。
>
> 第 6 步:将边<A,B>加入 R 中。 上一步操作之后,边<F,G>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果 R 中
>
> 此时,最小生成树构造完成!它包括的边依次是:<E,F> <C,D> <D,E> <B,F><E,G><A,B>。
>
> 克 鲁 斯 卡 尔 算 法 分 析
>
> 根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:
>
> **问题一** 对图的所有边按照权值大小进行排序。
>
> **问题二** 将边添加到最小生成树中时,怎么样判断是否形成了回路。
>
> 问题一很好解决,采用排序算法进行排序即可。
>
> 问题二,处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。
>
> 如 何 判 断 是 否 构 成 回 路 -举 例说明(如图)
>
> ![image-20221213092727754](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213092727754.png)
>
> 在将 <E,F> <C,D> <D,E> <B,F><E,G><A,B> 加入到最小生成树 R 中之后,这几条边的顶点就都有了终点:
>
> (01) C 的终点是 F。
>
> (02) D 的终点是 F。
>
> (03) E 的终点是 F。
>
> (04) F 的终点是 F。
>
> 关于终点的说明:
>
> 1) 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最大顶点"。
>
> 2) 因此,接下来,虽然是权值最小的边。但是 C 和 E 的终点都是 F,即它们的终点相同,因此,将加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的**两个顶点**不能都**指向同一个终点**,**否则将构成回路**。
### 14.7.4、克鲁斯卡尔最佳实践-公交站问题
看一个公交站问题:
1) 有北京有新增 7 个站点(A, B, C, D, E, F, G) ,现在需要修路把 7 个站点连通
2) 各个站点的距离用边线表示(权) ,比如 A – B 距离 12 公里
3) 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
4) 代码实现和注解
```JAVA
/**
* description
* 边类,它的对象实例就表示一条边
*
* @author xujicheng
* @since 2022年12月13日 17:17
*/
public class EData {
char start; //边的一个点
char end; //边的另外一个点
int weight; //边的权值
//构造器,用于初始化属性
public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
//重写toString方法,便于输出边的信息
@Override
public String toString() {
return "EData{" +
"start=" + start +
", end=" + end +
", weight=" + weight +
'}';
}
}
/**
* description
* 使用克鲁斯卡尔算法解决——公交站问题
*
* @author xujicheng
* @since 2022年12月13日 16:26
*/
public class KruskalCase {
private int edgeNum; //用于记录边的个数
private char[] vertex; //顶点数组的集合
private int[][] matrix; //邻接矩阵
private static final int INF = Integer.MAX_VALUE; //使用INF这个变量来表示两个顶点不能连通
public static void main(String[] args) {
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = {
/*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/ {0, 12, INF, INF, INF, 16, 14},
/*B*/ {12, 0, 10, INF, INF, 7, INF},
/*C*/ {INF, 10, 0, 3, 5, 6, INF},
/*D*/ {INF, INF, 3, 0, 4, INF, INF},
/*E*/ {INF, INF, 5, 4, 0, 2, 8},
/*F*/ {16, 7, 6, INF, 2, 0, 9},
/*G*/ {14, INF, INF, INF, 8, 9, 0}};
//创建一个KruskalCase的对象实例
KruskalCase kruskalCase = new KruskalCase(vertex, matrix);
//输出构建的矩阵图是否正确
kruskalCase.point();
kruskalCase.kruskal();
}
//构造器,用于初始化属性
public KruskalCase(char[] vertex, int[][] matrix) {
//初始化顶点数和边的个数
int vLength = vertex.length;
//使用拷贝的方式初始化顶点
this.vertex = new char[vLength];
for (int i = 0; i < vertex.length; i++) {
this.vertex[i] = vertex[i];
}
//使用拷贝的方式初始化边
this.matrix = new int[vLength][vLength];
for (int i = 0; i < vLength; i++) {
for (int j = 0; j < vLength; j++) {
this.matrix[i][j] = matrix[i][j];
}
}
//统计边的条数
for (int i = 0; i < vLength; i++) {
for (int j = i + 1; j < vertex.length; j++) {
if (this.matrix[i][j] != INF) {
edgeNum++;
}
}
}
}
public void kruskal() {
int index = 0; //表示最后结果数组的索引
int[] ends = new int[edgeNum]; //用于保存"已有最小生成树"中的每个顶点在最小生成树中的终点
//创建结果数组,保存最后的最小生成树
EData[] result = new EData[edgeNum];
//获取图中所有的边的集合,一共有12条边
EData[] edges = getEdges();
//按照边的权值大小进行排序(从小到大)
sortEdges(edges);
//遍历edges 数组,将边添加到最小生成树中时,判断准备加入的边是否形成回路,如果没有加入result,否则不能加入
for (int i = 0; i < edgeNum; i++) {
//获取到第i条边的第一个顶点(起点)
int p1 = getPosition(edges[i].start);
//获取到第i条边的第二个顶点(终点)
int p2 = getPosition(edges[i].end);
//获取p1这个顶点在已有的最小生成树中终点是哪一个
int m = getEnd(ends, p1);
//获取p2这个顶点在已有的最小生成树中终点是哪一个
int n = getEnd(ends, p2);
//判断是否构成回路
if (m != n) { //没有构成回路
ends[m] = n; //设置m在"已有最小生成树"中的终点
result[index++] = edges[i]; //有一条边加入到result数组
}
}
//统计并打印最小生成树,输出result[]
System.out.println("最小生成树为");
for (int i = 0; i < index; i++) {
System.out.println(result[i]);
}
}
//打印邻接矩阵的方法
public void point() {
System.out.println("邻接矩阵为:\n");
for (int i = 0; i < vertex.length; i++) {
for (int j = 0; j < vertex.length; j++) {
System.out.printf("%10d ", matrix[i][j]);
}
System.out.println(); //换行
}
}
/**
* 对边进行排序处理,使用冒泡排序
*
* @param edges 边的集合
*/
private void sortEdges(EData[] edges) {
for (int i = 0; i < edges.length - 1; i++) {
for (int j = 0; j < edges.length - 1 - i; j++) {
if (edges[j].weight > edges[j + 1].weight) { //交换
EData tmp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = tmp;
}
}
}
}
/**
* 根据一个顶点返回对应的下标的方法
*
* @param ch 顶点的值,比如'A'、'B'
* @return 返回ch顶点对应的下标,如果找不到,返回-1
*/
private int getPosition(char ch) {
for (int i = 0; i < vertex.length; i++) {
if (vertex[i] == ch) { //找到
return i;
}
}
//如果找不到返回-1即可
return -1;
}
/**
* 功能:获取图中的边,放到EData[]中,需要遍历该数组
* 是通过matrix 邻接矩阵来获取;存放形式:EData[] [['A','B',12],['B','F',7]......]
*
* @return 创建好的边
*/
public EData[] getEdges() {
int index = 0;
EData[] edges = new EData[edgeNum];
for (int i = 0; i < vertex.length; i++) {
for (int j = i + 1; j < vertex.length; j++) {
if (matrix[i][j] != INF) {
edges[index++] = new EData(vertex[i], vertex[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 获取下标为i的顶点的终点,用于判断两个顶点的终点是否相同
*
* @param ends 记录了各个顶点对应的重点是哪个,ends这个数组是在遍历过程中逐步形成的
* @param i 表示传入的顶点对应的下标
* @return 返回的是下标为i的这个顶点对应的终点的下标
*/
private int getEnd(int[] ends, int i) {
while (ends[i] != 0) {
i = ends[i];
}
return i;
}
}
```
## 14.8、 迪杰斯特拉算法
### 14.8.1、应用场景-最短路径问题
看一个应用场景和问题:
![image-20221213092929487](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213092929487.png)
1) 战争时期,胜利乡有 7 个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从 G 点出发,需要分别把邮件分别送到A, B, C , D, E, F 六个村庄
2) 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
3) 问:如何计算出 G 村庄到 其它各个村庄的最短距离? 4) 如果从其它点出发到各个点的最短距离又是多少?
### 14.8.2 、杰斯特拉(Dijkstra)算法介绍
迪杰斯特拉(Dijkstra)算法是**典型最短路径算法**,用于计算一个结点到其他结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(**广度优先搜索思想**),直到扩展到终点为止。
### 14.8.3、迪杰斯特拉(Dijkstra)算法过程
1) 设置出发顶点为 v,顶点集合 V{v1,v2,vi...},v 到 V 中各顶点的距离构成距离集合Dis,Dis{d1,d2,di...},Dis集合记录着 v 到图中各顶点的距离(到自身可以看作 0,v 到 vi 距离对应为 di)
2) 从 Dis 中选择值最小的 di 并移出 Dis 集合,同时移出 V 集合中对应的顶点 vi,此时的v 到vi 即为最短路径
3) 更新 Dis 集合,更新规则为:比较 v 到 V 集合中顶点的距离值,与 v 通过 vi 到 V 集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为 vi,表明是通过 vi 到达的)
4) 重复执行两步骤,直到最短路径顶点为目标顶点即可结束
### 14.8.4、迪杰斯特拉(Dijkstra)算法最佳应用-最短路径
![image-20221213225626577](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213225626577.png)
1) 战争时期,胜利乡有 7 个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从 G 点出发,需要分别把邮件分别送到A, B, C , D, E, F 六个村庄
2) 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
3) 问:如何计算出 G 村庄到 其它各个村庄的最短距离?
4) 如果从其它点出发到各个点的最短距离又是多少?
5) 使用图解的方式分析了迪杰斯特拉(Dijkstra)算法 思路
![image-20221214115655973](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221214115655973.png)
6) 代码实现
```JAVA
/**
* description
* 创建算法所需要的图
*
* @author xujicheng
* @since 2022年12月14日 9:54
*/
public class Graph {
private char[] vertex; //存放顶点的数组
private int[][] matrix; //邻接矩阵
private VisitedVertex vv; //表示已经访问的顶点的集合
//构造器,用于初始化顶点和邻接矩阵
public Graph(char[] vertex, int[][] matrix) {
this.vertex = vertex;
this.matrix = matrix;
}
//显示结果
public void showDijkstra(){
vv.show();
}
//显示图的方法
public void showGraph() {
for (int[] link : matrix) {
System.out.println(Arrays.toString(link));
}
}
/**
* 迪杰斯拉算法实现
*
* @param index 表示出发顶点对应的下标
*/
public void dsj(int index) {
vv = new VisitedVertex(vertex.length, index);
update(index); //更新index下标顶点到周围顶点的距离和前驱顶点
for (int i = 1; i < vertex.length; i++) {
index = vv.updateArr(); //选择并返回新的访问顶点
update(index); //更新index到周围顶点的距离和前驱节点
}
}
/**
* 更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点
*
* @param index 索引
*/
private void update(int index) {
int len = 0;
//根据遍历我们的邻接矩阵的matrix[index]行
for (int j = 0; j < matrix[index].length; j++) {
//出发顶点到index顶点的距离加上index顶点的距离 + 从index顶点到j顶点的距离的和
len = vv.getDis(index) + matrix[index][j];
//如果j这个顶点没有被访问,并且len小于出发顶点到j这个顶点的距离,就需要更新
if (!vv.in(j) && len < vv.getDis(j)) {
vv.updatePre(j, index); //更新j顶点的前驱为index这个顶点
vv.updateDis(j, len); //更新出发顶点j顶点的距离
}
}
}
}
/**
* description
* 已访问顶点集合
*
* @author xujicheng
* @since 2022年12月14日 10:03
*/
public class VisitedVertex {
//记录各个顶点是否访问过,1表示访问过,0表示未访问,会动态更新
public int[] already_arr;
//每个下标对应的值前一个顶点下标,会动态更新
public int[] pre_visited;
//记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就好记录G到其他顶点的距离,会动态更新,求的最短距离就会存放到dis
public int[] dis;
/**
* 构造器,用于初始化对象属性
*
* @param length 表示顶点的个数
* @param index 出发顶点对应的下标,表示从哪个顶点开始处理
*/
public VisitedVertex(int length, int index) {
this.already_arr = new int[length];
this.pre_visited = new int[length];
this.dis = new int[length];
//初始化dis[]
Arrays.fill(dis, 65535);
this.already_arr[index] = 1; //设置出发顶点被访问过
this.dis[index] = 0; //设置出发顶点的访问距离为0
}
/**
* 功能:判断index顶点是否被访问过
*
* @param index 顶点对应的下标
* @return 如果访问过,就返回true,否则返回false
*/
public boolean in(int index) {
return already_arr[index] == 1;
}
/**
* 功能:更新出发顶点到index这个顶点的距离
*
* @param index 需要被更新的顶点
* @param len 需要被更新的对应值
*/
public void updateDis(int index, int len) {
dis[index] = len;
}
/**
* 功能:更新pre这个顶点的前驱顶点为index的节点
*
* @param pre 下标为pre的前驱顶点
* @param index 索引
*/
public void updatePre(int pre, int index) {
pre_visited[pre] = index;
}
/**
* 功能:返回出发顶点到index这个顶点的距离
*
* @param index 参照顶点
* @return 返回出发顶点到index这个顶点的距离
*/
public int getDis(int index) {
return dis[index];
}
/**
* 继续选择并返回新的访问顶点,比如这里的G 完后,就是A点作为新的访问顶点(注意不是出发顶点)
*
* @return 返回新的访问顶点的下标
*/
public int updateArr() {
int min = 65535;
int index = 0;
for (int i = 0; i < already_arr.length; i++) {
if (already_arr[i] == 0 && dis[i] < min) {
min = dis[i];
index = i;
}
}
//更新index这个顶点被访问过
already_arr[index] = 1;
return index;
}
//显示最后的结果,即将三个数组的输出情况
public void show() {
System.out.println("==========================");
//输出already_arr
for (int i : already_arr) {
System.out.print(i + " ");
}
System.out.println();
//输出pre_visited
for (int i : pre_visited) {
System.out.print(i + " ");
}
System.out.println();
//输出dis
for (int i : dis) {
System.out.print(i + " ");
}
System.out.println();
//为了方便显示最后的最短距离,处理一下数据
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int count = 0;
for (int i : dis) {
if (i != 65535) {
System.out.print(vertex[count] + "(" + i + ")");
} else {
System.out.println("N");
}
count++;
}
System.out.println();
}
}
/**
* description
* 使用迪杰斯特拉算法解决最短路径问题
*
* @author xujicheng
* @since 2022年12月14日 9:53
*/
public class DijkstraAlgorithm {
public static void main(String[] args) {
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535; //表示不可连接
matrix[0]=new int[]{N,5,7,N,N,N,2};
matrix[1]=new int[]{5,N,N,9,N,N,3};
matrix[2]=new int[]{7,N,N,N,8,N,N};
matrix[3]=new int[]{N,9,N,N,N,4,N};
matrix[4]=new int[]{N,N,8,N,N,5,4};
matrix[5]=new int[]{N,N,N,4,5,N,6};
matrix[6]=new int[]{2,3,N,N,4,6,N};
//创建Graph(图对象)
Graph graph = new Graph(vertex, matrix);
//测试,查看图的邻接矩阵是否正常显示
graph.showGraph();
//测试迪杰斯拉算法
graph.dsj(6);
graph.showDijkstra();
}
}
```
## 14.9 、弗洛伊德算法
### 14.9.1、弗洛伊德(Floyd)算法介绍
1) 和 Dijkstra 算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978 年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
2) 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
3) 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
4) 弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径
### 14.9.2、弗洛伊德(Floyd)算法图解分析
1) 设置顶点 vi 到顶点 vk 的最短路径已知为 Lik,顶点 vk 到 vj 的最短路径已知为 Lkj,顶点vi 到vj 的路径为Lij,则 vi 到 vj 的最短路径为:min((Lik+Lkj),Lij),vk 的取值为图中所有顶点,则可获得vi 到vj 的最短路径
2) 至于 vi 到 vk 的最短路径 Lik 或者 vk 到 vj 的最短路径 Lkj,是以同样的方式获得
3) 弗洛伊德(Floyd)算法图解分析-举例说明
> 示例:求最短路径为例说明
>
> ![image-20221214142534434](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221214142534434.png)
>
> ![image-20221213093433790](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213093433790.png)
>
> 弗洛伊德算法的步骤: 第一轮循环中,以 A(下标为:0)作为中间顶点【即把 A 作为中间顶点的所有情况都进行遍历, 就会得到更新距离表和前驱关系】,距离表和前驱关系更新为:
>
> ![image-20221213093449542](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213093449542.png)
>
> 分析如下:
>
> 1) 以 A 顶点作为中间顶点是,B->A->C 的距离由 N->9,同理 C 到 B;C->A->G 的距离由 N->12,同理G 到C
> 2) 更换中间顶点,循环执行操作,直到所有顶点都作为中间顶点更新后,计算结束
>
> ![image-20221214143711454](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221214143711454.png)
>
> 中间顶点 [A, B, C, D, E, F, G]
>
> 出发顶点 [A, B, C, D, E, F, G]
>
> 终点 [A, B, C, D, E, F, G]
### 14.9.3 、弗洛伊德(Floyd)算法最佳应用-最短路径
![image-20221213093622415](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213093622415.png)
1) 胜利乡有 7 个村庄(A, B, C, D, E, F, G)
2) 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
3) 问:如何计算出各村庄到 其它各村庄的最短距离?
4) 代码实现
```JAVA
/**
* description
* 创建弗洛伊德算法对应的图
*
* @author xujicheng
* @since 2022年12月14日 14:52
*/
public class Graph {
private char[] vertex; //用于存放顶点
private int[][] dis; //保存从各个顶点出发到其他顶点的距离,最后的结果,也是保留在该数组中
private int[][] pre; //保存到达目标顶点的前驱顶点
/**
* 构造器,用于初始化对象属性
*
* @param length 大小
* @param matrix 邻接矩阵
* @param vertex 顶点数组
*/
public Graph(int length, int[][] matrix, char[] vertex) {
this.vertex = vertex;
this.dis = matrix;
this.pre = new int[length][length];
//对pre数组初始化,存放的是前驱节点的下标,并不是直接存放顶点
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
//显示pre数组和dis数组
public void show() {
//为了增加可读性,我们优化一下输出
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
for (int k = 0; k < dis.length; k++) {
//先将pre数组输出一行
for (int i = 0; i < dis.length; i++) {
System.out.print(vertex[pre[k][i]] + " ");
}
System.out.println();
//输出dis数组的一行数据
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路径是" + dis[k][i] + ") ");
}
System.out.println();
}
}
//弗洛伊德算法
public void floyd() {
int len = 0; //用于保存距离
//对中间顶点的遍历,k就是中间顶点的下标[A ,B ,C ,D ,E ,F ,G]
for (int k = 0; k < dis.length; k++) {
//从i顶点开始出发[A ,B ,C ,D ,E ,F ,G]
for (int i = 0; i < dis.length; i++) {
//到达j顶点
for (int j = 0; j < dis.length; j++) {
//求出从i顶点出发,经过k这个中间顶点到达j这个顶点的距离
len = dis[i][k] + dis[k][j];
if (len < dis[i][j]) { //如果len小于dis[i][j]直连距离
dis[i][j] = len; //更新距离
pre[i][j] = pre[k][j]; //更新前驱顶点
}
}
}
}
}
}
/**
* description
* 弗洛伊德解决最短路径问题
*
* @author xujicheng
* @since 2022年12月14日 14:48
*/
public class FloydAlgorithm {
public static void main(String[] args) {
//测试图是否创建成功
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//创建邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[]{0, 5, 7, N, N, N, 2};
matrix[1] = new int[]{5, 0, N, 9, N, N, 3};
matrix[2] = new int[]{7, N, 0, N, 8, N, N};
matrix[3] = new int[]{N, 9, N, 0, N, 4, N};
matrix[4] = new int[]{N, N, 8, N, 0, 5, 4};
matrix[5] = new int[]{N, N, N, 4, 5, 0, 6};
matrix[6] = new int[]{2, 3, N, N, 4, 6, 0};
//创建一个图对象
Graph graph = new Graph(vertex.length, matrix, vertex);
//调用弗洛伊德算法
graph.floyd();
graph.show();
}
}
```
## 14.10、马踏棋盘算法
### 14.10.1 、马踏棋盘算法介绍和游戏演示
1) 马踏棋盘算法也被称为骑士周游问题
2) 将马随机放在国际象棋的 8×8 棋盘 Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部 64 个方格
![image-20221213093706999](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213093706999.png)
### 14.10.2、马踏棋盘游戏代码实现
1) 马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。
2) 如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了 53 个点,如图:走到了第53 个,坐标(1,0),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯……,思路分析+代码实现
* 对第一种实现方式的思路图解
![image-20221213093753899](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221213093753899.png)
4) 使用前面的游戏来验证算法是否正确
5) 代码实现
```JAVA
/**
* description
* 使用回溯算法解决马踏棋盘问题
*
* @author xujicheng
* @since 2022年12月14日 16:10
*/
public class HoseChessboard {
private static int X; //表示棋盘的列数
private static int Y; //棋盘的行数
//创建一个数组,标记棋盘的各个位置是否被访问
private static boolean[] visited;
//使用一个属性标记棋盘的所有位置是否都被访问过了
private static boolean finished; //如果为ture,表示成功
public static void main(String[] args) {
//测试骑士周游算法是否正确
X = 8;
Y = 8;
int row = 1; //马儿走的初始位置的行,从1开始编号
int column = 1; //马儿走的初始位置的列,也从1开始编号
//创建棋盘
int[][] chessboard = new int[X][Y];
visited = new boolean[X * Y]; //初始值都是false
//测试一下耗时
long start = System.currentTimeMillis();
traversalChessboard(chessboard, row - 1, column - 1, 1);
long end = System.currentTimeMillis();
System.out.println("共耗时:" + (end - start) + " 毫秒");
//输出棋盘的最后情况
for (int[] rows : chessboard){
for (int step:rows){
System.out.print(step + "\t");
}
System.out.println();
}
}
/**
* 完成骑士周游问题的算法
*
* @param chessboard 棋盘
* @param row 马儿当前的位置的行,从0开始
* @param column 马儿当前位置的列,从0开始
* @param step 是第几步,初始位置就是第一步
*/
public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
//先把行和列标记为当前这一步走的
chessboard[row][column] = step;
//再把当前位置标记成已经访问过的
visited[row * X + column] = true;
//获取当前位置可以的下一个位置的集合
ArrayList<Point> ps = next(new Point(column, row));
//遍历ps
while (!ps.isEmpty()) {
Point p = ps.remove(0); //取出下一个可以走的位置
//判断该点是否已经访问过了
if (!visited[p.y * X + p.x]) { //说明还没有访问过
traversalChessboard(chessboard, p.y, p.x, step + 1);
}
}
//判断马儿是否完成了任务,使用step和应该走的步数比较,如果没有达到数量,则表示没有完成任务,将整个棋盘置0
if (step < X * Y && !finished) {
chessboard[row][column] = 0;
//棋盘到目前位置,仍然没有走完
visited[row * X + column] = false;
} else {
//棋盘处于一个回溯过程
finished = true;
}
}
/**
* 功能:根据当前位置(Point对象),计算马儿还能走哪些位置(Point)
* 并放入到一个集合中(ArrayList),最多有八个位置
*
* @param curPoint 当前这个点
*/
public static ArrayList<Point> next(Point curPoint) {
//创建一个ArrayList
ArrayList<Point> ps = new ArrayList<>();
//创建一个Point
Point p1 = new Point();
//表示马儿可以是否走5这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走6这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走7这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走0这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走1这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走2这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走3这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走4这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
return ps;
}
}
```
### 14.10.3、使用贪心算法对马踏棋盘进行优化
思路分析:
![image-20221214175520289](https://xujicheng.oss-cn-guangzhou.aliyuncs.com/picture/typoraimage-20221214175520289.png)
代码实现:
```JAVA
/**
* description
* 使用回溯算法解决马踏棋盘问题
*
* @author xujicheng
* @since 2022年12月14日 16:10
*/
public class HoseChessboard {
private static int X; //表示棋盘的列数
private static int Y; //棋盘的行数
//创建一个数组,标记棋盘的各个位置是否被访问
private static boolean[] visited;
//使用一个属性标记棋盘的所有位置是否都被访问过了
private static boolean finished; //如果为ture,表示成功
public static void main(String[] args) {
//测试骑士周游算法是否正确
X = 8;
Y = 8;
int row = 1; //马儿走的初始位置的行,从1开始编号
int column = 1; //马儿走的初始位置的列,也从1开始编号
//创建棋盘
int[][] chessboard = new int[X][Y];
visited = new boolean[X * Y]; //初始值都是false
//测试一下耗时
long start = System.currentTimeMillis();
traversalChessboard(chessboard, row - 1, column - 1, 1);
long end = System.currentTimeMillis();
System.out.println("共耗时:" + (end - start) + " 毫秒");
//输出棋盘的最后情况
for (int[] rows : chessboard) {
for (int step : rows) {
System.out.print(step + "\t");
}
System.out.println();
}
}
/**
* 完成骑士周游问题的算法
*
* @param chessboard 棋盘
* @param row 马儿当前的位置的行,从0开始
* @param column 马儿当前位置的列,从0开始
* @param step 是第几步,初始位置就是第一步
*/
public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
//先把行和列标记为当前这一步走的
chessboard[row][column] = step;
//再把当前位置标记成已经访问过的
visited[row * X + column] = true;
//获取当前位置可以的下一个位置的集合
ArrayList<Point> ps = next(new Point(column, row));
//对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
sort(ps);
//遍历ps
while (!ps.isEmpty()) {
Point p = ps.remove(0); //取出下一个可以走的位置
//判断该点是否已经访问过了
if (!visited[p.y * X + p.x]) { //说明还没有访问过
traversalChessboard(chessboard, p.y, p.x, step + 1);
}
}
//判断马儿是否完成了任务,使用step和应该走的步数比较,如果没有达到数量,则表示没有完成任务,将整个棋盘置0
if (step < X * Y && !finished) {
chessboard[row][column] = 0;
//棋盘到目前位置,仍然没有走完
visited[row * X + column] = false;
} else {
//棋盘处于一个回溯过程
finished = true;
}
}
/**
* 功能:根据当前位置(Point对象),计算马儿还能走哪些位置(Point)
* 并放入到一个集合中(ArrayList),最多有八个位置
*
* @param curPoint 当前这个点
*/
public static ArrayList<Point> next(Point curPoint) {
//创建一个ArrayList
ArrayList<Point> ps = new ArrayList<>();
//创建一个Point
Point p1 = new Point();
//表示马儿可以是否走5这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走6这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走7这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走0这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走1这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走2这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走3这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走4这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
return ps;
}
//根据当前这一步的所有的下一步的选择位置,进行非递减排序
public static void sort(ArrayList<Point> ps) {
ps.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
//获取到o1的下一步的所有位置个数
int count = next(o1).size();
//获取到o2的下一步的所有位置个数
int count2 = next(o2).size();
if (count < count2) {
return -1;
} else if (count == count2) {
return 0;
} else {
return 1;
}
}
});
}
}
```
至此算法完结,持续刷题!!!!