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+maxsize-front)\%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\((1\leqslant k\leqslant n)\) 的人从 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(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)

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

  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(\frac{N}{2})+T(\frac{N}{2})+O(N)\)

master 公式:

\(T(n)=aT(\frac{n}{b})+O(N^{d})\)

\[\left\{\begin{matrix} logb^{a}>d &O(Nlogb^{a} )& \\ d>logb^{a}& O(N^{d})& \\ d=logb^{a}& O(N^{d}logN) &\\ \end{matrix}\right. \]

所以,此时: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(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. 二分查找(前提:有序)

思路:

  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+\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 份

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

\[mid=low+F(k-1)-1 \]

公式推导:

由于斐波那契公式可知:\(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\)

但是顺序表长度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 @ 2023-03-03 19:38  爱新觉罗LQ  阅读(47)  评论(0编辑  收藏  举报