文章目录
- 一、数据结构与算法
- 三、稀疏数组和队列
- 四、链表
- 五、栈
- 六、递归
- 七、排序算法
- 八、查找算法
- 九、哈希表
- 十、树结构的基础部分
- 十一、树结构实际应用
- 十二、多路查找树
- 十三、图
- 十四、程序员常用算法
一、数据结构与算法
线性结构:数组、队列、链表和栈
非线性结构:二维数组、多维数组、广义表、树结构、图结构
三、稀疏数组和队列
3.1.稀疏数组
因为该二维数组的很多值是默认值 0, 因此记录了很多没有意义的数据.->稀疏数组
3.1.2.应用实例
-
使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
-
把稀疏数组存盘,并且可以从新恢复原来的二维数组数
-
整体思路分析
注意:稀疏数组中的除了第一行的row 和col 外,其他的都是索引,默认0是第一行
-
二维数组转 稀疏数组的思路
1、遍历 原始的二维数组,得到有效数据的个数 sum
2、根据sum 就可以创建稀疏数组 sparseArr int[sum+1][3]
3、将二维数组的有效数据存入到 稀疏数组中,之后存入到磁盘中
-
稀疏数组转原始的二维数组的思路
1、先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的chessArr2=int[11][11]]
2、在读取稀疏数组后几行的数据,并赋给原始的二维数组即可
3.1.3.代码实现
//创建一个原始的二维数组 11*11
//0:表示没有棋子,1表示黑子 2表示蓝字
int chessArr1[][] = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][3] = 2;
chessArr1[4][5] = 2;
//输出原始的二维数组
System.out.println("原始的二维数组");
for (int[] row : chessArr1) {
for (int data : row) {
System.out.printf("%d\t", data);
}
System.out.println();
}
//将二维数组转 稀疏数组的思路
//1.先遍历二维数组 得到非0数据的个数
int sum = 0;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if (chessArr1[i][j] != 0) {
sum++;
}
}
}
//2.创建对应的稀疏数组
int sparseArr[][] = new int[sum + 1][3];
//给稀疏数组赋值
sparseArr[0][0] = 11;
sparseArr[0][1] = 11;
sparseArr[0][2] = sum;
//遍历二维数组 将非零的
int count = 0; //count用于记录是第几个非零数据
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if (chessArr1[i][j] != 0) {
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessArr1[i][j];
}
}
}
//输出稀疏数组的形式
System.out.println();
System.out.println("得到稀疏数组为~~~");
for (int i = 0; i < sparseArr.length; i++) {
System.out.printf("%d\t%d\t%d\t\n", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]);
}
System.out.println();
//将稀疏数组----》 恢复成原始的二维数组
/**
* 1、先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的chessArr2=int[11][11]
* 2、在读取稀疏数组后几行的数据,并赋给原始的二维数组即可
*/
//1、先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的chessArr2=int[11][11]
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
//2、在读取稀疏数组后几行的数据,并赋给原始的二维数组即可
for (int i = 1; i < sparseArr.length; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
//输出恢复后的二维数组
System.out.println();
System.out.println("恢复后的二维数组");
for (int[] row : chessArr2) {
for (int data : row) {
System.out.printf("%d\t", data);
}
System.out.println();
}
3.1.4.课后作业
要求:
\1) 在前面的基础上,将稀疏数组保存到磁盘上,比如 map.data
\2) 恢复原来的数组时,读取 map.data 进行恢复
//将稀疏数组保存到磁盘上,如map.data
System.out.println("将稀疏数组保存到磁盘并命名为map.data");
File f = new File("D:\\test\\map.data");
FileOutputStream fos = new FileOutputStream(f);
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
System.out.println("写入中----------");
for(int i = 0; i < sparseArr.length; i++) {
osw.write(sparseArr[i][0] + "," + sparseArr[i][1] + "," + sparseArr[i][2] + ",");
}
osw.close();//关闭输出流
fos.close();//关闭输出流
System.out.println("写入磁盘成功");
//读取磁盘中的map.data文件
System.out.println("读取中----------");
FileInputStream fis = new FileInputStream(f);
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
StringBuffer sb = new StringBuffer();
while(isr.ready()) {
sb.append((char)isr.read());
}
isr.close();//关闭输入流
fis.close();//关闭输入流
System.out.println("读取成功");
String ss = sb.toString();
String[] sb1 = sb.toString().split(",");
System.out.printf("从磁盘读取的字符串为:\n%s\n", ss);//格式化输出
//恢复稀疏数组
int sum1 = 0;
int[][] sparseArr1 = new int[sb1.length/3][3];
sparseArr1[0][0] = Integer.parseInt(sb1[0]);
sparseArr1[0][1] = Integer.parseInt(sb1[1]);
sparseArr1[0][2] = Integer.parseInt(sb1[2]);
for(int i = 3; i < sb1.length; i += 3) {
sum1++;
sparseArr1[sum1][0] = Integer.parseInt(sb1[i]);
sparseArr1[sum1][1] = Integer.parseInt(sb1[i+1]);
sparseArr1[sum1][2] = Integer.parseInt(sb1[i+2]);
}
System.out.println("还原后的稀疏数组为:");
for(int i = 0; i < sparseArr1.length; i++) {
System.out.printf("%d\t%d\t%d\n", sparseArr1[i][0], sparseArr1[i][1], sparseArr1[i][2]);
}
//恢复原始二维数组
int[][] chessArr3 = new int[sparseArr1[0][0]][sparseArr1[0][1]];
for(int i = 1; i < sparseArr1.length; i++) {
chessArr3[sparseArr1[i][0]][sparseArr1[i][1]] = sparseArr1[i][2];
}
System.out.println("还原后的二维数组为:");
for(int[] row : chessArr3) {
for(int data : row) {
System.out.printf("%d\t", data);
}
System.out.println();
}
3.2.队列
-
队列是一个有序列表,可以用数组或是链表来实现。
-
遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
-
示意图:(使用数组模拟队列示意图)
3.1.普通队列
问题分析并优化
-
目前数组使用一次就不能用, 没有达到复用的效果
-
将这个数组使用算法,改进成一个环形的队列 取模:%
3.2.数组模拟环形队列
对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取模的方式来实现即可)
- 分析说明 :
- 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的
时候需要注意**(rear + 1) % maxSize == front** 为队满
-
rear == front [空]
-
分析示意图:
队列中有效的数据的个数**(rear + maxSize - front)%maxSize**
因为rear - front 可能为负数,所以需要加上 maxSize
3.2.1 代码实现
package com.million.queue;
import java.util.Scanner;
/**
* @Author: studyboy
* @Date: 2021/12/14 20:46
*/
public class CircleArrayQueue {
public static void main (String[] args) {
//测试
System.out.println("测试数组模拟环形队列的案例");
//创建一个队列 设置了4 其队列有效个数为3
CircleArray queue = new CircleArray(4);
char key = ' '; //接收用户输入
Scanner scanner = new Scanner(System.in);//
boolean loop = true;
//输出一个菜单
while (loop) {
System.out.println("s(show): 显示队列");
System.out.println("e(exit): 退出程序");
System.out.println("a(add): 添加数据到队列");
System.out.println("g(get): 从队列取出数据");
System.out.println("h(head): 查看队列头的数据");
key = scanner.next().charAt(0);//接收一个字符
switch (key) {
case 's':
queue.showQueue();
break;
case 'a':
System.out.println("输出一个数");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'g': //取出数据
try {
int res = queue.getQueue();
System.out.printf("取出的数据是%d\n", res);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
break;
case 'h': //查看队列头的数据
try {
int res = queue.headQueue();
System.out.printf("队列头的数据是%d\n", res);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
break;
case 'e': //退出
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
class CircleArray {
private int maxSize; // 表示数组的最大容量
/**
* front 变量的含义做一个调整: front 就指向队列的第一个元素,
* 也就是说 arr[front] 就是队列的第一个元素
* front 的初始值 = 0
*/
private int front;
/**
* rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置.
* 因为希望空出一个空间做为约定.
* rear 的初始值 = 0
*/
private int rear;
private int[] arr; // 该数据用于存放数据, 模拟队列
public CircleArray (int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
}
// 判断队列是否满
public boolean isFull () {
return (rear + 1) % maxSize == front;
}
// 判断队列是否为空
public boolean isEmpty () {
return rear == front;
}
// 添加数据到队列
public void addQueue (int n) {
// 判断队列是否满
if (isFull()) {
System.out.println("队列满,不能加入数据~");
return;
}
//直接将数据加入
arr[rear] = n;
//将rear 后移,这里必须考虑取模
rear = (rear + 1) % maxSize;
}
// 获取队列的数据, 出队列
public int getQueue () {
// 判断队列是否空
if (isEmpty()) {
// 通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
/**
* 这里需要分析出front 是指向队列的第一个元素
* 1.先把front 对应的值保留到一个临时变量中
* 2.将front 后移,考虑取模
* 3.将临时变量的变量返回
*/
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
// 显示队列的所有数据
public void showQueue () {
// 遍历
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
//思路:从front开始遍历,遍历多少个元素
for (int i = front; i < front + size(); i++) {
//因为是环形队列 以maxSize为一个周期 i需要取模
System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
}
}
private int size () {
return (rear + maxSize - front) % maxSize;
}
// 显示队列的头数据, 注意不是取出数据
public int headQueue () {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front];
}
}
四、链表
4.1.链表的介绍
链表是有序的列表,但是它在内存中是存储如下
小结上图:
\1) 链表是以节点的方式来存储,是链式存储
\2) 每个节点包含 data 域, next 域:指向下一个节点.
\3) 如图:发现链表的各个节点不一定是连续存储.
\4) 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
单链表(带头结点) 逻辑结构示意图如下
使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作, 注: 删除和修改,查找
可以考虑学员独立完成,也可带学员完成
4.2.单链表实现方式
- 第一种方法在添加英雄时,直接添加到链表的尾部
思路分析示意图:
2.第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
思路的分析示意图:
3.修改节点功能
思路:
(1) 先找到该节点,通过遍历,
(2) temp.name = newHeroNode.name ; temp.nickname= newHeroNode.nickname
- 删除节点
思路分析的示意图:
4.2.1.代码实现
package com.million.linkedList;
/**
* @Author: studyboy
* @Date: 2021/12/15 15:58
*/
public class SingleLinkedListDemo {
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 linkedList = new SingleLinkedList();
// linkedList.add(hero1);
// linkedList.add(hero4);
// linkedList.add(hero3);
// linkedList.add(hero2);
linkedList.addByOrder(hero1);
linkedList.addByOrder(hero4);
linkedList.addByOrder(hero3);
linkedList.addByOrder(hero2);
linkedList.addByOrder(hero3);
linkedList.list();
//修改之后节点的代码
HeroNode newHeroNode = new HeroNode(2, "小卢", "王德峰");
linkedList.update(newHeroNode);
System.out.println("修改之后的代码");
linkedList.list();
System.out.println("---------------------------");
//删除节点
linkedList.del(5);
System.out.println("修改之后的代码");
linkedList.list();
System.out.println("------------------------");
//查询单个结点
linkedList.findSingleNode(2);
linkedList.findSingleNode(1);
}
}
//定义SingleLinkedList 管理我们的英雄
class SingleLinkedList {
//先创建一个头结点,头结点不能动
HeroNode head = new HeroNode(0, "", "");
/**
* 添加节点到单链表中
* 思路:当不考虑编号顺序时
* 1.找到当前链表的最后节点
* 2.将新创建的节点赋值给最后节点的next域
*
* @param heroNode
*/
//尾插法
public void add (HeroNode heroNode) {
//因为head不能动,我们需要一个辅助变量temp
HeroNode temp = head;
//遍历链表 找到最后的节点 也就是next=null的
while (true) {
//找到链表的最后
if (temp.next == null) {
break;
}
//没找到就让temp后移
temp = temp.next;
}
//当退出循环的时候,temp指向了链表的最后
temp.next = heroNode;
}
//第二种添加英雄的方式,根据英雄排名将其插入到指定的位置
//如果有这个排名 则添加失败 并给出提示
public void addByOrder (HeroNode heroNode) {
//因为头结点不能移动 ,因此我们需要一个辅助指针(变量)来帮助我们找到添加的位置
//因为是单链表 所以我们需要找到的temp 是添加位置的前一个
HeroNode temp = head;
boolean flag = false;//flag标志添加的编号是否存在 默认为false
while (true) {
//说明已经到链表的结尾了
if (temp.next == null) {
break;
}
//如果temp的下一个节点的编号比插入的节点的编号大,那么就插入到temp节点后面
if (temp.next.no > heroNode.no) {
break;
} else if (temp.next.no == heroNode.no) {
flag = true; //说明已经存在了这个编号
break;
}
//如果以上都不满足 则需要往后找
temp = temp.next;
}
if (flag) {
System.out.println(heroNode.no + "节点已经存在了,不能重复添加");
} else {
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点
//no 不修改 只修改name和nickName
public void update (HeroNode newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//定义一个 辅助变量temp
HeroNode temp = head.next;
boolean flag = false; //表示是否找到该节点
//找到需要修改的no编号
while (true) {
if (temp == null) {
break; //遍历完成
}
if (temp.no == newHeroNode.no) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
} else {
System.out.println("没有找到编号为" + newHeroNode.no + "的英雄");
}
}
//删除节点
//根据no来删除
public void del (int no) {
//temp 是带删除的前一个结点
HeroNode temp = head;
boolean flag = false; //判断是否找到了这个节点
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no == no) {
flag = true; //已经找到了
break;
}
temp = temp.next;
}
if (flag) {
temp.next = temp.next.next;
} else {
System.out.println("没有找到需要删除的" + no + "号结点");
}
}
//查找单个结点
//根据编号来查找
public void findSingleNode (int no) {
HeroNode temp = head.next;
boolean flag = false;
while (true) {
if (temp == null) {
break;
}
if(temp.no == no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
System.out.println(temp);
}else{
System.out.println("没有找到需要删除的" + no + "号结点");
}
}
//显示链表(遍历)
public void list () {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//定义一个 辅助变量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 no;
public String name;
public String nickName;
public HeroNode next; //指向下一个节点
public HeroNode (int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
//为了显示方法 需重写toString()方法
@Override
public String toString () {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
查询单链表中的结点个数
//查询单链表中的结点个数
public static int getLength (HeroNode head) {
if (head.next == null) {
return 0;
}
int length = 0;
HeroNode cur = head.next;
while (cur != null) {
length++;
cur = cur.next;
}
return length;
}
4.2.2.单链表新浪面试题
面试题:查找单链表中的倒数第k个结点
//查找单链表中的倒数第k个结点【新浪】
/**
* 思路:先遍历链表 统计出有效结点个数
* 有效结点的个数减去第k个结点,然后循环遍历他们的差值
*/
public static HeroNode getLastNode (int num, HeroNode head) {
if (head.next == null) {
return null;
}
if(num <=0 || num> getLength(head)){
return null;
}
//用一个赋值遍历temp
HeroNode temp = head.next;
//getLength(head)返回的是链表有效个数
for (int i = 0; i < getLength(head) - num; i++) {
temp = temp.next;
}
return temp;
}
4.2.3.单链表百度面试题
**面试题:**从尾到头打印单链表 方式1:反向遍历 方式2:Stack栈
//从尾到头打印单链表 方式1:反向遍历(不推荐) 方式2:Stack栈(用栈)先进后出
/**
* 思路:可以利用栈,这个先进后出的特点,将数据压栈.如果弹栈
*/
public static void reversePrint (HeroNode head) {
if (head.next == null) {
return; //空链表
}
HeroNode temp = head.next;
//创建一个空栈
Stack<HeroNode> stack = new Stack<>();
while (temp != null) {
stack.push(temp);
temp = temp.next;
}
//逆序打印
while (stack.size() > 0) {
System.out.println(stack.pop());
}
}
4.2.4.单链表腾讯面试题
面试题:单链表的反转
//单链表的反转
/**
* 思路:1.定义一个新的结点reverseHead
* 2.遍历链表 从头到尾,每遍历一个链表就取下来放到新结点reverseHead后面
* 3.最后将head结点指向放在reverseHead后面的结点
*/
public static void reverseList (HeroNode head) {
//定义一个新的结点reverseHead
HeroNode reverseHead = new HeroNode(0,"","");
if (head.next == null || head.next.next == null) {
return;
}
//定义临时结点
HeroNode temp = head.next;
HeroNode next = null; //这个结点需要指向当前结点【temp】的下一个
while (temp !=null) {
//先保存当前结点的下一个结点 要不然会断开连接
next = temp.next;
//将当前结点的next指向reverseHead结点的next
temp.next = reverseHead.next;
//将reverseHead结点指向temp
reverseHead.next = temp;
temp = next;
}
head.next = reverseHead.next;
}
4.2.5.课后练习
合并两个有序的单链表,合并之后的链表依然有序
4.3.双向链表
4.4.1双向链表的操作分析和实现
使用带 head 头的双向链表实现 –水浒英雄排行榜
管理单向链表的缺点分析:
-
单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
-
单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除
时节点,总是找到 temp,temp 是待删除节点的前一个节点(认真体会).
- 分析了双向链表如何完成遍历,添加,修改和删除的思路
对上图的说明:
分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现
-
遍历 方和 单链表一样,只是可以向前,也可以向后查找
-
添加 (默认添加到双向链表的最后)
(1) 先找到双向链表的最后这个节点
(2) temp.next = newHeroNode
(3) newHeroNode.pre = temp;
-
修改 思路和 原来的单向链表一样.
-
删除
(1) 因为是双向链表,因此,我们可以实现自我删除某个节点
(2) 直接找到要删除的这个节点,比如 temp
(3) temp.pre.next = temp.next
(4) temp.next.pre = temp.pre;
4.4.2.代码实现
package com.million.linkedList;/** * @Author: studyboy * @Date: 2021/12/15 22:46 */public class DoubleLinkedListDemo { 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, "埼玉", "光头王"); HeroNode2 hero5 = new HeroNode2(5, "哈哈", "长得高"); //创建一个双向链表 DoubleLinkedList doubleLinkedList = new DoubleLinkedList();// doubleLinkedList.add(hero1);// doubleLinkedList.add(hero3);// doubleLinkedList.add(hero2);// doubleLinkedList.add(hero4); doubleLinkedList.addByOrder(hero1); doubleLinkedList.addByOrder(hero2); doubleLinkedList.addByOrder(hero3); doubleLinkedList.addByOrder(hero5); doubleLinkedList.addByOrder(hero4); doubleLinkedList.list(); //修改 HeroNode2 heroNode2 = new HeroNode2(4, "张三丰", "天下第一"); doubleLinkedList.update(heroNode2); System.out.println("修改之后的"); doubleLinkedList.list(); doubleLinkedList.del(5); System.out.println("删除之后的"); doubleLinkedList.list(); }}//创建双向链表class DoubleLinkedList { //先创建一个头结点,头结点不能动 private HeroNode2 head = new HeroNode2(0, "", ""); //调用头结点 public HeroNode2 getHead () { return head; } //显示双向链表(遍历) public void list () { //判断链表是否为空 if (head.next == null) { System.out.println("链表为空"); return; } //定义一个 辅助变量temp HeroNode2 temp = head.next; while (true) { //判断链表是否为空 if (temp == null) { break; } //输出节点的信息 System.out.println(temp); //需要将temp后移 temp = temp.next; } } //添加双向链表 public void add (HeroNode2 heroNode) { //因为head不能动,我们需要一个辅助变量temp HeroNode2 temp = head; //遍历链表 找到最后的节点 也就是next=null的 while (true) { //找到链表的最后 if (temp.next == null) { break; } //没找到就让temp后移 temp = temp.next; } //当退出循环的时候,temp指向了链表的最后 //形成双向链表 temp.next = heroNode; heroNode.pre = temp; } //修改双向链表 public void update (HeroNode2 newHeroNode) { //判断链表是否为空 if (head.next == null) { System.out.println("链表为空"); return; } //定义一个 辅助变量temp HeroNode2 temp = head.next; boolean flag = false; //表示是否找到该节点 //找到需要修改的no编号 while (true) { if (temp == null) { break; //遍历完成 } if (temp.no == newHeroNode.no) { flag = true; break; } temp = temp.next; } if (flag) { temp.name = newHeroNode.name; temp.nickName = newHeroNode.nickName; } else { System.out.println("没有找到编号为" + newHeroNode.no + "的英雄"); } } //从双向链表中删除一个结点 //(1) 因为是双向链表,因此,我们可以实现自我删除某个节点 //(2) 直接找到要删除的这个节点,比如 temp //注意:这个删除只能删除连续的结点 public void del (int no) { //判断当前链表是否为空 if (head.next == null) { System.out.println("链表为空,无法删除"); return; } HeroNode2 temp = head.next; //辅助遍历 boolean flag = false; //判断是否找到了这个节点 while (true) { if (temp == null) { break; } if (temp.no == no) { flag = true; //已经找到了链表最后结点的next break; } temp = temp.next; } if (flag) {// temp.next = temp.next.next;//单向链表 temp.pre.next = temp.next; //这句话有问题? //如果是最后一个结点,就不需要执行下面这句话,否则出现空指针 if (temp.next != null) { temp.next.pre = temp.pre; } } else { System.out.println("没有找到需要删除的" + no + "号结点"); } }//定义HeroNode2,每一个HeroNode就是一个节点class HeroNode2 { public int no; public String name; public String nickName; public HeroNode2 next; //指向下一个结点,默认为null public HeroNode2 pre; //指向前一个结点,默认为null public HeroNode2 (int no, String name, String nickName) { this.no = no; this.name = name; this.nickName = nickName; } //为了显示方法 需重写toString()方法 @Override public String toString () { return "HeroNode2{" + "no=" + no + ", name='" + name + '\'' + ", nickName='" + nickName + '\'' + '}'; }}
4.4.3.课后作业
双向链表的第二种添加方式,按照编号顺序 [示意图]按照单链表的顺序添加,稍作修改即可
//按照顺序编号插入 public void addByOrder (HeroNode2 heroNode) { //因为头结点不能移动 ,因此我们需要一个辅助指针(变量)来帮助我们找到添加的位置 //因为是双链表 所以我们需要找到的temp是添加位置的前一个 HeroNode2 temp = head; boolean flag = false;//flag标志添加的编号是否存在 默认为false while (true) { //说明已经到链表的结尾了 if (temp.next == null) { break; } //如果temp的下一个节点的编号比插入的节点的编号大,那么就插入到temp节点后面 if (temp.next.no > heroNode.no) { break; } else if (temp.next.no == heroNode.no) { flag = true; //说明已经存在了这个编号 break; } //如果以上都不满足 则需要往后找 temp = temp.next; } if (flag) { System.out.println(heroNode.no + "结点已经存在了,不能重复添加"); } else { //判断是temp.next是否为空 if(temp.next !=null){ heroNode.next = temp.next; //这是将heroNode的下一个结点的pre指向heroNode temp.next.pre = heroNode; temp.next = heroNode; heroNode.pre = temp; }else{ temp.next = heroNode; heroNode.pre = temp; } } }}
4.4.单项环形链表
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数
到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由
此产生一个出队编号的序列。
4.5.Josephu 问题(约瑟夫)
- **约瑟夫问题的示意图 **
提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结
点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直
到最后一个结点从链表中删除算法结束。
- 约瑟夫问题-创建环形链表的思路图解
- 约瑟夫问题-小孩出圈的思路分析图
为什么要在小孩报数前,先让first和helper指针移动k-1次?
因为是为了确保first是一直指向要报数的小孩结点,而helper则是确保指向报数小孩的前一个结点
4.5.1.代码实现
package com.million.linkedList;/** * @Author: studyboy * @Date: 2021/12/16 11:08 * 使用单项环形链表实现约瑟夫问题 */public class JosePfu { public static void main (String[] args) { //测试 CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList(); circleSingleLinkedList.addBoy(125); circleSingleLinkedList.showBoy(); //测试小孩出圈 circleSingleLinkedList.countBoy(10,20,125); }}//创建一个单项环形链表class CircleSingleLinkedList { //创建一个first结点,当前没有结点 private Boy first = null; //添加小孩结点,构成一个环形链表 public void addBoy (int nums) { //做一个数据校验 if (nums < 1) { System.out.println("数据不正确"); return; } //创建一个辅助遍历curBoy Boy curBoy = null; for (int i = 1; i <= nums; i++) { //创建结点 Boy boy = new Boy(i); //如果是第一个结点 让first指向该结点 并形成环形 if (i == 1) { first = boy; first.setNext(first); //形成环形 curBoy = first; //让curBoy指向第一个小孩 } else { curBoy.setNext(boy); boy.setNext(first); curBoy = boy; } } } //遍历当前的链表 public void showBoy () { if (first == null) { System.out.println("当前链表为空"); } //需要一个辅助变量 Boy curBoy = first; while (true) { //输出 System.out.println("小孩编号:" + curBoy.getNo()); //说明已经遍历结束 if (curBoy.getNext() == first) { break; } curBoy = curBoy.getNext(); //后移 } } //根据用户的输入,计算出小孩出圈的顺序 /** * @param startNo 表示从第几个小孩开始报数 * @param countNum 表示数几下 * @param nums 表示最初有多少小孩在圈中 */ public void countBoy (int startNo, int countNum, int nums) { //先对数据进行校验 if (first == null || startNo < 1 || startNo > nums) { System.out.println("参数输入有误,请重写输入"); } //需要创建一个辅助指针helper,事先应该指向环形链表的最后这个结点 Boy helper = first; while (true) { if (helper.getNext() == first) { //说明helper指向最后这个结点 break; } helper = helper.getNext(); } //1.要在小孩报数前,先让first和helper指针移动k-1次 //因为是为了确保first是一直指向要报数的小孩结点, // 而helper则是确保指向报数小孩的前一个结点 方便出圈 for (int i = 0; i < startNo - 1; i++) { first = first.getNext(); helper = helper.getNext(); } //2.当小孩报数的时候,让first和helper同时移动m - 1次,然后出圈 while (true) { if (helper == first) { //说明圈中只有一个结点 break; } //让first和helper同时移动countNum - 1次 for (int i = 0; i < countNum - 1; i++) { first = first.getNext(); helper = helper.getNext(); } System.out.println("小孩编号为" + first.getNo() + "出圈"); //删除出圈的结点 first = first.getNext(); helper.setNext(first); } System.out.println("最后一个小孩编号为" + first.getNo() + "出圈"); }}//创建一个Boy类,表示一个节点class Boy { private int no; private Boy next; //指向下一个结点,默认为空 public Boy (int no) { this.no = no; } public int getNo () { return no; } public void setNo (int no) { this.no = no; } public Boy getNext () { return next; } public void setNext (Boy next) { this.next = next; }}
五、栈
5.1.栈的介绍
1) 栈的英文为**(stack)**
2) **栈是一个先入后出(FILO-First In Last Out)**的有序列表。
3) 栈**(stack)**是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的
一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底**(Bottom)****。**
4) 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元
素最先删除,最先放入的元素最后删除
5) 图解方式说明出栈**(pop)和入栈(push)**的概念
5.2 栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以
回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆
栈中。
-
表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
-
二叉树的遍历。
-
图形的深度优先(depth 一 first)搜索法
5.3 栈的快速入门
- 用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,
下面我们就用数组模拟栈的出栈,入栈等操作。
- 实现思路分析,并画出示意图
5.3.1 代码实现:
package com.million.stack;import java.util.Scanner;/** * @Author: studyboy * @Date: 2021/12/16 21:31 * 栈 */public class ArrayStackDemo { public static void main (String[] args) { ArrayStack stack = new ArrayStack(4); String key = ""; boolean loop = true; //控制是否退出菜单 Scanner scanner = new Scanner(System.in); while (loop) { System.out.println("show: 表示显示栈"); System.out.println("exit: 退出程序"); System.out.println("push: 表示添加数据到栈(入栈)"); System.out.println("pop: 表示从栈取出数据(出栈)"); System.out.println("请输入你的选择"); key = scanner.next(); switch (key) { case "show": stack.list(); break; case "push": System.out.println("请输入一个数"); int value = scanner.nextInt(); stack.push(value); break; case "pop": try { int res = stack.pop(); System.out.printf("出栈的数据是 %d\n", res); } catch (Exception e) { //TODO:handle exception System.out.println(e.getMessage()); } break; case "exit": scanner.close(); loop = false; break; default: break; } } System.out.println("程序退出~~~"); }}class ArrayStack { private int maxSize; private int[] stack; private int top = -1; public ArrayStack (int maxSize) { this.maxSize = maxSize; stack = new int[this.maxSize]; } //栈满 public boolean isFull () { return top == maxSize - 1; } //栈空 public boolean isEmpty () { return top == -1; } //入栈 public void push (int value) { if (isFull()) { System.out.println("栈已经满了"); } top++; stack[top] = value; } //出栈 public int pop () { if (isEmpty()) { throw new RuntimeException("栈为空"); } int value = stack[top]; top--; return value; } //遍历栈 public void list () { if (isEmpty()) { System.out.println("栈为空"); } //需要从栈顶开始遍历 for (int i = top; i >= 0; i--) { System.out.printf("stack[%d]=%d\n", i, stack[i]); } }}
关于栈的一个小练习
5.3.2 课堂练习
将老师写的程序改成使用链表来模拟栈
package com.million.stack;/** * @Author: studyboy * @Date: 2021/12/16 22:03 * 使用链表来模拟栈 */public class StackLinkedList { public static void main (String[] args) { LinkedList linkedList = new LinkedList(); HeroNode node1 = new HeroNode(1); HeroNode node2 = new HeroNode(2); HeroNode node3 = new HeroNode(3); HeroNode node4 = new HeroNode(4); linkedList.add(node1); linkedList.add(node2); linkedList.add(node3); linkedList.add(node4); linkedList.list(); }}class LinkedList{ HeroNode head = new HeroNode(); //添加 头插法 public void add(HeroNode heroNode){ heroNode.setNext(head.getNext()); head.setNext(heroNode); } //遍历 public void list(){ if(head.getNext() == null){ System.out.println("链表为空"); } HeroNode temp = head.getNext(); while(true){ if(temp == null){ break; } System.out.println("栈的数据为:"+temp.getValue()); temp = temp.getNext(); } }}class HeroNode{ private int value; //存放值 private HeroNode next; //指向下一个结点 public HeroNode () { } public HeroNode (int value) { this.value = value; } public int getValue () { return value; } public void setValue (int value) { this.value = value; } public HeroNode getNext () { return next; } public void setNext (HeroNode next) { this.next = next; }}
5.4 栈实现综合计算器(中缀表达式)
使用栈来实现综合计算器-自定义优先级[priority]
7*2*2-5+1-5+3-4
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnGBBdmQ-1645524836174)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211225180317018.png)]
1.思路分析(图解)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bh3WM0bV-1645524836175)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211225180354863.png)]
2.代价实现
[1. 先实现一位数的运算, 2. 扩展到多位数的运算]
package com.million.stack;/** * @Author: studyboy * @Date: 2021/12/25 15:52 * 栈实现综合计算器 */public class Calculator { public static void main (String[] args) { String expression = "7*2*2-5+1-5+3-4"; //创建两个栈,数栈,一个符号栈 ArrayStack2 numStack = new ArrayStack2(10); ArrayStack2 operStack = new ArrayStack2(10); //定义需要的相关变量 int index = 0; //用于扫描 int num1 = 0; int num2 = 0; int oper = 0; int res = 0; char ch = ' '; //将每次扫描得到char保存到ch中 String keepNum = ""; //用于拼接多位数 //开始while循环的扫描expression while (!(index >= expression.length())) { //依次得到expression的每一个字符 ch = expression.substring(index, index + 1).charAt(0); //先判断ch 是符号 还是数字 if (operStack.isOper(ch)) { //判断当前符号栈是否为空 if (!operStack.isEmpty()) { //如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数, // 在从符号栈中pop出一个符号,进行运算, //将得到结果,入数栈,然后将当前的操作符入符号栈 if (operStack.priority(ch) <= operStack.priority(operStack.peek())) { num1 = numStack.pop(); num2 = numStack.pop(); oper = operStack.pop(); res = numStack.cal(num1, num2, oper); //把运算结果入数栈 numStack.push(res); //如果是同级别的话,就需要继续计算 if (!operStack.isEmpty() && operStack.priority(ch) == operStack.priority(operStack.peek())) { num1 = numStack.pop(); num2 = numStack.pop(); oper = operStack.pop(); res = numStack.cal(num1, num2, oper); //把运算结果入数栈 numStack.push(res); } //将当前的操作符入符号栈 operStack.push(ch); } else { //如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈。 operStack.push(ch); } } else { //当前的符号栈为空,就直接入栈 operStack.push(ch); } } else { //是数字,直接入数字栈 //因为是字符,存在ascill码的转换 /** 处理多位数的思路 * 1.当处理多位数时,不能发现时一个数就立即入栈,因为她可能是多位数的 * 2. 在处理数,需要向expression的表达式的index 后再看一位,如果是数继续扫描 * 如果是符号 就入栈 * 3.需要定义一个字符串变量 用于拼接 */ keepNum += ch; //如果ch 已经是最后一位了 if (index == expression.length() - 1) { numStack.push(Integer.parseInt(keepNum)); } else { //判断下一位是不是数字,如果是数字,就继续扫描,如果是运算符,就入栈 if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) { numStack.push(Integer.parseInt(keepNum)); //重要的!!!!keepNum需要清空 keepNum = ""; } } } //让index+1,并判断是否扫描到expression最后 index++; } //当表达式扫描完毕,就顺序的从数栈和符号中pop出相对应的数和符号,并运行 while (!operStack.isEmpty()) { //如果符号栈为空,则计算最后的结果 num1 = numStack.pop(); num2 = numStack.pop(); oper = operStack.pop(); res = numStack.cal(num1, num2, oper); numStack.push(res); //入栈 } System.out.printf("表达式%s=%d", expression, numStack.pop()); }}/** *用栈来实现中缀表达式的计算 */class ArrayStack2 { private int maxSize; private int[] stack; private int top = -1; public ArrayStack2 (int maxSize) { this.maxSize = maxSize; stack = new int[this.maxSize]; } //返回栈顶的值,但不是真正的出栈 public int peek () { return stack[top]; } //栈满 public boolean isFull () { return top == maxSize - 1; } //栈空 public boolean isEmpty () { return top == -1; } //入栈 public void push (int value) { if (isFull()) { System.out.println("栈已经满了"); } top++; stack[top] = value; } //出栈 public int pop () { if (isEmpty()) { //抛出异常 throw new RuntimeException("栈为空"); } int value = stack[top]; top--; return value; } //遍历栈 public void list () { if (isEmpty()) { System.out.println("栈为空"); 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 isOper (char val) { return val == '*' || val == '/' || val == '+' || val == '-'; } //计算方法 public int cal (int num1, int num2, int oper) { //res用于存放计算的结果 int res = 0; switch (oper) { case '+': res = num1 + num2; break; case '-': res = num2 - num1; //注意顺序 break; case '*': res = num2 * num1; break; case '/': res = num2 / num1; break; default: break; } return res; }}
课后的练习-给表达式加入小括号 (逆波兰表达式要用)
5.5.逆波兰表达式
也叫后缀表达式
我们完成一个逆波兰计算器,要求完成如下任务:
-
输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果
-
支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
-
思路分析
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下: 1.从左至右扫描,将 3 和 4 压入堆栈; 2.遇到+运算符,因此弹出 4 和 3(4 为栈顶元素,3 为次顶元素),计算出 3+4 的值,得 7,再将 7 入栈; 3.将 5 入栈; 4.接下来是×运算符,因此弹出 5 和 7,计算出 7×5=35,将 35 入栈; 5.将 6 入栈; 6.最后是-运算符,计算出 35-6 的值,即 29,由此得出最终结果
4.代码实现
package com.million.stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* @Author: studyboy
* @Date: 2021/12/25 20:07
* 逆波兰表达式 逆波兰计算器
*/
public class PolandNotation {
public static void main (String[] args) {
//先定义一个波兰表达式
//4 * 5 - 8 + 60 + 8 / 2 => 4 5 * 8 - 60 + 8 2 / +
// String suffixExpression = "30 4 + 5 * 6 -";
String suffixExpression = "4 5 * 8 - 60 + 8 2 / +";
/**
* 思路:
* 1. 先将"3 4 + 5 * 6 -" => 放到ArrayList中
* 2. 将ArrayList 传递给一个方法,遍历ArrayList 配合栈 完成计算
*/
List<String> list = getListString(suffixExpression);
System.out.println("rpnList=" + list);
int res = calculate(list);
System.out.println("计算的结果=" + res);
}
//将一个逆波兰表达式,依次将数据和运算符放入到 ArrayList 中
public static List<String> getListString (String suffixExpression) {
//将suffixExpression 分割
String[] split = suffixExpression.split(" "); //根据空格分割
ArrayList<String> list = new ArrayList<>();
//遍历split 保存到集合中
for (String ele : split) {
list.add(ele);
}
return list;
}
/**
* 计算机运行思路
* 1.从左至右扫描,将 3 和 4 压入堆栈;
* 2.遇到+运算符,因此弹出 4 和 3(4 为栈顶元素,3 为次顶元素),计算出 3+4 的值,得 7,再将 7 入栈; 3.将 5 入栈;
* 4.接下来是×运算符,因此弹出 5 和 7,计算出 7×5=35,将 35 入栈;
* 5.将 6 入栈;
* 6.最后是-运算符,计算出 35-6 的值,即 29,由此得出最终结果
*
* @param list
* @return
*/
//将ArrayList配合栈来计算
public static int calculate (List<String> list) {
//创建。只需要一个栈即可
Stack<String> stack = new Stack<>();
//遍历list
for (String item : list) {
//这里使用正则表达式来取出数
if (item.matches("\\d+")) {//匹配多位数
//入栈
stack.push(item);
} else {
//pop出两个数,并运算 再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
//把 res入栈
stack.push("" + res); //将数字 转成字符串
}
}
//最后留在stack中阿数据就是运算结果
return Integer.parseInt(stack.pop());
}
}
5.6.中缀表达式转换为后缀表达式(重点)
5.6.1.概述
可以看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长得情况下,因此在开发中,需要将 中缀表达式转成后缀表达式。
5.6.2.具体步骤如下
1.初始化两个栈:运算符栈s1和储存中间结果的栈s2;2.从左至右扫描中缀表达式;3.遇到操作数时,将其压s2;4.遇到运算符时,比较其与s1栈顶运算符的优先级: (1)如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈; (2)否则,若优先级比栈顶运算符的高,也将运算符压入s1; (3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较; 5.遇到括号时: (1)如果是左括号"(",则直接压入s1 (2)如果是右括号")",则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃6.重复步骤2至5,直到表达式的最右边7.将s1中剩余的运算符依次弹出并压入s28.依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
5.6.3.案例
将中缀表达式 “1+((2+3)*4)-5” 转换为后缀表达式的过程如下:
结果为:"1 2 3 + 4 * 5 - "
5.6.4.思路分析示意图
5.6.5.代码实现
public class PolandNotation { public static void main (String[] args) { //完成一个中缀表达式转换成后缀表达式 /** * 说明: * 1. 1+((2+3)x4)-5 => 1 2 3 + 4 * + 5 - * 2. 因为直接对str 进行操作,不方便,因此,先将"1+((2+3)x4)-5" =>中缀表达式转换成对应的List * // 即"1+((2+3)x4)-5" => ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] * 3. 将得到的中缀表达式对应的list => 后缀表达式对应的list * //即ArrayList [1,+,(,(,2,+,3,),x,4,),-,5] => ArrayList[1,2,3,+,4,*,+,5,-] */ String expression = "1+((2+3)*4)-5"; List<String> infixExpressionList = toInfixExpression(expression); System.out.println("中缀表达式对应的List" + infixExpressionList); //ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] List<String> suffixExpressionList = parseSuffixExpressionList(infixExpressionList); System.out.println("后缀表达式对应的List" + suffixExpressionList); //ArrayList[1,2,3,+,4,*,+,5,-] System.out.println("expression="+calculate(suffixExpressionList)); } /** * 将得到的中缀表达式对应的list => 后缀表达式对应的list * 即ArrayList [1,+,(,(,2,+,3,),x,4,),-,5] => ArrayList[1,2,3,+,4,*,+,5,-] * * @param list * @return */ public static List<String> parseSuffixExpressionList (List<String> list) { //定义两个栈 Stack<String> s1 = new Stack<>(); // 符号栈 //说明:因为s2这个栈,再整个转换过程中,没有pop操作,而且后面我们还需要逆序输出 //因此比较麻烦,这里我们就不用Stack<String> 直接使用List<String> s2 List<String> s2 = new ArrayList<>(); //储存中间结果的List s2 //遍历list for (String item : list) { //如果是一个数,加入到s2中 if (item.matches("\\d+")) { s2.add(item); } else if (item.equals("(")) { s1.push(item); } else if (item.equals(")")) { //如果是右括号")",则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃 while (!s1.peek().equals("(")) { s2.add(s1.pop()); } s1.pop();//!!! 将小括号消除 } else { //当item的优先级小于等于s1栈顶运算符,将s1栈顶的运算符弹出并压入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较 //问题:我们缺少一个比较优先级高低的方法 while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) { s2.add(s1.pop()); } //还需要将item 入栈 s1.push(item); } } //将s1中剩余的运算符依次弹出并压入s2 while (s1.size() != 0) { s2.add(s1.pop()); } return s2; //将list返回去 即为中缀表达式对应的后缀表达式 } /** * 方法:将 中缀表达式转换成对应的List * s="1+((2+3)x4)-5" * * @param s * @return */ public static List<String> toInfixExpression (String s) { //定义一个List,存放中缀表达式 对应的内容 List<String> list = new ArrayList<>(); int i = 0; //这是一个指针,用于遍历 中缀表达式字符串 String str; //用于多位数的拼接 char c; //每遍历到一个字符,就保存到c中 do { //如果c 是一个非数字,就需要加入到list中 if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) { list.add("" + c); i++; //i需要后移 } else { //如果是一个数,就需要考虑到多位数 //需要先将str 置为空串 str = ""; while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) { str += c; //拼接 i++; } list.add(str); } } while (i < s.length()); return list; }} /** * 这个Operation可以返回一个方法,即运算符的优先级 */class Operation { private static final int ADD = 1; private static final int SUB = 1; private static final int MUL = 2; private static final int DIV = 2; public static int getValue (String operation) { int result = 0; switch (operation) { case "+": result = ADD; break; case "-": result = SUB; break; case "*": result = MUL; break; case "/": result = DIV; break; default: System.out.println("不存在该运算符"); break; } return result; }}
重点的方法
toInfixExpression() 将 中缀表达式转换成对应的List
parseSuffixExpressionList() 将得到的中缀表达式对应的list => 后缀表达式对应的list
六、递归
6.1.递归的概念
简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时
可以让代码变得简洁。
6.2.递归调用机制
我列举两个小案例,来帮助大家理解递归,部分学员已经学习过递归了,这里在给大家回顾一下递归调用机制
-
打印问题
-
阶乘问题
-
使用图解方式说明了递归的调用机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AAOf0CE1-1645524836176)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211226160600180.png)]
6.3.递归能解决什么样的问题
递归用于解决什么样的问题
-
各种数学问题如: **8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛) **
-
各种算法中也会使用到递归,比如**快排,归并排序,二分查找,分治算法等. **
-
将用栈解决的问题–>第归代码比较简洁
6.4.递归需要遵守的重要规则
递归需要遵守的重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响, 比如 n 变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
- ) 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError,死龟了:)
- 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或 者返回时,该方法也就执行完毕
6.5.递归-迷宫问题
6.5.1.迷宫问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3DUsp5ct-1645524836176)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211226161855027.png)]
6.5.2.代码实现
package com.million.recursion;/** * @Author: studyboy * @Date: 2021/12/26 16:15 * 迷宫回溯问题 */public class MiGong { 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 = 1; i < 7; i++) { map[i][0] = 1; map[i][6] = 1; } //设置挡板,1表示 map[3][1] = 1; map[3][2] = 1;// map[1][2] = 1;// map[2][2] = 1; //输出地图 for (int i = 0; i < 8; i++) { for (int j = 0; j < 7; j++) { System.out.print(map[i][j] + " "); } System.out.println(); } //使用递归回溯给小球找路// setWay(map, 1, 1); setWay2(map, 1, 1); //输出新的地图,小球走过,并标识过的递归 System.out.println("地图的情况小球走过,并标识过的递归"); for (int i = 0; i < 8; i++) { for (int j = 0; j < 7; j++) { System.out.print(map[i][j] + " "); } System.out.println(); } } //使用递归回溯来给小球找路 //说明 //1. map 表示地图 //2. i,j 表示从地图的哪个位置开始出发(1,1) //3. 如果小球能到map[6][5]位置,则说明通路找到 //4. 约定: 当map[i][j] 为0 表示该点没有走过,当为1表示墙; 2表示通路可以走;3表示该点走过,但是走不通 //5. 在走迷宫时,需要确定一个策略(方法) 下->右->上->左,如果该点走不通 ,再回溯 /** * @param map 表示地图 * @param i 从哪个位置开始找 * @param j * @return 如果找到通路,就返回true,否则返回false */ public static boolean setWay (int[][] map, int i, int j) { if (map[6][5] == 2) { //通路已经找到了 return true; } else { if (map[i][j] == 0) { //如果这个点没有走过 //假设这个点可以走 map[i][j] = 2; //需要确定一个策略(方法) 下->右->上->左,如果该点走不通 ,再回溯 if (setWay(map, i + 1, j)) { // 向下走 return true; } else if (setWay(map, i, j + 1)) { // 向右走 return true; } else if (setWay(map, i - 1, j)) { // 向上走 return true; } else if (setWay(map, i, j - 1)) { //向左走 return true; } else { //如果下->右->上->左都走不通,就置为3 然后回溯 map[i][j] = 3; return false; } } else { //如果这个点不是0,也就是可能是1 2 3 直接false return false; } } } //修改迷宫策略 上右下左 public static boolean setWay2 (int[][] map, int i, int j) { if (map[6][5] == 2) { //通路已经找到了 return true; } else { if (map[i][j] == 0) { //如果这个点没有走过 //假设这个点可以走 map[i][j] = 2; //需要确定一个策略(方法) 上->右->下->左,如果该点走不通 ,再回溯 if (setWay2(map, i - 1, j)) { // 向上走 return true; } else if (setWay2(map, i, j + 1)) { // 向右走 return true; } else if (setWay2(map, i + 1, j)) { // 向下走 return true; } else if (setWay2(map, i, j - 1)) { //向左走 return true; } else { //如果下->右->上->左都走不通,就置为3 然后回溯 map[i][j] = 3; return false; } } else { //如果这个点不是0,也就是可能是1 2 3 直接false return false; } } }}
6.5.3对迷宫问题的讨论
-
小球得到的路径,和程序员设置的找路策略有关即:找路的上下左右的顺序相关
-
再得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
-
测试回溯现象
-
思考: 如何求出最短路径? 思路-》代码实现.
思路:用集合保存小球走过的所有路径,再比较大小,谁小就是最短路径
6.6.递归-八皇后问题(回溯算法)
6.6.1八皇后问题介绍
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于
1848 年提出:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、
同一列或同一斜线上,问有多少种摆法**(92)**。
6.6.2八皇后问题算法思路分析
-
第一个皇后先放第一行第一列
-
第二个皇后放在第二行第一列、然后判断是否 OK, 如果不 OK,继续放在第二列、第三列、依次把所有列都
放完,找到一个合适
- 继续第三个皇后,还是第一列、第二列……直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确
解
- 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,
全部得到.
-
然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4 的步骤
-
示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wqjbLEoH-1645524836177)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211226221430835.png)]
说明:
理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] =
{0 , 4, 7, 5, 2, 6, 1, 3} //对应 arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第 i+1 个皇后,放在第 i+1
行的第 val+1 列
6.6.3八皇后问题算法代码实现
package com.million.recursion;/** * @Author: studyboy * @Date: 2021/12/26 20:52 * 八皇后问题 */public class Queue8 { //定义一个max表示共有多少个皇后 int max = 8; //定义数组array,保存皇后放置位置的结果,比如arr={0,4,7,5,6,1,3} int[] array = new int[max]; public static void main (String[] args) { Queue8 queue8 = new Queue8(); queue8.check(0); } //编写一个方法,放置第n个皇后 //特别注意:check是每一次递归时,进入到check中都有 for (int i = 0; i < max; i++) private void check (int n) { if (n == max) { //n=8 说明要放第9个皇后 ,所有8个皇后已经放好了 prinf(); return; } //依次放入皇后,并判断是否冲突 //如果循环遍历介绍还是返回false,就会回溯到前一个check for (int i = 0; i < max; i++) { //先把当前这个皇后n,放到该行的第1列 array[n] = i; //判断当放置第n个皇后到i列时,是否冲突 if (judge(n)) { //不冲突 check(n + 1); } //如果冲突,就继续执行array[n] = i; 即将第n个皇后,放置在本行后移一个位置 } } //查看当我们放置第n个皇后,就去检测该皇后是否和前面已经摆放的皇后冲突 /** * @param n 表示放第n个皇后,从0开始 * @return true是不冲突 */ private boolean judge (int n) { for (int i = 0; i < n; i++) { //1.array[i] == array[n] 表示判断 第n个皇后是否和前面的n-1个皇后在同一列 //2.Math.abs(n - i) == Math.abs(array[n] - array[i]) 表示判断第n个皇后是否和第i皇后是否在同一斜线 //n = 1(表示第二个皇后) 放置第2列 n =1 array[1] = 1 //Math.abs(1-0) == 1 Math.abs(array[n] - array[i]) = Math.abs(1-0) = 1 //3.判断是否在同一行,没有必要 n每次都在递增 if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) { return false; } } return true; } //写一个方法,可以将皇后摆放的位置输出 private void prinf () { for (int i = 0; i < array.length; i++) { System.out.printf(array[i] + " "); } System.out.println(); }}
七、排序算法
7.1 排序算法的介绍
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程
7.2 排序的分类:
- 内部排序:
指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
- 外部排序法:
数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
- 常见的排序算法分类(见右图):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aWb9RLo5-1645524836177)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211226230744752.png)]
7.3 算法的时间复杂度
7.3.1度量一个程序(算法)执行时间
- 事前估算的方法
通过分析某个算法的时间复杂度来判断哪个算法更优.
7.3.2时间频度
基本介绍
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间
就多。一个算法中的语句执行次数称为语句频度或时间频度。记为 T(n)
结论:
-
2n^2+3n+10 和 2n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 3n+10
-
n^2+5n+20 和 n^2 随着 n 变大,执行曲线无限接近, 可以忽略 5n+20
7.3.3常见的时间复杂度
Tn = O(f(n))
-
常数阶 O(1)
-
对数阶 O(log2n)
-
线性阶 O(n)
-
线性对数阶 O(nlog2n)
-
平方阶 O(n^2)
-
立方阶 O(n^3)
-
k 次方阶 O(n^k)
-
指数阶 O(2^n)
常见的时间复杂度对应的图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JP6QXTb-1645524836178)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211226231323790.png)]
说明:
- 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <
Ο(2n) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低
- 从图中可见,我们应该尽可能避免使用指数阶的算法
7.3.4.平均时间复杂度和最坏时间复杂度
-
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
-
最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的
原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会
比最坏情况更长。
- 平均时间复杂度和最坏时间复杂度是否一致,和算法有关(如图:)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9n8rMmy-1645524836178)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211227215513359.png)]
7.3.5算法的空间复杂度
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是
问题规模 n 的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的
临时工作单元数与解决问题的规模 n 有关,它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元,例
如快速排序和归并排序算法 ,基数排序就属于这种情况
- 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品
(redis, memcache)和算法(基数排序)本质就是用空间换时间.
7.4 冒泡排序
7.4.1 简介
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较
相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
优化:
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在
排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排
序写好后,在进行)
7.4.2演示冒泡过程的例子(图解)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2u2Bk43-1645524836178)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211227222417287.png)]
小结上面的图解过程:
(1) 一共进行 数组的大小-1 次 大的循环
(2)每一趟排序的次数在逐渐的减少
(3) 如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序。这个就是优化
7.4.3冒泡排序应用实例
我们举一个具体的案例来说明冒泡法。我们将五个无序的数:3, 9, -1, 10, -2 使用冒泡排序法将其排成一个从小
到大的有序数列。
代码实现:
package com.million.sort;import java.util.Arrays;import java.util.Date;import java.util.Random;import java.util.concurrent.ForkJoinPool;/** * @Author: studyboy * @Date: 2021/12/27 22:26 * 冒泡排序 */public class BubbleSort { public static void main (String[] args) {// int arr[] = {3, 9, -1, 10, 20}; //测试80000个随机数据的数组,进行排序 int [] arr = new int[80000]; //8万只用了11秒 真TM的快 我这电脑 for (int i = 0; i < 80000; i++) { arr[i] = new Random().nextInt(); } //排序前的时间 System.out.println(new Date(System.currentTimeMillis())); bubbleSort(arr);// System.out.println(Arrays.toString(arr)); //排序之后的时间 System.out.println(new Date(System.currentTimeMillis())); } //将前面的冒泡排序算法,封装成一个方法 public static void bubbleSort (int[] arr) { //冒泡排序 时间复杂度是0(n^2) int temp = 0; boolean flag = false; // 标识符,表示是否进行过交换 for (int i = 0; i < arr.length - 1; i++) { for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { flag = true; temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } }// System.out.println("第" + (i + 1) + "趟排序后的数组");// System.out.println(Arrays.toString(arr)); if (!flag) { break; } else { flag = false; //需要重置flag !!! 进行下次判断 } } }}
7.5 选择排序
7.5.1基本介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到
排序的目的
7.5.2选择排序思想:
假定第i个值为最小值,然后和j=i+1个值进行比较,如果假定的值大于比较的值,就将比较的值赋值给假定的值,依次循环找出来。等当前第i轮结束之后,就将找到的最小值和一开始假定的值互换即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ra77BZos-1645524836179)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211228150025654.png)]
7.5.3 代码实现
package com.million.sort;import java.util.Arrays;import java.util.Date;import java.util.Random;/** * @Author: studyboy * @Date: 2021/12/28 14:26 * 选择排序 */public class SelectSort { public static void main (String[] args) {// int[] arr = {101, 34, 119, 1}; //测试80000个随机数据的数组,进行排序 int[] arr = new int[80000]; //8万只用了2秒 真TM的快 我这电脑 for (int i = 0; i < 80000; i++) { arr[i] = new Random().nextInt(); }// System.out.println("排序前"); System.out.println(new Date(System.currentTimeMillis())); selectSort(arr);// System.out.println("排序后"); System.out.println(new Date(System.currentTimeMillis())); } //选择排序 public static void selectSort (int[] arr) { //最小的索引 int minIndex = 0; //假设当前的最小值 int min = 0; for (int i = 0; i < arr.length - 1; i++) { minIndex = i; min = arr[i]; for (int j = i + 1; j < arr.length; j++) { if (min > arr[j]) { min = arr[j]; //重置最小值 minIndex = j; } } //如果假定的min是最小值成立的,那么不需要换 if (minIndex != i) { //将假定最小值的位置的值赋值给已经找到最小值的索引的位置 arr[minIndex] = arr[i]; //将最小值赋值给假定的最小值 arr[i] = min; } } }}
7.6 插入排序
7.6.1 插入排序法介绍:
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
7.6.2 插入排序法思想:
将第一个数当作有序,其他的数当无序
将预排序的值保存到一个临时变量insertVal中,获取预排序的前一个值的下标i-1,将其保存在insertIndex中,然后进行循环遍历,如果insertIndex大于等于0并且insertVal大于arr[insertIndex],就说明还没有找到,执行方法体arr[insertIndex+1] = arr[insertIndex],为了将arr[insertIndex]后移; insertIndex–,然后再和arr[insertIndex]的前一个值比较; 当退出循环的时候,说明插入的位置找到了,将arr[insertIndex+1] = insertVal; 只需要执行n-1轮
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OMjgVGvQ-1645524836179)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211228153009955.png)]
7.6.3 代码实现
package com.million.sort;import java.util.Arrays;import java.util.Date;import java.util.Random;/** * @Author: studyboy * @Date: 2021/12/28 15:30 * 插入排序 */public class InsertSort { public static void main (String[] args) {// int[] arr = {101, 34, 119, 1, -1, 89}; //测试80000个随机数据的数组,进行排序 int[] arr = new int[800000]; //80万只用了不到42秒 真TM的快 我这电脑 for (int i = 0; i < 800000; i++) { arr[i] = new Random().nextInt(); } System.out.println(new Date(System.currentTimeMillis())); insertSort(arr); System.out.println(new Date(System.currentTimeMillis())); } //插入排序 public static void insertSort (int[] arr) { int insertVal = 0; int insertIndex = 0; for (int i = 1; i < arr.length; i++) { insertVal = arr[i]; insertIndex = i - 1; //即arr[i]的前面这个数的小标 //给insertVal找到插入的位置 /**说明 * 1. insertIndex >= 0 是为了防止给insertVal找位置的时候,不越界 * 2. insertVal < arr[insertIndex] 待插入的数,还没有找到位置 * 3. 就需要将arr[insertIndex] 后移一位, */ while (insertIndex >= 0 && insertVal < arr[insertIndex]) { arr[insertIndex + 1] = arr[insertIndex]; insertIndex--; } //当循环退出的时候,就是找到了带插入的位置 //如果insertIndex + 1 =i 说明找了半天发现预插入的值就在自己所在的位置 //不等于就执行下面的语句 if (insertIndex + 1 != i) { arr[insertIndex + 1] = insertVal; } } }}
7.7 希尔排序
7.7.1 简单插入排序存在的问题
我们看简单的插入排序可能存在的问题.
数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小), 这样的过程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响
7.7.2 希尔排序法基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含
的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止
7.7.3 希尔排序法的示意图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bl5ULGYI-1645524836180)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211228172934144.png)]
7.7.4 希尔排序法应用实例
有一群小牛, 考试成绩分别是 {8,9,1,7,2,3,5,4,6,0} 请从小到大排序. 请分别使用
-
希尔排序时, 对有序序列在插入时采用交换法, 并测试排序速度. (比直接插入还慢)
-
希尔排序时, 对有序序列在插入时采用移动法, 并测试排序速度 (用这种 很快)
-
代码实现
package com.million.sort;import java.util.Arrays;import java.util.Random;/** * @Author: studyboy * @Date: 2021/12/28 16:04 * 希尔排序 */public class ShellSort { public static void main (String[] args) {// int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; int[] arr = new int[800000]; //80万只用了不到130毫秒 真TM的快 我这电脑 for (int i = 0; i < 800000; i++) { arr[i] = new Random().nextInt(); } System.out.println(System.currentTimeMillis());// shellSort(arr); shellSort2(arr); //移位法 System.out.println(System.currentTimeMillis()); } //使用逐步推导的方式来编写希尔排序 //希尔排序时,对有序序列才插入时采用交换法 public static void shellSort (int[] arr) { int temp = 0; //临时变量 int count = 0; //根据前面的逐步分析,使用循环处理 for (int gap = arr.length / 2; gap > 0; gap /= 2) { //希尔排序第一轮,10个数据分成5组 for (int i = gap; i < arr.length; i++) { //遍历各组中所有的元素(共5组,每组有2个元素) 步长5 for (int j = i - gap; j >= 0; j -= gap) { if (arr[j] > arr[j + gap]) { temp = arr[j]; arr[j] = arr[j + gap]; arr[j + gap] = temp; } } } System.out.println("希尔排序第" + (++count) + "轮后=" + Arrays.toString(arr)); } /** //希尔排序第一轮,10个数据分成5组 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]) { temp = arr[j]; arr[j] = arr[j + 5]; arr[j + 5] = temp; } } } System.out.println("希尔排序第1轮后=" + Arrays.toString(arr)); //希尔排序第二轮,10个数据分成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]) { temp = arr[j]; arr[j] = arr[j + 2]; arr[j + 2] = temp; } } } System.out.println("希尔排序第2轮后=" + Arrays.toString(arr)); //希尔排序第三轮,10个数据分成1组 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]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } System.out.println("希尔排序第3轮后=" + Arrays.toString(arr)); */ } //对交换式的希尔排序进行优化-> 移位法 public static void shellSort2 (int[] arr) { int j = 0; //保存插入的值的下标 int temp = 0;//临时变量 //增量gap,并逐步的缩小增量 for (int gap = arr.length / 2; gap > 0; gap /= 2) { //从第gap个元素,逐个对其所造的组进行直接插入排序 for (int i = gap; i < arr.length; i++) { j = i; temp = arr[i];// if (arr[j] < arr[j - gap]) { while (j - gap >= 0 && temp < arr[j - gap]) { //后移 arr[j] = arr[j - gap]; j -= gap; } //退出循环,就是给temp找到了插入的位置 arr[j] = temp;// } } } }}
7.8 快速排序
7.8.1 快速排序法介绍:
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两
部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排
序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
7.8.2 快速排序法示意图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mezo77WL-1645524836180)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211229182737054.png)]
7.9.3 快速排序法应用实例:
要求: 对 [-9, 78, 88, 0, 0, 23, 99, 70, 100, 70] 进行从小到大的排序,要求使用快速排序法。【测试 8w 和 800w】
说明[验证分析]:
代码实现:
package com.million.sort;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Random;
/**
* @Author: studyboy
* @Date: 2021/12/29 16:34
* 快速排序
*/
public class QuickSort {
public static void main (String[] args) {
// int[] arr = {-9, 78, 88, 0, 0, 23, 99, 70, 100, 70};
int[] arr = new int[800000]; //80万只用了不到80毫秒 真TM的快 我这电脑
for (int i = 0; i < 800000; i++) {
arr[i] = new Random().nextInt();
}
System.out.println(System.currentTimeMillis());
quickSort(arr, 0, arr.length - 1);
System.out.println(System.currentTimeMillis());
}
public static void quickSort (int[] arr, int left, int right) {
int l = left; //左下标
int r = right; //右下标
int temp = 0; //临时变量
int pivot = arr[(left + right) / 2];
//while (l < r) 这个是在左右两边交替寻找
//让比pivot值大的放在右边,小的放在左边
while (l < r) {
//在pivot的右边一直找,找到小于等于pivot的值,才退出
while (arr[r] > pivot) {
r--;
}
//在pivot的左边一直找,找到大于等于pivot的值,才退出
while (arr[l] < pivot) {
l++;
}
//如果l>=r说明pivot的左右两边的值,已经按照左边全部是小于pivot值
//右边全部大于pivot的值了
if (l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果arr[l] == pivot ,就让r-- 前移 防止死循环
//当左边找到了大于pivot的值时,而右边找不到,那么就会在右边等于pivot的值时退出循环
//这时候arr[l] == pivot 会相等,而arr[r] > pivot这个条件就成立了,r--,但是如果前一个还是和pivot
//的值相等,就会出现死循环
if (arr[l] == pivot) {
r--; //不能是l++
}
//如果arr[r] == pivot ,就让l++ 后移 防止死循环 同理
if (arr[r] == pivot) {
l++; //不能是r--
}
}
//如果l == r 需要将l++ r-- 防止栈满溢出
if (l == r) {
l++;
r--;
}
//向左递归
if (left < r) {
quickSort(arr, left, r);
}
//向右递归
if (right > l) {
quickSort(arr, l, right);
}
}
}
7.9 归并排序
7.9.1 归并排序介绍:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(
divide-and-conquer**)**
策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修
补"在一起,即分而治之)。
7.9.2 归并排序思想示意图 1-基本思想
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0zDVAQOf-1645524836181)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211229221452832.png)]
7.9.3 归并排序思想示意图 2-合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将
[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jAz0cRwB-1645524836181)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211229221601807.png)]
7.9.4 归并排序的应用实例
给你一个数组, val arr = Array(8, 4, 5, 7, 1, 3, 6, 2 ), 请使用归并排序完成排序
package com.million.sort;import java.util.Arrays;import java.util.Random;/** * @Author: studyboy * @Date: 2021/12/29 20:15 * 归并排序 */public class MergeSort { public static void main (String[] args) {// int[] arr = {8, 4, 5, 7, 1, 3, 6, 2}; int[] arr = new int[800000]; //80万只用了100毫秒 真TM的快 我这电脑 for (int i = 0; i < 800000; i++) { arr[i] = new Random().nextInt(); } int[] temp = new int[arr.length]; System.out.println(System.currentTimeMillis()); mergeSort(arr,0,arr.length-1,temp); System.out.println(System.currentTimeMillis()); } //分+合算法 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) { //初始化i,左边有序序列的初始索引 int i = left; //初始化j,右边有序序列的初始索引 int j = mid + 1; //指向temp数组的当前索引 int t = 0; //(一) //先把左右两边(有序)的数据按照规则填充到temp 数组中 //直到左右两边的有序序列,有一边处理完毕即可 while (i <= mid && j <= right) { //如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素 //将左边的当前值拷贝到temp中 //如果t++ i++ if (arr[i] <= arr[j]) { temp[t++] = arr[i++]; } else { //反之 temp[t++] = arr[j++]; } } //(二) //把有剩余数据的一边数据依次全部填充到temp中 //左边剩余数据 while (i <= mid) { temp[t++] = arr[i++]; } //右边剩余数据 while (j <= right) { temp[t++] = arr[j++]; } //(三) //将temp 数组的元素拷贝到arr //注意,并不是每次都拷贝所有 t = 0; int tempLeft = left; //第一次:tempLeft = 0,right =1;第二次:tempLeft=2,right=3;第三次:tL=0,right=3; //最后一次:tempLeft = 0,right=7;// System.out.println("tempLeft=" + tempLeft + " right=" + right); while (tempLeft <= right) { arr[tempLeft++] = temp[t++]; } }}
7.10 基数排序
7.10.1 基数排序(桶排序)介绍:
- 基数排序(
radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 bin sort,顾 名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
-
基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
-
基数排序(Radix Sort)是桶排序的扩展
-
基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个
位数分别比较。
7.10.2 基数排序基本思想
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。
这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
- 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤
7.10.3 基数排序图文说明
将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q420wXto-1645524836182)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211230000959357.png)]
7.10.4 基数排序代码实现
要求:将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序
-
思路分析:前面的图文已经讲明确
-
代码实现:看老师演示
package com.million.sort;import java.util.Arrays;import java.util.Random;import java.util.concurrent.ForkJoinPool;/** * @Author: studyboy * @Date: 2021/12/29 23:04 * 基数排序(桶排序) */public class RadixSort { public static void main (String[] args) {// int[] arr = {53, 3, 542, 748, 14, 214}; int[] arr = new int[800000]; //80万只用了75毫秒 真TM的快 我这电脑 for (int i = 0; i < 800000; i++) { arr[i] = (int) (Math.random()*800000); //生成一个[0,800000] } System.out.println(System.currentTimeMillis()); radixSort(arr); System.out.println(System.currentTimeMillis()); } //基数排序方法 public static void radixSort (int[] arr) { //根据前面的推导过程,整合一下代码 //1.得到数组中最大的位数 int max = arr[0]; for (int i = 0; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } //得到最大数是几位 int maxLength = (max + "").length(); //定义一个二维数组,表示10个桶(因为数字为0~9),每一个桶就是一维数组 //说明 //1.二维数组包含10个一维数组 //2.为了防止在放入数的时候,数据溢出,则每一个一维数组(桶),大小定位arr.length //3.明确,基数排序是使用空间换时间的经典算法 int[][] bucket = new int[10][arr.length]; //为了记录每个桶中,实际存放了多少个数据,外面定义一个二维数组来记录各个桶的每次放入的数据个数 //可以理解 //比如:bucketElementCounts[0],记录就是bucket[0]桶的放入数据个数 int[] bucketElementCounts = new int[10]; //这里使用循环代码实现 for (int i = 0, n = 1; i < maxLength; i++, n *= 10) { //第1轮 for (int j = 0; j < arr.length; j++) { //取出每个元素的个位的值 int digitOfElement = arr[j] / n % 10; //放入到对应的桶中 bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; //可能有多位数放入这个桶中 bucketElementCounts[digitOfElement]++; } //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组) int index = 0; //遍历每一个桶,并将桶中的数据放入原数组 for (int k = 0; k < bucketElementCounts.length; k++) { //如果桶中有数据,我们才放入到原数组 if (bucketElementCounts[k] != 0) { //循环遍历该桶第k个桶(即第K个一位数组),放入 for (int l = 0; l < bucketElementCounts[k]; l++) { //取出元素放入到arr中 arr[index++] = bucket[k][l]; } } //每一轮处理之后,需要将每个bucketElementCounts[k] =0 !!! bucketElementCounts[k] = 0; }// System.out.println("第" + (i + 1)+"轮基数排序:" + Arrays.toString(arr)); }// //第1轮// for (int j = 0; j < arr.length; j++) {// //取出每个元素的个位的值// int digitOfElement = arr[j] % 10;// //放入到对应的桶中// bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];// //可能有多位数放入这个桶中// bucketElementCounts[digitOfElement]++;// }// //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)// int index = 0;// //遍历每一个桶,并将桶中的数据放入原数组// for (int k = 0; k < bucketElementCounts.length; k++) {// //如果桶中有数据,我们才放入到原数组// if (bucketElementCounts[k] != 0) {// //循环遍历该桶第k个桶(即第K个一位数组),放入// for (int l = 0; l < bucketElementCounts[k]; l++) {// //取出元素放入到arr中// arr[index++] = bucket[k][l];// }// }// //每一轮处理之后,需要将每个bucketElementCounts[k] =0 !!!// bucketElementCounts[k] = 0;// }// System.out.println("第1轮基数排序:" + Arrays.toString(arr));// //后面2轮不写了,整合起来 }}
7.10.5 基数排序的说明:
- 基数排序是对传统桶排序的扩展,速度很快.
- 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
- 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些
记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i]在 r[j]之前,而在排序后的序列中,r[i]仍在 r[j]之前,
则称这种排序算法是稳定的;否则称为不稳定的]
- 有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: https://code.i-harness.com/zh-CN/q/e98fa9
7.11 常用排序算法总结和对比
7.11.1 一张排序算法的比较图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VeopzFKU-1645524836182)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20211230001400222.png)]
7.11.2 相关术语解释:
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
- 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
- n: 数据规模
- k: “桶”的个数
- In-place:
- 不占用额外内存
- Out-place: 占用额外内存
八、查找算法
8.1 查找算法介绍
在 java 中,我们常用的查找有四种:
-
顺序(线性)查找
-
二分查找/折半查找
-
插值查找
-
斐波那契查找
8.2 二分查找
请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下
标,如果没有就提示"没有这个数"。
8.2.1 思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vpDgss3g-1645524836183)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220107205926269.png)]
8.2.2 代码实现
package com.million.search;import java.util.ArrayList;import java.util.List;/** * @Author: studyboy * @Date: 2022/1/7 20:15 * 二分查找,前提是数组是有序 */public class BinarySearch { public static void main (String[] args) { int[] arr = {1, 8, 10, 89, 1000, 1000, 1000, 1000, 1234}; System.out.println("查找的值的索引为=" + binarySearch2(arr, 0, arr.length - 1, 1000)); } //二分查找算法 /** * @param arr 传入数组 * @param left 左边索引 * @param right 右边索引 * @param findVal 查找的值 * @return */ public static int binarySearch (int[] arr, int left, int right, int findVal) { //当left > right 时,数组遍历完成了,需要退出递归 if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) { return -1; } int mid = (left + right) / 2; if (findVal < arr[mid]) { //向左递归 right = mid - 1; return binarySearch(arr, left, right, findVal); } else if (findVal > arr[mid]) { //向右递归 left = mid + 1; return binarySearch(arr, left, right, findVal); } else { return mid; } } //课后思考题,当有序数组中出现了相同的值,如何查找出全部,代码完善 public static List<Integer> binarySearch2 (int[] arr, int left, int right, int findVal) { List<Integer> list = new ArrayList<>(); //当left > right 时,数组遍历完成了,需要退出递归 if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) { return new ArrayList<Integer>(); } int mid = (left + right) / 2; if (findVal < arr[mid]) { //向左递归 right = mid - 1; return binarySearch2(arr, left, right, findVal); } else if (findVal > arr[mid]) { //向右递归 left = mid + 1; return binarySearch2(arr, left, right, findVal); } else { /** * 思路分析 * 1.再找到mid索引时,不要立马返回 * 2.向mid 索引值的左边扫描,将所有满足查找的值的下标放在集合中 * 3.向mid 索引值的右边扫描,将所有满足查找的值的下标放在集合中 * 4.返回这个集合 */ //向左查找 for (int i = mid - 1; i >= 0; i--) { //因为是有序数组,所有可以全部遍历出来相同的值的下标 if (arr[i] != findVal) { break; } list.add(i); } //先将查找到的值的下标保存下来,中间的值 list.add(mid); //向右查找 for (int i = mid + 1; i <= right; i++) { if (arr[i] != findVal) { break; } list.add(i); } return list; } }}
8.3 插值查找
- 插值查找原理介绍:
插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。当二分查找查询的不是均匀分布的有序数组时,这时候就得用插值查找来了,可以加快查询效率。
- 将折半查找中的求 mid 索引的公式 , low 表示左边索引 left, high 表示右边索引 right.
key 就是前面我们讲的 findVal
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DBIBuVFj-1645524836183)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220107214223043.png)]
- **int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;/插值索引/ **
对应前面的代码公式:
int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])
- 举例说明插值查找算法 1-100 的数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ce3cl0Co-1645524836183)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220107214245352.png)]
8.3.1 插值的案例
一个从1~100的有序数组,使用插值查找来查找指定的值
8.3.2 代码实现
package com.million.search;import java.util.Arrays;/** * @Author: studyboy * @Date: 2022/1/7 21:47 * 插值查找 也是有序的 */public class InsertValueSearch { public static void main (String[] args) { int[] arr = new int[100]; for (int i = 0; i < 100; i++) { arr[i] = i + 1; }// System.out.println(Arrays.toString(arr)); int index = insertValueSearch(arr, 0, arr.length - 1, 1); System.out.println("index=" + index); } //插值查找 /** * @param arr 查找的数组 * @param left 数组的起始下标 * @param right 数组的最大值下标 * @param findVal 查找的值 * @return 如果找到就返回查找的值的下标,找不到就返回-1 */ public static int insertValueSearch (int[] arr, int left, int right, int findVal) { System.out.println("使用的插值~~~"); //findVal < arr[0] || findVal > arr[arr.length-1] 这个除了可以优化查找效率以外 //还可以防止mid 下标越界 if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) { return -1; } //插值定义的mid int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]); if (findVal < arr[mid]) { //向左递归 return insertValueSearch(arr, left, mid - 1, findVal); } else if (findVal > arr[mid]) { return insertValueSearch(arr, mid + 1, right, findVal); } else { return mid; } }}
8.3.3 注意实现
1.对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
2.关键字分布不均匀的情况下,该方法不一定比折半查找要好
8.4 斐波那契(黄金分割法)查找算法
8.4.1 使用这个算法的目的
是因为二分查找向左递归和向右递归的成本不同,向左递归需要经过一次判断,向右递归需要两个判断,为了弥补这种专项成本的不平衡,可以将mid向右调整多一些,而不是中心,这就是斐波那契查找的意义。
8.4.2 原理
斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid 不再是中间或插值得到,而是位
于黄金分割点附近,即 mid=low+F(k-1)-1(F 代表斐波那契数列),如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JcmKi6Gt-1645524836184)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220107223436125.png)]
对 F(k-1)-1 的理解:
(1).由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。该式说明: 只要顺序表的长度为 F[k]-1,则可以将该表分成长度为 F[k-1]-1 和 F[k-2]-1 的两段,即如上图所示。从而中间位置为 mid=low+F【k-1】-1
(2).类似的,每一子段也可以用相同的方式分割
(3).但顺序表长度 n 不一定刚好等于 F[k]-1,所以需要将原来的顺序表长度 n 增加至 F[k]-1。这里的 k 值只要能使
得 F[k]-1 恰好大于或等于 n 即可,由以下代码得到,顺序表长度增加后,新增的位置(从 n+1 到 F[k]-1 位置),
都赋为 n 位置的值即可。
while(n>fib(k)-1) k++;
8.4.3 斐波那契查找应用案例:
请对一个有序数组进行斐波那契查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求
出下标,如果没有就提示"没有这个数"。
**代码实现: **
package com.million.search;import java.util.Arrays;/** * @Author: studyboy * @Date: 2022/1/8 20:24 * 斐波那契 查找算法 也是有序的 */public class FibonacciSearch { public static int maxSize = 20; public static void main (String[] args) { int[] arr = {1, 8, 10, 89, 1000, 1234}; System.out.println("index="+fibSearch(arr,1234)); } //因为后面我们mid=low+F[k-1]-1,需要使用到斐波那契,因此我们需要先获取到一个斐波那契数列 //非递归方法得到一个斐波那契数列 public static int[] fib () { int[] f = new int[maxSize]; f[0] = 1; f[1] = 1; for (int i = 2; i < maxSize; i++) { f[i] = f[i - 1] + f[i - 2]; } return f; } //编写斐波那契查找算法 //使用非递归的方式查找算法 /** * @param a * @param key * @return */ public static int fibSearch (int[] a, int key) { int low = 0; int high = a.length - 1; int k = 0; //表示斐波那契分割数值的下标 int mid = 0; int f[] = fib(); //获取斐波那契数列 //获取到斐波那契分割数值的下标 //但顺序表长度 n 不一定刚好等于 F[k]-1,所以需要将原来的顺序表长度 n 增加至 F[k]-1 while (a.length > f[k] - 1) { k++; } //因为f[k]值可能大于a的长度,因此我们需要使用Arrays类,构造一个新的数组,并指向a[] //不足的部分会使用0填充 int[] temp = Arrays.copyOf(a, f[k]); //需要将数组a最后的数填充到temp中 for (int i = high + 1; i < temp.length; i++) { temp[i] = a[high]; } //使用while来循环处理,找到我们的数key while (low <= high) { mid = low + f[k - 1] - 1; if (key < temp[mid]) { //向数组的前面查找(也就是左边) high = mid - 1; //k--的原因是,为了向数组的前面查找,因为f[k] = f[k-1]+f[k-2], //前面有f[k-1]个元素,前一个值就是f[k-1] = f[k-2]+f[k-3] //即下次循环mid=f[k-1-1]-1 k--; } else if (key > temp[mid]) {//向数组的后面查找(也就是右边) low = mid + 1; //k -=2 的原因是为了向数组的右边查找 //后面有f[k-2]个元素 f[k-2] = f[k-3]+f[k-4] //即下次循环mid=f[k-2-1]-1 k -= 2; }else{ //找到 if(mid <=high){ return mid; }else{ //为什么要返回high,因为对数组进行了填充,mid可能会比high的要大,而high后面的 //值都是和high下标的值一样的,所有就返回小的high return high; } } } return -1; }}
九、哈希表
9.1 哈希表的基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通
过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组
叫做散列表。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bzyGZlg-1645524836184)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220109000004649.png)]
9.2 google 公司的一个上机题:
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,名字,住址…),当输入该员工的 id 时,
要求查找到该员工的 所有信息.
要求:
-
不使用数据库,速度越快越好=>哈希表(散列)
-
添加时,保证按照 id 从低到高插入 [课后思考:如果 id 不是从低到高插入,但要求各条链表仍是从低到
高,怎么解决?] 单链表有
-
使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
-
思路分析并画出示意图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Id5w0J6G-1645524836184)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220109000054984.png)]
9.2.1 代码实现
package com.million.hashtab;import java.util.Scanner;/** * @Author: studyboy * @Date: 2022/1/8 22:21 */public class HashTabDemo { public static void main (String[] args) { //创建哈希表 HashTab hashTab = new HashTab(7); //写一个简单的菜单 String key = ""; Scanner scanner = new Scanner(System.in); while (true) { System.out.println("add: 添加雇员"); System.out.println("list: 显示雇员"); System.out.println("find: 查找雇员"); System.out.println("del: 删除雇员"); System.out.println("exit: 退出系统"); key = scanner.next(); switch (key) { case "add": System.out.println("输入 id"); int id = scanner.nextInt(); System.out.println("输入名字"); String name = scanner.next(); //创建 雇员 Emp emp = new Emp(id, name); hashTab.add(emp); break; case "list": hashTab.list(); break; case "find": System.out.println("请输入要查找的 id"); id = scanner.nextInt(); hashTab.findEmpById(id); break; case "del": System.out.println("请输入要删除的 id"); id = scanner.nextInt(); hashTab.del(id); break; case "exit": scanner.close(); System.exit(0); default: break; } } }}//创建HashTab 管理多条链表class HashTab { private EmplinkedList[] emplinkedListArray; private int size; //构造器 public HashTab (int size) { this.size = size; //初始化EmplinkedList emplinkedListArray = new EmplinkedList[size]; //?这时需要分别初始化每个链表 for (int i = 0; i < size; i++) { emplinkedListArray[i] = new EmplinkedList(); } } //添加雇员 public void add (Emp emp) { //根据员工的id,得到该员工应当添加到哪条链表 int empListkedListNo = hashFun(emp.getId()); //将emp添加到对应的链表里 emplinkedListArray[empListkedListNo].add(emp); } //遍历雇员 public void list () { for (int i = 0; i < size; i++) { emplinkedListArray[i].list(i); } } //根据id查找雇员 public void findEmpById (int id) { int empListkedListNo = hashFun(id); Emp emp = emplinkedListArray[empListkedListNo].findEmpById(id); if (emp != null) { System.out.printf("在第%d条链表中找到 雇员 id=%d\n", (empListkedListNo + 1), id); } else { System.out.println("没有找到"); } } //根据id删除雇员信息 public void del (int id) { int empListkedListNo = hashFun(id); emplinkedListArray[empListkedListNo].del(id); } //编写散列函数,使用简单取模法 public int hashFun (int id) { return id % size; }}//雇员class Emp { private Integer id; private String name; private Emp next; public Emp (Integer id, String name) { this.id = id; this.name = name; } public Integer getId () { return id; } public void setId (Integer id) { this.id = id; } public String getName () { return name; } public void setName (String name) { this.name = name; } public Emp getNext () { return next; } public void setNext (Emp next) { this.next = next; }}//创建EmplLinkedList,表示链表class EmplinkedList { //头指针,执行第一个Emp,因此我们这个链表的head是直接指向第一个Emp的 private Emp head; //默认为null //添加雇员到链表 //说明 //1.添加雇员时,id是自增长,即id的分配总是从小到大 //因此我们将雇员直接加入到本链表的最后即可 public void add (Emp emp) { //如果是添加第一个雇员 if (head == null) { head = emp; return; } //如果不是第一个雇员,则使用一个辅助的指针,帮助定位到最后 Emp curEmp = head; while (true) { if (curEmp.getNext() == null) { break; } curEmp = curEmp.getNext(); //后移 } //直接将emp接到curEmp后面 curEmp.setNext(emp); } //遍历链表的信息 public void list (int no) { //链表为空 if (head == null) { System.out.println("第" + (no + 1) + "链表为空"); return; } //辅助指针 Emp curEmp = head; while (true) { System.out.printf("==> 第%d条链表的id=%d name=%s\t", (no + 1), curEmp.getId(), curEmp.getName()); if (curEmp.getNext() == null) { break; } curEmp = curEmp.getNext(); //后移 } System.out.println(); } //根据id查找雇员 public Emp findEmpById (int id) { if (head == null) { System.out.println("链表为空"); return null; } //辅助指针 Emp curEmp = head; while (true) { //找到了 if (curEmp.getId() == id) { break; } //说明遍历当前链表没有找到该雇员 if (curEmp.getNext() == null) { return null; } curEmp = curEmp.getNext(); //后移 } return curEmp; } //根据id删除雇员 public void del (int id) { //辅助指针 Emp curEmp = head; //如果删除的是第一个 if(head.getId() == id){ head = null; return; } boolean flag = false; //判断是否找到了这个节点 while (true) { //找删除id的前一个结点 if (curEmp.getNext().getId() == id) { flag = true; break; } //说明遍历当前链表没有找到该雇员 if (curEmp.getNext() == null) { return; } curEmp = curEmp.getNext(); //后移 } if (flag) { curEmp.setNext(curEmp.getNext().getNext()); } else { System.out.println("没有找到需要删除的" + id + "号结点"); } }}
十、树结构的基础部分
10.1 二叉树遍历
10.1.1 二叉树遍历的说明
使用前序,中序和后序对下面的二叉树进行遍历.
-
前序遍历: 先输出父节点,再遍历左子树和右子树
-
中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
-
后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
-
小结**😗* 看输出父节点的顺序,就确定是前序,中序还是后序
10.1.2 二叉树遍历应用实例(前序,中序,后序)
应用实例的说明和思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mlwsTYcK-1645524836185)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220115185126280.png)]
10.1.3 代码实现
package com.million.tree;
/**
* @Author: studyboy
* @Date: 2022/1/15 18:17
* 二叉树的前中后序遍历
*/
public class BinaryTreeDemo {
public static void main (String[] args) {
BinaryTree binaryTree = new BinaryTree();
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
binaryTree.setRoot(root);
//前序遍历
System.out.println("前序遍历");
binaryTree.preOrder();
System.out.println("中序遍历");
binaryTree.infixOrder();
System.out.println("后序遍历");
binaryTree.postOrder();
}
}
//创建二叉树
class BinaryTree {
private HeroNode root;
public void setRoot (HeroNode root) {
this.root = root;
}
//前序遍历
public void preOrder () {
if (this.root != null) {
this.root.preOrder();
}
}
//中序遍历
public void infixOrder () {
if (this.root != null) {
this.root.infixOrder();
}
}
//后序遍历
public void postOrder () {
if (this.root != null) {
this.root.postOrder();
}
}
}
//创建结点
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 + '\'' +
'}';
}
//前序遍历
public void preOrder () {
//输出当前结点
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
//中序遍历
public void infixOrder () {
if (this.left != null) {
this.left.infixOrder();
}
//输出当前结点
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
//后序遍历
public void postOrder () {
if (this.left != null) {
this.left.postOrder();
}
if (this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
}
10.2 二叉树查找结点
要求
-
请编写前序查找,中序查找和后序查找的方法。
-
并分别使用三种查找方式,查找 heroNO = 5 的节点
-
并分析各种查找方式,分别比较了多少次
-
思路分析图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tnRzs8RC-1645524836185)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220119020111738.png)]
- 代码实现
//前序遍历查找 public HeroNode preOrderSearch (int no) { if (root != null) { return root.preOrderSearch(no); } else { return null; } } //中序遍历查找 public HeroNode infixOrderSearch (int no) { if (root != null) { return root.infixOrderSearch(no); } else { return null; } } //后续遍历查找 public HeroNode postOrderSearch (int no) { if (root != null) { return root.postOrderSearch(no); } else { return null; } }====================================================================================== //前序遍历查找 public HeroNode preOrderSearch (int no) { //根 左右 if (this.no == no) { return this; } //需要一个临时遍历,如果找到了就需要返回结果 HeroNode resNode = null; //判断左结点是否为空 if (this.left != null) { resNode = this.left.preOrderSearch(no); } if (resNode != null) { return resNode; } //判断右节点是否为空 if (this.right != null) { resNode = this.right.preOrderSearch(no); } return resNode; } //中序遍历查找 public HeroNode infixOrderSearch (int no) { //需要一个临时遍历,如果找到了就需要返回结果 HeroNode resNode = null; if (this.left != null) { resNode = this.left.infixOrderSearch(no); } if (resNode != null) { return resNode; } if (this.no == no) { return this; } //判断右节点是否为空 if (this.right != null) { resNode = this.right.infixOrderSearch(no); } return resNode; } //后续遍历查找 public HeroNode postOrderSearch (int no) { //需要一个临时遍历,如果找到了就需要返回结果 HeroNode resNode = null; if (this.left != null) { resNode = this.left.postOrderSearch(no); } if (resNode != null) { return resNode; } if (this.right != null) { resNode = this.right.postOrderSearch(no); } if (resNode != null) { return resNode; } if (this.no == no) { return this; } return resNode; }
10.3 二叉树删除结点
要求
-
如果删除的节点是叶子节点,则删除该节点
-
如果删除的节点是非叶子节点,则删除该子树
-
测试,删除掉 5 号叶子节点 和 3 号子树.
-
完成删除思路分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mB5eq0bR-1645524836185)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220120005324244.png)]
5.代码实现
//在 BinaryTree 类增加方法 //删除二叉树结点 public void delNode (int no) { if (this.root != null) { //判断是否是一个结点,也就是root结点 if (this.root.getNo() == no) { this.root = null; } else { this.root.delNode(no); } } else { System.out.println("空树,不能删除"); } }//HeroNode类添加方法//删除结点 //1.如果删除的节点是叶子节点,则删除该节点 // 2.如果删除的节点是非叶子节点,则删除该子树 public void delNode (int no) { /** * 思路 * 1. 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点, * 而不能去判断 当前这个结点是不是需要删除结点. * 2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点, * 就将 this.left = null; 并且就返回 (结束递归删除) * 3. 如果当前结点的右子结点不为空,并且右子结点 就是要删除结点, * 就将 this.right= null ;并且就返回 (结束递归删除) * 4. 如果第 2 和第 3 步没有删除结点,那么我们就需要向左子树进行递归删除 * 5. 如果第 4 步也没有删除结点,则应当向右子树进行递归删除 */ if (this.left != null && this.left.no == no) { this.left = null; return; } if (this.right != null && this.right.no == no) { this.right = null; return; } if (this.left != null) { this.left.delNode(no); // return; //不能return,要不然右子树就不能遍历,删除不了结点 } if (this.right != null) { this.right.delNode(no); } }
10.4 顺序存储二叉树
1.概念
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,
看右面的示意图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBYCZl5v-1645524836186)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220120011550303.png)]
要求:
- 右图的二叉树的结点,要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 6]
- 要求在遍历数组 arr 时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历
2.特点
- 顺序二叉树通常只考虑完全二叉树
- 第 i 个元素的左子节点为 2 * i + 1
- 第 i 个元素的右子节点为 2 * i + 2
- 第 i 个元素的父节点为 (i-1) / 2
- i : 表示二叉树中的第几个元素(按 0 开始编号如图所示),也是数组元素的下标
3.遍历
需求: 给你一个数组 {1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。 前序遍历的结果应当为
1,2,4,5,3,6,7
package com.million.tree;/** * @Author: studyboy * @Date: 2022/1/20 1:23 */public class ArrBinaryTreeDemo { public static void main (String[] args) { int[] arr = {1, 2, 3, 4, 5, 6, 7}; int[] arr2 = {}; BinTree binTree = new BinTree(arr2); binTree.preOrder(); }}class BinTree { private int[] arr; public BinTree (int[] arr) { this.arr = arr; } public void preOrder () { this.preOrder(0); } //顺序存储二叉树 public void preOrder (int index) { //arr==null 是一个arr指针没有指向任何对象,而arr.length==0是arr指针指向 // 一个长度为零的对象,判空在前面是为了防止空指针异常 if (arr == null || arr.length == 0) { System.out.println("数组为空,不能按照二叉树的前序遍历"); //这里需要return,因为如果数组为空的话,没有返回,就会继续往下执行,就会报数组越界异常 return; } //输出当前结点 System.out.println(arr[index]); //左递归 if ((2 * index + 1) < arr.length) { preOrder(2 * index + 1); } //右递归 if ((2 * index + 2) < arr.length) { preOrder(2 * index + 2); } }}
4.作业
课后练习:请同学们完成对数组以二叉树中序,后序遍历方式的代码.
//中序遍历 public void infixOrder (int index) { //arr==null 是一个arr指针没有指向任何对象,而arr.length==0是arr指针指向 // 一个长度为零的对象,判空在前面是为了防止空指针异常 if (arr == null || arr.length == 0) { System.out.println("数组为空,不能按照二叉树的前序遍历"); //这里需要return,因为如果数组为空的话,没有返回,就会继续往下执行,就会报数组越界异常 return; } //左递归 if ((2 * index + 1) < arr.length) { infixOrder(2 * index + 1); } //输出当前结点 System.out.println(arr[index]); //右递归 if ((2 * index + 2) < arr.length) { infixOrder(2 * index + 2); } } //后续遍历 public void postOrder (int index) { //arr==null 是一个arr指针没有指向任何对象,而arr.length==0是arr指针指向 // 一个长度为零的对象,判空在前面是为了防止空指针异常 if (arr == null || arr.length == 0) { System.out.println("数组为空,不能按照二叉树的前序遍历"); //这里需要return,因为如果数组为空的话,没有返回,就会继续往下执行,就会报数组越界异常 return; } //左递归 if ((2 * index + 1) < arr.length) { postOrder(2 * index + 1); } //右递归 if ((2 * index + 2) < arr.length) { postOrder(2 * index + 2); } //输出当前结点 System.out.println(arr[index]); }
5.应用
八大排序算法中的堆排序,就会使用到顺序存储二叉树, 关于堆排序,我们放在<<树结构实际应用>> 章节讲解。
10.5 线索化二叉树
10.5.1 先看一个问题
将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DsoHdktn-1645524836186)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220121164103025.png)]
问题分析:
-
当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
-
但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上. 有n+1=7个空指针域没被用上
-
如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
-
解决方案-线索二叉树
10.5.2 线索二叉树基本介绍
- n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向
该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
-
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质 的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
-
一个结点的前一个结点,称为前驱结点
-
一个结点的后一个结点,称为后继结点
10.5.3 线索二叉树应用案例
应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6IHaVg2B-1645524836187)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220121164323020.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iqDuZ9Gr-1645524836187)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220121164333905.png)]
说明: 当线索化二叉树后,Node 节点的 属性 left 和 right ,有如下情况:
- left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的
就是前驱节点.
- right 指向的是右子树,也可能是指向后继节点,比如 ① 节点 right 指向的是右子树,而⑩ 节点的 right 指向
的是后继节点
代码实现
class ThreadedBinaryTree { private HeroNode root; private HeroNode pre = null; //表示当前结点的前驱结点 public void setRoot (HeroNode root) { this.root = root; } public void threadNodes () { this.threadNodes(root); } /** * 二叉树的线索化 * * @param node 表示当前结点 */ public void threadNodes (HeroNode node) { //如果node==null,不能线索化 if (node == null) { return; } //(一)线索化左子树 threadNodes(node.getLeft()); //(二)线索化当前结点 //线索化当前结点的左指针 if (node.getLeft() == null) { node.setLeft(pre); node.setLeftType(1); } //线索化当前结点的右指针[动脑子] //这里处理右指针是进行第二轮递归的时候处理的 //此时的pre指针应该指向的是上一个递归的当前结点 if (pre != null && pre.getRight() == null) { pre.setRight(node); pre.setRightType(1); } //!!! 需要更新pre指针 pre = node; //(三)线索化右子树 threadNodes(node.getRight()); }}//创建结点class HeroNode { private int no; private String name; private HeroNode left; //默认为null private HeroNode right; //默认为null private int leftType; //0表示指向左子树,1表示指向前驱结点 private int rightType;//0表示指向右子树,1表示指向后驱结点
10.5.4 遍历线索化二叉树
-
说明:对前面的中序线索化的二叉树, 进行遍历
-
分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历
线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次
序应当和中序遍历保持一致。
- 代码:
class ThreadedBinaryTree {
private HeroNode root;
private HeroNode pre = null; //表示当前结点的前驱结点
public void setRoot (HeroNode root) {
this.root = root;
}
public void threadNodes () {
this.threadNodes(root);
}
/**
* 中序遍历线索化二叉树
*/
public void threadNodesInfixList () {
//定义一个临时变量,保存当前结点
HeroNode node = root;
while (node != null) {
//循环找到leftType == 1 的结点,第一个找到的就是8结点
//后面随着遍历而变化,因为leftType == 1时,说明该结点是按照线索化
//处理后的有效结点
while (node.getLeftType() == 0) {
node = node.getLeft();
}
//打印当前这个结点
System.out.println(node);
//如果当前结点的右指针指向的是有效的(也就是线索化过的)后继结点,就一直输出,
// 因为rightType==1
while (node.getRightType() == 1) {
node = node.getRight();
System.out.println(node);
}
//如果不是,就替换这个遍历的结点
node = node.getRight();
}
}
/**
* 前序遍历线索化二叉树
*/
public void threadNodesPreList () {
//定义一个临时变量,保存当前结点
HeroNode node = root;
while (node != null) {
//循环找到leftType == 1 的结点,第一个找到的就是8结点
//后面随着遍历而变化,因为leftType == 1时,说明该结点是按照线索化
//处理后的有效结点
while (node.getLeftType() == 0) {
//如果当前结点指向左子树,就输出当前结点
System.out.println(node);
node = node.getLeft();
}
//如果当前结点的右指针指向的是有效的(也就是线索化过的)后继结点,就一直输出,
// 因为rightType==1
while (node.getRightType() == 1) {
System.out.println(node);
node = node.getRight();
}
//如果不是,就替换这个遍历的结点
node = node.getRight();
}
}
}
十一、树结构实际应用
11.1 堆排序
11.1.1 堆排序基本介绍
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复
杂度均为 O(nlogn),它也是不稳定排序。
-
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有 要求结点的左孩子的值和右孩子的值的大小关系。
-
**每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆 **
-
大顶堆举例说明
-
一般升序采用大顶堆,降序采用小顶堆
11.1.2 堆排序基本思想
堆排序的基本思想是:
-
将待排序序列构造成一个大顶堆
-
此时,整个序列的最大值就是堆顶的根节点。
-
将其与末尾元素进行交换,此时末尾就为最大值。
-
然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。
11.1.3 堆排序步骤图解说
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OSqbHI52-1645524836187)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220122173706704.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JdsN1ANl-1645524836188)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220122173741135.png)]
11.1.4 代码实现
package com.million.tree;
import java.util.Arrays;
import java.util.Random;
/**
* @Author: studyboy
* @Date: 2022/1/22 16:38
* 堆排序
*/
public class HeapSort {
public static void main (String[] args) {
// int[] arr = {4, 6, 8, 5, 9, -1};
int[] arr = new int[800000]; //80万只用了不到93毫秒 真TM的快 我这电脑 O(nlogn)
for (int i = 0; i < 800000; i++) {
arr[i] = new Random().nextInt();
}
System.out.println(System.currentTimeMillis());
// shellSort(arr);
heapSort(arr); //移位法
System.out.println(System.currentTimeMillis());
}
//进行堆排序的方法
public static void heapSort (int[] arr) {
int temp = 0;
//构建大顶堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
bigHeapTree(arr, i, arr.length);
}
/**
* 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,
* 再将堆顶元素与末尾元素交换得到第二大元素。如此反复进行交换、重建、交换。
*/
for (int i = arr.length - 1; i > 0; i--) {
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//这里为什么要让i=0,因为是将堆顶元素与末尾元素进行交换,比较的一直但是堆顶的元素
bigHeapTree(arr, 0, i);
}
}
//将以数组为存储数据的树转换成大顶堆
/**
* @param arr 传进来的当前数组
* @param i 树的非叶子结点
* @param length 数组的长度
*/
public static void bigHeapTree (int[] arr, int i, int length) {
//先获取最后一个非叶子结点的值
int temp = arr[i];
//循环判断当前结点的左右结点的大小
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
//如果当前结点的左结点小于右结点,就指向大的那个结点,也就是右结点
//K+1 要小于数组的长度
if ((k + 1) < length && arr[k] < arr[k + 1]) {
k++; //k指向右节点
}
//如果右结点大于父节点,就赋值给父节点
if (arr[k] > temp) {
arr[i] = arr[k];
i = k; //!!!继续向左结点比较
} else {
//为什么可以直接退出循环?
//因为只需要比较右结点就好了
//因为假如你右结点比左结点大,但是右结点又比父节点小,那么就不需要更换了
//而且将无序序列构造成了一个大顶堆,是从最后一个非叶子结点往上堆的
break;
}
}
///当 for 循环结束后,我们已经将以 i 为父结点的树的最大值,放在了 最顶(局部)
// 将父节点的值赋值给大的那个结点
arr[i] = temp;
}
}
11.2 哈夫曼树
11.2.1 基本介绍
- 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度**(wpl)**达到最小,称这样的二叉树为
最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
11.2.2 创建哈夫曼树
给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树
步骤:
1.从小到大进行排序, 将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树 2.取出根节点权值最小的两颗二叉树 3.组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和 4.再将这颗新的二叉树,以根节点的权值大小 再次排序, 断重复 1-2-3-4 的步骤,直到数列中,所有的数 据都被处理,就得到一颗赫夫曼树
代码实现
package com.million.huffmanTree;import java.util.ArrayList;import java.util.Collections;import java.util.List;/** * @Author: studyboy * @Date: 2022/1/23 19:16 * 创建哈夫曼树 */public class HuffManTree { public static void main (String[] args) { int[] arr = {13, 7, 8, 3, 29, 6, 1}; Node root = createHuffManTree(arr); preOrder(root); } //哈夫曼树的前序遍历 private static void preOrder(Node root){ if(root !=null){ root.preOrder(); }else{ System.out.println("哈夫曼树为空,不能遍历"); } } //创建哈夫曼树的方法 /** * @param arr 构建哈夫曼树的数组 * @return 返回构建完成之后的哈夫曼树的根结点 * 1.从小到大进行排序, 将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树 * 2.取出根节点权值最小的两颗二叉树 * 3.组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和 * 4.再将这颗新的二叉树,以根节点的权值大小 再次排序, * 断重复 1-2-3-4 的步骤,直到数列中,所有的数 据都被处理,就得到一颗赫夫曼树 */ private static Node createHuffManTree (int[] arr) { //需要将数组里的每个元素转换成结点,如果保存在结合中 List<Node> list = new ArrayList<>(); for (int value : arr) { list.add(new Node(value)); } //循环处理这些结点 while (list.size() > 1) { //先排序 Collections.sort(list); Node leftNode = list.get(0); Node rightNode = list.get(1); Node parent = new Node(leftNode.value + rightNode.value); //将这两个结点置为父结点的左右子结点 parent.left = leftNode; parent.right = rightNode; //将这两个结点删除,将相加后的父结点放入list集合中 list.remove(leftNode); list.remove(rightNode); list.add(parent); } return list.get(0); }}//创建结点class Node implements Comparable { /** * 权值 */ int value; /** * 左结点 */ Node left; /** * 右结点 */ Node right; public Node (int value) { this.value = value; } //前序遍历 public void preOrder () { System.out.println(this); if (this.left != null) { this.left.preOrder(); } if (this.right != null) { this.right.preOrder(); } } @Override public String toString () { return "Node{" + "value=" + value + '}'; } @Override public int compareTo (Object o) { Node node = (Node) o; //从小到大进行排序 //大于0 就是this > node return this.value - node.value; }}
11.3 哈夫曼编码
11.3.1 基本介绍
- 赫夫曼编码也翻译为
哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
-
赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
-
赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间
-
赫夫曼码是可变字长编码(VLC)的一种。Huffman 于 1952 年提出一种编码方法,称之为最佳编码
11.3.2 原理剖析
通信领域中信息的处理方式 3-赫夫曼编码
步骤如下;
传输的 字符串
-
i like like like java do you like a java
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
-
按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eUDw4YlZ-1645524836188)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220124160050962.png)]
4) 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为 0 向右的路径为 1 , 编码 如下: o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a : 110 k: 1110 e: 1111 j: 0000 v: 0001 l: 001 : 01
5) 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩) 10101001101111011110100110111101111010011011110111101000011000011100110011110000110 01111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133
6长度为 :133 说明: 原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性 赫夫曼编码是无损处理方案
注意事项
注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是
一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,
11.3.3 最佳实践-数据压缩(创建赫夫曼树)
将给出的一段文本,比如 “i like like like java do you like a java” , 根据前面的讲的赫夫曼编码原理,对其进行数 据 压 缩 处 理 , 形 式 如 "1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100 110111101111011100100001100001110 "
需要分为8个一组,以字节码的形式发送
思路分析:
(1)Node {data(存放数据),weight(权值),left和right}
(2)得到"i like like like java do you like a java"对应的byte[]数组
(3)编写一个方法,将准备构建哈夫曼树的Node结点放到list中,形式[Node[data=97,weight=5]…]
(4)通过List创建对应的哈夫曼树
Node类
//创建结点class Node implements Comparable<Node> { Byte data; //存放数据 a=> 97 .. int weight; //存放权重 Node left; Node right; public Node (Byte data, int weight) { this.data = data; this.weight = weight; } //前序遍历 public void preOrder () { System.out.println(this); if (this.left != null) { this.left.preOrder(); } if (this.right != null) { this.right.preOrder(); } } @Override public String toString () { return "Node{" + "data=" + data + ", weight=" + weight + '}'; } @Override public int compareTo (Node o) { //从小到大排序 return this.weight - o.weight; }}
代码实现
/** * 将准备构建哈夫曼树的Node结点放到list中,形式[Node[data=97,weight=5].....] * * @param bytes 字节数组 * @return 返回存放Node结点的List集合 */ private static List<Node> TranNodeIntoList (byte[] bytes) { List<Node> nodes = new ArrayList<>(); //需要统计每个字符遍历了几次,用map来统计 Map<Byte, Integer> counts = new HashMap<>(); for (byte b : bytes) { //先获取当前字符遍历的次数 Integer count = counts.get(b); if (count == null) { //说明还未遍历过,直接放入 counts.put(b, 1); } else { //已经存在过了 counts.put(b, count + 1); } } //遍历map集合,将结点中的数据放入map集合中 for (Map.Entry<Byte, Integer> entry : counts.entrySet()) { nodes.add(new Node(entry.getKey(), entry.getValue())); } return nodes; } private static Node createHuffmanTree (List<Node> nodes) { while (nodes.size() > 1) { //先排序 Collections.sort(nodes); //获取起始1 2结点 Node leftNode = nodes.get(0); Node rightNode = nodes.get(1); Node parent = new Node(null, leftNode.weight + rightNode.weight); parent.left = leftNode; parent.right = rightNode; //删除这两个结点 nodes.remove(leftNode); nodes.remove(rightNode); //将parent结点添加到nodes中 nodes.add(parent); } return nodes.get(0); }
11.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"
字符串生成对应的编码数据, 形式如下.
10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001010 00101111111100110001001010011011100
//使用一个方法,将前面进行压缩涉及到的方法封装起来,便于我们调用 /** * @param bytes 原始的字符串对应的字节数组 * @return 是经过哈夫曼编码处理后的字节数组(压缩后的数组) */ private static byte[] huffmanZip (byte[] bytes) { List<Node> nodes = TranNodeIntoList(bytes); //根据 nodes 创建哈夫曼树 Node huffmanTreeRoot = createHuffmanTree(nodes); //对应的哈夫曼树编码(根据哈夫曼树) Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot); //根据生成的哈夫曼编码,压缩得到压缩后的哈夫曼编码字节数组 return zip(bytes, huffmanCodes); }
代码实现
//编写一个方法,将字符串对应的byte[]数组,通过生成的哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]
/**
* @param bytes 这是原始的字符串对应的byte[]
* @param huffmanCodes 生成的哈夫曼编码表
* @return 返回哈夫曼编码处理后的byte[]
* 对于这一串字符串10101000101111....
* 对应的byte[] huffmanCodeBytes 即将8位为一个byte放入到 huffmanCodeBytes中
* 即huffmanCodeBytes中[0]=10101000(补码),补码等于反码+1,这里的1要加在反码的末尾
* 10101000(补码)=> 10101000-1=>10100111(反码)=>11011000(原码)=>-88 满2进1
* 正数的原码、反码、补码都是其本身,最高位为0的是正数,为1的是负数
* 负数的反码等于原码的最高位不变,其他的取反,比较0就要变成1
byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77 , -57, 6, -24, -14, -117, -4, -60, -90, 28]
*/
private static byte[] zip (byte[] bytes, Map<Byte, String> huffmanCodes) {
//1.利用huffmanCodes 将bytes 转换成 哈弗码编码的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes数组
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
// System.out.println(new String(stringBuilder));
//获取huffmanCodeBytes的长度
//一句话 int lne = (stringBuilder.length()+7)/8
int len = (stringBuilder.length() % 8 == 0) ? stringBuilder.length() / 8 : stringBuilder.length() / 8 + 1;
//创建 存储压缩后点的byte数组
byte[] huffmanCodeBytes = new byte[len];
//8位为一个byte放入到 huffmanCodeBytes中
for (int i = 0, index = 0; i < stringBuilder.length(); i += 8, index++) {
String strByte;
//防止数组越界
if (i + 8 >= stringBuilder.length()) {
strByte = stringBuilder.substring(i);
//lastNum 是一个静态变量,记录最后一个字节的长度
lastNum = strByte.length();
} else {
strByte = stringBuilder.substring(i, i + 8);
}
//将strByte转换成一个byte,放入到huffmanCodeBytes
//2表示的是求2进制的String s 的十进制是多少
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
}
return huffmanCodeBytes;
}
//生成哈夫曼树对应的哈夫曼树编码
//思路:
//1. 将哈夫曼编码保存在Map<Byte,String>的形式
//生成哈夫曼树编码表 32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011,
// 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011
static Map<Byte, String> huffmanCodes = new HashMap<>();
//2.在生成哈夫曼编码是,需要拼接字符串,定义一个StringBuilder 存储某个叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();
//使用一个全局变量记录最后一个字节编码的长度
static int lastNum;
//为了调用方便,重载getCodes
private static Map<Byte, String> getCodes (Node root) {
if (root != null) {
//递归左子树
getCodes(root.left, "0", stringBuilder);
//递归右子树
getCodes(root.right, "1", stringBuilder);
} else {
System.out.println("根结点为空,不能生成哈夫曼编码");
}
return huffmanCodes;
}
/**
* 功能:将传入的node结点的所有叶子结点的哈夫曼树编码得到,并放入到huffmanCodes集合中
*
* @param node 传入的结点
* @param code 路径,规定左边路径为0,右边路径为1
* @param stringBuilder 用来拼接字符串
*/
private static void getCodes (Node node, String code, StringBuilder stringBuilder) {
//定义一个新的stringBuilder2 用来拼接字符串
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringBuilder2中
stringBuilder2.append(code);
if (node != null) { //结点不为空就执行
if (node.data == null) { //说明是非叶子结点
//递归处理
//向左递归
getCodes(node.left, "0", stringBuilder2);
//向右递归
getCodes(node.right, "1", stringBuilder2);
} else { //说明是叶子结点
//直接加入到huffmanCodes中
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
11.3.5 最佳实践-数据解压(使用赫夫曼编码解码)
- 前面我们得到了赫夫曼编码和对应的编码
byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77 , -57, 6, -24, -14, -117, -4, -60, -90, 28]
-
现在要求使用赫夫曼编码, 进行解码,又 重新得到原来的字符串"i like like like java do you like a java"
-
思路:解码过程,就是编码的一个逆向操作。
-
代码实现
//对哈夫曼编码进行解码 /** * @param huffmanCodes 哈夫曼编码表 map * @param huffmanBytes 哈夫曼编码得到的字节数组 * @return */ private static byte[] decode (Map<Byte, String> huffmanCodes, byte[] huffmanBytes) { //1.先得到huffmanBytes 对应的二进制的字符串,形式1010100010111... 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(new String(stringBuilder)); //把字符串按照指定的哈夫曼编码进行解码 //把哈夫曼编码进行调换,因为反向查询a->100 100->a Map<String, Byte> map = new HashMap<>(); for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) { map.put(entry.getValue(), entry.getKey()); } //创建一个集合,存放byte List<Byte> list = new ArrayList<>(); //i 可以理解成就是索引,扫描stringBuilder for (int i = 0; i < stringBuilder.length(); ) { int count = 1; //这个相当于一个指针 boolean flag = true; Byte b = null; while (flag) { //这里是i不同,让count动,直到找到匹配的字符串。就结束循环 String key = stringBuilder.substring(i, i + count); b = map.get(key); if (b == null) { count++; } else { //匹配到 flag = false; } } //将找到对应字符串的值存入到list集合中 list.add(b); //让i指针指向找到的字符串末尾的后一个字符 i += count; } //当for循环结束后,我们list中就存放了所有的字符"i like like like java do you like a java" //把list 中的数据放入到byte[] 并返回 byte[] b = new byte[list.size()]; for (int i = 0; i < list.size(); i++) { b[i] = list.get(i); } return b; } /** * 将一个byte 转换成一个二进制的字符串 * * @param flag 标志是否需要补高位,如果是true,表示需要补高位,如果是false表示不补 * 如果是最后一个字节,并且是正数,像010、0010这种的,需要把当时编码时的最后一位的字节数保存, * 然后传入到这里,从后往前截取对应的数字。,其他的肯定满8位,直接截取,如果最后一位是 * 负数,就会多出24个1,就需要判断其为负数,然后截取。 * @param b 传入的byte * @return 返回的是该b对应的二进制的字符串(注意是按补码返回) * 这里的lastNum是最后一位字节的长度,正数,且是0011这种形式的就补零 */ private static String byteToBitString (boolean flag, byte b) { //使用遍历保存b int temp = b; //如果是正数,但是还不是最后一位的我们还存在补高位 if (flag) { temp |= 256; //按位或 假设temp=1 temp=temp|256=> 0000 0001 | 1 0000 0000=>1 0000 00001 } String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码 if (str.length() < lastNum) { int length = lastNum - str.length(); for (int i = 0; i < length; i++) { str = '0' + str; } } if (flag) { return str.substring(str.length() - 8); } else { return str; } }
11.3.6 最佳实践-文件压缩
我们学习了通过赫夫曼编码对一个字符串进行编码和解码, 下面我们来完成对文件的压缩和解压, 具体要求:
给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
-
思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩
-
代码实现:
/**
* 对文件进行压缩
*
* @param srcFile 需要压缩的文件的全路径
* @param dstFile 压缩之后存放的文件路径
*/
public static void zipFile (String srcFile, String dstFile) {
//创建文件输入流,读取文件
FileInputStream is = null;
FileOutputStream os = null;
ObjectOutputStream oos = null;
try {
is = new FileInputStream(srcFile);
//创建一个和源文件大小一样的byte[],可以为1204
byte[] b = new byte[is.available()];
//将数据读入
is.read(b);
//直接将文件压缩
byte[] huffmanBytes = huffmanZip(b);
//创建文件输出流
os = new FileOutputStream(dstFile);
//创建一个和文件输出流相关联的ObjectOutputStream
oos = new ObjectOutputStream(os);
//把哈夫曼编码后的字节数组写入压缩文件中
oos.writeObject(huffmanBytes);
//这里我们以对象流的方式写入 哈夫曼编码,是为了以后我们恢复源文件时使用
//注意一定要把哈夫曼编码写入压缩文件
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
11.3.7 最佳实践-文件解压(文件恢复)
具体要求:将前面压缩的文件,重新恢复成原来的文件。
-
思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)
-
代码实现
/**
* 对文件进行解压
*
* @param zipFile 压缩后的文件存放的路径
* @param dstFile 解压后的文件存放的路径
*/
public static void unzipFile (String zipFile, String dstFile) {
//定义一个文件输入流 读取zipFile文件
FileInputStream is = null;
//定义一个对象输入流 读取压缩后的huffmanBytes字节数组和哈夫曼编码表
ObjectInputStream ois = null;
//定义一个文件输出流
FileOutputStream os = null;
try {
//创建文件输入流
is = new FileInputStream(zipFile);
//创建一个和文件输入流相关联的对象输入流
ois = new ObjectInputStream(is);
//读取byte数组 huffmanBytes
byte[] huffmanBytes = (byte[]) ois.readObject();
//读取哈夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
//解码
byte[] bytes = decode(huffmanCodes, huffmanBytes);
//将byte 数组写入到目标文件
os = new FileOutputStream(dstFile);
//写数据到dstFile文件
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
11.3.9 赫夫曼编码压缩文件注意事项
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件
[举例压一个 .ppt]
-
赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml 文件]
-
如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.
11.4 二叉排序树
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当
前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PlC15jO4-1645524836189)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220127173001406.png)]
11.4.1 二叉排序树创建和遍历
11.4.5 二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
-
删除叶子节点 (比如:2, 5, 9, 12)
-
删除只有一颗子树的节点 (比如:1)
-
删除有两颗子树的节点. (比如:7, 3,10 )
-
操作的思路分析
package com.million.tree;
import javax.swing.text.BadLocationException;
/**
* @Author: studyboy
* @Date: 2022/1/27 14:18
* 二叉排序树的遍历 (BST)
*/
public class BinarySortTreeDemo {
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 Node(arr[i]));
}
System.out.println("中序遍历");
binarySortTree.infixOrder();
System.out.println("删除结点");
binarySortTree.delNode(2);
binarySortTree.delNode(5);
binarySortTree.delNode(9);
binarySortTree.delNode(12);
binarySortTree.delNode(7);
binarySortTree.delNode(3);
binarySortTree.delNode(10);
binarySortTree.delNode(1);
System.out.println("根结点"+binarySortTree.getRoot());
// binarySortTree.delNode(1);
binarySortTree.infixOrder();
}
}
//创建二叉排序树
class BinarySortTree {
private Node root;
public Node getRoot () {
return root;
}
//添加结点
public void add (Node node) {
if (root == null) {
//如果根结点为空的话,就让根结点指向第一个添加的结点
root = node;
} else {
root.add(node);
}
}
//中序遍历
public void infixOrder () {
if (root != null) {
root.infixOrder();
} else {
System.out.println("根结点为空,不能遍历");
}
}
//查找要删除的结点
public Node findSingleNode (int value) {
if (root != null) {
return root.findSingleNode(value);
} else {
return null;
}
}
//查找要删除结点的父结点
public Node searchParentNode (int value) {
if (root != null) {
return root.searchParentNode(value);
} else {
return null;
}
}
/**
* //1. 返回的 以 node 为根结点的二叉排序树的最小结点的值,右子树找最小的,左子树找最大的
* //2. 删除 node 为根结点的二叉排序树的最小结点
*
* @param node 作为根结点的二叉树
* @return 返回 以node为根结点的二叉树排序树的最小结点的值
*/
public int delRightTreeMinNode (Node node) {
Node target = node;
while (target.left != null) {
target = target.left;
}
//删除最小结点
delNode(target.value);
return target.value;
}
//第一种情况,删除叶子结点
/**
* 思路:
* 1.需要先去找到要删除的结点 targetNode
* 2.找到 targetNode 的父结点parent
* 3.确定 targetNode 是parent的左子结点 还是右子结点
* 4.根据前面的情况来对应删除
* parent.left = null; parent.right = null
*
* @param value 要删除的值
*/
public void delNode (int value) {
if (root == null) {
return;
}
Node targetNode = findSingleNode(value);
if (targetNode == null) {
return;
}
//如果root是根结点并且只有这一个结点
if (root.left == null && root.right == null) {
root = null;
return;
}
Node parent = searchParentNode(value);
//第一种情况,删除叶子结点
if (targetNode.left == null && targetNode.right == null) {
if (parent.left == targetNode) {
parent.left = null;
} else if (parent.right == targetNode) {
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) {
/**
* 情况三 : 删除有两颗子树的节点. (比如:7, 3,10 )
* 思路
* (1) 需求先去找到要删除的结点 targetNode
* (2) 找到 targetNode 的 父结点 parent
* (3) 从 targetNode 的右子树找到最小的结点,是以targetNode为根结点的右子树
* (4) 用一个临时变量,将 最小结点的值保存 temp = 11
* (5) 删除该最小结点
* (6) targetNode.value = temp
*/
int minValue = delRightTreeMinNode(targetNode.right);
// int minValue = delLeftTreeMaxNode(targetNode.left);
targetNode.value = minValue;
} else {
//第二种情况,BST(二叉排序树)删除只有一颗子树的结点
/**
* 思路(1)
* 需求先去找到要删除的结点 targetNode
* (2) 找到 targetNode 的 父结点 parent
* (3) 确定 targetNode 的子结点是左子结点还是右子结点
* (4) targetNode 是 parent 的左子结点还是右子结点
* (5) 如果 targetNode 有左子结点
* 5. 1 如果 targetNode 是 parent 的左子结点
* parent.left = targetNode.left;
* 5.2 如果 targetNode 是 parent 的右子结点
* parent.right = targetNode.left;
* (6) 如果 targetNode 有右子结点
* 6.1 如果 targetNode 是 parent 的左子结点
* parent.left = targetNode.right;
* 6.2 如果 targetNode 是 parent 的右子结点
* parent.right = targetNode.right
*/
if (targetNode.left != null) {
if(parent !=null) {
if (parent.left == targetNode) {
parent.left = targetNode.left;
} else {
parent.right = targetNode.left;
}
}else{
root = targetNode.left;
}
} else { //如果删除的结点有右结点
if(parent !=null) {
if (parent.left == targetNode) {
parent.left = targetNode.right;
} else {
parent.right = targetNode.right;
}
}else{
root = targetNode.right;
}
}
}
}
}
//创建结点
class Node {
int value;
Node left;
Node right;
public Node (int value) {
this.value = value;
}
@Override
public String toString () {
return "Node{" +
"value=" + value +
'}';
}
//查找需要的删除结点
/**
* @param value 需要查找的值
* @return 返回查找的结点
*/
public Node findSingleNode (int value) {
if (this.value == value) {
return this;
} else if (value < this.value) { //如果查找的值小于当前结点的值,左递归查找
if (this.left == null) {
return null;
}
return this.left.findSingleNode(value);
} else { //如果查找的结点不是
if (this.right == null) {
return null;
}
return this.right.findSingleNode(value);
}
}
//需要查找删除的父结点
/**
* @param value 删除的值
* @return 返回查找删除的父结点
*/
public Node searchParentNode (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.searchParentNode(value);
//实际使用的时候避免有相同的值
} else if (value > this.value && this.right != null) {
return this.right.searchParentNode(value);
} else {
//左右结点为空,就返回null,比如要查找的是根结点
return null;
}
}
}
//添加结点
//递归的形式添加结点,注意需要满足二叉排序树的要求
public void add (Node node) {
if (node == null) {
return;
}
//如果添加的结点比当前结点的值要小
if (node.value < this.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else { //添加的结点比当前结点的值要大或者等于
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
}
//中序遍历
public void infixOrder () {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
11.4.6 作业
如果我们从左子树找到最大的结点,然后前面的思路完成.
/**
* @param node 以 node 为根结点的二叉排序树的最大结点的值
* @return 返回查询左子树最大结点的值
*/
public int delLeftTreeMaxNode (Node node) {
Node target = node;
while (target.right != null) {
target = target.right;
}
//删除最大结点
delNode(target.value);
return target.value;
}
11.5 平衡二叉树
11.5.1 看一个案例(说明二叉排序树可能的问题)
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.
左边 BST 存在的问题分析:
-
左子树全部为空,从形式上看,更像一个单链表.
-
插入速度没有影响
-
查询速度明显降低(因为需要依次比较), 不能发挥 BST的优势,因为每次还需要比较左子树,其查询速度比
单链表还慢
- 解决方案-平衡二叉树(AVL)
11.5.2 基本介绍
-
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树, 可以保证查询效率较高。
-
具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵
平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
11.5.3 应用案例-单旋转(左旋转)
-
要求: 给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}
-
思路分析(示意图)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dc8CQUaY-1645524836189)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220127235318976.png)]
- 代码实现
//左旋转操作
public void leftRotate () {
//创建新的结点newNode,将当前结点的值赋给新结点
Node newNode = new Node(this.value);
//把新结点的左子树置为当前结点的左子树
newNode.left = this.left;
//把新结点的右子树置为当前结点的右子树的左子树
newNode.right = this.right.left;
//把当前结点的值替换成当前结点的右子树的值
this.value = this.right.value;
//把当前结点的右子树置为当前结点的右子树的右子树
this.right = this.right.right;
//把当前结点的左子树置为新的结点
this.left = newNode;
}
11.5.4 应用案例-单旋转(右旋转)
-
要求: 给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}
-
思路分析(示意图)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jcg7bm87-1645524836190)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220127235609156.png)]
- 代码实现
//右旋转操作
public void rightRotate () {
//创建新的结点newNode,将当前结点的值赋给新结点
Node newNode = new Node(this.value);
//把新结点的右子树置为当前结点的右子树
newNode.right = this.right;
//把新结点的左子树置为当前结点的左子树的右子树
newNode.left = this.left.right;
//把当前结点的值置为当前结点的左子树的值
this.value = this.left.value;
//把当前结点的左子树置为当前结点的左子树的左子树
this.left = this.left.left;
//把当前结点的右子树置为新结点
this.right = newNode;
}
11.5.5 应用案例-双旋转
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转
不能完成平衡二叉树的转换。比如数列
int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL 树.
int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL 树
- 问题分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sMdGnCCx-1645524836190)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220127235844922.png)]
- 解决思路分析
当符合右旋转的条件时
1.如果当前结点的左子树的右子树的高度大于当前结点的左子树的左子树的高度
2.先对当前结点的左子树(左结点)->左旋转
3.再对当前结点进行右旋转
如果不符合,直接右旋转
当符合左旋转的条件时
1.如果当前结点的右子树的左子树的高度大于当前结点的右子树的右子树的高度
2.先对当前结点的右子树(右结点)进行-> 右旋转
3.再对当前结点进行左旋转
如果不符合,直接左旋转
- 代码实现
//返回左子树的高度
public int leftHigh () {
if (left == null) {
return 0;
}
return left.height();
}
//返回右子树的高度
public int rightHigh () {
if (right == null) {
return 0;
}
return right.height();
}
//返回当前结点的高度,以该结点为根结点的树的高度
public int height () {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
//添加结点
//递归的形式添加结点,注意需要满足二叉排序树的要求
public void add (Node node) {
if (node == null) {
return;
}
//如果添加的结点比当前结点的值要小
if (node.value < this.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else { //添加的结点比当前结点的值要大或者等于
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
//当添加完一个结点之后,如果:(右子树的高度- 左子树的高度)>1,左旋转
if (rightHigh() - leftHigh() > 1) {
//如果当前结点的右子树的左子树的高度大于当前结点的右子树的右子树的高度
if (this.right != null && this.right.leftHigh() > this.right.rightHigh()) {
//先对当前结点的右子树(右结点)进行-> 右旋转
this.right.rightRotate();
//再对当前结点进行左旋转
leftRotate();
} else {
//直接进行左旋转即可
leftRotate();
}
//加不加都可以,因为经过了旋转之后就平衡了,所有不会进入下一个if,但是可以提高效率
return;
}
//当添加完一个结点之后,如果:(左子树的高度- 右子树的高度)>1,右旋转
if (leftHigh() - rightHigh() > 1) {
//如果当前结点的左子树的右子树的高度大于当前结点的左子树的左子树的高度
if (this.left != null && this.left.rightHigh() > this.left.leftHigh()) {
//先对当前结点的左子树(左结点)->左旋转
this.left.leftRotate();
//再对当前结点进行右旋转
rightRotate();
} else {
//直接进行右旋转即可
rightRotate();
}
}
}
十二、多路查找树
12.1 二叉树与 B 树
12.1.1 二叉树的问题分析
二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5WdRp12R-1645524836191)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128003603005.png)]
- 二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如 1 亿), 就
存在如下问题:
-
问题 1:在构建二叉树时,需要多次进行 i/o 操作(海量数据存在数据库或文件中),节点海量,构建二叉树时, 速度有影响
-
问题 2:节点海量,也会造成二叉树的高度很大,会降低操作速度.
12.1.2 多叉树
- 在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,
就是多叉树(multiway tree)
- 后面我们讲解的 2-3 树,2-3-4 树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
- 举例说明(下面 2-3 树就是一颗多叉树)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NROqcd1F-1645524836191)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128003637047.png)]
12.1.3 B 树的基本介绍
B 树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数来提升效率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cl8SGT92-1645524836192)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128003648802.png)]
-
如图 B 树通过重新组织节点, 降低了树的高度.
-
文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为 4k), 这样每个节点只需要一次 I/O 就可以完全载入
-
将树的度 M 设置为 1024,在 600 亿个元素中最多只需要 4 次 I/O 操作就可以读取到想要的元素, B 树(B+)广泛 应用于文件存储系统以及数据库系统中
12.2 2-3 树
12.2.1 2-3 树是最简单的 B 树结构
, 具有如下特点:
-
2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
-
有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
-
有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.
-
2-3 树是由二节点和三节点构成的树。
12.2.2 2-3 树应用案例
将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成 2-3 树,并保证数据插入的大小顺序。(演示一下构建 2-3 树的过程.)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wh4C24Bs-1645524836192)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128003736610.png)]
插入规则:
-
2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
-
有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
-
有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
-
当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,
拆后仍然需要满足上面 3 个条件。
- 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
12.2.3 其它说明
除了 23 树,还有 234 树等,概念和 23 树类似,也是一种 B 树。 如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OQN0eA9k-1645524836192)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128003755387.png)]
12.3 B 树、B+树和 B*树
12.3.1 B 树的介绍
B-tree 树即 B 树,B 即 Balanced,平衡的意思。有人把 B-tree 翻译成 B-树,容易让人产生误解。会以为 B-树
是一种树,而 B 树又是另一种树。实际上,B-tree 就是指的 B 树。
12.3.2 B 树的介绍
前面已经介绍了 2-3 树和 2-3-4 树,他们就是 B 树(英语:B-tree 也写成 B-树),这里我们再做一个说明,我们在学
习 Mysql 时,经常听到说某种类型的索引是基于 B 树或者 B+树的,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V7M4SOGE-1645524836192)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128003810107.png)]
对上图的说明:
1.B 树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4
2.B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
3.关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据
4.搜索有可能在非叶子结点结束
5.其搜索性能等价于在关键字全集内做一次二分查找
12.3.3 B+树的介绍
B+树是 B 树的变体,也是一种多路搜索树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K7eQIfB4-1645524836193)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128004027551.png)]
对上图的说明:
- B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性
能也等价于在关键字全集做一次二分查找
-
所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据) 恰好是有序的。
-
不可能在非叶子结点命中
-
非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
-
更适合文件索引系统
-
B 树和 B+树各有自己的应用场景,不能说 B+树完全比 B 树好,反之亦然.
12.3.4 B*树的介绍
B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qu74Jx8X-1645524836193)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220128004101298.png)]
B*树的说明:
- B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为 2/3,而 B+树的块的最低使用率为的
1/2。
- 从第 1 个特点我们可以看出,B*树分配新结点的概率比 B+树要低,空间使用率更高
十三、图
13.2 图的表示方式
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。
13.2.1 邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于 n 个顶点的图而言,矩阵是的 row 和 col 表示的是 1…n
个点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1nAmYdCa-1645524836193)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220204191640207.png)]
13.2.2 邻接表
-
邻接矩阵需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在,会造成空间的一定损失.
-
邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
-
举例说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JI0ngi6o-1645524836194)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220204191707207.png)]
13.3 图的快速入门案例
- 要求: 代码实现如下图结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-InlVrHbY-1645524836194)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220207215442241.png)]
-
思路分析
(1) 存储顶点 String 使用 List集合存储
(2) 保存矩阵 int[][] edges
-
代码实现
public class Graph {
private List<String> vertexList; // 存储顶点集合
private int[][] edges; //存储图对应的邻接矩阵
private int numOfEdges; //表示边的数组
//定义数组boolean[],记录某个结点是否被访问
private boolean[] isVisited;
public Graph (int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
}
public static void main (String[] args) {
int n = 5;
String[] vertexs = {
"A", "B", "C", "D", "E"
};
//创建图对象
Graph graph = new Graph(n);
//循环添加顶点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
//添加边
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
//显示图
graph.showGraph();
//深度优先遍历
System.out.println("深度优先");
graph.dfs();
System.out.println();
System.out.println("广度优先");
graph.bfs();
}
//插入结点
public void insertVertex (String vertex) {
vertexList.add(vertex);
}
//显示图对应的矩阵
public void showGraph () {
for (int[] link : edges) {
System.err.println(Arrays.toString(link));
}
}
//返回结点的个数
public int getNumOfVertex () {
return vertexList.size();
}
//得到边的个数
public int getNumOfEdge () {
return numOfEdges;
}
//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
public String getValueByIndex (int i) {
return vertexList.get(i);
}
/**
* 添加边 无向图
*
* @param v1 表示点的下标即是第几个顶点 "A"-"B" "A"->0 "B"->1
* @param v2 第二个顶点对应的下标
* @param weight 表示0还是1
*/
public void insertEdge (int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
13.4 图的深度优先遍历介绍
13.4.3 深度优先遍历算法步骤
-
访问初始结点 v,并标记结点 v 为已访问。
-
查找结点 v 的第一个邻接结点 w。
-
若 w 存在,则继续执行 4,如果 w 不存在,则回到第 1 步,将从 v 的下一个结点继续。
-
若 w 未被访问,对 w 进行深度优先遍历递归(即把 w 当做另一个 v,然后进行步骤 123)。
-
查找结点 v 的 w 邻接结点的下一个邻接结点,转到步骤 3。
-
分析图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a5JydbYS-1645524836195)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220207215742857.png)]
13.4.4 深度优先算法的代码实现
/**
* 得到第一个邻接结点的小标w
*
* @param index 给当前行的结点
* @return 如果存在就返回对应的下标,否则返回-1
*/
public int getFirstNeighbor (int index) {
for (int i = 0; i < vertexList.size(); i++) {
if (edges[index][i] > 0) {
return i;
}
}
return -1;
}
/**
* 根据前一个邻接结点的小标来获取下一个邻接结点
*
* @param v1 前一个邻接结点的横坐标
* @param v2 前一个邻接结点的纵坐标
* @return 如果得到下一个邻接结点就返回,没有就返回-1
*/
public int getNextNeighbor (int v1, int v2) {
for (int i = v2 + 1; i < vertexList.size(); i++) {
if (edges[v1][i] > 0) {
return i;
}
}
return -1;
}
//深度优先遍历 (如果是连通图,这个方法就可以了)
private void dfs (boolean[] isVisited, int i) {
//首先我们访问该结点,输出
System.out.print(getValueByIndex(i) + "->");
//将结点设置为已经访问
isVisited[i] = true;
//查找结点 v 的第一个邻接结点 w
int w = getFirstNeighbor(i);
while (w != -1) { //说明存在
//未访问
if (!isVisited[w]) {
dfs(isVisited, w);
}
//如果该结点已经被访问过
w = getNextNeighbor(i, w);
}
}
//如果考虑到非连通图,需要遍历所有的结点,并进行dfs
public void dfs () {
isVisited = new boolean[vertexList.size()];
//遍历所有的结点
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]) {
dfs(isVisited, i);
}
}
}
13.5 图的广度优先遍历
13.5.1 广度优先遍历基本思想
-
图的广度优先搜索(Broad First Search) 。
-
类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来
访问这些结点的邻接结点
13.5.2 广度优先遍历算法步骤
-
访问初始结点 v 并标记结点 v 为已访问。
-
结点 v 入队列
-
当队列非空时,继续执行,否则算法结束。
-
出队列,取得队头结点 u。
-
查找结点 u 的第一个邻接结点 w。
-
若结点 u 的邻接结点 w 不存在,则转到步骤 3;否则循环执行以下三个步骤:
6.1 若结点 w 尚未被访问,则访问结点 w 并标记为已访问。
6.2 结点 w 入队列
6.3 查找结点 u 的继 w 邻接结点后的下一个邻接结点 w,转到步骤 6。
13.5.3 广度优先遍历代码实现
//图的广度优先遍历 先单个结点
private void bfs (boolean[] isVisited, int i) {
//表示队列的头结点对应下标
int u;
//邻接结点w
int w;
//队列,记录结点访问的顺序
LinkedList queue = new LinkedList();
//访问结点,输出信息
System.out.print(getValueByIndex(i) + "->");
//标记为已访问
isVisited[i] = true;
//将结点加入队列 添加到末尾
queue.addLast(i);
while (!queue.isEmpty()) {
//取出队列的头结点下标
u = (Integer) queue.removeFirst();
//查找结点 u 的第一个邻接结点 w
w = getFirstNeighbor(u);
while (w != -1) {
if (!isVisited[w]) {
System.out.print(getValueByIndex(w) + "->");
//标记已经访问
isVisited[w] = true;
//入队
queue.addLast(w);
}
//如果访问过了,以u所在的下标为横坐标,w为纵坐标,
//查找结点u的邻接结点w后的下一个邻接结点w,
w = getNextNeighbor(u, w);
}
}
}
//遍历所有的结点,都进行广度优先遍历
public void bfs () {
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
}
13.6 图的深度优先 VS 广度优先
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQsWa6Vj-1645524836195)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220207220116307.png)]
十四、程序员常用算法
14.1 二分查找算法(非递归)
-
前面我们讲过了二分查找算法,是使用递归的方式,下面我们讲解二分查找算法的非递归方式
-
二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
-
二分查找法的运行时间为对数时间 O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n 步,假设从[0,99]的
队列(100 个数,即 n=100)中寻到目标数 30,则需要查找步数为㏒₂100 , 即最多需要查找 7 次( 2^6 < 100 < 2^7)
14.1.2 二分查找算法(非递归)代码实现
数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成
package com.million.binarysearchnorecur;
/**
* @Author: studyboy
* @Date: 2022/2/7 22:09
* 二分查找算法(非递归)
*/
public class BinarySearchNoRecur {
public static void main (String[] args) {
int[] arr = {1, 3, 8, 10, 11, 67, 100};
System.out.println("index=" + binarySearch(arr, 1));
}
//二分查找的非递归方式来实现
/**
* @param arr 待查找的数组,arr是升序排序
* @param target 需要查找的数
* @return 返回对应下标,-1表示没有找到
*/
public static int binarySearch (int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (target == arr[mid]) {
return mid;
} else if (target < arr[mid]) {
//向左边查找
right = mid - 1;
} else {
//向右边查找
left = mid + 1;
}
}
return -1;
}
}
14.2 分治算法
14.2.1 分治算法介绍
- 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或
相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题
的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变
换)……
- 分治算法可以求解的一些经典问题
- 二分搜索
- 大整数乘法
- 棋盘覆盖
- **合并排序 **
- **快速排序 **
- 线性时间选择
- 最接近点对问题
- 循环赛日程表
- 汉诺塔
14.2.2 分治算法的基本步骤
分治法在每一层递归上都有三个步骤:
1) 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
2) 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3) 合并:将各个子问题的解合并为原问题的解。
14.2.3 分治算法最佳实践-汉诺塔
汉诺塔游戏的演示和思路分析:
- 如果是有一个盘, A->C
如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的盘 2. 上面的盘
-
先把 最上面的盘 A->B
-
把最下边的盘 A->C
-
把 B 塔的所有盘 从 B->C
汉诺塔游戏的代码实现:
package com.million.hanoitower;
/**
* @Author: studyboy
* @Date: 2022/2/7 22:39
* 使用分治算法实现汉诺塔问题
*/
public class HanoiTower {
public static void main (String[] args) {
hanoiTower(4,'A','B','C');
}
/**
* 汉诺塔的移动的算法,使用分治算法
* 思路:
* 1) 如果是有一个盘, A->C 如
* 果我们有 n >= 2 情况,我们总是可以看做是两个盘
* 1.最下边的盘 2. 上面的盘
* 2) 先把 最上面的盘 A->B
* 3) 把最下边的盘 A->C
* 4) 把 B 塔的所有盘 从 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) {
if (num == 1) {
System.out.println("第1个盘从" + a + "->" + c);
} else {
//1.先把 最上面的盘 A->B 移动过程会使用c
hanoiTower(num - 1, a, c, b);
//2.把最下边的盘 A->C
System.out.println("第" + num + "个盘从" + a + "->" + c);
//3.把 B 塔的所有盘 从 B->C 移动过程会使用a
hanoiTower(num - 1, b, a, c);
}
}
}
14.3 动态规划算法
14.3.1 动态规划算法介绍
- 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解
的处理算法
- 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这
些子问题的解得到原问题的解。
- 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子
阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
- 动态规划可以通过填表的方式来逐步推进,得到最优解.
14.3.2 应用场景-背包问题
背包问题:有一个背包,容量为 4 磅 , 现有如下物品
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3r7y762H-1645524836196)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220212223930589.png)]
-
要求达到的目标为装入的背包的总价值最大,并且重量不超出
-
要求装入的物品不能重复
14.3.3 背包问题-思路分析和图解
- 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价
值最大。其中又分 01 背包和完全背包(完全背包指的是:每种物品都有无限件可用)
-
这里的问题属于 01 背包,即每个物品最多放一个。而无限背包可以转化为 01 背包。
-
算法的主要思想,利用动态规划来解决。每次遍历到的第 i 个物品,根据 w[i]和 v[i]来确定是否需要将该物品
放入背包中。即对于给定的 n 个物品,设 val[i]、w[i]分别为第 i 个物品的价值和重量,m 为背包的容量。再令v[i][j]表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值。则我们有下面的结果:
(1) v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是 0
(2) 当 w[i]> j 时:v[i][j]=v[i-1][j] // 当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
(3) 当 j>=w[i]时: v[i][j]=max{v[i-1][j], val[i]+v[i-1][j-w[i]]} // 当准备加入的新增的商品的容量小于等于当前背包的容量,
// 装入的方式: v[i-1][j]: 就是上一个单元格的装入的最大值
val[i] : 表示当前商品的价值
v[i-1][j-w[i]] : 装入 i-1 商品,到剩余空间 j-w[i]的最大值(比如装入了重量为3的商品,背包容量为4,那么背包的剩下值为1,那么就找到第i-1行,第j=1列的值,相加)
当 j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :
14.3.4 代码实现
package com.million.dynamic;
/**
* @Author: studyboy
* @Date: 2022/2/12 21:16
* 使用动态规划(Dynamic Programming) dp 解决背包问题
*/
public class KnapsackProblem {
public static void main (String[] args) {
int[] w = {1, 4, 3}; //w数组存储磅数 1是1磅,4是4磅,3是3磅
int[] val = {1500, 3000, 2000}; //val数组存储价值 就是ppt中的v[i]
int m = 4; //表示背包的容量
int n = val.length; //物品的个数
//前 i 个物品中能够装入容量为 j 的背包中的最大价值
int[][] v = new int[n + 1][m + 1];
//为了记录哪几个背包放入容器中,定义一个二维数组path
int[][] path = new int[n + 1][m + 1];
//初始化第一行和第一列, 这里在本程序中,可以不去处理,因为默认就是 0
for (int i = 0; i < v.length; i++) {
v[i][0] = 0; //设置第一列为0
}
for (int i = 0; i < v[0].length; i++) {
v[0][i] = 0; //设置第一行为0
}
//根据前面得到公式来动态规划处理
for (int i = 1; i < v.length; i++) { //不处理第一行 i从1开始
for (int j = 1; j < v[0].length; j++) { //不处理第一列 j从1开始
//当准备加入新增的商品的容量大于当前背包的容量时,
// 就直接使用上一个 单元格的装入策略
if (w[i - 1] > j) { // 因为我们程序i从 1 开始的,因此原来公式中的 w[i] 修改成 w[i-1]
v[i][j] = v[i - 1][j];
} else {
//说明: //因为我们的 i 从 1 开始的, 因此公式需要调整成
// v[i][j] = Math.max(v[i - 1][j], val[i-1] + v[i - 1][j - w[i - 1]]);
if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
//找最大的,最优解
path[i][j] = 1;
} else {
v[i][j] = v[i - 1][j];
}
}
}
}
//输出一下 v 看看目前的情况
for (int i = 0; i < v.length; i++) {
for (int j = 0; j < v[i].length; j++) {
System.out.print(v[i][j] + " ");
}
System.out.println();
}
//输出最后我们放入的是哪些商品
//动脑筋
int i = path.length - 1; //行的最大下标
int j = path[0].length - 1; //列的最大下标
while (i > 0 && j > 0) {
if (path[i][j] == 1) {
System.out.println("第" + i + "个商品放入到背包");
//查找把商品放入背包后剩下的容量又多少
j -= w[i - 1];
}
//查找到1个,继续找
i--;
}
}
}
14.4 KMP算法
14.4.1 应用场景-字符串匹配问题
字符串匹配问题::
- 有一个字符串 str1= ““硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好””,和一个子串 str2="尚硅谷你尚硅
你"
- 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
14.4.2 暴力算法
如果用暴力匹配的思路,并假设现在 str1 匹配到 i 位置,子串 str2 匹配到 j 位置,则有:
-
如果当前字符匹配成功(即 str1[i] == str2[j]),则 i++,j++,继续匹配下一个字符
-
如果失配(即 str1[i]! = str2[j]),令 i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为 0。
-
用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量
的时间。(不可行!)
-
暴力匹配算法实现.
-
代码
package com.million.kmp;
/**
* @Author: studyboy
* @Date: 2022/2/12 23:07
* 使用暴力算法解决字符串匹配问题
*/
public class ViolenceMatch {
public static void main (String[] args) {
String str1 = "硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好";
String str2 = "尚硅谷你尚硅你";
System.out.println("index=" + violenceMatch(str1, str2));
}
// 暴力匹配算法方法
/**
* @param str1 源字符串
* @param str2 要匹配的字符串
* @return 如果匹配成功,就返回要匹配的字符串对应源字符串的首字母的下标,没找到就返回-1
*/
public static int violenceMatch (String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0; //i索引指向s1
int j = 0; //j索引指向s2
while (i < s1Len && j < s2Len) {
if (s1[i] == s2[j]) { //匹配ok
i++;
j++;
} else {
//如果失配(即 str1[i]! = str2[j]),令 i = i - (j - 1),j = 0。
// 相当于每次匹配失败时,i 回溯,j 被置为 0。
i = i - (j - 1);
j = 0;
}
}
//判断是否匹配成功
if (j == s2Len) {
return i - j;
} else {
return -1;
}
}
}
14.4.3 KMP 算法介绍
字符串匹配问题::
-
有一个字符串 str1= “BBC ABCDAB ABCDABCDABDE”,和一个子串 str2=“ABCDABD”
-
现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
-
要求:使用 KMP 算法完成判断,不能使用简单的暴力匹配算法.
14.4.3.1 思路分析图解
1.需要一个next数组,记录字串相同前缀后缀的最大长度
比如ABCDABD的next值为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tuP78JYg-1645524836196)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220213015257938.png)]
2.用next数组匹配的时候,如果字串的某一个字符与模式串匹配不上,i不变,就将j置为next[j-1],就是字串匹配不上字符的上一个字符的next值,用这值作为字串的j索引,用它来和模式串匹配。相当于字串向右移动了j-next[j-1]个位置。
14.4.3.2 代码实现
package com.million.kmp;
import java.util.Arrays;
/**
* @Author: studyboy
* @Date: 2022/2/13 0:37
* KMP 算法 实现匹配字符串
*/
public class KMPAlgorithm {
public static void main (String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmpNext(str2);
System.out.println("next" + Arrays.toString(next));
System.out.println("index=" + kmpSearch(str1, str2, next));
}
/**
* @param str1 文本串
* @param str2 模式串 (字串)
* @param next 部分匹配表 是字串对应的部分皮肤表
* @return 如果是-1就是没有匹配到,否则返回匹配到的位置
*/
public static int kmpSearch (String str1, String str2, int[] next) {
//遍历
for (int i = 0, j = 0; i < str1.length(); i++) {
//如果str1.charAt(i) != str2.charAt(j)
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
//next[j - 1],根据《最大长度表》可得失配字符的上一位字符对应的长度值为next[j - 1],
// 所以根据之前的结论,就是next[j-1]的值是str2的下标,然后用该下标的字符与
// 文本串中失配的地方进行比较,想当预字串向右移动j - next[j - 1]个位置
j = next[j - 1];
}
//如果匹配得上,就继续匹配
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
//如果指向字串得索引j的长度等于字串的长度,就返回插找到的位置的索引
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
//获取到一个字串的部分匹配值表
public static int[] kmpNext (String dest) {
//创建一个next数组保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0; //如果字符串的长度是1,部分匹配值也是0
for (int i = 1, j = 0; i < dest.length(); i++) {
//如果dest.charAt(i) != dest.charAt(j),我们需要从next[j-1]获取新的j
//知道我们发现dest.charAt(i) == dest.charAt(j)满足就退出
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
//核心算法
//既然没有长度为j+1的相同前缀后缀,咱们可以寻找长度短点的相同前缀后缀
//就让j = next[j - 1]
j = next[j - 1];
}
//如果dest.charAt(i) == dest.charAt(j)满足的话,就将j的个数++
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
14.5 贪心算法
14.5.1 贪心算法介绍
-
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而 希望能够导致结果是最好或者最优的算法
-
贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
14.5.2 贪心算法最佳应用-集合覆盖
- 假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有
的地区都可以接收到信号
- 思路分析
(1) 用set集合 allAreas 用来保存需要覆盖的地区
(2) 用selects (ArrayList)来表示可选择的地区
(3)定义一个maxkey指针,当遍历K1…K5的过程中,需要求出每个key值的赋给地区与所有需要覆盖地区的交集的个数(用一个临时变量表示),将maxKey指针指向最大的个数,也就是maxKey=key
(4)key是K1到K5对应的key值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8TpYwQUJ-1645524836197)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220214145301689.png)]
3.代码实现
package com.million.greedy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
/**
* @Author: studyboy
* @Date: 2022/2/14 13:54
* 使用贪心算法解决 集合覆盖问题
*/
public class GreedyAlgorithm {
public static void main (String[] args) {
//创建广播电台 放到Map中
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
//将各个电台放入到broadcasts中
HashSet<String> hashSet1 = new HashSet<>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<>();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<>();
hashSet5.add("杭州");
hashSet5.add("大连");
//加入到map中
broadcasts.put("K1", hashSet1);
broadcasts.put("K2", hashSet2);
broadcasts.put("K3", hashSet3);
broadcasts.put("K4", hashSet4);
broadcasts.put("K5", hashSet5);
//allAreas 存放所有的地区 无重复的
HashSet<String> allAreas = new HashSet<>();
for (HashSet<String> value : broadcasts.values()) {
allAreas.addAll(value);
}
//创建ArrayList,存放选择的电台的集合
ArrayList<String> selects = new ArrayList<>();
//定义一个临时的集合,在遍历过程中,存放在遍历过程中,遍历到Key值的地区和所有地区allAreas的交集
HashSet<String> tempSet = new HashSet<>();
//定义maxKey,保存在一次遍历过程中,得到未被覆盖最多的key值
//如果maxKey 不为null,则会加入到selects
String maxKey;
//用一个临时的变量,记录mayKey这个key值未覆盖地区的最大数量
int tempNum;
while (allAreas.size() != 0) { //不为0 则继续添加
//每次遍历 需要将maxKey置空 tempNum置为0
maxKey = null;
tempNum = 0;
//遍历broadcasts的key值
for (String key : broadcasts.keySet()) {
//每次遍历key值,需要清空tempSet
tempSet.clear();
//表示当前这个key所对应的地区
HashSet<String> areas = broadcasts.get(key);
//添加到临时集合中
tempSet.addAll(areas);
//求出tempSet 和allAreas 集合的交集,如果重新赋值给tempSet
tempSet.retainAll(allAreas);
//如果当前这个集合包含的未覆盖的地区比上一个(mayKey未覆盖的地区)多
//就需要重置maxKey
//tempSet.size() > tempNum)体现出贪心算法的特性,选择最优的
if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > tempNum)) {
maxKey = key;
tempNum = tempSet.size();
}
}
//如果maxKey不为空,就说明可以放入
if (maxKey != null) {
selects.add(maxKey);
//将maxKey指向的广播电台覆盖的地区,从allAreas 去掉
allAreas.removeAll(broadcasts.get(maxKey));
}
}
System.out.println(selects);
}
}
14.6 Prim(普利姆)算法
14.6.1 普里姆算法介绍
普利姆(Prim)算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有(n-1)条边包含所有 n 个顶点的
连通子图,也就是所谓的极小连通子图
14.6.2 普里姆算法最佳实践(修路问题)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-laYe1fkD-1645524836197)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220214184642134.png)]
-
有胜利乡有 7 个村庄(A, B, C, D, E, F, G) ,现在需要修路把 7 个村庄连通
-
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
-
问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
图解普利姆算法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dpUFAY4X-1645524836198)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220214184713534.png)]
代码实现:
package com.million.prim;
import java.util.Arrays;
/**
* @Author: studyboy
* @Date: 2022/2/14 17:48
* 使用prim(普利姆算法) 解决修路问题
*/
public class PrimAlgorithm {
public static void main (String[] args) {
//测试看看图是否创建 ok
char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int verxs = data.length;
//邻接矩阵的关系使用二维数组表示,10000 这个大数,表示两个点不联通
int[][] weight = new int[][]{{10000, 5, 7, 10000, 10000, 10000, 2},
{5, 10000, 10000, 9, 10000, 10000, 3},
{7, 10000, 10000, 10000, 8, 10000, 10000},
{10000, 9, 10000, 10000, 10000, 4, 10000},
{10000, 10000, 8, 10000, 10000, 5, 4},
{10000, 10000, 10000, 4, 5, 10000, 6},
{2, 3, 10000, 10000, 4, 6, 10000},};
//创建 MGraph 对象
MGraph graph = new MGraph(verxs);
//创建一个 MinTree 对象
MinTree minTree = new MinTree();
minTree.createGraph(graph, verxs, data, weight);
//输出
minTree.showGraph(graph);
//最小生成树
minTree.prim(graph, 0);
}
}
//创建最小生成树
class MinTree {
//创建图的邻接矩阵
/**
* @param graph 图对象
* @param verxs 图对应的顶点个数
* @param data 图的各个顶点的值
* @param weight 图的邻接矩阵
*/
public void createGraph (MGraph graph, int verxs, char[] data, int[][] weight) {
for (int i = 0; i < verxs; i++) {
graph.data[i] = data[i];
for (int j = 0; j < verxs; j++) {
graph.weight[i][j] = weight[i][j];
}
}
}
//显示图的邻接矩阵
public void showGraph (MGraph graph) {
for (int[] link : graph.weight) {
System.out.println(Arrays.toString(link));
}
}
//用prim算法解决修路问题
/**
* @param graph 图的对象
* @param v 表示访问的结点
*/
public void prim (MGraph graph, int v) {
//visited数组表示标记(顶点)是否被访问过
int[] visited = new int[graph.verxs];
//将当前这个顶点标记为已访问
visited[v] = 1;
//h1 和 h2 记录最短路径的两个结点对应的下标
int h1 = -1;
int h2 = -1;
//先初始化为一个大数,后面遍历的时候会被替换
int minWeight = 10000;
//这里让k=1,是因为最小生成树的边为n-1条,也就是graph.verxs-1
for (int k = 1; k < graph.verxs; k++) {
//这个双层循环是为了确定每一次生成的子图,哪两个结点最近
for (int i = 0; i < graph.verxs; i++) { //i结点表示已经访问过的结点
for (int j = 0; j < graph.verxs; j++) { //j结点表示未被访问过的结点
if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight) {
minWeight = graph.weight[i][j];
h1 = i;
h2 = j;
}
}
}
//当退出这个双层循环,表示找到了子图中最短的路径
System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + ">权值:" + minWeight);
//将当前这个结点标记为已访问
visited[h2] = 1;
//将minWeight 重新设置为最大值10000
minWeight = 10000;
}
}
}
//图类
class MGraph {
int verxs; //表示图的节点个数
char[] data; //存放节点数据
int[][] weight; //存放边,就是邻接矩阵
public MGraph (int verxs) {
this.verxs = verxs;
data = new char[verxs];
weight = new int[verxs][verxs];
}
}
14.7 Kruskal(克鲁斯卡尔)算法
14.7.1 克鲁斯卡尔算法介绍
-
克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
-
基本思想:按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路
-
具体做法:首先构造一个只含 n 个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森 林中不产生回路,直至森林变成一棵树为止
克鲁斯卡尔算法图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1qyvjwj-1645524836198)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220215124310965.png)]
14.7.2 应用场景-公交站问题
看一个应用场景和问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9SXcMJK8-1645524836198)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220215124452656.png)]
-
某城市新增 7 个站点(A, B, C, D, E, F, G) ,现在需要修路把 7 个站点连通
-
各个站点的距离用边线表示(权) ,比如 A – B 距离 12 公里
-
问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问
题:
问题一 对图的所有边按照权值大小进行排序。
问题二 将边添加到最小生成树中时,怎么样判断是否形成了回路。
问题一很好解决,采用排序算法进行排序即可。
问题二,处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。
然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。
关于终点的说明:
-
就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最大顶点"。
-
因此,接下来,虽然<C,E>是权值最小的边。但是 C 和 E 的终点都是 F,即它们的终点相同,因此,将<C,E>
加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。【后面有代码说明】
14.7.2.1 代码实现和注解
package com.million.kruskal;
import java.util.Arrays;
/**
* @Author: studyboy
* @Date: 2022/2/15 0:00
* 使用克鲁斯卡尔(Kruskal)算法 解决公交问题
*/
public class KruskalAlgorithm {
//边的个数
private int edgeNum;
//顶点数组
private char[] vertexs;
//邻接矩阵的数组
private int[][] matrix;
//使用INF 表示两个点不能连通
public static final int INF = Integer.MAX_VALUE;
public static void main (String[] args) {
char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//克鲁斯卡尔算法的邻接矩阵
int matrix[][] = { /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/ {0, 12, INF, INF, INF, 16, 14},
/*B*/ {12, 0, 10, INF, INF, 7, INF},
/*C*/ {INF, 10, 0, 3, 5, 6, INF},
/*D*/ {INF, INF, 3, 0, 4, INF, INF},
/*E*/ {INF, INF, 5, 4, 0, 2, 8},
/*F*/ {16, 7, 6, INF, 2, 0, 9},
/*G*/ {14, INF, INF, INF, 8, 9, 0}};
//创建 KruskalCase 对象实例
KruskalAlgorithm kruskalCase = new KruskalAlgorithm(vertexs, matrix);
//输出构建的
kruskalCase.print();
System.out.println(Arrays.toString(kruskalCase.getEdges()));
kruskalCase.kruskal();
}
public KruskalAlgorithm (char[] vertexs, int[][] matrix) {
int vlen = vertexs.length;
//初始化顶点,使用拷贝的方式,当改变里面的值时不会影响到传入的参数
this.vertexs = new char[vlen];
this.vertexs = vertexs.clone();
//初始化边,使用的是复制拷贝的方式
this.matrix = new int[vlen][vlen];
for (int i = 0; i < vlen; i++) {
for (int j = 0; j < vlen; j++) {
this.matrix[i][j] = matrix[i][j];
}
}
//统计边的个数,统计上三角,因为这个邻接矩阵是对称的
for (int i = 0; i < vlen; i++) {
for (int j = i + 1; j < vlen; j++) {
if (this.matrix[i][j] != INF) {
edgeNum++;
}
}
}
}
public void kruskal () {
//表示最后结果数组的索引
int index = 0;
//用于保存'已有最小生成树'中的每个顶点在最小生成树中的终点
int[] ends = new int[vertexs.length];
//创建结果数组,保存最后的最小生成树
EData[] rets = new EData[vertexs.length - 1];
//获取图中所有的边的集合,一共有12条边
EData[] edges = getEdges();
//按照边的权值从大到小进行排序
sortEdge(edges);
//遍历edges数组,将边添加到最小生成树中,判断是准备加入的边是否形成回路,如果没有
//就加入rets,否则不能加入
for (int i = 0; i < edgeNum; i++) {
//获取第i条边的第一个顶点
int p1 = getPosition(edges[i].start);
//获取第i条边的第二个顶点
int p2 = getPosition(edges[i].end);
//获取p1这个顶点在已有最小生成树中的终点
int m = getEnd(ends, p1);
//获取p2这个顶点在已有最小生成树中的终点
int n = getEnd(ends, p2);
//判断是否构成回路
if (m != n) { //不构成
ends[m] = n;
rets[index++] = edges[i];
}
}
for (int i = 0; i < rets.length; i++) {
System.out.println(rets[i]);
}
}
//打印邻接矩阵
private void print () {
System.out.println("邻接矩阵为:");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.printf("%-12d\t", matrix[i][j]);
}
System.out.println();
}
}
//对边进行排序处理,冒泡排序
/**
* 功能:对边进行排序处理,冒泡排序
*
* @param edges 边的集合
*/
private void sortEdge (EData[] edges) {
for (int i = 0; i < edges.length - 1; i++) {
for (int j = 0; j < edges.length - 1 - i; j++) {
if (edges[j].weight > edges[j + 1].weight) {
EData tmp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = tmp;
}
}
}
}
/**
*
* @param ch 顶点的值
* @return 返回顶点的下标,找不到,返回-1
*/
private int getPosition (char ch) {
for (int i = 0; i < vertexs.length; i++) {
if (vertexs[i] == ch) {
return i;
}
}
return -1;
}
/**
* 功能:获取图中的边,放入到EData[] 数组中,后面我们需要遍历该数组
* 是通过matrix 邻接矩阵来获取
* EData[] 形式[['A','B',12],['B],'F',7]...
*
* @return 返回图中的边的集合
*/
private EData[] getEdges () {
int index = 0;
EData[] edges = new EData[edgeNum];
//只用遍历上三角,因为这个邻接矩阵是对称的
for (int i = 0; i < vertexs.length; i++) {
for (int j = i + 1; j < vertexs.length; j++) {
if (matrix[i][j] != INF) {
edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 功能:获取下标为i的顶点的终点的下标,用于后面判断两个顶点的终点是否相同
*
* @param ends 该数组就是记录了各个顶点对应的终点是哪个,ends数组是在遍历过程中,逐步形成
* @param i 表示传入的顶点对应的终点
* @return 返回的就是下标为i的这个顶点对应的终点的下标
*/
private int getEnd (int[] ends, int i) {
//循环查找i的顶点的终点 比如C的终点是D,D的终点是F
while (ends[i] != 0) {
i = ends[i];
}
//返回自身
return i;
}
}
//创建一个类EData,它的对象实例就是表示一条边
class EData {
char start; //边的起点
char end; //边的终点
int weight; //边的权值
public EData (char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString () {
return "EData[" +
"<" + start +
"," + end +
"=" + weight +
">]";
}
}
14.8 弗洛伊德算法
14.8.1 弗洛伊德(Floyd)算法介绍
-
和 Dijkstra 算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法 名称以创始人之一、1978 年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
-
弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
-
迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
-
弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点
的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每
一个顶点到其他顶点的最短路径。
14.8.2 弗洛伊德(Floyd)算法图解分析
-
设置顶点 vi 到顶点 vk 的最短路径已知为 Lik,顶点 vk 到 vj 的最短路径已知为 Lkj,顶点 vi 到 vj 的路径为 Lij, 则 vi 到 vj 的最短路径为:min((Lik+Lkj),Lij),vk 的取值为图中所有顶点,则可获得 vi 到 vj 的最短路径
-
至于 vi 到 vk 的最短路径 Lik 或者 vk 到 vj 的最短路径 Lkj,是以同样的方式获得
-
弗洛伊德(Floyd)算法图解分析-举例说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vLzR1PRN-1645524836199)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220220182109072.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IGqECsGe-1645524836199)(C:\Users\13028836273\AppData\Roaming\Typora\typora-user-images\image-20220220182024377.png)]
14.8.3 弗洛伊德(Floyd)算法最佳应用-最短路径
-
胜利乡有 7 个村庄(A, B, C, D, E, F, G)
-
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
-
问:如何计算出各村庄到 其它各村庄的最短距离?
-
代码实现
package com.million.floyd;
import java.util.Arrays;
/**
* @Author: studyboy
* @Date: 2022/2/20 17:37
* 使用Floyd(非洛伊德)算法实现最短路径,是各顶点到其他顶点的最短路径
*/
public class FloydAlgorithm {
public static void main (String[] args) {
// 测试看看图是否创建成功
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//创建邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[]{0, 5, 7, N, N, N, 2};
matrix[1] = new int[]{5, 0, N, 9, N, N, 3};
matrix[2] = new int[]{7, N, 0, N, 8, N, N};
matrix[3] = new int[]{N, 9, N, 0, N, 4, N};
matrix[4] = new int[]{N, N, 8, N, 0, 5, 4};
matrix[5] = new int[]{N, N, N, 4, 5, 0, 6};
matrix[6] = new int[]{2, 3, N, N, 4, 6, 0};
Graph graph = new Graph(vertex.length, matrix, vertex);
//调用floyd算法
graph.floyd();
//调用show方法
graph.show();
}
}
//创建图
class Graph {
private char[] vertex; //存放顶点的数组
private int[][] dis; // 保存,从各个顶点出发到其它顶点的距离,最后的结果,也是保留在该数组
private int[][] pre;// 保存到达目标顶点的前驱顶点
/**
* @param length 顶点的个数
* @param matrix 邻接矩阵
* @param vertex 存放顶点的数组
*/
public Graph (int length, int[][] matrix, char[] vertex) {
this.vertex = vertex;
this.dis = matrix;
this.pre = new int[length][length];
// 对 pre 数组初始化, 注意存放的是前驱顶点的下标
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
//非洛伊德算法实现各个顶点到其他顶点的最短距离
public void floyd () {
int len = 0; //保存距离
//对中间顶点k(就是以k为中间顶点的所有路径)进行遍历,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]){ //如果len小于dis[i][j]的直接距离
dis[i][j] = len; //更新距离
//这里不直接使用k赋值是因为B-C经过A,A-C可能也经过某个点,所有用pre[k][j]赋值
pre[i][j] = pre[k][j]; //更新前驱顶点(重点)
}
}
}
}
}
// 显示 pre 数组和 dis 数组
public void show () {
//为了显示便于阅读,我们优化一下输出
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
for (int k = 0; k < dis.length; k++) {
// 先将 pre 数组输出的一行
for (int i = 0; i < dis.length; i++) {
System.out.print(vertex[pre[k][i]] + " ");
}
System.out.println();
// 输出 dis 数组的一行数据
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路径是" + dis[k][i] + ") ");
}
System.out.println();
}
}
}
`
j = i + 1; j < vertexs.length; j++) {
if (matrix[i][j] != INF) {
edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 功能:获取下标为i的顶点的终点的下标,用于后面判断两个顶点的终点是否相同
*
* @param ends 该数组就是记录了各个顶点对应的终点是哪个,ends数组是在遍历过程中,逐步形成
* @param i 表示传入的顶点对应的终点
* @return 返回的就是下标为i的这个顶点对应的终点的下标
*/
private int getEnd (int[] ends, int i) {
//循环查找i的顶点的终点 比如C的终点是D,D的终点是F
while (ends[i] != 0) {
i = ends[i];
}
//返回自身
return i;
}
}
//创建一个类EData,它的对象实例就是表示一条边
class EData {
char start; //边的起点
char end; //边的终点
int weight; //边的权值
public EData (char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString () {
return "EData[" +
"<" + start +
"," + end +
"=" + weight +
">]";
}
}
### 14.8 弗洛伊德算法
#### 14.8.1 弗洛伊德(Floyd)算法介绍
1) 和 Dijkstra 算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法 名称以创始人之一、1978 年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
2) 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
3) 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
4) 弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点
的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每
一个顶点到其他顶点的最短路径。
#### 14.8.2 弗洛伊德(Floyd)算法图解分析
1) 设置顶点 vi 到顶点 vk 的最短路径已知为 Lik,顶点 vk 到 vj 的最短路径已知为 Lkj,顶点 vi 到 vj 的路径为 Lij, 则 vi 到 vj 的最短路径为:min((Lik+Lkj),Lij),vk 的取值为图中所有顶点,则可获得 vi 到 vj 的最短路径
2) 至于 vi 到 vk 的最短路径 Lik 或者 vk 到 vj 的最短路径 Lkj,是以同样的方式获得
3) 弗洛伊德(Floyd)算法图解分析-举例说明
[外链图片转存中...(img-vLzR1PRN-1645524836199)]
[外链图片转存中...(img-IGqECsGe-1645524836199)]
#### 14.8.3 弗洛伊德(Floyd)算法最佳应用-最短路径
1) 胜利乡有 7 个村庄(A, B, C, D, E, F, G)
2) 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
3) 问:如何计算出各村庄到 其它各村庄的最短距离?
4) 代码实现
```java
package com.million.floyd;
import java.util.Arrays;
/**
* @Author: studyboy
* @Date: 2022/2/20 17:37
* 使用Floyd(非洛伊德)算法实现最短路径,是各顶点到其他顶点的最短路径
*/
public class FloydAlgorithm {
public static void main (String[] args) {
// 测试看看图是否创建成功
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//创建邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[]{0, 5, 7, N, N, N, 2};
matrix[1] = new int[]{5, 0, N, 9, N, N, 3};
matrix[2] = new int[]{7, N, 0, N, 8, N, N};
matrix[3] = new int[]{N, 9, N, 0, N, 4, N};
matrix[4] = new int[]{N, N, 8, N, 0, 5, 4};
matrix[5] = new int[]{N, N, N, 4, 5, 0, 6};
matrix[6] = new int[]{2, 3, N, N, 4, 6, 0};
Graph graph = new Graph(vertex.length, matrix, vertex);
//调用floyd算法
graph.floyd();
//调用show方法
graph.show();
}
}
//创建图
class Graph {
private char[] vertex; //存放顶点的数组
private int[][] dis; // 保存,从各个顶点出发到其它顶点的距离,最后的结果,也是保留在该数组
private int[][] pre;// 保存到达目标顶点的前驱顶点
/**
* @param length 顶点的个数
* @param matrix 邻接矩阵
* @param vertex 存放顶点的数组
*/
public Graph (int length, int[][] matrix, char[] vertex) {
this.vertex = vertex;
this.dis = matrix;
this.pre = new int[length][length];
// 对 pre 数组初始化, 注意存放的是前驱顶点的下标
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
//非洛伊德算法实现各个顶点到其他顶点的最短距离
public void floyd () {
int len = 0; //保存距离
//对中间顶点k(就是以k为中间顶点的所有路径)进行遍历,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]){ //如果len小于dis[i][j]的直接距离
dis[i][j] = len; //更新距离
//这里不直接使用k赋值是因为B-C经过A,A-C可能也经过某个点,所有用pre[k][j]赋值
pre[i][j] = pre[k][j]; //更新前驱顶点(重点)
}
}
}
}
}
// 显示 pre 数组和 dis 数组
public void show () {
//为了显示便于阅读,我们优化一下输出
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
for (int k = 0; k < dis.length; k++) {
// 先将 pre 数组输出的一行
for (int i = 0; i < dis.length; i++) {
System.out.print(vertex[pre[k][i]] + " ");
}
System.out.println();
// 输出 dis 数组的一行数据
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路径是" + dis[k][i] + ") ");
}
System.out.println();
}
}
}
`