数据结构和算法
数据结构和算法
几个编程中遇到的实际问题
问题一:字符串替换问题

小结:需要使用到单链表数据结构。
问题二:五子棋程序

如何判断游戏的输赢,并可以完成存盘退出和继续上局的功能。
- 棋盘 --> 二维数组 --> 稀疏数组 --> 写入文件 【存档功能】
- 读取文件 --> 稀疏数组 --> 二维数组 --> 棋盘 【接上局】
问题三:约瑟夫(Josephu)问题(丢手帕问题)
- Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
- 提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表(单向环形链表),然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。
- 小结:完成约瑟夫问题,需要使用到单向环形链表这个数据结构
其他常见算法问题
- 修路问题 --> 最小生成树 + 普利姆算法
- 最短路径问题 --> 图 + 弗洛伊德算法
- 汉诺塔 --> 分支算法
- 八皇后问题 --> 回溯法
线性结构和非线性结构
数据结构包括:线性结构和非线性结构
线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即 顺序存储结构(数组) 和 链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的(地址是连续的)
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
- 线性结构常见的有:数组、队列、链表和栈,后面我们会详细讲解
非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
稀疏数组和队列
稀疏数组
基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
举例

应用实例
- 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
- 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
- 整体思路分析

4. 代码实现
public class SparseArray {
public static void main(String[] args) {
// 创建一个原始的二维数组 11*11 // 0:表示没有棋子,1:表示黑子,2:表示蓝子
int chessArr1[][] = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][3] = 2;
// 输出原始的二维数组
System.out.println("========原始的二维数组========");
for (int[] row : chessArr1) {
for (int data : row) {
System.out.print(data + "\t");
}
System.out.println();
}
// 将二维数组转为稀疏数组
// 1. 先遍历二维数组,得到非零数据的个数
int sum = 0;
for (int i = 0; i < chessArr1.length; i++) {
for (int j = 0; j < chessArr1[i].length; j++) {
if (chessArr1[i][j] != 0) {
sum++;
}
}
}
// 2. 创建对应的稀疏数组
int sparseArr[][] = new int[sum + 1][3];
// 给稀疏数组赋值
sparseArr[0][0] = chessArr1.length;
sparseArr[0][1] = chessArr1[1].length;
sparseArr[0][2] = sum;
// 遍历二维数组,将非0的值存放到稀疏数组中
int count = 0; // 用于记录是第几个非0数据
for (int i = 0; i < chessArr1.length; i++) {
for (int j = 0; j < chessArr1[i].length; 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.print(sparseArr[i][0] + "\t" + sparseArr[i][1] + "\t" + sparseArr[i][2] + "\n");
}
// 将稀疏数组恢复成原始的二维数组
// 1. 先读取稀疏数组的读一行,根据第一行的数据,创建原始的二维数组
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.print(data + "\t");
}
System.out.println();
}
}
}
队列
队列介绍
- 队列是一个有序列表,可以用数组或是链表来实现。
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
- 示意图:(使用数组模拟队列示意图)

数组模拟队列
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear 则是随着数据输入而改变。
- 当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析
- 将尾指针往后移:rear+1 , 当 front == rear 【空】
- 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。
rear == maxSize - 1[队列满]
- 代码实现
import java.util.Scanner;
/**
* 使用数组模拟队列
*/
public class ArrayQueueDemo {
public static void main(String[] args) {
// 创建一个队列
ArrayQueue arrayQueue = new ArrayQueue(3);
char key = ' '; // 接收用户输入
Scanner sc = 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 = sc.next().charAt(0); // 接受一个字符
switch (key) {
case 's':
arrayQueue.showQueue();
break; case 'e':
sc.close();
loop = false;
break; case 'a':
System.out.println("请输入一个数字");
int value = sc.nextInt();
arrayQueue.addQueue(value);
break; case 'g':
try {
int res = arrayQueue.getQueue();
System.out.println("取出的数据为:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int res = arrayQueue.headQueue();
System.out.println("队列的头数据为:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
default:
break;
}
}
System.out.println("========程序退出========");
}
}
class ArrayQueue {
private int maxSize; // 表示数组最大容量
private int front; // 指向队列头
private int rear; // 指向队列尾
private int[] arr; // 存放数据,模拟队列
// 创建队列的构造器
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize];
front = -1; // 指向队列头的前一个数据
rear = -1; // 指向队列尾的数据
}
// 判断队列是否满
public boolean isFull() {
return rear == maxSize - 1;
}
// 判断队列是否为空
public boolean isEmpty() {
return rear == front;
}
// 添加数据到队列
public void addQueue(int n) {
// 判断队列是否满
if (isFull()) {
System.out.println("队列满了,不能再加入数据");
return; }
// 让rear后移
rear++;
arr[rear] = n;
}
// 获取队列的数据
public int getQueue() {
// 判断是否为空
if (isEmpty()) {
// 抛出异常
throw new RuntimeException("队列为空,无法取数据");
}
front++;
return arr[front];
}
// 显示队列的所有数据
public void showQueue() {
if (isEmpty()) {
System.out.println("队列为空,没有数据");
return; }
for (int i = 0; i < arr.length; i++) {
System.out.println("arr[" + i + "]=" + arr[i]);
}
}
// 显示队列的头数据,不是取数据
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,没有数据");
}
return arr[front + 1];
}
}
问题分析并优化
- 目前数组使用一次就不能用了, 没有达到复用的效果
- 将这个数组使用算法,改进成一个环形的队列 取模:%
数组模拟环形队列
对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取模的方式来实现即可)
分析说明:
- 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的
时候需要注意 (rear + 1) % maxSize == front 满] - rear == front [空]
- 分析示意图:

4. 代码实现
import java.util.Scanner;
/**
* 使用数组模拟环形队列
*/
public class CircleArrayQueueDemo {
public static void main(String[] args) {
// 创建一个队列
CircleArray circleArray = new CircleArray(4);
char key = ' '; // 接收用户输入
Scanner sc = 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 = sc.next().charAt(0); // 接受一个字符
switch (key) {
case 's':
circleArray.showQueue();
break; case 'e':
sc.close();
loop = false;
break; case 'a':
System.out.println("请输入一个数字");
int value = sc.nextInt();
circleArray.addQueue(value);
break; case 'g':
try {
int res = circleArray.getQueue();
System.out.println("取出的数据为:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int res = circleArray.headQueue();
System.out.println("队列的头数据为:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
default:
break;
}
}
System.out.println("========程序退出========");
}
}
class CircleArray {
private int maxSize; // 表示数组最大容量
private int front; // 指向队列头,初始值为0
private int rear; // 指向队列尾的前一个位置,空出一个位置作为约定,初始值为0
private int[] arr; // 存放数据,模拟队列
// 创建队列的构造器
public CircleArray(int maxSize) {
this.maxSize = maxSize;
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; }
// rear是前一个位置,直接将数据加入
arr[rear] = n;
// 将rear后移
rear = (rear + 1) % maxSize;
}
// 获取队列的数据
public int getQueue() {
// 判断是否为空
if (isEmpty()) {
// 抛出异常
throw new RuntimeException("队列为空,无法取数据");
}
// front指向队列的第一个元素
// 1. 先把front对应的值保存到一个临时变量
int temp = arr[front];
// 2. 将front后移
front = (front + 1) % maxSize;
// 3. 将临时保存的变量返回
return temp;
}
// 显示队列的所有数据
public void showQueue() {
if (isEmpty()) {
System.out.println("队列为空,没有数据");
return; }
// 从front开始遍历,遍历有效元素个数个元素
for (int i = front; i < front + size(); i++) {
System.out.println("arr[" + (i % maxSize) + "]=" + arr[i % maxSize]);
}
}
// 求出当前队列有效数据的个数
public int size() {
return (rear + maxSize - front) % maxSize;
}
// 显示队列的头数据,不是取数据
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,没有数据");
}
// front指向队列的第一个元素,无需加1
return arr[front];
}
}
链表
链表(Linked List)介绍
链表是有序的列表,但是它在内存中是存储如下:

小结:
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点
- 如图:发现链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
- 单链表(带头结点) 逻辑结构示意图如下:

单链表的应用实例
使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作
- 第一种方法在添加英雄时,直接添加到链表的尾部
思路分析示意图:

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 singleLinkedList = new SingleLinkedList();
// 加入
singleLinkedList.add(hero1);
singleLinkedList.add(hero2);
singleLinkedList.add(hero3);
singleLinkedList.add(hero4);
// 显示链表
singleLinkedList.list();
}
}
// 定义一个SingleLinkedList管理英雄人物
class SingleLinkedList {
// 初始化一个头节点,头节点不要动,不存放具体数据
private HeroNode head = new HeroNode(0, "", "");
// 添加节点到单向链表
// 思路:当不考虑编号的顺序时,找到当前链表的最后节点,将最后这个节点的next指向新的节点
public void add(HeroNode heroNode) {
// 因为head节点不能动,伊因此需要一个辅助指针
HeroNode temp = head;
// 遍历链表,找到最后
while (true) {
// 找到链表的最后了
if (temp.next == null) {
break;
}
// 如果没有找到最后,就将temp后移
temp = temp.next;
}
// 当退出这个循环时,temp就指向了链表的最后
temp.next = heroNode;
}
// 显示链表
public void list() {
// 判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return; }
// 因为头节点不能动,因此需要一个辅助变量来遍历
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 void addByOrder(HeroNode heroNode) {
// 因为头节点不能动,通过辅助变量来找到添加的位置
// 因为时单链表,因此找的temp在添加位置的前一个节点,否则无法插入
HeroNode temp = head;
boolean flag = false; // 标识添加的编号是否存在
while (true) {
if (temp.next == null) {
// 说明temp已经在链表的最后了
break;
}
if (temp.next.no > heroNode.no) {
// 位置找到了,就在temp后面插入新节点
break;
} else if (temp.next.no == heroNode.no) {
// 新节点的编号已经存在,不能添加
flag = true; // 说明编号存在
break;
}
temp = temp.next; // 后移,相当于遍历当前列表
}
// 判断flag的值
if (flag) {
// 新节点的编号已经存在,不能添加
System.out.println("准备插入的英雄的编号" + heroNode.no + "已经存在,不能加入");
} else {
// 插入到链表中,temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
- 修改节点功能
思路:
(1) 先找到该节点,通过遍历
(2)temp.name = newHeroNode.name; temp.nickname= newHeroNode.nickname;
// 修改节点的信息,根据no编号来修改,即no编号不能改
public void update(HeroNode newHeroNode) {
// 判断是否为空
if (head.next == null) {
System.out.println("链表为空");
return; }
// 找到需要修改的节点,根据no编号
HeroNode temp = head.next;
boolean flag = false; // 表示是否找到该节点
while (true) {
if (temp == null) {
// 链表已经遍历完了
break;
}
if (temp.no == newHeroNode.no) {
// 说明找到了
flag = true;
break; }
temp = temp.next;
}
// 根据flag判断是否找到要修改的节点
if (flag) {
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
} else {
// 没有找到
System.out.println("没有找到编号为" + newHeroNode.no + "的节点");
}
}
- 删除节点
思路分析的示意图:

5) 完成的代码演示:
// 删除节点
public void delete(int no) {
HeroNode temp = head;
boolean flag = false; // 标识是否找到待删除节点
while (true) {
if (temp.next == null) {
// 已经到链表的最后
break;
}
if (temp.next.no == no) {
// 找到了待删除节点的前一个节点temp
flag = true;
break; }
temp = temp.next;
}
if (flag) {
// 找到,可以删除
temp.next = temp.next.next;
} else {
System.out.println("要删除的" + no + "节点不存在");
}
}
单链表面试题(新浪、百度、腾讯)
求单链表中有效节点的个数
/**
* 面试题一:获取到单链表节点的个数(如果是带头节点的链表,则不统计头节点)
*
* @param head 链表的头节点
* @return 返回值是有效节点的个数
*/
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;
}
查找单链表中的倒数第 k 个结点 【新浪面试题】
/**
* 面试题二:查找单链表中的倒数第k个节点
* 思路:
* 1. 编写一个方法,接收head节点,同时接收一个index
* 2. index表示倒数第index个节点
* 3. 先把链表从头到尾遍历,的到链表的总长度size
* 4。 得到size后,从第一个元素开始遍历(size - index)个,就可以得到
* 5. 如果找到了,就返回该节点;没找到就返回null
*
* @param head 链表的头节点
* @param index 倒数第index个节点
* @return 返回值是倒数第index个节点
*/
public static HeroNode findLastIndexOfNode(HeroNode head, int index) {
// 判断链表如果为空,返回null
if (head.next == null) {
return null;
}
// 第一次遍历得到链表的长度
int size = getLength(head);
// 校验数据
if (index <= 0 || index > size) {
return null;
}
// 第二次遍历
HeroNode temp = head.next;
for (int i = 0; i < size - index; i++) {
temp = temp.next;
}
return temp;
}
单链表的反转【腾讯面试题,有点难度】

/**
* 将单链表反转
*
* @param head 头节点
*/
public static void reverseList(HeroNode head) {
// 如果当前链表为空,或者只有一个节点,就无需反转
if (head.next == null || head.next.next == null) {
return;
}
// 定义一个辅助指针,帮助我们遍历原来的链表
HeroNode cur = head.next;
HeroNode next = null; // 指向当前节点[cur]的下一个节点
HeroNode reverseHead = new HeroNode(0, "", "");
// 遍历原来的链表
while (cur != null) {
next = cur.next; // 暂时保存当前节点的下一个节点
cur.next = reverseHead.next; // 将cur下一个节点指向新链表的头节点
reverseHead.next = cur;
cur = next; // 让cur后移一次
}
// 将head.next指向reverseHead.next,实现单链表的反转
head.next = reverseHead.next;
}
从尾到头打印单链表 【百度,要求方式 1:反向遍历 。 方式 2:Stack 栈】


// 使用stack栈的方式来逆序打印单链表,没有改变链表本身的结构
public static void reversePrint(HeroNode head) {
if (head.next == null) {
// 空链表,无法打印
return;
}
// 创建一个栈,将各个节点压入栈中
Stack<HeroNode> stack = new Stack<>();
HeroNode cur = head.next;
// 将链表的所有节点压入栈中
while (cur != null) {
stack.push(cur);
cur = cur.next; // cur后移,这样可以压入下一个节点
}
while (stack.size() > 0) {
System.out.println(stack.pop());
}
}
双向链表应用实例
双向链表的操作分析和实现
使用带 head 头的双向链表实现 –水浒英雄排行榜
管理单向链表的缺点分析:
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到 temp,temp 是待删除节点的前一个节点.
- 分析双向链表如何完成遍历,添加,修改和删除的思路。

对上图的说明:
分析 双向链表的遍历,添加,修改,删除的操作思路 --> 代码实现
- 遍历方式和单链表一样,只是可以向前查找,也可以向后查找
- 添加 (默认添加到双向链表的最后)
- 先找到双向链表的最后这个节点
temp.next = newHeroNode;newHeroNode.pre = temp;
- 修改思路和原来的单向链表一样.
- 删除
- 因为是双向链表,因此,我们可以实现自我删除某个节点
- 直接找到要删除的这个节点,比如 temp
temp.pre.next = temp.next;temp.next.pre = temp.pre;
双向链表的代码实现
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, "林冲", "豹子头");
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
/*doubleLinkedList.add(hero1);
doubleLinkedList.add(hero2); doubleLinkedList.add(hero3); doubleLinkedList.add(hero4);*/
// 第二种添加方式
doubleLinkedList.addByOrder(hero1);
doubleLinkedList.addByOrder(hero4);
doubleLinkedList.addByOrder(hero2);
doubleLinkedList.addByOrder(hero3);
System.out.println("========原始链表========");
doubleLinkedList.list();
// 修改
HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "入云龙");
doubleLinkedList.update(newHeroNode);
System.out.println("========修改后的链表========");
doubleLinkedList.list();
// 删除
doubleLinkedList.delete(3);
System.out.println("========删除后的链表========");
doubleLinkedList.list();
}
}
// 创建一个双向链表的类
class DoubleLinkedList {
// 先初始化一个头节点,头节点不要动,不存放具体的数据
private final HeroNode2 head = new HeroNode2(0, "", "");
// 返回头节点
public HeroNode2 getHead() {
return head;
}
// 第二种添加英雄的方法
public void addByOrder(HeroNode2 heroNode) {
// 因为头节点不能动,通过辅助变量来找到添加的位置
HeroNode2 temp = head;
boolean flag = false; // 标识添加的编号是否存在
while (true) {
if (temp.next == null) {
// 说明temp已经在链表的最后了
break;
}
if (temp.next.no > heroNode.no) {
// 位置找到了,就在temp下一个的前面插入新节点
break;
}
if (temp.next.no == heroNode.no) {
// 新节点的编号已经存在,不能添加
flag = true; // 说明编号存在
break;
}
temp = temp.next; // 后移,相当于遍历当前列表
}
// 判断flag的值
if (flag) {
// 新节点的编号已经存在,不能添加
System.out.println("准备插入的英雄的编号" + heroNode.no + "已经存在,不能加入");
} else if (temp.next != null) {
temp.next.pre = heroNode;
heroNode.next = temp.next;
heroNode.pre = temp;
temp.next = heroNode;
} else {
temp.next = heroNode;
heroNode.pre = heroNode;
}
}
// 从双向链表中删除一个节点,双向链表可以直接找到需要删除的节点,然后自我删除即可
public void delete(int no) {
// 判断当前链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法删除");
}
HeroNode2 temp = head.next;
boolean flag = false; // 标识是否找到待删除节点
while (true) {
if (temp == null) {
// 已经到链表的最后
break;
}
if (temp.no == no) {
// 找到了待删除节点
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
// 找到,可以删除
temp.pre.next = temp.next;
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.println("要删除的" + no + "节点不存在");
}
}
// 修改一个节点的内容,可以看到双向链表的节点内容修改方法和单向链表一样
public void update(HeroNode2 newHeroNode) {
// 判断是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
// 找到需要修改的节点,根据no编号
HeroNode2 temp = head.next;
boolean flag = false; // 表示是否找到该节点
while (true) {
if (temp == null) {
// 链表已经遍历完了
break;
}
if (temp.no == newHeroNode.no) {
// 说明找到了
flag = true;
break;
}
temp = temp.next;
}
// 根据flag判断是否找到要修改的节点
if (flag) {
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
} else {
// 没有找到
System.out.println("没有找到编号为" + newHeroNode.no + "的节点");
}
}
// 添加一个节点到双向链表的最后
public void add(HeroNode2 heroNode) {
// 因为head节点不能动,伊因此需要一个辅助指针
HeroNode2 temp = head;
// 遍历链表,找到最后
while (true) {
// 找到链表的最后了
if (temp.next == null) {
break;
}
// 如果没有找到最后,就将temp后移
temp = temp.next;
}
// 当退出这个循环时,temp就指向了链表的最后
// 形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
// 遍历双向链表
public void list() {
// 判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
// 因为头节点不能动,因此需要一个辅助变量来遍历
HeroNode2 temp = head.next;
while (true) {
// 判断是否到链表最后
if (temp == null) {
break;
}
// 输出节点的信息
System.out.println(temp);
// 将temp后移
temp = temp.next;
}
}
}
// 定义一个heroNode,每一个heroNode对象就是一个节点
class HeroNode2 {
public int no;
public String name;
public String nickName;
public HeroNode2 next; // 指向下一个节点
public HeroNode2 pre; // 指向前一个节点
// 构造器
public HeroNode2(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 + '\'' +
'}';
}
}
单向环形链表应用场景
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。
单向环形链表介绍

Josephu 问题
约瑟夫问题的示意图

约瑟夫问题-创建环形链表的思路图解

约瑟夫问题-小孩出圈的思路分析图

Josephu 问题的代码实现
public class Josephu {
public static void main(String[] args) {
// 测试
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5); // 加入5个小孩节点
circleSingleLinkedList.showBoy();
System.out.println("===================================");
circleSingleLinkedList.countBoy(1, 2, 5);
}
}
// 创建一个环形的单向链表
class CircleSingleLinkedList {
// 创建一个first节点,当前没有编号
private Boy first = null;
/**
* 根据用户的输入,计算出小孩出圈的顺序
*
* @param start 表示从第几个小孩开始数
* @param countNum 表示数几下
* @param nums 表述最初有多少个小孩在圈中
*/
public void countBoy(int start, int countNum, int nums) {
// 先对数据进行校验
if (first == null || start < 1 || start > nums) {
System.out.println("参数输入有误,请重新输入");
return;
}
// 创建一个辅助指针,帮助完成小孩出圈
Boy helper = first;
// 让辅助指针指向链表最后的节点
while (true) {
if (helper.getNext() == first) {
// helper指向了最后一个节点
break;
}
helper = helper.getNext();
}
// 小孩报数前,先让helper和first移动 k-1 次
for (int i = 0; i < start - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
// 当小孩报数时,让helper和first指针同时移动 m-1 次,然后让first指向的小孩节点出圈
// 这里是一个循环操作,直到圈中只有一个节点
while (true) {
if (helper == first) {
// 说明圈中只有一个节点
break;
}
// 让helper和first指针同时移动 countNum-1 次,然后让first指向的小孩节点出圈
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
// 这时first指向的节点就是要出圈的小孩节点
System.out.println("小孩" + first.getIndex() + "出圈");
// 这时让first指向的小孩节点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.println("最后留在圈中的小孩编号是:" + first.getIndex());
}
// 添加小号节点,构建成一个环形链表
public void addBoy(int nums) {
// 对nums做一个数据校验
if (nums < 1) {
System.out.println("nums的值不正确");
return;
}
Boy currentBoy = null; // 辅助指针,帮助构建环形链表
// 使用for循环来创建环形链表
for (int i = 1; i <= nums; i++) {
// 根据编号创建小孩节点
Boy boy = new Boy(i);
// 如果是第一个小孩
if (i == 1) {
first = boy;
first.setNext(first); // 构成一个环
currentBoy = first; // 让currentBoy指向第一个小孩
} else {
currentBoy.setNext(boy);
boy.setNext(first);
currentBoy = boy;
}
}
}
// 遍历当前环形链表
public void showBoy() {
// 判断链表是否为空
if (first == null) {
// 为空
System.out.println("链表为空");
return;
}
// 因为first不能动,因此我们任然使用一个辅助指针完成遍历
Boy currentBoy = first;
while (true) {
System.out.println("小孩的编号为:" + currentBoy.getIndex());
if (currentBoy.getNext() == first) {
// 已经遍历完毕
break;
}
currentBoy = currentBoy.getNext(); // 让currentBoy后移
}
}
}
// 创建一个boy类,表示一个节点
class Boy {
private int index; // 编号
private Boy next; // 指向下一个节点,默认为空
public Boy(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
栈
栈的一个实际需求
请输入一个表达式
计算式:[722-5+1-5+3-3] 点击计算【如下图】

请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。-> 栈
栈的介绍
- 栈的英文为(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
- 图解方式说明出栈(pop)和入栈(push)的概念


栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth 一 first)搜索法。
栈的快速入门
- 用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈,入栈等操作。
- 实现思路分析,并画出示意图

3. 代码实现
import java.util.Scanner;
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("pop:添加数据到栈");
System.out.println("push:从栈中取数据");
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.println("出栈的数据为:" + res);
} catch (Exception e) {
e.printStackTrace();
}
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; // 栈顶,初始化为-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("栈满");
return;
}
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.println("stack[" + i + "]=" + stack[i]);
}
}
}
栈实现综合计算器(中缀表达式)
- 使用栈来实现综合计算器

- 思路分析(图解)

- 代码实现
public class Calculator {
public static void main(String[] args) {
String expression = "7*2*2-5+1-5+3-4";
ArrayStack2 numberStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
// 定义需要的相关变量
int index = 0; // 用于扫描
int num1 = 0;
int num2 = 0;
int oper = 0;
int result = 0;
char ch = ' '; // 将每次扫描得到的char保存到ch中
String keepNum = ""; // 用于拼接多位数
// 开始循环扫描expression
while (true) {
// 依次得到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 = numberStack.pop();
num2 = numberStack.pop();
oper = operStack.pop();
result = numberStack.calc(num1, num2, oper);
// 把运算的结果如数栈
numberStack.push(result);
// 将当前的操作符入符号栈
operStack.push(ch);
} else {
// 如果当前的操作符的优先级大于栈中的操作符, 就直接入符号栈
operStack.push(ch);
}
} else {
// 如果为空,直接入符号栈
operStack.push(ch);
}
} else {
// 如果是数,则直接入数栈
// 处理多位数
keepNum += ch;
// 如果ch已经是expression的最后一位,就直接入栈
if (index == expression.length() - 1) {
numberStack.push(Integer.parseInt(keepNum));
} else {
// 判断下一个字符是不是数字,如果是数字就继续扫描,如果是运算符,则入栈
if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) {
// 如果后一位是运算符,则入栈
numberStack.push(Integer.parseInt(keepNum));
// 清空keepNum
keepNum = "";
}
}
}
// 让index+1,并判断是否扫描到expression的最后
index++;
if (index >= expression.length()) {
break;
}
}
// 当表达式扫描完毕,就顺序的从 数栈和符号栈中 pop 出相应的数和符号,并运行.
while (true) {
// 如果符号栈为空,则计算到最后的结果,则数栈中只有一个数字(结果)
if (operStack.isEmpty()) {
break;
}
num1 = numberStack.pop();
num2 = numberStack.pop();
oper = operStack.pop();
result = numberStack.calc(num1, num2, oper);
numberStack.push(result);
}
System.out.println("表达式 " + expression + " = " + numberStack.pop());
}
}
// 先创建一个栈
class ArrayStack2 {
private int maxSize; // 栈的大小
private int[] stack; // 数组模拟栈,数据就放在该数组
private int top = -1; // 栈顶,初始化为-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("栈满");
return; }
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.println("stack[" + 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 calc(int num1, int num2, int oper) {
int result = 0; // 用于存放计算的结果
switch (oper) {
case '+':
result = num1 + num2;
break;
case '-':
result = num2 - num1;
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num2 / num1;
break;
default:
break;
}
return result;
}
}
前缀、中缀、后缀表达式(逆波兰表达式)
前缀表达式(波兰表达式)
- 前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
- 举例说明:(3+4)×5-6对应的前缀表达式为 - × + 3 4 5 6
前缀表达式的计算机求值
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。
例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
- 从右至左扫描,将6、5、4、3压入堆栈
- 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
- 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
中缀表达式
- 中缀表达式就是常见的运算表达式,如(3+4)×5-6
- 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)
后缀表达式
- 后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
- 举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
- 再比如:

后缀表达式的计算机求值
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
- 从左至右扫描,将3和4压入堆栈;
- 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
- 将5入栈;
- 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
- 将6入栈;
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
逆波兰计算器
我们完成一个逆波兰计算器,要求完成如下任务:
- 输入一个逆波兰表达式(后缀表达式),使用栈(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,由此得出最终结果
- 代码完成
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class PolandNotation {
public static void main(String[] args) {
// 先定义一个逆波兰表达式
//说明为了方便,逆波兰表达式 的数字和符号使用空格隔开
String suffixExpression = "3 4 + 5 * 6 -";
//思路
//1. 先将 "3 4 + 5 × 6 - " => 放到 ArrayList 中
//2. 将 ArrayList 传递给一个方法,遍历 ArrayList 配合栈 完成计算
List<String> rpnList = getListString(suffixExpression);
System.out.println("rpnList=" + rpnList);
int result = calculator(rpnList);
System.out.println("计算的结果是:" + result);
}
// 依次将数据和运算符放入ArrayList中
public static List<String> getListString(String suffixExpression) {
// 将suffixExpression分割
String[] split = suffixExpression.split(" ");
ArrayList<String> list = new ArrayList<String>();
for (String ele : split) {
list.add(ele);
}
return list;
}
// 完成对逆波兰表达式的运算
public static int calculator(List<String> ls) {
// 创建一个栈
Stack<String> stack = new Stack<>();
// 遍历ls
for (String item : ls) {
// 使用正则表达式来取数
if (item.matches("\\d+")) {
// 匹配的是多位数
stack.push(item);
} else {
// pop 出两个数,并运算, 再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int result = 0;
if (item.equals("+")) {
result = num1 + num2;
} else if (item.equals("-")) {
result = num1 - num2;
} else if (item.equals("*")) {
result = num1 * num2;
} else if (item.equals("/")) {
result = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
// 把结果入栈
stack.push("" + result);
}
}
// 最后留在stack中的数据就是运算结果
return Integer.parseInt(stack.pop());
}
}
中缀表达式转换为后缀表达式
大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将中缀表达式转成后缀表达式。
具体步骤如下:
- 初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2;
- 从左至右扫描中缀表达式;
- 遇到操作数时,将其压 s2;
- 遇到运算符时,比较其与 s1 栈顶运算符的优先级:
- 如果 s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
- 否则,若优先级比栈顶运算符的高,也将运算符压入 s1;
- 否则,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4-1)与 s1 中新的栈顶运算符相比较;
- 遇到括号时:
- 如果是左括号“(”,则直接压入 s1
- 如果是右括号“)”,则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到左括号为止,此时将这一对括号丢弃
- 重复步骤 2 至 5,直到表达式的最右边
- 将 s1 中剩余的运算符依次弹出并压入 s2
- 依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
举例说明:
将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下

因此结果为 :"1 2 3 + 4 × + 5 –"
代码实现中缀表达式转为后缀表达式
- 思路分析

- 代码实现
// 将得到的中缀表达式对应的 List => 后缀表达式对应的 List
public static List<String> parseSuffixExpression(List<String> ls) {
// 定义两个栈
Stack<String> s1 = new Stack<String>(); // 符号栈
//说明:因为 s2 这个栈,在整个转换过程中,没有 pop 操作,而且后面我们还需要逆序输出
//因此比较麻烦,这里我们就不用 Stack<String> 直接使用 List<String> s2 // Stack<String> s2 = new Stack<>(); // 存储中间结果的栈s2
ArrayList<String> s2 = new ArrayList<String>(); // 存储中间结果的栈s2
// 遍历ls
for (String item : ls) {
// 如果是一个数,就入s2栈
if (item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
s1.push(item);
} else if (item.equals(")")) {
while (!s1.peek().equals("(")) {
//如果是右括号“)”,则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到左括号为止,此时将这一对括号丢弃
s2.add(s1.pop());
}
s1.pop(); // 将(弹出s1栈,消除小括号
} else {
// 当item的优先级小于等于栈顶运算符的优先级,将 s1 栈顶的运算符弹出并加入到 s2 中,再次转到(4.1)与 s1 中新的栈顶运算符相比较
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
// 还需要将item压入s1栈
s1.push(item);
}
}
// 将 s1 中剩余的运算符依次弹出并加入 s2 while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2; // 因为是存放到List,因此按顺序输出就是对应的后缀表达式所对应的List
}
// 将中缀表达式转成对应的中缀表达式List
public static List<String> toInfixExpressionList(String s) {
// 先定义一个List,存放中缀表达式对应的内容
ArrayList<String> ls = new ArrayList<String>();
int index = 0; // 这是一个指针,用于遍历中缀表达式字符串
String str; // 对多位数的拼接工作
char c; //每遍历到一个字符,就放入到c
do {
// 如果c是一个非数字,就需要加入到ls中
if ((c = s.charAt(index)) < 48 || (c = s.charAt(index)) > 57) {
ls.add("" + c);
index++; // index后移
} else {
// 如果是一个数,需要考虑多位数的问题
str = ""; // 先将str清空
while (index < s.length() && (c = s.charAt(index)) >= 48 && (c = s.charAt(index)) <= 57) { // 0[48]~9[57]
str += c; // 拼接
index++;
}
ls.add(str);
}
} while (index < s.length());
return ls;
}
// 编写一个类,返回一个运算对应的优先级
class Operation {
private static int ADD = 1;
private static int DEC = 1;
private static int MUL = 2;
private static int DIV = 2;
// 返回对应的优先级数字
public static int getValue(String operation) {
int result = 0;
switch (operation) {
case "+":
result = ADD;
break;
case "-":
result = DEC;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
System.out.println("不存在该运算符");
break;
}
return result;
}
}
测试:
String expression = "1+((2+3)*4)-5";
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List" + infixExpressionList);
List<String> parseSuffixExpression = parseSuffixExpression(infixExpressionList);
System.out.println("后缀表达式对应的List:" + parseSuffixExpression);
System.out.println("expression="+calculator(parseSuffixExpression));
逆波兰计算器完整版
完整版的逆波兰计算器,功能包括:
- 支持 + - * / ( )
- 多位数,支持小数, 3) 兼容处理, 过滤任何空白字符,包括空格、制表符、换页符
说明:逆波兰计算器完整版考虑的因素较多,下面给出完整版代码供同学们学习,其基本思路和前面一样,也是使用到:中缀表达式转后缀表达式。
代码实现:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
public class ReversePolishMultiCalc {
/**
* 匹配 + - * / ( ) 运算符
*/
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS = "-";
static final String TIMES = "*";
static final String DIVISION = "/";
/**
* 加減 + -
*/
static final int LEVEL_01 = 1;
/**
* 乘除 * /
*/
static final int LEVEL_02 = 2;
/**
* 括号
*/
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());
/**
* 去除所有空白符
*
* @param s
* @return
*/
public static String replaceAllBlank(String s) {
// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
return s.replaceAll("\\s+", "");
}
/**
* 判断是不是数字 int double long float
* @param s
* @return
*/
public static boolean isNumber(String s) {
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
/**
* 判断是不是运算符
*
* @param s
* @return
*/
public static boolean isSymbol(String s) {
return s.matches(SYMBOL);
}
/**
* 匹配运算等级
*
* @param s
* @return
*/
public static int calcLevel(String s) {
if ("+".equals(s) || "-".equals(s)) {
return LEVEL_01;
} else if ("*".equals(s) || "/".equals(s)) {
return LEVEL_02;
}
return LEVEL_HIGH;
}
/**
* 匹配
*
* @param s
* @throws Exception
*/
public static List<String> doMatch(String s) throws Exception {
if (s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if (!isNumber(s.charAt(0) + "")) throw new RuntimeException("data illeagle,start not with a number");
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if (isSymbol(s.charAt(i) + "")) {
each = s.charAt(i) + "";
//栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
if (stack.isEmpty() || LEFT.equals(each) || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)) {
stack.push(each);
} else if (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) {
//栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())) {
if (calcLevel(stack.peek()) == LEVEL_HIGH) {
break;
}
data.add(stack.pop());
}
stack.push(each);
} else if (RIGHT.equals(each)) {
// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())) {
if (LEVEL_HIGH == calcLevel(stack.peek())) {
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i; //前一个运算符的位置
} else if (i == s.length() - 1 || isSymbol(s.charAt(i + 1) + "")) {
each = start == 0 ? s.substring(start, i + 1) : s.substring(start + 1, i + 1);
if (isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个 stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出结果
*
* @param list
* @return
*/
public static Double doCalc(List<String> list) {
Double d = 0d;
if (list == null || list.isEmpty()) {
return null;
}
if (list.size() == 1) {
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if (isSymbol(list.get(i))) {
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i - 1);
list1.set(i - 2, d1 + "");
list1.addAll(list.subList(i + 1, list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 运算
*
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1, String s2, String symbol) {
Double result;
switch (symbol) {
case ADD:
result = Double.valueOf(s1) + Double.valueOf(s2);
break;
case MINUS:
result = Double.valueOf(s1) - Double.valueOf(s2);
break;
case TIMES:
result = Double.valueOf(s1) * Double.valueOf(s2);
break;
case DIVISION:
result = Double.valueOf(s1) / Double.valueOf(s2);
break;
default:
result = null;
}
return result;
}
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}
递归
递归应用场景
看个实际应用场景,迷宫问题(回溯), 递归(Recursion)

递归的概念
简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
递归调用机制
我列举两个小案例,来帮助大家理解递归,部分学员已经学习过递归了,这里在给大家回顾一下递归调用机制
- 打印问题
- 阶乘问题
- 使用图解方式说明了递归的调用机制

- 代码演示
public class RecuesionTest {
public static void main(String[] args) {
test(4);
}
public static void test(int n) {
if (n > 2) {
test(n - 1);
}
System.out.println("n=" + n);
}
}

public class RecuesionTest {
public static void main(String[] args) {
int result = factorial(4);
System.out.println(result); // 1*2*3*4=24
}
// 阶乘问题
public static int factorial(int n) {
if (n == 1) {
return 1;
} else {
return factorial(n - 1) * n;
}
}
}
递归能解决什么样的问题
- 各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
- 将用栈解决的问题-->递归代码比较简洁
递归需要遵守的重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响, 比如 n 变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError(栈溢出)
- 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
递归-迷宫问题

代码实现
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 = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
// 设置挡板
map[3][1] = 1;
map[3][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);
// 输出新的地图
System.out.println("========小球走过的地图========");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
/**
* 使用递归回溯来给小球找路
* 说明:
* 1. map 表示地图
* 2. i,j 表示从地图的哪个位置开始出发 (1,1)
* 3. 如果小球能到 map[6][5] 位置,则说明通路找到.
* 4. 约定: 当 map[i][j] 为 0 表示该点没有走过 当为 1 表示墙 ; 2 表示通路可以走 ; 3 表示该点已经走过,但是走不通
* 5. 在走迷宫时,需要确定一个策略(方法) 下->右->上->左 , 如果该点走不通,再回溯
*
* @param map 地图
* @param i 开始位置的行
* @param j 开始位置的列
* @return 如果是通路,即返回true,否则返回false
*/ public static boolean setWay(int[][] map, int i, int j) {
if (map[6][5] == 2) {
// 说明通路已经找到
return true;
} else {
if (map[i][j] == 0) {
// 说明当前这个点还没有走过,按照策略走:下->右->上->左
map[i][j] = 2; // 假定该点是可以走通的
if (setWay(map, i + 1, j)) {
// 先向下走
return true;
} else if (setWay(map, i, j + 1)) {
// 向右走
return true;
} else if (setWay(map, i - 1, j)) {
// 向上走
return true;
} else if (setWay(map, i, j - 1)) {
// 向左走
return true;
} else {
// 说明该点走不通
map[i][j] = 3;
return false;
}
} else {
// map[i][j]可能是1,2,3
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 {
// 说明该点走不通
map[i][j] = 3;
return false;
}
} else {
// map[i][j]可能是1,2,3
return false;
}
}
}

递归-八皇后问题(回溯算法)
八皇后问题介绍
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848 年提出:在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)。
八皇后问题算法思路分析
- 第一个皇后先放第一行第一列
- 第二个皇后放在第二行第一列、然后判断是否 OK, 如果不 OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适
- 继续第三个皇后,还是第一列、第二列……直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确解
- 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到.
- 然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4 的步骤
- 示意图:

说明:
理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] ={0 , 4, 7, 5, 2, 6, 1, 3} //对应 arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第 i+1 个皇后,放在第 i+1行的第 val+1 列
八皇后问题算法代码实现
public class Queue8 {
// 定义一个max表示有多少个皇后
int max = 8;
// 定义一个数组用于保存结果
int[] array = new int[max];
static int count = 0;
public static void main(String[] args) {
Queue8 queue8 = new Queue8();
queue8.check(0);
System.out.println("一共有" + count + "种解法");
}
// 放置第n个皇后
private void check(int n) {
if (n == max) {
// n = 8,其实8个皇后就已经放好了
print();
return;
}
// 依次放入皇后,并判断是否冲突
for (int i = 0; i < max; i++) {
// 先把当前这个皇后n,放到该行的第i列
array[n] = i;
// 判断是否冲突
if (judge(n)) {
// 不冲突,接着放第n+1个皇后,即开始递归
check(n + 1);
}
// 冲突,就继续执行array[n]=i,即将第n个皇后本行的下一个位置
}
}
/**
* 放置第n个皇后时,就去检测该皇后是否和前面已经摆放的皇后冲突
*
* @param n 表示放第n个皇后
* @return
*/ private boolean judge(int n) {
for (int i = 0; i < n; i++) {
// array[i] == array[n] 表示判断 第 n 个皇后是否和前面的 n-1 个皇后在同一列
// Math.abs(n-i) == Math.abs(array[n] - array[i]) 表示判断第 n 个皇后是否和第 i 皇后是否在同一斜
if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) {
return false;
}
}
return true; // 不冲突
}
// 写一个方法,将皇后摆放的位置输出
private void print() {
count++;
for (int i = 0; i < array.length; i++) {
System.out.print(" " + array[i]);
}
System.out.println();
}
}
排序算法
排序算法的介绍
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
排序的分类
- 内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
- 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
- 常见的排序算法分类:

算法的时间复杂度
度量一个程序(算法)执行时间的两种方法
- 事后统计的方法
这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。
- 事前估算的方法
通过分析某个算法的时间复杂度来判断哪个算法更优.
时间频度
基本介绍
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为 T(n)。
举例说明-基本案例
比如计算 1-100 所有数字之和, 我们设计两种算法:

举例说明-忽略常数项

结论:
- 2n+20 和 2n 随着 n 变大,执行曲线无限接近, 20 可以忽略
- 3n+10 和 3n 随着 n 变大,执行曲线无限接近, 10 可以忽略
举例说明-忽略低次项

结论:
- 2n^2+3n+10 和 2n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 3n+10
- n^2+5n+20 和 n^2 随着 n 变大,执行曲线无限接近, 可以忽略 5n+20
举例说明-忽略系数

结论:
- 随着 n 值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明这种情况下, 5 和 3 可以忽略。
- 而 n^3+5n 和 6n^3+4n ,执行曲线分离,说明多少次方式关键
时间复杂度
- 一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数,用 T(n)表示,若有某个辅助函数 f(n),使得当 n 趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称 f(n)是 T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
- T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同,但时间复杂度相同,都为 O(n²)。
- 计算时间复杂度的方法:
- 用常数 1 代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1
- 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²
- 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)
常见的时间复杂度
- 常数阶 O(1)
- 对数阶 O(log2n)
- 线性阶 O(n)
- 线性对数阶 O(nlog2n)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- k 次方阶 O(n^k)
- 指数阶 O(2^n)
常见的时间复杂度对应的图:

说明:
-
常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(n^k) <Ο(2^n) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低
-
从图中可见,我们应该尽可能避免使用指数阶的算法
- 常数阶 O(1)

- 对数阶 O(log2n)

- 线性阶 O(n)

- 线性对数阶 O(nlogN)

- 平方阶 O(n²)

- 立方阶 O(n³)、K 次方阶 O(n^k)
说明:参考上面的 O(n²) 去理解就好了,O(n³)相当于三层 n 循环,其它的类似
平均时间复杂度和最坏时间复杂度
- 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
- 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
- 平均时间复杂度和最坏时间复杂度是否一致,和算法有关(如图)。

算法的空间复杂度简介
基本介绍
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模 n 有关,它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元,例如快速排序和归并排序算法, 基数排序就属于这种情况
- 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间.
冒泡排序
基本介绍
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
优化:
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)
演示冒泡过程的例子(图解)

小结上面的图解过程:
- 一共进行 数组的大小-1 次 大的循环
- 每一趟排序的次数在逐渐的减少
- 如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序。这个就是优化
冒泡排序应用实例
我们举一个具体的案例来说明冒泡法。我们将五个无序的数:3, 9, -1, 10, -2 使用冒泡排序法将其排成一个从小到大的有序数列。
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 9, -1, 10, 20};
int temp = 0;
// 冒泡排序的时间复杂度为O(n^2)
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第" + (i + 1) + "次排序后的数组为:" + Arrays.toString(arr));
}
}
}

代码优化:
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 9, -1, 10, 20};
int temp = 0;
boolean flag = false; // 标识变量,表示是否进行过交换
// 冒泡排序的时间复杂度为O(n^2)
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) + "次排序后的数组为:" + Arrays.toString(arr));
if (!flag) {
// 在一趟排序中,一次交换也没有发生
break;
} else {
flag = false; // 重置flag,进行下次判断
}
}
}
}

将冒泡排序封装成一个方法:
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 9, -1, 10, 20};
System.out.println("排序前的数组为:" + Arrays.toString(arr));
System.out.println("========================================");
bubbleSort(arr);
System.out.println("========================================");
System.out.println("排序后的数组为:" + Arrays.toString(arr));
}
// 冒泡排序
public static void bubbleSort(int[] arr) {
int temp = 0;
boolean flag = false; // 标识变量,表示是否进行过交换
// 冒泡排序的时间复杂度为O(n^2)
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) + "次排序后的数组为:" + Arrays.toString(arr));
if (!flag) {
// 在一趟排序中,一次交换也没有发生
break;
} else {
flag = false; // 重置flag,进行下次判断
}
}
}
}
选择排序
基本介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
选择排序思想
选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从 arr[0]~arr[n-1]中选取最小值,与 arr[0]交换,第二次从 arr[1]~arr[n-1]中选取最小值,与 arr[1]交换,第三次从 arr[2]~arr[n-1]中选取最小值,与 arr[2]交换,…,第 i 次从 arr[i-1]~arr[n-1]中选取最小值,与 arr[i-1]交换,…, 第 n-1 次从 arr[n-2]~arr[n-1]中选取最小值,与 arr[n-2]交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。
选择排序思路分析图

对一个数组的选择排序再进行讲解

选择排序应用实例
有一群牛 , 颜值分别是 101, 34, 119, 1 请使用选择排序从低到高进行排序 [101, 34, 119, 1]
import java.util.Arrays;
public class SelectSort {
public static void main(String[] args) {
int[] arr = {101, 34, 119, 1};
selectSort(arr);
System.out.println("=========================================");
System.out.println("排序后的数组为:" + Arrays.toString(arr));
}
public static void selectSort(int[] arr) {
// 选择排序,时间复杂度为O(n^2)
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {
minIndex = j;
min = arr[j];
}
}
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
System.out.println("第" + (i + 1) + "轮排序后的数组为:" + Arrays.toString(arr));
}
}
}
插入排序
插入排序法介绍
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
插入排序法思想
插入排序(Insertion Sorting)的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
插入排序思路图

插入排序法应用实例
有一群小牛, 考试成绩分别是 101, 34, 119, 1 请从小到大排序
代码实现:
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = {101, 34, 119, 1};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
// 插入排序
public static void insertSort(int[] arr) {
int insertValue = 0;
int insertIndex = 0;
for (int i = 0; i < arr.length - 1; i++) {
insertValue = arr[i + 1];
insertIndex = i;
// 1. insertIndex >= 0 保证在给 insertVal 找插入位置,不越界
// 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置
// 3. 就需要将 arr[insertIndex] 后移
while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex]; // arr[insertIndex]后移
insertIndex--;
}
// 当退出 while 循环时,说明插入的位置找到, insertIndex + 1
arr[insertIndex + 1] = insertValue;
}
}
}
希尔排序
简单插入排序存在的问题
我们看简单的插入排序可能存在的问题. 数组 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}
结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
希尔排序法介绍
希尔排序是希尔(Donald Shell)于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
希尔排序法基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止.
希尔排序法的示意图

希尔排序法应用实例
有一群小牛, 考试成绩分别是 {8,9,1,7,2,3,5,4,6,0} 请从小到大排序. 请分别使用
- 希尔排序时, 对有序序列在插入时采用交换法, 并测试排序速度.
- 希尔排序时, 对有序序列在插入时采用移动法, 并测试排序速度
- 代码实现
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSort(arr);
System.out.println(Arrays.toString(arr));
System.out.println("==================================");
int[] arr2 = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSort2(arr2);
System.out.println(Arrays.toString(arr2));
}
// 希尔排序——交换法
public static void shellSort(int[] arr) {
int temp = 0;
// 将数组分为gap组
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素,步长为gap
for (int j = i - gap; j >= 0; j -= gap) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}
// 希尔排序——移动法
public static void shellSort2(int[] arr) {
// 将数组分为gap组
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// 从第gap个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
// 移动
arr[j] = arr[j - gap];
j -= gap;
}
// 退出循环,就给temp找到了插入的位置
arr[j] = temp;
}
}
}
}
}
快速排序
快速排序法介绍
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列.
快速排序法示意图


快速排序法应用实例
要求: 对 [-9,78,0,23,-567,70] 进行从小到大的排序,要求使用快速排序法。
说明[验证分析]:
- 如果取消左右递归,结果是 -9 -567 0 23 78 70
- 如果取消右递归,结果是 -567 -9 0 23 78 70
- 如果取消左递归,结果是 -9 -567 0 23 70 78
- 代码实现
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9, 78, 0, 23, -567, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
// 快速排序
public static void quickSort(int[] arr, int left, int right) {
int l = left; // 左下标
int r = right; // 右下标
// pivot 中轴
int pivot = arr[(left + right) / 2];
int temp = 0;
// while循环的目的是让比pivot值小的放到它的左边,比pivot值大的放到它的右边
while (l < r) {
// 在pivot的左边找大于等于pivot的值
while (arr[l] < pivot) {
l += 1;
}
// 在pivot的右边找小于等于pivot的值
while (arr[r] > pivot) {
r -= 1;
}
if (l >= r) {
// pivot的左右两边的值已经按照左边全部是小于等于pivot的,右边全部是大于等于pivot的值
break;
}
// 交换位置
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
// 如果发现arr[l] == pivot,就让r--,前移
if (arr[l] == pivot) {
r--;
}
// 如果发现arr[r] == pivot,就让l++,后移
if (arr[r] == pivot) {
l++;
}
}
// 如果 l == r,必须l++,r--,否则会出现栈溢出
if (l == r) {
l++;
r--;
}
// 向左递归
if (left < r) {
quickSort(arr, left, r);
}
// 向右递归
if (right > l) {
quickSort(arr, l, right);
}
}
}
归并排序
归并排序介绍
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
归并排序思想示意图 1-基本思想

归并排序思想示意图 2-合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤:

归并排序的应用实例
给你一个数组, val arr = Array(8, 4, 5, 7, 1, 3, 6, 2 ), 请使用归并排序完成排序。
代码演示:
import java.util.Arrays;
public class Mergeort {
public static void main(String[] args) {
int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
int[] temp = new int[arr.length]; // 归并排序需要一个额外的空间
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println(Arrays.toString(arr));
}
// 分+合的方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2; // 中间的索引
// 向左递归进行分解
mergeSort(arr, left, mid, temp);
// 向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
// 每分解一次就合并一次
merge(arr, left, mid, right, temp);
}
}
/**
* 合并的方法
*
* @param arr 排序的原始数组
* @param left 左边有序数列的初始索引
* @param mid 中间索引
* @param right 右边的索引
* @param temp 中转数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 初始化i,左边有序序列的初始索引
int j = mid + 1; // 初始化j,右边有序序列的初始索引
int t = 0; // 指向temp数组的当前索引
// 1.先把左右两边的数组按照规则填充到temp数组,直到左右两边的有序序列有一边处理完为止
while (i <= mid && j <= right) {
// 如果左边有序序列的当前元素小于等于右边有序数列的当前元素,就把左边的当前元素拷贝到temp中
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t++;
i++;
} else {
// 反之,把右边的当前元素拷贝到temp中
temp[t] = arr[j];
t++;
j++;
}
}
// 2.把有剩余数据的一边的数据依次全部填充到temp去
while (i <= mid) {
// 左边的有序序列还有剩余的元素,就按顺序全部填充到temp中
temp[t] = arr[i];
t++;
i++;
}
while (j <= right) {
// 右边的有序序列还有剩余的元素,就按顺序全部填充到temp中
temp[t] = arr[j];
t++;
j++;
}
// 3.将temp数组的元素拷贝到arr,并不是每次都拷贝8个
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
}
基数排序
基数排序(桶排序)介绍
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
- 基数排序(Radix Sort)是桶排序的扩展
- 基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
基数排序基本思想
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
- 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤
基数排序图文说明
将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序


基数排序代码实现
要求:将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序
- 思路分析:前面的图文已经讲明确
- 代码实现
public static void redixSort(int[] arr) {
// 得到数组中最大的数的位数
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
// 得到最大数是几位数
int maxLength = (max + "").length();
int[][] bucket = new int[10][arr.length];
int[] bucketElementCount = new int[10];
int index;
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
for (int j = 0; j < arr.length; j++) {
// 取出每个元素对应位的值
int digitOfElement = arr[j] / n % 10;
// 放入到对应的桶中
bucket[digitOfElement][bucketElementCount[digitOfElement]] = arr[j];
bucketElementCount[digitOfElement]++;
}
// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
index = 0;
// 遍历每一个桶,并将桶中的数据放入原数组
for (int k = 0; k < bucket.length; k++) {
// 如果桶中有数据,才放入原数组
if (bucketElementCount[k] != 0) {
// 循环该桶,即第i个桶,即第i个一维数组,放入
for (int l = 0; l < bucketElementCount[k]; l++) {
// 取出元素放入arr中
arr[index] = bucket[k][l];
index++;
}
}
// 处理后,需要将bucketElementCount[i] = 0;
bucketElementCount[k] = 0;
}
System.out.println("第" + (i + 1) + "轮处理后的数组为:" + Arrays.toString(arr));
}
}
基数排序的说明
- 基数排序是对传统桶排序的扩展,速度很快.
- 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
- 基数排序时稳定的。【注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i]在 r[j]之前,而在排序后的序列中,r[i]仍在 r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的】
- 有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: https://code.i-harness.com/zh-CN/q/e98fa9
常用排序算法总结和对比
一张排序算法的比较图

相关术语解释
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
- 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
- n: 数据规模
- k: “桶”的个数
- In-place: 不占用额外内存
- Out-place: 占用额外内存
查找算法
查找算法介绍
在 java 中,我们常用的查找有四种:
- 顺序(线性)查找
- 二分查找/折半查找
- 插值查找
- 斐波那契查找
线性查找算法
有一个数列: {1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称【顺序查找】 要求: 如果找到了,就提示找到,并给出下标值。
代码实现:
public class SeqSeearch {
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
int index = seqSearch(arr, 1);
if (index == -1) {
System.out.println("没有查找到");
} else {
System.out.println("找到了,下标为:" + index);
}
}
/**
* 找到一个满足条件的值就返回
*
* @param arr
* @param target
* @return
*/ public static int seqSearch(int[] arr, int target) {
// 线性查找就是逐一比对,发现有相同值,就返回下标
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
}
二分查找算法
二分查找
请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
/**
* 二分查找
* 前提:该数组是有序的
*/
public class BinarySearch {
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
int index = binarySearch(arr, 0, arr.length - 1, 10);
if (index == -1) {
System.out.println("没有查找到");
} else {
System.out.println("找到了,下标为:" + index);
}
}
/**
* @param arr 数组
* @param left 左边的索引
* @param right 右边的索引
* @param target 目标值
* @return 如果找到就返回下标,如果没有找到就返回-1
*/ public static int binarySearch(int[] arr, int left, int right, int target) {
int mid = (left + right) / 2;
int midValue = arr[mid];
// 当 left > right 时,说明递归整个数组,但是没有找到
if (left > right) {
return -1;
}
if (target > midValue) {
// 向右递归
return binarySearch(arr, mid + 1, right, target);
} else if (target < midValue) {
// 向左递归
return binarySearch(arr, left, mid - 1, target);
} else {
return mid;
}
}
}
二分查找算法的思路

二分查找的代码
说明:增加了找到所有的满足条件的元素下标:
课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000.
/**
* 返回所有符合条件的元素的下标
* 思路分析
* 1. 在找到 mid 索引值,不要马上返回
* 2. 向 mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
* 3. 向 mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
* 4. 将 Arraylist 返回
*
* @param arr
* @param left
* @param right
* @param target
* @return
*/public static List<Integer> binarySearch2(int[] arr, int left, int right, int target) {
int mid = (left + right) / 2;
int midValue = arr[mid];
// 当 left > right 时,说明递归整个数组,但是没有找到
if (left > right) {
return new ArrayList<Integer>();
}
if (target > midValue) {
// 向右递归
return binarySearch2(arr, mid + 1, right, target);
} else if (target < midValue) {
// 向左递归
return binarySearch2(arr, left, mid - 1, target);
} else {
List<Integer> list = new ArrayList<Integer>();
int temp = mid - 1;
while (true) {
if (temp < 0 || arr[temp] != target) {
// 退出
break;
}
// 否则,就把temp放入list中
list.add(temp);
temp--; // temp左移
}
list.add(mid);
temp = mid + 1;
while (true) {
if (temp > arr.length - 1 || arr[temp] != target) {
// 退出
break;
}
// 否则,就把temp放入list中
list.add(temp);
temp++; // temp右移
}
return list;
}
}
插值查找算法
- 插值查找原理介绍:
插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。 - 将折半查找中的求 mid 索引的公式 , low 表示左边索引 left, high 表示右边索引 right. key 就是前面我们讲的 target

int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;/*插值索引*/
对应前面的代码公式:
int mid = left + (right – left) * (target – arr[left]) / (arr[right] – arr[left])- 举例说明插值查找算法 1-100 的数组

插值查找应用案例
请对一个有序数组进行插值查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
代码实现:
/**
* 插值查找
* 也要求数组是有序的
*/
public class InsertSearch {
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
int index = insertSearch(arr, 0, arr.length - 1, 1000);
if (index == -1) {
System.out.println("没有查找到");
} else {
System.out.println("找到了,下标为:" + index);
}
}
public static int insertSearch(int[] arr, int left, int right, int target) {
if (left > right || target < arr[0] || target > arr[arr.length - 1]) {
return -1;
}
int mid = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left]);
int midValue = arr[mid];
if (target > midValue) {
// 向右递归
return insertSearch(arr, mid + 1, right, target);
} else if (target < midValue) {
// 向左递归
return insertSearch(arr, left, mid - 1, target);
} else {
return mid;
}
}
}
插值查找注意事项
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
斐波那契(黄金分割法)查找算法
斐波那契(黄金分割法)查找基本介绍
- 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是 0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
- 斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 发现斐波那契数列的两个相邻数的比例,无限接近黄金分割值0.618
斐波那契(黄金分割法)原理
斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid 不再是中间或插值得到,而是位于黄金分割点附近,即 mid=low+F(k-1)-1(F 代表斐波那契数列),如下图所示:

对 F(k-1)-1 的理解:
- 由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。该式说明:只要顺序表的长度为 F[k]-1,则可以将该表分成长度为 F[k-1]-1 和 F[k-2]-1 的两段,即如上图所示。从而中间位置为 mid=low+F(k-1)-1
- 类似的,每一子段也可以用相同的方式分割
- 但顺序表长度 n 不一定刚好等于 F[k]-1,所以需要将原来的顺序表长度 n 增加至 F[k]-1。这里的 k 值只要能使得 F[k]-1 恰好大于或等于 n 即可,由以下代码得到,顺序表长度增加后,新增的位置(从 n+1 到 F[k]-1 位置),都赋为 n 位置的值即可。
while(n > fib(k) - 1) k++;
斐波那契查找应用案例
请对一个有序数组进行斐波那契查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
代码实现:
import java.util.Arrays;
/**
* 斐波那契查找——非递归方式
*/
public class FibonacciSearch {
public static int maxSize = 20;
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
int index = fibonacciSearch(arr, 1234);
if (index == -1) {
System.out.println("没有查找到");
} else {
System.out.println("找到了,下标为:" + index);
}
}
/**
* @param arr 数组
* @param target 需要查找的关键码(值)
* @return 返回对应的下标,如果没有就返回-1
*/ public static int fibonacciSearch(int[] arr, int target) {
int low = 0;
int high = arr.length - 1;
int k = 0; // 表示斐波那契分割数值的下标
int mid; // 存放mid值
int[] f = fib(); // 获取斐波那契数列
// 获取k
while (high > f[k] - 1) {
k++;
}
// 因为f[k]的值可能大于arr的长度,因此需要构造一个新的数组,并指向arr
// 不足的部分会使用0填充
int[] temp = Arrays.copyOf(arr, f[k]);
// 实际上需要使用arr数组最后的数填充temp
//举例:
//temp = {1,8, 10, 89, 1000, 1234, 0, 0} -> {1,8, 10, 89, 1000, 1234, 1234}
for (int i = high + 1; i < temp.length; i++) {
temp[i] = arr[high];
}
// 使用while来循环处理,找到target
while (low <= high) {
// 只要满足条件,就一直找
mid = low + f[k - 1] - 1;
if (target < temp[mid]) {
// 向数组的前面查找(左边)
high = mid - 1;
//为甚是 k--
//说明
//1. 全部元素 = 前面的元素 + 后边元素
//2. f[k] = f[k-1] + f[k-2]
//因为 前面有 k-1个元素,所以可以继续拆分 f[k-1] = f[k-2] + f[k-3]
//即 从 f[k-1] 往前面继续查找,所以 k--
//即下次循环 mid = f[k-1-1]-1
k--;
} else if (target > temp[mid]) {
// 向数组的后面查找(右边)
low = mid + 1;
//为什么是 k -=2
//说明
//1. 全部元素 = 前面的元素 + 后边元素
//2. f[k] = f[k-1] + f[k-2]
//3. 因为后面我们有 k-2 个元素,所以可以继续拆分 f[k-2] = f[k-3] + f[k-4]
//4. 即从 f[k-2] 往前面进行查找,所以 k -=2
//5. 即下次循环 mid = f[k - 1 - 2] - 1
k -= 2;
} else {
// 找到了,需要确定返回的是哪个下标
if (mid <= high) {
return mid;
} else {
return high;
}
}
}
return -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;
}
}
哈希表
哈希表(散列)-Google 上机题
- 看一个实际需求,google 公司的一个上机题:
- 有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工的 id 时,要求查找到该员工的 所有信息. 3) 要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
哈希表的基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。


google 公司的一个上机题
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,名字,住址..),当输入该员工的 id 时, 要求查找到该员工的 所有信息. 要求:
- 不使用数据库,,速度越快越好=>哈希表(散列)
- 添加时,保证按照 id 从低到高插入
- 使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
- 思路分析并画出示意图

- 代码实现
import java.util.Scanner;
public class HashTableDemo {
public static void main(String[] args) {
// 创建哈希表
HashTable hashTable = new HashTable(7);
// 写一个简单菜单
String key;
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("==========菜单===============");
System.out.println("add:添加雇员");
System.out.println("list:显示雇员");
System.out.println("find:根据id查找雇员");
System.out.println("exit:退出系统");
System.out.println("=============================");
System.out.println("请输入命令:");
key = scanner.next();
switch (key) {
case "add":
System.out.println("请输入员工id:");
int id = scanner.nextInt();
System.out.println("请输入员工的姓名:");
String name = scanner.next();
// 创建雇员
Employee employee = new Employee(id, name);
hashTable.add(employee);
System.out.println("添加成功!");
break;
case "list":
System.out.println("=======查询结果如下=========");
hashTable.list();
break;
case "exit":
scanner.close();
System.out.println("===========退出系统,谢谢使用!============");
System.exit(0);
break;
case "find":
System.out.println("请输入要查找的雇员的id:");
int id2 = scanner.nextInt();
System.out.println("=======查询结果如下=========");
hashTable.findEmployeeById(id2);
break;
default:
break;
}
}
}
}
// 哈希表
class HashTable {
private EmployeeLinkedList[] employeeLinkedListArray;
private int size; // 表示共有多少条链表
// 构造器
public HashTable(int size) {
this.size = size;
// 初始化employeeLinkedListArray
employeeLinkedListArray = new EmployeeLinkedList[size];
// 分别初始化每一个链表
for (int i = 0; i < size; i++) {
employeeLinkedListArray[i] = new EmployeeLinkedList();
}
}
// 根据输入的id查找雇员
public void findEmployeeById(int id) {
// 使用散列函数,确定到哪条链表查找
int employeeLinkedListId = hashFun(id);
Employee employee = employeeLinkedListArray[employeeLinkedListId].getById(id);
if (employee != null) {
// 找到
System.out.println("在第" + (employeeLinkedListId + 1) + "条连链表中找到该雇员,id为:" + id);
} else {
System.out.println("在哈希表中没有找到该雇员");
}
}
// 添加雇员
public void add(Employee employee) {
// 根据员工的id得到该员工应当添加到哪条链表
int employeeLinkedListId = hashFun(employee.id);
// 将employee添加到对应的链表中
employeeLinkedListArray[employeeLinkedListId].add(employee);
}
// 遍历所有的链表,即遍历哈希表
public void list() {
for (int i = 0; i < size; i++) {
employeeLinkedListArray[i].list(i);
}
}
// 散列函数——取模法
public int hashFun(int id) {
return id % size;
}
}
// 链表
class EmployeeLinkedList {
// 头指针,指向第一个雇员
private Employee head; // 默认为空
// 添加雇员到链表
public void add(Employee employee) {
// 如果是添加第一个雇员
if (head == null) {
head = employee;
return;
}
// 如果不是添加第一个雇员,则使用一个辅助指针帮助定位最后
Employee currentEmployee = head;
while (true) {
if (currentEmployee.next == null) {
// 说明到链表最后
break;
}
currentEmployee = currentEmployee.next; // 后移
}
// 退出时直接将employee加入到链表最后即可
currentEmployee.next = employee;
}
// 遍历链表的雇员信息
public void list(int id) {
if (head == null) {
// 说明链表为空
System.out.println("第" + (id + 1) + "条链表为空");
return;
}
Employee currentEmployee = head;
System.out.print("链表" + (id + 1) + ":");
while (true) {
System.out.print(" => id = " + currentEmployee.id + ", name = " + currentEmployee.name);
if (currentEmployee.next == null) {
// 说明currentEmployee已经是最后节点
break;
}
currentEmployee = currentEmployee.next; // 后移,遍历
}
System.out.println();
}
// 根据id查找雇员
public Employee getById(int id) {
// 判断链表是否为空
if (head == null) {
System.out.println("链表为空");
return null;
}
// 辅助指针
Employee currentEmployee = head;
while (true) {
if (currentEmployee.id == id) {
// 找到
break;
}
// 退出
if (currentEmployee.next == null) {
// 说明遍历当前链表没有找到该雇员
currentEmployee = null;
break;
}
currentEmployee = currentEmployee.next; // 后移
}
return currentEmployee;
}
}
// 创建雇员类
class Employee {
public int id;
public String name;
public Employee next; // next默认为空
public Employee(int id, String name) {
super();
this.id = id;
this.name = name;
}
}
树结构的基础部分
二叉树
为什么需要树这种数据结构
- 数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低 [示意图]
画出操作示意图:

- 链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
操作示意图:

- 树存储方式的分析
能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
案例: [7, 3, 10, 1, 5, 9, 12]

树示意图

树的常用术语(结合示意图理解):
- 节点
- 根节点
- 父节点
- 子节点
- 叶子节点 (没有子节点的节点)
- 节点的权(节点值)
- 路径(从 root 节点找到该节点的路线)
- 层
- 子树
- 树的高度(最大层数)
- 森林 :多颗子树构成森林
二叉树的概念
- 树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。
- 二叉树的子节点分为左节点和右节点
- 示意图

- 如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树。

- 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树

二叉树遍历的说明
使用前序,中序和后序对下面的二叉树进行遍历.
- 前序遍历: 先输出父节点,再遍历左子树和右子树
- 中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
- 小结: 看输出父节点的顺序,就确定是前序,中序还是后序
二叉树遍历应用实例(前序,中序,后序)
- 应用实例的说明和思路

- 代码实现
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, "林冲");
binaryTree.setRoot(root);
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
System.out.println("========前序遍历结果========");
binaryTree.preOrder(); // 1, 2, 3, 4
System.out.println("===========================");
System.out.println("========中序遍历结果========");
binaryTree.infixOrder(); // 2, 1, 3, 4
System.out.println("===========================");
System.out.println("========后序遍历结果========");
binaryTree.postOrder(); // 2, 4, 3, 1
}
}
// 定义二叉树
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
}
// 先创建HeroNode节点
class HeroNode {
private int id;
private String name;
private HeroNode left;
private HeroNode right;
public HeroNode(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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{" +
"id=" + id +
", 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);
}
}
二叉树-查找指定节点
要求
- 请编写前序查找,中序查找和后序查找的方法。
- 并分别使用三种查找方式,查找 heroNO = 5 的节点
- 并分析各种查找方式,分别比较了多少次
- 思路分析图解

- 代码实现
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, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
binaryTree.setRoot(root);
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
System.out.println("========前序查找查找结果========");
HeroNode result = binaryTree.preOederSearch(5); // 前序查找比较4次
if (result != null) {
System.out.println("找到了,信息为:id=" + result.getId() + ",name=" + result.getName());
} else {
System.out.println("没找到");
}
System.out.println();
System.out.println("========中序查找查找结果========");
result = binaryTree.infixOrderSearch(5); // 中序查找比较3次
if (result != null) {
System.out.println("找到了,信息为:id=" + result.getId() + ",name=" + result.getName());
} else {
System.out.println("没找到");
}
System.out.println();
System.out.println("========后序查找查找结果========");
result = binaryTree.postOrderSearch(5); // 后序查找比较2次
if (result != null) {
System.out.println("找到了,信息为:id=" + result.getId() + ",name=" + result.getName());
} else {
System.out.println("没找到");
}
}
}
// 定义二叉树
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 前序遍历查找
public HeroNode preOederSearch(int id) {
if (root != null) {
return root.preOrderSearch(id);
} else {
return null;
}
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
if (root != null) {
return root.infixOrderSearch(id);
} else {
return null;
}
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
if (root != null) {
return root.postOrderSearch(id);
} else {
return null;
}
}
}
// 先创建HeroNode节点
class HeroNode {
private int id;
private String name;
private HeroNode left;
private HeroNode right;
public HeroNode(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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{" +
"id=" + id +
", 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);
}
// 前序遍历查找
public HeroNode preOrderSearch(int id) {
// 先比较当前节点是不是
if (this.id == id) {
return this;
}
//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到结点,则返回
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.preOrderSearch(id);
}
if (resNode != null) {
//找到
return resNode;
}
//1.左递归前序查找,找到结点,则返回,否继续判断,
//2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
if (this.right != null) {
resNode = this.right.preOrderSearch(id);
}
return resNode;
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.infixOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
if (this.id == id) {
return this;
}
//否则继续进行右递归的中序查找
if (this.right != null) {
resNode = this.right.infixOrderSearch(id);
}
return resNode;
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左子树没有找到,则向右子树递归进行后序遍历查找
if (this.right != null) {
resNode = this.right.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左右子树都没有找到,就比较当前结点是不是
if (this.id == id) {
return this;
}
return resNode;
}
}
二叉树-删除节点
- 要求
- 如果删除的节点是叶子节点,则删除该节点
- 如果删除的节点是非叶子节点,则删除该子树.
- 测试,删除掉 5 号叶子节点 和 3 号子树.
- 完成删除思路分析

- 代码实现
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, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
binaryTree.setRoot(root);
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.deleteNode(5);
binaryTree.deleteNode(3);
binaryTree.preOrder();
}
}
// 定义二叉树
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 前序遍历查找
public HeroNode preOederSearch(int id) {
if (root != null) {
return root.preOrderSearch(id);
} else {
return null;
}
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
if (root != null) {
return root.infixOrderSearch(id);
} else {
return null;
}
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
if (root != null) {
return root.postOrderSearch(id);
} else {
return null;
}
}
// 删除节点
public void deleteNode(int id) {
if (root != null) {
if (root.getId() == id) {
root = null;
} else {
// 进行递归删除
root.deleteNode(id);
}
} else {
System.out.println("空树,不能删除");
}
}
}
// 先创建HeroNode节点
class HeroNode {
private int id;
private String name;
private HeroNode left;
private HeroNode right;
public HeroNode(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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{" +
"id=" + id +
", 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);
}
// 前序遍历查找
public HeroNode preOrderSearch(int id) {
// 先比较当前节点是不是
if (this.id == id) {
return this;
}
//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到结点,则返回
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.preOrderSearch(id);
}
if (resNode != null) {
//找到
return resNode;
}
//1.左递归前序查找,找到结点,则返回,否继续判断,
//2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
if (this.right != null) {
resNode = this.right.preOrderSearch(id);
}
return resNode;
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.infixOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
if (this.id == id) {
return this;
}
//否则继续进行右递归的中序查找
if (this.right != null) {
resNode = this.right.infixOrderSearch(id);
}
return resNode;
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左子树没有找到,则向右子树递归进行后序遍历查找
if (this.right != null) {
resNode = this.right.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左右子树都没有找到,就比较当前结点是不是
if (this.id == id) {
return this;
}
return resNode;
}
// 递归删除节点
public void deleteNode(int id) {
if (this.left != null && this.left.id == id) {
this.left = null;
return;
}
if (this.right != null && this.right.id == id) {
this.right = null;
return;
}
// 向左子树进行递归删除
if (this.left != null) {
this.left.deleteNode(id);
}
// 向右子树进行递归删除
if (this.right != null) {
this.right.deleteNode(id);
}
}
}
顺序存储二叉树
顺序存储二叉树的概念
- 基本说明
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,看右面的示意图。

- 要求:
- 右图的二叉树的结点,要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 6]
- 要求在遍历数组 arr 时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历
- 顺序存储二叉树的特点:
- 顺序二叉树通常只考虑完全二叉树
- 第 n 个元素的左子节点为 2 * n + 1
- 第 n 个元素的右子节点为 2 * n + 2
- 第 n 个元素的父节点为 (n-1) / 2
- n : 表示二叉树中的第几个元素(按 0 开始编号如图所示)
顺序存储二叉树遍历
需求: 给你一个数组 {1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。 前序遍历的结果应当为1,2,4,5,3,6,7
代码实现:
/**
* 顺序存储二叉树
*/
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7};
ArrarBinaryTree arrarBinaryTree = new ArrarBinaryTree(arr);
arrarBinaryTree.preOrder(); // 1,2,4,5,3,6,7
}
}
// 编写一个ArrayBinaryTree实现顺序存储二叉树遍历
class ArrarBinaryTree {
private int[] arr; // 存储数据节点的数组
public ArrarBinaryTree(int[] arr) {
this.arr = arr;
}
// 重载preOrder
public void preOrder() {
this.preOrder(0);
}
/**
* 编写一个方法,完成顺序存储二叉树的前序遍历
*
* @param index 数组的下标
*/
public void preOrder(int index) {
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能按照二叉树的前序遍历");
}
// 输出当前这个元素
System.out.println(arr[index]);
// 向左递归遍历
if ((index * 2 + 1) < arr.length) {
preOrder(index * 2 + 1);
}
// 向右递归遍历
if ((index * 2 + 2) < arr.length) {
preOrder(index * 2 + 2);
}
}
}
线索化二叉树
先看一个问题
将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树. n+1=7

问题分析:
- 当我们对上面的二叉树进行中序遍历时,数列为
- 但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上.
- 如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
- 解决方案-线索二叉树
线索二叉树基本介绍
- n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
- 一个结点的前一个结点,称为前驱结点
- 一个结点的后一个结点,称为后继结点
线索二叉树应用案例
应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}

思路分析: 中序遍历的结果:{8, 3, 10, 1, 14, 6}

- 说明: 当线索化二叉树后,Node 节点的 属性 left 和 right ,有如下情况:
- left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点.
- right 指向的是右子树,也可能是指向后继节点,比如 ① 节点 right 指向的是右子树,而⑩ 节点的 right 指向的是后继节点.
- 代码实现:
/**
* 中序线索二叉树
*/
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "tom");
HeroNode node2 = new HeroNode(3, "jack");
HeroNode node3 = new HeroNode(6, "smith");
HeroNode node4 = new HeroNode(8, "marry");
HeroNode node5 = new HeroNode(10, "king");
HeroNode node6 = new HeroNode(14, "tim");
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.setRoot(root);
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
// 测试中序线索化
threadedBinaryTree.threadedNode();
// 测试:以10号节点测试
HeroNode left = node5.getLeft();
System.out.println("10号节点的前驱节点是:" + left);
HeroNode right = node5.getRight();
System.out.println("10号节点的后继节点是:" + right);
}
}
// 定义线索化二叉树
class ThreadedBinaryTree {
private HeroNode root;
// 为了实现线索化,需要创建一个指向当前节点的前驱节点的指针
// 在线索化时,pre总是保留前一个结点
private HeroNode pre = null;
public void setRoot(HeroNode root) {
this.root = root;
}
// 重载线索化的方法
public void threadedNode() {
this.threadedNode(root);
}
/**
* 编写对二叉树进行中序线索化的方法
*
* @param node 需要线索化的节点
*/
public void threadedNode(HeroNode node) {
if (node == null) {
return;
}
// 先线索化左子树
threadedNode(node.getLeft());
// 线索化当前节点
// 先处理当前节点的前驱节点
if (node.getLeft() == null) {
// 让当前节点的做指针,指向前驱节点
node.setLeft(pre);
// 修改当前节点的做指针的类型
node.setLeftType(1);
}
// 处理当前节点的后继节点
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightType(1);
}
// 每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
// 线索化右子树
threadedNode(node.getRight());
}
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 前序遍历查找
public HeroNode preOederSearch(int id) {
if (root != null) {
return root.preOrderSearch(id);
} else {
return null;
}
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
if (root != null) {
return root.infixOrderSearch(id);
} else {
return null;
}
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
if (root != null) {
return root.postOrderSearch(id);
} else {
return null;
}
}
// 删除节点
public void deleteNode(int id) {
if (root != null) {
if (root.getId() == id) {
root = null;
} else {
// 进行递归删除
root.deleteNode(id);
}
} else {
System.out.println("空树,不能删除");
}
}
}
// 先创建HeroNode节点
class HeroNode {
private int id;
private String name;
private HeroNode left;
private HeroNode right;
private int leftType; // 0——左子树 1——前驱节点
private int rightType; // 0——右子树 1——后继节点
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
public HeroNode(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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{" +
"id=" + id +
", 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);
}
// 前序遍历查找
public HeroNode preOrderSearch(int id) {
// 先比较当前节点是不是
if (this.id == id) {
return this;
}
//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到结点,则返回
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.preOrderSearch(id);
}
if (resNode != null) {
//找到
return resNode;
}
//1.左递归前序查找,找到结点,则返回,否继续判断,
//2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
if (this.right != null) {
resNode = this.right.preOrderSearch(id);
}
return resNode;
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.infixOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
if (this.id == id) {
return this;
}
//否则继续进行右递归的中序查找
if (this.right != null) {
resNode = this.right.infixOrderSearch(id);
}
return resNode;
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左子树没有找到,则向右子树递归进行后序遍历查找
if (this.right != null) {
resNode = this.right.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左右子树都没有找到,就比较当前结点是不是
if (this.id == id) {
return this;
}
return resNode;
}
// 递归删除节点
public void deleteNode(int id) {
if (this.left != null && this.left.id == id) {
this.left = null;
return;
}
if (this.right != null && this.right.id == id) {
this.right = null;
return;
}
// 向左子树进行递归删除
if (this.left != null) {
this.left.deleteNode(id);
}
// 向右子树进行递归删除
if (this.right != null) {
this.right.deleteNode(id);
}
}
}
遍历线索化二叉树
- 说明:对前面的中序线索化的二叉树, 进行遍历
- 分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次序应当和中序遍历保持一致。
- 代码:
/**
* 中序线索二叉树
*/
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "tom");
HeroNode node2 = new HeroNode(3, "jack");
HeroNode node3 = new HeroNode(6, "smith");
HeroNode node4 = new HeroNode(8, "marry");
HeroNode node5 = new HeroNode(10, "king");
HeroNode node6 = new HeroNode(14, "tim");
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.setRoot(root);
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
// 测试中序线索化
threadedBinaryTree.threadedNode();
// 测试:以10号节点测试
HeroNode left = node5.getLeft();
System.out.println("10号节点的前驱节点是:" + left);
HeroNode right = node5.getRight();
System.out.println("10号节点的后继节点是:" + right);
System.out.println("使用线索化的方式遍历线索化二叉树");
threadedBinaryTree.threadedList(); // 8, 3, 10, 1, 14, 6
}
}
// 定义线索化二叉树
class ThreadedBinaryTree {
private HeroNode root;
// 为了实现线索化,需要创建一个指向当前节点的前驱节点的指针
// 在线索化时,pre总是保留前一个结点
private HeroNode pre = null;
public void setRoot(HeroNode root) {
this.root = root;
}
// 重载线索化的方法
public void threadedNode() {
this.threadedNode(root);
}
// 遍历线索化二叉树的方法
public void threadedList() {
// 定义一个变量,存储当前遍历的节点,从root开始
HeroNode node = root;
while (node != null) {
// 循环找到leftType为1的节点,第一个找到的就是8节点,后面随着遍历而变化,因为当leftType为1时,说明该节点时按照线索化处理后的节点
while (node.getLeftType() == 0) {
node = node.getLeft();
}
// 打印当前节点
System.out.println(node);
// 如果当前节点的右指针指向了后继节点,就一直输出
while (node.getRightType() == 1) {
// 获取当前节点的后继节点
node = node.getRight();
System.out.println(node);
}
// 替换遍历的节点
node = node.getRight();
}
}
/**
* 编写对二叉树进行中序线索化的方法
*
* @param node 需要线索化的节点
*/
public void threadedNode(HeroNode node) {
if (node == null) {
return;
}
// 先线索化左子树
threadedNode(node.getLeft());
// 线索化当前节点
// 先处理当前节点的前驱节点
if (node.getLeft() == null) {
// 让当前节点的做指针,指向前驱节点
node.setLeft(pre);
// 修改当前节点的做指针的类型
node.setLeftType(1);
}
// 处理当前节点的后继节点
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightType(1);
}
// 每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
// 线索化右子树
threadedNode(node.getRight());
}
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 前序遍历查找
public HeroNode preOederSearch(int id) {
if (root != null) {
return root.preOrderSearch(id);
} else {
return null;
}
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
if (root != null) {
return root.infixOrderSearch(id);
} else {
return null;
}
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
if (root != null) {
return root.postOrderSearch(id);
} else {
return null;
}
}
// 删除节点
public void deleteNode(int id) {
if (root != null) {
if (root.getId() == id) {
root = null;
} else {
// 进行递归删除
root.deleteNode(id);
}
} else {
System.out.println("空树,不能删除");
}
}
}
// 先创建HeroNode节点
class HeroNode {
private int id;
private String name;
private HeroNode left;
private HeroNode right;
private int leftType; // 0——左子树 1——前驱节点
private int rightType; // 0——右子树 1——后继节点
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
public HeroNode(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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{" +
"id=" + id +
", 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);
}
// 前序遍历查找
public HeroNode preOrderSearch(int id) {
// 先比较当前节点是不是
if (this.id == id) {
return this;
}
//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到结点,则返回
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.preOrderSearch(id);
}
if (resNode != null) {
//找到
return resNode;
}
//1.左递归前序查找,找到结点,则返回,否继续判断,
//2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
if (this.right != null) {
resNode = this.right.preOrderSearch(id);
}
return resNode;
}
// 中序遍历查找
public HeroNode infixOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.infixOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
if (this.id == id) {
return this;
}
//否则继续进行右递归的中序查找
if (this.right != null) {
resNode = this.right.infixOrderSearch(id);
}
return resNode;
}
// 后序遍历查找
public HeroNode postOrderSearch(int id) {
//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左子树没有找到,则向右子树递归进行后序遍历查找
if (this.right != null) {
resNode = this.right.postOrderSearch(id);
}
if (resNode != null) {
return resNode;
}
//如果左右子树都没有找到,就比较当前结点是不是
if (this.id == id) {
return this;
}
return resNode;
}
// 递归删除节点
public void deleteNode(int id) {
if (this.left != null && this.left.id == id) {
this.left = null;
return; }
if (this.right != null && this.right.id == id) {
this.right = null;
return; }
// 向左子树进行递归删除
if (this.left != null) {
this.left.deleteNode(id);
}
// 向右子树进行递归删除
if (this.right != null) {
this.right.deleteNode(id);
}
}
}
树结构实际应用
堆排序
堆排序基本介绍
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
- 大顶堆举例说明

- 小顶堆举例说明

- 一般升序采用大顶堆,降序采用小顶堆
堆排序基本思想
堆排序的基本思想是:
- 将待排序序列构造成一个大顶堆
- 此时,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.
堆排序步骤图解说明
要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
原始的数组 [4, 6, 8, 5, 9]
- .假设给定无序序列结构如下

- .此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,也就是下面的 6 结点),从左至右,从下至上进行调整。

- .找到第二个非叶节点 4,由于[4,9,8]中 9 元素最大,4 和 9 交换。

- 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。

此时,我们就将一个无序序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
- .将堆顶元素 9 和末尾元素 4 进行交换

- .重新调整结构,使其继续满足堆定义

- .再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8.

- 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

再简单总结下堆排序的基本思路:
- 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
堆排序代码实现
要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
说明:
- 堆排序不是很好理解,老师通过 Debug 帮助大家理解堆排序
- 堆排序的速度非常快,在我的机器上 8 百万数据 3 秒左右。O(nlogn)
- 代码实现
import java.util.Arrays;
/**
* 堆排序
*/
public class HeapSort {
public static void main(String[] args) {
// 要求将数组进行升序排列
int[] arr = {4, 6, 8, 5, 9};
// 分步完成
adjustHeap(arr, 1, arr.length);
System.out.println("第一次调整后的数组:" + Arrays.toString(arr)); // 4, 9, 8, 5, 6
adjustHeap(arr, 0, arr.length);
System.out.println("第二次调整后的数组:" + Arrays.toString(arr)); // 9, 6, 8, 5, 4
heapSort(arr);
System.out.println("堆排序后:" + Arrays.toString(arr));
}
// 编写一个堆排序的方法
public static void heapSort(int[] arr) {
int temp;
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
for (int j = arr.length - 1; j > 0; j--) {
// 交换栈顶和栈底
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
}
/**
* 将一个数组(二叉树)调整成一个大顶堆
*
* @param arr 待调整的数组
* @param i 非叶子节点在数组中的索引
* @param length 对多少个元素进行调整(逐渐减少)
*/
public static void adjustHeap(int[] arr, int i, int length) {
// 先取出当前元素的值保存起来
int temp = arr[i];
// 开始调整,k 指向 i 的左子节点
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) {
// 左子节点的值小于右子节点的值,让 k 指向右子节点
k++;
}
if (arr[k] > temp) {
// 子节点大于父节点
arr[i] = arr[k]; // 把较大的值赋给当前节点
i = k; // 让 i 指向 k ,继续循环比较
} else {
break;
}
}
// 循环结束,已经将以 i 为父节点的树的最大值,放在了最顶上(局部)
arr[i] = temp; // 将temp值放到调整后的位置
}
}
赫夫曼树
基本介绍
- 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
赫夫曼树几个重要概念和举例说明
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted pathlength) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
- WPL 最小的就是赫夫曼树

赫夫曼树创建思路图解
给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.
思路分析(示意图):{13, 7, 8, 3, 29, 6, 1}
构成赫夫曼树的步骤:
- 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
- 图解:

赫夫曼树的代码实现
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 哈夫曼树
*/
public class HaffumanTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
Node root = createHaffumanTree(arr);
preOrder(root);
}
// 编写一个前序遍历的方法
public static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("空树,不能遍历");
}
}
// 创建赫夫曼树
public static Node createHaffumanTree(int[] arr) {
List<Node> nodes = new ArrayList<Node>();
// 遍历arr数组
for (int value : arr) {
// 将arr的每个元素构建成一个node放入ArrayList
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
// 排序:从小到大
Collections.sort(nodes);
System.out.println("nodes=" + nodes);
// 取出根节点权值最小的两颗二叉树
Node leftNode = nodes.get(0); // 取出最小的
Node rightNode = nodes.get(1); // 取出次小的
// 构建一颗新的二叉树
Node parent = new Node(leftNode.value + rightNode.value);
parent.legt = leftNode;
parent.right = rightNode;
// 从ArrayList删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
// 将parent加入nodes
nodes.add(parent);
}
// 返回赫夫曼树的root
return nodes.get(0);
}
}
// 创建节点类
class Node implements Comparable<Node> {
int value; // 节点权值
Node legt; // 指向左子节点
Node right; // 指向右子节点
// 前序遍历方法
public void preOrder() {
System.out.println(this);
if (this.legt != null) {
this.legt.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
// 从小到大进行排序
return this.value - o.value;
}
}
赫夫曼编码
基本介绍
- 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
- 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一
- 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间
- 赫夫曼码是可变字长编码(VLC)的一种。Huffman 于 1952 年提出一种编码方法,称之为最佳编码
原理剖析
- 通信领域中信息的处理方式 1-定长编码

- 通信领域中信息的处理方式 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 // 各个字符对应的个数
- 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
- 构成赫夫曼树的步骤:
- 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

- 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为 0 向右的路径为 1 , 编码如下:
o: 1000 u: 10010 d: 100110 y: 100111 i: 101
a : 110 k: 1110 e: 1111 j: 0000 v: 0001
l: 001 : 01
- 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133 - 长度为 : 133
说明:
原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
赫夫曼编码是无损处理方案
注意事项
注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:

最佳实践-数据压缩(创建赫夫曼树)
将给出的一段文本,比如 "i like like like java do you like a java" , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理 , 形 式 如"1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110"
步骤 1:根据赫夫曼编码压缩数据的原理,需要创建 "i like like like java do you like a java" 对应的赫夫曼树
代码实现:
最佳实践-数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)
我们已经生成了 赫夫曼树, 下面我们继续完成任务
- 生成赫夫曼树对应的赫夫曼编码 , 如下表: =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
- 使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下. 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
- 思路:前面已经分析过了,而且我们讲过了生成赫夫曼编码的具体实现。
- 代码实现:
最佳实践-数据解压(使用赫夫曼编码解码)
使用赫夫曼编码来解码数据,具体要求是
- 前面我们得到了赫夫曼编码和对应的编码
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" 3) 思路:解码过程,就是编码的一个逆向操作。
- 代码实现:
最佳实践-文件压缩
我们学习了通过赫夫曼编码对一个字符串进行编码和解码, 下面我们来完成对文件的压缩和解压, 具体要求:
给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
- 思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩
- 代码实现:
最佳实践-文件解压(文件恢复)
具体要求:将前面压缩的文件,重新恢复成原来的文件。
- 思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)
- 代码实现:
代码汇总,把前面所有的方法放在一起
赫夫曼编码压缩文件注意事项
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件
- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件)
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.
二叉排序树
先看一个需求
给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
解决方案分析
- 使用数组
数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢.
数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。 - 使用链式存储-链表
不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。 - 使用二叉排序树
二叉排序树介绍
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:

二叉排序树创建和遍历
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为 :

二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
- 删除叶子节点 (比如:2, 5, 9, 12)
- 删除只有一颗子树的节点 (比如:1)
- 删除有两颗子树的节点. (比如:7, 3,10 )
- 操作的思路分析

思路分析
第一种情况:
删除叶子节点 (比如:2, 5, 9, 12)
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 确定 targetNode 是 parent 的左子结点 还是右子结点
(4) 根据前面的情况来对应删除
左子结点 parent.left = null
右子结点 parent.right = null;第二种情况: 删除只有一颗子树的节点 比如 1
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 确定 targetNode 的子结点是左子结点还是右子结点
(4) targetNode 是 parent 的左子结点还是右子结点
(5) 如果 targetNode 有左子结点
5. 1 如果 targetNode 是 parent 的左子结点
parent.left = targetNode.left;
5.2 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.left;
(6) 如果 targetNode 有右子结点
6.1 如果 targetNode 是 parent 的左子结点
parent.left = targetNode.right;
6.2 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right情况三 : 删除有两颗子树的节点. (比如:7, 3,10 )
思路:
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 从 targetNode 的右子树找到最小的结点
(4) 用一个临时变量,将 最小结点的值保存 temp = 11
(5) 删除该最小结点
(6) targetNode.value = temp
二叉排序树删除结点的代码实现

浙公网安备 33010602011771号