HSP数据结构
HSP
1. 线性结构和非线性结构
线性
- 顺序存储结构
- 链式存储结构
非线性
- 二维数组
- 多维数组
- 广义表
- 树结构
- 图结构
2. 稀疏数组和队列
2.1 稀疏数组(sparseArr)
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个 小规模的数组 中,从而缩小程序的规模
2.2 应用实例
- 使用稀疏数组,保留二维数组
- 将稀疏数组存盘,并且可以重新恢复原来的二维数组
二维数组转成稀疏数组思路:
- 遍历原始二维数组,得到有效数据的个数 sum
- 根据 sum 就可以创建稀疏数组 sparseArr int[sum+1][3]
- 将二维数组的有效数据存入到稀疏数组
稀疏数组转回原始二维数组:
- 先读取稀疏数组的第一行,根据第一行的数据创建原始的二维数组,比如 chessArr= new int[11][11]
- 再读取稀疏数组后几行的数据,并赋给原始的二维数组即可
public class sparseArr {
static int[][] chessArr = new int[11][11];
static int row = chessArr.length;
static int col = chessArr[0].length;
static int sum = 0; // 有效数字
public static void main(String[] args) {
chessArr[1][2] = 1;
chessArr[2][3] = 2;
System.out.println("原始二维数组如下:");
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
System.out.print(chessArr[i][j] + " ");
if (chessArr[i][j] != 0){
sum++;
}
}
System.out.println();
}
System.out.println();
System.out.println("稀疏数组如下:");
int[][] sparseArr = chessToSparseArr(chessArr);
printArr(sparseArr);
System.out.println();
System.out.println("稀疏数组还原回二维数组:");
int[][] chess = sparseToChess(sparseArr);
printArr(chess);
}
public static int[][] chessToSparseArr(int[][] chessArr){
int k = 0; // 记录有效数个数
int[][] sparseArr = new int[sum + 1][3];
sparseArr[0] = new int[]{row, col, sum}; // 一维数组
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (chessArr[i][j] != 0){
sparseArr[++k] = new int[]{i, j, chessArr[i][j]}; // 将非 0 值传入到 稀疏数组 sparseArr中
}
}
}
return sparseArr;
}
public static int[][] sparseToChess(int[][] sparseArr){
int m = sparseArr[0][0];
int n = sparseArr[0][1];
int[][] chess1 = new int[m][n];
for (int i = 1; i < sparseArr.length; i++) {
chess1[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
return chess1;
}
public static void printArr(int[][] arr){
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
}
}
课后练习:
- 在前面基础上,将稀疏数组保存到磁盘上,比如:map.data
- 恢复原来的数组时,读取 map.data 进行恢复
2.3 队列(普通)
有序列表
- 可以用数组或链表实现
- 先入先出
数组模拟:
- 入队:rear++
- 出队:front++
当我们将数据存入队列时称为 ”addQueue“,addQueue的处理需要有2个步骤:
- 将 rear++,当 front == rear 【空】
- 若 rear < maxSize,则将数据存入 rear 所指的数组元素中,否则无法存入数据
- rear == maxSize - 1【队列满】
public class ArrayQueueDemo {
public static void main(String[] args) {
ArrayQueue arrayQueue = new ArrayQueue(3);
Scanner scanner = new Scanner(System.in);
boolean loop = true;
// 输出一个菜单
while (loop){
System.out.println("s(show): 显示队列");
System.out.println("e(exit): 退出队列");
System.out.println("a(add): 添加数据到队列");
System.out.println("g(get): 从队列取出队列");
System.out.println("p(peek): 查看队列头的数据");
String s = scanner.next();
switch (s){
case "s":
arrayQueue.showQueue();
break;
case "a":
System.out.println("输出一个数");
int value = scanner.nextInt();
arrayQueue.addQueue(value);
break;
case "g":
try {
int queue = arrayQueue.getQueue();
System.out.println("取出的数据是: " + queue);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "p":
try {
int peek = arrayQueue.peek();
System.out.println("队头的数据是: " + peek);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "e":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
class ArrayQueue{
// 定义属性
private final 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;
}
arr[++rear] = n;
}
// 获取队列数据,数据出列
public int getQueue(){
if (isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
return arr[++front];
}
// 显示队列的头数据,注意不是取出数据
public int peek(){
if (isEmpty()){
throw new RuntimeException("队列空,不能取数据");
}
return arr[front + 1];
}
// 显示队列所有数据
public void showQueue(){
if (isEmpty()){
System.out.println("队列为空,没有数据~~");
return;
}
System.out.print("当前队列为:");
for (int i = 0; i < rear + 1; i++) { // rear 初始值为 -1
if (i > front){
System.out.print(arr[i] + " ");
}
}
System.out.println();
}
}
问题分析并优化:
- 目前数组使用一次就不能用了,没有达到复用效果
- 将这个数组使用算法,改进成一个环形的队列,取模:%
2.4 队列(环形)--- 取模方式实现
思路如下:
-
front 变量的含义做一个调整:front 就指向队列的第一个元素,也就是 arr[front] 就是队列的 第一个元素
- front 初始值:0
-
rear 变量的含义也做一个调整:rear 指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定
- rear 的初始值:0
-
当队列满时,条件是(rear+1)% maxsize == front【满】
-
当队列为空的条件,rear == front
-
当我们这样分析,队列中有效的数据的个数 \((rear+maxsize-front)\%maxsize\)
-
我们就可以在原来的队列上修改得到一个环形队列
public class CircleArrayQueueDemo {
public static void main(String[] args) {
CircleArray circleArray = new CircleArray(4); // 说明:设置4,其队列的有效数据最大是 3
Scanner scanner = new Scanner(System.in);
boolean loop = true;
// 输出一个菜单
while (loop){
System.out.println("s(show): 显示队列");
System.out.println("e(exit): 退出队列");
System.out.println("a(add): 添加数据到队列");
System.out.println("g(get): 从队列取出队列");
System.out.println("p(peek): 查看队列头的数据");
String s = scanner.next();
switch (s){
case "s":
circleArray.showQueue();
break;
case "a":
System.out.println("输出一个数");
int value = scanner.nextInt();
circleArray.addQueue(value);
break;
case "g":
try {
int queue = circleArray.getQueue();
System.out.println("取出的数据是: " + queue);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "p":
try {
int peek = circleArray.peek();
System.out.println("队头的数据是: " + peek);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "e":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
class CircleArray{
// 定义属性
private final int maxSize;
// front 变量的含义做一个调整:front 就指向队列的第一个元素,也就是arr[front]的第一个元素
// front 的初始值 = 0
private int front;
// rear 变量的含义也做一个调整:rear 指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定
// rear 的初始值 = 0
private int rear;
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;
}
arr[rear] = n;
rear = (rear + 1) % maxSize; // 防止越界
}
// 获取队列数据,数据出列
public int getQueue(){
if (isEmpty()){
// 通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
int val = arr[front];
front = (front + 1) % maxSize; // 同样避免越界
return val;
}
// 显示队列的头数据,注意不是取出数据
public int peek(){
if (isEmpty()){
throw new RuntimeException("队列空,不能取数据");
}
return arr[front];
}
// 显示队列所有数据
public void showQueue(){
if (isEmpty()){
System.out.println("队列为空,没有数据~~");
return;
}
System.out.print("当前队列为:");
for (int i = front; i < front + size(); i++) {
System.out.print("arr[" + (i % maxSize) +"]= " + arr[i % maxSize] + " ");
}
System.out.println();
}
// 求出当前队列有效数据的个数
public int size(){
return (rear + maxSize - front) % maxSize;
}
}
3. 链表(Linked List)介绍
链表是有序的列表,在内存中存储如下:
小结:
- 链表是以节点的方式来存储,是链式
- 每个节点包含 data 域、next 域:指向下一个节点
- 如图:发现链表的各个节点不一定是连续存放
- 链表分带头节点的链表和没有头节点的链表,根据实际需求来确
3.1 单链表
单链表(带头结点)逻辑结构示意图如下:
3.2 单链表应用实例
1. 普通添加
单链表的创建示意图(添加),显示单向链表的分析
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.show();
}
}
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;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
// 定义 SingleLinkedList 管理英雄
class SingleLinkedList{
// 先初始化头节点,头节点不要动,不存放具体的数据
private HeroNode head = new HeroNode(0, "", "");
// 添加节点到单向链表
// 思路:当不考虑编号顺序时
// 1. 找到当前链表的最后节点
// 2. 将最后这个节点的 next 指向 新的节点
public void add(HeroNode heroNode){
// 因为 head 节点不能动,因此我们需要一个辅助变量 temp
HeroNode temp = head;
// 遍历链表,找到最后
while (temp.next!=null){
temp = temp.next;
}
temp.next = heroNode;
}
// 显示链表【遍历】
public void show(){
// 判断链表是否为空
if (head.next == null){
System.out.println("链表为空");
return;
}
// 因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (temp.next!=null){
temp = temp.next;
System.out.println(temp);
}
}
}
2. 按顺序添加
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.addByOrder(hero1);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
singleLinkedList.show();
}
}
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;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
// 定义 SingleLinkedList 管理英雄
class SingleLinkedList{
// 先初始化头节点,头节点不要动,不存放具体的数据
private HeroNode head = new HeroNode(0, "", "");
// 添加节点到单向链表
// 思路:当不考虑编号顺序时
// 1. 找到当前链表的最后节点
// 2. 将最后这个节点的 next 指向 新的节点
public void add(HeroNode heroNode){
// 因为 head 节点不能动,因此我们需要一个辅助变量 temp
HeroNode temp = head;
// 遍历链表,找到最后
while (temp.next!=null){
temp = temp.next;
}
temp.next = heroNode;
}
public void addByOrder(HeroNode heroNode){
// 因为 head 节点不能动,因此我们需要一个辅助变量 temp
// 因为是单链表,所以我们找的 temp 是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
// 遍历链表,找到最后
if (temp.next == null){
temp.next = heroNode;
return;
}
while (temp.next!=null){
if (heroNode.no == temp.next.no) {
System.out.println("该节点已经存在,无法插入");
break;
}
if (heroNode.no < temp.next.no){
heroNode.next = temp.next;
temp.next = heroNode;
// 如果在中间被拦截了,在结尾处就不用添加了
break;
}
temp = temp.next;
}
if (temp.next == null){ // 遍历到结尾,既没有重复的,而且下标也没有比待加入的no 小的,就直接甩到最后
temp.next = heroNode;
}
}
// 显示链表【遍历】
public void show(){
// 判断链表是否为空
if (head.next == null){
System.out.println("链表为空");
return;
}
// 因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (temp.next!=null){
temp = temp.next;
System.out.println(temp);
}
}
}
3. 单链表节点的修改
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.addByOrder(hero1);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
singleLinkedList.update(new HeroNode(3, "Kobe", "24"));
singleLinkedList.show();
}
}
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;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
// 定义 SingleLinkedList 管理英雄
class SingleLinkedList{
// 先初始化头节点,头节点不要动,不存放具体的数据
private HeroNode head = new HeroNode(0, "", "");
// 添加节点到单向链表
// 思路:当不考虑编号顺序时
// 1. 找到当前链表的最后节点
// 2. 将最后这个节点的 next 指向 新的节点
public void add(HeroNode heroNode){
// 因为 head 节点不能动,因此我们需要一个辅助变量 temp
HeroNode temp = head;
// 遍历链表,找到最后
while (temp.next!=null){
temp = temp.next;
}
temp.next = heroNode;
}
public void addByOrder(HeroNode heroNode){
// 因为 head 节点不能动,因此我们需要一个辅助变量 temp
// 因为是单链表,所以我们找的 temp 是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
// 遍历链表,找到最后
if (temp.next == null){
temp.next = heroNode;
return;
}
while (temp.next!=null){
if (heroNode.no == temp.next.no) {
System.out.println("该节点已经存在,无法插入");
break;
}
if (heroNode.no < temp.next.no){
heroNode.next = temp.next;
temp.next = heroNode;
// 如果在中间被拦截了,在结尾处就不用添加了
break;
}
temp = temp.next;
}
if (temp.next == null){ // 遍历到结尾,既没有重复的,而且下标也没有比待加入的no 小的,就直接甩到最后
temp.next = heroNode;
}
}
// 显示链表【遍历】
public void show(){
// 判断链表是否为空
if (head.next == null){
System.out.println("链表为空");
return;
}
// 因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (temp.next!=null){
temp = temp.next;
System.out.println(temp);
}
}
// 修改节点的信息,根据 no 编号来修改,即:no 编号不能改
public void update(HeroNode newHeroNode){
HeroNode temp = head;
while (temp.next!=null){
if (newHeroNode.no == temp.next.no){
temp.next.name = newHeroNode.name; // 只要修改成功,temp.next != null
temp.next.nickname = newHeroNode.nickname;
System.out.println("修改成功!");
break;
}
temp = temp.next;
}
if (temp.next == null){ // 遍历到结尾了,还没有找到
System.out.println("你要修改的节点不存在,无法修改");
}
}
}
4. 单链表节点的删除和小结
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.addByOrder(hero1);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
singleLinkedList.show();
singleLinkedList.update(new HeroNode(3, "Kobe", "24"));
singleLinkedList.show();
singleLinkedList.delete(4);
singleLinkedList.show();
}
}
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;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
// 定义 SingleLinkedList 管理英雄
class SingleLinkedList{
// 先初始化头节点,头节点不要动,不存放具体的数据
private HeroNode head = new HeroNode(0, "", "");
// 添加节点到单向链表
// 思路:当不考虑编号顺序时
// 1. 找到当前链表的最后节点
// 2. 将最后这个节点的 next 指向 新的节点
public void add(HeroNode heroNode){
// 因为 head 节点不能动,因此我们需要一个辅助变量 temp
HeroNode temp = head;
// 遍历链表,找到最后
while (temp.next!=null){
temp = temp.next;
}
temp.next = heroNode;
}
public void addByOrder(HeroNode heroNode){
// 因为 head 节点不能动,因此我们需要一个辅助变量 temp
// 因为是单链表,所以我们找的 temp 是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
// 遍历链表,找到最后
if (temp.next == null){
temp.next = heroNode;
return;
}
while (temp.next!=null){
if (heroNode.no == temp.next.no) {
System.out.println("该节点已经存在,无法插入");
break;
}
if (heroNode.no < temp.next.no){
heroNode.next = temp.next;
temp.next = heroNode;
// 如果在中间被拦截了,在结尾处就不用添加了
break;
}
temp = temp.next;
}
if (temp.next == null){ // 遍历到结尾,既没有重复的,而且下标也没有比待加入的no 小的,就直接甩到最后
temp.next = heroNode;
}
}
// 显示链表【遍历】
public void show(){
// 判断链表是否为空
if (head.next == null){
System.out.println("链表为空");
return;
}
// 因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (temp.next!=null){
temp = temp.next;
System.out.println(temp);
}
}
// 修改节点的信息,根据 no 编号来修改,即:no 编号不能改
public void update(HeroNode newHeroNode){
HeroNode temp = head;
while (temp.next!=null){
if (newHeroNode.no == temp.next.no){
temp.next.name = newHeroNode.name;
temp.next.nickname = newHeroNode.nickname;
System.out.println("修改成功!");
break;
}
temp = temp.next;
}
if (temp.next == null){ // 遍历到结尾了,还没有找到
System.out.println("你要修改的节点不存在,无法修改");
}
}
// 删除当前节点
public void delete(int no){
HeroNode temp = head;
boolean flag = true; // 如果在遍历过程中找到了并删除(但是如果到结尾都没找到的话,就输出没找到)
while (temp.next!=null){
if (no == temp.next.no){
temp.next = temp.next.next; // temp.next 可能为 Null
System.out.println("删除成功");
flag = false;
break;
}
temp = temp.next;
}
if (flag){
System.out.println("该节点不存在,无法删除");
}
}
}
5. 面试题
-
有效节点个数
public int size(){ int count = 0; HeroNode cur = head.next; while (cur != null){ count++; cur = cur.next; } return count; }
-
倒数第 k 个节点
public HeroNode backKthNode(int k){ if (k > size() || k <= 0){ System.out.println("越界,无法返回"); return null; } HeroNode cur = head.next; // cur 为头节点 int frontOrder = size() - k; // 头节点移动的次数【 3 - 2 = 1】 while (cur.next != null && frontOrder > 0){ cur = cur.next; // 移动了2次 frontOrder--; } return cur; }
-
单链表的反转
public void reverse(){ HeroNode reverseHead = new HeroNode(0, "", ""); HeroNode nextNode = null; // 指向当前节点的下一个节点【保存的是节点】 HeroNode cur = head.next; if (cur == null || size() <= 1){ System.out.println("无法反转"); return; } while (cur != null) { // 指针 next 的线会断(移动),所以我们用节点 next来保存 nextNode = cur.next; // 先暂时保存当前节点的下一个接待你,因为后面要用【next是指针,而 nextNode为节点】 cur.next = reverseHead.next; // 节点添加 reverseHead.next = cur; // 将 cur 连接到新的链表中 cur = nextNode; // 循环(指向下一个节点) } head.next = reverseHead.next; }
-
从头到尾打印单链表
-
反向遍历(先反转,再打印)
- 会破坏原来单链表的结构
-
Stack栈【使用双端队列 Deque 实现】
public void printReverse1(){ Deque<HeroNode> heroNodes = new LinkedList<>(); // 定义一个双端队列(模拟栈) HeroNode cur = head.next; while (cur != null){ heroNodes.addFirst(cur); cur = cur.next; } while (heroNodes.size() > 0){ System.out.println(heroNodes.removeFirst()); } }
-
-
合并2个有序的单链表,合并之后依旧有序、
-
建一个新链表,哪个小就加进去
public void merge(HeroNode heroNode1, HeroNode heroNode2) { System.out.println("=====合并 2个 链表===="); HeroNode mergeHead = new HeroNode(0, "", ""); HeroNode cur1 = heroNode1.next; HeroNode cur2 = heroNode2.next; HeroNode temp = mergeHead; // 添加时候需要找到最后一个节点 while (cur1 != null || cur2 != null) { while (cur1 != null && cur2 != null) { // 当都不为null 时,才可以比较 // 储存当前节点的下一个节点 while (temp.next != null) { // 找到末尾节点 temp = temp.next; } if (cur1.no < cur2.no) { HeroNode temp33 = cur1.next; // 储存当前节点的下一个节点 cur1.next = null; temp.next = cur1; cur1 = temp33; } else { HeroNode temp44 = cur2.next; // 储存当前节点的下一个节点 cur2.next = null; temp.next = cur2; cur2 = temp44; } } if (cur2 != null) { while (temp.next != null) { // 找到末尾节点 temp = temp.next; } HeroNode temp22 = cur2.next; // 储存当前节点的下一个节点 cur2.next = null; temp.next = cur2; cur2 = temp22; } if (cur1 != null) { while (temp.next != null) { // 找到末尾节点 temp = temp.next; } HeroNode temp11 = cur1.next; // 储存当前节点的下一个节点 cur1.next = null; temp.next = cur1; cur1 = temp11; } } // 输出 HeroNode cur = mergeHead.next; while (cur != null) { System.out.println(cur); cur = cur.next; } }
-
3.3 双向链表
单向链表缺点分析:
- 查找方向只能是一个方向,而双向链表可以向前或者向后查找
- 单项链表不能自我删除,需要靠辅助节点,而双向链表,则可以 自我删除,所以前面我们单链表删除节点时,总是找到temp,temp是待删除节点的前一个结点
双向链表分析:
-
遍历、添加、修改
-
删除(自我删除)
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 dbList = new doubleLinkedList();
dbList.addLast(hero1);
dbList.addLast(hero2);
dbList.addLast(hero3);
dbList.addLast(hero4);
dbList.show();
// 修改链表
HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "入云龙");
dbList.update(newHeroNode);
dbList.show();
// 删除节点
dbList.delete(3);
dbList.show();
}
}
class doubleLinkedList{
public HeroNode2 getHead() {
return head;
}
private final HeroNode2 head = new HeroNode2(0, "", "");
// 遍历双向链表的方法
public void show(){
HeroNode2 cur = head.next;
while (cur != null){
System.out.println(cur);
cur = cur.next;
}
}
// 添加到末尾
public void addLast(HeroNode2 newHeroNode2){
HeroNode2 temp = head;
while (temp.next != null){ // temp 最终指向最后一个节点
temp = temp.next;
}
temp.next = newHeroNode2;
newHeroNode2.pre = temp;
}
// 修改一个节点的内容
public void update(HeroNode2 heroNode2){
HeroNode2 cur = head.next;
while (cur != null){
if (cur.no == heroNode2.no){
cur.name = heroNode2.name;
cur.nickname = heroNode2.nickname;
System.out.println("修改成功");
return;
}
cur = cur.next;
}
System.out.println("你要修改的节点不存在,请重试!!!");
}
// 删除节点
public void delete(int id){
HeroNode2 cur = head.next;
while (cur != null){
if (cur.no == id){
cur.pre.next = cur.next;
if (cur.next != null) {
cur.next.pre = cur.pre; // 如果是最后一个节点,就不需要执行这句话 【避免空指针】
}
System.out.println("删除成功~");
return;
}
cur = cur.next;
}
System.out.println("你要修改的节点不存在,请重试!!!");
}
}
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;
}
@Override
public String toString() {
return "HeroNode2{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
1. 普通添加
// 添加到末尾
public void addLast(HeroNode2 newHeroNode2){
HeroNode2 temp = head;
while (temp.next != null){ // temp 最终指向最后一个节点
temp = temp.next;
}
temp.next = newHeroNode2;
newHeroNode2.pre = temp;
}
2. 按顺序添加
// 按照 id 来添加
public void addByOrder(HeroNode2 newHeroNode2){
HeroNode2 cur = head.next; // cur 不为 null
while (cur != null){
if (cur.no > newHeroNode2.no){
HeroNode2 temp = cur.pre; // 保存前一个节点 [方便处理 4 根线]
newHeroNode2.next = cur;
cur.pre = newHeroNode2;
temp.next = newHeroNode2;
newHeroNode2.pre = temp;
System.out.println("插入成功");
return;
}
cur = cur.next;
}
}
3. 修改
// 修改一个节点的内容
public void update(HeroNode2 heroNode2){
HeroNode2 cur = head.next;
while (cur != null){
if (cur.no == heroNode2.no){
cur.name = heroNode2.name;
cur.nickname = heroNode2.nickname;
System.out.println("修改成功");
return;
}
cur = cur.next;
}
System.out.println("你要修改的节点不存在,请重试!!!");
}
4. 删除(注意空指针)
// 删除节点
public void delete(int id){
HeroNode2 cur = head.next;
while (cur != null){
if (cur.no == id){
cur.pre.next = cur.next;
if (cur.next != null) {
cur.next.pre = cur.pre; // 如果是最后一个节点,就不需要执行这句话 【避免空指针】
}
System.out.println("删除成功~");
return;
}
cur = cur.next;
}
System.out.println("你要修改的节点不存在,请重试!!!");
}
3.4 环形链表(Josephu环)
- 编号 1,2,…… n 的 n 个人 围坐一圈
- 约定编号 k\((1\leqslant k\leqslant n)\) 的人从 1 开始报数,数到 m 的那个人又出列
- 以此类推,直到所有人都出列为止
- 由此产生一个出队编号的序列
实现方式:
- 不带头节点的循环链表来处理 Josephu 问题
- 先构成一个有 n 个节点的单循环链表
- 然后从 k 节点起从 1 开始计数,计到 m 时,对应节点从链表中删除
- 然后再从被删除节点的下一个节点又从 1 开始计数
- 直到最后一个节点从链表中删除
1. 单向循环链表
- n = 5
- k = 1(从第一个人开始报数)
- m = 2
根据用户输入,生成一个小孩出圈的顺序:
-
需要创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
-
当小孩报数时,让 first 和 helper 指针同时移动 m - 1次
-
这时就可以将 first 指向的小孩节点出圈
- first = first.next
- helper.next = first
-
这样,原来 first 指向的接待你就没有任何引用,就会被 GC 回收
// 根据用户的输入,计算 出圈顺序
/**
* @param startNo:从第几个小孩开始数数
* @param countNum:数几下
* @param nums:最初有多少小孩在圈中
*/
public void countBoy(int startNo, int countNum, int nums){
// 数据校验
if (first == null || startNo < 1 || startNo > nums){
System.out.println("不符合规范!无法输出序列");
return;
}
for (int i = 1; i < startNo; i++) { // 开始位置 startNo
first = first.getNext(); // 确定起始位置 first
}
// 创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
Boy helper = first;
while (helper.getNext() != first){
helper = helper.getNext(); // 确认初始 helper 位置
}
// 开始输出队列:停止条件:helper == first
while (helper != first){
for (int i = 1; i <= countNum - 1; i++) { // 开始数数
first = first.getNext(); // 当小孩报数时,让 first 和 helper 指针同时移动 m - 1 次
helper = helper.getNext(); // (helper 紧紧挨着 first)
}
// 这时就可以将 first 指向的小孩节点出圈
System.out.println(first);
first = first.getNext();
helper.setNext(first);
}
System.out.println(first); // 输出最后一个节点
}
2. 构建一个单向的环形链表思路:
- 先创建第一个节点,让 first 指向该节点,并形成环形
- 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可
遍历环形链表
- 先让一个辅助指针(变量)curBoy,指向 first 节点
- 然后通过一个 while 循环遍历该环形链表即可
- 结束条件 curBoy.next = first
public class JosePhu {
public static void main(String[] args) {
CircleSingleLinkedList list = new CircleSingleLinkedList();
list.add(25);
list.show();
System.out.println("===== 出列顺序 ====");
list.countBoy(1, 2, 25);
}
}
// 创建环形单向链表
class CircleSingleLinkedList{
// 创建一个 first 节点,当前没有编号
private Boy first = null;
// 添加小孩节点,构建成也给环形链表
public void add(int nums){
if (nums < 1){
System.out.println("nums 不正确");
return;
}
Boy cur = null; // 辅助指针,帮助构建环形链表
// 使用 for 循环来创建环形链表
for (int i = 1; i <= nums; i++) {
Boy boy = new Boy(i);
if (i == 1){ // 第一个小孩
first = boy;
boy.setNext(first); // 构成环
cur = first; // 指向第一个小孩
}else {
cur.setNext(boy); // 改变线
boy.setNext(first); // 改变线
cur = boy; // 最后移动 cur 指针
}
}
}
public void show(){
// 因为 first 指针不能动,因此我们仍然使用一个辅助指针完成遍历
Boy cur = first;
while (cur.getNext() != first){
System.out.println(cur);
cur = cur.getNext();
}
System.out.println(cur); // 输出末尾节点
}
}
class Boy{
private int no;
private Boy next;
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
@Override
public String toString() {
return "Boy{" +
"no=" + no +
'}';
}
}
4. 栈
-
Stack
- 允许变化的一端,称为 栈顶 Top
- 另一端为固定的一端,称为栈底 Buttom
-
FIFO
-
使用双端队列 Deque 实现
public void printReverse1(){ Deque<HeroNode> heroNodes = new LinkedList<>(); // 定义一个双端队列(模拟栈) HeroNode cur = head.next; while (cur != null){ heroNodes.addFirst(cur); cur = cur.next; } while (heroNodes.size() > 0){ System.out.println(heroNodes.removeFirst()); } }
4.1 栈的应用场景
- 子程序调用
- 处理递归调用
- 表达式的转换【中缀表达式转后缀表达式】与 求值
- 二叉树遍历
- 图形的深度优先【DFS】
4.2 用数组模拟栈
由于栈是一种有序列表,当然可以使用数组的结构来存储栈的数据结构
思路分析:
- 使用数组模拟栈
- 定义一个 top 来表示栈顶,初始化为 -1
- 入栈的操作,当有数据加入到栈时,stack[++top] = data;
- 出栈的操作
- int val = stack[top]
- top--
- return val
代码实现:
public class ArrayStackDemo {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(5);
Scanner in = new Scanner(System.in);
boolean flag = true;
while (flag){
System.out.println("show: 显示栈");
System.out.println("exit: 退出程序");
System.out.println("push: 入栈");
System.out.println("pop: 出栈");
System.out.println("请输入你的选择");
String next = in.next();
switch (next){
case "show":
stack.show();
break;
case "exit":
flag = false;
break;
case "push":
System.out.println("请输入你要 入栈的数");
String next1 = in.next();
int i = Integer.parseInt(next1);
stack.push(i);
break;
case "pop": // 出栈时可能有运行异常抛出,我们需要 catch 来看具体是什么情况
try {
int res = stack.pop();
System.out.println("出栈的数据是:" + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
default:
break;
}
}
stack.push(1);
stack.push(2);
System.out.println("栈顶元素为:" + stack.peek());
stack.push(3);
stack.show();
}
}
class ArrayStack{
private final int maxSize;
private final int[] stack;
private int top = -1;
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
// 栈满
public Boolean isFull(){
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty(){
return top == -1;
}
// 入栈
public void push(int data){
if (isFull()){
System.out.println("栈满了,无法 push");
return;
}
stack[++top] = data;
}
// 出栈
public int pop(){
if (isEmpty()){
// 抛出异常,本身就代表终止了,所以就不需要有 return 了
throw new RuntimeException("栈空,没有数据");
}
int val = stack[top];
top--;
return val;
}
// 栈顶元素
public int peek(){
if (isEmpty()){
System.out.println("栈顶为 null");
return -1;
}
return stack[top];
}
// 遍历
public void show(){
// 遍历要从栈顶开始遍历
if (isEmpty()){
System.out.println("栈空,无法遍历");
return;
}
for (int i = top; i >= 0; i--) { // 从栈顶开始遍历
System.out.format("stack[%d] = %d", i, stack[i]);
System.out.println();
}
}
}
4.3 用链表模拟栈
4.4 栈实现综合计算器【中缀】
代码实现:(Ver1 )
存在问题:不能有2位数字 --->比如:"70+2*6-4";
public class cal {
public static void main(String[] args) {
String exp = "7+2*6-4";
// 创建2个栈
ArrayStack numStack = new ArrayStack(10);
ArrayStack operStack = new ArrayStack(10);
// 定义相关的变量
int index = 0;
int num1 = 0;
int num2 = 0;
int oper = 0;
int res = 0;
char ch = ' '; // 每次扫描得到的 char 放入 ch 中
// 开始 while 循环扫描 exp
while (true){
ch = exp.substring(index, index + 1).charAt(0);
if (operStack.isOper(ch)){
// 判断当前符号栈是否为 null
if (!operStack.isEmpty()){
if (operStack.priority(ch) <= operStack.priority(operStack.peek())){
int pop1 = numStack.pop();
int pop2 = numStack.pop();
int operTop = operStack.pop();
int i = operStack.calNum(pop2, pop1, operTop);
numStack.push(i);
operStack.push(ch);
}else { // 当前优先级 > 栈顶
operStack.push(ch);
}
}else { // operStack 不为 null
operStack.push(ch);
}
}else {
numStack.push(ch - '0');
}
index++;
if (index == exp.length()){
break;
}
}
// 现在扫描完毕了,现在处理2个栈中的数据就可以了
while (true){
// 终止条件
if (operStack.isEmpty()){
break;
}
int n1 = numStack.pop();
int n2 = numStack.pop();
int op = operStack.pop();
int i = numStack.calNum(n2, n1, op);
numStack.push(i);
}
res = numStack.peek();
System.out.println(res);
}
}
class ArrayStack { // 需要扩展功能
private final int maxSize;
private final int[] stack;
private int top = -1;
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
// 栈满
public Boolean isFull() {
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty() {
return top == -1;
}
// 入栈
public void push(int data) {
if (isFull()) {
System.out.println("栈满了,无法 push");
return;
}
stack[++top] = data;
}
// 出栈
public int pop() {
if (isEmpty()) {
// 抛出异常,本身就代表终止了,所以就不需要有 return 了
throw new RuntimeException("栈空,没有数据");
}
int val = stack[top];
top--;
return val;
}
// 栈顶元素
public int peek() {
if (isEmpty()) {
System.out.println("栈顶为 null");
return -1;
}
return stack[top];
}
// 遍历
public void show() {
// 遍历要从栈顶开始遍历
if (isEmpty()) {
System.out.println("栈空,无法遍历");
return;
}
for (int i = top; i >= 0; i--) { // 从栈顶开始遍历
System.out.format("stack[%d] = %d", i, stack[i]);
System.out.println();
}
}
// 返回运算符的优先级
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 calNum(int num1, int num2, int oper){
int res = 0;
switch (oper){
case '+':
res = num1 + num2;
break;
case '-':
res = num1 - num2;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num1 / num2;
break;
default:
break;
}
return res;
}
}
解决当处理多位数时,不能发现一个数就立即入栈,因为它可能是多位数
在处理数时,需要向 exp 的 index后再看一位
- 如果是数,继续扫描
- 如果是符号才入栈
- 最后一个数直接入栈
定义一个字符串变量用于拼接
StringBuilder sb = new StringBuilder();
public class cal {
public static void main(String[] args) {
String exp = "7*2*2-5+1-5+3-4";
// 创建2个栈
ArrayStack numStack = new ArrayStack(10);
ArrayStack operStack = new ArrayStack(10);
StringBuilder sb = new StringBuilder();
// 定义相关的变量
int index = 0;
int num1 = 0;
int num2 = 0;
int oper = 0;
int res = 0;
char ch = ' '; // 每次扫描得到的 char 放入 ch 中
// 开始 while 循环扫描 exp
while (true){
ch = exp.substring(index, index + 1).charAt(0);
if (operStack.isOper(ch)){
if (sb.length() != 0) {
String s = sb.toString();
int i1 = Integer.parseInt(s);
numStack.push(i1);
sb.delete(0, sb.length());
}
// 判断当前符号栈是否为 null
if (!operStack.isEmpty()){
if (operStack.priority(ch) <= operStack.priority(operStack.peek())){
int pop1 = numStack.pop();
int pop2 = numStack.pop();
int operTop = operStack.pop();
int i = operStack.calNum(pop2, pop1, operTop);
numStack.push(i);
operStack.push(ch);
}else { // 当前优先级 > 栈顶
operStack.push(ch);
}
}else { // operStack 不为 null
operStack.push(ch);
}
}else { // 当前指针是 数字
sb.append(ch - '0'); // 暂时存到 sb中,等待下一个是符号时才压入
}
index++;
if (index == exp.length()){
numStack.push(Integer.parseInt(sb.toString())); // 处理最后一个数字
break;
}
}
// 现在扫描完毕了,现在处理2个栈中的数据就可以了
while (true){
// 终止条件
if (operStack.isEmpty()){
break;
}
int n1 = numStack.pop();
int n2 = numStack.pop();
int op = operStack.pop();
int i = numStack.calNum(n2, n1, op);
numStack.push(i);
}
res = numStack.peek();
System.out.println(res);
}
}
4.5 前缀【前缀】、中缀、后缀【逆波兰表达式】表达式规则
前缀(顶 - 次顶)
- 符号在数字前
- (3 + 4) * 5 - 6【中缀】
- - * + 3 4 5 6【前缀】
计算机使用前缀表达式求值
从 右向左 扫描
- 数字 入栈
- 符号
- 弹出栈顶2个数
- 用运算符计算
- 将结果压入栈
- 重复上述步骤直到表达式最左端
(3 + 4) * 5 - 6【中缀】---> - * + 3 4 5 6【前缀】
中缀(一般会转成后缀表达式)
- 日常写法
- 需要判断符号优先级
- 对计算机来说不好操作
后缀 (次顶 - 顶)
- 又称为逆波兰表达式,与前缀类似,只是符号在数字之后
- 比如:(3 + 4) * 5 - 6【中缀】
- 转为:3 4 + 5 * 6 -
表达式 | 后缀表达式 |
---|---|
a + b | a b+ |
a + (b - c) | a b c - + |
a + (b - c) * d | a b c - d * + |
a + d * (b - c) | a d b c - * + |
a = 1 + 3 | a 1 3 + = |
后缀表达式的计算机求值
从 左至右 扫描表达式
- 数字,入栈
- 符号
- 弹出2个数(次顶、顶),并将结果入栈
- 重复上述过程到表达式右端
- 最后得出的值即为表达式的结果
例如:比如:(3 + 4) * 5 - 6【中缀】 ---> 3 4 + 5 * 6 - 【后缀】
4.6 逆波兰计算器(后缀)suffix
- 输入的表达式已经是后缀表达式
- 使用系统栈实现(双端队列)
- 支持小括号和多位整数
public class polandCal {
public static void main(String[] args) {
// 先定义逆波兰表达式
String suffixExp = "4 5 * 8 - 60 + 8 2 / +";
ArrayList<String> list = new ArrayList<>();
String[] s = suffixExp.split(" ");
for (String s1 : s) {
list.add(s1);
}
System.out.println(list);
// 定义一个栈
Deque<Integer> stack = new LinkedList<>();
// 开始扫描
for (String s1 : list) {
char c = s1.charAt(0);
if (s1.matches("\\d+")){ // 正则表达式【匹配多位数(0个或者)】
stack.addFirst(Integer.parseInt(s1));
}else { // 符号
int num1 = stack.removeFirst();
int num2 = stack.removeFirst();
int i = calNum(num2, num1, c);
stack.addFirst(i);
}
}
System.out.println("res = " + stack.peekFirst());
}
public static boolean isOper(char c){
return c < '0' || c > '9';
}
// 计算方法
public static int calNum(int num1, int num2, int oper){
int res = 0;
switch (oper){
case '+':
res = num1 + num2;
break;
case '-':
res = num1 - num2;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num1 / num2;
break;
default:
break;
}
return res;
}
}
4.7 中缀 ---> 后缀
- 后缀表达式适合计算机运算
- 中缀符合人类常识
具体实现步骤:
- 初始化2个栈:
- S1:运算符栈
- S2:中间结果
- 从左到右扫描中缀表达式
- 遇到数字:压入 S2
- 遇到符号:观察 S1
- S1 为空,或 peek() 为 "(",则直接将此运算符入栈
- S1 不为空,
- 比栈顶符号的优先级高,压入 S1
- 比栈顶符号的优先级低或者相同,弹出 peek() 并压入到 s2中。继续与新的栈顶进行比较
- 遇到括号时:
- "(",直接压入 S1
- ")":依次弹出 S1 符号,并压入 S2,直到遇到 左括号位置,此时将这一对括号丢弃
- 扫描完毕后,将 S1 中剩余运算符依次弹出i并压入 S2
- 依次弹出 S2 中的元素并输出,结果的逆序就是答案
// 中缀 --> 后缀
1 + ((2 + 3) * 4) - 5
扫描到的元素 | 52(栈底->栈顶) | s1(栈底->栈顶) | 说明 |
---|---|---|---|
1 | 1 | 空 | 数字,直接入栈 |
+ | 1 | + | s1为空,运算符直接入栈 |
( | 1 | + ( | 左括号,直接入栈 |
( | 1 | + ( ( | 同上 |
2 | 1 2 | + ( ( | 数字 |
+ | 1 2 | + ( ( + | s1栈顶为左括号,运算符直接入栈 |
3 | 1 2 3 | + ( ( + | 数字 |
) | 1 2 3 + | + ( | 右括号,弹出运算符直至遇到左括号 |
x | 1 2 3 + | + ( × | s1栈顶为左括号,运算符直接入栈 |
4 | 1 2 3 + 4 | + ( × | 数字 |
) | 1 2 3 + 4 × | + | 右括号,弹出运算符直至遇到左括号 |
- | 1 2 3 + 4 × ÷ | - | -与+优先级相同,因此弹出+,再压入- |
5 | 1 2 3 + 4 × + 5 | - | 数字 |
到达最右端 | 1 2 3 + 4 ÷ + 5 - | 空 | s1中剩余的运算符 |
public class polandCal {
static Deque<String> S1 = new LinkedList<>(); // 符号栈
static Deque<String> S2 = new LinkedList<>(); // 数字栈 【由于只有加入的,其实可以用 list 代替】
public static void main(String[] args) {
// 先定义逆波兰表达式
String suffixExp = "4 5 * 8 - 60 + 8 2 / +";
ArrayList<String> list = new ArrayList<>();
String[] s = suffixExp.split(" ");
ArrayList<String> strings1 = new ArrayList<>();
String str = "1 + ( ( 2 + 3 ) * 4 ) - 5";
String[] s2 = str.split(" ");
for (String s1 : s2) {
strings1.add(s1);
}
List<String> strings2 = inSuffixToSuffix(strings1);
System.out.println(strings2);
// 定义一个栈
Deque<Integer> stack = new LinkedList<>();
// 开始扫描
for (String s1 : strings2) {
char c = s1.charAt(0);
if (s1.matches("\\d+")){ // 正则表达式【匹配多位数(0个或者)】
stack.addFirst(Integer.parseInt(s1));
}else { // 符号
int num1 = stack.removeFirst();
int num2 = stack.removeFirst();
int i = calNum(num2, num1, c);
stack.addFirst(i);
}
}
System.out.println("res = " + stack.peekFirst());
}
public static boolean isOper(char c){
return c < '0' || c > '9';
}
// 计算方法
public static int calNum(int num1, int num2, int oper){
int res = 0;
switch (oper){
case '+':
res = num1 + num2;
break;
case '-':
res = num1 - num2;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num1 / num2;
break;
default:
break;
}
return res;
}
// 计算方法
public static int operPriority(String str){
String priority = "";
switch (str){
case "+", "-":
priority = "1";
break;
case "*", "/":
priority = "2";
break;
default:
break;
}
return Integer.parseInt(priority);
}
// 1 + ((2 + 3) * 4) - 5
// 中缀 ---> 后缀
public static List<String> inSuffixToSuffix(List<String> list){
for (String s : list) {
if (s.matches("\\d")){ // 数字
S2.addFirst(s);
}else if (s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")){ // 运算符
if (S1.isEmpty() || S1.peekFirst().equals("(")){ // 观察 S1 是否为空
S1.addFirst(s);
}else { // + - * /
if (operPriority(s) > operPriority(S1.peekFirst())){
S1.addFirst(s);
}else{ // s 优先级 <= 栈顶
while (!S1.isEmpty() && operPriority(s) > operPriority(S1.peekFirst())){
S2.addFirst(S1.removeFirst());
}
S1.addFirst(s);
}
}
}else if (s.equals("(") || s.equals(")")){ // 左右括号
if (s.equals("(")){ // 左括号
S1.addFirst(s);
}else { // 右括号
while (!S1.peekFirst().equals("(")){
S2.addFirst(S1.removeFirst());
}
S1.removeFirst(); // 将左括号弹出
}
}
}
// 扫描完毕之后,将 S1 余下的部分放入到 S1 中
for (String s : S1) {
S2.addFirst(s);
}
List<String> list2 = new ArrayList<>();
while (!S2.isEmpty()){
list2.add(S2.removeFirst());
}
Collections.reverse(list2);
return list2;
}
}
5. 递归(Recursion)
- 自己调用自己
- 每次调用传入不同的变量
递归有助于解决复杂问题
阶乘:
public class factorial {
public static void main(String[] args) {
System.out.println(jieCheng(5));
}
public static int jieCheng(int n){
if (n == 1){
return 1;
}else {
return jieCheng(n - 1) * n;
}
}
}
5.1 递归调用规则
- 当程序执行到一个方法时,就会开辟一个独立的空间(栈)
- 每个空间的数据(局部变量),是独立的
5.2 递归用于解决什么问题
- 数学问题:
- 8 皇后
- 汉诺塔
- 阶乘
- 迷宫
- 球和篮子
- 各种算法中也会用到递归
- 快排
- 归并排序
- 二分查找
- 分治算法
- 利用栈解决的问题 ---> 递归代码比较简洁
5.3 递归遵守重要规则
- 执行一个方法时,就创建 1 个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响
- 如果方法中使用的是引用类型变量(比如:数组),就会共享该引用类型的数据
- 递归必须面退出递归的条件逼近,否则就是无限递归,死龟了:)
- 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
5.4 迷宫回溯
使用递归回溯来给小球找路
当 map[i][j] 的值为:
- 0:没有走过
- 1:墙体
- 2:通过可以走
- 3:该点已经走过,但是走不通
走迷宫时候,确定策略
-
下 ---> 右 ---> 上 ---> 左
public class labyrinth { public static void main(String[] args) { int[][] map = new int[8][7]; for (int i = 0; i < 7; i++) { map[0][i] = 1; map[7][i] = 1; } for (int i = 0; i < 8; i++) { map[i][0] = 1; map[i][6] = 1; } // 挡板位置 map[3][1] = 1; map[3][2] = 1; setWay(map, 1, 1); printMap(map); } public static boolean setWay(int[][] map, int i, int j){ if (map[6][5] == 2){ return true; }else { // base case; 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; } } } return false; } public static void printMap(int[][] arr){ for (int i = 0; i < arr.length; i++) { for (int j = 0; j < arr[i].length; j++) { System.out.print(arr[i][j] + " "); } System.out.println(); } } }
可以看到,这种走法没有体现回溯,我们构造一种回溯情景
// 构建回溯 map[1][2] = 1; map[2][2] = 1;
分析:只要点的值不是0,就免谈,根本走不到这个点上!!!
-
小球路径,和设计的路径有关系!!!---> 策略改为上右下左
public static boolean setWay(int[][] map, int i, int j){ if (map[6][5] == 2){ return true; }else { // base case; if (map[i][j] == 0) { // 利用墙体来防止越界,并且如果走过了设定为2 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; } } } return false; }
5.5 8 皇后(回溯)\(O(n!)\)
如果在 (i,j) 位置上放置了一个皇后,那么以下几种情况下都不能放置了
- 第 i 行 (同行) 【逐行放置,这条规则就被规避掉了!!!】
- 第 j 列 (同列)
- |a - i| = |b - j|;(a, b)指的是某个位置!!!(对角线)
接下来使用一个一维数组 new int[] record ---> record[i]:表示:第 i 行皇后所在列数
// 皇后位置
[i, record[i]]
// 说明:理论上应该创建一个二维数组来表示棋盘,但是实际,上可以通过算法,用一个一维数组即可解决问题
// arr[8] = {0, 4, 7, 5, 2, 6, 1,3}
// 对应arr下标表示第几行,即第几个皇后
// arr[i] = val, val表示
// 第i+1个皇后,放在第i+ 1行的 第val+ 1列
public class MyQueen {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入皇后的数目:");
String next = scanner.next();
int num = Integer.parseInt(next);
System.out.println(num + " 皇后共有 " + Queen(num) + " 种排列方法");
}
public static int Queen(int n) {
if (n < 1) {
return 0;
}
int[] record = new int[n];
return process(0, record, n); // 从第一行开始添加
}
public static int process(int i, int[] record, int n) { // 执行递归
if (i == n) { // Base Case 【n 从0 开始】---> 所有皇后已经放置完毕
for (int k = 0; k < record.length; k++) {
System.out.print(record[k] + "\t");
}
System.out.println();
return 1; // 8行都摆满了,排列次数 + 1
}
// 每调用一次process,就会产生一个新的栈
int res = 0;
for (int j = 0; j < n; j++) {
if (isValid(record, i, j)) { // 判断当前点是否可以放入
record[i] = j;
res += process(i + 1, record, n); // 当前行可以的话,就去判断下一行了
}
}
return res;
}
// (i, j) ---> 待放入的位置
// (k, record[k] ---> 已经放入棋盘的皇后
public static boolean isValid(int[] record, int i, int j) {
// 当考虑第 i 行,第 j 列的位置时
// 说明前面的行都已经放置了皇后了【放置的列为:record[0 - (i - 1)]】
// 第一行不判断,肯定可以放进去
for (int k = 0; k < i; k++) {
if (j == record[k] || Math.abs(k - i) == Math.abs(record[k] - j)) {
return false;
}
}
return true;
}
}
回溯思路:
思想:
- 当前行所有列都试过之后,不行的话,要返回上一列可行的地方继续走,依次类推
从第二行开始,每一行不管找没找到,都得遍历到最后为止
对于 res 的理解:
以第一行为基准行,第一行不进行判断,肯定可以放进去,共有 4种可能:
- (i = 0, j = 0)
- (i = 0, j = 1)
- (i = 0, j = 2)
- (i = 0, j = 3)
i 的范围是 0 - 3,当 i 到达 4 时,越界(base case),说明所有皇后都已经放好
-
return 1;【完成一次排列】
-
将这4种情况下,可以将皇后都放置【n == 4】的次数都加起来就好了
6. 排序算法
6.1 排序分类
1. 内部排序
将需要处理的所有数据都加载到内存中进行排序
2. 外部排序
数据量过大,无法全部加载到内存中,需要协助外部存储(文件等)进行排序
3. 常见的排序分类
6.2 算法时间复杂度
1. 时间频度
一个算法花费的时间与算法中语句的执行次数成正比,一个算法中的语句执行次数称为语句频度或时间频度,记为:\(T(n)\)
- 常数项忽略
- 低次项忽略
- 系数可以忽略
常见的时间复杂度:
常见的算法的时间复杂度由小到大依次为:
\(O(1)< O(log_{2}n)<O(n)<O(nlog_{2}n)<O(n^{k})<O(2^{n})\)
随着问题规模 n 的不断增大,上述事件复杂度不断增大,算法执行效率越低
对数阶:\(log_{2}n\)
// log2(1024)
int i = 1;
while(i < n){
i = i * 2;
}
线性对数阶:\(nlog_{2}n\)
- 将时间复杂度为:\(log_{2}n\) 的代码执行了 N 遍
2. 平均、最坏事件复杂度
徘序法| | 平均时间 | 最差 情形 | 稳定度 | 额外空间 | 备注 |
---|---|---|---|---|---|
冒泡 | \(O(n^{2})\) | \(O(n^{2})\) | 稳定 | \(O(1)\) | n小时较好 |
交换 | \(O(n^{2})\) | \(O(n^{2})\) | 不稳定 | \(O(1)\) | n小时较好 |
选择 | \(O(n^{2})\) | \(O(n^{2})\) | 不稳定 | \(O(1)\) | n小时较好 |
插入 | \(O(n^{2})\) | \(O(n^{2})\) | 稳定 | \(O(1)\) | 大部分已排序时较好 |
基数【桶排序】 | \(O(log_{R}B)\) | \(O(log_{R}B)\) | 稳定 | \(O(n)\) | B是真数(0-9), R是基数(个十百) |
Shell | \(O(nlogn)\) | \(O(n^{s})\) 1<s<2 | 不稳定 | \(O(1)\) | s是所选分组 |
快速 | \(O(nlogn)\) | \(O(n^{2})\) | 不稳定 | \(O(logn)\) | n大时较好 |
归并 | \(O(nlogn)\) | \(O(nlogn)\) | 稳定 | \(O(n)\) | n大时较好 |
堆 | \(O(nlogn)\) | \(O(nlogn)\) | 不稳定 | \(O(1)\) | n大时较好 |
3. 空间复杂度
一个算法在运行过程中临时占用存储空间大小的量度
- 有的算法需要占用的临时工作单元数与解决问题的规模 n 有关
- 它随着 n 的增大而增大,当 n 较大时,将占用较多的存储单元
- 例如快速排序,和归并排序算法就属于这种情况
从用户体验看,更看重的是程序执行的速度,一些缓存产品(redis、memcache)和算法(基数排序)本质就是用 空间换时间
6.3 冒泡(Bubble)
public class Bubble {
public static void main(String[] args) {
int[] arr = {2, 32, 231, 232, 2321};
BubbleSort(arr);
printArr(arr);
}
public static void BubbleSort(int[] arr){
for (int i = arr.length - 1; i > 0; i--) { // 每次循环都将最大的值放在最后!!!【尾部指针】
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]){
swap(arr, j, j + 1);
}
}
}
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void swap(int[] arr, int i, int j){
arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];
}
}
如果我们发现在某趟排序中,一次交换也没有,可以进行 **优化 **!!!---> 提前结束冒泡排序
static boolean flag = false;
public static void BubbleSort(int[] arr){
for (int i = arr.length - 1; i > 0; i--) { // 每次循环都将最大的值放在最后!!!【尾部指针】
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]){
swap(arr, j, j + 1);
flag = true; // 只要进行了操作,就说明进行了排序
}
}
if (!flag){ // flag为false:说明这趟排序中,没有进行 swap,说明已经排好序了,提前结束冒泡!!!
return;
}
flag = false; // 重置 flag 为false,用于下一趟排序判断!!!
}
}
测试时间代码:
// 测试 80000个数据,进行排序!!!
// 9739 ms
int[] test = new int[80000];
for (int i = 0; i < 80000; i++) {
test[i] = (int) (Math.random() * 80000);
}
long start = System.currentTimeMillis();
BubbleSort(test);
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
6.4 选择(Select)
思路:【每一轮只会交换一次】
- 第一次从 arr[0] ~ arr[n - 1] 中选取最小值,与 arr[0] 交换
- 第二次从 arr[1] ~ arr[n - 1] 中选取最小值,与 arr[1] 交换
- 以此类推
- 总共交换 n - 1次
代码思路:
- n - 1 轮排序
- 每一轮排序,又是一个循环
- 先假定当前值为最小值
- 然后和后面的值进行比较
- 如果发现有比 min 小的值,则重新确定 min,并得到下标
- 当遍历到数组最后时,就得到 min 和 下标
- swap
public class Select {
static int min_index = 0; // 记录最小值的下标
public static void main(String[] args) {
int[] arr = {2, 32, 231, 232, 2321, 88, 343, 22, 1, 34};
selectSort(arr);
printArr(arr);
int[] test = new int[80000];
for (int i = 0; i < 80000; i++) {
test[i] = (int) (Math.random() * 80000);
}
long start = System.currentTimeMillis();
selectSort(test);
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
}
public static void selectSort(int[] arr){
// n - 1 轮排序 【(n - 2)- 0 + 1 = n - 1】
for (int i = 0; i < arr.length - 1; i++) {
arr[min_index] = arr[i]; // 记录最小值下标
for (int j = i + 1; j < arr.length; j++) { // j:最小值的下标
if (arr[min_index] > arr[j]){
min_index = j;
}
}
swap(arr, i, min_index); // 如果 min_index 就是 i 的话(假设成功,就不用交换了)
}
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void swap(int[] arr, int i, int j){
arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];
}
}
// 优化
if (min_index != i) {
swap(arr, i, min_index);
}
注意:选择排序不是稳定的
比如:
6、7、6、2、8 ---> 第一次交换时,就破坏了稳定性
6.5 插入(Insert)--- 扑克牌
将 n 个待排序的元素看作:
- 一个有序表
- 一个无序表
- 开始时,有序表中只有一个元素,无序表中有 n - 1 个元素
- 排序过程中,每次从无序表中取出第一个元素,放入到有序表中,使其成为新的有序表
public class Insert {
static int insertValue = 0;
static int insert_index = 0;
public static void main(String[] args) {
int[] arr = {2, 32, 231, 232, 2321, 88, 343, 22, 1, 34, -1 , -10};
InsertSort(arr);
printArr(arr);
int[] test = new int[80000];
for (int i = 0; i < 80000; i++) {
test[i] = (int) (Math.random() * 80000);
}
long start = System.currentTimeMillis();
InsertSort(test);
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
}
public static void InsertSort(int[] arr){
// 第一个数不用比较,所以一共进行 n - 1轮 【(n - 1) - 1 + 1 = n - 1】
for (int i = 1; i < arr.length; i++) { // 无序表的第一个元素
for (int j = 0; j <= i - 1; j++) { // 有序表
if (arr[i] < arr[j]){
insertValue = arr[i];
insert_index = j; // 待交换位置
for (int k = i; k > j ; k--) {
arr[k] = arr[k - 1]; // 1. 后移
}
arr[insert_index] = insertValue; // 2. 插入
}
}
}
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
6.6 希尔排序(Shell):分组实现
引出:
简单插入算法可能存在的问题:
数组 [2, 3, 4, 5, 6],此时要插入1 的话,后移次数明显增多,对效率有影响
我们考虑算法时,要考虑它最差的情况
Donal Shell 于 1959 年提出的一种排序算法,优化的插入排序,缩小增量排序
基本思想:
- 把记录按下标的一定增量 分组
- 对每组使用直接插入算法排序
- 随着增量逐渐减少
交换式实现
for (int i = increase; i < arr.length; i++) {
for (int j = i - increase; j >= 0; j -= increase) { // 有序表
if (arr[j] > arr[j + increase]){
swap(arr, j, j + increase);
}
}
}
移位式实现(⭐)
public static void InsertSort(int[] arr){
int gap = arr.length / 2;
while (gap != 0){
// 从第increase 个元素进行直接插入
// 第一个数默认有序
for (int i = gap; i < arr.length; i++) { // 无序列表的第一个
insert_index = i;
insertValue = arr[i]; // 待插入的值
if (arr[i] < arr[i - gap]){
while (insert_index - gap >= 0 && insertValue < arr[insert_index - gap]){
// 移动
arr[insert_index] = arr[insert_index- gap]; // 右移
insert_index -= gap; // 向前继续确定位置
}
// 当退出while循环后,就找到了插入的位置 insert_index
arr[insert_index] = insertValue;
}
}
gap /= 2;
}
}
关于 gap 的理解图示:
6.7 快速排序(Quick)--- partation 划分区域
做不到稳定
每次都要记录断点线的位置,断点这个空间一定省不掉,类似于二分(可能中间位置挑一点 \(O(logN)\)),最差\(O(n)\) ---> 拿 划分值 划分的过程
整个过程中,t1、t2、t3 这几个变量是可以复用的,所以深度决定了额外空间
经典快排
最后一个数作划分值
- 小于等于这个数的都放在左边(可以无序)
- 大于这个数的都放在右边
【该过程:时间复杂度:O(N),空间复杂度:O(1)】
思路:
- cur <= 划分值
- cur 与 <= 区的下一个数交换
- <= 区向右扩一个位置
- 直接往下走
public static int partition(int[] arr, int l ,int r){
int partittion_num = arr[r]; // 最后一个数作为划分
int less = l - 1; // 小于等于区域右边界
for (int i = l; i <= r; i++){
if(arr[i] <= partition_num){
// 1. cur 与 <=区的下一个位置交换
// 2. <= 区扩一位
swap(arr, i, ++less);
}
}
// 返回 <= 区域的最后一个位置
return less;
}
荷兰国旗(三种颜色)
分为3种情形:
- cur < 划分值
- 调整完之后,下标是跳的
- cur = 划分值
- 无任何操作,下标直接跳
- cur > 划分值
- 交换之后,下标不跳,继续判定!!!
public static int[] partition(int[] arr, int l ,int r){
int less = l - 1; // < 区
int more = r; // > 区(默认有一个值)---》最后再调整
while (l < more){
if(arr[l] < arr[r]){ // 利用 l 变量开始遍历
swap(arr, l++, ++less)
}else if(arr[l] > arr[r]){
swap(arr, l, --more) // cur > 划分值,swap后,当前数下标仍留在原地
}else{
l++;
}
}
// 最后处理下大于区域的最后一个数 more
swap(arr, more, r);
return new int[] {less + 1, more}; // 返回的是 =区 范围
}
随机快排(⭐:工程常用)
随机选择一个数,和最后一个数字交换,当做划分值。后续流程和经典快排一致 【而经典是每次都选择最后一个数进行划分】
public class Quick {
public static void main(String[] args) {
int[] arr = {2, 32, 231, 232, 2321, 11};
quickSort(arr, 0, arr.length - 1);
printArr(arr);
int[] test = new int[80000];
for (int i = 0; i < 80000; i++) {
test[i] = (int) (Math.random() * 80000);
}
long start = System.currentTimeMillis();
quickSort(test, 0, test.length - 1);
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
}
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
// 随机选择一个数,和最后一个数字交换,当做划分值。后续流程和经典快排一致
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
int[] p = partition(arr, l, r); // 中间的相等部分
quickSort(arr, l, p[0] - 1); // [l, 相等区间左侧前一个位置]
quickSort(arr, p[1] + 1, r); // [相等区间右侧后一个位置, r]
}
}
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l < more) { // 下标 < 大于区的边界
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
// 注意:这里不能用与的方法交换,因为一开始是自己和自己交换
// 2 个 相同的数 与的话,结果是 0
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
6.8 归并排序(merge)--- master 公式
package recursion;
public class getMax {
static int maxLeft = 0;
static int maxRight = 0;
static int mid = 0; // 中点
public static void main(String[] args) {
int[] arr = {2, 32, 231, 232, 2321};
System.out.println(max(arr, 0, arr.length - 1));
}
public static int max(int[] arr, int l, int r){
// base case
if (l == r){
return arr[l];
}
mid = l + (r - l) / 2;
maxLeft = max(arr, l, mid);
maxRight = max(arr, mid + 1, r);
return Math.max(maxLeft, maxRight);
}
}
public class merge {
public static void main(String[] args) {
int[] arr = {2, 32, 231, 232, 2321, 11};
mergeSort(arr, 0, arr.length - 1);
printArr(arr);
}
public static void mergeSort(int[] arr, int l, int r) {
// base case
if(l == r){
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r); // 左右都排好之后,统一外排
}
// 左右两边排好之后,左右各有一个指针,申请一个额外空间,合成一个整体有序的东西
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1]; // 额外空间
int i = 0;
int p1 = l;
int p2 = m + 1;
// 两边都有数的情况
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 右侧已经没了【这个和下面2个while,虽然是顺序结构,但是只会执行一个】
while (p1 <= m) {
help[i++] = arr[p1++];
}
// 左侧已经没了
while (p2 <= r) {
help[i++] = arr[p2++];
}
// 将排好序的数组 help 重新赋给原数组 arr
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
时间复杂度:\(T(n)=T(\frac{N}{2})+T(\frac{N}{2})+O(N)\)
master 公式:
\(T(n)=aT(\frac{n}{b})+O(N^{d})\)
所以,此时:b = 2,a = 2,d = 1,所以时间复杂度为:\(O(nlogn)\)
补充阅读:算法的复杂度与 Master 定理 · GoCalf Blog
应用:求数组小和(在 merge中计算)
在 merge 过程中,如果左侧值 < 右侧值,产生小和
- arr[index_left] * (r - index_right + 1)
- 对应每个数,都是看它的右侧有多少个数比它大
- 每次都是 组间产生小和,组内不产生小和
import java.util.Scanner;
public class Main {
static long sum = 0; // 防止越界
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
String size = in.nextLine();
String s = in.nextLine();
String[] s1 = s.split(" ");
int[] arr = new int[s1.length];
for (int i = 0; i < s1.length; i++) {
arr[i] = Integer.parseInt(s1[i]);
}
mergeSort(arr, 0, arr.length - 1);
System.out.println(sum);
}
public static void mergeSort(int[] arr, int l, int r) {
// base case
if(l == r){
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r); // 左右都排好之后,统一外排
}
// 左右两边排好之后,左右各有一个指针,申请一个额外空间,合成一个整体有序的东西
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1]; // 额外空间
int i = 0;
int p1 = l; // 左侧指针
int p2 = m + 1; // 右侧指针
// 两边都有数的情况
while (p1 <= m && p2 <= r) { // 组内没有小和,组间才有小和
if (arr[p1] <= arr[p2]){
sum += arr[p1] * (r - p2 + 1);
help[i++] = arr[p1++];
}else {
help[i++] = arr[p2++];
}
}
// 右侧已经没了【这个和下面2个while,虽然是顺序结构,但是只会执行一个】
while (p1 <= m) {
help[i++] = arr[p1++];
}
// 左侧已经没了
while (p2 <= r) {
help[i++] = arr[p2++];
}
// 将排好序的数组 help 重新赋给原数组 arr
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
}
应用:求降序对
整体类似,即:求出右边有多少个数 比当前小
6.9 堆排序(Heap)--- 完全二叉树
堆结构:完全二叉树结构
- 满二叉树,或通向满二叉树的路上
- 每一层节点都是从左向右依次填好
- 落地结构:数组
脑补结构如下:
生成规则:
- 当前节点 i
- 左孩子:2 i + 1
- 右孩子:2 i + 2
- 父节点:(i - 1) / 2
大根堆(单独用也很好用)
- 头部最大
- 左右无要求
构建方式:
- 拿到一个数,都和自己的父比较
- 如果能交换就交换了
- 来到新位置后,再看父节点,如果能交换则继续交换
- 当添加完最后一个数后,数组调完之后的样子就是大根堆
heapInsert【从下至上】\(O(N)\)
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
public static void heapInsert(int[] arr, int index) {
// 注意:-1 / 2 = 0
while (arr[index] > arr[(index - 1) / 2]) { // 插入的值 > 父节点【一直比较】
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2; // 更新下标
}
}
heapify【从上至下】调整成大根堆
- 将堆顶与最后一个交换
- 从上至下
- 比2个儿子小,与儿子中最大值交换
- 一直到没有儿子比自己大
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1; // 左孩子
while (left < heapSize) { // 左孩子不越界
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; // 右孩子不越界 [largest:孩子中最大值下标]
largest = arr[largest] > arr[index] ? largest : index; // 我 和 我 2个孩子中,最大值下标
if (largest == index) {
break; // 我就是最大的,无需交换了
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
堆排序整体代码
package heap;
public class bigHeap {
public static void main(String[] args) {
int[] arr = {5, 7, 0 , 6, 8};
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize); // 0 位置和最后一个位置交换,并且堆的大小 - 1
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
printArr(arr);
}
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1; // 左孩子
while (left < heapSize) { // 左孩子不越界
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; // 右孩子不越界 [largest:孩子中最大值下标]
largest = arr[largest] > arr[index] ? largest : index; // 我 和 我 2个孩子中,最大值下标
if (largest == index) {
break; // 我就是最大的,无需交换了
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
6.10 Arrays.sort 系统排序
- size < 60 :Insertion
- size > 60
- merge【非基础类型,自己定义的一个 class,根据比较器比较】---> 稳定性
- quick【int、double、char】
比较器
package heap;
import java.util.Arrays;
import java.util.Comparator;
public class Code_09_Comparator {
public static class Student {
public String name;
public int id;
public int age;
public Student(String name, int id, int age) {
this.name = name;
this.id = id;
this.age = age;
}
}
// 实现 Comparator 接口,并重写 compare 方法
public static class IdAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.id - o2.id;
}
}
public static class IdDescendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.id - o1.id;
}
}
public static class AgeAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
public static class AgeDescendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.age - o1.age;
}
}
public static void printStudents(Student[] students) {
for (Student student : students) {
System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age);
}
System.out.println("===========================");
}
public static void main(String[] args) {
Student student1 = new Student("A", 1, 23);
Student student2 = new Student("B", 2, 21);
Student student3 = new Student("C", 3, 22);
Student[] students = new Student[] { student3, student2, student1 };
printStudents(students);
Arrays.sort(students, new IdAscendingComparator()); // 利用比较器进行排序
printStudents(students);
Arrays.sort(students, new IdDescendingComparator());
printStudents(students);
Arrays.sort(students, new AgeAscendingComparator());
printStudents(students);
Arrays.sort(students, new AgeDescendingComparator());
printStudents(students);
}
}
6.11 桶排序(bucket)【之前所以的排序都是基于比较】
我要准备桶把东西放进来,再依次倒出!!!
计数排序(Counting)
统计 词频
// only for 0~200 value
public static void bucketSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
// 我准备一个数组,长度为 200 + 1,
int[] bucket = new int[max + 1];
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]]++; // 记录某一个数出现多少次(原始数组的值为 桶的下标)---》统计词频
}
int i = 0;
for (int j = 0; j < bucket.length; j++) {
while (bucket[j]-- > 0) {
arr[i++] = j; // 通过词频,再把 arr 拷贝回去
}
}
}
基数排序(Radix)--- 稳定
基本思想:
- 将所有待比较的数统一为同样的长度,数位较短的前面补0
- 从最低位开始,依次进行依次排序
- 这样从最低位排序,一直到最高位排序完成后,就得到一个 有序序列
public class Radix {
static int count = 0; // 次数取决于数组中位数最大鹅那个!!!
static int[][] bucket; // 桶(二维数组)
// 记录每个桶【10个桶】中,实际存放了多少个数据
// 可以这么理解:
// each_bucket_numIndex[0] 记录的就是 bucket[0] 桶放入数据个数
static int[] each_bucket_numIndex = new int[10]; // 每个桶内所放数据的指针
public static void main(String[] args) {
int[] arr = {53, 3, 542, 748, 14, 214, 78787};
for (int i = 0; i < arr.length; i++) {
int length = (arr[i] + "").length(); // 整数 ---> 字符串
if (count < length){
count = length;
}
}
BucketRadixSort(arr);
}
public static void BucketRadixSort(int[] arr){
bucket = new int[10][arr.length]; // 桶
for (int i = 0, n = 1; i < count; i++, n *= 10) { // 需要遍历的轮数
for (int j = 0; j < arr.length; j++) {
int bucket_number = arr[j] / n % 10;
// 根据算出的 bucket_number 放入桶中,所放桶中指针 + 1
bucket[bucket_number][each_bucket_numIndex[bucket_number]++] = arr[j];
}
popBucket(arr); // 弹出时,要清空 each_bucket_numIndex数组,即:重置每个桶内数据下标
printArr(arr);
System.out.println();
}
}
public static void popBucket(int[] arr){ // 遍历每一个桶,并将桶中数据,返回到 arr
int index = 0; // arr下标
for (int i = 0; i < 10; i++) {
if (each_bucket_numIndex[i] > 0){ // 第 i 号桶中有数据
for (int j = 0; j < each_bucket_numIndex[i]; j++) {
arr[index++] = bucket[i][j];
}
}
}
// 全部出桶后,将各个桶中的指针全部置为 0
for (int i = 0; i < 10; i++) {
each_bucket_numIndex[i] = 0;
}
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
堆排序注意事项
由于基数排序是以 空间换时间
假如要排 8000 0000 个数组
- 那么就需要 11【本身也算一个】 个 8000 0000 个大小的数组
- 11 * 8000 000 * 4(1个int 4 个字节)/ 1024 / 1024 = 3.3G
- 容易造成 OutOfMemoryError
负数情况
- 求绝对值
- 取出的时候要进行一个反转
排序算法| | 平均时间复茶度 | 泵好情况 | 最坏情况 | 空问复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | \(O(n^{2})\) | \(O(n)\) | \(O(n^{2})\) | \(O(1)\) | ln-place | 稳定 |
选择排序 | \(O(n^{2})\) | \(O(n^{2})\) | \(O(n^{2})\) | \(O(1)\) | ln-place | 不稳定 |
插入排序 | \(O(n^{2})\) | \(O(n)\) | \(O(n^{2})\) | \(O(1)\) | In-place | 稳定 |
希尔排序 | \(O(nlogn)\) | \(O(nlog^{2}n)\) | \(O(nlog^{2}n)\) | \(O(1)\) | ln-place | 不稳定 |
归并排序 | \(O(nlogn)\) | \(O(nlogn)\) | \(O(nlogn)\) | \(O(n)\) | Out-place | 稳定 |
快速排序 | \(O(nlogn)\) | \(O(nlogn)\) | \(O(n^{2})\) | \(O(logn)\) | In-place | 不稳定 |
维排序 | \(O(nlogn)\) | \(O(nlogn)\) | \(O(nlogn)\) | \(O(1)\) | ln-place | 不稳定 |
计数排序 | \(O(n + k)\) | \(O(n + k)\) | \(O(n + k)\) | \(O(k)\) | Out-place | 稳定 |
桶排序 | \(O(n + k)\) | \(O(n + k)\) | \(O(n^{2})\) | \(O(n+k)\) | Out-place | 稳定 |
基数排序 | \(O(n * k)\) | \(O(n * k)\) | \(O(n * k)\) | \(O(n+k)\) | Out-place | 稳定 |
In-place:不占用额外内存
Out-place:占用额外内存
n:数据规模
k:桶的个数
7. 查找算法
1. 线性查找
public class seqSearch {
public static void main(String[] args) {
int[] arr = {1, 9, 11, -1, 34, 89}; // 没有顺序的数组
int index = seqSearch(arr, 11);
if (index == -1){
System.out.println("没有找到");
}else {
System.out.println("找到了,下标为:" + index);
}
}
public static int seqSearch(int[] arr, int value){
for (int i = 0; i < arr.length; i++) {
// 逐一比对
if (arr[i] == value){
return i;
}
}
return -1;
}
}
2. 二分查找(前提:有序)
思路:
- 确定中点:mid = left + (right - left) / 2
- 比较:findVal 和 arr[mid]
- findVal < arr[mid]:递归向左,查找
- findVal > arr[mid]:递归向右,查找
- findVal == arr[mid],返回
什么时候结束递归:
- 找到就结束递归
- 递归完整个数组,仍然没有找到 findVal,也需要结束递归
package search;
public class binarySearch {
static int findVal = 100;
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1234};
int i = binarySearch_(arr, 0, 5);
if (i == -1){
System.out.println("没有找到!!!");
}else {
System.out.println("找到了,index=" + i);
}
}
public static int binarySearch_(int[] arr, int l, int r){
// 递归了整个数组,但是没有找到!!!
if (l > r){
return -1;
}
int mid = l + (r - l) / 2;
if (findVal < arr[mid]){ // 搜索是为了查找具体的数,所以递归时不要 mid了
return binarySearch_(arr, l, mid - 1);
}else if (findVal > arr[mid]){
return binarySearch_(arr, mid + 1, r);
}else {
return mid;
}
}
}
改进版本:找出数组中的所有相同的值
- 找到 mid 后,不着急返回
- 向左扫描
- 向右扫描
- 放入一个 arrayList中
package search;
public class binarySearch {
static int findVal = 1000;
static ArrayList<Integer> list = new ArrayList<>();
public static void main(String[] args) {
int[] arr = {1, 8, 10, 89, 1000, 1000, 1000, 1000, 1234};
ArrayList<Integer> list = binarySearch_(arr, 0, arr.length - 1);
if (list == null){
System.out.println("没有找到");
} else {
System.out.println("找到了,list=" + list);
}
}
public static ArrayList<Integer> binarySearch_(int[] arr, int l, int r){
// 递归了整个数组,但是没有找到!!!
if (l > r){
return null;
}
int mid = l + (r - l) / 2;
if (findVal < arr[mid]){ // 搜索是为了查找具体的数,所以递归时不要 mid了
return binarySearch_(arr, l, mid - 1);
}else if (findVal > arr[mid]){
return binarySearch_(arr, mid + 1, r);
}else {
// 向左扫描
int temp1 = mid - 1;
while (temp1 >= 0){ // 向左扫描
if (arr[temp1] != findVal){
break;
}
list.add(temp1);
temp1--;
}
list.add(mid); // mid
int temp2 = mid + 1;
while (temp2 < arr.length){ // 向右扫描
if (arr[temp2] != findVal){
break;
}
list.add(temp2);
temp2++;
}
return list;
}
}
}
3. 插值查找(分布均匀)
原理:
-
类似二分
-
不同的是:每次从自适应 mid 处开始查找
-
修改 二分查找中求 mid 的公式:
\[mid=l+\frac{1}{2}(r-l) \]修改为如下公式:
\[mid=l+\frac{key-a[l]}{a[r]-a[l]}(r-l) \]
在插值算法中修改 求 mid 的代码即可:
// mid:自适应!!!
int mid = l + (findVal - arr[l]) / (arr[r] - arr[l]) * (r - l);
4. 斐波那契(Fibonacci):黄巾分割法 0.618
一个线段分成 2 份
- 其中一部分与全长之比
- 等于另一部分与这部分之比
公式推导:
由于斐波那契公式可知:\(F(k)=F(k-1)+F(k-2)\)
- 2侧 同时减去 1,\(F(k)-1=F(k-1)+F(k-2)-1\)
- 右侧继续处理:\(F(k)-1=[F(k-1)-1]+[F(k-2)-1]+1\)
- 可以看作是
- 顺序表的长度为为:\(F(k)-1\),划分成3块,mid坐标为:low + 【F(k-1) - 1】
- 左侧:\(F(k-1)-1\)
- mid:1
- 右侧:\(F(k-2)+1\)
- 顺序表的长度为为:\(F(k)-1\),划分成3块,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++; }