34.数据结构和算法
一、数据结构和算法的重要性
- 算法是程序的灵魂,优秀的程序可以在海量数据计算时,依然保持高速计算
- 数据结构有两大类:线性结构(栈、队列、链表)和非线性结构(图、树)
- 一般来讲,程序会使用了内存计算框架(比如Spark)和缓存技术(比如Redis等)来优化程序,这些计算框架和缓存技术,它的核心功能也是算法
- 拿实际工作经历来说,在Unix下开发服务器程序,功能是要支持上千万人同时在线,在上线前,做内测,一切OK,可上线后,服务器就支撑不住了,公司的CTO对代码进行优化,再次上线,坚如磐石。你就能感受到程序是有灵魂的,就是算法。
- 目前程序员面试的门槛越来越高,很多一线IT公司(大厂),都会有数据结构和算法面试题
- 如果你不想永远都是代码工人,那就花时间来研究下数据结构和算法
二、数据结构和算法概述
1.数据结构和算法的关系#
- 数据(data)结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构。学好数据结构可以编写出更加漂亮,更加有效率的代码。(学好数据结构只是为学好算法打下基础)
- 要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决。
- 程序 = 数据结构 + 算法
- 数据结构是算法的基础,换言之,想要学好算法,需要把数据结构学到位
- 简单的一些问题,只用数据结构基本就可以搞定,对于一些比较复杂的问题,要有专门的数据结构+算法
2.看几个实际编程中遇到的问题#
1.字符串替换#
public static void main(String[] args) {
String str = "Java,Java, world!";
String newStr = str.replaceAll("Java", "hello"); //算法
System.out.println("newStr=" + newStr);
}
- 问:试写出用单链表表示的字符串类及字符串节点类的定义,并依次实现它的构造函数、以及计算串长度、串赋值、判断两串相等、求子串、两串连接、求子串在串中位置等7个成员函数。 (用单链表实现字符串的相关功能)
- 解决方式:需要使用到单链表数据结构
2.其它常见算法问题#
- 磁盘问题
- 公交车
- 画图
- 矩阵中查找单词路径数
- 球和篮子
- 扔石头
- PlayCards
- 扑克牌组三张以上
- 修路问题:最小生成树(加权值)(数据结构)+普利姆算法
- 最短路径问题:图(数据结构)+弗洛伊德算法
- 汉诺塔:分治算法
- 八皇后问题:回溯算法
3.线性结构和非线性结构#
- 数据结构包括:线性结构和非线性结构
- 线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)
- 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的(在内存分配的时候,地址是连续的)
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息(有可能连续,也有可能不连续,每一个数据相当于一个节点,靠指针或者地址连接,节点之间的地址不一定是连续的,链表可以充分利用碎片内存)
- 线性结构常见的有:数组、队列、链表、栈
- 非线性结构:二维数组、多维数组、广义表、树结构、图结构
三、稀疏数组和队列
1.稀疏数组sparsearray#
1.实际需求#
2.基本介绍#
-
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
-
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组(稀疏数组)中,从而缩小程序的规模
-
稀疏数组举例说明:左边是原始二维数组,右边是稀疏数组
3.应用实例#
-
使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
-
把稀疏数组存盘,并且可以从新恢复原来的二维数组数
-
整体思路分析
- 存盘退出:棋盘=>二维数组=》(稀疏数组)=>写入文件
- 遍历原始的二维数组,得到有效数据的个数
sum
- 根据有效数据的个数
sum
就可以创建稀疏数组sparseArr int[sum][3]
- 将二维数组的有效数据存入稀疏数组
- 遍历原始的二维数组,得到有效数据的个数
- 继续上局:读取文件=》(稀疏数组)=>二维数组=>棋盘
- 先读取稀疏数组的第一行的数据,创建原始的二维数组
arrays2 int[11][11]
- 再读取稀疏数组后面的数据,并赋给原始的二维数组
- 先读取稀疏数组的第一行的数据,创建原始的二维数组
- 存盘退出:棋盘=>二维数组=》(稀疏数组)=>写入文件
-
代码实现
/** * 稀疏数组 */ public class A_SparseArray { public static void main(String[] args) { /*二维数组(11*11) 稀疏数组(3*3) 0 0 0 0 0 0 0 0 0 0 0 ==> 行 列 值 0 0 1 0 0 0 0 0 0 0 0 11 11 2 0 0 0 2 0 0 0 0 0 0 0 1 2 1 0 0 0 0 0 0 0 0 0 0 0 2 3 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 */ //1.创建一个二维数组 11 * 11, 0:没有棋子,1:黑棋, 2:白棋 int[][] arrays = new int[11][11]; arrays[1][2] = 1; arrays[2][3] = 2; //1.获取有效值(非0数据)个数 int sum = 0; System.out.println("输出原始的二维数组:"); for (int i = 0; i < arrays.length; i++) { for (int j = 0; j < arrays[i].length; j++) { //格式化输出 System.out.printf("%d\t", arrays[i][j]); if (arrays[i][j] != 0) { sum++; } } System.out.println(); } //2.创建一个稀疏数组 int[][] sparseArray = new int[sum + 1][3]; //二维数组的有效值为稀疏数组的第1行第3列 sparseArray[0][2] = sum; //3.遍历二维数组,将非 0 的值,存放到稀疏数组中 int count = 0; //计数,用于记录是第几个非 0 的值 for (int i = 0; i < arrays.length; i++) { //二维数组的行数为稀疏数组的第1行第1列 sparseArray[0][0] = arrays.length; for (int j = 0; j < arrays[i].length; j++) { //二维数组的列数为稀疏数组的第1行第2列 sparseArray[0][1] = arrays[i].length; if (arrays[i][j] != 0) { count++; sparseArray[count][0] = i; sparseArray[count][1] = j; sparseArray[count][2] = arrays[i][j]; } } } System.out.println("输出稀疏数组:"); for (int[] sparse : sparseArray) { System.out.printf("%d\t%d\t%d\t\n", sparse[0], sparse[1], sparse[2]); } //还原稀疏数组:根据稀疏数组重新构建一个二维数组 //1.读取稀疏数组第一行的值,创建原始的二维数组 int[][] arrays2 = new int[sparseArray[0][0]][sparseArray[0][1]]; //2.读取稀疏数组后面的数据,并赋给原始的二维数组 for (int i = 1; i < sparseArray.length; i++) { arrays2[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2]; } System.out.println("输出恢复后的二维数组:"); for (int[] ints : arrays2) { for (int anInt : ints) { System.out.print(anInt + "\t"); } System.out.println(); } } }
4.课后练习#
- 要求:
- 在前面的基础上,将稀疏数组保存到磁盘上,比如 map.data
- 恢复原来的数组时,读取 map.data 进行恢复
- (可以使用对象流输出)
2.队列#
1.应用场景#
- 银行排队
2.队列介绍#
- 队列是一个有序列表,可以用数组或是链表来实现
- 遵循先入先出的原则。即:先存入队列的数据,要先取出,后存入的要后取出
3.数组模拟队列#
-
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图:(因为队列的输出、输入是分别从前后端来处理,因此需要两个变量
front
及rear
分别记录队列前后端的下标)Queue
:类,里面有一个数组maxSize
:是该队列的最大容量front
:记录队列前端的下标,会随着数据输出而改变(队列的头部,初始为-1
,数据加入不变化,取数据的时候不停的增加,取数据是在队列的头部取)(不包含)rear
:记录队列后端的下标,会随着数据输入而改变(队列的尾部,初始为-1
,数据加入不停的增加,取数据的时候不变化,加数据是在队列的尾部加)(包含)
-
思路分析:(将数据存入队列时称为
addQueue()
,以此分析)- 将尾指针往后移:
rear + 1
(front == rear //队列为空或者未满可以添加数据
) - 若尾指针
rear
小于队列的最大下标maxSize - 1
,则将数据存入rear
所指的数组元素中,否则无法存入数据。(rear == maxSize - 1 //队列满不可以添加数据
)
- 将尾指针往后移:
-
代码实现:
/** * 数组模拟队列 */ public class A_ArrayQueue { public static void main(String[] args) { //创建一个队列 ArrayQueue queue = new ArrayQueue(3); char key = ' '; //接受用户的输入 Scanner scanner = new Scanner(System.in); //扫描器 boolean loop = true; //用于循环的变量 //输出一个菜单 while (loop) { System.out.println("a(add):将数据存入队列"); System.out.println("g(get):从队列中取数据"); System.out.println("s(show):显示队列所有数据"); System.out.println("h(head):查看队列头元素"); System.out.println("e(exit):退出系统"); key = scanner.next().charAt(0); //接收一个字符 switch (key) { case 'a': System.out.println("请输入数字:"); int num = scanner.nextInt(); queue.addQueue(num); break; case 'g': try { int res = queue.getQueue(); System.out.printf("取出的数据:%d\n", res); } catch (Exception e) { System.out.println(e.getMessage()); } break; case 's': try { queue.showQueue(); } catch (Exception e) { System.out.println(e.getMessage()); } break; case 'h': try { int res = queue.headQueue(); System.out.printf("队列头元素:%d\n", res); } catch (Exception e) { System.out.println(e.getMessage()); } break; case 'e': scanner.close(); loop = false; break; default: break; } } System.out.println("退出系统"); } } //使用数组模拟队列 class ArrayQueue { private int maxSize; //数组的最大容量 private int front; //队列的头部 private int rear; //队列的尾部 private int[] arr; //用于存放数据的数组,模拟队列 //创建队列的构造器 public ArrayQueue(int arrMaxSize) { this.maxSize = arrMaxSize; this.arr = new int[maxSize]; this.front = -1; //指向队列头部前一个位置(不包含) this.rear = -1; //指向队列尾部具体的位置(包含) } //判断队列是否满 public boolean isFull() { return rear == maxSize - 1; } //判断队列是否空 public boolean isNull() { return rear == front; } //将数据存入队列 public void addQueue(int n) { //判断队列是否满 if (isFull()) { System.out.println("队列已满,不能加入数据"); return; } rear++; //队列的尾部后移 arr[rear] = n; } //从队列中取数据,出队列操作 public int getQueue() { //判断队列是否空 if (isNull()) { //通过抛出异常处理 throw new RuntimeException("队列为空,不能取出数据"); } front++; //队列的头部后移 return arr[front]; } //显示队列所有数据 public void showQueue() { //遍历数组 if (isNull()) { //通过抛出异常处理 throw new RuntimeException("队列为空,没有数据"); } for (int i = front + 1; i < rear + 1; i++) { System.out.printf("arr[%d]=%d\n", i, arr[i]); } } //查看队列头元素(不是取出数据) public int headQueue() { if (isNull()) { //通过抛出异常处理 throw new RuntimeException("队列为空,没有数据"); } return arr[front + 1]; } }
-
问题分析并优化:
-
问题分析:目前数组只能使用一次, 没有达到复用的效果
-
优化:将这个数组使用算法,改进成一个环形的队列(取模:
%
)
-
4.数组模拟环形队列#
-
对前面的数组模拟队列的优化,充分利用数组,将数组看做是一个环形的。(通过取模的方式来实现即可)
Queue
:类,里面有一个数组maxSize
:是该队列的最大容量front
:指向队列第一个元素(arr[front]
),记录队列前端的下标,会随着数据输出而改变(队列的头部,初始为0
,取数据是在队列的头部取)(包含)rear
:指向队列最后一个元素的后一位,空出一个空间作为约定,记录队列后端的下标,会随着数据输入而改变(队列的尾部,初始为0
,加数据是在队列的尾部加)(不包含)
-
分析说明:
- 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定:
(rear + 1) % maxSize == front //队列满不可以添加数据
rear == front //队列为空或者未满可以添加数据
- 队列中有效的数据的个数:
(rear + maxSize - front) % maxSize
- 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定:
-
代码实现:
/** * 数组模拟环形队列 */ public class B_CircularQueue { public static void main(String[] args) { //创建一个环形队列 CircularQueue circularQueue = new CircularQueue(4); //队列的有效数据是3,有一个空间作为约定 char key = ' '; //接受用户的输入 Scanner scanner = new Scanner(System.in); //扫描器 boolean loop = true; //输出一个菜单 while (loop) { System.out.println("a(add):将数据存入队列"); System.out.println("g(get):从队列中取数据"); System.out.println("s(show):显示队列所有数据"); System.out.println("h(head):查看队列头元素"); System.out.println("e(exit):退出系统"); key = scanner.next().charAt(0); //接收一个字符 switch (key) { case 'a': System.out.println("请输入数字:"); int num = scanner.nextInt(); circularQueue.addQueue(num); break; case 'g': try { int res = circularQueue.getQueue(); System.out.printf("取出的数据:%d\n", res); } catch (Exception e) { System.out.println(e.getMessage()); } break; case 's': try { circularQueue.showQueue(); } catch (Exception e) { System.out.println(e.getMessage()); } break; case 'h': try { int res = circularQueue.headQueue(); System.out.printf("队列头元素:%d\n", res); } catch (Exception e) { System.out.println(e.getMessage()); } break; case 'e': scanner.close(); loop = false; break; default: break; } } System.out.println("退出系统"); } } //使用数组模拟队列 class CircularQueue { private int maxSize; //数组的最大容量 private int front; //队列的头部:指向队列第一个元素(arr[front]),默认为0 private int rear; //队列的尾部:指向队列最后一个元素的后一位,空出一个空间作为约定,默认为0 private int[] arr; //用于存放数据的数组,模拟队列 //创建队列的构造器 public CircularQueue(int arrMaxSize) { this.maxSize = arrMaxSize; this.arr = new int[maxSize]; } //判断队列是否满:尾索引的下一个为头索引时表示队列满 public boolean isFull() { return (rear + 1) % maxSize == front; } //判断队列是否空 public boolean isNull() { return rear == front; } //将数据存入队列 public void addQueue(int n) { //判断队列是否满 if (isFull()) { System.out.println("队列已满,不能加入数据"); return; } //rear指向队列最后一个元素的后一个位置,可以直接将数据加入 arr[rear] = n; rear = (rear + 1) % maxSize; //队列的尾部后移,必须考虑取模 } //从队列中取数据,出队列操作 public int getQueue() { //判断队列是否空 if (isNull()) { //通过抛出异常处理 throw new RuntimeException("队列为空,不能取出数据"); } //1.先把front对应的值保存到一个临时变量 int value = arr[front]; //2.将front后移 front = (front + 1) % maxSize; //队列的头部后移,必须考虑取模 //3.将临时保存的变量返回 return value; } //显示队列所有数据 public void showQueue() { //遍历数组 if (isNull()) { //通过抛出异常处理 throw new RuntimeException("队列为空,没有数据"); } //从front开始遍历,遍历多少个元素 for (int i = front; i < front + size(); i++) { System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]); } } //查看队列头元素(不是取出数据) public int headQueue() { if (isNull()) { //通过抛出异常处理 throw new RuntimeException("队列为空,没有数据"); } return arr[front]; } //队列中有效的数据的个数 public int size() { return (rear + maxSize - front) % maxSize; } }
四、链表
1.链表(Linked List)介绍#
-
链表是有序的列表,但是它在内存中实际存储结构如下:
-
小结:
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域(存数据), next 域(指向下一个节点)
- 如图:发现链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
2.单链表#
1.单链表介绍#
2.单链表的应用实例(单链表增删改查,包括顺序插入)及面试题(新浪、百度、腾讯)#
- 使用带head头的单向链表实现–水浒英雄排行榜管理,完成对英雄人物的增删改查操作:
- 第一种方法在添加英雄时,直接添加到链表的尾部(不考虑排序)
- 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
- 思路分析:
- 添加:
- 先创建一个
head
头节点,表示单链表的头 - 后面每添加一个节点,就直接加入到链表的最后
- 先创建一个
- 按照编号顺序添加:
- 首先通过辅助变量找到新添加的节点的位置(通过遍历找到)
新的节点.next = temp.next
temp.next = 新的节点
- 修改:
- 根据编号直接找到需要修改的节点
temp.name = newHeroNode.name
- 删除一个节点:
- 先找到需要删除的节点的前一个节点
temp
temp.next = temp.next.next
- 被删除的节点将不会有其他引用指向,会被垃圾回收机制回收
- 先找到需要删除的节点的前一个节点
- 遍历:
- 通过一个辅助变量,帮助遍历整个链表
- 添加:
- 面试题
- 求单链表中有效节点的个数
- 查找单链表中的倒数第n个节点(新浪面试题)
- 编写一个方法,接收head节点,同时接收一个index(倒数第index个节点)
- 先把链表从头到尾遍历,得到链表总的长度(
getLength(HeroNode head)
) - 得到长度length后,从链表的第一个开始遍历(length - index)个
- 单链表的反转(腾讯面试题,有点难度)
- 先定义一个节点
HeroNode reverseHead = new HeroNode(0, "");
- 从头到尾遍历原来的链表,每遍历一个节点,将其取出,并放在新的链表的最前端
原来的链表的head.next = reverseHead.next;
- 先定义一个节点
- 从尾到头打印单链表(逆序打印单链表)(百度面试题,要求方式1:反向遍历。方式2:Stack栈)
- 方式1:先将单链表反转,然后再遍历(缺点:会破坏原来的单链表的结构,不建议)
- 方式2:可以利用Stack栈这个数据结构,将各个节点压入到栈中,利用栈的先进后出的特点,实现逆序打印(没有改变链表的结构)
- 合并两个有序的单链表,合并之后的链表依然有序
/**
* 使用带head头的 单向链表 实现-水浒英雄排行榜管理,完成对英雄人物的增删改查操作
* 1. 第一种方法在添加英雄时,直接添加到链表的尾部(不考虑排序)
* 2. 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
*/
public class A_SingleLinkedList {
public static void main(String[] args) {
//创建节点
HeroNode hero1 = new HeroNode(1, "张三");
HeroNode hero2 = new HeroNode(2, "李四");
HeroNode hero3 = new HeroNode(3, "王五");
HeroNode hero4 = new HeroNode(4, "赵六");
//创建链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
System.out.println("直接添加到链表的尾部:");
singleLinkedList.add(hero1);
singleLinkedList.add(hero4);
singleLinkedList.add(hero2);
singleLinkedList.add(hero3);
singleLinkedList.list();
//创建链表
SingleLinkedList singleLinkedListByOrder = new SingleLinkedList();
System.out.println("根据排名插入到指定位置:");
singleLinkedListByOrder.addByOrder(hero1);
singleLinkedListByOrder.addByOrder(hero4);
singleLinkedListByOrder.addByOrder(hero2);
singleLinkedListByOrder.addByOrder(hero3);
singleLinkedListByOrder.list();
System.out.println("根据编号修改节点信息:");
HeroNode newHeroNode = new HeroNode(2, "ls");
singleLinkedListByOrder.update(newHeroNode);
singleLinkedListByOrder.list();
System.out.println("根据编号删除节点信息:");
singleLinkedListByOrder.delete(1);
singleLinkedListByOrder.list();
}
}
//定义SingleLinkedList管理英雄
class SingleLinkedList {
//初始化头节点,不存放具体的数据,作用就是表示单链表的头(固定不动,防止找不到链表最顶端)
private HeroNode head = new HeroNode(0, "");
//第一种方法在添加英雄时,直接添加到链表的尾部(不考虑排序)
public void add(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助变量temp
HeroNode temp = head;
//遍历链表,找到最后
while (true) {
//1.当下一个节点为空,找到当前链表的最后节点
if (temp.next == null) {
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//2.当退出while循环时,temp就指向了链表的最后,将最后这个节点的next指向新的节点
temp.next = heroNode;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助变量temp
//因为是单链表,因此找的temp是位于添加位置的前一个节点,否则不能插入
HeroNode temp = head;
boolean flag = false; //标志添加的编号是否存在,默认为false
//遍历链表,找到最后
while (true) {
if (temp.next == null) { //说明temp已经在链表的最后
break;
}
if (temp.next.id > heroNode.id) { //1.首先通过辅助变量找到新添加的节点的位置,在temp的后面插入
break;
} else if (temp.next.id == heroNode.id) { //说明添加的编号已经存在
flag = true;
break;
}
//将temp后移,遍历当前链表
temp = temp.next;
}
//判断flag的值
if (flag) {
System.out.printf("不能添加,编号 %d 已经存在\n", heroNode.id);
} else { //插入到链表中temp的后面
//2.新的节点.next = temp.next
heroNode.next = temp.next;
//3.temp.next = 新的节点
temp.next = heroNode;
}
}
//根据编号修改节点信息(编号不能修改)
public void update(HeroNode newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//1.根据编号直接找到需要修改的节点
//定义一个辅助变量temp
HeroNode temp = head.next;
boolean flag = false; //表示是否找到该节点
while (true) {
if (temp == null) {
break; //已经遍历完链表
}
if (temp.id == newHeroNode.id) {
flag = true;
break;
}
//将temp后移,遍历当前链表
temp = temp.next;
}
//根据flag判断是否找到要修改的节点
if (flag) {
//2.temp.name = newHeroNode.name
temp.name = newHeroNode.name;
} else {
System.out.printf("不能修改,编号 %d 不存在\n", newHeroNode.id);
}
}
//删除一个节点
public void delete(int id) {
//1.head不能动,因此需要一个temp辅助节点找到待删除节点的前一个节点
HeroNode temp = head;
boolean flag = false; //标志是否找到待删除节点
while (true) {
if (temp.next == null) { //已经到链表的最后
break;
}
if (temp.next.id == id) { //在比较时,是temp.next.id 和 需要删除的节点的id比较
//找到待删除节点的前一个节点temp
flag = true;
break;
}
//将temp后移,遍历当前链表
temp = temp.next;
}
//判断flag
if (flag) { //找到节点,删除
//2.temp.next = temp.next.next
temp.next = temp.next.next;
} else {
System.out.printf("不能删除,编号 %d 不存在\n", id);
}
}
//显示链表(遍历)
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//1.因为head节点不能动,因此需要一个辅助变量temp,帮助遍历整个链表
HeroNode temp = head.next;
//遍历链表,找到最后
while (true) {
//判断是否到链表的最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将temp后移,否则死循环
temp = temp.next;
}
}
}
//定义一个HeroNode,每个HeroNode对象就是一个节点
class HeroNode {
public int id; //编号
public String name; //姓名
public HeroNode next; //指向下一个节点
//构造器
public HeroNode(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
3.双向链表#
1.双向链表的应用实例#
-
单向链表的缺点分析:
- 单向链表查找的方向只能是一个方向(从
head
开始遍历),而双向链表可以向前或者向后查找 - 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除节点时,总是找到
temp
(temp
是待删除节点的前一个节点)
- 单向链表查找的方向只能是一个方向(从
-
使用带head头的双向链表实现–水浒英雄排行榜管理,完成对英雄人物的增删改查操作:
- 第一种方法在添加英雄时,直接添加到链表的尾部(不考虑排序)
- 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
-
思路分析:
-
每个节点包含 data 域(存数据), next 域(指向下一个节点), pre 域(指向前一个节点)
-
添加:
- 先找到双向链表的最后这个节点
temp.next = 新的节点
新的节点.pre = temp
-
按照编号顺序添加:(按照单链表的顺序添加,稍作修改)
- 首先通过辅助变量找到新添加的节点的位置(通过遍历找到)
新的节点.next = temp.next
temp.next = 新的节点
-
修改:(和单链表一样)
- 根据编号直接找到需要修改的节点
temp.name = newHeroNode.name
-
删除一个节点:(双向链表可以实现自我删除)
- 直接找到需要删除的节点
temp
temp.pre.next = temp.next
temp.next.pre = temp.pre
- 直接找到需要删除的节点
-
遍历:(和单链表一样,只是可以向前,也可以向后查找)
- 通过一个辅助变量,帮助遍历整个链表
-
/**
* 使用带head头的 双向链表 实现-水浒英雄排行榜管理,完成对英雄人物的增删改查操作
* 1. 第一种方法在添加英雄时,直接添加到链表的尾部(不考虑排序)
* 2. 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
*/
public class B_DoubleLinkedList {
public static void main(String[] args) {
//创建节点
HeroNode2 hero1 = new HeroNode2(1, "张三");
HeroNode2 hero2 = new HeroNode2(2, "李四");
HeroNode2 hero3 = new HeroNode2(3, "王五");
HeroNode2 hero4 = new HeroNode2(4, "赵六");
//创建链表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
System.out.println("直接添加到链表的尾部:");
doubleLinkedList.add(hero1);
doubleLinkedList.add(hero4);
doubleLinkedList.add(hero2);
doubleLinkedList.add(hero3);
doubleLinkedList.list();
//创建链表
DoubleLinkedList doubleLinkedListByOrder = new DoubleLinkedList();
System.out.println("根据排名插入到指定位置:");
doubleLinkedListByOrder.addByOrder(hero1);
doubleLinkedListByOrder.addByOrder(hero4);
doubleLinkedListByOrder.addByOrder(hero2);
doubleLinkedListByOrder.addByOrder(hero3);
doubleLinkedListByOrder.list();
System.out.println("根据编号修改节点信息:");
HeroNode2 newHeroNode = new HeroNode2(2, "ls");
doubleLinkedListByOrder.update(newHeroNode);
doubleLinkedListByOrder.list();
System.out.println("根据编号删除节点信息:");
doubleLinkedListByOrder.delete(1);
doubleLinkedListByOrder.list();
}
}
//定义DoubleLinkedList管理英雄
class DoubleLinkedList {
//初始化头节点,不存放具体的数据,作用就是表示双向链表的头(固定不动,防止找不到链表最顶端)
private HeroNode2 head = new HeroNode2(0, "");
//返回头节点
public HeroNode2 getHead() {
return head;
}
//第一种方法在添加英雄时,直接添加到链表的尾部(不考虑排序)
public void add(HeroNode2 heroNode) {
//因为head节点不能动,因此需要一个辅助变量temp
HeroNode2 temp = head;
//遍历链表,找到最后
while (true) {
//1.当下一个节点为空,找到当前链表的最后节点
if (temp.next == null) {
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//形成一个双向链表
//2.temp.next = 新的节点
temp.next = heroNode;
//3.新的节点.pre = temp
heroNode.pre = temp;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode2 heroNode) {
//因为head节点不能动,因此需要一个辅助变量temp
//因为是单链表,因此找的temp是位于添加位置的前一个节点,否则不能插入
HeroNode2 temp = head;
boolean flag = false; //标志添加的编号是否存在,默认为false
//遍历链表,找到最后
while (true) {
if (temp.next == null) { //说明temp已经在链表的最后
break;
}
if (temp.next.id > heroNode.id) { //1.首先通过辅助变量找到新添加的节点的位置,在temp的后面插入
break;
} else if (temp.next.id == heroNode.id) { //说明添加的编号已经存在
flag = true;
break;
}
//将temp后移,遍历当前链表
temp = temp.next;
}
//判断flag的值
if (flag) {
System.out.printf("不能添加,编号 %d 已经存在\n", heroNode.id);
} else { //插入到链表中temp的后面
//2.新的节点.next = temp.next
heroNode.next = temp.next;
//3.temp.next = 新的节点
temp.next = heroNode;
}
}
//根据编号修改节点信息(编号不能修改)(和单链表一样)
public void update(HeroNode2 newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//1.根据编号直接找到需要修改的节点
//定义一个辅助变量temp
HeroNode2 temp = head.next;
boolean flag = false; //表示是否找到该节点
while (true) {
if (temp == null) {
break; //已经遍历完链表
}
if (temp.id == newHeroNode.id) {
flag = true;
break;
}
//将temp后移,遍历当前链表
temp = temp.next;
}
//根据flag判断是否找到要修改的节点
if (flag) {
//2.temp.name = newHeroNode.name
temp.name = newHeroNode.name;
} else {
System.out.printf("不能修改,编号 %d 不存在\n", newHeroNode.id);
}
}
//删除一个节点(双向链表可以实现自我删除)
public void delete(int id) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//1.head不能动,直接找到需要删除的节点temp
HeroNode2 temp = head.next;
boolean flag = false; //标志是否找到待删除节点
while (true) {
if (temp == null) { //已经到链表的最后节点的next
break;
}
if (temp.id == id) { //在比较时,是temp.next.id 和 需要删除的节点的id比较
//找到待删除节点的前一个节点temp
flag = true;
break;
}
//将temp后移,遍历当前链表
temp = temp.next;
}
//判断flag
if (flag) { //找到节点,删除
//2.temp.pre.next = temp.next
temp.pre.next = temp.next;
//3.temp.next.pre = temp.pre(如果是最后一个节点,会出现空指针)
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.printf("不能删除,编号 %d 不存在\n", id);
}
}
//显示链表(遍历)(和单链表一样,只是可以向前,也可以向后查找)
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//1.因为head节点不能动,因此需要一个辅助变量temp,帮助遍历整个链表
HeroNode2 temp = head.next;
//遍历链表,找到最后
while (true) {
//判断是否到链表的最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将temp后移,否则死循环
temp = temp.next;
}
}
}
//定义一个HeroNode,每个HeroNode对象就是一个节点
class HeroNode2 {
public int id; //编号
public String name; //姓名
public HeroNode2 next; //指向下一个节点,默认为null
public HeroNode2 pre; //指向前一个节点,默认为null
//构造器
public HeroNode2(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "HeroNode2{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
4. 单向环形链表#
1.单向环形链表应用实例(Josephu问题)#
-
Josephu(约瑟夫、约瑟夫环)问题:设编号为1,2,……n 的 n 个人围坐一圈,约定编号为k(1 <= k <= n)的人从1开始报数,数到 m 的那个人出列,它的下一位又从1开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
-
提示:用一个不带头节点的循环链表来处理 Josephu 问题:先构成一个有 n 个节点的单循环链表,然后由 k 节点起从1开始计数,计到 m 时,对应节点从链表中删除,然后再从被删除节点的下一个节点又从1开始计数,直到最后一个节点从链表中删除算法结束。
-
思路分析:
-
添加:
- 先创建第一个节点,让
first
指向该节点,并形成环形 - 后面每创建一个新的节点,就把该节点,加入到已有的环形链表中即可
- 先创建第一个节点,让
-
遍历:
- 先让一个辅助变量
temp
,指向first
节点 - 然后通过一个
while
循环遍历该环形链表即可,temp.next == first
结束
- 先让一个辅助变量
-
出圈:
-
创建一个辅助变量
temp
,事先指向环形链表的尾节点(指向待删除节点的前一个节点) -
报数前,
first
和temp
移动k - 1
次 -
报数时,
first
和temp
指针同时移动m - 1
次 -
将
first
指向的节点出圈,first = first.next
,temp.next = first
,原来first
指向的节点没有任何引用,就会被回收
-
-
-
使用环形单向链表解决约瑟夫问题
/**
* Josephu(约瑟夫、约瑟夫环)问题:
* 设编号为1,2,……n 的 n 个人围坐一圈,约定编号为 k(1 <= k <= n)的人从1开始报数,
* 数到 m 的那个人出列,它的下一位又从1开始报数,数到 m 的那个人又出列,依次类推,
* 直到所有人出列为止,由此产生一个出队编号的序列
*/
public class C_CircleSingleLinkedList_Josephu {
public static void main(String[] args) {
//创建节点
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
System.out.println("添加5个人:");
circleSingleLinkedList.add(5);
circleSingleLinkedList.list();
System.out.println("出圈:");
circleSingleLinkedList.count(5, 1, 2);
}
}
//定义CircleSingleLinkedList,环形的单向链表
class CircleSingleLinkedList {
//创建一个first节点,当前没有编号,默认为空(头指针)
private Boy first = null;
/**
* 添加节点,构建一个环形链表
*
* @param nums 添加节点数量
*/
public void add(int nums) {
//nums要至少大于0
if (nums < 1) {
System.out.println("节点数量要至少有1个");
return;
}
Boy temp = null; //辅助变量,帮助构建环形链表(尾指针)
//使用for循环创建环形链表
for (int i = 1; i <= nums; i++) {
//根据编号,创建节点
Boy boy = new Boy(i);
//如果是第一个小孩,要形成一个环形
if (i == 1) {
//1. 先创建第一个节点,让first指向该节点,并形成环形
first = boy;
first.setNext(first);
temp = first; //让temp指向第一个节点,帮助形成环形
} else {
//2. 后面每创建一个新的节点,就把该节点,加入到已有的环形链表中即可
temp.setNext(boy);
boy.setNext(first);
temp = boy;
}
}
}
//遍历
public void list() {
//判断链表是否为空
if (first == null) {
System.out.println("链表为空");
return;
}
//因为first节点不能动,因此需要一个辅助变量temp,帮助遍历整个链表
//1.先让一个辅助变量temp,指向first节点
Boy temp = first;
while (true) {
System.out.printf("编号%d\n", temp.getId());
//2.然后通过一个while循环遍历该环形链表即可,temp.next == first结束
if (temp.getNext() == first) {
break;
}
temp = temp.getNext(); //temp后移
}
}
/**
* 根据用户的输入,生成一个出圈的顺序(2>4>1>5>3)
*
* @param n 最初有几个人在圈中(n=5:有5个人)
* @param k 从第几个人开始报数(k=1,从第1个人开始报数)
* @param m 数几下(m=2,数2下)
*/
public void count(int n, int k, int m) {
//先对数据进行校验
if (first == null || k < 1 || k > n) {
System.out.println("链表不能为空,要从正整数个人开始数,开始报数的人大于总人数");
return;
}
//1.创建一个辅助变量temp,事先指向环形链表的尾节点(指向待删除节点的前一个节点)
Boy temp = first;
while (true) {
if (temp.getNext() == first) { //temp指向环形链表的尾节点
break;
}
temp = temp.getNext(); //temp后移
}
//2.报数前,first和temp移动k - 1次
for (int i = 0; i < k - 1; i++) {
first = first.getNext();
temp = temp.getNext();
}
//循环操作,直到圈中只有一个节点
while (true) {
if (temp == first) { //圈中只有一个节点
break;
}
//3.报数时,first和temp指针同时移动m - 1次
for (int i = 0; i < m - 1; i++) {
first = first.getNext();
temp = temp.getNext();
}
//此时first指向的节点,就是要出圈的节点
System.out.printf("%d出圈\n", first.getId());
//4.将first指向的节点出圈,first = first.next,temp.next = first,原来first指向的节点没有任何引用,就会被回收
first = first.getNext();
temp.setNext(first);
}
System.out.printf("最后留在圈中的编号%d\n", first.getId()); //first和temp指向同一个节点
}
}
//定义一个Boy,每个Boy对象就是一个节点
class Boy {
private int id; //编号
private Boy next; //指向下一个节点,默认为null
public Boy(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
五、栈
1.栈的介绍#
-
栈的英文为(stack)
-
栈是一个先入后出(FILO-First In Last Out)的有序列表
-
栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
-
根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
-
入栈(push)和出栈(pop)的概念(如图所示)
2.栈的应用场景#
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中(
return
语句) - 递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中
- 表达式的转换(中缀表达式转后缀表达式)与求值
- 二叉树的遍历
- 图形的深度优先(depth-first)搜索法
3.栈的快速入门#
-
用数组模拟栈的使用,由于栈是一种有序列表,可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈、入栈等操作。
-
思路分析:
- 使用数组模拟栈
- 定义
top
表示栈顶,初始化为-1
- 入栈(push):当有数据加入到栈时,
top++;
,stack[top] = data;
- 出栈(pop):将栈顶的数据返回,
int temp = stack[top];
,top--;
,return temp;
- 遍历栈:栈是从栈顶取的,遍历的时候不能从0开始遍历,要从栈顶往下遍历
/**
* 用数组模拟栈的使用,由于栈是一种有序列表,可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈、入栈等操作
*/
public class A_ArrayStack {
public static void main(String[] args) {
//创建栈
ArrayStack stack = new ArrayStack(4);
char key = ' ';
boolean loop = true; //控制是否退出菜单
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("a(push):将数据存入栈(入栈)");
System.out.println("g(pop):从栈中取数据(出栈)");
System.out.println("s(show):显示栈中所有数据(遍历栈)");
System.out.println("e(exit):退出系统");
key = scanner.next().charAt(0); //接收一个字符
switch (key) {
case 'a':
System.out.println("请输入数字:");
int data = scanner.nextInt();
stack.push(data);
break;
case 'g':
try {
int res = stack.pop();
System.out.printf("出栈的数据:%d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
stack.list();
break;
case 'e':
scanner.close(); //关闭流,避免造成资源的泄露
loop = false;
break;
default:
break;
}
}
System.out.println("退出系统");
}
}
//定义ArrayStack表示栈
class ArrayStack {
private int maxTop; //栈的大小
private int[] stack; //1.使用数组模拟栈,数据放在该数组中
private int top = -1; //2.定义top表示栈顶,初始化为-1
public ArrayStack(int maxTop) {
this.maxTop = maxTop;
stack = new int[this.maxTop]; //初始化数组
}
//栈满
public boolean isFull() {
return top == maxTop - 1;
}
//栈空
public boolean isNull() {
return top == -1;
}
//3.入栈(push):当有数据加入到栈时,top++;,stack[top] = data;
public void push(int data) {
//先判断栈是否满
if (isFull()) {
System.out.println("栈满");
return;
}
top++;
stack[top] = data; //数据放入栈中
}
//4.出栈(pop):将栈顶的数据返回,int temp = stack[top];,top--;,return temp;
public int pop() {
//先判断栈是否空
if (isNull()) {
//抛出运行时异常(直接可以抛出,不捕获也没问题)
throw new RuntimeException("栈空");
}
//获取栈顶的数据
int temp = stack[top];
top--;
//将栈顶的数据返回
return temp;
}
//5.遍历栈:栈是从栈顶取的,遍历的时候不能从0开始遍历,要从栈顶往下遍历
public void list() {
//先判断栈是否空
if (isNull()) {
System.out.println("栈空");
return;
}
//从栈顶开始显示数据
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
}
- 练习:使用链表模拟栈
4.栈实现综合计算器(中缀表达式)#
- 使用栈来实现综合计算器 - 自定义优先级(priority)
7*2*2-5+1-5+3-4
- 请问:计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串)==》栈
- 思路分析:
3+2*6-2
- 创建两个栈,一个数栈
numStack
(存放数据),一个符号栈operStack
(存放运算符) - 通过一个
index
(索引),来遍历表达式 - 如果是一个数字,就直接入数栈
- 如果是一个符号,分如下情况:
- 如果当前的符号栈为空,就直接入符号栈
- 如果当前的符号栈不为空,就进行比较:
- 如果当前操作符的优先级小于或者等于栈中的操作符,就需要从数栈中
pop
出两个数,再从符号栈中pop
出一个符号,进行运算,将得到的结果入数栈,然后将当前的操作符入符号栈 - 如果当前操作符的优先级大于栈中的操作符,就直接入符号栈
- 如果当前操作符的优先级小于或者等于栈中的操作符,就需要从数栈中
- 当表达式扫描完毕,就顺序从数栈和符号栈中
pop
出相应的数和符号,并运算(后面pop
的数字与前面pop
出的数字做运算) - 最后在数栈中只有一个数字,就是表达式的结果
- 创建两个栈,一个数栈
- 发现问题:当处理多位数时,不能发现是一个数就立即入栈
- 解决方法:在处理数时,需要向表达式的
index
后再看一位,如果是数就继续扫描不能立即入栈,如果是符号才入栈,因此需要定义一个字符串变量,用于拼接
- 解决方法:在处理数时,需要向表达式的
/**
* 使用栈来实现综合计算器 - 自定义优先级(priority)
*/
public class B_Calculator {
public static void main(String[] args) {
//表达式
// String expression = "3+2*6-2"; //13
// String expression = "7*2*2-5+1-5+3-4"; //18
String expression = "70+2*6-4"; //78
//1. 创建两个栈,一个数栈(存放数据),一个符号栈(存放运算符)
CalculatorStack numStack = new CalculatorStack(10);
CalculatorStack operStack = new CalculatorStack(10);
//2. 通过一个index(索引),来遍历表达式
int index = 0; //用于扫描
int num1 = 0;
int num2 = 0;
int oper = 0;
int res = 0; //计算结果
char ch = ' '; //将每次扫描得到的char保存到ch中
String digits = ""; //用于拼接多位数
//使用while循环扫描表达式
while (true) {
//依次得到表达式的每一个字符
ch = expression.substring(index, index + 1).charAt(0);
//判断ch是什么,然后做相应的处理
//4. 如果是一个符号,分如下情况:
if (operStack.isOperator(ch)) {
//4.2. 如果当前的符号栈不为空,就进行比较:
if (!operStack.isNull()) {
//4.2.1. 如果当前操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,再从符号栈中pop出一个符号,进行运算,将得到的结果入数栈,然后将当前的操作符入符号栈
if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.calculate(num1, num2, oper);
//运算结果入数栈
numStack.push(res);
//当前的操作符入符号栈
operStack.push(ch);
} else {
//4.2.2. 如果当前操作符的优先级大于栈中的操作符,就直接入符号栈
operStack.push(ch);
}
} else {
//4.1. 如果当前的符号栈为空,就直接入符号栈
operStack.push(ch);
}
} else {
//3. 如果是一个数字,就直接入数栈
// numStack.push(ch - 48); //此为字符,要转换成数字
//发现问题:当处理多位数时,不能发现是一个数就立即入栈
//解决方法:在处理数时,需要向表达式的index后再看一位,如果是数就继续扫描不能立即入栈,如果是符号才入栈,因此需要定义一个字符串变量,用于拼接
//处理多位数
digits += ch;
//如果ch已经是表达式的最后一位,就直接入栈
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(digits));
} else {
//判断下一位字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
//只看后一位,不是index++
if (operStack.isOperator(expression.substring(index + 1, index + 2).charAt(0))) {
//如果后一位是运算符,则入栈
numStack.push(Integer.parseInt(digits));
//清空digits
digits = "";
}
}
}
//index + 1,判断是否扫描到表达式最后
index++;
if (index >= expression.length()) {
break;
}
}
//5. 当表达式扫描完毕,就顺序从数栈和符号栈中pop出相应的数和符号,并运算(后面pop的数字与前面pop出的数字做运算)
while (true) {
//6. 最后在数栈中只有一个数字,就是表达式的结果
//如果符号栈为空,则计算到最后的结果,数栈中只有一个数字(结果)
if (operStack.isNull()) {
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.calculate(num1, num2, oper);
numStack.push(res); //结果入栈
}
System.out.printf("表达式%s = %d", expression, numStack.pop());
}
}
//定义CalculatorStack表示栈
class CalculatorStack {
private int maxTop; //栈的大小
private int[] stack; //1.使用数组模拟栈,数据放在该数组中
private int top = -1; //2.定义top表示栈顶,初始化为-1
public CalculatorStack(int maxTop) {
this.maxTop = maxTop;
stack = new int[this.maxTop]; //初始化数组
}
//栈满
public boolean isFull() {
return top == maxTop - 1;
}
//栈空
public boolean isNull() {
return top == -1;
}
//3.入栈:当有数据加入到栈时,top++;,stack[top] = data;
public void push(int data) {
//先判断栈是否满
if (isFull()) {
System.out.println("栈满");
return;
}
top++;
stack[top] = data; //数据放入栈中
}
//4.出栈:int temp = stack[top];,top--;,return temp;
public int pop() {
//先判断栈是否空
if (isNull()) {
//抛出运行时异常(直接可以抛出,不捕获也没问题)
throw new RuntimeException("栈空");
}
//获取栈顶的数据
int temp = stack[top];
top--;
//将栈顶数据返回
return temp;
}
//5.遍历栈:栈是从栈顶取的,遍历的时候不能从0开始遍历,要从栈顶往下遍历
public void list() {
//先判断栈是否空
if (isNull()) {
System.out.println("栈空");
return;
}
//从栈顶开始显示数据
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 isOperator(char oper) {
return oper == '+' || oper == '-' || oper == '*' || oper == '/';
}
//计算
public int calculate(int num1, int num2, int oper) {
int res = 0; //计算结果
switch (oper) {
case '+':
res = num2 + num1;
break;
case '-':
res = num2 - num1; //注意顺序
break;
case '*':
res = num2 * num1;
break;
case '/':
res = num2 / num1; //注意顺序
break;
default:
break;
}
return res;
}
//只返回当前栈顶的值,不出栈
public int peek() {
return stack[top];
}
}
- 练习:加入小括号
5.前缀(波兰表达式)、中缀、后缀表达式(逆波兰表达式)#
1.前缀表达式(波兰表达式)#
-
前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
-
举例说明:中缀表达式
(3+4)*5-6
对应的前缀表达式就是- * + 3 4 5 6
2.前缀表达式的计算机求值#
- 从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果
- 例如:
(3+4)*5-6
对应的前缀表达式就是- * + 3 4 5 6
,针对前缀表达式求值步骤如下:- 从右至左扫描,将6、5、4、3压入堆栈
- 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
- 接下来是*运算符,因此弹出7和5,计算出7 * 5=35,将35入栈
- 最后是-运算符,计算出35 - 6的值,即29,由此得出最终结果
3.中缀表达式#
- 中缀表达式就是常见的运算表达式,如
(3+4)*5-6
- 中缀表达式的求值是人最熟悉的,但是对计算机来说却不好操作,因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)
4.后缀表达式#
-
后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
-
举例说明:
(3+4)*5-6
对应的后缀表达式就是3 4 + 5 * 6 –
正常的表达式 逆波兰表达式 a+b
a b +
a+(b-c)
a b c - +
a+(b-c)*d
a b c – d * +
a+d*(b-c)
a d b c - * +
a=1+3
a 1 3 + =
5.后缀表达式的计算机求值#
- 从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
- 例如:
(3+4)*5-6
对应的后缀表达式就是3 4 + 5 * 6 -
,针对后缀表达式求值步骤如下:- 从左至右扫描,将3和4压入堆栈;
- 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
- 将5入栈;
- 接下来是*运算符,因此弹出5和7,计算出7 * 5=35,将35入栈;
- 将6入栈;
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
6.逆波兰计算器#
-
完成一个逆波兰计算器:输入一个逆波兰表达式(后缀表达式),使用栈(Stack)计算其结果
-
要求:支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算
-
思路分析:
(3+4)*5-6
- 先将一个逆波兰表达式依次将数据和运算符放到
ArrayList
中 - 将
ArrayList
传递给一个方法,遍历ArrayList
配合栈完成计算- 从左至右扫描,将3和4压入堆栈;
- 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
- 将5入栈;
- 接下来是*运算符,因此弹出5和7,计算出7 * 5=35,将35入栈;
- 将6入栈;
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
- 先将一个逆波兰表达式依次将数据和运算符放到
/**
* 完成一个逆波兰计算器:输入一个逆波兰表达式(后缀表达式),使用栈(Stack)计算其结果
* 要求:支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算
*/
public class C_ReversePolishCalculator {
public static void main(String[] args) {
//定义逆波兰表达式(后缀表达式)
//(3+4)*5-6 =》 3 4 + 5 * 6 -
//4*5-8+60+8/2 =》 4 5 * 8 - 60 + 8 2 / +
//为了方便,逆波兰表达式的数字和符号之间使用空格隔开
// String suffixExpression = "30 4 + 5 * 6 -"; //29
String suffixExpression = "4 5 * 8 - 60 + 8 2 / +"; //76
List<String> list = getListString(suffixExpression);
System.out.println(list);
int res = calculate(list);
System.out.println(res);
}
//1.先将一个逆波兰表达式依次将数据和运算符放到ArrayList中
public static List<String> getListString(String suffixExpression) {
//分割suffixExpression
String[] strings = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String s : strings) {
//先将3 4 + 5 × 6 -放到ArrayList中
list.add(s);
}
return list;
}
/**
* 2.将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
* 2.1.从左至右扫描,将3和4压入堆栈;
* 2.2.遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
* 2.3.将5入栈;
* 2.4.接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
* 2.5.将6入栈;
* 2.6.最后是-运算符,计算出35-6的值,即29,由此得出最终结果
* @param list 数据和运算符
* @return 计算结果
*/
public static int calculate(List<String> list) {
//创建栈,只需要一个即可
Stack<String> stack = new Stack<>();
//遍历ArrayList
for (String s : list) {
//使用正则表达式取出数
if (s.matches("\\d+")) { //匹配多位数
//入栈
stack.push(s);
} else {
//pop出两个数,并运算,再入栈
int num1 = Integer.parseInt(stack.pop());
int num2 = Integer.parseInt(stack.pop());
//结果
int res = 0;
switch (s) {
case "+":
res = num2 + num1;
break;
case "-":
res = num2 - num1;
break;
case "*":
res = num2 * num1;
break;
case "/":
res = num2 / num1;
break;
default:
throw new RuntimeException("运算符有误");
}
//将结果入栈
stack.push(String.valueOf(res));
}
}
//最后留着栈中的数据就是运算结果
return Integer.parseInt(stack.pop());
}
}
7.中缀表达式转换为后缀表达式#
-
后缀表达式适合计算机进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,需要将中缀表达式转成后缀表达式
-
具体步骤如下:
- 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
- 从左至右扫描中缀表达式;
- 遇到操作数时,将其压s2;
- 遇到运算符时,比较其与s1栈顶运算符的优先级:
- 如果s1为空,或栈顶运算符为左括号
(
,则直接将此运算符入栈; - 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
- 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(步骤4.1)与s1中新的栈顶运算符相比较;
- 如果s1为空,或栈顶运算符为左括号
- 遇到括号时:
- 如果是左括号
(
,则直接压入s1 - 如果是右括号
)
,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号(
为止,此时将这一对括号丢弃
- 如果是左括号
- 重复(步骤3至5),直到表达式的最右边
- 将s1中剩余的运算符依次弹出并压入s2
- 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
-
举例说明:
将中缀表达式
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中剩余的运算符依次弹出并压入s2 将s2出栈:
- 5 + * 4 + 3 2 1
因此结果的逆序为:
1 2 3 + 4 * + 5 –
/**
* 将中缀表达式1+((2+3)*4)-5转换为后缀表达式1 2 3 + 4 * + 5 –
*/
public class D_InfixExpressionToSuffixExpression {
public static void main(String[] args) {
//表达式
String expression = "1+((2+3)*4)-5";
System.out.println("中缀表达式对应的对应的List:");
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println(infixExpressionList);
System.out.println("后缀表达式对应的对应的List:");
List<String> suffixExpressionList = toSuffixExpressionList(infixExpressionList);
System.out.println(suffixExpressionList);
System.out.println("计算结果:" + Calculate.calculate(suffixExpressionList)); //16
}
/**
* 将中缀表达式转成对应的List
* 因为直接对字符串进行操作不方便,因此先将1+((2+3)*4)-5转成中缀表达式对应的List[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
*
* @param expression 表达式
* @return 中缀表达式对应的List
*/
public static List<String> toInfixExpressionList(String expression) {
//定义List,存放中缀表达式对应的内容
List<String> list = new ArrayList<>();
int index = 0; //指针,用于遍历中缀表达式字符串
String digits; //对多位数的拼接
char c; //每遍历一个字符就放入到c
do {
if (((c = expression.charAt(index)) < 48) || ((c = expression.charAt(index)) > 57)) { //c是非数字,需要加入到list
list.add("" + c);
index++; //指针后移
} else { //数字需要考虑多位数
digits = ""; //将digits置空
while (index < expression.length() && (c = expression.charAt(index)) >= 48 && (c = expression.charAt(index)) <= 57) {
digits += c; //拼接字符串,形成多位数
index++; //指针后移
}
list.add(digits);
}
} while (index < expression.length());
return list;
}
/**
* 将得到的中缀表达式对应的对应的List[1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]转成后缀表达式对应的List[1, 2, 3, +, 4, *, +, 5, –]
*
* @param list 中缀表达式
* @return 后缀表达式对应的List
*/
public static List<String> toSuffixExpressionList(List<String> list) {
//1.初始化两个栈:运算符栈s1和储存中间结果的栈s2;
Stack<String> s1 = new Stack<>(); //运算符栈s1
//因为s2栈在整个转换过程中,没有pop操作,而且后面还需要逆序输出,比较麻烦,直接使用List替代,正常按顺序输出就是对应的后缀表达式对应的List
// Stack<String> s2 = new Stack<>(); //储存中间结果的栈s2
List<String> s2 = new ArrayList<>(); //储存中间结果的栈s2
//2.从左至右扫描中缀表达式;
//6.重复(步骤3至5),直到表达式的最右边
//遍历list
for (String str : list) {
//3.遇到操作数时,将其压s2;
if (str.matches("\\d+")) {
s2.add(str);
//5.遇到括号时:
} else if (str.equals("(")) {
//5.1.如果是左括号`(`,则直接压入s1
s1.push(str);
} else if (str.equals(")")) {
//5.2.如果是右括号`)`,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号`(`为止,此时将这一对括号丢弃
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop(); //将左括号`(`弹出s1栈(消除小括号)
} else {
//4.遇到运算符时,比较其与s1栈顶运算符的优先级:
//4.3.否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(步骤4.1)与s1中新的栈顶运算符相比较;
while (s1.size() != 0 && Calculate.getValue(str) <= Calculate.getValue(s1.peek())) {
s2.add(s1.pop());
}
//4.1.如果s1为空,或栈顶运算符为左括号`(`,则直接将此运算符入栈;
//4.2.否则,若优先级比栈顶运算符的高,也将运算符压入s1;
s1.push(str); //将符号入栈
}
}
//7.将s1中剩余的运算符依次弹出并压入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
//8.依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
return s2; //因为存放到List中,正常按顺序输出就是对应的后缀表达式对应的List
}
}
//定义Calculate返回运算符对应的优先级
class Calculate {
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 operator) {
int res = 0; //计算结果
switch (operator) {
case "+":
res = ADD;
break;
case "-":
res = SUB;
break;
case "*":
res = MUL;
break;
case "/":
res = DIV;
break;
default:
System.out.println("运算符有误");
break;
}
return res;
}
//1.先将一个逆波兰表达式依次将数据和运算符放到ArrayList中
public static List<String> getListString(String suffixExpression) {
//分割suffixExpression
String[] strings = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String s : strings) {
//先将3 4 + 5 × 6 -放到ArrayList中
list.add(s);
}
return list;
}
/**
* 2.将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
* 2.1.从左至右扫描,将3和4压入堆栈;
* 2.2.遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
* 2.3.将5入栈;
* 2.4.接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
* 2.5.将6入栈;
* 2.6.最后是-运算符,计算出35-6的值,即29,由此得出最终结果
* @param list 数据和运算符
* @return 计算结果
*/
public static int calculate(List<String> list) {
//创建栈,只需要一个即可
Stack<String> stack = new Stack<>();
//遍历ArrayList
for (String s : list) {
//使用正则表达式取出数
if (s.matches("\\d+")) { //匹配多位数
//入栈
stack.push(s);
} else {
//pop出两个数,并运算,再入栈
int num1 = Integer.parseInt(stack.pop());
int num2 = Integer.parseInt(stack.pop());
//结果
int res = 0;
switch (s) {
case "+":
res = num2 + num1;
break;
case "-":
res = num2 - num1;
break;
case "*":
res = num2 * num1;
break;
case "/":
res = num2 / num1;
break;
default:
throw new RuntimeException("运算符有误");
}
//将结果入栈
stack.push(String.valueOf(res));
}
}
//最后留着栈中的数据就是运算结果
return Integer.parseInt(stack.pop());
}
}
8.逆波兰计算器完整版#
- 完整版的逆波兰计算器,功能包括:
- 支持 + - * / ( )
- 多位数,支持小数
- 兼容处理,过滤任何空白字符(包括空格、制表符、换页符)
/**
* 完整版的逆波兰计算器,功能包括:
* 1. 支持 + - * / ( )
* 2. 多位数,支持小数
* 3. 兼容处理,过滤任何空白字符(包括空格、制表符、换页符)
*/
public class E_ReversePolishCalculatorCase {
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0"; //8.600000000000001
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
//匹配 + - * / ( ) 运算符
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS = "-";
static final String TIMES = "*";
static final String DIVISION = "/";
//加減 + -
static final int LEVEL_01 = 1;
//乘除 * /
static final int LEVEL_02 = 2;
//括号
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<>());
/**
* 去除所有空白符
*
* @param s
* @return
*/
public static String replaceAllBlank(String s) {
// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
return s.replaceAll("\\s+", "");
}
/**
* 判断是不是数字 int double long float
*
* @param s
* @return
*/
public static boolean isNumber(String s) {
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
/**
* 判断是不是运算符
*
* @param s
* @return
*/
public static boolean isSymbol(String s) {
return s.matches(SYMBOL);
}
/**
* 匹配运算等级
*
* @param s
* @return
*/
public static int calcLevel(String s) {
if ("+".equals(s) || "-".equals(s)) {
return LEVEL_01;
} else if ("*".equals(s) || "/".equals(s)) {
return LEVEL_02;
}
return LEVEL_HIGH;
}
/**
* 匹配
*
* @param s
* @throws Exception
*/
public static List<String> doMatch(String s) throws Exception {
if (s == null || "".equals(s.trim())) {
throw new RuntimeException("data is empty");
}
if (!isNumber(s.charAt(0) + "")) {
throw new RuntimeException("data illeagle,start not with a number");
}
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if (isSymbol(s.charAt(i) + "")) {
each = s.charAt(i) + "";
//栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
if (stack.isEmpty() || LEFT.equals(each) || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)) {
stack.push(each);
} else if (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) {
//栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) {
if (calcLevel(stack.peek()) == LEVEL_HIGH) {
break;
}
data.add(stack.pop());
}
stack.push(each);
} else if (RIGHT.equals(each)) {
// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())) {
if (LEVEL_HIGH == calcLevel(stack.peek())) {
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i; //前一个运算符的位置
} else if (i == s.length() - 1 || isSymbol(s.charAt(i + 1) + "")) {
each = start == 0 ? s.substring(start, i + 1) : s.substring(start + 1, i + 1);
if (isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出结果
*
* @param list
* @return 计算结果
*/
public static Double doCalc(List<String> list) {
Double d = 0d;
if (list == null || list.isEmpty()) {
return null;
}
if (list.size() == 1) {
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if (isSymbol(list.get(i))) {
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i - 1);
list1.set(i - 2, d1 + "");
list1.addAll(list.subList(i + 1, list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 运算
*
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1, String s2, String symbol) {
Double result;
switch (symbol) {
case ADD:
result = Double.valueOf(s1) + Double.valueOf(s2);
break;
case MINUS:
result = Double.valueOf(s1) - Double.valueOf(s2);
break;
case TIMES:
result = Double.valueOf(s1) * Double.valueOf(s2);
break;
case DIVISION:
result = Double.valueOf(s1) / Double.valueOf(s2);
break;
default:
result = null;
}
return result;
}
}
六、递归
1.递归的概念#
- 递归(Recursion)就是方法自己调用自己,每次调用时传入不同的变量。递归有助于解决复杂的问题,同时可以让代码变得简洁。
2.递归调用机制#
-
打印问题
/** * 打印问题 */ public class A_Print { public static void main(String[] args) { print(4); } public static void print(int n) { if (n > 2) { print(n - 1); //形成递归 } else { System.out.println("else=" + n); //2 } System.out.println("n=" + n); //2,3,4 } }
-
阶乘问题
/** * 阶乘问题 */ public class B_Factorial { public static void main(String[] args) { System.out.println(factorial(3)); } public static int factorial(int n) { if (n == 1) { return 1; } else { //factorial(3-1) * 3 = factorial(1) * 2 * 3 = 1 * 2 * 3 return factorial(n - 1) * n; } } }
- 递归调用规则:
- 当程序执行到一个方法时,就会开辟一个独立的空间(栈)
- 每个空间的数据(局部变量)是独立的
3.递归能解决什么样的问题#
- 各种数学问题如:八皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子等(google算法编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
- 用栈解决的问题-->递归代码比较简洁
4.递归需要遵守的重要原则#
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响,比如打印问题的变量
n
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据(迷宫问题:多个栈共享同一个数组)
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现
StackOverflowError
,死归了 - 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
5.迷宫问题(回溯算法)#
- 说明:
- 小球得到的路径,和设置的找路策略有关即:找路的上下左右的顺序相关
- 在得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
- 思考:如何求出最短路径?
/**
* 迷宫问题
* 1. 小球得到的路径,和设置的找路策略有关即:找路的上下左右的顺序相关
* 2. 在得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
*/
public class C_Labyrinth {
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;
// map[1][2] = 1;
// map[2][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();
}
//使用递归回溯给小球找路(起始位置 map[1][1],起始终点 map[6][5])
// findWay(map, 1, 1); //下->右->上->左
findWay2(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();
}
}
/**
* 使用递归回溯给小球找路,需要确定一个策略(方法):下->右->上->左,如果该点走不通,再回溯
*
* @param map 地图:0 可以走还没有走;1 墙;2 路已经找到;3 走不通
* @param i 起始位置纵坐标
* @param j 起始位置横坐标
* @return true 找到终点,false 未找到
*/
public static boolean findWay(int[][] map, int i, int j) {
if (map[6][5] == 2) { //路已经找到
return true;
} else {
if (map[i][j] == 0){ //0 可以走还没有走
//需要确定一个策略(方法):下->右->上->左
map[i][j] = 2; //假定该点可以走通
if (findWay(map, i + 1, j)) { //向下走
return true;
} else if (findWay(map, i, j + 1)) { //向右走
return true;
} else if (findWay(map, i -1 , j)) { //向上走
return true;
} else if (findWay(map, i, j - 1)) { //向左走
return true;
} else { //走不通,回溯
map[i][j] = 3;
return false;
}
} else { //1 墙;2 路已经找到;3 走不通
return false;
}
}
}
/**
* 使用递归回溯给小球找路,需要确定一个策略(方法):上->右->下->左,如果该点走不通,再回溯
*
* @param map 地图:0 可以走还没有走;1 墙;2 路已经找到;3 走不通
* @param i 起始位置纵坐标
* @param j 起始位置横坐标
* @return true 找到终点,false 未找到
*/
public static boolean findWay2(int[][] map, int i, int j) {
if (map[6][5] == 2) { //路已经找到
return true;
} else {
if (map[i][j] == 0){ //0 可以走还没有走
//需要确定一个策略(方法):上->右->下->左
map[i][j] = 2; //假定该点可以走通
if (findWay2(map, i -1 , j)) { //向上走
return true;
} else if (findWay2(map, i, j + 1)) { //向右走
return true;
} else if (findWay2(map, i + 1, j)) { //向下走
return true;
} else if (findWay2(map, i, j - 1)) { //向左走
return true;
} else { //走不通,回溯
map[i][j] = 3;
return false;
}
} else { //1 墙;2 路已经找到;3 走不通
return false;
}
}
}
}
6.八皇后问题(回溯算法)#
-
八皇后问题介绍
- 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)
- 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题
-
思路分析:
-
第一个皇后先放第一行第一列
-
第二个皇后放在第二行第一列、然后判断是否OK, 如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适位置
-
继续第三个皇后,还是第一列、第二列……直到第八个皇后也能放在一个不冲突的位置,算是找到了一个正确解
-
当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到
-
然后回头继续第一个皇后放第二列,后面继续循环执行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列
-
/**
* 八皇后问题
* 在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)
*/
public class D_EightQueens {
private int max = 8; //共有多少个皇后
private int[] arr = new int[max]; //数组arr,保存皇后放置位置的结果(index:行;val:列),arr[8] = {0, 4, 7, 5, 2, 6, 1, 3}
private static int count = 0; //摆法
private static int conflictCount = 0; //冲突次数
public static void main(String[] args) {
D_EightQueens queens = new D_EightQueens();
queens.check(0);
System.out.printf("有%d种摆法\n", count); //92
System.out.printf("有%d次冲突\n", conflictCount); //15720
}
/**
* 放置第n个皇后
*
* @param n 从第n个皇后开始
*/
private void check(int n) {
if (n == max) { //n=8,第9个,此时8个皇后已经放好
print();
return;
}
//依次放入皇后,并判断是否冲突
for (int i = 0; i < max; i++) {
//先把当前皇后n放到该行的第1列
arr[n] = i;
//判断当放置第n个皇后到i列时,是否冲突
if (conflict(n)) { //不冲突
//接着放n+1个皇后,开始递归,会有回溯
check(n + 1);
}
//如果冲突,就继续执行arr[n] = i,即将第n个皇后放置在本行的后移的一个位置
}
}
/**
* 查看放置的第n个皇后是否和前面已经摆放的皇后冲突
*
* @param n 第n个皇后
* @return 是否和前面已经摆放的皇后冲突
*/
private boolean conflict(int n) {
conflictCount++;
for (int i = 0; i < n; i++) {
//没有必要判断是否在同一行,n每次都在递增
//arr[i] == arr[n]:第n个皇后是否和前面的n-1个皇后在同一列
//Math.abs(n - i) == Math.abs(arr[n] - arr[i]):第n个皇后是否和前面的i个皇后在同一斜线(行的差值的绝对值与列的差值的绝对值进行对比,跟数组的设计有关系)
if (arr[i] == arr[n] || Math.abs(n - i) == Math.abs(arr[n] - arr[i])) { //同一列或者同一斜线
return false;
}
}
return true;
}
//打印皇后摆放的位置
private void print() {
count++;
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
七、排序算法
1.排序算法的介绍#
-
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
-
常见的排序算法的分类:
- 内部排序:将需要处理的所有数据都加载到内部存储器(内存)中进行排序
- 插入排序
- 直接插入排序
- 希尔排序
- 选择排序
- 简单选择排序
- 堆排序
- 交换排序
- 冒泡排序
- 快速排序
- 归并排序
- 基数排序(升级版的桶排序)
- 插入排序
- 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储(文件、磁盘等)进行排序
- 内部排序:将需要处理的所有数据都加载到内部存储器(内存)中进行排序
2.算法的时间复杂度#
-
度量一个程序(算法)执行时间的两种方法:
- 事后统计的方法:这种方法可行,但是有两个问题:
- 一是要想对设计的算法的运行性能进行评测,需要实际运行该程序(需要运行程序)
- 二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,这种方式,要在同一台计算机的相同状态下运行,才能比较哪个算法速度更快(需要统一计算机环境)
- 事前估算的方法:通过分析某个算法的时间复杂度来判断哪个算法更优
- 事后统计的方法:这种方法可行,但是有两个问题:
-
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
-
举例说明
-
基本案例:比如计算1-100所有数字之和,我们设计两种算法:
int total = 0; int end = 100; //使用for循环计算 for (int i = 1; i <= end; i++) { total += i; //T(n)=n+1(最后需要做1次判断) } //直接计算 total = (1 + end) * end / 2; //T(n)=1
-
忽略常数项
$T(n)=2n+20$ $T(n)=2n$ $T(n)=3n+10$ $T(n)=3n$ 1 22 2 13 3 2 24 4 16 6 5 30 10 25 15 8 36 16 34 24 15 50 30 55 45 30 80 60 100 90 100 220 200 310 300 300 620 600 910 900 结论:(统计时间频度的时候,常数项是可以忽略的)
- $2n+20$ 和 $2n$ 随着n变大,执行曲线无限接近,$20$可以忽略
- $3n+10$ 和 $3n$ 随着n变大,执行曲线无限接近,$10$可以忽略
-
忽略低次项
T(n)=2n^2+3n+10 T(n)=2n^2 T(n)=n^2+5n+20 T(n)=n^2 1 15 2 26 1 2 24 8 34 4 5 75 50 70 25 8 162 128 124 64 15 505 450 320 225 30 1900 1800 1070 900 100 20310 20000 10520 10000 结论:(统计时间频度的时候,低次项是可以忽略的)
- $2n^2+3n+10$ 和 $2n^2$ 随着n变大,执行曲线无限接近,可以忽略 $3n+10$
- $n^2+5n+20$ 和 $n^2$ 随着n变大,执行曲线无限接近,可以忽略 $5n+20$
-
忽略系数
$T(n)=3n^2+2n$ $T(n)=5n^2+7n$ $T(n)=n^3+5n$ $T(n)=6n^3+4n$ 1 5 12 6 10 2 16 34 18 56 5 85 160 150 770 8 208 376 552 3104 15 705 1230 3450 20310 30 2760 4710 27150 162120 100 30200 50700 1000500 6000400 结论:(统计时间频度的时候,高次项的系数是可以忽略的)
- 随着n值变大,$5n^2+7n$ 和 $3n^2+2n$ ,执行曲线重合,说明这种情况下,5和3可以忽略
- 而$n^3+5n$ 和 $6n^3+4n$ ,执行曲线分离(系数会逐渐形成一个常量 1:6 ),说明多少次方是关键
-
-
-
时间复杂度
- 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示(时间频度),若有某个辅助函数f(n),使得当n趋近于无穷大时,$T(n) / f(n)$ 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作$T(n)=O(f(n))$,称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度
- T(n) 不同,但时间复杂度可能相同。 如:$T(n)=n²+7n+6$ 与 $T(n)=3n²+2n+2$ 它们的T(n) 不同,但时间复杂度相同,都为O(n²)
- 计算时间复杂度的方法:
- 用常数1代替运行时间中的所有加法常数:
T(n)=n²+7n+6 => T(n)=n²+7n+1
- 修改后的运行次数函数中,只保留最高阶项:
T(n)=n²+7n+1 => T(n)=n²
- 去除最高阶项的系数:
T(n)=n² => O(n²)
- 用常数1代替运行时间中的所有加法常数:
-
常见的时间复杂度
-
说明:
-
常数阶O(1)
-
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
int i = 1; int j = 2; ++i; j++; int k = i + j;
上述代码在执行的时候,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
-
-
对数阶O(log2n)
-
如果N=ax(a>0,a≠1),即a的x次方等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作x=logaN。其中,a叫做对数的底数,N叫做真数,x叫做以a为底N的对数
int i = 1; while (i < n) { i = i * 2; }
说明:在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,
i = i * 3
,则是 O(log3n) .
-
-
线性阶O(n)
for (int i = 0; i < n; i++) { j = i; j++; }
说明:在for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度
-
线性对数阶O(nlog2n)
for (int j = 0; j < n; j++) { i = 1; while (i < n) { i = i * 2; } }
说明:线性对数阶O(nlog2N) 其实非常容易理解,将时间复杂度为O(log2n)的代码循环N遍的话,那么它的时间复杂度就是 n * O(log2N),也就是了O(nlog2N)
-
平方阶O(n2)
for (int k = 0; k < n; k++) { for (int i = 0; i < n; i++) { j = i; j++; } }
说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n * n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m * n)
-
立方阶O(n3)、k次方阶O(nk)、指数阶O(2n)
- 说明:参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似
-
-
平均时间复杂度和最坏时间复杂度
3.算法的空间复杂度#
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
- 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis,memcache)和算法(基数排序)本质就是用空间换时间
4.冒泡排序(Bubble Sort)#
1.基本介绍#
- 冒泡排序的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒
- 优化:因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换,从而减少不必要的比较
2.图解分析#
-
思路分析:
- 比较数组中,两个相邻的元素,如果第一个数比第二个数大,我们就交换它们的位置(如果相邻的元素逆序就较换)
- 每一次比较,都会产生出一个最大或最小的数字
- 下一轮则可以少一次排序
- 依次循环,直到结束
-
规则小结:
- 一共进行
arr.length - 1
次大的循环 - 每一次排序的次数在逐渐的减少
- 如果发现在某次排序中,没有发生一次交换,可以提前结束冒泡排序(优化)
- 一共进行
3.应用实例#
- 将五个无序的数:3, 9, -1, 10, -2使用冒泡排序将其排成一个从小到大的有序数列
/**
* 将五个无序的数:3, 9, -1, 10, -2使用冒泡排序将其排成一个从小到大的有序数列
* 冒泡排序的时间复杂度:O(n^2)
*/
public class A_BubbleSort {
public static void main(String[] args) {
// int[] arr = {3, 9, -1, 10, -2};
// System.out.println("冒泡排序");
// bubbleSort(arr);
// System.out.println("简化冒泡排序");
// bubbleSortSimplify(arr);
// System.out.println("优化冒泡排序");
// bubbleSortOptimization(arr);
System.out.println("冒泡排序的时间复杂度:O(n^2)");
//创建80000个随机数的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
bubbleSortOptimization(arr);
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData)); //8312
}
//冒泡排序
public static void bubbleSort(int[] arr) {
//第一次排序,就是将最大的数排在最后
int temp = 0; //临时变量
for (int j = 0; j < arr.length - 1; j++) {
//如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第一次排序");
System.out.println(Arrays.toString(arr));
//第二次排序,就是将最二大的数排在倒数第二位(刚才排好的最大的数不进行比较)
for (int j = 0; j < arr.length - 2; j++) {
//如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第二次排序");
System.out.println(Arrays.toString(arr));
//第三次排序,就是将第三大的数排在倒数第三位(刚才排好的数不进行比较)
for (int j = 0; j < arr.length - 3; j++) {
//如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第三次排序");
System.out.println(Arrays.toString(arr));
//第四次排序,就是将第四大的数排在倒数第四位(刚才排好的数不进行比较)
for (int j = 0; j < arr.length - 4; j++) {
//如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第四次排序");
System.out.println(Arrays.toString(arr));
}
//简化冒泡排序
public static void bubbleSortSimplify(int[] arr) {
int temp = 0; //临时变量
//外层冒泡轮数:1. 一共进行`arr.length - 1`次大的循环
for (int i = 0; i < arr.length - 1; i++) {
//里层依次比较:2. 每一次排序的次数在逐渐的减少
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));
}
}
//优化冒泡排序:
public static void bubbleSortOptimization(int[] arr) {
int temp = 0; //临时变量
boolean flag = false; //标识变量,表示是否进行过交换
//外层冒泡轮数:1. 一共进行`arr.length - 1`次大的循环
for (int i = 0; i < arr.length - 1; i++) {
//里层依次比较:2. 每一次排序的次数在逐渐的减少
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;
}
}
// System.out.println("第" + (i + 1) + "次排序");
// System.out.println(Arrays.toString(arr));
//3. 如果发现在某次排序中,没有发生一次交换,可以提前结束冒泡排序
if (flag == false) { //在一次排序中,没有发生过交换
break;
} else {
flag = false; //重置flag,进行下次判断
}
}
}
}
5.选择排序(Select Sort)#
1.基本介绍#
-
选择排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。选择排序也是一种简单的排序方法。
-
选择排序的基本思想是:
- 第一次从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次,得到一个按排序码从小到大排列的有序序列
2.图解分析#
- 思路分析:
- 一共有
arr.length - 1
次排序 - 每一次排序又是一个循环,循环的规则:
- 先假定当前数是最小值
- 然后和后面的每个数进行比较,如果发现更小值,就重新确定最小值,并得到下标
- 当遍历到数组的最后时,就得到本次最小值和下标
- 将最小值放在
arr[i]
,即交换
- 一共有
3.应用实例#
- 将四个无序的数:101, 34, 119, 1使用选择排序从低到高进行排序
/**
* 将四个无序的数:101, 34, 119, 1使用选择排序从低到高进行排序
* 选择排序的时间复杂度:O(n^2)
*
* 算法思想:先简单,再复杂;把一个复杂的算法拆分成简单的问题(逐步解决)
*/
public class B_SelectSort {
public static void main(String[] args) {
// int[] arr = {101, 34, 119, 1};
// System.out.println("选择排序");
// selectSort(arr);
// System.out.println("简化选择排序");
// selectSortSimplify(arr);
// System.out.println("优化选择排序");
// selectSortOptimization(arr);
System.out.println("选择排序的时间复杂度:O(n^2)");
//创建80000个随机数的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
selectSortOptimization(arr);
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData)); //1452
}
//选择排序
public static void selectSort(int[] arr) {
//第一次从arr[0] ~ arr[n-1]中选取最小值,与arr[0]交换
int minIndex = 0; //最小值索引
int min = arr[0]; //最小值
for (int j = 0 + 1; j < arr.length; j++) {
if (min > arr[j]) { //说明假定的最小值,并不是最小
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//将最小值放在arr[0],即交换
arr[minIndex] = arr[0];
arr[0] = min;
System.out.println("第一次排序");
System.out.println(Arrays.toString(arr));
//第二次从arr[1] ~ arr[n-1]中选取最小值,与arr[1]交换
minIndex = 1; //最小值索引
min = arr[1]; //最小值
for (int j = 1 + 1; j < arr.length; j++) {
if (min > arr[j]) { //说明假定的最小值,并不是最小
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//将最小值放在arr[1],即交换
arr[minIndex] = arr[1];
arr[1] = min;
System.out.println("第二次排序");
System.out.println(Arrays.toString(arr));
//第三次从arr[2] ~ arr[n-1]中选取最小值,与arr[2]交换
minIndex = 2; //最小值索引
min = arr[2]; //最小值
for (int j = 2 + 1; j < arr.length; j++) {
if (min > arr[j]) { //说明假定的最小值,并不是最小
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//将最小值放在arr[2],即交换
arr[minIndex] = arr[2];
arr[2] = min;
System.out.println("第三次排序");
System.out.println(Arrays.toString(arr));
}
//简化选择排序
public static void selectSortSimplify(int[] arr) {
//1.一共有`arr.length - 1`次排序
for (int i = 0; i < arr.length - 1; i++) {
//2.1.先假定当前数是最小值
int minIndex = i; //最小值索引
int min = arr[i]; //最小值
//2.每一次排序又是一个循环,循环的规则:
for (int j = i + 1; j < arr.length; j++) {
//2.2.然后和后面的每个数进行比较,如果发现更小值,就重新确定最小值,并得到下标
if (min > arr[j]) { //说明假定的最小值,并不是最小
//2.3.当遍历到数组的最后时,就得到本次最小值和下标
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//2.4.将最小值放在arr[i],即交换
arr[minIndex] = arr[i];
arr[i] = min;
System.out.println("第" + (i + 1) + "次排序");
System.out.println(Arrays.toString(arr));
}
}
//优化选择排序
public static void selectSortOptimization(int[] arr) {
//1.一共有`arr.length - 1`次排序
for (int i = 0; i < arr.length - 1; i++) {
//2.1.先假定当前数是最小值
int minIndex = i; //最小值索引
int min = arr[i]; //最小值
//2.每一次排序又是一个循环,循环的规则:
for (int j = i + 1; j < arr.length; j++) {
//2.2.然后和后面的每个数进行比较,如果发现更小值,就重新确定最小值,并得到下标
if (min > arr[j]) { //说明假定的最小值,并不是最小
//2.3.当遍历到数组的最后时,就得到本次最小值和下标
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//2.4.将最小值放在arr[i],即交换
if (minIndex != i) { //判断是否需要交换
arr[minIndex] = arr[i];
arr[i] = min;
}
// System.out.println("第" + (i + 1) + "次排序");
// System.out.println(Arrays.toString(arr));
}
}
}
6.插入排序(Insertion Sort)#
1.基本介绍#
-
插入排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
-
插入排序的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表
2.图解分析#
3.应用实例#
- 将四个无序的数:101, 34, 119, 1使用插入排序从低到高进行排序
/**
* 将四个无序的数:101, 34, 119, 1使用插入排序从低到高进行排序
* 插入排序的时间复杂度:O(n^2)
*/
public class C_InsertionSort {
public static void main(String[] args) {
// int[] arr = {101, 34, 119, 1};
// System.out.println("插入排序");
// insertSort(arr);
// System.out.println("简化插入排序");
// insertSortSimplify(arr);
// System.out.println("优化插入排序");
// insertSortOptimization(arr);
System.out.println("插入排序的时间复杂度:O(n^2)");
//创建80000个随机数的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
insertSortOptimization(arr);
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData)); //252
}
//插入排序
public static void insertSort(int[] arr) {
//第一次排序:{101, 34, 119, 1} =》{34, 101, 119, 1}
int insertValue = arr[1]; //临时变量,用来存储待插入的数
int insertIndex = 1 - 1; //需要比较的数的索引,即arr[1]的前面这个数的下标
//找到待插入的数的位置
//1.insertIndex >= 0:防止给待插入的数找插入位置下标越界
//2.insertValue < arr[insertIndex]:待插入的数还没有找到适当的插入位置
//3.需要将arr[insertIndex]后移
while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex]; //将arr[insertIndex]后移
insertIndex--; //跟更前面的数比较
}
//当退出while循环时,说明找到插入的位置
arr[insertIndex + 1] = insertValue;
System.out.println("第一次排序");
System.out.println(Arrays.toString(arr));
//第二次排序:{101, 34, 119, 1} =》{34, 101, 119, 1}
insertValue = arr[2]; //临时变量,用来存储待插入的数
insertIndex = 2 - 1; //需要比较的数的索引,即arr[2]的前面这个数的下标
//找到待插入的数的位置
//1.insertIndex >= 0:防止给待插入的数找插入位置下标越界
//2.insertValue < arr[insertIndex]:待插入的数还没有找到适当的插入位置
//3.需要将arr[insertIndex]后移
while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex]; //将arr[insertIndex]后移
insertIndex--; //跟更前面的数比较
}
//当退出while循环时,说明找到插入的位置
arr[insertIndex + 1] = insertValue;
System.out.println("第二次排序");
System.out.println(Arrays.toString(arr));
//第三次排序:{101, 34, 119, 1} =》{1, 34, 101, 119}
insertValue = arr[3]; //临时变量,用来存储待插入的数
insertIndex = 3 - 1; //需要比较的数的索引,即arr[3]的前面这个数的下标
//找到待插入的数的位置
//1.insertIndex >= 0:防止给待插入的数找插入位置下标越界
//2.insertValue < arr[insertIndex]:待插入的数还没有找到适当的插入位置
//3.需要将arr[insertIndex]后移
while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex]; //将arr[insertIndex]后移
insertIndex--; //跟更前面的数比较
}
//当退出while循环时,说明找到插入的位置
arr[insertIndex + 1] = insertValue;
System.out.println("第三次排序");
System.out.println(Arrays.toString(arr));
}
//简化插入排序
public static void insertSortSimplify(int[] arr) {
int insertValue = 0; //临时变量,用来存储待插入的数
int insertIndex = 0; //需要比较的数的索引,即arr[1]的前面这个数的下标
for (int i = 1; i < arr.length; i++) {
insertValue = arr[i]; //临时变量,用来存储待插入的数
insertIndex = i - 1; //需要比较的数的索引,即arr[1]的前面这个数的下标
//找到待插入的数的位置
//1.insertIndex >= 0:防止给待插入的数找插入位置下标越界
//2.insertValue < arr[insertIndex]:待插入的数还没有找到适当的插入位置
//3.需要将arr[insertIndex]后移
while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex]; //将arr[insertIndex]后移
insertIndex--; //跟更前面的数比较
}
//当退出while循环时,说明找到插入的位置
arr[insertIndex + 1] = insertValue;
System.out.println("第" + i + "次排序");
System.out.println(Arrays.toString(arr));
}
}
//优化插入排序
public static void insertSortOptimization(int[] arr) {
int insertValue = 0; //临时变量,用来存储待插入的数
int insertIndex = 0; //需要比较的数的索引,即arr[1]的前面这个数的下标
for (int i = 1; i < arr.length; i++) {
insertValue = arr[i]; //临时变量,用来存储待插入的数
insertIndex = i - 1; //需要比较的数的索引,即arr[1]的前面这个数的下标
//找到待插入的数的位置
//1.insertIndex >= 0:防止给待插入的数找插入位置下标越界
//2.insertValue < arr[insertIndex]:待插入的数还没有找到适当的插入位置
//3.需要将arr[insertIndex]后移
while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex]; //将arr[insertIndex]后移
insertIndex--; //跟更前面的数比较
}
//当退出while循环时,说明找到插入的位置
if (insertIndex + 1 != i) { //判断是否需要赋值
arr[insertIndex + 1] = insertValue;
}
// System.out.println("第" + i + "次排序");
// System.out.println(Arrays.toString(arr));
}
}
}
7.希尔排序(Shell Sort)#
1.基本介绍#
-
简单插入排序存在的问题:数组
arr = {2, 3, 4, 5, 6, 1}
这时需要插入的数 1(最小),这样的过程是:- 移动6:
{2, 3, 4, 5, 6, 6}
- 移动5:
{2, 3, 4, 5, 5, 6}
- 移动4:
{2, 3, 4, 4, 5, 6}
- 移动3:
{2, 3, 3, 4, 5, 6}
- 移动2:
{2, 2, 3, 4, 5, 6}
- 插入1:
{1, 2, 3, 4, 5, 6}
结论:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响
- 移动6:
-
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
-
希尔排序的基本思想是:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
2.图解分析#
- 思路分析:
- 定义一个数组
arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}
- 初始增量
gap = arr.length / 2 = 5
,意味着整个数组被分为5组,[8, 3][9, 5][1, 4][7, 6][2, 0]
- 对这5组分别进行直接插入排序,小元素都被调到前面变成
{3, 5, 1, 6, 0, 8, 9, 4, 7, 2}
,然后缩小增量gap = 5 / 2 = 2
,数组被分为2组,[3, 1, 0, 9, 7][5, 6, 8, 4, 2]
- 对以上2组再分别进行直接插入排序,此时整个数组的有序程度更进一步,再缩小增量
gap = 2 / 2 = 1
,此时整个数组为1组[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
- 此时,仅仅需要对以上数列简单微调,无需大量移动操作即可完成整个数组的排序
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
- 定义一个数组
3.应用实例#
- 将十个无序的数:8, 9, 1, 7, 2, 3, 5, 4, 6, 0使用希尔排序从低到高进行排序
- 希尔排序时,对有序序列在插入时采用交换法,速度相对较慢
- 希尔排序时,对有序序列在插入时采用移动法,速度相对较快
/**
* 将十个无序的数:8, 9, 1, 7, 2, 3, 5, 4, 6, 0使用希尔排序从低到高进行排序
* 1. 希尔排序时,对有序序列在插入时采用交换法,速度相对较慢
* 2. 希尔排序时,对有序序列在插入时采用移动法,速度相对较快
* 希尔排序的时间复杂度:O(n^3)
*/
public class D_ShellSort {
public static void main(String[] args) {
//1.定义一个数组`arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}`
// int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
// System.out.println("希尔排序——交换法");
// shellSortExchange(arr);
// System.out.println("简化希尔排序——交换法");
// shellSortExchangeSimplify(arr);
// System.out.println("简化希尔排序——移动法");
// shellSortMoveSimplify(arr);
System.out.println("希尔排序的时间复杂度:O(n^3)");
//创建80000个随机数的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
// shellSortExchangeSimplify(arr); //交换法:3115
shellSortMoveSimplify(arr); //移动法:16
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData));
}
//希尔排序——交换法,速度相对较慢
public static void shellSortExchange(int[] arr) {
//第一次排序:
//2.初始增量`gap = arr.length / 2 = 5`,意味着整个数组被分为5组,`[8, 3][9, 5][1, 4][7, 6][2, 0]`
for (int i = 5; i < arr.length; i++) {
//遍历各组中所有的元素(共5组,每组2个元素),步长为5
for (int j = i - 5; j >= 0; j -= 5) {
//当前元素大于加上步长后的元素,交换
if (arr[j] > arr[j + 5]) {
int temp = arr[j];
arr[j] = arr[j + 5];
arr[j + 5] = temp;
}
}
}
System.out.println("第一次排序");
System.out.println(Arrays.toString(arr));
//第二次排序:
//3.对这5组分别进行直接插入排序,小元素都被调到前面变成`{3, 5, 1, 6, 0, 8, 9, 4, 7, 2}`,然后缩小增量`gap = 5 / 2 = 2`,数组被分为2组,`[3, 1, 0, 9, 7][5, 6, 8, 4, 2]`
for (int i = 2; i < arr.length; i++) {
//遍历各组中所有的元素(共2组,每组5个元素),步长为2
for (int j = i - 2; j >= 0; j -= 2) {
//当前元素大于加上步长后的元素,交换
if (arr[j] > arr[j + 2]) {
int temp = arr[j];
arr[j] = arr[j + 2];
arr[j + 2] = temp;
}
}
}
System.out.println("第二次排序");
System.out.println(Arrays.toString(arr));
//第三次排序:
//4.对以上2组再分别进行直接插入排序,此时整个数组的有序程度更进一步,再缩小增量`gap = 2 / 2 = 1`,此时整个数组为1组`[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]`
//5.此时,仅仅需要对以上数列简单微调,无需大量移动操作即可完成整个数组的排序`{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}`
for (int i = 1; i < arr.length; i++) {
//遍历各组中所有的元素(共1组,每组10个元素),步长为1
for (int j = i - 1; j >= 0; j -= 1) {
//当前元素大于加上步长后的元素,交换
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println("第三次排序");
System.out.println(Arrays.toString(arr));
}
//简化希尔排序——交换法,速度相对较慢
public static void shellSortExchangeSimplify(int[] arr) {
int count = 0;
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
//遍历各组中所有的元素(共gap组,每组arr.length / gap个元素),步长为gap
for (int j = i - gap; j >= 0; j -= gap) {
//当前元素大于加上步长后的元素,交换
if (arr[j] > arr[j + gap]) {
int temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
// System.out.println("第" + (++count) + "次排序");
// System.out.println(Arrays.toString(arr));
}
}
//简化希尔排序——移动法,速度相对较快
public static void shellSortMoveSimplify(int[] arr) {
int count = 0;
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i; //需要比较的数的索引,即arr[1]的前面这个数的下标
int temp = arr[j]; //临时变量,用来存储待插入的数
if (arr[j] < arr[j - gap]) {
//找到待插入的数的位置
//1.j - gap >= 0:防止给待插入的数找插入位置下标越界
//2.temp < arr[j - gap]:待插入的数还没有找到适当的插入位置
//3.需要将arr[j]后移
while (j - gap >= 0 && temp < arr[j - gap]) {
arr[j] = arr[j - gap]; //移动
j -= gap; //跟更前面的数比较
}
//当退出while循环时,说明找到插入的位置
if (arr[j] + 1 != i) { //判断是否需要赋值
arr[j] = temp;
}
}
}
// System.out.println("第" + (++count) + "次排序");
// System.out.println(Arrays.toString(arr));
}
}
}
8.快速排序(Quick Sort)#
1.基本介绍#
- 快速排序是对冒泡排序的一种改进。
- 快速排序的基本思想是:通过一次排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归(空间换时间)进行,以此达到整个数据变成有序序列
2.图解分析#
- 思路分析:
- 以数组中间位置的值为基准,分解数组
- 左边都是比基准小的数,右边都是比基准大的数,此时基准左右两边的两个数组不一定是有序的
- 按上面的思想继续处理,当只有一个数的时候就不需要做处理了
3.应用实例#
- 将六个无序的数:-9, 78, 0, 23, -567, 70使用快速排序从低到高进行排序
- 如果取消左右递归,只是把比基准小的数放在左边,比基准大的数放在右边,两边都不是有序的:
-9, -567, 0, 23, 78, 70
- 如果取消右递归,只是把比基准大的数放在右边,左边有序,右边无序:
-567, -9, 0, 23, 78, 70
- 如果取消左递归,只是把比基准小的数放在左边,左边无序,右边有序:
-9, -567, 0, 23, 70, 78
- 如果取消左右递归,只是把比基准小的数放在左边,比基准大的数放在右边,两边都不是有序的:
/**
* 将六个无序的数:-9, 78, 0, 23, -567, 70使用快速排序从低到高进行排序
* 1. 如果取消左右递归,只是把比基准小的数放在左边,比基准大的数放在右边,两边都不是有序的:`-9, -567, 0, 23, 78, 70`
* 2. 如果取消右递归,只是把比基准大的数放在右边,左边有序,右边无序:`-567, -9, 0, 23, 78, 70`
* 3. 如果取消左递归,只是把比基准小的数放在左边,左边无序,右边有序:`-9, -567, 0, 23, 70, 78`
*/
public class E_QuickSort {
public static void main(String[] args) {
// int[] arr = {-9, 78, 0, 23, -567, 70};
//
// quickSort(arr, 0, arr.length - 1);
// System.out.println(Arrays.toString(arr));
//创建80000个随机数的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
quickSort(arr, 0, arr.length - 1);
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData)); //13
}
/**
* 快速排序
*
* @param arr 数组
* @param left 最左边的索引
* @param right 最右边的索引
*/
public static void quickSort(int[] arr, int left, int right) {
int l = left; //左下标
int r = right; //右下标
int median = arr[(left + right) / 2]; //中间值(基准)
int temp = 0; //临时变量,交换使用
//为了让比中间值小的放到左边,比中间值大的放到右边
while (l < r) {
//在中间值的左边,找到大于等于中间值,才退出
while (arr[l] < median) {
l++; //后移
}
//在中间值的右边,找到小于等于中间值,才退出
while (arr[r] > median) {
r--; //前移
}
//中间值左边全部都是小于等于中间值的值,右边全部都是大于等于中间值的值
if (l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//为了防止已经交换的两个数都等于中间值,之后出现死循环,两个数一直交换
if (arr[l] == median) {
r--; //前移
}
if (arr[r] == median) {
l++; //后移
}
}
//防止栈溢出
if (l == r) {
l++; //后移
r--; //前移
}
//向左递归
if (left < r) {
quickSort(arr, left, r);
}
//向右递归
if (right > l) {
quickSort(arr, l, right);
}
}
}
9.归并排序(Merge Sort)#
1.基本介绍#
- 归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案“修补”在一起,即分而治之)
2.图解分析#
- 说明:这种结构很像一棵完全二叉树,可以采用递归去实现(也可采用迭代的方式去实现)
- 分阶段就是递归拆分子序列的过程(主要是为治阶段提供条件)
- 治阶段需要将两个已经有序的子序列合并成一个有序序列,比如最后一次合并,要将
{4, 5, 7, 8}
和{1, 2, 3, 6}
两个已经有序的子序列,合并为最终序列{1, 2, 3, 4, 5, 6, 7, 8}
- 先把左右两边(有序)的数据按规则填充到
temp
,直到左右两边的有序序列,有一边处理完毕为止 - 把有剩余数据的一边的数据依次全部填充到
temp
- 将
temp
中的内容全部拷到原数组arr
中,排序完成
- 先把左右两边(有序)的数据按规则填充到
3.应用实例#
- 将九个无序的数:8, 4, 5, 7, 1, 3, 6, 2使用归并排序从低到高进行排序
/**
* 将九个无序的数:8, 4, 5, 7, 1, 3, 6, 2使用归并排序从低到高进行排序
*/
public class F_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));
//创建80000个随机数的数组
int[] arr = new int[80000];
int[] temp = new int[arr.length]; //临时数组
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
mergeSort(arr, 0, arr.length - 1, temp);
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData)); //16
}
/**
* 归并排序(分+合)
*
* @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 mid 中间索引(右边有序序列前一个位置)
* @param right 最右边的索引
* @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) {
//如果左边有序序列的当前元素小于等于右边有序序列的当前元素
if (arr[i] <= arr[j]) {
temp[t] = arr[i]; //将左边的当前元素拷贝到temp
t++; //临时数组后移
i++; //左边有序序列后移
} else {
temp[t] = arr[j]; //将右边的当前元素拷贝到temp
t++; //临时数组后移
j++; //右边有序序列后移
}
}
//2.把有剩余数据的一边的数据依次全部填充到`temp`
while (i <= mid) { //左边的有序序列还有剩余的元素
temp[t] = arr[i]; //将左边剩余元素填充进temp中
t++; //临时数组后移
i++; //左边有序序列后移
}
while (j <= right) { //右边的有序序列还有剩余的元素
temp[t] = arr[j]; //将右边剩余元素填充进temp中
t++; //临时数组后移
j++; //右边有序序列后移
}
//3.将`temp`中的内容全部拷到原数组`arr`中,排序完成
//注意:并不是每次都拷贝所有的数据
t = 0; //temp数组的索引
int tempLeft = left; //arr数组的索引
/*
第1次合并:tempLeft:0 right:1
第2次合并:tempLeft:2 right:3
第3次合并:tempLeft:0 right:3
第4次合并:tempLeft:4 right:5
第5次合并:tempLeft:6 right:7
第6次合并:tempLeft:4 right:7
第7次合并:tempLeft:0 right:7
*/
// System.out.println("tempLeft:" + tempLeft + " right:" + right);
while (tempLeft <= right) {
arr[tempLeft] = temp[t]; //每次将temp[t]位置的数,逐个赋值给arr[templeft]
t++;
tempLeft++;
}
}
}
10.基数排序(Radix Sort)(升级版的桶排序)#
1.基本介绍#
- 基数排序属于“分配式排序”(Distribution Sort),又称“桶子法”(Bucket Sort 或 Bin Sort),顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序是属于稳定性的排序,基数排序是效率高的稳定性排序法
- 基数排序是桶排序的扩展
- 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
- 基数排序的基本思想是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
2.图解分析#
-
思路分析:
- 第一次排序:(针对每个元素的个位进行排序处理)
- 将每个元素的个位数取出,然后看这个数应该放在哪个对应的“桶”(一维数组)
- 按照“桶”的顺序(一维数组的下标)依次取出数据,放入原数组
- 第二次排序:(针对每个元素的十位进行排序处理)
- 将每个元素的十位数取出,然后看这个数应该放在哪个对应的“桶”(一维数组)
- 按照“桶”的顺序(一维数组的下标)依次取出数据,放入原数组
- 第三次排序:(针对每个元素的百位进行排序处理)
- 将每个元素的百位数取出,然后看这个数应该放在哪个对应的“桶”(一维数组)
- 按照“桶”的顺序(一维数组的下标)依次取出数据,放入原数组
- 注:数字取完了,指针重置为0,添加一个数指针加1,取出一个数指针减一
- 第一次排序:(针对每个元素的个位进行排序处理)
3.应用实例#
- 将六个无序的数:53, 3, 542, 748, 14, 214使用基数排序从低到高进行排序
/**
* 将六个无序的数:53, 3, 542, 748, 14, 214使用基数排序从低到高进行排序
* 基数排序是使用空间换时间的经典算法
*/
public class G_RadixSort {
public static void main(String[] args) {
// int[] arr = {53, 3, 542, 748, 14, 214};
// System.out.println("基数排序");
// radixSort(arr);
// System.out.println("简化基数排序");
// radixSortSimplify(arr);
//创建80000个随机数的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
radixSortSimplify(arr);
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData)); //19
}
//基数排序
public static void radixSort(int[] arr) {
//定义一个二维数组,表示10个“桶”,每个“桶”就是一个一维数组
//为了防止在放入数的时候,数据溢出,每个“桶”(一维数组)的大小定义为arr.length
int[][] bucket = new int[10][arr.length];
//为了记录每个“桶”中,实际存放了多少个数据,定义一个一维数组记录各个“桶”每次放入的数据个数
int[] bucketElementCounts = new int[10];
//1. 第一次排序:(针对每个元素的**个**位进行排序处理)
for (int j = 0; j < arr.length; j++) {
//1.1. 将每个元素的**个**位数取出,然后看这个数应该放在哪个对应的“桶”(一维数组)
int digitOfElement = arr[j] / 1 % 10; //取出每个元素的个位的值
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; //当前值放入对应的“桶”的第几个位置
bucketElementCounts[digitOfElement]++; //这个数组的默认值都是0,所以后面才要+1,即表示个数,又表示坐标
}
//1.2. 按照“桶”的顺序(一维数组的下标)依次取出数据,放入原数组
int index = 0;
//遍历每一个“桶”,并将“桶”中的数据放入原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果“桶”中有数据才放入原数组
if (bucketElementCounts[k] != 0) {
//循环该“桶”(第k个一维数组)放入原数组
for (int l = 0; l < bucketElementCounts[k]; l++) {
//取出数据放入原数组
arr[index++] = bucket[k][l];
}
}
//第一次排序后,需要将每个bucketElementCounts[k]置为0
bucketElementCounts[k] = 0;
}
System.out.println("第一次排序");
System.out.println(Arrays.toString(arr));
//2. 第二次排序:(针对每个元素的**十**位进行排序处理)
for (int j = 0; j < arr.length; j++) {
//2.1. 将每个元素的**十**位数取出,然后看这个数应该放在哪个对应的“桶”(一维数组)
int digitOfElement = arr[j] / 10 % 10; //取出每个元素的十位的值(748 / 10 =》74 % 10=》4)
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; //当前值放入对应的“桶”的第几个位置
bucketElementCounts[digitOfElement]++; //这个数组的默认值都是0,所以后面才要+1,即表示个数,又表示坐标
}
//2.2. 按照“桶”的顺序(一维数组的下标)依次取出数据,放入原数组
index = 0;
//遍历每一个“桶”,并将“桶”中的数据放入原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果“桶”中有数据才放入原数组
if (bucketElementCounts[k] != 0) {
//循环该“桶”(第k个一维数组)放入原数组
for (int l = 0; l < bucketElementCounts[k]; l++) {
//取出数据放入原数组
arr[index++] = bucket[k][l];
}
}
//第二次排序后,需要将每个bucketElementCounts[k]置为0
bucketElementCounts[k] = 0;
}
System.out.println("第二次排序");
System.out.println(Arrays.toString(arr));
//3. 第三次排序:(针对每个元素的**百**位进行排序处理)
for (int j = 0; j < arr.length; j++) {
//3.1. 将每个元素的**百**位数取出,然后看这个数应该放在哪个对应的“桶”(一维数组)
int digitOfElement = arr[j] / 100 % 10; //取出每个元素的百位的值(748 / 100 =》7 % 10 =》7)
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; //当前值放入对应的“桶”的第几个位置
bucketElementCounts[digitOfElement]++; //这个数组的默认值都是0,所以后面才要+1,即表示个数,又表示坐标
}
//3.2. 按照“桶”的顺序(一维数组的下标)依次取出数据,放入原数组
index = 0;
//遍历每一个“桶”,并将“桶”中的数据放入原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果“桶”中有数据才放入原数组
if (bucketElementCounts[k] != 0) {
//循环该“桶”(第k个一维数组)放入原数组
for (int l = 0; l < bucketElementCounts[k]; l++) {
//取出数据放入原数组
arr[index++] = bucket[k][l];
}
}
//第三次排序后,需要将每个bucketElementCounts[k]置为0
bucketElementCounts[k] = 0;
}
System.out.println("第三次排序");
System.out.println(Arrays.toString(arr));
}
//简化基数排序
public static void radixSortSimplify(int[] arr) {
//得到数组中最大值
int max = arr[0]; //假设第一个数就是最大值
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//得到数组中最大值的位数
int maxLength = String.valueOf(max).length();
//定义一个二维数组,表示10个“桶”,每个“桶”就是一个一维数组
//为了防止在放入数的时候,数据溢出,每个“桶”(一维数组)的大小定义为arr.length
int[][] bucket = new int[10][arr.length];
//为了记录每个“桶”中,实际存放了多少个时间,定义一个一维数组记录各个“桶”每次放入的数据个数
int[] bucketElementCounts = new int[10];
//经过几轮跟数字中最大值的位数有关
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
for (int j = 0; j < arr.length; j++) {
//1.1. 将每个元素的**个**位数取出,然后看这个数应该放在哪个对应的“桶”(一维数组)
int digitOfElement = arr[j] / n % 10; //取出每个元素的个位的值
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; //当前值放入对应的“桶”的第几个位置
bucketElementCounts[digitOfElement]++; //这个数组的默认值都是0,所以后面才要+1,即表示个数,又表示坐标
}
//1.2. 按照“桶”的顺序(一维数组的下标)依次取出数据,放入原数组
int index = 0;
//遍历每一个“桶”,并将“桶”中的数据放入原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果“桶”中有数据才放入原数组
if (bucketElementCounts[k] != 0) {
//循环该“桶”(第k个一维数组)放入原数组
for (int l = 0; l < bucketElementCounts[k]; l++) {
//取出数据放入原数组
arr[index++] = bucket[k][l];
}
}
//第一次排序后,需要将每个bucketElementCounts[k]置为0
bucketElementCounts[k] = 0;
}
// System.out.println("第" + i + "次排序");
// System.out.println(Arrays.toString(arr));
}
}
}
说明:
- 基数排序是对传统桶排序的扩展,速度很快
- 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成 OutOfMemoryError
- 基数排序是稳定的。(注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的)
- 有负数的数组,我们不用基数排序来进行排序,如果要支持负数,参考:基数排序负数处理方案
11.常用排序算法总结和对比#
- 堆排序和二叉树相关
- 相关术语解释:
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面
- 内排序:所有排序操作都在内存中完成
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
- 时间复杂度:一个算法执行所耗费的时间
- 空间复杂度:运行完一个程序所需内存的大小
- n:数据规模(数据量)
- k:“桶”的个数
- In-place:不占用额外内存
- Out-place:占用额外内存
八、查找算法
1.常用四种查找算法#
- 顺序(线性)查找:按照顺序比对,找到需要的数据
- 二分(折半)查找
- 插值查找
- 斐波那契查找
2.顺序(线性)查找(Sequential Search)#
- 有一个数列(可以是有序,也可以是无序):
{1, 8, 10, 89, 1000, 1234}
,输入一个数看看该数组是否存在此数(如果找到了,就给出下标值)
/**
* 顺序(线性)查找
* 有一个数列(可以是有序,也可以是无序):`{1, 8, 10, 89, 1000, 1234}`,输入一个数看看该数组是否存在此数(如果找到了,就给出下标值)
*/
public class A_SequentialSearch {
public static void main(String[] args) {
// int[] arr = {1, 8, 10, 89, 1000, 1234}; //有序数组
int[] arr = {1, 9, 11, -1, 34, 89, -1, -1}; //无序数组
sequentialSearch(arr, -1);
}
/**
* 顺序(线性)查找
*
* @param arr 数组
* @param target 要查找的值
*/
public static void sequentialSearch(int[] arr, int target) {
List<Integer> list = new ArrayList<>(); //存放要查找的值在数组中的下标
//线性查找是逐一比对,发现有相同值就返回下标
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
list.add(i);
}
}
if (list.size() == 0) {
System.out.println("未找到目标值");
} else {
System.out.println("目标值下标为:" + list);
}
}
}
3.二分(折半)查找(Binary Search)#
-
查找一个值:请对一个有序数组(无序数组需要先排序成有序数组再进行二分查找)进行二分查找
{1, 8, 10, 89, 1000, 1234}
,输入一个数看看该数组是否存在此数(如果找到了,就给出下标值)- 思路分析:(使用递归查找,另一种是非递归查找)
- 首先确定该数组中间的下标
mid = (left + right) / 2
- 让要查找的值
target
和arr[mid]
比较:(数组是从小到大的)target > arr[mid]
:要查找的值在mid
的右边,需要向右递归查找target < arr[mid]
:要查找的值在mid
的左边,需要向左递归查找target = arr[mid]
:找到要查找的值,结束递归,返回
left > right
:递归完整个数组,仍找不到target
,结束递归
- 首先确定该数组中间的下标
- 思路分析:(使用递归查找,另一种是非递归查找)
-
查找多个值:
{1, 8, 10, 89, 1000, 1000, 1000, 1234}
当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到(如果找到了,就给出下标值)- 思路分析:
- 首先确定该数组中间的下标
mid = (left + right) / 2
- 让要查找的值
target
和arr[mid]
比较:target > arr[mid]
:要查找的值在mid
的右边,需要向右递归查找target < arr[mid]
:要查找的值在mid
的左边,需要向左递归查找target = arr[mid]
:找到要查找的值,结束递归,返回- 在找到
mid
索引时,不要马上返回 - 向
mid
索引的左边扫描,将所有满足要求的下标,加入到list
集合中 - 向
mid
索引的右边扫描,将所有满足要求的下标,加入到list
集合中 - 将
list
返回
- 在找到
left > right
:递归完整个数组,仍找不到target
,结束递归
- 首先确定该数组中间的下标
- 思路分析:
/**
* 二分(折半)查找
* 使用二分(折半)查找的前提是该数组是有序的
*/
public class B_BinarySearch {
public static void main(String[] args) {
System.out.println("查找一个值:");
int[] arr = {1, 8, 10, 89, 1000, 1234};
Integer index = binarySearch(arr, -1, 0, arr.length);
System.out.println(index);
System.out.println("查找多个值:");
int[] arrs = {1, 8, 10, 89, 1000, 1000, 1000, 1234};
List<Integer> indexs = binarySearchBatch(arrs, 1000, 0, arrs.length);
System.out.println(indexs);
}
/**
* 查找一个值:请对一个**有序数组**(无序数组需要先排序成有序数组再进行二分查找)进行二分查找`{1, 8, 10, 89, 1000, 1234}`,输入一个数看看该数组是否存在此数(如果找到了,就给出下标值)
*
* @param arr 数组
* @param target 要查找的值
* @param left 左边的索引
* @param right 右边的索引
* @return 返回要查找的值在数组中的下标
*/
public static Integer binarySearch(int[] arr, int target, int left, int right) {
//3. `left > right`:递归完整个数组,仍找不到`target`,结束递归
if (left > right) {
return null;
}
//1. 首先确定该数组中间的下标`mid = (left + right) / 2`
int mid = (left + right) / 2;
int midVal = arr[mid];
//2. 让要查找的值`target`和`arr[mid]`比较:(数组是从小到大的)
//2.1. `target > arr[mid]`:要查找的值在`mid`的右边,需要向右递归查找
if (target > arr[mid]) { //向右递归
return binarySearch(arr, target, mid + 1, right);
//2.2. `target < arr[mid]`:要查找的值在`mid`的左边,需要向左递归查找
} else if (target < arr[mid]) { //向左递归
return binarySearch(arr, target, left, mid - 1);
//2.3. `target = arr[mid]`:找到要查找的值,结束递归,返回
} else {
return mid;
}
}
/**
* 查找多个值:`{1, 8, 10, 89, 1000, 1000, 1000, 1234}`当一个**有序数组**中,有多个相同的数值时,如何将所有的数值都查找到(如果找到了,就给出下标值)
*
* @param arr 数组
* @param target 要查找的值
* @param left 左边的索引
* @param right 右边的索引
* @return 返回要查找的值在数组中的下标
*/
public static List<Integer> binarySearchBatch(int[] arr, int target, int left, int right) {
//3. `left > right`:递归完整个数组,仍找不到`target`,结束递归
if (left > right) {
return null;
}
//1. 首先确定该数组中间的下标`mid = (left + right) / 2`
int mid = (left + right) / 2;
int midVal = arr[mid];
//2. 让要查找的值`target`和`arr[mid]`比较:
//2.1. `target > arr[mid]`:要查找的值在`mid`的右边,需要向右递归查找
if (target > arr[mid]) { //向右递归
return binarySearchBatch(arr, target, mid + 1, right);
//2.2. `target < arr[mid]`:要查找的值在`mid`的左边,需要向左递归查找
} else if (target < arr[mid]) { //向左递归
return binarySearchBatch(arr, target, left, mid - 1);
//2.3. `target = arr[mid]`:找到要查找的值,结束递归,返回
} else {
//2.3.1. 在找到`mid`索引时,不要马上返回
List<Integer> list = new ArrayList<>(); //存放要查找的值在数组中的下标
//2.3.2. 向`mid`索引的左边扫描,将所有满足要求的下标,加入到`list`集合中
int temp = mid - 1; //临时变量
while (true) {
if (temp < 0 || arr[temp] != target) { //扫描到最左边,或者向左遍历过程中没有目标值
break; //退出
}
list.add(temp); //将temp放到list中
temp--;
}
list.add(mid); //将mid放到list中
//2.3.3. 向`mid`索引的右边扫描,将所有满足要求的下标,加入到`list`集合中
temp = mid + 1; //临时变量
while (true) {
if (temp > arr.length - 1 || arr[temp] != target) { //扫描到最右边,或者向右遍历过程中没有目标值
break; //退出
}
list.add(temp); //将temp放到list中
temp++;
}
//2.3.4. 将`list`返回
return list;
}
}
}
4.插值查找算法(Interpolation Search)#
-
插值查找原理介绍:
-
插值查找算法类似于二分查找,不同的是插值查找每次从自适应
mid
处开始查找 -
将二分查找中的求
mid
索引的公式,low
表示左边索引left
,high
表示右边索引right
,key
表示要查找的值target
-
int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]); //插值索引
对应前面的代码公式:(目标数据到头元素的距离,比上整个数组,可以得到目标元素在数组位置的比例)(原理就是等差数列)
int mid = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left]);
-
对一个 1-100 的有序数组进行插值查找:(使用二分查找,需要递归多次,才能找到1;使用插值查找,一次找到)
- 思路分析:
- 首先确定该数组中间的下标
mid = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left])
- 让要查找的值
target
和arr[mid]
比较:target > arr[mid]
:要查找的值在mid
的右边,需要向右递归查找target < arr[mid]
:要查找的值在mid
的左边,需要向左递归查找target = arr[mid]
:找到要查找的值,结束递归,返回
left > right
:递归完整个数组,仍找不到target
,结束递归
- 首先确定该数组中间的下标
- 思路分析:
-
/**
* 插值查找
* 对一个 1-100 的有序数组进行插值查找:(使用二分查找,需要递归多次,才能找到1;使用插值查找,一次找到)
*/
public class C_InterpolationSearch {
public static void main(String[] args) {
//生成一个 1-100 的有序数组
// int[] arr = new int[100];
// for (int i = 0; i < 100; i++) {
// arr[i] = i + 1;
// }
// System.out.println(Arrays.toString(arr));
System.out.println("查找一个值:");
int[] arr = {1, 8, 10, 89, 1000, 1234};
Integer index = interpolationSearch(arr, 100, 0, arr.length - 1);
System.out.println(index);
System.out.println("查找多个值:");
int[] arrs = {1, 8, 10, 89, 1000, 1000, 1000, 1234};
List<Integer> indexs = interpolationSearchBatch(arrs, 100, 0, arr.length - 1);
System.out.println(indexs);
}
/**
* 查找一个值
*
* @param arr 数组
* @param target 要查找的值
* @param left 左边的索引
* @param right 右边的索引
* @return 返回要查找的值在数组中的下标
*/
public static Integer interpolationSearch(int[] arr, int target, int left, int right) {
//3. `left > right`:递归完整个数组,仍找不到`target`,结束递归
//target < arr[left] || target > arr[right]:防止StackOverflowError,要查找的值小于左边的值或大于右边的值
if (left > right || target < arr[left] || target > arr[right]) {
return null;
}
/*
查找1:int mid = 0 + (99 - 0) * (1 - 1) / (100 - 1) = 0
查找100:int mid = 0 + (99 - 0) * (100 - 1) / (100 - 1) = 99
*/
//1. 首先确定该数组中间的下标`int mid = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left]);`
//自适应写法
int mid = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left]);
int midVal = arr[mid];
//2. 让要查找的值`target`和`arr[mid]`比较:
//2.1. `target > arr[mid]`:要查找的值在`mid`的右边,需要向右递归查找
if (target > arr[mid]) { //向右递归
return interpolationSearch(arr, target, mid + 1, right);
//2.2. `target < arr[mid]`:要查找的值在`mid`的左边,需要向左递归查找
} else if (target < arr[mid]) { //向左递归
return interpolationSearch(arr, target, left, mid - 1);
//2.3. `target = arr[mid]`:找到要查找的值,结束递归,返回
} else {
return mid;
}
}
/**
* 查找多个值
*
* @param arr 数组
* @param target 要查找的值
* @param left 左边的索引
* @param right 右边的索引
* @return 返回要查找的值在数组中的下标
*/
public static List<Integer> interpolationSearchBatch(int[] arr, int target, int left, int right) {
//3. `left > right`:递归完整个数组,仍找不到`target`,结束递归
//target < arr[left] || target > arr[right]:防止StackOverflowError,要查找的值小于左边的值或大于右边的值
if (left > right || target < arr[left] || target > arr[right]) {
return null;
}
/*
查找1:int mid = 0 + (99 - 0) * (1 - 1) / (100 - 1) = 0
查找100:int mid = 0 + (99 - 0) * (100 - 1) / (100 - 1) = 99
*/
//1. 首先确定该数组中间的下标`int mid = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left]);`
//自适应写法
int mid = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left]);
int midVal = arr[mid];
//2. 让要查找的值`target`和`arr[mid]`比较:
//2.1. `target > arr[mid]`:要查找的值在`mid`的右边,需要向右递归查找
if (target > arr[mid]) { //向右递归
return interpolationSearchBatch(arr, target, mid + 1, right);
//2.2. `target < arr[mid]`:要查找的值在`mid`的左边,需要向左递归查找
} else if (target < arr[mid]) { //向左递归
return interpolationSearchBatch(arr, target, left, mid - 1);
//2.3. `target = arr[mid]`:找到要查找的值,结束递归,返回
} else {
//2.3.1. 在找到`mid`索引时,不要马上返回
List<Integer> list = new ArrayList<>(); //存放要查找的值在数组中的下标
//2.3.2. 向`mid`索引的左边扫描,将所有满足要求的下标,加入到`list`集合中
int temp = mid - 1; //临时变量
while (true) {
if (temp < 0 || arr[temp] != target) { //扫描到最左边,或者向左遍历过程中没有目标值
break; //退出
}
list.add(temp); //将temp放到list中
temp--;
}
list.add(mid); //将mid放到list中
//2.3.3. 向`mid`索引的右边扫描,将所有满足要求的下标,加入到`list`集合中
temp = mid + 1; //临时变量
while (true) {
if (temp > arr.length - 1 || arr[temp] != target) { //扫描到最右边,或者向右遍历过程中没有目标值
break; //退出
}
list.add(temp); //将temp放到list中
temp++;
}
//2.3.4. 将`list`返回
return list;
}
}
}
-
注意:
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
5.斐波那契(黄金分割法)查找算法(Fibonacci Search)#
-
斐波那契(黄金分割法)查找基本介绍:
- 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
- 斐波那契数列
{1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
发现斐波那契数列的两个相邻数的比例,无限接近黄金分割值0.618
-
斐波那契(黄金分割法)原理:斐波那契查找原理与前两种相似,仅仅改变了中间节点
mid
的位置,mid
不再是中间或插值得到,而是位于黄金分割点附近,即mid = low + F[k-1] - 1(F代表斐波那契数列,k代表斐波那契数列分割数值的下标)
,如下图所示对
F(k-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
-
类似的,每一子段也可以用相同的方式分割
-
但顺序表长度n不一定刚好等于
F[k] - 1
,所以需要将原来的顺序表长度n增加至F[k] - 1
。这里的k值只要能使得F[k] - 1
恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1到F[k] - 1
位置),都赋为n位置的值即可while (high > f[k] - 1) { k++; }
-
-
请对一个有序数组进行斐波那契查找
{1, 8, 10, 89, 1000, 1234}
,输入一个数看看该数组是否存在此数(如果找到了,就给出下标值)
/**
* 斐波那契(黄金分割法)查找
* 请对一个有序数组进行斐波那契查找`{1, 8, 10, 89, 1000, 1234}`,输入一个数看看该数组是否存在此数(如果找到了,就给出下标值)
*/
public class D_FibonacciSearch {
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
System.out.println("斐波那契数列:");
System.out.println(Arrays.toString(fibonacci()));
System.out.println("斐波那契(黄金分割法)查找:");
System.out.println(fibonacciSearch(arr, 100));
}
//因为mid = low + F(k-1) - 1需要使用到斐波那契数列,因此需要使用非递归的方法得到一个斐波那契数列(也可以使用递归的方法得到)
public static int[] fibonacci() {
int maxSize = 20; //斐波那契数列大小,初始化为20
int[] f = new int[maxSize];
f[0] = 1;
f[1] = 1;
for (int k = 2; k < maxSize; k++) {
f[k] = f[k - 1] + f[k - 2];
}
return f;
}
/**
* 斐波那契(黄金分割法)查找
* 使用非递归的方法(也可以使用递归的方法得到)
*
* @param arr 数组
* @param target 要查找的值
* @return 返回要查找的值在数组中的下标
*/
public static Integer fibonacciSearch(int[] arr, int target) {
int low = 0; //左边的索引
int high = arr.length - 1; //右边的索引
int k = 0; //斐波那契数列分割数值的下标
int mid = 0; //数组中间的下标,默认为0
int[] f = fibonacci(); //斐波那契数列
//顺序表长度n不一定刚好等于`F[k] - 1`,所以需要将原来的顺序表长度n增加至`F[k] - 1`(获取斐波那契数列分割数值的下标)
while (high > f[k] - 1) {
k++;
}
//因为f[k]的值可能大于arr的长度,因此需要使用Arrays类,构造一个新的数组并指向temp[],不足的部分默认使用0填充
int[] temp = Arrays.copyOf(arr, f[k]);
//需要使用arr数组最后的数填充temp{1, 8, 10, 89, 1000, 1234, 0, 0}=》{1, 8, 10, 89, 1000, 1234, 1234, 1234}
for (int i = high + 1; i < temp.length; i++) {
temp[i] = arr[high];
}
//使用while循环找到要查找的值
while (low <= high) {
mid = low + f[k - 1] - 1;
if (target < temp[mid]) { //向左查找
high = mid - 1;
/*说明:
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--
5.即下次循环mid = f[k - 1 - 1] - 1
*/
k--;
} else if (target > temp[mid]) { //向右查找
low = mid + 1;
/*说明:
1.全部元素 = 前面的元素 + 后面的元素
2.f[k] = f[k - 1] + f[k - 2]
3.后面有f[k - 2]个元素,所以可以继续拆分f[k - 2] = 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 null;
}
}
九、哈希表(数据结构)
1.哈希表的基本原理#
-
散列表(Hash table,也叫哈希表),是根据关键码值(Key value,关键属性)而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
-
缓存层:(提升数据查找速度)
- 缓存产品
- Redis
- Memcache
- 手写
- 哈希表
- 数组+链表
- 数组+二叉树
- 哈希表
- 缓存产品
-
哈希表的内存布局(链表数组)(之所以能够提高效率,是因为它可以同时管理多条链表)
2.哈希表(散列表)-Google上机题#
-
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,姓名,性别,年龄,住址……),当输入该员工的id时,要求查找到该员工的所有信息
-
要求:
- 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
- 添加时,保证按照id从低到高插入(思考:如果id不是从低到高插入,但要求各条链表仍是从低到高,怎么解决?)
- 使用链表来实现哈希表, 该链表不带表头(即:链表的第一个节点就存放雇员信息**)
-
思路分析:手写一个哈希表(数组+链表)
-
雇员
//1.雇员 class Emp { public int id; //id public String name; //姓名 }
-
链表,存放数据
//2.链表,存放数据 class EmpLinkedList { public Emp head; //头指针,指向当前链表的第一个雇员,默认为null }
-
哈希表创建,管理多条链表:里面有相关对雇员的操作
- 哈希表的添加:添加雇员
- 哈希表的遍历:显示所有员工
- 哈希表的查找:按id查询
- 哈希表的删除:按id查询
- 散列函数(取模法):决定id对应到哪个链表
//哈希表创建,管理多条链表:里面有相关对雇员的操作 class HashTable { private EmpLinkedList[] empLinkedLists; //数组,每个元素指向一条链表 //3.1.哈希表的添加:添加雇员 public void add(Emp emp) { } //3.2.哈希表的遍历:遍历所有的链表 public void list() { } //3.3.哈希表的查找:按id查询 public void findEmpById(int id) { } //3.4.散列函数(取模法):决定id对应到哪个链表 public int hashFun(int id) { return id % size; } }
-
/**
* 有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,姓名,性别,年龄,住址……),当输入该员工的id时,要求查找到该员工的所有信息
* 要求:
* 1. 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
* 2. 添加时,保证按照id从低到高插入(思考:如果id不是从低到高插入,但要求各条链表仍是从低到高,怎么解决?)
* 3. 使用**链表**来实现哈希表, 该链表不带表头(即:**链表的第一个节点就存放雇员信息**)
*/
public class A_HashTable {
public static void main(String[] args) {
//创建哈希表
HashTable hashTable = new HashTable(7);
char key = ' '; //接受用户的输入
Scanner scanner = new Scanner(System.in); //扫描器
boolean loop = true; //用于循环的变量
//输出一个菜单
while (loop) {
System.out.println("a(add):添加雇员");
System.out.println("g(get):查找雇员");
System.out.println("s(show):显示雇员");
System.out.println("e(exit):退出系统");
key = scanner.next().charAt(0); //接收一个字符
switch (key) {
case 'a':
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 'g':
try {
System.out.println("请输入id:");
id = scanner.nextInt();
hashTable.findEmpById(id);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
try {
hashTable.list();
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("退出系统");
}
}
//3.哈希表创建,管理多条链表:里面有相关对雇员的操作
class HashTable {
private EmpLinkedList[] empLinkedLists; //数组,每个元素指向一条链表
private int size; //链表数
public HashTable(int size) {
this.size = size;
//初始化EmpLinkedList
empLinkedLists = new EmpLinkedList[size];
//分别初始化每条链表
for (int i = 0; i < size; i++) {
empLinkedLists[i] = new EmpLinkedList();
}
}
//3.1.哈希表的添加:添加雇员
public void add(Emp emp) {
//根据员工的id,得到该员工应当添加到哪条链表
int empLinkedListNo = hashFunction(emp.id);
//将emp添加到对应的链表中
empLinkedLists[empLinkedListNo].add(emp);
}
//3.2.哈希表的遍历:遍历所有的链表
public void list() {
for (int i = 0; i < size; i++) {
empLinkedLists[i].list(i);
}
}
/**
* 3.3.哈希表的查找:按id查询
*
* @param id 雇员id
*/
public void findEmpById(int id) {
//使用散列函数确定到哪条链表查找
int empLinkedListNo = hashFunction(id);
Emp emp = empLinkedLists[empLinkedListNo].findEmpById(id);
if (emp != null) {
System.out.printf("%d当前链表找到该雇员:%d\n", empLinkedListNo, id);
} else {
System.out.println("没有找到该雇员");
}
}
/**
* 3.4.散列函数(取模法):决定id对应到哪个链表
*
* @param id 雇员id
* @return 链表编号
*/
public int hashFunction(int id) {
return id % size;
}
}
//1.雇员
class Emp {
public int id; //id
public String name; //姓名
public Emp next; //指向下一个节点的引用,默认为null
public Emp(int id, String name) {
this.id = id;
this.name = name;
}
}
//2.链表,存放数据
class EmpLinkedList {
public Emp head; //头指针,指向当前链表的第一个雇员,默认为null
//添加雇员到链表
public void add(Emp emp) {
//假设,当添加雇员时,id是自增的,直接把雇员加到当前链表的最后即可
//添加第一个雇员
if (head == null) {
head = emp;
return;
}
//如果不是第一个雇员,使用辅助指针,帮助定位到最后
Emp temp = head;
while (true) {
if (temp.next == null) { //到链表的最后
break;
}
temp = temp.next; //后移
}
//退出时,直接将emp加入链表
temp.next = emp;
}
/**
* 遍历链表的雇员信息
*
* @param no 链表编号
*/
public void list(int no) {
if (head == null) {
System.out.println(no + "当前链表为空");
return;
}
System.out.print(no + "链表的雇员信息:");
//不是第一个雇员,使用辅助指针,帮助定位到最后
Emp temp = head;
while (true) {
System.out.printf("=》id=%d name=%s", temp.id, temp.name);
if (temp.next == null) { //到链表的最后
break;
}
temp = temp.next; //后移
}
System.out.println();
}
/**
* 根据id查找雇员
*
* @param id 雇员id
* @return 雇员信息
*/
public Emp findEmpById(int id) {
if (head == null) {
System.out.println("当前链表为空");
return null;
}
//不是第一个雇员,使用辅助指针,帮助定位到最后
Emp temp = head;
while (true) {
if (temp.id == id) { //找到
break; //temp指向要查找的雇员
}
if (temp.next == null) { //当前链表没有找到
temp = null;
break; //退出
}
temp = temp.next; //后移
}
return temp;
}
}
十、树结构
1.二叉树#
1.为什么需要树这种数据结构-非线性结构#
-
数据存储的几种方式:
-
数组存储方式的分析:
- 优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找等方式提高检索速度
- 缺点:如果要检索具体某个值要逐个比对,或者插入值(按一定顺序)会整体移动(底层会进行数组扩容:每次在底层都需要创建新的数组,要将原来的数据拷贝到新的数组,并插入新的数据),效率较低
- 集合可以动态增长的原因:
ArrayList
底层维护了一个Object[]
类型的数组elementData
- 当创建对象时,如果使用的是无参构造器,则初始
elementData
容量为0(JDK7默认为10) - 如果使用的是指定容量的构造器,则初始
elementData
容量为initialCapacity
- 当添加元素时:先判断是否需要扩容,如果需要则调用
grow(int minCapacity)
方法,否则直接添加元素到合适位置 - 如果使用的是无参构造器,如果第一次添加需要扩容的话,则扩容
elementData
为10,如果需要再次扩容的话,则扩容elementData
为1.5倍 - 如果使用的是指定容量
initialCapacity
的构造器,如果需要扩容,则直接扩容elementData
为1.5倍
- 集合可以动态增长的原因:
-
链式存储方式的分析:
- 优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)
- 缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
-
树存储方式的分析:
-
能提高数据存储,读取的效率,比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度
-
案例:
{7, 3, 10, 1, 5, 9, 12}
- 分析:如果以二叉排序树来存储数据,那么对数据的增删改查的效率都可以提高
- 查找12:比7大往右找,比10大往右找,经过两次比较找到12
- 添加13:找到链表最大的,挂到12的右子节点
- 删除1:比7小往左找,比3小往左找,找到1,置空父节点的左子节点
-
-
-
2.二叉树示意图#
- 节点:对象、节点对象
- 根节点:没有父节点的节点
- 父节点:
- 子节点:
- 叶子节点:没有子节点的节点
- 节点的权:节点的值
- 路径:从根节点找到该节点的路线
- 层:处于同一个级别/层面归为同一层
- 子树:
- 树的高度:最大层数
- 森林:多颗子树构成森林
3.二叉树的概念#
-
树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树
-
二叉树的子节点分为左子节点和右子节点
-
如果该二叉树的所有叶子节点都在最后一层,并且$节点总数=2^n -1(n 为层数)$,则我们称为满二叉树
-
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
4.二叉树的实际应用#
1.遍历#
-
使用前序,中序和后序对下面的二叉树进行遍历(3号节点增加一个左子节点,结果又是什么)
- 前序遍历:先输出父节点,再遍历左子树和右子树
- 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
- 小结:看输出父节点的顺序,就确定是前序,中序还是后序
-
思路分析:
- 创建一颗二叉树,从根节点出发
- 前序遍历:
- 先输出当前节点(初始是根节点)
- 如果左子节点不为空,则递归继续前序遍历
- 如果右子节点不为空,则递归继续前序遍历
- 中序遍历:
- 如果左子节点不为空,则递归继续中序遍历
- 输出当前节点
- 如果右子节点不为空,则递归继续中序遍历
- 后续遍历:
- 如果左子节点不为空,则递归继续后序遍历
- 如果右子节点不为空,则递归继续后序遍历
- 输出当前节点
2.查找#
- 使用前序,中序和后序的方式查询指定节点:
- 请编写前序查找,中序查找和后序查找的方法
- 并分别使用三种查找方式,查找5号叶子节点
- 并分析各种查找方式,分别比较了多少次
- 思路分析:
- 前序查找:(比较4次)
- 先判断当前节点是否等于要查找的值,如果相等,返回当前节点
- 如果不相等,则判断左子节点是否为空,如果不为空,则向左递归前序查找,找到要查找的值就返回
- 如果没找到,则判断右子节点是否为空,如果不为空,则向右递归前序查找,找到要查找的值就返回,否则返回null
- 中序查找:(比较3次)
- 先判断当前节点的左子节点是否为空,如果不为空,则向左递归中序查找,找到要查找的值就返回
- 如果没找到,则判断当前节点是否是要查找的值,找到要查找的值就返回当前节点
- 如果没找到,则判断右子节点是否为空,如果不为空,则向右递归中序查找,找到要查找的值就返回,否则返回null
- 后续查找:(比较2次)
- 先判断当前节点的左子节点是否为空,如果不为空,则向左递归后序查找,找到要查找的值就返回
- 如果没找到,则判断右子节点是否为空,如果不为空,则向右递归后序查找,找到要查找的值就返回
- 如果没找到,则判断当前节点是否是要查找的值,找到要查找的值就返回,否则返回null
- 前序查找:(比较4次)
3.删除#
-
删除指定节点:
- 如果删除的节点是叶子节点(没有子节点),则删除该节点
- 如果删除的节点是非叶子节点,则删除该子树
- 删除掉5号叶子节点和3号子树
-
思路分析:
- 判断是否是空树
- 判断根节点是否需要删除,则如果是将二叉树置空
- 因为我们的二叉树是单向的,需要判断当前节点的子节点是否需要删除,不能判断当前节点是否需要删除
- 如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,则
this.left = null;
,返回结束递归删除 - 如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,则
this.right = null;
,返回结束递归删除 - 如果4,5步骤没有删除节点,就需要向左子树递归删除
- 如果向左子树没有删除成功,就需要向右子树递归删除
-
思考:如果要删除的节点是非叶子节点,现在不希望将该非叶子节点为根节点的子树删除,需要指定规则,假如规定如下:(二叉排序树)
- 如果该非叶子节点A只有一个子节点B,则子节点B替代节点A
- 如果该非叶子节点A有左子节点B和右子节点C,则让左子节点B替代节点A
/**
* 二叉树的实际应用
* 1.遍历
* 2.查找
* 3.删除
*/
public class A_BinaryTree {
public static void main(String[] args) {
//创建需要的节点
HeroNode node1 = new HeroNode(1, "刘一");
HeroNode node2 = new HeroNode(2, "陈二");
HeroNode node3 = new HeroNode(3, "张三");
HeroNode node4 = new HeroNode(4, "李四");
HeroNode node5 = new HeroNode(5, "王五");
//1.1.创建一颗二叉树,从根节点出发
BinaryTree binaryTree = new BinaryTree();
node1.setLeft(node2);
node1.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5); //3号节点增加一个左子节点
binaryTree.setRoot(node1);
System.out.println("前序遍历:");
binaryTree.prefixOrder(); //1, 2, 3, 4=》1, 2, 3, 5, 4
System.out.println("中序遍历:");
binaryTree.infixOrder(); //2, 1, 3, 4=》2, 1, 5, 3, 4
System.out.println("后序遍历:");
binaryTree.suffixOrder(); //2, 4, 3, 1=》2, 5, 4, 3, 1
int target = 5; //要查找的值
System.out.println("前序查找:");
HeroNode prefixNode = binaryTree.prefixOrderSearch(target);
if (prefixNode != null) {
System.out.printf("找到%d,name=%s\n", prefixNode.getNo(), prefixNode.getName());
} else {
System.out.printf("没有找到%d\n", target);
}
System.out.println("中序查找:");
HeroNode infixNode = binaryTree.infixOrderSearch(target);
if (infixNode != null) {
System.out.printf("找到%d,name=%s\n", infixNode.getNo(), infixNode.getName());
} else {
System.out.printf("没有找到%d\n", target);
}
System.out.println("后序查找:");
HeroNode suffixNode = binaryTree.suffixOrderSearch(target);
if (suffixNode != null) {
System.out.printf("找到%d,name=%s\n", suffixNode.getNo(), suffixNode.getName());
} else {
System.out.printf("没有找到%d\n", target);
}
System.out.println("删除前(前序遍历):");
binaryTree.prefixOrder(); //1, 2, 3, 5, 4
System.out.println("删除5号叶子节点(前序遍历):");
binaryTree.delete(5);
binaryTree.prefixOrder(); //1, 2, 3, 4
System.out.println("删除3号子树(前序遍历):");
binaryTree.delete(3);
binaryTree.prefixOrder(); //1, 2
}
}
//定义二叉树
class BinaryTree {
private HeroNode root; //根节点
public void setRoot(HeroNode root) {
this.root = root;
}
//1.2.前序遍历:**先输出父节点**,再遍历左子树和右子树
public void prefixOrder() {
if (root != null) {
root.prefixOrder();
} else {
System.out.println("当前二叉树为空");
}
}
//1.3.中序遍历:先遍历左子树,**再输出父节点**,再遍历右子树
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("当前二叉树为空");
}
}
//1.4.后序遍历:先遍历左子树,再遍历右子树,**最后输出父节点**
public void suffixOrder() {
if (root != null) {
root.suffixOrder();
} else {
System.out.println("当前二叉树为空");
}
}
/**
* 2.1.前序查找(比较4次)
*
* @param no 要查找的值
* @return 返回要查找的值
*/
public HeroNode prefixOrderSearch(int no) {
if (root != null) {
return root.prefixOrderSearch(no);
} else {
System.out.println("当前二叉树为空");
return null;
}
}
/**
* 2.2.中序查找(比较3次)
*
* @param no 要查找的值
* @return 返回要查找的值
*/
public HeroNode infixOrderSearch(int no) {
if (root != null) {
return root.infixOrderSearch(no);
} else {
System.out.println("当前二叉树为空");
return null;
}
}
/**
* 2.3.后序查找(比较2次)
*
* @param no 要查找的值
* @return 返回要查找的值
*/
public HeroNode suffixOrderSearch(int no) {
if (root != null) {
return root.suffixOrderSearch(no);
} else {
System.out.println("当前二叉树为空");
return null;
}
}
/**
* 删除指定节点
* 1. 如果删除的节点是叶子节点(没有子节点),则删除该节点
* 2. 如果删除的节点是非叶子节点,则删除该子树
*
* @param no 要删除的值
*/
public void delete(int no) {
//3.1. 判断是否是空树
if (root != null) {
//3.2. 判断根节点是否需要删除,则如果是将二叉树置空
if (root.getNo() == no) {
root = null;
} else {
root.delete(no); //递归删除
}
} else {
System.out.println("当前二叉树为空");
}
}
}
//创建HeroNode节点
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 int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
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;
}
public void setRight(HeroNode right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode{no=" + no + ", name=" + name + '}';
}
//1.2.前序遍历:**先输出父节点**,再遍历左子树和右子树
public void prefixOrder() {
//1.2.1. 先输出当前节点(初始是根节点)
System.out.println(this);
//1.2.2. 如果左子节点不为空,则递归继续前序遍历
if (this.left != null) {
this.left.prefixOrder();
}
//1.2.3. 如果右子节点不为空,则递归继续前序遍历
if (this.right != null) {
this.right.prefixOrder();
}
}
//1.3.中序遍历:先遍历左子树,**再输出父节点**,再遍历右子树
public void infixOrder() {
//1.3.1. 如果左子节点不为空,则递归继续中序遍历
if (this.left != null) {
this.left.infixOrder();
}
//1.3.2. 输出当前节点
System.out.println(this);
//1.3.3. 如果右子节点不为空,则递归继续中序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
//1.4.后序遍历:先遍历左子树,再遍历右子树,**最后输出父节点**
public void suffixOrder() {
//1.4.1. 如果左子节点不为空,则递归继续后序遍历
if (this.left != null) {
this.left.suffixOrder();
}
//1.4.2. 如果右子节点不为空,则递归继续后序遍历
if (this.right != null) {
this.right.suffixOrder();
}
//1.4.3. 输出当前节点
System.out.println(this);
}
/**
* 2.1.前序查找(比较4次)
*
* @param no 要查找的值
* @return 返回要查找的值
*/
public HeroNode prefixOrderSearch(int no) {
System.out.println("前序查找比较次数"); //写在比较语句之前,前面都是判断是否为空
//2.1.1. 先判断当前节点是否等于要查找的值,如果相等,返回当前节点
if (this.no == no) {
return this;
}
HeroNode node = null; //临时变量,存放结果节点
//2.1.2. 如果不相等,则判断左子节点是否为空,如果不为空,则向左递归前序查找,找到要查找的值就返回
if (this.left != null) {
node = this.left.prefixOrderSearch(no);
}
if (node != null) { //左子节点找到要查找的值
return node;
}
//2.1.3. 如果没找到,则判断右子节点是否为空,如果不为空,则向右递归前序查找,找到要查找的值就返回,否则返回null
if (this.right != null) {
node = this.right.prefixOrderSearch(no);
}
//右子节点找到要查找的值 或者 没找到要查找的值返回null
return node;
}
/**
* 2.2.中序查找(比较3次)
*
* @param no 要查找的值
* @return 返回要查找的值
*/
public HeroNode infixOrderSearch(int no) {
HeroNode node = null; //临时变量,存放结果节点
//2.2.1. 先判断当前节点的左子节点是否为空,如果不为空,则向左递归中序查找,找到要查找的值就返回
if (this.left != null) {
node = this.left.infixOrderSearch(no);
}
if (node != null) { //左子节点找到要查找的值
return node;
}
System.out.println("中序查找比较次数"); //写在比较语句之前,前面都是判断是否为空
//2.2.2. 如果没找到,则判断当前节点是否是要查找的值,找到要查找的值就返回当前节点
if (this.no == no) {
return this;
}
//2.2.3. 如果没找到,则判断右子节点是否为空,如果不为空,则向右递归中序查找,找到要查找的值就返回,否则返回null
if (this.right != null) {
node = this.right.infixOrderSearch(no);
}
//右子节点找到要查找的值 或者 没找到要查找的值返回null
return node;
}
/**
* 2.3.后序查找(比较2次)
*
* @param no 要查找的值
* @return 返回要查找的值
*/
public HeroNode suffixOrderSearch(int no) {
HeroNode node = null; //临时变量,存放结果节点
//2.3.1. 先判断当前节点的左子节点是否为空,如果不为空,则向左递归后序查找,找到要查找的值就返回
if (this.left != null) {
node = this.left.suffixOrderSearch(no);
}
if (node != null) { //左子节点找到要查找的值
return node;
}
//2.3.2. 如果没找到,则判断右子节点是否为空,如果不为空,则向右递归后序查找,找到要查找的值就返回当前节点
if (this.right != null) {
node = this.right.suffixOrderSearch(no);
}
if (node != null) { //右子节点找到要查找的值
return node;
}
System.out.println("后序查找比较次数"); //写在比较语句之前,前面都是判断是否为空
//2.3.3. 如果没找到,则判断当前节点是否是要查找的值,找到要查找的值就返回,否则返回null
if (this.no == no) {
return this;
}
//没找到要查找的值返回null
return node;
}
/**
* 删除指定节点
* 1. 如果删除的节点是叶子节点(没有子节点),则删除该节点
* 2. 如果删除的节点是非叶子节点,则删除该子树
*
* @param no 要删除的值
*/
public void delete(int no) {
//3.3. 因为我们的二叉树是单向的,需要判断当前节点的子节点是否需要删除,不能判断当前节点是否需要删除
//3.4. 如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,则`this.left = null;`,返回结束递归删除
if (this.left != null && this.left.no == no) {
this.left = null;
return;
}
//3.5. 如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,则`this.right = null;`,返回结束递归删除
if (this.right != null && this.right.no == no) {
this.right = null;
return;
}
//3.6. 如果4,5步骤没有删除节点,就需要向左子树递归删除
if (this.left != null) {
this.left.delete(no);
}
//3.7. 如果向左子树没有删除成功,就需要向右子树递归删除
if (this.right != null) {
this.right.delete(no);
}
}
}
2.顺序存储二叉树#
1.顺序存储二叉树的概念#
-
基本说明:从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,如图所示
-
要求:(以数组的方式存放,遍历使用前序遍历,中序遍历和后序遍历)
- 如图二叉树的节点,要求以数组的方式来存放
arr[1, 2, 3, 4, 5, 6, 7]
- 要求在遍历数组
arr
时,仍然以前序遍历,中序遍历和后序遍历的方式完成节点的遍历
- 如图二叉树的节点,要求以数组的方式来存放
2.顺序存储二叉树的特点#
- 顺序二叉树通常只考虑完全二叉树
- 第n个元素的左子节点(索引)为$2 * n + 1$(比如2的左子节点是$2*1+1=3$,即4的索引)
- 第n个元素的右子节点(索引)为$2 * n + 2$(比如3的右子节点是$2*2+2=6$,即7的索引)
- 第n个元素的父节点(索引)为$(n-1) / 2$(比如5的父节点是$(4-1)/2=1$,即2的索引)
- n:表示二叉树中的第几个元素(按0开始编号,为了跟数组保持一致)
3.顺序存储二叉树遍历#
-
需求:对数组
{1, 2, 3, 4, 5, 6, 7}
以二叉树前序遍历,中序遍历和后序遍历的方式进行遍历 -
顺序存储二叉树应用实例:八大排序算法中的堆排序,就会使用到顺序存储二叉树
/**
* 需求:对数组`{1, 2, 3, 4, 5, 6, 7}`以二叉树**前序遍历**,**中序遍历**和**后序遍历**的方式进行遍历
*/
public class B_ArrayBinarySortTree {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7};
ArrayBinarySortTree arrayBinarySortTree = new ArrayBinarySortTree(arr);
System.out.println("前序遍历:");
arrayBinarySortTree.prefixOrder(0); //{1, 2, 4, 5, 3, 6, 7}
System.out.println("中序遍历:");
arrayBinarySortTree.infixOrder(0); //{4, 2, 5, 1, 6, 3, 7}
System.out.println("后序遍历:");
arrayBinarySortTree.suffixOrder(0); //{4, 5, 2, 6, 7, 3, 1}
}
}
//定义顺序存储二叉树
class ArrayBinarySortTree {
private int[] arr; //存储数据节点的数组
public ArrayBinarySortTree(int[] arr) {
this.arr = arr;
}
/**
* 前序遍历
*
* @param index 数组的下标
*/
public void prefixOrder(int index) {
//如果数组为空,或者arr.length == 0
if (arr == null || arr.length == 0) {
System.out.println("数组为空");
return;
}
System.out.println("当前元素:" + arr[index]);
//向左递归遍历
int left = 2 * index + 1; //第n个元素的左子节点(索引)为2 * n + 1
if (left < arr.length) {
prefixOrder(left);
}
//向右递归遍历
int right = 2 * index + 2; //第n个元素的右子节点(索引)为2 * n + 2
if (right < arr.length) {
prefixOrder(right);
}
}
/**
* 中序遍历
*
* @param index 数组的下标
*/
public void infixOrder(int index) {
//如果数组为空,或者arr.length == 0
if (arr == null || arr.length == 0) {
System.out.println("数组为空");
return;
}
//向左递归遍历
int left = 2 * index + 1; //第n个元素的左子节点(索引)为2 * n + 1
if (left < arr.length) {
infixOrder(left);
}
System.out.println("当前元素:" + arr[index]);
//向右递归遍历
int right = 2 * index + 2; //第n个元素的右子节点(索引)为2 * n + 2
if (right < arr.length) {
infixOrder(right);
}
}
/**
* 后序遍历
*
* @param index 数组的下标
*/
public void suffixOrder(int index) {
//如果数组为空,或者arr.length == 0
if (arr == null || arr.length == 0) {
System.out.println("数组为空");
return;
}
//向左递归遍历
int left = 2 * index + 1; //第n个元素的左子节点(索引)为2 * n + 1
if (left < arr.length) {
suffixOrder(left);
}
//向右递归遍历
int right = 2 * index + 2; //第n个元素的右子节点(索引)为2 * n + 2
if (right < arr.length) {
suffixOrder(right);
}
System.out.println("当前元素:" + arr[index]);
}
}
3.线索化二叉树#
1.先看一个实际问题-引入线索化二叉树#
-
将数列
{1, 3, 6, 8, 10, 14}
构建成一颗二叉树 -
问题分析:
- 当对上面的二叉树进行中序遍历时,数列为
{8, 3, 10, 1, 14, 6}
- 但是 6, 8, 10, 14 这几个节点的左右指针,并没有完全的利用上(n+1=7个空指针域)
- 如果希望充分的利用各个节点的左右指针,让各个节点可以指向自己的前后节点,怎么办?(线索化二叉树)
- 当对上面的二叉树进行中序遍历时,数列为
-
线索化作用:
- 节约空间,空指针都能得到利用
- 每次要输出前序、中序、后序的前后节点时候都需要遍历一次才能拿出来,而线索化可以直接保存他的前后节点,节省时间上的开销
2.线索二叉树基本介绍#
- n个节点的二叉链表中含有n+1【公式:$2n-(n-1)=n+1$】个空指针域。利用二叉链表中的空指针域,存放指向该节点在某种遍历次序下的前驱和后继节点的指针(这种附加的指针称为“线索”)(n个节点有2n个指针域,每个节点需要一个指针,除根节点外)
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded Binary Tree)。根据线索性质(遍历次序)的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
- 一个节点的前一个节点,称为前驱节点(3的前驱节点是8)
- 一个节点的后一个节点,称为后继节点(3的后继节点是10)
3.线索二叉树分析图解#
1.线索化二叉树#
-
需求:将数列
{1, 3, 6, 8, 10, 14}
构建成一颗二叉树,进行中序线索二叉树。中序遍历的结果为:{8, 3, 10, 1, 14, 6}
中序线索二叉树:
-
思路分析:结合中序遍历的结果:
{8, 3, 10, 1, 14, 6}
分析- 8前面没有前驱节点,左指针不用动,8的后继节点是3,右指针指向后继节点3
- 3的左指针和右指针已经充分使用,不能动
- 10的前驱节点是3,左指针指向前驱节点3,10的后继节点是1,右指针指向后继节点1
- 1的左指针和右指针已经充分使用,不能动
- 14的前驱节点是1,左指针指向前驱节点1,14的后继节点是6,右指针指向后继节点6
- 6的前驱节点是14,已经指向14,不用动,6的后继节点没有,不用动
-
说明:当线索化二叉树后,Node节点的属性 left 和 right ,有如下情况:
- left 可能指向左子树,也可能指向前驱节点:比如节点1的 left 指向的左子树,而节点10的 left 指向的就是前驱节点
- right 可能指向右子树,也可能指向后继节点:比如节点1的 right 指向的是右子树,而节点10的 right 指向的是后继节点
2.遍历线索化二叉树#
- 说明:对前面的中序线索化的二叉树, 进行遍历
- 分析:因为线索化后,各个节点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历线索化二叉树的次序应当和对应的遍历方式保持一致
/**
* 需求:将数列`{1, 3, 6, 8, 10, 14}`构建成一颗二叉树,进行**中序线索二叉树**。中序遍历的结果为`{8, 3, 10, 1, 14, 6}`
* 说明:对前面的中序线索化的二叉树, 进行遍历
*/
public class C_ThreadedBinaryTree {
public static void main(String[] args) {
//创建需要的节点
Node node1 = new Node(1, "刘一");
Node node3 = new Node(3, "张三");
Node node6 = new Node(6, "赵六");
Node node8 = new Node(8, "周八");
Node node10 = new Node(10, "郑十");
Node node14 = new Node(14, "十四");
//手动创建线索化二叉树(也可以递归创建)
node1.setLeft(node3);
node1.setRight(node6);
node3.setLeft(node8);
node3.setRight(node10);
node6.setLeft(node14);
//定义线索化二叉树
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.setRoot(node1);
System.out.println("中序线索二叉树:");
threadedBinaryTree.infixThreadedNodes(node1);
//10的前驱节点是3,左指针指向前驱节点3,10的后继节点是1,右指针指向后继节点1
Node left = node10.getLeft();
System.out.println("10的前驱节点是:" + left);
Node right = node10.getRight();
System.out.println("10的后继节点是:" + right);
//使用之前的遍历方式会出现死循环,因为原先为空的左指针或右指针不再为空
System.out.println("遍历中序线索二叉树:");
threadedBinaryTree.infixOrder(); //{8, 3, 10, 1, 14, 6}
}
}
//定义线索化二叉树:在二叉树的基础上增加一个可以进行线索化的功能
class ThreadedBinaryTree {
private Node root; //根节点
private Node pre = null; //为了实现线索化,需要创建一个指向当前节点的前驱节点的指针,在递归进行线索化时,pre总是保留前一个节点
public void setRoot(Node root) {
this.root = root;
}
/**
* 前序线索二叉树
*
* @param node 当前需要线索化的节点
*/
public void prefixThreadedNodes(Node node) {
if (node == null) {
System.out.println("节点为空,不能线索化"); //n+1个空指针域
return;
}
//2.线索化当前节点
//2.1.先处理当前节点的前驱节点
if (node.getLeft() == null) { //当前节点的左子树为空
//让当前节点的左指针指向前驱节点
node.setLeft(pre);
//修改当前节点的左指针的类型为1,指向前驱节点
node.setLeftType(1);
}
//2.2.再处理当前节点的后继节点
if (pre != null && pre.getRight() == null) { //当前节点的右子树为空
//让前驱节点的右指针指向当前节点
pre.setRight(node);
//修改前驱节点的右指针的类型为1,指向当前节点
pre.setRightType(1);
}
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
//1.先线索化左子树
if (node.getLeftType() == 0) {
prefixThreadedNodes(node.getLeft());
}
//3.再线索化右子树
if (node.getRightType() == 0) {
prefixThreadedNodes(node.getRight());
}
}
/**
* 中序线索二叉树
*
* @param node 当前需要线索化的节点
*/
public void infixThreadedNodes(Node node) {
if (node == null) {
System.out.println("节点为空,不能线索化"); //n+1个空指针域
return;
}
//1.先线索化左子树
infixThreadedNodes(node.getLeft());
//2.线索化当前节点
//2.1.先处理当前节点的前驱节点
if (node.getLeft() == null) { //当前节点的左子树为空
//让当前节点的左指针指向前驱节点
node.setLeft(pre);
//修改当前节点的左指针的类型为1,指向前驱节点
node.setLeftType(1);
}
//2.2.再处理当前节点的后继节点
if (pre != null && pre.getRight() == null) { //当前节点的右子树为空
//让前驱节点的右指针指向当前节点
pre.setRight(node);
//修改前驱节点的右指针的类型为1,指向当前节点
pre.setRightType(1);
}
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
//3.再线索化右子树
infixThreadedNodes(node.getRight());
}
/**
* 后序线索二叉树
*
* @param node 当前需要线索化的节点
*/
public void suffixThreadedNodes(Node node) {
if (node == null) {
System.out.println("节点为空,不能线索化"); //n+1个空指针域
return;
}
//1.先线索化左子树
if (node.getLeftType() == 0) {
suffixThreadedNodes(node.getLeft());
}
//3.再线索化右子树
if (node.getRightType() == 0) {
suffixThreadedNodes(node.getRight());
}
//2.线索化当前节点
//2.1.先处理当前节点的前驱节点
if (node.getLeft() == null) { //当前节点的左子树为空
//让当前节点的左指针指向前驱节点
node.setLeft(pre);
//修改当前节点的左指针的类型为1,指向前驱节点
node.setLeftType(1);
}
//2.2.再处理当前节点的后继节点
if (pre != null && pre.getRight() == null) { //当前节点的右子树为空
//让前驱节点的右指针指向当前节点
pre.setRight(node);
//修改前驱节点的右指针的类型为1,指向当前节点
pre.setRightType(1);
}
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
}
//1.1.遍历前序线索二叉树:**先输出父节点**,再遍历左子树,再遍历右子树
public void prefixOrder() {
Node node = root; //临时变量,存储当前遍历的节点,从根节点开始
while (node != null) {
//循环找到leftType == 1的节点,第一个找到的就是节点8,后面随着遍历而变化,因为当leftType == 1的时候,说明该节点是按照线索化处理后的有效节点
while (node.getLeftType() == 0) {
System.out.println("当前节点:" + node);
node = node.getLeft();
}
System.out.println("当前节点:" + node);
//如果当前节点的右指针指向的是后继节点,就一直输出
// while (node.getRightType() == 1) {
// //获取到当前节点的后继节点
// node = node.getRight();
// System.out.println("当前节点:" + node);
// }
//替换遍历的节点
node = node.getRight();
}
}
//1.2.遍历中序线索二叉树:先遍历左子树,**再输出父节点**,再遍历右子树
public void infixOrder() {
Node node = root; //临时变量,存储当前遍历的节点,从根节点开始
while (node != null) {
//循环找到leftType == 1的节点,第一个找到的就是节点8,后面随着遍历而变化,因为当leftType == 1的时候,说明该节点是按照线索化处理后的有效节点
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();
}
}
//1.3.遍历后序线索二叉树:先遍历左子树,再遍历右子树,**再输出父节点**
public void suffixOrder() {
Node node = root; //临时变量,存储当前遍历的节点,从根节点开始
while (node != null) {
//循环找到leftType == 1的节点,第一个找到的就是节点8,后面随着遍历而变化,因为当leftType == 1的时候,说明该节点是按照线索化处理后的有效节点
while (node.getLeftType() == 0) {
node = node.getLeft();
}
pre = null;
while (node != null) {
//如果当前节点的右指针指向的是后继节点,就一直输出
if (node.getRightType() == 1) {
System.out.println("当前节点:" + node);
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
//获取到当前节点的后继节点
node = node.getRight();
} else {
if (node.getRight() == pre) {
System.out.println("当前节点:" + node);
if (node == root) {
return;
}
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
node = root;
} else {
//替换遍历的节点
node = node.getRight();
while (node != null && node.getLeftType() != 1) {
node = node.getLeft();
}
}
}
}
}
}
}
//创建Node节点
class Node {
private int no; //编号
private String name; //姓名
private Node left; //指向左子节点的索引,默认null
private Node right; //指向右子节点的索引,默认null
private int leftType; //0指向左子树,1指向前驱节点
private int rightType; //0指向右子树,1指向后继节点
public Node(int no, String name) {
this.no = no;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
@Override
public String toString() {
return "HeroNode{no=" + no + ", name=" + name + '}';
}
}
4.堆排序#
1.堆排序基本介绍#
-
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
-
堆是具有以下性质的完全二叉树:
- 每个节点的值都大于或等于其左右子节点的值,称为大顶堆
- 每个节点的值都小于或等于其左右子节点的值,称为小顶堆
- 注意:没有要求节点的左子节点的值和右子节点的值的大小关系
-
大顶堆举例说明
对堆中的节点按层进行编号,映射到数组中就是:
- 大顶堆特点:
arr[i] >= arr[2 * i + 1] && arr[i] >= arr[2 * i + 2] //i对应第几个节点,i从0开始编号
- 大顶堆特点:
-
小顶堆举例说明
对堆中的节点按层进行编号,映射到数组中就是:
- 小顶堆特点:
arr[i] <= arr[2 * i + 1] && arr[i] <= arr[2 * i + 2] //i对应第几个节点,i从0开始编号
- 小顶堆特点:
-
一般升序采用大顶堆,降序采用小顶堆
2.堆排序基本思想#
- 将待排序序列构造成一个大顶堆(数组,把树以数组的形式存放)
- 此时,整个序列的最大值就是堆顶的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余 n-1个元素重新构造成一个堆,这样会得到 n-1 个元素的次小值。如此反复执行,便能得到一个有序序列了
- 在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
3.堆排序步骤图解说明#
-
对数组
{4, 6, 8, 5, 9}
使用堆排序,将数组升序排序-
思路分析:
-
构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)
-
给定无序序列结构如下:
对堆中的节点按层进行编号,映射到数组中就是:
-
此时从最后一个非叶子节点开始(叶子节点自然不用调整,第一个非叶子节点
arr.length / 2 - 1 = 5 / 2 - 1 = 1
,就是上面的节点6),从左到右,从下到上进行调整对堆中的节点按层进行编号,映射到数组中就是:
-
找到第二个非叶节点4,由于
{4, 9, 8}
中9最大,4和9交换对堆中的节点按层进行编号,映射到数组中就是:
-
这时,交换导致了子根
{4, 5, 6}
结构混乱,继续调整,{4, 5, 6}
中6最大,交换4和6,此时,就将一个无序序列构造成了一个大顶堆对堆中的节点按层进行编号,映射到数组中就是:
-
-
将堆顶元素与末尾元素进行交换,使末尾元素最大,然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、调整,直到整个序列有序
-
将堆顶元素9和末尾元素4进行交换
对堆中的节点按层进行编号,映射到数组中就是:
-
重新调整结构,使其继续满足堆定义
对堆中的节点按层进行编号,映射到数组中就是:
-
再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
对堆中的节点按层进行编号,映射到数组中就是:
-
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
对堆中的节点按层进行编号,映射到数组中就是:
-
-
-
/**
* 对数组`{4, 6, 8, 5, 9}`使用堆排序,将数组升序排序
* 堆排序的速度非常快,堆排序的时间复杂度:O(nlogn)
*/
public class D_HeapSort {
public static void main(String[] args) {
// int[] arr = {4, 6, 8, 5, 9};
// System.out.println("堆排序");
// heapSort(arr);
// System.out.println(Arrays.toString(arr));
System.out.println("堆排序的时间复杂度:O(nlogn)");
//创建80000个随机数的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000)的数
}
long startData = System.currentTimeMillis();
heapSortSimplify(arr);
long endData = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (endData - startData)); //8
}
//堆排序
public static void heapSort(int[] arr) {
//1.**构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)**
adjustmentHeap(arr, 1, arr.length);
System.out.println("第一次排序:" + Arrays.toString(arr));
adjustmentHeap(arr, 0, arr.length);
System.out.println("第二次排序:" + Arrays.toString(arr));
int temp = 0; //临时变量,用于交换
//2.**将堆顶元素与末尾元素进行交换,使末尾元素最大,然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、调整,直到整个序列有序**
for (int j = arr.length - 1; j > 0; j--) {
//交换
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustmentHeap(arr, 0, j);
}
}
//简化堆排序
public static void heapSortSimplify(int[] arr) {
//1.**构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)**
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustmentHeap(arr, i, arr.length);
}
int temp = 0; //临时变量,用于交换
//2.**将堆顶元素与末尾元素进行交换,使末尾元素最大,然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、调整,直到整个序列有序**
for (int j = arr.length - 1; j > 0; j--) {
//将堆顶元素与末尾元素进行交换
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustmentHeap(arr, 0, j);
}
}
/**
* 将一个数组(二叉树)调整成一个大顶堆
* 完成将以i对应的非叶子节点的树调整成大顶堆:
* {4, 6, 8, 5, 9}=》i=1=》adjustmentHeap=》{4, 9, 8, 5, 6}
* =》i=0=》adjustmentHeap=》{9, 6, 8, 5, 4}
*
* @param arr 数组
* @param i 非叶子节点在数组中的索引
* @param length 对多少个元素进行调整(逐渐减少)
*/
public static void adjustmentHeap(int[] arr, int i, int length) {
int temp = arr[i]; //临时变量,保存当前元素的值
//k = 2 * i + 1:指向i节点的左子节点
for (int k = 2 * i + 1; k < length; k = 2 * i + 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;
}
}
//循环结束后,已经将以i为父节点的树的最大值,放在了最顶,局部构成大顶堆
arr[i] = temp; //将temp的值放到调整后的位置
}
}
5.赫夫曼树#
1.基本介绍#
- 给定n个权值作为n个叶子节点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree),还有的书翻译为霍夫曼树
- 赫夫曼树是带权路径长度最短的树,权值较大的节点离根较近
2.赫夫曼树几个重要概念和举例说明#
-
路径和路径长度:在一棵树中,从一个节点往下可以达到的子节点或孙子节点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根节点到第L层节点的路径长度为L-1
-
节点的权及带权路径长度:若将树中节点赋给一个有着某种含义的数值,则这个数值称为该节点的权。节点的带权路径长度为:从根节点到该节点之间的路径长度与该节点的权的乘积
-
树的带权路径长度(wpl):树的带权路径长度规定为所有叶子节点的带权路径长度之和,记为WPL(weighted path length),权值越大的节点离根节点越近的二叉树才是最优二叉树
-
WPL最小的就是赫夫曼树(最优二叉树)
$wpl=132+72+82+32=62$
赫夫曼树:$wpl=131+82+73+33=59$
$wpl=71+32+83+133=76$
3.赫夫曼树步骤图解说明#
-
给数列
{13, 7, 8, 3, 29, 6, 1}
转成一颗赫夫曼树 -
构成赫夫曼树的步骤:
- 从小到大进行排序,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1,2,3,4的步骤,直到数列中所有的数据都被处理,就得到一颗赫夫曼树
/** * 给数列`{13, 7, 8, 3, 29, 6, 1}`转成一颗赫夫曼树 */ public class E_HuffmanTree { public static void main(String[] args) { int[] arr = {13, 7, 8, 3, 29, 6, 1}; Point root = huffmanTree(arr); System.out.println("前序遍历:"); prefixOrder(root); } /** * 赫夫曼树 * * @param arr 需要转成一颗赫夫曼树的数列 * @return 转成一颗赫夫曼树的根节点 */ public static Point huffmanTree(int[] arr) { //1. 从小到大进行排序,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树 List<Point> points = new ArrayList<>(); //遍历arr数组 for (int value : arr) { //将arr数组的每个元素构成一个Point,放入到List集合中,便于管理 points.add(new Point(value)); } //4. 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1,2,3,4的步骤,直到数列中所有的数据都被处理,就得到一颗赫夫曼树 while (points.size() > 1) { //从小到大进行排序 Collections.sort(points); System.out.println("points:" + points); //2. 取出根节点权值最小的两颗二叉树 //取出权值最小的节点(一个节点可以看成最简单的左右子节点为空的二叉树) Point leftPoint = points.get(0); //取出权值第二小的节点 Point rightPoint = points.get(1); //3. 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和 Point parent = new Point(leftPoint.value + rightPoint.value); parent.left = leftPoint; parent.right = rightPoint; //从List集合中删除处理过的二叉树 points.remove(leftPoint); points.remove(rightPoint); //将parent加入到points points.add(parent); } //返回赫夫曼树的根节点 return points.get(0); } //1.2.前序遍历:**先输出父节点**,再遍历左子树和右子树 public static void prefixOrder(Point root) { if (root != null) { root.prefixOrder(); } else { System.out.println("当前二叉树为空"); } } } //创建Point节点,为了让Point支持排序,需要实现Comparable接口 class Point implements Comparable<Point> { public int value; //节点权值 public Point left; //指向左子节点的索引,默认null public Point right; //指向右子节点的索引,默认null public Point(int value) { this.value = value; } @Override public String toString() { return "Point{value=" + value + '}'; } @Override public int compareTo(Point o) { //对权值进行从小到大比较 return this.value - o.value; } //1.2.前序遍历:**先输出父节点**,再遍历左子树和右子树 public void prefixOrder() { //1.2.1. 先输出当前节点(初始是根节点) System.out.println(this); //1.2.2. 如果左子节点不为空,则递归继续前序遍历 if (this.left != null) { this.left.prefixOrder(); } //1.2.3. 如果右子节点不为空,则递归继续前序遍历 if (this.right != null) { this.right.prefixOrder(); } } }
6.赫夫曼编码#
1.基本介绍#
- 赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法
- 赫夫曼编码是赫夫曼树在电讯通信中的经典的应用之一(利用赫夫曼树的特性形成的一种编码)
- 赫夫曼编码广泛地用于数据文件压缩和解压。其压缩率通常在20%~90%之间(如果文本重复数据越多压缩率越高)
- 赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
2.原理剖析及其图解#
1.通信领域中信息的处理方式1-定长编码#
- i like like like java do you like a java //共40个字符(包括空格)
- 105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 //对应Ascii码的十进制
- 01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应计算机的二进制
- 完全按照原始的数据进行编码,总的长度是359(包括空格)
2.通信领域中信息的处理方式2-变长编码#
-
i like like like java do you like a java // 共40个字符(包括空格)
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 //各个字符出现的次数
-
0= , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d
说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如:空格出现了9次,编码为0,其它依次类推
-
按照上面给各个字符规定的编码,则我们在传输"i like like like java do you like a java"数据时,编码就是1001011010011010110100110101101001101000111110101111001010110100101011010011010100011111
-
这种方式会造成匹配的多义性,因为不是前缀编码(字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码)(编码不要有二义性)
3.通信领域中信息的处理方式3-赫夫曼编码#
-
思路分析:
-
获取字符串:i like like like java do you like a java //共40个字符(包括空格)
-
统计各个字符出现的次数:d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 //各个字符出现的次数
-
按照上面字符出现的次数构建一颗赫夫曼树,出现的次数作为权值
- 从小到大进行排序,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1,2,3,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
-
按照上面的赫夫曼编码,我们的"i like like like java do you like a java"字符串对应的编码为:(注意:这里使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
-
说明:
- 通过赫夫曼编码处理后长度为:133,原来长度是359,压缩了$(359-133)/359*100%=62.9%$
- 此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
- 赫夫曼编码是无损处理方案
-
-
注意:赫夫曼树根据排序方法不同,有可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl是一样的,都是最小的,最后生成的赫夫曼编码的长度是一样的,比如:如果让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的赫夫曼树为:
3.实际应用#
1.数据压缩#
-
将给出的一段文本,比如"i like like like java do you like a java",根据赫夫曼编码原理,对其进行数据压缩处理,形式如:1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
-
思路分析:
- 根据赫夫曼编码压缩数据的原理,需要创建"i like like like java do you like a java"对应的赫夫曼树(创建赫夫曼树)
- 构建一个新的节点(包含属性:data(存放数据),weight(权值),left(左子节点),right(右子节点))
- 得到字符串"i like like like java do you like a java"对应的
byte[]
数组 - 将准备构建赫夫曼树的节点放到
List
集合中,体现d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 //各个字符出现的次数 - 通过
List
集合创建对应的赫夫曼树- 从小到大进行排序,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1,2,3,4的步骤,直到数列中所有的数据都被处理,就得到一颗赫夫曼树
- 生成赫夫曼编码和赫夫曼编码后的数据
- 生成赫夫曼树对应的赫夫曼编码,形式如: =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
- 使用赫夫曼编码来生成赫夫曼编码数据,即按照上面的赫夫曼编码,将"i like like like java do you like a java"字符串生成对应的编码数据,形式如:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
- 根据赫夫曼编码压缩数据的原理,需要创建"i like like like java do you like a java"对应的赫夫曼树(创建赫夫曼树)
2.数据解压#
- 解码过程,就是编码的一个逆向操作
- 思路分析:
- 将赫夫曼编码生成的赫夫曼编码
byte[] = [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
数组重新转成赫夫曼编码对应的二进制的字符串,形式如:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100 - 将赫夫曼编码对应的二进制的字符串重新转成字符串"i like like like java do you like a java"
- 将赫夫曼编码生成的赫夫曼编码
3.文件压缩#
- 要求:对一个图片文件进行无损压缩,看看压缩效果如何
- 思路分析:
- 读取文件
- 得到赫夫曼编码表
- 完成压缩
- 注意事项:
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化(视频,ppt等文件)
- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件(图片)、文本文件)
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显
4.文件解压(文件恢复)#
- 要求:将压缩的文件,重新恢复成原来的文件
- 思路分析:
- 读取压缩文件
- 得到赫夫曼编码表
- 完成解压(文件恢复)
public class F_HuffmanCodeDataCompression {
public static void main(String[] args) {
//1.1.2. 得到字符串"i like like like java do you like a java"对应的`byte[]`数组
String str = "i like like like java do you like a java"; //共40个字符(包括空格)
System.out.println("字符串对应的byte[]字节数组的长度:");
byte[] strBytes = str.getBytes();
System.out.println(strBytes.length); //40个字符
System.out.println("数据压缩:");
byte[] huffmanBytes = zipData(strBytes);
System.out.println("数据解压:");
byte[] bytes = unZipData(huffmanCodes, huffmanBytes);
System.out.println(new String(bytes));
System.out.println("文件压缩:");
zipFile("D:\\笔记\\JavaSE\\java.png", "D:\\笔记\\JavaSE\\java.zip");
System.out.println("文件解压:");
unZipFile("D:\\笔记\\JavaSE\\java.zip", "D:\\笔记\\JavaSE\\javaunzip.png");
}
//前序遍历:**先输出父节点**,再遍历左子树和右子树
public static void prefixOrder(Nodu root) {
if (root != null) {
root.prefixOrder();
} else {
System.out.println("当前二叉树为空");
}
}
//1. 数据压缩
//1.1. 根据赫夫曼编码压缩数据的原理,需要创建"i like like like java do you like a java"对应的赫夫曼树(创建赫夫曼树)
/**
* 1.1.3. 将准备构建赫夫曼树的节点放到`List`集合中,体现d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 //各个字符出现的次数
*
* @param bytes 字节数组
* @return 节点集合
*/
private static List<Nodu> getNodus(byte[] bytes) {
//创建一个ArrayList,存储节点
List<Nodu> nodus = new ArrayList<>();
//使用Map<数据, 出现次数>统计每个byte出现的次数
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);
}
}
//把Map转成一个Nodu对象,并加入到List集合
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodus.add(new Nodu(entry.getKey(), entry.getValue()));
}
return nodus;
}
//1.1.4. 通过`List`集合创建对应的赫夫曼树
public static Nodu huffmanTree(List<Nodu> nodus) {
//1.1.4.4. 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1,2,3,4的步骤,直到数列中所有的数据都被处理,就得到一颗赫夫曼树
while (nodus.size() > 1) {
//1.1.4.1. 从小到大进行排序,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
//从小到大进行排序
Collections.sort(nodus);
//1.1.4.2. 取出根节点权值最小的两颗二叉树
//取出权值最小的节点(一个节点可以看成最简单的左右子节点为空的二叉树)
Nodu leftNodu = nodus.get(0);
//取出权值第二小的节点
Nodu rightNodu = nodus.get(1);
//1.1.4.3. 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
//创建一颗新的二叉树,根节点没有data,只有权值(所有的字符都放在叶子节点)
Nodu parent = new Nodu(null, leftNodu.weight + rightNodu.weight);
parent.left = leftNodu;
parent.right = rightNodu;
//从List集合中删除处理过的二叉树
nodus.remove(leftNodu);
nodus.remove(rightNodu);
//将parent加入到nodus
nodus.add(parent);
}
//返回赫夫曼树的根节点
return nodus.get(0);
}
//1.2. 生成赫夫曼编码和赫夫曼编码后的数据
//将赫夫曼编码表存放在Map<字符, 路径>中
static Map<Byte, String> huffmanCodes = new HashMap<>();
//在生成赫夫曼编码表时,需要拼接路径,定义一个StringBuilder存储叶子节点的路径
static StringBuilder path = new StringBuilder();
/**
* 1.2.1. 生成赫夫曼树对应的赫夫曼编码,形式如: =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
* 将传入的Nodu节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中
*
* @param nodu 节点,默认从根节点开始
* @param code 节点对应的路径的值:左子节点是0,右子节点是1
* @param stringBuilder 拼接路径
*/
public static void getHuffmanCoding(Nodu nodu, String code, StringBuilder stringBuilder) {
//临时变量,将节点对应的路径的值加入到临时变量中
StringBuilder stringBuilderTemp = new StringBuilder(stringBuilder);
stringBuilderTemp.append(code);
if (nodu != null) { //如果节点为空,不处理
//判断当前节点是叶子节点还是非叶子节点
if (nodu.date == null) { //非叶子节点
//向左递归
getHuffmanCoding(nodu.left, "0", stringBuilderTemp);
//向右递归
getHuffmanCoding(nodu.right, "1", stringBuilderTemp);
} else { //叶子节点
//找到某个叶子节点的最后
huffmanCodes.put(nodu.date, stringBuilderTemp.toString());
}
}
}
/**
* 1.2.2. 使用赫夫曼编码来生成赫夫曼编码数据,即按照上面的赫夫曼编码,将"i like like like java do you like a java"字符串生成对应的编码数据,形式如:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
* 将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码表压缩后的byte[]数组
*
* @param bytes 原始的字符串对应的byte[]数组
* @param huffmanCodes 生成的赫夫曼树对应的赫夫曼编码
* @return 赫夫曼编码表压缩后的byte[]数组(即8位对应一个byte,放入到byte[] huffmanCodesBytes数组中)
* huffmanCodesBytes[0] = 10101000(补码) =》byte(10101000(补码)-1=》10100111(反码)=》11011000(原码)=-88)
*/
public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//利用赫夫曼编码表huffmanCodes将原始bytes转成赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes数组
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println("编码数据:" + stringBuilder);
//将编码数据转成byte[]数组
//统计返回的赫夫曼编码huffmanCodesBytes对应的长度
// int len = (stringBuilder.length() + 7) / 8;
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//创建存储压缩后的byte数组
byte[] huffmanCodesBytes = new byte[len];
int index = 0; //计数器,记录第几个byte
for (int i = 0; i < stringBuilder.length(); i += 8) { //每8位对应一个byte
String strByte;
if (i + 8 > stringBuilder.length()) { //不够8位
strByte = stringBuilder.substring(i);
} else {
strByte = stringBuilder.substring(i, i + 8);
}
//将strByte转成一个byte,放入到huffmanCodesBytes
huffmanCodesBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanCodesBytes;
}
/**
* 1.数据压缩
*
* @param bytes 原始字符串对应的字节数组
* @return 经过赫夫曼编码处理后的字节数组
*/
public static byte[] zipData(byte[] bytes) {
System.out.println("将准备构建赫夫曼树的节点放到List集合中,并统计各个字符出现的次数:");
List<Nodu> nodus = getNodus(bytes);
System.out.println(nodus);
System.out.println("创建赫夫曼树:");
Nodu root = huffmanTree(nodus);
System.out.println(root);
System.out.println("前序遍历:");
prefixOrder(root);
System.out.println("生成赫夫曼树对应的赫夫曼编码:");
getHuffmanCoding(root, "", path);
System.out.println(huffmanCodes);
System.out.println("使用赫夫曼编码来生成赫夫曼编码数据:");
byte[] zip = zip(bytes, huffmanCodes);
System.out.println(Arrays.toString(zip)); //17个数字(压缩率:(40-17)/40*100%=57.5%)
return zip;
}
//2.数据解压(解码过程,就是编码的一个逆向操作)
/**
* 2.1. 将赫夫曼编码生成的赫夫曼编码`byte[] = [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]`数组重新转成赫夫曼编码对应的二进制的字符串,形式如:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
*
* @param flag 标识符,是否需要补高位,true是需要,false是不需要(最后一个字节不需要补高位)
* @param b 字符
* @return 字符对应的二进制字符串(按补码返回,因为是按补码编码的)
*/
public static String byteToBitString(boolean flag, byte b) {
int temp = b; //使用临时变量保存b转成int
if (flag) {
temp |= 256; //正数需要补高位(256:1 0000 0000 | 1:0000 0001=》100000001)
}
String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
// System.out.println(str);
if (flag) {
return str.substring(str.length() - 8); //取后8位
} else {
return str; //不够8位
}
}
/**
* 2.2. 将赫夫曼编码对应的二进制的字符串重新转成字符串"i like like like java do you like a java"
*
* @param huffmanCodes 赫夫曼树对应的赫夫曼编码
* @param huffmanBytes 赫夫曼编码对应的byte[]数组
* @return 原来字符串对应的数组
*/
public static byte[] unZipData(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
//得到huffmanBytes对应的二进制字符串
StringBuilder stringBuilder = new StringBuilder();
//将byte[]数组转成二进制字符串
for (int i = 0; i < huffmanBytes.length; i++) {
//判断是不是最后一个字节,最后一个字节不需要补高位
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
}
System.out.println("编码数据:" + stringBuilder);
//把字符串按照指定赫夫曼编码进行解码
//把赫夫曼编码表进行调换,因为要反向查询
HashMap<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
System.out.println("反向赫夫曼编码" + map);
//创建集合存放byte
List<Byte> list = new ArrayList<>();
//for循环存放所有的字符到list
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1; //计数器
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
}
//把list中的数据放入到byte[]并返回
byte[] bytes = new byte[list.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = list.get(i);
}
return bytes;
}
/**
* 3.文件压缩
*
* @param srcFile 准备压缩文件的全路径
* @param dstFile 压缩后文件的全路径
*/
public static void zipFile(String srcFile, String dstFile) {
FileInputStream fis = null;
FileOutputStream fos = null;
ObjectOutputStream oos = null;
try {
//创建文件输入流(读取文件)
fis = new FileInputStream(srcFile);
//创建一个和源文件大小一样的byte[]数组(读取数据)
byte[] b = new byte[fis.available()];
//读取文件
fis.read(b);
//使用赫夫曼编码进行编码:直接对源文件压缩,获取文件对应的赫夫曼编码表
byte[] huffmanBytes = zipData(b);
//创建文件输出流(存放压缩文件)
fos = new FileOutputStream(dstFile);
//创建一个和文件输出流关联的对象输出流
oos = new ObjectOutputStream(fos);
//把赫夫曼编码后的字节数组写入压缩文件
oos.writeObject(huffmanBytes);
//以对象流的方式写入赫夫曼编码,是为了恢复源文件时使用
//一定要把赫夫曼编码写入压缩文件
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
assert oos != null;
oos.close();
assert fos != null;
fos.close();
assert fis != null;
fis.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
/**
* 4.文件解压
*
* @param zipFile 准备解压文件的全路径
* @param dstFile 解压后文件的全路径
*/
public static void unZipFile(String zipFile, String dstFile) {
FileInputStream fis = null;
ObjectInputStream ois = null;
FileOutputStream fos = null;
try {
//创建文件输入流
fis = new FileInputStream(zipFile);
//创建和文件输入流关联的对象输入流
ois = new ObjectInputStream(fis);
//读取byte[]数组
byte[] huffmanBytes = (byte[]) ois.readObject();
//读取赫夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
//解码
byte[] bytes = unZipData(huffmanCodes, huffmanBytes);
//创建文件输出流(存放文件)
fos = new FileOutputStream(dstFile);
//将bytes写入到目标文件
fos.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
assert fos != null;
fos.close();
assert ois != null;
ois.close();
assert fis != null;
fis.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
}
//1.1.1. 构建一个新的节点(包含属性:data(存放数据),weight(权值),left(左子节点),right(右子节点))
class Nodu implements Comparable<Nodu> {
Byte date; //存放数据(字符本身,按Ascii码存)
int weight; //权值(字符出现的次数)
public Nodu left; //指向左子节点的索引,默认null
public Nodu right; //指向右子节点的索引,默认null
public Nodu(Byte date, int weight) {
this.date = date;
this.weight = weight;
}
@Override
public String toString() {
return "Nodu{date=" + date + ", weight=" + weight + '}';
}
@Override
public int compareTo(Nodu o) {
//对权值进行从小到大比较
return this.weight - o.weight;
}
//前序遍历:**先输出父节点**,再遍历左子树和右子树
public void prefixOrder() {
//先输出当前节点(初始是根节点)
System.out.println(this);
//如果左子节点不为空,则递归继续前序遍历
if (this.left != null) {
this.left.prefixOrder();
}
//如果右子节点不为空,则递归继续前序遍历
if (this.right != null) {
this.right.prefixOrder();
}
}
}
7.二叉排序树(BST树)#
1.先看一个需求引出二叉排序树#
-
对数列
{7, 3, 10, 12, 5, 1, 9}
,要求能够高效的完成对数据的查询和添加 -
解决方案分析:
-
使用数组
- 数组未排序:
- 优点:直接在数组尾添加,速度快
- 缺点:查找速度慢
- 数组排序:
- 优点:可以使用二分查找,速度快
- 缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢
- 数组未排序:
-
使用链式存储-链表
- 优点:添加数据速度比数组快,不需要数据整体移动
- 缺点:不管链表是否有序,查找速度都慢
-
使用二叉排序树
-
2.二叉排序树介绍#
- 二叉排序树:BST(Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大
- 特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点(尽量避免)
3.二叉排序树的应用#
1.创建和遍历#
-
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树(使用中序遍历出来是有序的),比如数组
{7, 3, 10, 12, 5, 1, 9, 2}
,创建成对应的二叉排序树为:
2.二叉排序树的删除#
- 二叉排序树的删除情况比较复杂,有下面三种情况需要考虑:
- 删除叶子节点,即该节点下没有左右子节点(比如:2, 5, 9, 12)
- 找到要删除的节点
targetHero
- 找到要删除的节点
targetHero
的父节点parent
(是否存在父节点) - 确定
targetHero
是父节点parent
的左子节点parent.left = null;
还是右子节点parent.right = null;
来对应删除
- 找到要删除的节点
- 删除只有一颗子树的节点,即该节点有左子节点或者右子节点(比如:1)
- 找到要删除的节点
targetHero
- 找到要删除的节点
targetHero
的父节点parent
(是否存在父节点) - 确定
targetHero
的子节点是左子节点还是右子节点 - 确定
targetHero
是parent
的左子节点还是右子节点 - 如果
targetHero
有左子节点:targetHero
是parent
的左子节点parent.left = targetHero.left;
targetHero
是parent
的右子节点parent.right = targetHero.left;
- 如果
targetHero
有右子节点:targetHero
是parent
的左子节点parent.left = targetHero.right;
targetHero
是parent
的右子节点parent.right = targetHero.right;
- 找到要删除的节点
- 删除有两颗子树的节点,即该节点有左子节点和右子节点(比如:7, 3, 10)
- 找到要删除的节点
targetHero
- 找到要删除的节点
targetHero
的父节点parent
(是否存在父节点) - 从
targetHero
的右子树找到最小的节点(或者从左子树找到最大的节点) - 用一个临时变量将最小节点的值保存
- 删除该最小节点
- 将最小节点的值赋给
targetHero
:targetHero.value = min;
- 找到要删除的节点
- 删除叶子节点,即该节点下没有左右子节点(比如:2, 5, 9, 12)
/**
* 二叉排序树的应用
*/
public class G_BinarySortTree {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
BinarySortTree binarySortTree = new BinarySortTree();
//循环的添加节点到二叉排序树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Hero(arr[i]));
}
System.out.println("中序遍历二叉排序树:");
binarySortTree.infixOrder();
System.out.println("删除叶子节点(比如:2, 5, 9, 12):");
// binarySortTree.delete(2);
// binarySortTree.infixOrder();
System.out.println("删除只有一颗子树的节点(比如:1):");
// binarySortTree.delete(1);
// binarySortTree.infixOrder();
System.out.println("删除有两颗子树的节点(比如:7, 3, 10):");
// binarySortTree.delete(7);
// binarySortTree.infixOrder();
System.out.println("删除全部的节点:");
// binarySortTree.delete(2);
// binarySortTree.delete(5);
// binarySortTree.delete(9);
// binarySortTree.delete(12);
// binarySortTree.delete(7);
// binarySortTree.delete(3);
// binarySortTree.delete(10);
// binarySortTree.delete(1);
// binarySortTree.infixOrder();
}
}
//二叉排序树
class BinarySortTree {
private Hero root;
//添加节点
public void add(Hero hero) {
if (root == null) { //如果根节点为空,直接将当前节点添加到根节点位置
root = hero;
} else {
root.add(hero);
}
}
//1.3.中序遍历:先遍历左子树,**再输出父节点**,再遍历右子树
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("当前二叉排序树为空");
}
}
//1.找到要删除的节点`targetHero`
public Hero search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
//2.找到要删除的节点`targetHero`的父节点`parent`(是否存在父节点)
public Hero searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 3.从`targetHero`的右子树找到最小的节点(或者从左子树找到最大的节点)
* 5.删除该最小节点
*
* @param hero 当作当前二叉排序树的根节点
* @return 返回以hero为根节点的二叉排序树的最小节点的值
*/
public int deleteRightTreeMin(Hero hero) {
//4. 用一个临时变量将最小节点的值保存
Hero target = hero; //临时变量,存放最小值
//3.从`targetHero`的右子树找到最小的节点(或者从左子树找到最大的节点)
//循环的查找左节点,就会找到最小值
while (target.left != null) {
target = target.left; //target指向最小节点
}
//5.删除该最小节点
delete(target.value);
return target.value;
}
//删除节点
public void delete(int value) {
if (root == null) {
return;
} else {
//1.找到要删除的节点`targetHero`
Hero targetHero = search(value);
//如果没有找到要删除的节点
if (targetHero == null) {
return;
}
//2.找到要删除的节点`targetHero`的父节点`parent`(是否存在父节点)
//如果当前这颗二叉排序树只有一个节点(父节点不存在)
if (root.left == null && root.right == null) {
root = null; //删除根节点
return;
}
//找到要删除的节点`targetHero`的父节点`parent`
Hero parent = searchParent(value);
//**删除叶子节点**,即该节点下没有左右子节点(比如:2, 5, 9, 12)
if (targetHero.left == null && targetHero.right == null) {
//3.确定`targetHero`是父节点`parent`的左子节点`parent.left = null;`还是右子节点`parent.right = null;`来对应删除
if (parent.left != null && parent.left.value == value) { //targetHero是父节点的左子节点
parent.left = null;
} else if (parent.right != null && parent.right.value == value) { //targetHero是父节点的右子节点
parent.right = null;
}
//**删除有两颗子树的节点**,即该节点有左子节点和右子节点(比如:7, 3, 10)
} else if (targetHero.left != null && targetHero.right != null) {
//3. 从`targetHero`的右子树找到最小的节点(或者从左子树找到最大的节点)
//4. 用一个临时变量将最小节点的值保存
//5. 删除该最小节点
int min = deleteRightTreeMin(targetHero.right);
//6. 将最小节点的值赋给`targetHero`:`targetHero.value = min;`
targetHero.value = min;
//**删除只有一颗子树的节点**,即该节点有左子节点或者右子节点(比如:1)
} else {
//3. 确定`targetHero`的子节点是左子节点还是右子节点
//4. 确定`targetHero`是`parent`的左子节点还是右子节点
//5. 如果`targetHero`有左子节点:
if (targetHero.left != null) {
if (parent != null) {
//5.1. `targetHero`是`parent`的左子节点`parent.left = targetHero.left;`
if (parent.left.value == value) {
parent.left = targetHero.left;
//5.2. `targetHero`是`parent`的右子节点`parent.right = targetHero.left;`
} else {
parent.right = targetHero.left;
}
} else {
root = targetHero.left;
}
//6. 如果`targetHero`有右子节点:
} else {
if (parent != null) {
//6.1. `targetHero`是`parent`的左子节点`parent.left = targetHero.right;`
if (parent.left.value == value) {
parent.left = targetHero.right;
//6.2. `targetHero`是`parent`的右子节点`parent.right = targetHero.right;`
} else {
parent.right = targetHero.right;
}
} else {
root = targetHero.right;
}
}
}
}
}
}
//创建Hero节点
class Hero {
public int value; //编号
public Hero left; //指向左子节点的索引,默认null
public Hero right; //指向右子节点的索引,默认null
public Hero(int value) {
this.value = value;
}
@Override
public String toString() {
return "Hero{value=" + value + '}';
}
//递归的形式添加节点,需要满足二叉树的要求
public void add(Hero hero) {
if (hero == null) {
return;
}
//判断传入的节点的值和当前子树的根节点的值的关系
if (hero.value < this.value) { //添加的节点的值小于当前节点的值
if (this.left == null) { //当前节点左子节点为空,直接添加在左子节点
this.left = hero;
} else {
this.left.add(hero); //向左子树递归添加
}
} else { //添加的节点的值大于等于当前节点的值
if (this.right == null) { //当前节点右子节点为空,直接添加在右子节点
this.right = hero;
} else {
this.right.add(hero); //向右子树递归添加
}
}
}
//1.3.中序遍历:先遍历左子树,**再输出父节点**,再遍历右子树
public void infixOrder() {
//1.3.1. 如果左子节点不为空,则递归继续中序遍历
if (this.left != null) {
this.left.infixOrder();
}
//1.3.2. 输出当前节点
System.out.println(this);
//1.3.3. 如果右子节点不为空,则递归继续中序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
/**
* 1.找到要删除的节点`targetHero`
*
* @param value 要删除的节点的值
* @return 返回要删除的节点,否则返回null
*/
public Hero 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);
}
}
/**
* 2.找到要删除的节点`targetHero`的父节点`parent`(是否存在父节点)
*
* @param value 要查找的值
* @return 返回要删除的节点的父节点,否则返回null
*/
public Hero 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;
}
}
}
}
8.平衡二叉树(AVL树)#
1.看一个案例(说明二叉排序树可能的问题)#
-
根据数列
{1, 2, 3, 4, 5, 6}
,创建一颗二叉排序树(BST),并分析问题所在 -
二叉排序树存在的问题分析:
- 左子树全部为空,从形式上看,更像一个单链表
- 插入速度没有影响
- 查询速度明显降低(因为需要依次比较),不能发挥二叉排序树的优势,因为每次还需要判断左子树是否为空,其查询速度比单链表还慢
- 解决方案-平衡二叉树(AVL)
2.基本介绍#
-
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高
-
特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL算法、替罪羊树、Treap、伸展树等
-
平衡二叉树的前提是首先是一个二叉排序树,在二叉排序树的基础上实现的
-
举例说明,看看下面哪些AVL树,为什么?
①左子树高度为2,右子树高度为1,高度差的绝对值不超过1(是)
②左子树高度为2,右子树高度为2,高度差的绝对值不超过1(是)
③左子树高度为3,右子树高度为1,高度差的绝对值超过1(不是)
3.应用案例#
1.单旋转(左旋转)#
-
右子树的高度比左子树高度高的时候使用,目的是降低右子树的高度
-
要求:给数列
{4, 3, 6, 5, 7, 8}
创建出对应的平衡二叉树 -
对节点A进行左旋转的步骤
- 将A节点的右节点的左节点,指向A节点
- 将A节点的右节点,指向A节点的右节点的左节点
-
思路分析:
- 创建一个新的节点,以当前根节点的值作为新节点的值
- 把新节点的左子树设置为当前节点的左子树
- 把新节点的右子树设置为当前节点的右子树的左子树
- 把当前节点的值换为右子节点的值
- 把当前节点的右子树设置为当前节点的右子树的右子树
- 把当前节点的左子树设置为新节点
左旋转前:左子树高度为1,右子树高度为3
左旋转后的结果:左子树高度为2,右子树高度为2
-
根据Java垃圾回收机制,当有一个对象没有任何引用指向它的时候,这个节点就会被销毁
2.单旋转(右旋转)#
-
左子树的高度比右子树高度高的时候使用,目的是降低左子树的高度
-
要求:给数列
{10, 12, 8, 9, 7, 6}
创建出对应的平衡二叉树 -
说明:对节点A进行右旋转的步骤
- 将A节点的左节点的右节点,指向A节点
- 将A节点的左节点,指向A节点的左节点的右节点
-
思路分析:
- 创建一个新的节点,以当前根节点的值作为新节点的值
- 把新节点的右子树设置为当前节点的右子树
- 把新节点的左子树设置为当前节点的左子树的右子树
- 把当前节点的值换为左子节点的值
- 把当前节点的左子树设置为当前节点的左子树的左子树
- 把当前节点的右子树设置为新节点
右旋转前:左子树高度为3,右子树高度为1
右旋转后的结果:左子树高度为2,右子树高度为2
3.双旋转#
-
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列
{10, 11, 7, 6, 8, 9}
运行原来的代码,并没有转成AVL树 -
问题分析:
- 当符合左旋转的条件时,如果右子树的左子树的高度大于右子树的右子树的高度,先对当前节点的右子节点进行右旋转,再对当前节点进行左旋转操作
- 当符合右旋转的条件时,如果左子树的右子树的高度大于左子树的左子树的高度,先对当前节点的左子节点进行左旋转,再对当前节点进行右旋转操作
右旋转前:左子树高度为3,右子树高度为1
右旋转后的结果:左子树高度为1,右子树高度为3
双旋转后的结果:左子树高度为2,右子树高度为2
-
先对当前节点的左子节点进行左旋转
-
再对当前节点进行右旋转操作
/**
* AVL树是在二叉排序树的基础上增加功能
*/
public class H_SelfBalancingBinarySearchTree {
public static void main(String[] args) {
// int[] arr = {4, 3, 6, 5, 7, 8}; //单旋转(左旋转)
// int[] arr = {10, 12, 8, 9, 7, 6}; //单旋转(右旋转)
int[] arr = {10, 11, 7, 6, 8, 9}; //双旋转
//创建平衡二叉树
SelfBalancingBinarySearchTree selfBalancingBinarySearchTree = new SelfBalancingBinarySearchTree();
//循环的添加节点到平衡二叉树
for (int i = 0; i < arr.length; i++) {
selfBalancingBinarySearchTree.add(new Nodal(arr[i]));
}
System.out.println("中序遍历平衡二叉树:");
selfBalancingBinarySearchTree.infixOrder();
System.out.println("树的高度" + selfBalancingBinarySearchTree.getRoot().height()); //4
System.out.println("左子树的高度" + selfBalancingBinarySearchTree.getRoot().leftHeight()); //1
System.out.println("右子树的高度" + selfBalancingBinarySearchTree.getRoot().rightHeight()); //3
System.out.println("当前的根节点" + selfBalancingBinarySearchTree.getRoot());
System.out.println("根节点的左子节点" + selfBalancingBinarySearchTree.getRoot().left);
System.out.println("根节点的右子节点" + selfBalancingBinarySearchTree.getRoot().right);
}
}
//平衡二叉树
class SelfBalancingBinarySearchTree {
private Nodal root;
public Nodal getRoot() {
return root;
}
//添加节点
public void add(Nodal nodal) {
if (root == null) { //如果根节点为空,直接将当前节点添加到根节点位置
root = nodal;
} else {
root.add(nodal);
}
}
//1.3.中序遍历:先遍历左子树,**再输出父节点**,再遍历右子树
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("当前二叉排序树为空");
}
}
//1.找到要删除的节点`targetNodal`
public Nodal search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
//2.找到要删除的节点`targetNodal`的父节点`parent`(是否存在父节点)
public Nodal searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 3.从`targetNodal`的右子树找到最小的节点(或者从左子树找到最大的节点)
* 5.删除该最小节点
*
* @param nodal 当作当前二叉排序树的根节点
* @return 返回以nodal为根节点的二叉排序树的最小节点的值
*/
public int deleteRightTreeMin(Nodal nodal) {
//4. 用一个临时变量将最小节点的值保存
Nodal target = nodal; //临时变量,存放最小值
//3.从`targetHero`的右子树找到最小的节点(或者从左子树找到最大的节点)
//循环的查找左节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
//5.删除该最小节点
delete(target.value);
return target.value;
}
//删除节点
public void delete(int value) {
if (root == null) {
return;
} else {
//1.找到要删除的节点`targetNodal`
Nodal targetNodal = search(value);
//如果没有找到要删除的节点
if (targetNodal == null) {
return;
}
//2.找到要删除的节点`targetNodal`的父节点`parent`(是否存在父节点)
//如果当前这颗二叉排序树只有一个节点(父节点不存在)
if (root.left == null && root.right == null) {
root = null; //删除根节点
return;
}
//找到要删除的节点`targetNodal`的父节点`parent`
Nodal parent = searchParent(value);
//**删除叶子节点**,即该节点下没有左右子节点
if (targetNodal.left == null && targetNodal.right == null) {
//3.确定`targetHero`是父节点`parent`的左子节点`parent.left = null;`还是右子节点`parent.right = null;`来对应删除
if (parent.left != null && parent.left.value == value) { //targetNodal是父节点的左子节点
parent.left = null;
} else if (parent.right != null && parent.right.value == value) { //targetNodal是父节点的右子节点
parent.right = null;
}
//**删除有两颗子树的节点**,即该节点有左子节点和右子节点
} else if (targetNodal.left != null && targetNodal.right != null) {
//3. 从`targetNodal`的右子树找到最小的节点(或者从左子树找到最大的节点)
//4. 用一个临时变量将最小节点的值保存
//5. 删除该最小节点
int min = deleteRightTreeMin(targetNodal.right);
//6. 将最小节点的值赋给`targetHero`:`targetNodal.value = min;`
targetNodal.value = min;
//**删除只有一颗子树的节点**,即该节点有左子节点或者右子节点
} else {
//3. 确定`targetNodal`的子节点是左子节点还是右子节点
//4. 确定`targetNodal`是`parent`的左子节点还是右子节点
//5. 如果`targetNodal`有左子节点:
if (targetNodal.left != null) {
if (parent != null) {
//5.1. `targetNodal`是`parent`的左子节点`parent.left = targetNodal.left;`
if (parent.left.value == value) {
parent.left = targetNodal.left;
//5.2. `targetNodal`是`parent`的右子节点`parent.right = targetNodal.left;`
} else {
parent.right = targetNodal.left;
}
} else {
root = targetNodal.left;
}
//6. 如果`targetNodal`有右子节点:
} else {
if (parent != null) {
//6.1. `targetNodal`是`parent`的左子节点`parent.left = targetNodal.right;`
if (parent.left.value == value) {
parent.left = targetNodal.right;
//6.2. `targetNodal`是`parent`的右子节点`parent.right = targetNodal.right;`
} else {
parent.right = targetNodal.right;
}
} else {
root = targetNodal.right;
}
}
}
}
}
}
//创建Nodal节点
class Nodal {
public int value; //编号
public Nodal left; //指向左子节点的索引,默认null
public Nodal right; //指向右子节点的索引,默认null
public Nodal(int value) {
this.value = value;
}
@Override
public String toString() {
return "Nodal{value=" + value + '}';
}
//递归的形式添加节点,需要满足二叉树的要求
public void add(Nodal Nodal) {
if (Nodal == null) {
return;
}
//判断传入的节点的值和当前子树的根节点的值的关系
if (Nodal.value < this.value) { //添加的节点的值小于当前节点的值
if (this.left == null) { //当前节点左子节点为空,直接添加在左子节点
this.left = Nodal;
} else {
this.left.add(Nodal); //向左子树递归添加
}
} else { //添加的节点的值大于等于当前节点的值
if (this.right == null) { //当前节点右子节点为空,直接添加在右子节点
this.right = Nodal;
} else {
this.right.add(Nodal); //向右子树递归添加
}
}
//当添加完一个节点后,如果右子树的高度比左子树高度差超过1,左旋转
if (rightHeight() - leftHeight() > 1) {
//3.1 当符合左旋转的条件时,如果右子树的左子树的高度大于右子树的右子树的高度,先对当前节点的右子节点进行右旋转,再对当前节点进行左旋转操作
//如果右子树的左子树的高度大于右子树的右子树的高度
if (right != null && right.leftHeight() > right.rightHeight()) {
//先对当前这个节点的右子节点进行右旋转
right.rightRotate();
//再对当前进行左旋转操作
leftRotate(); //左旋转
} else {
leftRotate(); //左旋转
}
return; //此时已经平衡
}
//当添加完一个节点后,如果左子树的高度比右子树高度差超过1,右旋转
if (leftHeight() - rightHeight() > 1) {
//3.2 当符合右旋转的条件时,如果左子树的右子树的高度大于左子树的左子树的高度,先对当前节点的左子节点进行左旋转,再对当前节点进行右旋转操作
//如果左子树的右子树的高度大于左子树的左子树的高度
if (left != null && left.rightHeight() > left.leftHeight()) {
//先对当前这个节点的左子节点进行左旋转
left.leftRotate();
//再对当前进行右旋转操作
rightRotate(); //右旋转
} else {
rightRotate(); //右旋转
}
}
}
//1.3.中序遍历:先遍历左子树,**再输出父节点**,再遍历右子树
public void infixOrder() {
//1.3.1. 如果左子节点不为空,则递归继续中序遍历
if (this.left != null) {
this.left.infixOrder();
}
//1.3.2. 输出当前节点
System.out.println(this);
//1.3.3. 如果右子节点不为空,则递归继续中序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
/**
* 1.找到要删除的节点`targetNodal`
*
* @param value 要删除的节点的值
* @return 返回要删除的节点,否则返回null
*/
public Nodal 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);
}
}
/**
* 2.找到要删除的节点`targetNodal`的父节点`parent`(是否存在父节点)
*
* @param value 要查找的值
* @return 返回要删除的节点的父节点,否则返回null
*/
public Nodal 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;
}
}
}
//返回以当前节点为根节点的树的高度
public int height() {
//要加上当前节点那一层
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
//返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
//返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
//单旋转(左旋转)
public void leftRotate() {
//1.1. 创建一个新的节点,以当前根节点的值作为新节点的值
Nodal newNodal = new Nodal(value);
//1.2. 把新节点的左子树设置为当前节点的左子树
newNodal.left = left;
//1.3. 把新节点的右子树设置为当前节点的右子树的左子树
newNodal.right = right.left;
//1.4. 把当前节点的值换为右子节点的值
value = right.value;
//1.5. 把当前节点的右子树设置为当前节点的右子树的右子树
right = right.right;
//1.6. 把当前节点的左子树设置为新节点
left = newNodal;
}
//单旋转(右旋转)
public void rightRotate() {
//2.1. 创建一个新的节点,以当前根节点的值作为新节点的值
Nodal newNodal = new Nodal(value);
//2.2. 把新节点的右子树设置为当前节点的右子树
newNodal.right = right;
//2.3. 把新节点的左子树设置为当前节点的左子树的右子树
newNodal.left = left.right;
//2.4. 把当前节点的值换为左子节点的值
value = left.value;
//2.5. 把当前节点的左子树设置为当前节点的左子树的左子树
left = left.left;
//2.6. 把当前节点的右子树设置为新节点
right = newNodal;
}
}
十一、多路查找树
1.二叉树与B树#
1.二叉树的问题分析#
-
二叉树的操作效率较高,但是也存在问题:二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:
- 在构建二叉树时,需要多次进行I/O操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
- 节点海量,也会造成二叉树的高度很大,会降低操作速度
2.多叉树#
-
在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
-
2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化
-
举例说明(下面2-3树就是一颗多叉树)
3.B树的基本介绍#
-
B树通过重新组织节点,降低树的高度,并且减少I/O读写次数来提升效率
- B树通过重新组织节点, 降低树的高度
- 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页的大小通常为4k),这样每个节点只需要一次I/O就可以完全载入
- 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素,B树(B+)广泛应用于文件存储系统以及数据库系统中
2.2-3树#
1.基本介绍#
- 2-3树是最简单的B树结构,具有如下特点:
- 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
- 2-3树是由二节点和三节点构成的树
2.应用案例#
-
将数列
{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20}
构建成2-3树,并保证数据插入的大小顺序 -
插入规则:(满足排序树的特点)
- 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
- 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面3个条件
- 对于三节点的子树的值大小仍然遵守二叉排序树的规则
-
说明:
- 当插入10时,应当在10-12-14的位置,但是这时满了,因此向上层看,16-26也满了
- 因此将10-12-14拆成10<-12->14,因为其它拆法不满足二节点或三节点的要求,但是这时叶子节点没有全部在同一层,需要调整26到下面
3.其它说明#
3.B树、B+树和B*树#
1.B树的介绍和原理#
-
B-tree树即B树,B即Balanced,平衡的意思。有人把B-tree翻译成B-树,容易让人产生误解。会以为B-树是一种树,而B树又是另一种树实际上,B-tree就是指的B树(B-tree也写成B-树)
-
2-3树和2-3-4树,就是B树,在学习Mysql时,经常听到说某种类型的索引是基于B树或者B+树的,如图:
-
B树的说明:
- B树的阶:节点的最多子节点个数(比如2-3树的阶是3,2-3-4树的阶是4)
- B树的搜索,从根节点开始,对节点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点;重复,直到所对应的儿子指针为空,或已经是叶子节点
- 关键字集合分布在整颗树中,即叶子节点和非叶子节点都存放数据
- 搜索有可能在非叶子节点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
2.B+树的介绍和原理#
-
B+树是B树的变体,也是一种多路搜索树
-
B+树的说明:
- B+树的搜索与B树也基本相同,区别是B+树只有达到叶子节点才命中(B树可以在非叶子节点命中),其性能也等价于在关键字全集做一次二分查找
- 所有关键字都出现在叶子节点的链表中(数据只能在叶子节点,也叫稠密索引),且链表中的关键字(数据)恰好是有序的
- 不可能在非叶子节点命中
- 非叶子节点相当于是叶子节点的索引,也叫稀疏索引,叶子节点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
- B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然
-
索引选择B+树的理由:
- B+树将所有的叶子节点使用链表连接起来,并且按照顺序存储,使得范围查询更加高效
- B树则需要在每个层级进行逐个匹配,性能较差
3.B*树的介绍和原理#
-
B*树是B+树的变体,在B+树的非根和非叶子节点再增加指向兄弟的指针:
-
B*树的说明:
- B* 树定义了非叶子节点关键字个数至少为$(2/3)*M$,即块的最低使用率为2/3,而B+树的块的最低使用率为1/2
- 从第1个特点我们可以看出,B* 树分配新节点的概率比B+ 树要低,空间使用率更高
十二、图
1.图的基本介绍#
-
为什么要有图?
- 线性表局限于一个直接前驱和一个直接后继的关系
- 树也只能有一个直接前驱也就是父节点
- 当需要表示多对多的关系时,就用到了图
-
图是一种数据结构,其中节点可以具有零个或多个相邻元素。两个节点之间的连接称为边。节点也可以称为顶点。如图:
2.图的常用概念#
-
顶点(vertex):每个节点
-
边(edge):顶点之间的连线
-
路径:比如从 D->C 的路径有:
- D->B->C
- D->A->B->C
-
无向图:顶点之间的连接没有方向,比如A-B,即可以是 A->B 也可以 B->A
-
有向图:顶点之间的连接有方向,比如A-B,只能是 A-> B 不能是 B->A
-
带权图:边带权值的图,也叫网
3.图的表示方式(两种)#
-
邻接矩阵(二维数组表示)
-
表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵的行和列表示的是1....n个点(1表示能够直接连接,0表示不能直接连接)
-
需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失
-
-
邻接表(数组+链表表示)
- 邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
- 说明:
- 标号为0的节点的相关联的节点为 1 2 3 4
- 标号为1的节点的相关联节点为 0 4
- 标号为2的节点相关联的节点为 0 4 5
4.图的快速入门案例#
1.图的创建#
2.图的遍历#
1.图遍历介绍:#
- 所谓图的遍历,即是对节点的访问。一个图有那么多个节点,如何遍历这些节点,需要特定策略,一般有两种访问策略:深度优先遍历和广度优先遍历
2.图的深度优先搜索(Depth First Search)#
-
基本思想:
- 深度优先遍历,从初始访问节点出发,初始访问节点可能有多个邻接节点,深度优先遍历的策略就是首先访问第一个邻接节点,然后再以这个被访问的邻接节点作为初始节点,访问它的第一个邻接节点, 可以这样理解:每次都在访问完当前节点后首先访问当前节点的第一个邻接节点
- 这样的访问策略是优先往纵向挖掘深入,而不是对一个节点的所有邻接节点进行横向访问
- 显然,深度优先搜索是一个递归的过程
-
遍历步骤:
- 访问初始节点v,并标记节点v为已访问
- 查找节点v的第一个邻接节点w
- 若w存在,则继续执行步骤4,如果w不存在,则回到步骤1,将从v的下一个节点继续
- 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行1,2,3的步骤)
- 查找节点v的w邻接节点的下一个邻接节点,转到步骤3
-
要求:对下图进行深度优先搜索,从A 开始遍历
- 从A开始,先把A标记为已访问(输出)
- 节点存放在链表里面的顺序是:A,B,C,D,E,A的下一个节点为B
- B存在,继续指向下一步
- B未被访问,访问输出,以B为出发点,访问下一个节点C,访问输出
- 以C为出发点,访问下一个节点D,C和D并不是连通的,没有找到,不能输出D,回到B,B和D是连通的,输出D,查找B的下一个邻接节点E,而且B和E是连通的,输出E
3.图的广度优先搜索(Broad First Search)#
-
基本思想:类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保存访问过的节点的顺序,以便按这个顺序来访问这些节点的邻接节点
-
遍历步骤:
- 访问初始节点v,并标记节点v为已访问
- 节点v入队列
- 当队列非空时,继续执行,否则算法结束
- 出队列,取得队头节点u
- 查找节点u的第一个邻接节点w
- 若节点u的邻接节点w不存在,则转到步骤3;否则循环执行以下三个步骤:
- 若节点w尚未被访问,则访问节点w并标记为已访问
- 节点w入队列
- 查找节点u的继w邻接节点后的下一个邻接节点w,转到步骤6
-
要求:对下图进行广度优先搜索,从A 开始遍历
- 从A开始,先把A标记为已访问(输出)
- 访问A的下一个节点B,连通输出,A访问B的下一个节点C,连通输出,A访问C的下一个节点D,没有找到,不能输出D
- 从队列里面把B取出来,B找到A,发现已经访问过,跳过,B找到C,发现已经访问过,跳过,B找到D,连通输出,B找到E,连通输出
4.DFS和BFS比较#
- 深度优先遍历顺序为:1->2->4->8->5->3->6->7
- 广度优先遍历顺序为:1->2->3->4->5->6->7->8(逐层遍历)
public class A_Graphic {
public static void main(String[] args) {
// int n = 5; //节点个数
// String[] vertexs = {"A", "B", "C", "D", "E"};
//
// //创建图
// Graphic graphic = new Graphic(n);
// //循环的添加节点
// for (String vertex : vertexs) {
// graphic.insertVertex(vertex);
// }
// //添加边:A-B,A-C,B-C,B-D,B-E
// graphic.insertEdge(0, 1, 1);
// graphic.insertEdge(0, 2, 1);
// graphic.insertEdge(1, 2, 1);
// graphic.insertEdge(1, 3, 1);
// graphic.insertEdge(1, 4, 1);
// System.out.println("显示图对应的矩阵:");
// graphic.showGraphic();
//
// System.out.println("深度优先遍历:");
// graphic.depthFirstSearch();
//
// System.out.println("广度优先遍历:");
// graphic.broadFirstSearch();
System.out.println("DFS和BFS比较:");
int n = 8; //节点个数
String[] vertexs = {"1", "2", "3", "4", "5", "6", "7", "8"};
//创建图
Graphic graphic = new Graphic(n);
//循环的添加节点
for (String vertex : vertexs) {
graphic.insertVertex(vertex);
}
//添加边:
graphic.insertEdge(0, 1, 1);
graphic.insertEdge(0, 2, 1);
graphic.insertEdge(1, 3, 1);
graphic.insertEdge(1, 4, 1);
graphic.insertEdge(3, 7, 1);
graphic.insertEdge(4, 7, 1);
graphic.insertEdge(2, 5, 1);
graphic.insertEdge(2, 6, 1);
graphic.insertEdge(5, 6, 1);
System.out.println("显示图对应的矩阵:");
graphic.showGraphic();
System.out.println("深度优先遍历:");
graphic.depthFirstSearch(); //1->2->4->8->5->3->6->7->
System.out.println("广度优先遍历:");
graphic.broadFirstSearch(); //1->2->3->4->5->6->7->8->
}
}
class Graphic {
private List<String> vertexList; //1. 存储顶点(`String`)使用`ArrayList`保存
private int[][] edges; //2. 存储图对应的邻接矩阵`int[][] edges`
private int edgeNum; //边的数目
private boolean[] isVisited; //记录某个节点是否被访问
/**
* 初始化矩阵和vertexList
*
* @param n 顶点数量
*/
public Graphic(int n) {
edges = new int[n][n]; //初始化邻接矩阵
vertexList = new ArrayList<>(n); //初始化存储顶点集合
edgeNum = 0; //初始化边的数目为0
}
/**
* 插入节点
*
* @param vertex 顶点的字符串
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/**
* 添加边
*
* @param v1 前一个顶点对应的下标
* @param v2 后一个顶点对应的下标
* @param weight 权值(1表示能够直接连接,0表示不能直接连接)
*/
public void insertEdge(int v1, int v2, int weight) {
//无向图
edges[v1][v2] = weight;
edges[v2][v1] = weight;
edgeNum++;
}
//图中常用方法:
//返回节点个数
public int countVertex() {
return vertexList.size();
}
//返回边的数目
public int countEdge() {
return edgeNum;
}
//返回节点对应的数据
public String valueByIndex(int i) {
return vertexList.get(i);
}
//返回前后节点的权值
public int weightByIndex(int v1, int v2) {
return edges[v1][v2];
}
//显示图对应的矩阵
public void showGraphic() {
for (int[] edge : edges) {
System.out.println(Arrays.toString(edge));
}
}
/**
* 得到第一个邻接节点的下标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;
}
/**
* 对一个节点进行深度优先遍历
*
* @param isVisited 记录某个节点是否被访问
* @param i 节点索引,第一次为0
*/
public void depthFirstSearch(boolean[] isVisited, int i) {
//1. 访问初始节点v,并标记节点v为已访问
System.out.print(valueByIndex(i) + "->"); //访问初始节点v并输出
isVisited[i] = true; //标记节点v为已访问
//2. 查找节点v的第一个邻接节点w
int w = getFirstNeighbor(i);
//3. 若w存在,则继续执行步骤4,如果w不存在,则回到步骤1,将从v的下一个节点继续
while (w != -1) { //邻接节点存在
//4. 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行1,2,3的步骤)
if (!isVisited[w]) {
depthFirstSearch(isVisited, w);
}
//5. 查找节点v的w邻接节点的下一个邻接节点,转到步骤3
w = getNextNeighbor(i, w); //w节点已经被访问过
}
}
//遍历所有的节点,并进行深度优先遍历
public void depthFirstSearch() {
isVisited = new boolean[vertexList.size()];
//回溯
for (int i = 0; i < countVertex(); i++) {
if (!isVisited[i]) {
depthFirstSearch(isVisited, i);
}
}
}
/**
* 对一个节点进行广度优先遍历
*
* @param isVisited 记录某个节点是否被访问
* @param i 节点索引,第一次为0
*/
public void broadFirstSearch(boolean[] isVisited, int i) {
int u; //队列头节点u对应的下标
int w; //邻接节点w对应的下标
LinkedList<Object> queue = new LinkedList<>(); //节点队列,记录节点访问顺序
//1. 访问初始节点v,并标记节点v为已访问
System.out.print(valueByIndex(i) + "=>"); //访问初始节点v并输出
isVisited[i] = true; //标记节点v为已访问
//2. 节点v入队列
queue.addLast(i);
//3. 当队列非空时,继续执行,否则算法结束
while (!queue.isEmpty()) {
//4. 出队列,取得队头节点u
u = (Integer) queue.removeFirst();
//5. 查找节点u的第一个邻接节点w
w = getFirstNeighbor(u);
//6. 若节点u的邻接节点w不存在,则转到步骤3;否则循环执行以下三个步骤:
while (w != -1) { //邻接节点存在
//6.1. 若节点w尚未被访问,则访问节点w并标记为已访问
if (!isVisited[w]) {
System.out.print(valueByIndex(w) + "=>"); //访问初始节点v并输出
isVisited[w] = true; //节点w并标记为已访问
//6.2. 节点w入队列
queue.addLast(w);
}
//6.3. 查找节点u的继w邻接节点后的下一个邻接节点w,转到步骤6
w = getNextNeighbor(u, w); //广度优先的体现
}
}
}
//遍历所有的节点,并进行广度优先遍历
public void broadFirstSearch() {
isVisited = new boolean[vertexList.size()];
//回溯
for (int i = 0; i < countVertex(); i++) {
if (!isVisited[i]) {
broadFirstSearch(isVisited, i);
}
}
}
}
十三、程序员常用10大算法
1.二分查找算法(非递归)#
1.介绍#
- 之前使用递归的方式实现二分查找算法,下面之前使用非递归的方式实现二分查找算法
- 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
- 二分查找法的运行时间为对数时间O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n步,假设从[0, 99]的队列(100个数,即n=100)中寻到目标数30,则需要查找步数为㏒₂100 , 即最多需要查找7次( 2^6 < 100 < 2^7)
2.应用实例#
- 数组
{1, 3, 8, 10, 11, 67, 100}
实现二分查找,要求使用非递归的方式完成 - 查找一个值思路分析:(使用非递归查找,另一种是递归查找)
- 首先确定该数组中间的下标
mid = (left + right) / 2
- 让要查找的值
target
和arr[mid]
比较:(数组是从小到大的)target > arr[mid]
:要查找的值在mid
的右边,需要向右递归查找target < arr[mid]
:要查找的值在mid
的左边,需要向左递归查找target = arr[mid]
:找到要查找的值,结束递归,返回
left > right
:递归完整个数组,仍找不到target
,结束递归
- 首先确定该数组中间的下标
查找多个值思路分析:(使用非递归查找,另一种是递归查找)
- 查找多个值思路分析:(使用非递归查找,另一种是递归查找)
- 首先确定该数组中间的下标
mid = (left + right) / 2
- 让要查找的值
target
和arr[mid]
比较:target > arr[mid]
:要查找的值在mid
的右边,需要向右递归查找target < arr[mid]
:要查找的值在mid
的左边,需要向左递归查找target = arr[mid]
:找到要查找的值,结束递归,返回- 在找到
mid
索引时,不要马上返回 - 向
mid
索引的左边扫描,将所有满足要求的下标,加入到list
集合中 - 向
mid
索引的右边扫描,将所有满足要求的下标,加入到list
集合中 - 将
list
返回
- 在找到
left > right
:递归完整个数组,仍找不到target
,结束递归
- 首先确定该数组中间的下标
/**
* 二分查找算法(非递归)
* 使用二分(折半)查找的前提是该数组是有序的
*/
public class A_BinarySearchNonRecursive {
public static void main(String[] args) {
System.out.println("查找一个值:");
int[] arr = {1, 3, 8, 10, 11, 67, 100};
Integer index = binarySearchNonRecursive(arr, -1);
System.out.println(index);
System.out.println("查找多个值:");
int[] arrs = {1, 3, 8, 10, 10, 10, 11, 67, 100};
List<Integer> indexs = binarySearchNonRecursiveBatch(arrs, 10);
System.out.println(indexs);
}
/**
* 查找一个值:
*
* @param arr 数组
* @param target 要查找的值
* @return 返回要查找的值在数组中的下标
*/
public static Integer binarySearchNonRecursive(int[] arr, int target) {
int left = 0; //左边的索引
int right = arr.length - 1; //右边的索引
while (left <= right) { //可以继续查找
//1. 首先确定该数组中间的下标`mid = (left + right) / 2`
int mid = (left + right) / 2;
//2. 让要查找的值`target`和`arr[mid]`比较:(数组是从小到大的)
//2.1. `target > arr[mid]`:要查找的值在`mid`的右边,需要向右递归查找
if (target > arr[mid]) {
left = mid + 1; //向右查找
//2.2. `target < arr[mid]`:要查找的值在`mid`的左边,需要向左递归查找
} else if (target < arr[mid]) {
right = mid - 1; //向左查找
//2.3. `target = arr[mid]`:找到要查找的值,结束递归,返回
} else {
return mid;
}
}
return null;
}
/**
* 查找多个值:
*
* @param arr 数组
* @param target 要查找的值
* @return 返回要查找的值在数组中的下标
*/
public static List<Integer> binarySearchNonRecursiveBatch(int[] arr, int target) {
int left = 0; //左边的索引
int right = arr.length - 1; //右边的索引
while (left <= right) { //可以继续查找
//1. 首先确定该数组中间的下标`mid = (left + right) / 2`
int mid = (left + right) / 2;
//2. 让要查找的值`target`和`arr[mid]`比较:(数组是从小到大的)
//2.1. `target > arr[mid]`:要查找的值在`mid`的右边,需要向右递归查找
if (target > arr[mid]) {
left = mid + 1; //向右查找
//2.2. `target < arr[mid]`:要查找的值在`mid`的左边,需要向左递归查找
} else if (target < arr[mid]) {
right = mid - 1; //向左查找
//2.3. `target = arr[mid]`:找到要查找的值,结束递归,返回
} else {
//2.3.1. 在找到`mid`索引时,不要马上返回
List<Integer> list = new ArrayList<>(); //存放要查找的值在数组中的下标
//2.3.2. 向`mid`索引的左边扫描,将所有满足要求的下标,加入到`list`集合中
int temp = mid - 1; //临时变量
while (true) {
if (temp < 0 || arr[temp] != target) { //扫描到最左边,或者向左遍历过程中没有目标值
break; //退出
}
list.add(temp); //将temp放到list中
temp--;
}
list.add(mid); //将mid放到list中
//2.3.3. 向`mid`索引的右边扫描,将所有满足要求的下标,加入到`list`集合中
temp = mid + 1; //临时变量
while (true) {
if (temp > arr.length - 1 || arr[temp] != target) { //扫描到最右边,或者向右遍历过程中没有目标值
break; //退出
}
list.add(temp); //将temp放到list中
temp++;
}
//2.3.4. 将`list`返回
return list;
}
}
return null;
}
}
2.分治(Divide-and-Conquer)算法#
1.分治算法介绍#
- 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
- 分治算法可以求解的一些经典问题
- 二分搜索
- 大整数乘法
- 棋盘覆盖
- 合并排序
- 快速排序
- 线性时间选择
- 最接近点对问题
- 循环赛日程表
- 汉诺塔
2.分治算法的基本步骤#
- 分治法在每一层递归上都有三个步骤:
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
- 解决:若子问题规模较小而容易被解决则直接解决,否则递归地解决各个子问题
- 合并:将各个子问题的解合并为原问题的解
3.分治(Divide-and-Conquer(P))算法设计模式#
if |P|≤n0
then return(ADHOC(P)) //将P分解为较小的子问题P1,P2,…,Pk
for i←1 to k
do yi ← Divide-and-Conquer(Pi) //递归解决Pi
T ← MERGE(y1,y2,…,yk) //合并子问题
return(T)
- |P|表示问题P的规模
- n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解
- ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解
- 算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解
4.分治算法最佳实践-汉诺塔实现#
- 汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘
- 假如每秒钟一次,共需多长时间呢?移完这些金片需要5845.54亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭
- 汉诺塔游戏的演示和思路分析:
- 如果只有一个盘,A->C
- 如果有n>=2情况,总是可以看做是两个盘:最下面的一个盘和上面的所有盘
- 把最上面的盘A->B,移动过程会使用C
- 把最下面的盘A->C
- 把B的所有盘从B->C,移动过程会使用A
/**
* 分治算法最佳实践-汉诺塔实现
*/
public class B_DivideAndConquer {
public static void main(String[] args) {
hanoiTower(5, 'A', 'B', 'C');
}
/**
* 汉诺塔实现
*
* @param num 盘子数量
* @param a A塔
* @param b B塔
* @param c C塔
*/
public static void hanoiTower(int num, char a, char b, char c) {
//1. 如果只有一个盘,A->C
if (num == 1) {
System.out.println("第1个盘从" + a + "->" + c);
//2. 如果有n>=2情况,总是可以看做是两个盘:最下面的一个盘和上面的所有盘
} else {
//2.1. 先把最上面的盘A->B,移动过程会使用C
hanoiTower(num - 1, a, c, b);
//2.2. 把最下面的盘A->C
System.out.println("第" + num + "个盘从" + a + "->" + c);
//2.3. 把B的所有盘从B->C,移动过程会使用A
hanoiTower(num - 1, b, a, c);
}
}
}
3.动态规划(Dynamic Programming)算法#
1.动态规划算法介绍#
- 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
- 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解
- 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
- 动态规划可以通过填表的方式来逐步推进,得到最优解
2.动态规划算法最佳实践-背包问题#
-
背包问题:有一个背包,容量为4磅 , 现有如下物品:
物品 重量 价格 吉他(G) 1 1500 音响(S) 4 3000 电脑(L) 3 2000 -
要求达到的目标为装入的背包的总价值最大,并且重量不超出
-
要求装入的物品不能重复
-
思路分析和图解
-
背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包(每个物品最多放一个)和完全背包(每种物品都有无限件可用)
-
这里的问题属于01背包。而无限背包可以转化为01背包
-
算法的主要思想,利用动态规划来解决。每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i] [j]表示在前i个物品中能够装入容量为j的背包中的最大价值。则我们有下面的结果:
v[i][0] = v[0][j] = 0; //表示填入表第一行和第一列是0
- 当w[i] > j时:
v[i][j] = v[i-1][j]; //当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
- 当w[i] <= j时:
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]的最大值)
- i:物品的索引
- j:容量的索引
- v[i]:此时物品的价值
- w[i]:此时物品的重量
- v[i] [j]:最大价值
-
解决类似的问题可以分解成一个个的小问题进行解决,假设存在背包容量大小分为1,2,3,4的各种容量的背包(分配容量的规则为最小重量的整数倍):
物品 0磅 1磅 2磅 3磅 4磅 无物品 0 0 0 0 0 吉他(G) 0 1500(G) 1500(G) 1500(G) 1500(G) 音响(S) 0 1500(G) 1500(G) 1500(G) 3000(S) 电脑(L) 0 1500(G) 1500(G) 2000(L) 2000(L)+1500(G) - 假设只有吉他(G),这时不管背包容量多大,只能放一个吉他(G)
- 假设有吉他(G)和音响(S),背包容量为1,2,3磅,只能放一个吉他(G),直到背包容量为4磅,只能放一个音响(S),比较音响(S)和吉他(G)的价值,发现音响(S)的价值比吉他(G)高,换成音响(S)的3000
- 假设有吉他(G)和音响(S)和电脑(L),背包容量为1,2,3磅,只能放一个吉他(G),直到背包容量为3磅,可以放一个电脑(L),背包容量为4磅,可以放电脑(L)和吉他(G)
-
-
public class C_DynamicProgramming {
public static void main(String[] args) {
int[] w = {1, 4, 3}; //物品的重量
int[] v = {1500, 3000, 2000}; //物品的价值
int C = 4; //背包的容量
int n = v.length; //物品的个数
//创建二维数组,v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值(包括初始化行列)
int[][] arr = new int[n + 1][C + 1];
//记录商品存放到背包的情况
int[][] path = new int[n + 1][C + 1];
//1. `v[i][0] = v[0][j] = 0; //表示填入表第一行和第一列是0`
for (int i = 0; i < arr.length; i++) {
arr[i][0] = 0; //表示填入表第一列是0
}
for (int i = 0; i < arr[0].length; i++) {
arr[0][i] = 0; //表示填入表第一行是0
}
for (int i = 1; i < arr.length; i++) { //不处理是0的第一行
for (int j = 1; j < arr[0].length; j++) { //不处理是0的第一列
//2. 当w[i] > j时:`v[i][j] = v[i-1][j]; //当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略`
if (w[i - 1] > j) {
arr[i][j] = arr[i - 1][j]; //当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
//3. 当w[i] <= j时: `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]的最大值)`
} else {
// arr[i][j] = Math.max(arr[i - 1][j], v[i - 1] + arr[i - 1][j - w[i - 1]]); //当准备加入的新增的商品的容量小于等于当前背包的容量,装入的方式(v[i-1][j]:上一个单元格的装入的最大值;v[i]:表示当前商品的价值;v[i-1][j-w[i]]:装入i-1商品的时候,到剩余空间j-w[i]的最大值)
//也可以使用if-else体现商品放入背包的情况
if (arr[i - 1][j] < v[i - 1] + arr[i - 1][j - w[i - 1]]) {
arr[i][j] = v[i - 1] + arr[i - 1][j - w[i - 1]];
path[i][j] = 1; //记录当前情况
} else {
arr[i][j] = arr[i - 1][j];
}
}
}
}
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();
}
//输出最后放入的是哪些商品
// System.out.println("得到所有的放入情况:");
// for (int i = 0; i < path.length; i++) {
// for (int j = 0; j < path[i].length; j++) {
// if (path[i][j] == 1) {
// System.out.printf("第%d个商品放入背包\n",i);
// }
// }
// }
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--;
}
}
}
4.KMP算法#
1.暴力匹配算法#
- 如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:
- 如果当前字符匹配成功(即
str1[i] == str2[j]
),则i++; j++;
,继续匹配下一个字符 - 如果当前字符匹配不成功(即
str1[i]! = str2[j]
),令i = i - (j - 1); j = 0;
。相当于每次匹配失败时,i 回溯,j 被置为0 - 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间(不可行!)
- 如果当前字符匹配成功(即
- 解决方式:
- 暴力匹配:简单,回溯的次数很多,速度很慢
- KMP算法:建立一个《部分匹配表》,通过《部分匹配表》里面的搜索词进行匹配,效率高
2.KMP算法介绍#
- KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
- 字符串查找算法(Knuth-Morris-Pratt),简称为“KMP算法”,常用于在一个文本串S内查找一个模式串P的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法
- KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间
- 很详尽KMP算法(厉害) - ZzUuOo666 - 博客园 (cnblogs.com)
3.KMP算法最佳应用-字符串匹配问题#
-
字符串匹配问题:
- 有一个字符串
str1 = "BBC ABCDAB ABCDABCDABDE"
,和一个子串str2 = "ABCDABD"
- 现在要判断str1是否含有str2,如果存在,就返回第一次出现的位置,如果没有,则返回-1
- 要求:使用KMP算法完成判断,不能使用简单的暴力匹配算法
- 有一个字符串
-
思路分析:
-
首先,用str1的第一个字符和str2的第一个字符去比较,不符合,关键词向后移动一位
-
重复第一步,还是不符合,再后移
-
一直重复,直到str1有一个字符与str2的第一个字符符合为止
-
接着比较字符串和搜索词的下一个字符,还是符合
-
遇到str1有一个字符与str2对应的字符不符合
-
这时候,想到的是继续遍历str1的下一个字符,重复第1步。(其实是很不明智的,因为此时"BCD"已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是”ABCDAB”。KMP算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率)
-
怎么做到把刚刚重复的步骤省略掉?可以对str2计算出一张《部分匹配表》:
-
已知空格与D不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符B对应的”部分匹配值”为2,因此按照公式算出向后移动的位数:$移动位数=已匹配的字符数-对应的部分匹配值$。因为$6-2=4$,所以将搜索词向后移动4位
-
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(”AB”),对应的”部分匹配值”为0。所以,$移动位数=2-0=2$,于是将搜索词向后移2位
-
因为空格与A不匹配,继续后移一位
-
逐位比较,直到发现C与D不匹配。于是,$移动位数=6-2$,继续将搜索词向后移动4位
-
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),$移动位数=7-0$,再将搜索词向后移动7位,这里就不再重复了
-
介绍《部分匹配表》怎么产生的?先介绍前缀,后缀是什么
“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”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
-
”部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动 4 位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置
-
/**
* 字符串匹配问题:
* 1. 有一个字符串`str1 = "BBC ABCDAB ABCDABCDABDE"`,和一个子串`str2 = "ABCDABD"`
* 2. 现在要判断str1是否含有str2,如果存在,就返回第一次出现的位置,如果没有,则返回-1
* 3. 要求:使用KMP算法完成判断,不能使用简单的暴力匹配算法
*/
public class D_KMP {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
System.out.println("暴力匹配算法:");
int index = bruteForceMatching(str1, str2);
System.out.println(index);
System.out.println("KMP算法:");
int[] next = kmpNext("ABCDABD");
System.out.println("next=" + Arrays.toString(next)); //[0, 0, 0, 0, 1, 2, 0]
int kmp = kmp(str1, str2, next);
System.out.println(kmp);
}
//暴力匹配算法
public static int bruteForceMatching(String str1, String str2) {
//将字符串转成字符数组
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Length = s1.length;
int s2Length = s2.length;
int i = 0; //i索引指向s1
int j = 0; //j索引指向s2
while (i < s1Length && j < s2Length) { //保证匹配时不越界
//1.如果当前字符匹配成功(即`str1[i] == str2[j]`),则`i++,j++`,继续匹配下一个字符
if (s1[i] == s2[j]) {
i++;
j++;
//2.如果当前字符匹配不成功(即`str1[i]! = str2[j]`),令`i = i - (j - 1),j = 0`。相当于每次匹配失败时,i 回溯,j 被置为0
} else {
i = i - (j - 1);
j = 0;
}
}
//判断是否匹配成功
if (j == s2Length) {
return i - j;
} else {
return -1;
}
}
/**
* 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++) {
//KMP算法的核心
//直到dest.charAt(i) == dest.charAt(j)成立才退出
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1]; //需要从next[j - 1]获取新的j
}
if (dest.charAt(i) == dest.charAt(j)) {
j++; //部分匹配值+1
}
next[i] = j;
}
return next;
}
/**
* 2.使用部分匹配表完成KMP算法
*
* @param str1 字符串
* @param str2 字符串子串
* @param next 字符串子串对应的部分匹配表
* @return 如果存在,就返回第一次出现的位置,如果没有,则返回-1
*/
public static int kmp(String str1, String str2, int[] next) {
for (int i = 0, j = 0; i < str1.length(); i++) {
//KMP算法的核心
//直到str1.charAt(i) == str2.charAt(j)成立才退出
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1]; //调整j的大小
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) { //字符匹配成功
return i - (j - 1);
}
}
return -1;
}
}
5.贪心算法(Greedy Algorithm)#
1.贪心算法介绍#
- 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法
- 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
2.贪心算法最佳应用-集合覆盖问题#
-
假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号:
广播台 覆盖地区 K1 "北京", "上海", "天津" K2 "广州", "北京", "深圳" K3 "成都", "上海", "杭州" K4 "上海", "天津" K5 "杭州", "大连" -
思路分析:如何找出覆盖所有地区的广播台的集合呢?
-
使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有2ⁿ-1个,假设每秒可以计算10个子集,如:
广播台数量n 子集总数2ⁿ 需要的时间 5 32 3.2秒 10 1024 102.4秒 32 4294967296 13.6年 100 1.26*100³º 4*10²³年 -
使用贪婪算法,效率高:目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
- 遍历所有的广播台,找到一个覆盖了最多未覆盖的地区的广播台(此广播台可能包含一些已覆盖的地区,但没有关系)
- 将这个广播台加入到一个集合中(比如ArrayList),想办法把该广播台覆盖的地区在下次比较时去掉
- 重复第1步直到覆盖了全部的地区
-
3.贪心算法注意事项和细节#
- 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
- 比如上题的算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区
- 但是发现K2, K3, K4, K5也可以覆盖全部地区,如果K2的使用成本低于K1,那么上题的K1, K2, K3, K5虽然是满足条件,但是并不是最优的
/**
* 贪心算法最佳应用-集合覆盖问题
* 使用**贪婪算法**,效率高:目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合
*/
public class E_GreedyAlgorithm {
public static void main(String[] args) {
//创建广播台,放入到Map<广播台, 覆盖地区>
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
//放入广播台
HashSet<String> k1 = new HashSet<>();
k1.add("北京");
k1.add("上海");
k1.add("天津");
HashSet<String> k2 = new HashSet<>();
k2.add("广州");
k2.add("北京");
k2.add("深圳");
HashSet<String> k3 = new HashSet<>();
k3.add("成都");
k3.add("上海");
k3.add("杭州");
HashSet<String> k4 = new HashSet<>();
k4.add("上海");
k4.add("天津");
HashSet<String> k5 = new HashSet<>();
k5.add("杭州");
k5.add("大连");
//放入到Map<广播台, 覆盖地区>
broadcasts.put("K1", k1);
broadcasts.put("K2", k2);
broadcasts.put("K3", k3);
broadcasts.put("K4", k4);
broadcasts.put("K5", k5);
//存放所有的覆盖地区
HashSet<String> allAreas = new HashSet<>();
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("广州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大连");
//存放选择的广播台集合
List<String> selects = new ArrayList<>();
//临时集合,在遍历过程中,存放遍历过程中的广播台覆盖的地区和当前还没有覆盖的地区的交集
HashSet<String> tempSet = new HashSet<>();
//保存在一次遍历过程中能够覆盖最多未覆盖的地区对应的广播台的key,如果不为空则会加入selects
String maxKey = null;
while (allAreas.size() != 0) { //还没有覆盖到所有的地区
maxKey = null; //每进行一次while,置空maxKey
//1. 遍历所有的广播台,找到一个覆盖了最多**未覆盖的地区**的广播台(此广播台可能包含一些已覆盖的地区,但没有关系)
for (String key : broadcasts.keySet()) {
tempSet.clear(); //每次for循环清空tempSet
//当前广播台能够覆盖的地区
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;
}
}
//2. 将这个广播台加入到一个集合中(比如ArrayList),想办法把该广播台覆盖的地区在下次比较时去掉
if (maxKey != null) {
selects.add(maxKey);
//将maxKey指向的广播广播台覆盖的地区从allAreas中去掉
allAreas.removeAll(broadcasts.get(maxKey));
}
//3. 重复第1步直到覆盖了全部的地区
}
System.out.println(selects); //[K1, K2, K3, K5]
}
}
6.普里姆(Prim)算法#
1.最小生成树(Minimum Cost Spanning Tree)#
-
修路问题本质就是就是最小生成树(Minimum Cost Spanning Tree)问题,简称MST
2.普里姆算法介绍#
- 普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图
- 普利姆的算法如下:
- 设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
- 若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
- 若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1
- 重复步骤2,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边
3.普里姆算法最佳实践-修路问题#
- 有7个村庄(A, B, C, D, E, F, G),现在需要修路把7个村庄连通
- 各个村庄的距离用边线(权)表示,比如A–B距离5公里
- 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
- 解决方案:
- 将10条边,连接即可,但是总的里程数不是最小
- 正确思路:尽可能的选择少的路线,并且每条路线最小,保证总里程数最少
- 思路分析:
- 从顶点A开始处理(A-C[7],A-G[2],A-B[5]),得到最小路径==》A-G[2]
- 从顶点A,G开始处理,将A,G顶点和他们相邻的还没有访问的顶点进行处理(A-C[7],A-B[5],G-B[3],G-F[6],G-E[4]),得到最小路径==》G-B[3]
- 从顶点A,G,B开始处理,将A,G,B顶点和他们相邻的还没有访问的顶点进行处理(A-C[7],G-F[6],G-E[4],B-D[9]),得到最小路径==》G-E[4]
- 从顶点A,G,B,E开始处理,将A,G,B,E顶点和他们相邻的还没有访问的顶点进行处理(A-C[7],G-F[6],B-D[9],E-C[8],E-F[5]),得到最小路径==》E-F[5]
- 从顶点A,G,B,E,F开始处理,将A,G,B,E,F顶点和他们相邻的还没有访问的顶点进行处理(A-C[7],B-D[9],E-C[8],F-D[4]),得到最小路径==》F-D[4]
- 从顶点A,G,B,E,F,D开始处理,将A,G,B,E,F,D顶点和他们相邻的还没有访问的顶点进行处理(A-C[7],E-C[8]),得到最小路径==》A-C[7]
- 得到结果
/**
* 普里姆算法最佳实践-修路问题
* 1. 有7个村庄(A, B, C, D, E, F, G),现在需要修路把7个村庄连通
* 2. 各个村庄的距离用边线(权)表示,比如A–B距离5公里
* 3. 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
*/
public class F_Prim {
public static void main(String[] args) {
char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int verxs = data.length;
//使用二维数组表示邻接矩阵的关系,10000表示两个点不联通
int[][] weight = new int[][]{
//A, B, C, D, E, F, G
{10000, 5, 7, 10000, 10000, 10000, 2}, //A
{5, 10000, 10000, 9, 10000, 10000, 3}, //B
{7, 10000, 10000, 10000, 8, 10000, 10000}, //C
{10000, 9, 10000, 10000, 10000, 4, 10000}, //D
{10000, 10000, 8, 10000, 10000, 5, 4}, //E
{10000, 10000, 10000, 4, 5, 10000, 6}, //F
{2, 3, 10000, 10000, 4, 6, 10000} //G
};
//创建图对象
PrimGraphic graphic = new PrimGraphic(verxs);
//创建最小生成树
PrimMinTree primMinTree = new PrimMinTree();
System.out.println("邻接矩阵:");
primMinTree.createGraphic(graphic, verxs, data, weight);
primMinTree.showGraphic(graphic);
System.out.println("普里姆算法:");
primMinTree.prim(graphic, 0); //从A开始,2+3+4+5+4+7=25
}
}
//最小生成树
class PrimMinTree {
/**
* 创建图的邻接矩阵
*
* @param graphic 图对象
* @param verxs 图的节点个数
* @param data 图的节点数据
* @param weight 图的邻接矩阵
*/
public void createGraphic(PrimGraphic graphic, int verxs, char[] data, int[][] weight) {
int i, j;
for (i = 0; i < verxs; i++) { //顶点
graphic.data[i] = data[i]; //顶点的数据赋给图
for (j = 0; j < verxs; j++) {
graphic.weight[i][j] = weight[i][j]; //初始化图的邻接矩阵
}
}
}
//显示图的邻接矩阵
public void showGraphic(PrimGraphic primGraphic) {
for (int[] link : primGraphic.weight) {
System.out.println(Arrays.toString(link));
}
}
/**
* 普里姆算法,得到最小生成树
*
* @param graphic 图对象
* @param v 从图的第几个顶点开始生成('A'==>0)
*/
public void prim(PrimGraphic graphic, int v) {
int[] visited = new int[graphic.verxs]; //标记顶点是否被访问过,默认为0,表示未访问
visited[v] = 1; //把当前节点标记为已访问
int minWeight = 10000; //最短路径,默认最大
//记录两个顶点的下标
int v1 = -1;
int v2 = -1;
//因为有graphic.verxs个顶点,普利姆算法结束后,有graphic.verxs-1条边,所以k从1开始
for (int k = 1; k < graphic.verxs; k++) {
//确定每一次生成的子图和哪个节点的距离最近
for (int i = 0; i < graphic.verxs; i++) { //遍历已访问的节点
for (int j = 0; j < graphic.verxs; j++) { //遍历所有未访问的节点
//已访问的节点和未访问的节点进行连接,当前节点的权值小于最短路径
if (visited[i] == 1 && visited[j] == 0 && graphic.weight[i][j] < minWeight) {
//替换最短路径(已访问的节点和未访问的节点间的权值最小的边)
minWeight = graphic.weight[i][j];
v1 = i;
v2 = j;
}
}
}
//找到一条边
System.out.println("对应边:<" + graphic.data[v1] + ", " + graphic.data[v2] + "> 权值:" + minWeight);
//把当前节点标记为已访问
visited[v2] = 1;
//重置最短路径为最大值
minWeight = 10000;
}
}
}
//图
class PrimGraphic {
int verxs; //图的节点个数
char[] data; //图的节点数据
int[][] weight; //存放边,邻接矩阵
/**
* @param verxs 节点个数
*/
public PrimGraphic(int verxs) {
this.verxs = verxs;
this.data = new char[verxs];
this.weight = new int[verxs][verxs];
}
}
7.克鲁斯卡尔(Kruskal)算法#
1.克鲁斯卡尔算法介绍#
- 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法
- 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
- 具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止
2.克鲁斯卡尔算法图解说明-公交站问题#
-
看一个应用场景和问题:
- 某城市新增7个站点(A, B, C, D, E, F, G),现在需要修路把7个站点连通
- 各个站点的距离用边线(权)表示,比如A–B距离12公里
- 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
-
思路分析:
-
在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树
对于上图所示的连通网可以有多棵权值总和不相同的生成树:
-
假设,用数组R保存最小生成树结果:
- 将边<E,F>加入R中:边<E,F>的权值最小,因此将它加入到最小生成树结果R中
- 将边<C,D>加入R中:上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果R中
- 将边<D,E>加入R中:上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果R中
- 将边<B,F>加入R中:上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果R中
- 将边<E,G>加入R中:上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果R中
- 将边<A,B>加入R中:上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳过边<B,C>。将边<A,B>加入到最小生成树结果R中
- 此时,最小生成树构造完成!它包括的边依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>(2+3+4+7+8+12=36)
-
-
克鲁斯卡尔算法重点需要解决以下两个问题:
-
对图的所有边按照权值大小进行排序(采用排序算法进行排序)
-
将边添加到最小生成树中时,如何判断是否形成了回路(记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路)
-
/**
* 克鲁斯卡尔算法图解说明-公交站问题
* 1. 某城市新增7个站点(A, B, C, D, E, F, G),现在需要修路把7个站点连通
* 2. 各个站点的距离用边线(权)表示,比如A–B距离12公里
* 3. 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
*/
public class G_Kruskal {
public static final int INF = Integer.MAX_VALUE; //表示两个顶点不能连通
public static void main(String[] args) {
char[] vartexs = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = {
//A, B, C, D, E, F, G(0是同一个点,INF表示两个顶点不能连通)
{0, 12, INF, INF, INF, 16, 14}, //A
{12, 0, 10, INF, INF, 7, INF}, //B
{INF, 10, 0, 3, 5, 6, INF}, //C
{INF, INF, 3, 0, 4, INF, INF}, //D
{INF, INF, 5, 4, 0, 2, 8}, //E
{16, 7, 6, INF, 2, 0, 9}, //F
{14, INF, INF, INF, 8, 9, 0} //G
};
//创建图对象
KruskalGraphic graphic = new KruskalGraphic(vartexs, matrix);
System.out.println("邻接矩阵:");
graphic.showGraphic();
System.out.println("通过邻接矩阵获取图中的边:");
EData[] edges = graphic.getEdges();
System.out.println(Arrays.toString(edges));
System.out.println("从小到大排序:");
graphic.sortEdges(edges);
System.out.println(Arrays.toString(edges));
System.out.println("克鲁斯卡尔算法:");
graphic.kruskal();
}
}
//图的一条边
class EData {
public char start; //边的一个点
public char end; //边的另外一个点
public int weight; //边的权值
public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "EData{<" + start + ", " + end + "> = " + weight + '}';
}
}
//图
class KruskalGraphic {
public int edgeNum; //边的个数
public char[] vartexs; //顶点数组
public int[][] matrix; //邻接矩阵
public static final int INF = Integer.MAX_VALUE; //表示两个顶点不能连通
public KruskalGraphic(char[] vartexs, int[][] matrix) {
int vartexsLength = vartexs.length;
//初始化顶点(复制拷贝)
// this.vartexs = vartexs; //这种方式也可以
this.vartexs = new char[vartexsLength];
for (int i = 0; i < vartexs.length; i++) {
this.vartexs[i] = vartexs[i];
}
//初始化边(复制拷贝)
this.matrix = new int[vartexsLength][vartexsLength];
for (int i = 0; i < vartexsLength; i++) {
for (int j = 0; j < vartexsLength; j++) {
this.matrix[i][j] = matrix[i][j];
}
}
//统计边的数量
for (int i = 0; i < vartexsLength; i++) {
for (int j = i + 1; j < vartexsLength; j++) { //自身相连的边不统计
if (this.matrix[i][j] != INF) {
edgeNum++; //有效边,边的数量增加
}
}
}
}
//显示图的邻接矩阵
public void showGraphic() {
for (int i = 0; i < vartexs.length; i++) {
for (int j = 0; j < vartexs.length; j++) {
System.out.printf("%10d\t", matrix[i][j]);
}
System.out.println();
}
}
/**
* 对边进行排序(冒泡)
*
* @param edges 边的集合
*/
public 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 temp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = temp;
}
}
}
}
/**
* 根据顶点的值返回顶点对应的下标
*
* @param c 顶点的值('A')
* @return 顶点对应的下标,找不到返回-1
*/
public int getPosition(char c) {
for (int i = 0; i < vartexs.length; i++) {
if (vartexs[i] == c) {
return i; //找到
}
}
return -1; //找不到
}
/**
* 通过邻接矩阵获取图中的边放到EData[]数组中,以便遍历该数组
* EData[{'A', 'B', 12}]
*
* @return 边的集合
*/
public EData[] getEdges() {
int index = 0;
EData[] edges = new EData[edgeNum]; //初始化的时候已经统计边的数量edgeNum
for (int i = 0; i < vartexs.length; i++) {
for (int j = i + 1; j < vartexs.length; j++) {
if (matrix[i][j] != INF) { //有效边
edges[index++] = new EData(vartexs[i], vartexs[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 获取下标为i的顶点的终点:用于判断两个顶点是否指向同一个终点
*
* @param ends 各个顶点对应的终点,在遍历过程中逐步形成(动态)
* @param i 当前顶点的下标
* @return 当前顶点对应的终点的下标
*/
public int getEnd(int[] ends, int i) {
while (ends[i] != 0) {
i = ends[i];
}
return i;
}
//克鲁斯卡尔算法
public void kruskal() {
int index = 0; //最后结果数组的索引
int[] ends = new int[edgeNum]; //保存已有"最小生成树"中的每个顶点在"最小生成树"中的终点
EData[] result = new EData[edgeNum]; //创建结果数组,保存最终的"最小生成树"
//获取图中所有边的集合(共12条)
EData[] edges = getEdges();
//按照边的权值从小到大排序
sortEdges(edges);
//将边添加到最小生成树中时,判断是否形成了回路(没有:加入结果集)
for (int i = 0; i < edgeNum; i++) {
//获取到第i条边的第1个顶点(起点)
int p1 = getPosition(edges[i].start);
//获取到第i条边的第2个顶点(终点)
int p2 = getPosition(edges[i].end);
//获取p1在已有"最小生成树"中的终点
int e1 = getEnd(ends, p1);
//获取p2在已有"最小生成树"中的终点
int e2 = getEnd(ends, p2);
//是否形成了回路
if (e1 != e2) { //没有形成回路
ends[e1] = e2; //设置p1在已有"最小生成树"中的终点
result[index++] = edges[i]; //将一条边加入结果集
}
}
//输出结果集
for (int i = 0; i < index; i++) {
System.out.println(result[i]);
}
}
}
8.迪杰斯特拉(Dijkstra)算法#
1.迪杰斯特拉算法介绍#
- 迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止
2.迪杰斯特拉算法过程#
- 设置出发顶点为v,顶点集合
V{v1, v2, vi...}
,v到V中各顶点的距离构成距离集合D,D{d1, d2, di...}
,D集合记录着v到图中各顶点的距离(到自身可以看作0,v到vi距离对应为di)- 从D中选择值最小的di并移出D集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径
- 更新D集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的)
- 重复1,2的步骤,直到最短路径顶点为目标顶点即可结束
3.迪杰斯特拉算法最佳应用-最短路径#
- 有7个村庄(A, B, C, D, E, F, G),现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄
- 各个村庄的距离用边线(权)表示,比如A–B距离5公里
- 问:如何计算出G村庄到其它各个村庄的最短距离 ?
- 如果从其它点出发到各个点的最短距离又是多少?
/**
* 迪杰斯特拉算法最佳应用-最短路径
* 1. 有7个村庄(A, B, C, D, E, F, G),现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄
* 2. 各个村庄的距离用边线(权)表示,比如A–B距离5公里
* 3. 问:如何计算出G村庄到其它各个村庄的最短距离 ?
* 4. 如果从其它点出发到各个点的最短距离又是多少?
*/
public class H_Dijkstra {
public static void main(String[] args) {
char[] vartexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = new int[vartexs.length][vartexs.length];
final int N = 65535; //表示不可连接
//创建邻接矩阵:A, B, C, D, E, F, G
matrix[0] = new int[]{0, 5, 7, N, N, N, 2}; //A
matrix[1] = new int[]{5, 0, N, 9, N, N, 3}; //B
matrix[2] = new int[]{7, N, 0, N, 8, N, N}; //C
matrix[3] = new int[]{N, 9, N, 0, N, 4, N}; //D
matrix[4] = new int[]{N, N, 8, N, 0, 5, 4}; //E
matrix[5] = new int[]{N, N, N, 4, 5, 0, 6}; //F
matrix[6] = new int[]{2, 3, N, N, 4, 6, N}; //G
//创建图对象
DijkstraGraphic graphic = new DijkstraGraphic(vartexs, matrix);
System.out.println("邻接矩阵:");
graphic.showGraphic();
System.out.println("迪杰斯特拉算法:");
graphic.dijkstra(6);
graphic.showDijkstra(vartexs);
}
}
//图
class DijkstraGraphic {
private char[] vartexs; //顶点数组
private int[][] matrix; //邻接矩阵
private VisitedVertex vv; //已访问的顶点集合
public DijkstraGraphic(char[] vartexs, int[][] matrix) {
this.vartexs = vartexs;
this.matrix = matrix;
}
//显示图的邻接矩阵
public void showGraphic() {
for (int[] link : matrix) {
System.out.println(Arrays.toString(link));
}
}
/**
* 更新index顶点到周围顶点的距离和周围顶点的前驱顶点
*
* @param index 当前节点
*/
public void update(int index) {
int len = 0;
//遍历邻接矩阵的matrix[index]行
for (int j = 0; j < matrix[index].length; j++) {
//出发顶点到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顶点的距离
}
}
}
/**
* 迪杰斯特拉算法
*
* @param index 出发顶点的下标
*/
public void dijkstra(int index) {
vv = new VisitedVertex(vartexs.length, index);
//更新index顶点到周围顶点的距离和前驱节点
update(index);
for (int j = 1; j < vartexs.length; j++) { //有一个顶点已访问过
//选择并返回新的访问顶点
index = vv.updateArr();
//更新index顶点到周围顶点的距离和前驱节点
update(index);
}
}
/**
* 显示结果
*
* @param vartexs 顶点数组
*/
public void showDijkstra(char[] vartexs) {
vv.show(vartexs);
}
}
//已访问的顶点集合
class VisitedVertex {
public int[] alreadyArr; //记录各个顶点是否访问过:1访问过,0未访问,会动态更新
public int[] preVisited; //每个下标对应的值为前一个顶点下标,会动态更新
public int[] dis; //记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其它顶点的距离,会动态更新,求的最短距离就会存放到dis
/**
* @param length 顶点个数
* @param index 出发顶点下标
*/
public VisitedVertex(int length, int index) {
this.alreadyArr = new int[length];
this.preVisited = new int[length];
this.dis = new int[length];
//设置出发顶点被访问过为1
this.alreadyArr[index] = 1;
//初始化dis数组,填充为最大值
Arrays.fill(dis, 65535);
//设置出发顶点的访问距离为0
this.dis[index] = 0;
}
/**
* 判断该顶点是否被访问过
*
* @param index 该顶点下标
* @return true为访问过
*/
public boolean in(int index) {
return alreadyArr[index] == 1;
}
/**
* 更新出发顶点到要更新的顶点的距离
*
* @param index 要更新的顶点的下标
* @param len 距离
*/
public void updateDis(int index, int len) {
dis[index] = len;
}
/**
* 更新pre顶点的前驱节点为index
*
* @param pre 当前前驱节点下标
* @param index 更新后的前驱节点
*/
public void updatePre(int pre, int index) {
preVisited[pre] = index;
}
/**
* 返回出发顶点到index顶点的距离
*
* @param index 当前顶点
* @return 新的距离
*/
public int getDis(int index) {
return dis[index];
}
/**
* 继续选择并返回新的访问顶点, 比如G完后,**A点作为新的访问顶点(注意不是出发顶点)**
*
* @return 新的访问顶点
*/
public int updateArr() {
int min = 65535, index = 0;
for (int i = 0; i < alreadyArr.length; i++) {
//i顶点未访问过,且当前顶点距离比最小距离小
if (alreadyArr[i] == 0 && dis[i] < min) {
min = dis[i]; //更新最小距离
index = i; //更新新的访问顶点
}
}
//更新index顶点被访问过
alreadyArr[index] = 1;
return index;
}
/**
* 显示结果
*
* @param vartexs 顶点数组
*/
public void show(char[] vartexs) {
for (int i : alreadyArr) {
System.out.print(i + " ");
}
System.out.println("《=记录各个顶点是否访问过");
for (int i : preVisited) {
System.out.print(i + " ");
}
System.out.println("《=每个下标对应的值为前一个顶点下标");
int count = 0;
for (int i : dis) {
if (i != 65535) {
System.out.print(vartexs[count] + "(" + i + ") ");
} else { //不能连接
System.out.print("N");
}
count++;
}
System.out.println("《=记录出发顶点到其他所有顶点的距离");
}
}
9.弗洛伊德(Floyd)算法#
1.弗洛伊德算法介绍#
- 和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
- 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
- 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径
- 弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径
2.弗洛伊德算法图解分析#
-
设置顶点vi到顶点vk的最短路径已知为Lik,顶点vk到vj的最短路径已知为Lkj,顶点vi到vj的路径为Lij,则vi到vj的最短路径为:min((Lik+Lkj), Lij),vk的取值为图中所有顶点,则可获得vi到vj的最短路径
-
至于vi到vk的最短路径Lik或者vk到vj的最短路径Lkj,是以同样的方式获得
3.弗洛伊德算法最佳应用-最短路径#
- 有7个村庄(A, B, C, D, E, F, G)
- 各个村庄的距离用边线(权)表示,比如A–B距离5公里
- 问:如何计算出各村庄到其它各村庄的最短距离?
-
思路分析:
/**
* 弗洛伊德算法最佳应用-最短路径
* 1. 有7个村庄(A, B, C, D, E, F, G)
* 2. 各个村庄的距离用边线(权)表示,比如A–B距离5公里
* 3. 问:如何计算出各村庄到其它各村庄的最短距离?
*/
public class I_Floyd {
public static void main(String[] args) {
char[] vartex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = new int[vartex.length][vartex.length];
final int N = 65535; //表示不可连接
//创建邻接矩阵:A, B, C, D, E, F, G
matrix[0] = new int[]{0, 5, 7, N, N, N, 2}; //A
matrix[1] = new int[]{5, 0, N, 9, N, N, 3}; //B
matrix[2] = new int[]{7, N, 0, N, 8, N, N}; //C
matrix[3] = new int[]{N, 9, N, 0, N, 4, N}; //D
matrix[4] = new int[]{N, N, 8, N, 0, 5, 4}; //E
matrix[5] = new int[]{N, N, N, 4, 5, 0, 6}; //F
matrix[6] = new int[]{2, 3, N, N, 4, 6, 0}; //G
//创建图对象
FloydGraphic graph = new FloydGraphic(matrix, vartex);
System.out.println("邻接矩阵:");
graph.show(vartex);
System.out.println("弗洛伊德算法:");
graph.floyd();
graph.show(vartex);
}
}
//图
class FloydGraphic {
private char[] vartex; //存放顶点
private int[][] dis; //存放从各个顶点出发到其它顶点的距离,以及最后的结果
private int[][] pre; //存放到达目标顶点的前驱节点下标
/**
* @param matrix 邻接矩阵
* @param vartex 顶点数组
*/
public FloydGraphic(int[][] matrix, char[] vartex) {
int vartexsLength = vartex.length;
this.vartex = vartex;
this.dis = matrix;
this.pre = new int[vartexsLength][vartexsLength];
//初始化pre数组
for (int i = 0; i < vartexsLength; i++) {
Arrays.fill(pre[i], i);
}
}
//显示pre数组和dis数组
public void show(char[] vartex) {
System.out.println("前驱关系表:");
for (int k = 0; k < dis.length; k++) {
//输出pre数组
for (int i = 0; i < dis.length; i++) {
System.out.print(vartex[pre[k][i]] + " ");
}
System.out.println();
}
System.out.println("距离表:");
for (int k = 0; k < dis.length; k++) {
//输出dis数组
for (int i = 0; i < dis.length; i++) {
System.out.print("<" + vartex[k] + ", " + vartex[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):[A, B, C, D, E, F, G]
for (int j = 0; j < dis.length; j++) {
len = dis[i][k] + dis[k][j]; //从i出发,经过中间顶点k,到达j的距离
//经过中间顶点的距离小于直连的距离
if (len < dis[i][j]) {
dis[i][j] = len; //更新成经过中间顶点的距离
pre[i][j] = pre[k][j]; //更新前驱顶点
}
}
}
}
}
}
10.马踏棋盘算法#
1.马踏棋盘算法介绍和游戏演示#
-
马踏棋盘算法也被称为骑士周游问题。将马随机放在国际象棋的 8×8 棋盘 Board [0~7] [0~7] 的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格
-
解决方式:图的深度优化遍历算法(DFS) + 贪心算法优化
2.马踏棋盘游戏#
-
马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用
-
如果使用回溯(深度优先搜索)来解决,假如马踏了53个点,如图:走到了第53个,坐标(1,0),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯……
-
分析第一种方式的问题,并使用贪心算法(greedyalgorithm)进行优化
3.简单回溯法解决马踏棋盘的问题#
-
思路分析:
- 创建棋盘
chessboard[][]
,是一个二维数组 - 将当前位置设置为已访问,然后根据当前位置(Point),计算马还能走哪些位置,并放入到一个集合中(ArrayList),最多有8个位置,每走一步,就使用step+1
- 遍历ArrayList中存放的所有位置,看看哪个可以走通,如果走通,就继续,走不通,就回溯
- 判断马是否完成了任务,使用step和应该走的步数比较,如果没有达到数量,则表示没有完成任务,将整个棋盘置0
- 注意:马不同的走法(策略),会得到不同的结果,效率也会有影响(优化)
- 创建棋盘
4.深度优先搜索+贪心算法优化#
-
使用贪心算法对原来的算法优化:
-
获取当前位置可以走的下一个位置的集合
ArrayList<Point> ps = next(new Point(column, row)); //获取当前位置可以走的下一个位置的集合(X:列,Y:行)
-
需要对ps中所有的Point的下一步的所有集合的数目,进行非递减排序(允许有重复的数据),就ok
9, 7, 6, 5, 3, 2, 1 //递减排序 1, 2, 3, 4, 5, 6, 9 //递增排序 1, 2, 2, 2, 3, 3, 4, 5, 6 //非递减排序 9, 7, 6, 6, 6, 5, 5, 2, 1 //非递增排序
-
/**
* 马踏棋盘算法也被称为骑士周游问题
* 将马随机放在国际象棋的 8×8 棋盘 Board [0~7] [0~7] 的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格
*/
public class J_HorseChessboard {
private static int X; //棋盘的列数
private static int Y; //棋盘的行数
private static boolean[] visited; //标记棋盘的各个位置是否被访问过
private static boolean finished; //标记棋盘的所有位置是否都被访问
public static void main(String[] args) {
X = 8;
Y = 8;
int row = 1; //马初始位置的行(从1开始)
int column = 1; //马初始位置的列(从1开始)
//1.创建棋盘`chessboard[][]`,是一个二维数组
int[][] chessboard = new int[X][Y];
visited = new boolean[X * Y]; //默认为false
long start = System.currentTimeMillis();
// horseChessboard(chessboard, row - 1, column - 1, 1); //20870
horseChessboardOptimization(chessboard, row - 1, column - 1, 1); //15
long end = System.currentTimeMillis();
System.out.println("用时(毫秒)" + (end - start));
System.out.println("马踏棋盘算法:");
for (int[] rows : chessboard) {
for (int step : rows) {
System.out.print(step + " ");
}
System.out.println();
}
}
/**
* 2.根据当前位置(Point)计算马还能走哪些位置,并放入到一个集合中(ArrayList),最多有8个位置,每走一步,就使用step+1
*
* @param curPoint 当前的点
* @return 下一个位置的集合
*/
public static ArrayList<Point> next(Point curPoint) {
//创建ArrayList
ArrayList<Point> ps = new ArrayList<>();
//创建Point(坐标原点是左上角)
Point p = new Point();
//表示马可以走 5 这个位置
if ((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p));
}
//判断马可以走 6 这个位置
if ((p.x = curPoint.x - 1) >= 0 && (p.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p));
}
//判断马可以走 7 这个位置
if ((p.x = curPoint.x + 1) < X && (p.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p));
}
//判断马可以走 0 这个位置
if ((p.x = curPoint.x + 2) < X && (p.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p));
}
//判断马可以走 1 这个位置
if ((p.x = curPoint.x + 2) < X && (p.y = curPoint.y + 1) < Y) {
ps.add(new Point(p));
}
//判断马可以走 2 这个位置
if ((p.x = curPoint.x + 1) < X && (p.y = curPoint.y + 2) < Y) {
ps.add(new Point(p));
}
//判断马可以走 3 这个位置
if ((p.x = curPoint.x - 1) >= 0 && (p.y = curPoint.y + 2) < Y) {
ps.add(new Point(p));
}
//判断马可以走 4 这个位置
if ((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y + 1) < Y) {
ps.add(new Point(p));
}
return ps;
}
/**
* 马踏棋盘算法
*
* @param chessboard 棋盘
* @param row 马当前位置的行(从0开始)
* @param column 马当前位置的列(从0开始)
* @param step 第几步,初始位置是第1步
*/
public static void horseChessboard(int[][] chessboard, int row, int column, int step) {
chessboard[row][column] = step; //标记当前棋盘执行的是第几步
visited[row * X + column] = true; //标记当前位置已访问(从0开始:4 * 8 + 4 = 36)
//获取当前位置可以走的下一个位置的集合(X:列,Y:行)
ArrayList<Point> ps = next(new Point(column, row));
//3.遍历ArrayList中存放的所有位置,看看哪个可以走通,如果走通,就继续,走不通,就**回溯**
while (!ps.isEmpty()) {
Point p = ps.remove(0); //取出下一个可以走的位置
//判断该点是否已经访问过
if (!visited[p.y * X + p.x]) { //未访问过(row:p.y,column:p.x)
horseChessboard(chessboard, p.y, p.x, step + 1);
}
}
//4.判断马是否完成了任务,使用step和**应该走的步数**比较,如果没有达到数量,则表示没有完成任务,将整个棋盘置0
if (step < X * Y && !finished) { //未走完/处于回溯过程,且未结束
chessboard[row][column] = 0; //将整个棋盘置0
visited[row * X + column] = false; //该点置为未访问
} else {
finished = true;
}
}
//根据当前位置的所有的下一步的选择位置,进行非递减排序(减少回溯次数)
public static void sort(ArrayList<Point> ps) {
ps.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
//1.获取当前位置可以走的下一个位置的集合
int count1 = next(o1).size();
int count2 = next(o2).size();
//2.需要对ps中所有的Point的下一步的所有集合的数目,进行非递减排序(允许有重复的数据),就ok
if (count1 < count2) {
return -1;
} else if (count1 == count2) {
return 0;
} else {
return 1;
}
}
});
}
/**
* 使用贪心算法对马踏棋盘算法优化
*
* @param chessboard 棋盘
* @param row 马当前位置的行(从0开始)
* @param column 马当前位置的列(从0开始)
* @param step 第几步,初始位置是第1步
*/
public static void horseChessboardOptimization(int[][] chessboard, int row, int column, int step) {
chessboard[row][column] = step; //标记当前棋盘执行的是第几步
visited[row * X + column] = true; //标记当前位置已访问(从0开始:4 * 8 + 4 = 36)
//获取当前位置可以走的下一个位置的集合(X:列,Y:行)
ArrayList<Point> ps = next(new Point(column, row));
//对ps进行排序,排序规则:对ps的所有的Point的下一步的位置的数目,进行非递减排序(优化)
sort(ps);
//3.遍历ArrayList中存放的所有位置,看看哪个可以走通,如果走通,就继续,走不通,就**回溯**
while (!ps.isEmpty()) {
Point p = ps.remove(0); //取出下一个可以走的位置
//判断该点是否已经访问过
if (!visited[p.y * X + p.x]) { //未访问过(row:p.y,column:p.x)
horseChessboardOptimization(chessboard, p.y, p.x, step + 1);
}
}
//4.判断马是否完成了任务,使用step和**应该走的步数**比较,如果没有达到数量,则表示没有完成任务,将整个棋盘置0
if (step < X * Y && !finished) { //未走完/处于回溯过程,且未结束
chessboard[row][column] = 0; //将整个棋盘置0
visited[row * X + column] = false; //该点置为未访问
} else {
finished = true;
}
}
}
作者:n-ning-g
出处:https://www.cnblogs.com/n-ning-g/p/17781129.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
备注:你可以在这里自定义其他内容,支持 HTML
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix