HSP数据结构

HSP

1. 线性结构和非线性结构

线性

  • 顺序存储结构
  • 链式存储结构

非线性

  • 二维数组
  • 多维数组
  • 广义表
  • 树结构
  • 图结构

2. 稀疏数组和队列

2.1 稀疏数组(sparseArr)

当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方法是:

  • 记录数组一共有几行几列,有多少个不同的值
  • 把具有不同值的元素的行列及值记录在一个 小规模的数组 中,从而缩小程序的规模

image-20221230234528477

image-20221230235159411

2.2 应用实例

  1. 使用稀疏数组,保留二维数组
  2. 将稀疏数组存盘,并且可以重新恢复原来的二维数组

image-20221231000213759

二维数组转成稀疏数组思路:

  • 遍历原始二维数组,得到有效数据的个数 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();
        }
    }
}

课后练习:

  1. 在前面基础上,将稀疏数组保存到磁盘上,比如:map.data
  2. 恢复原来的数组时,读取 map.data 进行恢复

2.3 队列(普通)

有序列表

  • 可以用数组或链表实现
  • 先入先出

数组模拟:

image-20221231101312425

  • 入队:rear++
  • 出队:front++

当我们将数据存入队列时称为 ”addQueue“,addQueue的处理需要有2个步骤:

  1. 将 rear++,当 front == rear 【空】
  2. 若 rear < maxSize,则将数据存入 rear 所指的数组元素中,否则无法存入数据
  3. 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 队列(环形)--- 取模方式实现

思路如下:

  1. front 变量的含义做一个调整:front 就指向队列的第一个元素,也就是 arr[front] 就是队列的 第一个元素

    • front 初始值:0
  2. rear 变量的含义也做一个调整:rear 指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定

    • rear 的初始值:0
  3. 当队列满时,条件是(rear+1)% maxsize == front【满】

  4. 当队列为空的条件,rear == front

  5. 当我们这样分析,队列中有效的数据的个数 (rear+maxsizefront)%maxsize

    312e命名绘图

  6. 我们就可以在原来的队列上修改得到一个环形队列

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)介绍

链表是有序的列表,在内存中存储如下:

image-20230103095919495

小结:

  • 链表是以节点的方式来存储,是链式
  • 每个节点包含 data 域、next 域:指向下一个节点
  • 如图:发现链表的各个节点不一定是连续存放
  • 链表分带头节点的链表和没有头节点的链表,根据实际需求来确

3.1 单链表

单链表(带头结点)逻辑结构示意图如下:

image-20230103100532963

3.2 单链表应用实例

1. 普通添加

单链表的创建示意图(添加),显示单向链表的分析

231未命名绘图

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. 按顺序添加

image_0.06788895745497969

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. 单链表节点的删除和小结

image_0.7012214720823009

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. 面试题

  1. 有效节点个数

    public int size(){
        int count = 0;
        HeroNode cur = head.next;
        while (cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }
    
  2. 倒数第 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;
    }
    
  3. 单链表的反转

    image_0.5706003331816261

    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;
    }
    
  4. 从头到尾打印单链表

    • 反向遍历(先反转,再打印)

      • 会破坏原来单链表的结构
    • 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());
          }
      }
      
  5. 合并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是待删除节点的前一个结点

双向链表分析:

  • 遍历、添加、修改12342

  • 删除(自我删除)

    image_0.8755135939787551

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(1kn) 的人从 1 开始报数,数到 m 的那个人又出列
  • 以此类推,直到所有人都出列为止
  • 由此产生一个出队编号的序列

实现方式:

  1. 不带头节点的循环链表来处理 Josephu 问题
  2. 先构成一个有 n 个节点的单循环链表
  3. 然后从 k 节点起从 1 开始计数,计到 m 时,对应节点从链表中删除
  4. 然后再从被删除节点的下一个节点又从 1 开始计数
  5. 直到最后一个节点从链表中删除

1. 单向循环链表

  • n = 5
  • k = 1(从第一个人开始报数)
  • m = 2

image_0.29316039409350214

根据用户输入,生成一个小孩出圈的顺序:

  1. 需要创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点

    c1

  2. 当小孩报数时,让 first 和 helper 指针同时移动 m - 1次

    c2

  3. 这时就可以将 first 指向的小孩节点出圈

    • first = first.next
    • helper.next = first
  4. 这样,原来 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. 构建一个单向的环形链表思路:

  1. 先创建第一个节点,让 first 指向该节点,并形成环形
  2. 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可

image_0.6825705918717684

遍历环形链表

  1. 先让一个辅助指针(变量)curBoy,指向 first 节点
  2. 然后通过一个 while 循环遍历该环形链表即可
  3. 结束条件 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 栈的应用场景

  1. 子程序调用
  2. 处理递归调用
  3. 表达式的转换【中缀表达式转后缀表达式】与 求值
  4. 二叉树遍历
  5. 图形的深度优先【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 栈实现综合计算器【中缀】

no1

image_0.9485540751725847

代码实现:(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【前缀】

image_0.8885328762070734

中缀(一般会转成后缀表达式)

  • 日常写法
    • 需要判断符号优先级
  • 对计算机来说不好操作

后缀 (次顶 - 顶)

  • 又称为逆波兰表达式,与前缀类似,只是符号在数字之后
  • 比如:(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 - 【后缀】

image_0.7283163702907425

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 中缀 ---> 后缀

  • 后缀表达式适合计算机运算
  • 中缀符合人类常识

具体实现步骤:

  1. 初始化2个栈:
    • S1:运算符栈
    • S2:中间结果
  2. 从左到右扫描中缀表达式
    • 遇到数字:压入 S2
    • 遇到符号:观察 S1
      • S1 为空,或 peek() 为 "(",则直接将此运算符入栈
      • S1 不为空,
        • 比栈顶符号的优先级高,压入 S1
        • 比栈顶符号的优先级低或者相同,弹出 peek() 并压入到 s2中。继续与新的栈顶进行比较
    • 遇到括号时:
      • "(",直接压入 S1
      • ")":依次弹出 S1 符号,并压入 S2,直到遇到 左括号位置,此时将这一对括号丢弃
  3. 扫描完毕后,将 S1 中剩余运算符依次弹出i并压入 S2
  4. 依次弹出 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 递归调用规则

  1. 当程序执行到一个方法时,就会开辟一个独立的空间(栈)
  2. 每个空间的数据(局部变量),是独立的

5.2 递归用于解决什么问题

  1. 数学问题:
    • 8 皇后
    • 汉诺塔
    • 阶乘
    • 迷宫
    • 球和篮子
  2. 各种算法中也会用到递归
    • 快排
    • 归并排序
    • 二分查找
    • 分治算法
  3. 利用栈解决的问题 ---> 递归代码比较简洁

5.3 递归遵守重要规则

  1. 执行一个方法时,就创建 1 个新的受保护的独立空间(栈空间)
  2. 方法的局部变量是独立的,不会相互影响
  3. 如果方法中使用的是引用类型变量(比如:数组),就会共享该引用类型的数据
  4. 递归必须面退出递归的条件逼近,否则就是无限递归,死龟了:)
  5. 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

5.4 迷宫回溯

使用递归回溯来给小球找路

当 map[i][j] 的值为:

  • 0:没有走过
  • 1:墙体
  • 2:通过可以走
  • 3:该点已经走过,但是走不通

走迷宫时候,确定策略

  1. 下 ---> 右 ---> 上 ---> 左

    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();
            }
        }
    }
    

    image-20230128162655573

    可以看到,这种走法没有体现回溯,我们构造一种回溯情景

    //  构建回溯
    map[1][2] = 1;
    map[2][2] = 1;
    

    image-20230128162859934

    分析:只要点的值不是0,就免谈,根本走不到这个点上!!!

    image_0.9170755621480102

  2. 小球路径,和设计的路径有关系!!!---> 策略改为上右下左

    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;
    }
    

    image-20230128170104229

5.5 8 皇后(回溯)O(n!)

如果在 (i,j) 位置上放置了一个皇后,那么以下几种情况下都不能放置了

  1. 第 i 行 (同行) 【逐行放置,这条规则就被规避掉了!!!】
  2. 第 j 列 (同列)
  3. |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;
    }
}

回溯思路:

思想:

  • 当前行所有列都试过之后,不行的话,要返回上一列可行的地方继续走,依次类推

3213213232133213未命名绘图

从第二行开始,每一行不管找没找到,都得遍历到最后为止

未321命名绘图

未命名绘图7789

未命名绘图32

对于 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. 常见的排序分类

33231

6.2 算法时间复杂度

1. 时间频度

一个算法花费的时间与算法中语句的执行次数成正比,一个算法中的语句执行次数称为语句频度或时间频度,记为:T(n)

  • 常数项忽略
  • 低次项忽略
  • 系数可以忽略

常见的时间复杂度:

1024px-Comparison_computational_complexity.svg

常见的算法的时间复杂度由小到大依次为:

O(1)<O(log2n)<O(n)<O(nlog2n)<O(nk)<O(2n)

随着问题规模 n 的不断增大,上述事件复杂度不断增大,算法执行效率越低

对数阶:log2n

//	log2(1024)
int i = 1;
while(i < n){
    i = i * 2;
}

线性对数阶:nlog2n

  • 将时间复杂度为:log2n 的代码执行了 N 遍

2. 平均、最坏事件复杂度

徘序法| 平均时间 最差 情形 稳定度 额外空间 备注
冒泡 O(n2) O(n2) 稳定 O(1) n小时较好
交换 O(n2) O(n2) 不稳定 O(1) n小时较好
选择 O(n2) O(n2) 不稳定 O(1) n小时较好
插入 O(n2) O(n2) 稳定 O(1) 大部分已排序时较好
基数【桶排序】 O(logRB) O(logRB) 稳定 O(n) B是真数(0-9),
R是基数(个十百)
Shell O(nlogn) O(ns) 1<s<2 不稳定 O(1) s是所选分组
快速 O(nlogn) O(n2) 不稳定 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)

思路:【每一轮只会交换一次】

  1. 第一次从 arr[0] ~ arr[n - 1] 中选取最小值,与 arr[0] 交换
  2. 第二次从 arr[1] ~ arr[n - 1] 中选取最小值,与 arr[1] 交换
  3. 以此类推
  4. 总共交换 n - 1次

jjiijj

代码思路:

  • 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 个元素
    • 排序过程中,每次从无序表中取出第一个元素,放入到有序表中,使其成为新的有序表

GHR

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 年提出的一种排序算法,优化的插入排序,缩小增量排序

基本思想:

  • 把记录按下标的一定增量 分组
  • 对每组使用直接插入算法排序
  • 随着增量逐渐减少

shell

交换式实现

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 的理解图示:

332211

6.7 快速排序(Quick)--- partation 划分区域

做不到稳定

每次都要记录断点线的位置,断点这个空间一定省不掉,类似于二分(可能中间位置挑一点 O(logN)),最差O(n) ---> 拿 划分值 划分的过程

223311

整个过程中,t1、t2、t3 这几个变量是可以复用的,所以深度决定了额外空间

经典快排

最后一个数作划分值

  • 小于等于这个数的都放在左边(可以无序)
  • 大于这个数的都放在右边

【该过程:时间复杂度:O(N),空间复杂度:O(1)】

aer

思路:

  • 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;
}

荷兰国旗(三种颜色)

Flag_of_the_Netherlands.svg

分为3种情形:

  1. cur < 划分值
    • 调整完之后,下标是跳的
  2. cur = 划分值
    • 无任何操作,下标直接跳
  3. cur > 划分值
    • 交换之后,下标不跳,继续判定!!!

qqeeww

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}; //	返回的是 =区 范围
}

随机快排(⭐:工程常用)

随机选择一个数,和最后一个数字交换,当做划分值。后续流程和经典快排一致 【而经典是每次都选择最后一个数进行划分】

image-20230201153014105

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] + " ");
        }
    }
}

image-20230201160308144

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(N2)+T(N2)+O(N)

master 公式:

T(n)=aT(nb)+O(Nd)

{logba>dO(Nlogba)d>logbaO(Nd)d=logbaO(NdlogN)

所以,此时: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)--- 完全二叉树

堆结构:完全二叉树结构

  • 满二叉树,或通向满二叉树的路上
  • 每一层节点都是从左向右依次填好
  • 落地结构:数组

image-20230213162926723

脑补结构如下:

image-20230213163141629

生成规则:

  • 当前节点 i
    • 左孩子:2 i + 1
    • 右孩子:2 i + 2
    • 父节点:(i - 1) / 2

大根堆(单独用也很好用)

  • 头部最大
  • 左右无要求

image-20230213163455648

构建方式:

  1. 拿到一个数,都和自己的父比较
  2. 如果能交换就交换了
  3. 来到新位置后,再看父节点,如果能交换则继续交换
  4. 当添加完最后一个数后,数组调完之后的样子就是大根堆

223311

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)--- 稳定

A4

基本思想:

  1. 将所有待比较的数统一为同样的长度,数位较短的前面补0
  2. 从最低位开始,依次进行依次排序
  3. 这样从最低位排序,一直到最高位排序完成后,就得到一个 有序序列
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

负数情况

  1. 求绝对值
  2. 取出的时候要进行一个反转
排序算法| 平均时间复茶度 泵好情况 最坏情况 空问复杂度 排序方式 稳定性
冒泡排序 O(n2) O(n) O(n2) O(1) ln-place 稳定
选择排序 O(n2) O(n2) O(n2) O(1) ln-place 不稳定
插入排序 O(n2) O(n) O(n2) O(1) In-place 稳定
希尔排序 O(nlogn) O(nlog2n) O(nlog2n) O(1) ln-place 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) Out-place 稳定
快速排序 O(nlogn) O(nlogn) O(n2) 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(n2) O(n+k) Out-place 稳定
基数排序 O(nk) O(nk) O(nk) 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. 二分查找(前提:有序)

思路:

  1. 确定中点:mid = left + (right - left) / 2
  2. 比较:findVal 和 arr[mid]
    • findVal < arr[mid]:递归向左,查找
    • findVal > arr[mid]:递归向右,查找
  3. 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. 插值查找(分布均匀)

原理:

  1. 类似二分

  2. 不同的是:每次从自适应 mid 处开始查找

  3. 修改 二分查找中求 mid 的公式:

    mid=l+12(rl)

    修改为如下公式:

    mid=l+keya[l]a[r]a[l](rl)

在插值算法中修改 求 mid 的代码即可:

//	mid:自适应!!!
int mid = l + (findVal - arr[l]) / (arr[r] - arr[l]) * (r - l); 

4. 斐波那契(Fibonacci):黄巾分割法 0.618

一个线段分成 2 份

  • 其中一部分与全长之比
  • 等于另一部分与这部分之比

mid=low+F(k1)1

公式推导:

由于斐波那契公式可知:F(k)=F(k1)+F(k2)

  • 2侧 同时减去 1,F(k)1=F(k1)+F(k2)1
  • 右侧继续处理:F(k)1=[F(k1)1]+[F(k2)1]+1
  • 可以看作是
    • 顺序表的长度为为:F(k)1,划分成3块,mid坐标为:low + 【F(k-1) - 1】
      • 左侧:F(k1)1
      • mid:1
      • 右侧:F(k2)+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++;
    }
    
posted @   爱新觉罗LQ  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示