Java数据结构

本文详细介绍了Java中的数据结构,包括稀疏数组、队列(数组和环形队列)、单链表(增删查及面试题)、双向链表、单向环形链表、栈(数组和链表实现)、递归调用机制、哈希表和树结构(如二叉树、哈夫曼树、二叉排序树和平衡二叉树)。内容覆盖了各种数据结构的定义、操作和应用,适合Java开发者学习和参考。

数据结构

数据结构包括:线性结构和非线性结构

线性结构

  1. 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
  2. 线性结构有两种不同的存储结构,即顺序存储结构(数组)链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的
  3. 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
  4. 线性结构常见的有:数组、队列、链表和栈

非线性结构

非线性结构包括:二维数组、多维数组、广义表、树结构、图结构

稀疏数组

基本介绍

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

稀疏数组的处理方法是:

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

image-20220816143943553

image-20220816145338931

package com.yhs.sparsearray;

import java.io.*;

public class SparseArray {
    public static void main(String[] args) throws IOException {
        //创建一个二维数组 11 * 11
        int[][] chessArr1 = new int[11][11];
        chessArr1[1][2] = 1;
        chessArr1[2][3] = 2;
        chessArr1[4][5] = 2;
        //输出原始的二维数组
        System.out.println("原始的二维数组");
        for (int[] row : chessArr1) {
            for (int data : row) {
                System.out.print(data + "\t");
            }
            System.out.println();
        }

        //将原始数组转稀疏数组
        //1. 先遍历二维数组 得到非0数据的个数
        int sum = 0;
        for (int i = 0; i < chessArr1.length; i++) {
            for (int j = 0; j < chessArr1.length; j++) {
                if (chessArr1[i][j] != 0) {
                    sum++;
                }
            }
        }

        //2. 创建对应的稀疏数组
        int sparseArr[][] = new int[sum + 1][3];
        // 给稀疏数组赋值
        sparseArr[0][0] = 11;
        sparseArr[0][1] = 11;
        sparseArr[0][2] = sum;

        // 遍历二维数组,将非零的值存放到稀疏数组
        int count = 0; //用来记录是第几个非零数据
        for (int i = 0; i < chessArr1.length; i++) {
            for (int j = 0; j < chessArr1.length; j++) {
                if (chessArr1[i][j] != 0) {
                    count++;
                    sparseArr[count][0] = i;
                    sparseArr[count][1] = j;
                    sparseArr[count][2] = chessArr1[i][j];
                }
            }
        }

        //输出稀疏数组的形式
        System.out.println();
        System.out.println("得到的稀疏数组为");
        for (int i = 0; i < sparseArr.length; i++) {
            System.out.printf("%d\t%d\t%d\t\n", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]);
        }
        System.out.println();

        //将稀疏数组 --> 恢复成原始的二维数组
        //读取稀疏数组后几行数据,并赋值给原始数组
        //1. 先读取稀疏数组第一行,得到原始数组的大小
        int[][] chessArr2 = new int[sparseArr[0][0]][sparseArr[0][1]];
        for (int i = 1; i < sparseArr.length; i++) {
            chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
        }
        //输出恢复后的二维数组
        System.out.println();
        System.out.println("恢复后的二维数组");
        for (int[] row : chessArr2) {
            for (int data : row) {
                System.out.print(data + "\t");
            }
            System.out.println();
        }

        //将稀疏数组写入文件
        System.out.println("稀疏数组写入map.data文件");
        String filePath = "d:\\map.data";
        File file = new File(filePath);
        OutputStreamWriter writer = null;
        try {
            System.out.println("文件创建成功");
            writer = new OutputStreamWriter(new FileOutputStream(file), "GB2312");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        for (int i = 0; i < sparseArr.length; i++) {
            if (i == sparseArr.length - 1) {
                writer.write(sparseArr[i][0] + "," + sparseArr[i][1] + "," + sparseArr[i][2]);
            } else {
                writer.write(sparseArr[i][0] + "," + sparseArr[i][1] + "," + sparseArr[i][2] + ",");
            }
        }
        writer.close();
        System.out.println("写入成功");


        //稀疏数组转二维数组首先读取文件
        InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(file));
        StringBuffer sbf = new StringBuffer();
        while (inputStreamReader.ready()) {
            sbf.append((char) inputStreamReader.read());
        }
        System.out.println("读出文件" + sbf.toString());
        inputStreamReader.close();

        //把读出的文件,赋值给稀疏数组
        String[] data = sbf.toString().split(",");
        System.out.println(data.length / 3);
        int[][] sparseArray2 = new int[data.length / 3][3];
        int i = 0;
        for (String s : data) {
            System.out.println(i % 3);
            sparseArray2[i / 3][i % 3] = Integer.parseInt(s);
            i++;
        }

        //输出读取到的稀疏数组
        System.out.println("读取到的稀疏数组如下++++++++++++");
        for (int j = 0; j < sparseArray2.length; j++) {
            System.out.print(sparseArray2[j][0] + "\t" + sparseArray2[j][1] + "\t" + sparseArray2[j][2]);
            System.out.println();
        }

        //1. 先读取稀疏数组的第一行,找出二维数组的大小
        int[][] charArray2 = new int[sparseArray2[0][0]][sparseArray2[0][1]];
        //2. 读取稀疏数组的值,给到二维数组
        for (int k = 1; k < sparseArray2.length; k++) {
            charArray2[sparseArray2[k][0]][sparseArray2[k][1]] = sparseArray2[k][2];
        }
        System.out.println("恢复后的二维数组");
        for (int[] row : charArray2) {
            for (int ds : row) {
                System.out.print(ds + "\t");
            }
            System.out.println();
        }
    }
}

队列

基本介绍

对列是一个有序列表,可以用数组链表来实现

遵循先入先出的原则,即:先存入队列的数据,要 先取出,后存入队列的数据,要后退出

数组模拟队列

image-20220817095725823

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

  1. 将尾指针往后移:rear+1,当front == rear【空】
  2. 若尾指针 rear 小于队列的最大下标 maxSize -1 ,则将数据存入 rear所指的数组元素中,否则无法存入数组。rear == maxSize -1【队列满】

代码实现

package com.yhs.queue;

import java.util.Scanner;

public class Queue {
    public static void main(String[] args) {
        ArrayQueue arrayQueue = new ArrayQueue(3);
        char key = ' ';
        Scanner scanner = new Scanner(System.in);
        boolean loop = true;
        //输出一个菜单
        while (loop) {
            System.out.println("s(show): 显示队列");
            System.out.println("e(exit): 退出程序");
            System.out.println("a(add): 添加数据到队列");
            System.out.println("g(get): 从队列取出数据");
            System.out.println("h(head): 查看队列头的数据");
            System.out.print("请输入你的选择: ");
            key = scanner.next().charAt(0);
            switch (key) {
                case 's':
                  arrayQueue.showQueue();
                  break;
                case 'a':
                    System.out.print("请输入添加的数据: ");
                    int value = scanner.nextInt();
                    arrayQueue.addQueue(value);
                    break;
                case 'g':
                    try {
                        int res = arrayQueue.getQueue();
                        System.out.printf("取出的数据是%d\n", res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'h':
                    try {
                        int res = arrayQueue.headQueue();
                        System.out.printf("取出的头数据=%d\n", res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'e':
                    scanner.close();
                    loop = false;
                    break;
                default:
                    break;
            }
        }
        System.out.println("程序退出");
    }
}

//使用数组模拟队列编写一个ArrayQueue类
class ArrayQueue {
    private int maxSize; //表示数组最大容量
    private int front; //队列头
    private int rear; //队列尾
    private int[] arr; //该数组用于存放数据,模拟队列

    //添加构造器
    public ArrayQueue(int arrMaxsize) {
        maxSize = arrMaxsize;
        arr = new int[maxSize];
        front = -1; //指向队列头部,front指向队列头的前一个位置
        rear = -1; //指向队列尾部,指向队列尾的数据
    }

    //判断数组是否为空
    public boolean isEmpty() {
        return front == rear;
    }

    //判断数组是否已满
    public boolean isFull() {
        return rear == maxSize - 1;
    }

    //添加数据
    public void addQueue(int n) {
        if (! isFull()) {
            rear++;
            arr[rear] = n;
        } else {
            System.out.println("队列已满不能添加数据");
        }
    }

    //获取队列数据
    public int getQueue() {
        if (! isEmpty()) {
            front++; //因为front代表队列头的前一个数据,所以需要++
            return arr[front];
        } else {
            throw new RuntimeException("队列为空 不能取出数据");
        }
    }

    //显示队列的所有数据
    public void showQueue() {
        if (! isEmpty()) {
            for (int i = 0; i < arr.length; i++) {
                System.out.printf("arr[%d]=%d\n", i, arr[i]);
            }
        } else {
            System.out.println("队列为空,无法遍历数据");
        }
    }

    //显示队列的头数据
    public int headQueue() {
        if (! isEmpty()) {
            return arr[front+1];
        } else {
            throw new RuntimeException("队列为空,无法取出");
        }
    }
}

数组模拟环形队列

在上一个ArrayQueueDemo的基础上优化程序,达到复用的效果

思路如下:

  1. front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素
  2. front的初始值为0
  3. rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定
  4. rear的初始值为0
  5. 当队列满时,条件是:(rear+1)%maxSize = front
  6. 当队列为空的条件,rear==front
  7. 当我们这样分析,队列中有效的数据个数(rear+maxSize-front)%maxSize
  8. 在上一个demo中修改得到一个环形队列

image-20220817115949096

代码实现

package com.yhs.queue;

import java.util.Scanner;

@SuppressWarnings("all")
public class CircleArrayQueue {
    public static void main(String[] args) {
        //测试
        //这里设置为4,其队列有效数据最大为3
        //约定的位置是动态变化的
        CircleArray circleArray = new CircleArray(4);
        char key = ' ';
        Scanner scanner = new Scanner(System.in);
        boolean loop = true;
        //输出一个菜单
        while (loop) {
            System.out.println("s(show): 显示队列");
            System.out.println("e(exit): 退出程序");
            System.out.println("a(add): 添加数据到队列");
            System.out.println("g(get): 从队列取出数据");
            System.out.println("h(head): 查看队列头的数据");
            System.out.print("请输入你的选择: ");
            key = scanner.next().charAt(0);
            switch (key) {
                case 's':
                    circleArray.showQueue();
                    break;
                case 'a':
                    System.out.print("请输入添加的数据: ");
                    int value = scanner.nextInt();
                    circleArray.addQueue(value);
                    break;
                case 'g':
                    try {
                        int res = circleArray.getQueue();
                        System.out.printf("取出的数据是%d\n", res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'h':
                    try {
                        int res = circleArray.headQueue();
                        System.out.printf("取出的头数据=%d\n", res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'e':
                    scanner.close();
                    loop = false;
                    break;
                default:
                    break;
            }
        }
        System.out.println("程序退出");
    }
}

class CircleArray {
    private int maxSize; //数组最大容量
    //front 变量的含义作调整:指向队列的第一个元素,也就是arr[front] = arr[0];
    //front 的初始值 = 0
    private int front;
    //rear 变量的含义作调整: 指向队列的最后一个元素的后一个位置
    //rear 的初始值 = = 0
    private int rear; //队列尾
    private int[] arr;

    //构造器
    public CircleArray(int arrMaxSize) {
        maxSize = arrMaxSize;
        arr = new int[maxSize];
    }

    //判断是否为空
    public boolean isEmpty() {
        return front == rear;
    }

    //判断是否已满
    public boolean isFull() {
        return (rear + 1) % maxSize == front;
    }

    //添加数据到队列
    public void addQueue(int n) {
        // 判断队列是否满
        if (!isFull()) {
            arr[rear] = n;
            rear = (rear + 1) % maxSize;
        } else {
            System.out.println("队列已满,不能加入...");
        }
    }

    //获取队列中的数据
    public int getQueue() {
        if (!isEmpty()) {
            //front指向队列中的第一个元素
            //1. 先将front存入一个零时变量
            //2. 将front 后移动,考虑取模
            //3. 将临时保存的变量返回
            int value = arr[front];
            front = (front + 1) % maxSize;
            return value;
        } else {
            throw new RuntimeException("队列为空,无法取出");
        }
    }

    //显示队列中的数据
    public void showQueue() {
        if (!isEmpty()) {
            for (int i = front; i <  front + dataNum(); i++) {
                System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
            }
        } else {
            System.out.println("队列为空,无法显示...");
        }
    }

    //求出当前队列有效数据的数量
    public int dataNum() {
        return (rear + maxSize - front) % maxSize;
    }

    public int headQueue() {
        //判断
        if (!isEmpty()) {
            return arr[front];
        } else {
            throw new RuntimeException("队列为空,没有数据...");
        }
    }
}

单链表

链表(Linked List)链表是有序的列表,是链式存储结构

image-20220817145427949

链表 增查 实现思路

【添加方式一】

image-20220817163526739

按照编号顺序添加

【添加方式二】

image-20220817164740784

单链表节点删除

image-20220817195035592

package com.yhs.linkedlist;

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //测试
        //创建节点
        HeroNode h1 = new HeroNode(1, "詹姆斯", "小皇帝");
        HeroNode h2 = new HeroNode(2, "奥尼尔", "大鲨鱼");
        HeroNode h3 = new HeroNode(3, "邓肯", "石佛");
        HeroNode h4 = new HeroNode(4, "库里", "小学生");

        //创建一个链表
        SingleLinkedList singleLinkedList = new SingleLinkedList();
        //方式一:加入
//        singleLinkedList.addHeroNode(h1);
//        singleLinkedList.addHeroNode(h4);
//        singleLinkedList.addHeroNode(h2);
//        singleLinkedList.addHeroNode(h3);

        //方式二:加入按照编号的顺序
        singleLinkedList.addByOrder(h1);
        singleLinkedList.addByOrder(h4);
        singleLinkedList.addByOrder(h2);
        singleLinkedList.addByOrder(h3);

        //显示
        singleLinkedList.list();

        //测试修改节点的代码
        HeroNode newHeroNode = new HeroNode(2, "科比", "小飞侠");
        singleLinkedList.update(newHeroNode);
        System.out.println("=========修改后=========");
        singleLinkedList.list();

        //测试删除节点
        singleLinkedList.delete(1);
        singleLinkedList.delete(2);
        singleLinkedList.delete(3);
        singleLinkedList.delete(4);
        System.out.println("=============删除后===========");
        singleLinkedList.list();

    }
}

//定义一个SingleLinkedList 管理我们的英雄
class SingleLinkedList {
    //初始化一个头节点,头节点不存放数据
    private HeroNode headN = new HeroNode(0, "", "");

    //【方式一】添加节点到单项链表,不考虑编号顺序时
    //1. 找到当前链表的最后的节点
    //2. 将最后的节点的next 指向新的节点
    public void addHeroNode(HeroNode heroNode) {
        //因为head节点不能动,因此我们需要一个辅助的变量 temp
        HeroNode temp = headN;
        //遍历链表,找到最后
        while (true) {
            if (temp.next == null) {
                break;
            }
            //如果没有找到最后,就将temp后移
            temp = temp.next;
        }
        //当退出while循环时,temp就指向新的节点
        temp.next = heroNode;
    }

    //【方式二】添加节点时,根据排名将英雄插入到指定位置
    //(如果有这个排名,则添加失败,并给出提示)
    public void addByOrder(HeroNode heroNode) {
        //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
        HeroNode temp = headN;
        boolean flag = false; //标志添加的编号是否存在,默认为false
        while (true) {
            if (temp.next == null) { //说明temp已经在链表的最后
                break;
            }
            if (temp.next.no > heroNode.no) { //位置找到就在temp后面插入
                break;
            } else if (temp.next.no == heroNode.no) { //说明希望添加的heroNode编号已存在
                flag = true;
                break;
            }
            temp = temp.next;
        }
        //判断flag的值
        if (flag) { //编号已经存在,不能添加
            System.out.printf("准备插入的英雄编号 %d 已经存在,不能添加\n", heroNode.no);
        } else {
            //插入到链表,temp的后面
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    //修改节点的信息,根据no编号来修改,即no编号不能改变
    //1. 根据 newHeroNode 的 no 来进行修改
    public void update(HeroNode newHeroNode) {
        //判断是否为空
        if (headN.next == null) {
            System.out.println("链表为空...");
            return;
        }
        //找到需要修改的节点
        //定义一个辅助指针
        HeroNode temp = headN.next;
        boolean loop = false; //表示是否找到该节点
        while (true) {
            if (temp.no == newHeroNode.no) {
                loop = true;
                break;
            }
            temp = temp.next;
            if (temp == null) {
                return;
            }
        }
        if (loop) {
            temp.name = newHeroNode.name;
            temp.nickName = newHeroNode.nickName;
        } else {
            System.out.printf("没有找到编号= %d 的节点,不能修改", newHeroNode.no);
        }
    }

    //删除节点
    //1. head 不能动,因此我们需要一个辅助指针找到删除节点的前一个节点
    //2. 说明我们比较时,时temp.next.no 和 需要删除的节点no比较
    public void delete(int no) {
        HeroNode temp = headN;
        boolean loop = false; //标志是否找到待删除的节点
        while (true) {
            if (temp.next == null) {
                break;
            }
            if (temp.next.no == no) {
                loop = true;
                break;
            }
            temp = temp.next;
        }
        //判断是否找到该节点
        if (loop) { //找到
            //删除该节点
            temp.next = temp.next.next;
        } else {
            System.out.printf("删除失败...该节点 %d不存在", no);
        }
    }


    //显示链表
    public void list() {
        //先判断链表是否为空
        if (headN.next == null) {
            System.out.println("链表为空");
            return;
        }
        //因为头节点不能动,因此我们需要定义一个辅助变量来遍历
        HeroNode temp = headN.next;
        while (true) {
            //判断是否到链表最后
            if (temp == null) {
                break;
            }
            //输出节点信息
            System.out.println(temp);
            //将temp后移
            temp = temp.next;

//            第二种写法
//        while (true) {
//            System.out.println(temp);
//            temp = temp.next;
//            if (temp == null) {
//                break;
//            }
//        }
        }
    }

}


class HeroNode {
    public int no;
    public String name;
    public String nickName;
    public HeroNode next;

    //构造器
    public HeroNode(int no, String name, String nickName) {
        this.no = no;
        this.name = name;
        this.nickName = nickName;
    }

    //为了方便显示,重写toString
    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickName='" + nickName + '\'' +
                '}';
    }
}

单链表面试题

获取单链表有效节点个数
public static int getLength(HeroNode headN) {
    if (headN.next == null) {
        return 0;
    }
    int length = 0;
    HeroNode temp = headN.next;
    while (temp != null) {
        length++;
        temp = temp.next;
    }
    return length;
}
查找单链表倒数第k个节点
//1. 编写一个方法,接收head节点,同时接收一个index
//2. index 表示倒数第index个节点
//3. 先把链表从头到尾遍历,得到链表的总长度
//4. 得到size后,我们从链表的第一个开始遍历(size - index)个,就可以得到
//5. 如果找到了,则返回该节点,否则返回null
public HeroNode getKNode(HeroNode headN, int index) {
    if (headN.next == null) {
        return null;
    }
    int size = 0;
    HeroNode temp = headN.next;
    while (temp != null) {
        size++;
        temp = temp.next;
    }
    //第二次遍历 size-index 位置,就是倒数的第k个节点
    //先做一个index的校验
    if (index <= 0 || index > size) {
        return null;
    }
    temp = headN.next;
    //for循环 定位到倒数的index
    for (int i = 0; i < size - index; i++) {
        temp = temp.next;
    }
    return temp;
}
单链表反转

方式一

image-20220818140548114

//将单链表反转
public static void reverseList(HeroNode headN) {
    //如果当前链表为空,或者只有一个节点,无序反转,直接返回
    if (headN.next == null || headN.next.next == null) {
        return;
    }
    //定义一个辅助的指针,帮助我们遍历原来的链表
    HeroNode cur = headN.next;
    HeroNode next = null; //指向当前节点[cur]的下一个节点
    HeroNode reverseHead = new HeroNode(0,"", "");
    //并从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
    while (cur != null) {
        next = cur.next; //先暂时保存当前节点的下一个节点,因为后面需要使用
        cur.next = reverseHead.next;
        reverseHead.next = cur; //将cur连接到新的链表上
        cur = next; //让temp后移
    }
    //将head.next 指向 reverseHead.next 实现链表反转
    headN.next = reverseHead.next;
}

方式二

//将单链表反转 -- 双指针解法
    public void reverseList(HeroNode headN) {
        //如果当前链表为空,或者只有一个节点,无序反转,直接返回
        if (headN.next == null || headN.next.next == null) {
            return;
        }
        //定义一个辅助的指针,帮助我们遍历原来的链表
        HeroNode cur = headN.next;
        HeroNode pre = null;
        HeroNode temp = null; //辅助指针
        while (cur != null) {
            temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        headN.next = pre;
    }
从尾到头打印单链表

测试Stack(栈)的使用

public class TestStock {
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        //入栈
        stack.add("jack");
        stack.add("tom");
        stack.add("smith");
        //出栈
        while (stack.size() > 0) {
            System.out.println(stack.pop());
        }
    }
}

image-20220818140744469

//倒序打印链表
//可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,实现逆序打印效果
public void reversePrint(HeroNode headN) {
    if (headN.next == null) {
        return;
    }
    //创建一个栈,将各个节点压入栈中
    Stack<HeroNode> stack = new Stack<>();
    HeroNode cur = headN.next;
    //将链表的所有节点压入栈
    while (cur != null) {
        stack.push(cur);
        cur = cur.next;
    }
    //将栈中的节点进行打印 pop出栈
    while (stack.size() > 0) {
        System.out.println(stack.pop()); //stack的特点:先进后出
    }
}

双向链表

使用单项链表的缺点分析

  1. 单项链表,查找的方向只能是一个方向,二双向链表可以向前或者向后查找
  2. 单项链表的不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以之前我们单项链表删除节点时,总是找到temp(待删除节点的前一个节点)

双向链表CRUD分析图解

image-20220818145104424

代码实现

package com.yhs.linkedlist;

public class DoubleLinkedListDemo {
    public static void main(String[] args) {
        //测试
        //创建节点
        System.out.println("~~~~~~~~双向链表测试~~~~~");
        HeroNode2 h1 = new HeroNode2(1, "詹姆斯", "小皇帝");
        HeroNode2 h2 = new HeroNode2(2, "奥尼尔", "大鲨鱼");
        HeroNode2 h3 = new HeroNode2(3, "邓肯", "石佛");
        HeroNode2 h4 = new HeroNode2(4, "库里", "小学生");

        //创建双向链表对象
        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();

        //方式一:加入到链表尾
//        doubleLinkedList.add(h1);
//        doubleLinkedList.add(h2);
//        doubleLinkedList.add(h3);
//        doubleLinkedList.add(h4);


        //方式二:加入按照编号的顺序
        doubleLinkedList.addByOrder(h1);
        doubleLinkedList.addByOrder(h4);
        doubleLinkedList.addByOrder(h2);
        doubleLinkedList.addByOrder(h3);
        System.out.println("=========按照编号顺序加入===========");
        doubleLinkedList.list();

        //修改
        HeroNode2 newHeroNode = new HeroNode2(3, "约翰逊", "魔术师");
        doubleLinkedList.update(newHeroNode);
        System.out.println("=======修改后的双向链表======");
        doubleLinkedList.list();

        //删除
        doubleLinkedList.delete(2);
        System.out.println("===========删除后的双向链表========");
        doubleLinkedList.list();
    }
}

//创建一个双向链表的类
class DoubleLinkedList {
    //先初始化一个头节点,头节点不要动,不存放具体的数据
    private HeroNode2 headN = new HeroNode2(0, "", "");

    //返回头节点
    public HeroNode2 getHead() {
        return headN;
    }

    //遍历双向链表的方法
    public void list() {
        //判断链表是否为空
        if (headN.next == null) {
            System.out.println("链表为空");
            return;
        }
        //因为头节点不能动,所以我们需要一个辅助变量遍历
        HeroNode2 temp = headN.next;
        while (true) {
            //判断是否到链表最后
            if (temp == null) {
                break;
            }
            //输出节点信息
            System.out.println(temp);
            //将temp后移
            temp = temp.next;
        }
    }

    //【方式一】
    //添加节点到双向链表(尾部添加)
    public void add(HeroNode2 heroNode) {
        //添加一个辅助节点
        HeroNode2 temp = headN;
        //遍历链表,找到最后
        while (true) {
            if (temp.next == null) {
                break;
            }
            //如果没有找到,就后移temp
            temp = temp.next;
        }
        //当退出while循环时,temp就指向了链表的最后
        //形成一个双向链表
        temp.next = heroNode;
        heroNode.pre = temp;
    }

    //【方式二】
    //添加节点到双向链表(按照排名添加)
    public void addByOrder(HeroNode2 newHero) {
        HeroNode2 temp = headN;
        boolean flag = false;
        while (true) {
            if (temp.next == null) {
                break;
            }
            if (temp.next.no > newHero.no) { //位置找到,就在temp的后面加入
                break;
            } else if (temp.next.no == newHero.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            System.out.printf("准备插入的英雄编号 %d 已经存在,不能添加\n", newHero.no);
        } else {
            //插入到链表中
            /**
             * temp从head的位置开始指,不断的向后移动
             * 发现当前节点的next等于空,就相当于已经走到链表的最后
             * 举例:将5加入到 head-1-2-3-4 这条链表当中
             * 那肯定是加到最后了
             * 此时,temp指向4
             * 代码:temp.next = heroNode;
             *      heroNode.pre = temp;
             * 4的next(后面一个节点)应该是newHeroNode
             * newHeroNode的pre(前面一个节点)应该是4
             *
             * 那因为我们这是排序插入,肯定会遇到将节点插入到中间部分的情况
             * 举例:将2加入到 head-1-3-5中
             * 我们要插在1和3之间
             * 当前temp指向1
             * 所以当temp.next != null 时候
             * 我们要提前将temp的下一个节点的前端指向新的节点
             * 还要将新节点的后端指向temp的后一个节点
             * temp.next.pre = heroNode;
             * heroNode.next = temp.next;
             */
            if (temp.next != null) { //相当于两个节点中间插入
                temp.next.pre = newHero;
                newHero.next = temp.next;
            }
            //相当于尾部插入
            temp.next = newHero;
            newHero.pre = temp;
        }
    }


    //修改节点内容(与单链表基本相同)
    public void update(HeroNode2 newHeroNode) {
        if (headN.next == null) {
            return;
        }
        HeroNode2 temp = headN.next;
        boolean loop = false;
        while (true) {
            if (temp.no == newHeroNode.no) {
                loop = true;
                break;
            }
            temp = temp.next;
            if (temp == null) {
                return;
            }
        }
        if (loop) {
            temp.name = newHeroNode.name;
            temp.nickName = newHeroNode.nickName;
        } else {
            System.out.printf("未找到编号=%d的节点", newHeroNode.no);
        }
    }


    //双向链表的删除
    //1.对于双向链表而言,我们可以直接找到要删除的节点
    //2. 找到后,自我删除即可
    public void delete(int n) {
        //判断当前链表是否为null
        if (headN.next == null) {
            System.out.println("链表为空,无法删除");
            return;
        }
        HeroNode2 temp = headN.next;
        boolean flag = false;
        while (true) {
            if (temp.no == n) {
                flag = true;
                break;
            }
            temp = temp.next;
            if (temp == null) {
                break;
            }
        }
        if (flag) {
            temp.pre.next = temp.next;
            if (temp.next != null) {
                temp.next.pre = temp.pre;
            }
        } else {
            System.out.println("未找到");
        }
    }
}

class HeroNode2 {
    public int no;
    public String name;
    public String nickName;
    public HeroNode2 next; //指向下一个节点 ,默认为null
    public HeroNode2 pre; //指向前一个节点 ,默认为null

    //构造器
    public HeroNode2(int no, String name, String nickName) {
        this.no = no;
        this.name = name;
        this.nickName = nickName;
    }

    //为了方便显示,重写toString
    @Override
    public String toString() {
        return "HeroNode2{" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickName='" + nickName + '\'' +
                '}';
    }
}

单向环形链表

Josephus问题

约瑟夫问题是以弗拉维奥·约瑟夫命名的,他是1世纪的一名犹太历史学家,他在自己的日记中写道,他和他的40个战友被罗马军队包围在洞中。他们讨论是自杀还是被俘,最终决定自杀,并以抽签的方式决定杀掉谁,约瑟夫和另外一个人是最后留下的两个人。约瑟夫说服了那个人,他们将向罗马军队投降,不再自杀。约瑟夫把他的存活归因于运气或天意,他不知道是究竟是哪一个。

约瑟夫问题是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。

人们站在一个等待被处决的圈子里。 计数从圆圈中的指定点开始,并沿指定方向围绕圆圈进行。 在跳过指定数量的人之后,处刑下一个人。 对剩下的人重复该过程,从下一个人开始,朝同一方向跳过相同数量的人,直到只剩下一个人,并被释放。

问题即,给定人数、起点、方向和要跳过的数字,选择初始圆圈中的位置以避免被处决。

问题描述
编号为1~N 的N个士兵站成一个圆圈(编号分别为1,2,3…N),从编号为k的士兵开始依次报数(1,2…m),数到m的士兵会出列,之后的士兵再从1开始报数。直到剩下最后一个士兵,求这个是士兵编号。

解法
比较简单的做法是用循环单链表模拟整个过程。如下图所示,由5个结点组成一圈,编号分别为1,2,3,4,5。

约瑟夫问题

比如,从2号开始数3个数,即1,2,3,则4号出圈;

继续从5号开始数3个数,即1,2,3,则2号出圈;依次类推,最后一个结点为5号。

img

创建环形链表的思路

image-20220818200217938

创建单项环形链表代码实现

package com.yhs.linkedlist;

public class josephus {
    public static void main(String[] args) {
        //测试构建环形链表是否成功,遍历是否ok
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        circleSingleLinkedList.addNode(5);
        circleSingleLinkedList.showSoldier();
    }
}

//创建一个环形的单项链表
class CircleSingleLinkedList {
    //创建一个first节点,当前没有编号
    private Soldier first = null;

    //添加节点,构建成环形列表
    public void addNode(int nums) {
        //对nums进行数据校验
        if (nums < 1) {
            System.out.println("nums值至少为1 ");
            return;
        }
        Soldier cur = null; //辅助指针,帮助构建环形链表
        //for循环创建环形列表
        for (int i = 1; i <= nums; i++) {
            //根据编号创建节点
            Soldier soldier = new Soldier(i);
            //如果是第一个节点
            if (i == 1) {
                first = soldier;
                first.setNext(first); //构成环状
                cur = first;  //cur指针指向第一个节点
            } else {
                cur.setNext(soldier);
                soldier.setNext(first);
                cur = soldier;
            }
        }
    }

    //遍历当前的环形链表
    public void showSoldier() {
        //判断链表是否为null
        if (first == null) {
            System.out.println("没有任何节点");
            return;
        }
        //因为first不能动,所以需要辅助指针帮助完成遍历
        Soldier cur = first;
        while (true) {
            System.out.printf("节点的编号 %d\n" , cur.getNo());
            if (cur.getNext() == first) { //说明遍历完毕
                break;
            }
            cur = cur.getNext();//cur 后移
        }
    }
}


//表示一个节点类
class Soldier {
    private int no; //编号
    private Soldier next; //指向下一个节点,默认null

    public Soldier(int no) {
        this.no = no;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public Soldier getNext() {
        return next;
    }

    public void setNext(Soldier next) {
        this.next = next;
    }

    @Override
    public String toString() {
        return "Soldier{" +
                "no=" + no +
                '}';
    }
}

出圈思路分析

image-20220819131926686

约瑟夫问题代码实现

package com.yhs.linkedlist;

public class josephus {
    public static void main(String[] args) {
        //测试构建环形链表是否成功,遍历是否ok
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        circleSingleLinkedList.addNode(5);
        circleSingleLinkedList.showSoldier();

        //测试士兵出圈
        circleSingleLinkedList.KillSolider(1, 2, 5);
    }
}

//创建一个环形的单项链表
class CircleSingleLinkedList {
    //创建一个first节点,当前没有编号
    private Soldier first = null;

    //添加节点,构建成环形列表
    public void addNode(int nums) {
        //对nums进行数据校验
        if (nums < 1) {
            System.out.println("nums值至少为1 ");
            return;
        }
        Soldier cur = null; //辅助指针,帮助构建环形链表
        //for循环创建环形列表
        for (int i = 1; i <= nums; i++) {
            //根据编号创建节点
            Soldier soldier = new Soldier(i);
            //如果是第一个节点
            if (i == 1) {
                first = soldier;
                first.setNext(first); //构成环状
                cur = first;  //cur指针指向第一个节点
            } else {
                cur.setNext(soldier);
                soldier.setNext(first);
                cur = soldier;
            }
        }
    }

    //遍历当前的环形链表
    public void showSoldier() {
        //判断链表是否为null
        if (first == null) {
            System.out.println("没有任何节点");
            return;
        }
        //因为first不能动,所以需要辅助指针帮助完成遍历
        Soldier cur = first;
        while (true) {
            System.out.printf("节点的编号 %d\n", cur.getNo());
            if (cur.getNext() == first) { //说明遍历完毕
                break;
            }
            cur = cur.getNext();//cur 后移
        }
    }

    //根据用户的输入,计算出出圈的顺序
    /**
     * @param startNode 表示从第几个士兵开始数
     * @param countNum  表示数几下
     * @param nums      表示最初有多少士兵在圈中
     */
    public void KillSolider(int startNode, int countNum, int nums) {
        //先校验数据合理性
        if (first == null || countNum < 0 || startNode > nums || startNode < 1) {
            System.out.println("参数输入有误,请重新输入");
            return;
        }
        //创建辅助指针,帮助完成出圈
        Soldier helper = first;
        //事先应该指向环形链表的最后这个节点
        while (true) {
            if (helper.getNext() == first) { //说明helper指向最后一个节点
                break;
            }
            helper = helper.getNext();
        }
        //报数前,让firs和helper移动到报数的士兵的位置
        for (int j = 0; j < startNode - 1; j++) {
            first = first.getNext();
            helper = helper.getNext();
        }
        //当开始报数时,让first 和 helper 指针同时移动 m -1 次,然后出圈
        //这里执行循环操作,直到圈中只有一个节点
        while (true) {
            if (helper == first) { //说明圈中只有一个节点
                break;
            }
            //让first和helper同时移动 countNum - 1次
            for (int j = 0; j < countNum -1 ;j++) {
                first = first.getNext();
                helper = helper.getNext();
            }
            //这时first指向的节点,就是要出圈的节点
            System.out.printf("-->士兵%d死亡\n", first.getNo());
            //这时将first指向的士兵删除(出圈)
            first = first.getNext();
            helper.setNext(first);
        }
        System.out.printf("最后幸存的士兵编号%d \n", first.getNo());
    }
}


//表示一个节点类
class Soldier {
    private int no; //编号
    private Soldier next; //指向下一个节点,默认null

    public Soldier(int no) {
        this.no = no;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public Soldier getNext() {
        return next;
    }

    public void setNext(Soldier next) {
        this.next = next;
    }

    @Override
    public String toString() {
        return "Soldier{" +
                "no=" + no +
                '}';
    }
}

基本介绍

  1. 栈(Stack)是一个先入后出(FILO-First In Last Out)的有序列表。
  2. 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom).
  3. 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元
  4. 素最先删除,最先放入的元素最后删除

image-20220819135304324

应用场景

  1. 子程序的调用(如JVM中的虚拟机栈):在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。

    2 )处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。

  2. 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)

  3. 二叉树的遍历。

  4. 图形的深度优先(depth—first)搜索法。

image-20220819140705918

数组模拟栈

package com.yhs.stack;

import java.util.Scanner;

public class ArrayStackDemo {
    public static void main(String[] args) {
        //测试ArrayStack 是否正确
        //先创建一个ArrayStack 对象, 表示一个栈
        ArrayStack stack = new ArrayStack(4);
        String key = "";
        boolean loop = true; //控制用来是否退出
        Scanner scanner = new Scanner(System.in);

        while (loop) {
            System.out.println("show: 表示显示栈");
            System.out.println("exit: 退出程序");
            System.out.println("push: 添加数据到栈(入栈)");
            System.out.println("pop: 从栈取出数据(出栈)");
            System.out.print("请输入你的选择: ");
            key = scanner.next();
            switch (key) {
                case "show":
                    stack.list();
                    break;
                case "exit":
//                    scanner.close();
//                    loop =false;
                    System.exit(0);
                    break;
                case "push":
                    System.out.print("请输入一个数: ");
                    int value = scanner.nextInt();
                    stack.push(value);
                    break;
                case "pop":
                    try {
                        int res = stack.pop();
                        System.out.println("出栈的数据= " + res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
            }
        }
        System.out.println("程序退出...");

    }
}
//定义一个ArrayStack 表示栈
class ArrayStack {
    private int maxSize; //栈的大小
    private int[] stack; //数组,数组模拟栈,数据存放在该数组
    private int top = -1; //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;
    }
    //入栈-push
    public void push(int value) {
        //先判断栈是否满
        if(isFull()) {
            System.out.println("栈满,无法添加数据");
            return;
        }
        top++;
        stack[top] = value;
    }
    //出栈-pop 将栈顶的数据返回
    public int pop() {
        //先判断是否栈空
        if (isEmpty()) {
            throw new RuntimeException("栈空,无法取出数据");
        }
        int value = stack[top];
        top--;
        return value;
    }
    //显示栈的情况【遍历栈】
    //遍历时,需要从栈顶开始显示数据
    public void list() {
        if (isEmpty()) {
            System.out.println("栈空,无数据...");
            return;
        }
        for (int i = top; i >= 0; i--) {
            System.out.println(String.format("stack[%d]=%d", i,stack[i]));
        }
    }
}

单链表模拟栈

package com.yhs.stack;

import java.util.Scanner;

public class LinkedListStackDemo {
    public static void main(String[] args) {

        SingleLinkedListStack singleLinkedListStack = new SingleLinkedListStack();
        String key = "";
        boolean loop = true; //控制用来是否退出
        Scanner scanner = new Scanner(System.in);
        while (loop) {
            System.out.println("show: 表示显示栈");
            System.out.println("exit: 退出程序");
            System.out.println("push: 添加数据到栈(入栈)");
            System.out.println("pop: 从栈取出数据(出栈)");
            System.out.print("请输入你的选择: ");
            key = scanner.next();
            switch (key) {
                case "show":
                    singleLinkedListStack.show();
                    break;
                case "exit":
//                    scanner.close();
//                    loop =false;
                    System.exit(0);
                    break;
                case "push":
                    System.out.print("请输入新节点编号: ");
                    int no = scanner.nextInt();
                    Node newNode = new Node(no);
                    singleLinkedListStack.push(newNode);
                    break;
                case "pop":
                    try {
                        Node res = singleLinkedListStack.pop();
                        System.out.println("出栈节点= " + res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
            }
        }
        System.out.println("程序退出...");
    }
}

class SingleLinkedListStack {
    //初始化一个头节点
    private Node top = new Node(-1);

    //判断是否栈空
    public boolean isEmpty() {
        return top.next == null;
    }

    //入栈.【入栈时采用头插法】
    //因为栈是先入后出的,所以使用头插法在pop时可以将最后插入的节点最先取出
    //从而是实现先入后出的效果
    public void push(Node newNode) {
        if (top.next == null) { //第一个节点插入
            top.next = newNode;
            return;
        }
        // 头插法
        Node temp = top.next; //创建一个辅助指针
        top.next = newNode;
        newNode.next = temp;
    }

    //出栈
    public Node pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈空无法取出数据");
        }
        Node value = top.next;
        top = top.next;
        return value;
    }

    //遍历栈
    public void show() {
        if (isEmpty()) {
            throw new RuntimeException("栈空");
        }
        Node temp = top;
        while (temp.next != null) {
            System.out.println("节点=" + temp.next);
            temp = temp.next;
        }
    }
}


class Node {
    public int no;
    public Node next;

    public Node(int no) {
        this.no = no;
    }

    @Override
    public String toString() {
        return "Node{" +
                "no=" + no +
                '}';
    }
}

栈实现综合计算器

思路分析

image-20220820125032315

package com.yhs.stack;

public class Calculator {
    public static void main(String[] args) {
        //表达式
        String expression = "29-3+5-6";
        //创建两个栈 1,数字栈,2,符号栈
        ArrayStack2 numStack = new ArrayStack2(10);
        ArrayStack2 signStack = new ArrayStack2(10);
        //定义需要的变量
        int index = 0; //扫描指针
        int num1;
        int num2;
        char sign = ' ';
        int res;
        char ch = ' '; //将每次扫描到的char保存到ch
        String keepNum = ""; //用于拼接多位数
        //循环扫描表达式
        while (true) {
            //依次得到表达式(expression)中的每一个字符
            ch = expression.substring(index, index+1).charAt(0);
            //判断是数字还是运算符,完成相应处理
            if (signStack.isSign(ch)) { //是运算符
                //判断符号栈是否为空
                if (signStack.isEmpty()) { //如果符号栈为空,直接入栈
                    signStack.push(ch);
                } else {
                    //不为空
                    //根据符号优先级,分情况处理
                    //1. 如果房前操作符优先级<=栈中的操作符:
                    //   从符号栈中pop出一个符号,进行运算,得到结果,结果入数字栈,然后将当前符号入符号栈
                    if (signStack.priority(ch) <= signStack.priority(signStack.peek())) {
                        num1 = numStack.pop();
                        num2 = numStack.pop();
                        sign = (char) signStack.pop();
                        res = numStack.cal(num1, num2, sign);
                        //把运算的结果入数字栈
                        numStack.push(res);
                        //然后将当前操作符放入符号栈
                        signStack.push(ch);
                    } else {
                        //如果当前操作符的优先级>当前符号栈中的运算符, 直接入符合栈
                        signStack.push(ch);
                    }
                }
            } else { //扫描是数字
                //由于字符'1' 与 数值1 ASCII码值相差48,所以需要减去48
                //1. 当处理数字是多位数时,不能扫描到一个数就立即入栈,
                //2. 在处理数时,需要expression表达式中的index后继续扫描,如果是数字,就继续进行扫描,直到是符号才入栈
                //3. 因此需要定义一个字符串变量,用于拼接
                keepNum += ch;

                //如果ch已经是expression最后一位,就直接入栈
                if (index == expression.length() - 1) {
                    numStack.push(ch - 48);
                } else {
                    //判断下一个字符是不是数字,如果是就继续扫描,如果是运算符,就入栈
                    if (signStack.isSign(expression.substring(index + 1, index + 2).charAt(0))) {
                        //是运算符
                        numStack.push(Integer.parseInt(keepNum));
                        //注意,清空keepNum!!!
                        keepNum = "";
                    }
                }
            }
            //扫描指针+1,判断是否扫描到expression最后
            index++;
            if (index >= expression.length()) { //扫描结束
                break;
            }
        }
        //当扫描结束,就顺序的从数字栈和符号栈pop出数和符号,并运算
        while (true) {
            if (signStack.isEmpty()) { //如果符号栈为空,表示计算到最后的结果,数字栈只有一个数【结果】
                break;
            }
            num1 = numStack.pop();
            num2 = numStack.pop();
            sign = (char) signStack.pop();
            res = numStack.cal(num1, num2, sign);
            numStack.push(res);
        }
        //将数字栈最后的数pop出来,即结果
        int finalRes = numStack.pop();
        System.out.printf("表达式:%s 结果= %d", expression, finalRes);
    }
}

//先定义一个栈,并实现相关功能的方法
class ArrayStack2 {
    private int maxSize; //栈的大小
    private int[] stack; //数组,数组模拟栈,数据存放在该数组
    private int top = -1; //top表示栈顶,初始化为-1

    //构造器
    public ArrayStack2(int maxSize) {
        this.maxSize = maxSize;
        stack = new int[maxSize];
    }

    //栈满
    public boolean isFull() {
        return top == maxSize - 1;
    }

    //栈空
    public boolean isEmpty() {
        return top == -1;
    }

    //入栈-push
    public void push(int value) {
        //先判断栈是否满
        if (isFull()) {
            System.out.println("栈满,无法添加数据");
            return;
        }
        top++;
        stack[top] = value;
    }

    //出栈-pop 将栈顶的数据返回
    public int pop() {
        //先判断是否栈空
        if (isEmpty()) {
            throw new RuntimeException("栈空,无法取出数据");
        }
        int value = stack[top];
        top--;
        return value;
    }

    //显示栈的情况【遍历栈】
    //遍历时,需要从栈顶开始显示数据
    public void list() {
        if (isEmpty()) {
            System.out.println("栈空,无数据...");
            return;
        }
        for (int i = top; i >= 0; i--) {
            System.out.println(String.format("stack[%d]=%d", i, stack[i]));
        }
    }

    //返回运算符优先级,优先级可以自己定义,优先级使用数字表示
    //数字越大优先级越高
    public int priority(int sign) {
        if (sign == '*' || sign == '/') { // 这里char的可以和int相互转换
            return 1;
        } else if (sign == '+' || sign == '-') {
            return 0;
        } else { //假定目前表达式只有 +,-,*,/
            return -1;
        }
    }

    //判断是不是一个运算符
    public boolean isSign(char val) {
        return val == '+' || val == '-' || val == '*' || val == '/';
    }

    //计算方法
    public int cal(int num1, int num2, char sign) {
        int res = 0; //res 用于存放运算结果
        switch (sign) {
            case '+':
                res = num1 + num2;
                break;
            case '-':
                res = num2 - num1; //注意要用后面的数 - 前面的数
                break;
            case '*':
                res = num1 * num2;
                break;
            case '/':
                res = num2 / num1;
                break;
            default:
                break;
        }
        return res;
    }

    //可以返回当前栈顶的值,并不pop出栈
    public int peek() {
        return stack[top];
    }
}

前缀表达式

前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前

例如:中缀表达式 ( 2 + 3 ) × 4 - 5,采用前缀表达式为:- × + 2 3 4 5

前缀表达式计算机运算:

  • 对前缀表达式进行从右至左依次扫描
  • 当遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 op 次顶元素),并将结果入栈
  • 重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果

例如前缀表达式 : - × + 2 3 4 5

  • 从右至左扫描,将5、4、3、2压入堆栈
  • 遇到 + 运算符,因此弹出 2 和 3( 2 为栈顶元素,3 为次顶元素,注意与后缀表达式做比较),计算出 2 + 3 的值,得 5,再将 5 入栈;
  • 接下来是 × 运算符,因此弹出 5 和 4 ,计算出 5 × 4 = 20,将 20 入栈•最后是 - 运算符,计算出 20 - 5 的值,即 15,由此得出最终计算结果

中缀表达式

image-20220820153234468

后缀表达式

后缀表达式也叫逆波兰表达式,其求值过程可以用到栈来辅助存储。

后缀表达式计算机运算:

  • 从左至右扫描表达式
  • 遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素和栈顶元素),并将结果入栈
  • 重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

例如: (3+4)×5-6 对应的后缀表达式就是 3 4+ 5 × 6 - 针对后缀表达式求值步骤如下:

  1. 从左至右扫描,将3和4压入堆栈;
  2. 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
  3. 将5入栈;
  4. 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
  5. 将6入栈;
  6. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
逆波兰计算器
package com.yhs.stack;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class PolandNotation {
    public static void main(String[] args) {
        //定义一个逆波兰表达式
        //4*5-8+60+8/2 => 4 5 * 8 - 60 + 8 2 / +
        //说明为了方便,逆波兰表达式 的数字和符号使用空格隔开
        String suffixExpression = "4 5 * 8 - 60 + 8 2 / +";
        //思路
        //1. 先将 "3 4 + 5 * 6 -" => 放入到ArrayList中
        //2. 将ArrayList 传递一个方法,遍历ArrayList 配合栈 完成计算
        List<String> rpnList = getListString(suffixExpression);
        System.out.println("rpnList=" + rpnList);

        int result = calculate(rpnList);
        System.out.println("计算结果= " + result);
    }

    //将一个逆波兰表达式,依次将数据和运算符 放到ArrayList中
    public static List<String> getListString(String suffixExpression) {
        //将 suffixExpression 分割
        String[] split = suffixExpression.split(" ");
        ArrayList<String> list = new ArrayList<>();
        for (String ele : split) {
            list.add(ele);
        }
        return list;
    }

    //完成逆波兰表达式的运算
    /**
     * 1. 从左至右扫描,将3和4压入堆栈;
     * 2. 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
     * 3. 将5入栈;
     * 4. 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
     * 5. 将6入栈;
     * 6. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
     */
    public static int calculate(List<String> ls) {
        //创建栈
        Stack<String> stack = new Stack<>();
        //遍历 ls
        for (String item : ls) {
            //使用正则表达式取出数
            if (item.matches("\\d+")) { //匹配多位数
                //入栈
                stack.push(item);
            } else { //运算符
                //pop出两个数,并运算,再运算
                int num2 = Integer.parseInt(stack.pop());
                int num1 = Integer.parseInt(stack.pop());
                int res = 0;
                if (item.equals("+")) {
                    res = num1 + num2;
                } else if (item.equals("-")) {
                    res = num1 - num2;
                } else if (item.equals("*")) {
                    res = num1 * num2;
                } else if (item.equals("/")) {
                    res = num1 / num2;
                } else {
                    throw new RuntimeException("运算符非法...");
                }
                //把res 入栈
                stack.push(String.valueOf(res));
            }
        }
        //最后留在stack的数据就是运算结果
        return Integer.parseInt(stack.pop());
    }
}
中缀表达式转后缀表达式
思路分析

具体步骤如下:

**1】**初始化两个栈:运算符栈s1和储存中间结果的栈s2;

**2】**从左至右扫描中缀表达式;

**3】**遇到操作数时,将其压s2;

**4】**遇到运算符时,比较其与s1栈顶运算符的优先级:

(1)如果s1为空,或栈顶运算符为左括号“(",则直接将此运算符入栈;

(2)否则,若优先级比栈顶运算符的,也将运算符压入s1;

(3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较

**5】**遇到括号时:

(1)如果是左括号“(",则直接压入s1

(2)如果是右括号“)",则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃

**6】**重复步骤2至5,直到表达式的最右边

**7】**将s1中剩余的运算符依次弹出并压入s2

**8】**依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式

从左向右扫描,转化过程如下:

image-20220820195844536

代码实现
package com.yhs.stack;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class PolandNotation {
    public static void main(String[] args) {

        //完成将一个中缀表达式转成后缀表达式的功能
        //1. 1+((2+3)*4)-5 => 转成 1 2 3 + 4 * + 5 -
        //2. 因为直接对 str 进行操作并不方便,因此 先将 “1+((2+3)*4)-5” => 转成中缀表达式对应的 List
        //   即 "1+((2+3)*4)-5" => ArrayList[1,+,(,(,2,+,3,),*,4,),-,5]
        //3. 将得到的中缀表达式对应的List => 后缀表达式对应的List
        //   即ArrayList[1,+,(,(,2,+,3,),*,4,),-,5] => ArrayList[1, 2, 3, +, 4, *, +, 5, -]

        String expression = "1+((2+3)*4)-5";
        List<String> infixExpressionList = toInfixExpression(expression);
        System.out.println("中缀表达式对应的List= " + infixExpressionList);
        List<String> parseSuffixExpressionList = parseSuffixExpressionList(infixExpressionList);
        System.out.println("后缀表达式对应的List= " + parseSuffixExpressionList);

        System.out.printf("expression=%d", calculate(parseSuffixExpressionList));
   
    }


    //即ArrayList[1,+,(,(,2,+,3,),*,4,),-,5] => ArrayList[1, 2, 3, +, 4, *, +, 5, -]
    //方法:将得到的中缀表达式对应的List => 后缀表达式对应的List
    public static List<String> parseSuffixExpressionList(List<String> ls) {
        //定义两个栈
        Stack<String> s1 = new Stack<>(); //符号栈
        //说明:因为s2这个栈,再整个转换过程中没有pop操作,而且后面操作还需逆序输出
        //因此不用Stack<String> 直接使用List<String> s2
        List<String> s2 = new ArrayList<String>(); //储存中间结果的List s2


        //遍历ls
        for (String item : ls) {
            //如果是一个数,加入到s2
            if (item.matches("\\d+")) {
                s2.add(item);
            } else if (item.equals("(")) {
                s1.push(item);
            } else if (item.equals(")")) {
                //如果是右括号“)",则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
                while (!s1.peek().equals("(")) {
                    s2.add(s1.pop());
                }
                s1.pop(); //将 ( 弹出 s1栈,消除小括号
            } else {
                //当item的优先级小于等于s1栈顶,将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较
                //问题: 缺少一个比较优先级高低的方法
                while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
                    s2.add(s1.pop());
                }
                //还需要将item压入栈
                s1.push(item);
            }
        }

        //将s1 中剩余的运算符依次弹出并加入 s2
        while (s1.size() != 0) {
            s2.add(s1.pop());
        }
        return s2; //注意:因为存放到List, 因此按顺序输出就是对应的后缀表达式
    }


    //方法:将中缀表达式转成对应的List
    public static List<String> toInfixExpression(String s) {
        //定义一个List,存放中缀表达式对应的内容
        ArrayList<String> ls = new ArrayList<>();
        int i = 0; //辅助指针,用来遍历中缀表达式字符串
        String str = ""; //用来对多位数的拼接工作
        char c; //每遍历一个字符,就存放到c
        do {
            //如果c是非数字,就需要加入ls
            // ASCII码数字范围[48,57]
            if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
                ls.add(c + "");
                i++;
            } else { //如果是数子,需要考虑多位数问题
                str = ""; //先将str制空
                while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) {
                    str += c; //拼接
                    i++;
                }
                ls.add(str);
            }
        } while (i < s.length());
        return ls;
    }


    //将一个逆波兰表达式,依次将数据和运算符 放到ArrayList中
    public static List<String> getListString(String suffixExpression) {
        //将 suffixExpression 分割
        String[] split = suffixExpression.split(" ");
        ArrayList<String> list = new ArrayList<>();
        for (String ele : split) {
            list.add(ele);
        }
        return list;
    }

    //完成逆波兰表达式的运算

    /**
     * 1. 从左至右扫描,将3和4压入堆栈;
     * 2. 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
     * 3. 将5入栈;
     * 4. 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
     * 5. 将6入栈;
     * 6. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
     */
    public static int calculate(List<String> ls) {
        //创建栈
        Stack<String> stack = new Stack<>();
        //遍历 ls
        for (String item : ls) {
            //使用正则表达式取出数
            if (item.matches("\\d+")) { //匹配多位数
                //入栈
                stack.push(item);
            } else { //运算符
                //pop出两个数,并运算,再运算
                int num2 = Integer.parseInt(stack.pop());
                int num1 = Integer.parseInt(stack.pop());
                int res = 0;
                if (item.equals("+")) {
                    res = num1 + num2;
                } else if (item.equals("-")) {
                    res = num1 - num2;
                } else if (item.equals("*")) {
                    res = num1 * num2;
                } else if (item.equals("/")) {
                    res = num1 / num2;
                } else {
                    throw new RuntimeException("运算符非法...");
                }
                //把res 入栈
                stack.push(String.valueOf(res));
            }
        }
        //最后留在stack的数据就是运算结果
        return Integer.parseInt(stack.pop());
    }
}

//编写一个类 Operation 可以返回一个运算符 对应的优先级
class Operation {
    private static int ADD = 1;
    private static int SUB = 1;
    private static int MUL = 2;
    private static int DIV = 2;

    //编写一个方法,返回对应的优先级数字
    public static int getValue(String operation) {
        int result = 0;
        switch (operation) {
            case "+":
                result = ADD;
                break;
            case "-":
                result = SUB;
                break;
            case "*":
                result = MUL;
                break;
            case "/":
                result = DIV;
                break;
            default:
                System.out.println("不存在该运算符");
                break;
        }
        return result;
    }

}

递归调用机制

递归,就是再运行过程中调用自己

例如:从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?‘从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……’”

image-20220822142448238

迷宫问题

package com.yhs.recursion;

public class MiGong {
    public static void main(String[] args) {

        //创建一个二维数组,模拟迷宫
        //地图
        int[][] map = new int[8][7];
        //使用1表示墙
        //上下全部置为1
        for (int i = 0; i < 7; i++) {
            map[0][i] = 1;
            map[7][i] = 1;
        }
        //左右全部置为1
        for (int i = 0; i < 8; i++) {
            map[i][0] = 1;
            map[i][6] = 1;
        }
        //设置挡板
        map[3][1] = 1;
        map[3][2] = 1;
//        map[1][2] = 1;
//        map[2][2] = 1;
        //输出地图
        System.out.println("============原先的地图=============");
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j] + "\t");
            }
            System.out.println();
        }

        //使用递归回溯给小球找路
        T.setWay(map, 1, 1);

        //输出新的地图,小球走过,并标识过的地图
        System.out.println("\n============小球走过后的地图==========");
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j] + "\t");
            }
            System.out.println();
        }
        System.out.println("共走了 " + T.num + "步");
    }

    //使用递归额lai小球找路
    //说明
    //1. map 表示地图
    //2. i, j 表示从地图的哪个位置开始出发(1,1)
    //3. 如果小球能到map[6][5] 则表示通路找到
    //4. 当map[i][j] 为0表示没有走过 当为1表示墙,为2表示通路 3表示该位置已经走过但是走不通
    //5. 在走迷宫时需要确定一个策略,(下->右->上->左),如果该点走不通,再回溯
    static class T {
        static int num = 0; //定义一个变量记录步数
        /**
         * @param map 表示地图
         * @param i   从哪个位置开始找
         * @param j
         * @return 如果找到通路,就返回true,否则返回false
         */
        public static boolean setWay(int[][] map, int i, int j) {
            num++;
            if (map[6][5] == 2) { //通路已找到
                return true;
            } else {
                if (map[i][j] == 0) { //如果当前这个点没有走过
                    //按照策略
                    map[i][j] = 2; //假定该点可以走通
                    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 if (setWay(map, i - 1, j)) { //向上走
                        return true;
                    } else {
                        //说明该点走不通,是死路
                        map[i][j] = 3;
                        return false;
                    }
                } else { //如果map[i][j] != 0, 可能是1,2,3
                    return false;
                }
            }
        }
    }
}

八皇后问题

问题表述:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。如果经过±90度、±180度旋转,和对角线对称变换的摆法看成一类,共有42类。

八皇后问题

image-20220822180153017

代码实现

package com.yhs.recursion;

public class Queen8 {
    //定义一个max表示一共有多少个皇后
    int max = 8;
    //定义数组array,保存皇后放置位置的结果,比如arr = {0, 4, 7, 5, 2, 6, 1, 3}
    int[] arr = new int[max];

    static int count = 0;
    static int judgeCount = 0;

    public static void main(String[] args) {
        Queen8 queen8 = new Queen8();
        queen8.check(0);
        System.out.println("共有=" + count + "个解法");
        System.out.printf("一共判断了%d次", judgeCount);
    }

    //编写一个方法,放置第n个皇后
    //特别注意:check 是每一次递归时,进入到check中都有 for(int i = 0; i < max; i++) 因此会有回溯
    private void check(int n) {
        if (n == max) { //n=8 因为n是从0开始,所以此时表示该放第9个皇后,即前8个皇后已经摆放好
            print();
            return;
        }
        //依次放入皇后,并判断是否冲突
        for (int i = 0; i < max; i++) {
            //先把当前这个皇后n, 放到该行的第1列
            arr[n] = i;
            //判断当放置第n个皇后到i时,是否冲突
            if (judge(n)) { //不冲突
                //接着放n+1个皇后,即开始递归
                check(n + 1);
            }
            //如果冲突,就继续执行arr[n] = i; 即将第n个皇后,放置在本行的后移的一个位置i++
        }
    }


    //查看当我们放置第n个皇后,就去检测该皇后是否和前面已经摆放的皇后冲突

    /**
     * @param n 表示第n个皇后,同时n也代表第几行
     * @return
     */
    private boolean judge(int n) {
        judgeCount++;
        for (int i = 0; i < n; i++) {
            //说明
            //1. arr[i] == arr[n] 表示判断第n个皇后是否和前面的n-1个皇后在同一列
            //2. Math.abs(n-i) == Math.abs(arr[n] - arr[i]) 表示判断第n个皇后是否和第i个皇后在同一斜线
            //   即:列差=行差时在同一条斜线上
            //3. 判断是否在同一行,没有必要,n 每次都在递增,不可能处于同一行
            if (arr[i] == arr[n] || Math.abs(n - i) == Math.abs(arr[n] - arr[i])) {
                return false;
            }
        }
        return true;
    }

    //写一个方法,可以将皇后摆放的位置输出
    private void print() {
        count++;
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "\t");
        }
        System.out.println();
    }
}

哈希表(散列)

*哈希表(Hash table,也叫散列表),是**根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。**这个映射函数叫做**散列函数**,存放记录的数组叫做**散列表**。***

记录的存储位置=f(关键字)

这里的对应关系f称为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。

哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。(或者:把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。)
而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。

数组的特点是:寻址容易,插入和删除困难;

而链表的特点是:寻址困难,插入和删除容易。

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:

img

*左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们**根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。*

import java.util.Scanner;

public class HashTableDemo {
    public static void main(String[] args) {
        //创建一个哈希表
        HashTab hashTab = new HashTab(7);
        //写一个简单的菜单
        String key = "";
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("add: 添加雇员");
            System.out.println("list: 显示雇员信息");
            System.out.println("find: 查找雇员");
            System.out.println("delete: 删除雇员");
            System.out.println("exit: 退出系统");

            key = scanner.nextLine();
            switch (key) {
                case "add":
                    System.out.print("输入id: ");
                    int id = scanner.nextInt();
                    System.out.print("输入姓名: ");
                    String name = scanner.next();
                    //创建雇员
                    Emp emp = new Emp(id, name);
                    hashTab.add(emp);
                    break;
                case "list":
                    hashTab.list();
                    break;
                case "find":
                    System.out.print("输入要查找员工的id: ");
                    id = scanner.nextInt();
                    hashTab.findById(id);
                    break;
                case "delete":
                    System.out.println("输入删除的员工id: ");
                    id = scanner.nextInt();
                    hashTab.deleteById(id);
                    break;
                case "exit":
                    scanner.close();
                    System.exit(0);
            }
        }
    }
}

//创建hashTab 管理多条链表
class HashTab {
    private EmpLinkedList[] empLinkedListArray;
    private int size; //定义链表数量

    //构造器
    public HashTab(int size) {
        this.size = size;
        empLinkedListArray = new EmpLinkedList[size];
        for (int i = 0; i < size; i++) {
            empLinkedListArray[i] = new EmpLinkedList();
        }
    }

    public void add(Emp emp) {
        //根据员工的id,得到该员工应该添加到哪条链表
        int empLinkedListNo = hashFun(emp.id);
        //将emp添加到对应的链表中
        empLinkedListArray[empLinkedListNo].add(emp);
    }

    //遍历所有链表
    public void list() {
        for (int i = 0; i < size; i++) {
            empLinkedListArray[i].list(i);
        }
    }

    //根据输入的id查找
    public void findById(int id) {
        //首先根据id,利用散列函数得到该员工属于哪条链表
        EmpLinkedList empLinkedList = empLinkedListArray[hashFun(id)];
        Emp emp = empLinkedList.findEmpById(id);
        if (emp != null) {
            System.out.printf("在第 %d 号链表找到该员工 员工id=%d", hashFun(id), id);
        }
    }

    //根据id删除
    public void deleteById(int id) {
        EmpLinkedList empLinkedList = empLinkedListArray[hashFun(id)];
        empLinkedList.deleteById(id);
    }

    //编写散列函数,使用一个简单取模法
    public int hashFun(int id) {
        return id % size;
    }
}


class Emp {
    public int id;
    public String name;
    public Emp next; //next默认为空

    public Emp(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

//创建一个LinkedList,表示链表
class EmpLinkedList {
    //头指针,指向第一个Emp,因此这个链表的head是直接指向第一个Emp
    private Emp head = new Emp(0, ""); //初始化一个头节点,不存放数据

    //添加雇员到链表
    //说明:
    //1. 假定:当添加雇员时,id是自增长的,即id的分配是从小到大
    //   因此我们将该雇员直接加入到本链表的最后即可
    public void add(Emp emp) {
        Emp temp = head;
        //如果不是第一个雇员,添加辅助指针帮助定位到最后
        while (true) {
            if (temp.next == null) {
                break;
            }
            temp = temp.next;
        }
        //退出时将emp挂载
        temp.next = emp;
    }

    //遍历链表的雇员信息
    public void list(int no) {
        if (head.next == null) { //链表为空
            System.out.println("====> 第 " + no + " 条链表为空");
            return;
        }
        System.out.print("第 " + no + " 条链表的信息为:  ");
        Emp curEmp = head.next; //辅助指针
        while (true) {
            System.out.printf("=> id=%d name=%s\t", curEmp.id, curEmp.name);

            if (curEmp.next == null) { //说明curEmp是最后节点
                break;
            }
            curEmp = curEmp.next;
        }
        System.out.println();
    }

    //查找
    public Emp findEmpById(int id) {
        //先判断是否为空
        if (head.next == null) {
            System.out.println("链表为空");
            return null;
        }
        //定义一个辅助指针,帮助查找
        Emp curEmp = head.next;
        while (true) {
            if (curEmp.id == id) {
                break;
            }
            if (curEmp.next == null) {
                System.out.println("该员工未查询到...");
                break;
            }
            curEmp = curEmp.next; //后移
        }
        return  curEmp;
    }

    public void deleteById(int id) {
        Emp temp = head;
        boolean flag = false;
        while (true) {
            if (temp.next == null) {
                break;
            }
            if (temp.next.id == id) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            System.out.println("删除成功");
            temp.next = temp.next.next;
        } else {
            System.out.printf("删除失败...该员工id=%d不存在", id);
        }
    }
}

树结构

树

  • 结点的度:结点拥有的子树的数目。eg:结点 A 的度为3
  • 树的度:树种各结点度的最大值。eg:树的度为3
  • 叶子结点:度为 0 的结点。eg:E、F、C、G 为叶子结点
  • 孩子结点:一个结点的子树的根节点。eg:B、C、D 为 A 的子结点
  • 双亲结点:B 为 A 的子结点,那么 A 为 B 的双亲结点
  • 兄弟结点:一个双亲结点结点的孩子互为兄弟结点。eg:B、C、D 为兄弟结点
  • 结点的层次:根节点为第一层,子结点为第二层,依次向下递推…eg:E、F、G 的层次均为 3
  • 树的深度:树种结点的最大深度。eg:该树的深度为 3
  • 森林:m 棵互不相交的树称为森林

树结构的性质

  1. 非空树的结点总数等于树种所有结点的度之和加 1
  2. 度为 K 的非空树的第 i 层最多有 ki-1 个结点(i >= 1)
  3. 深度为 h 的 k 叉树最多有(kh - 1)/(k - 1)个结点
  4. 具有 n 个结点的 k 叉树的最小深度为 logk(n(k-1)+1))

二叉树

二叉树是一种特殊的树:它或者为空或者由一个根节点加上根节点的左子树和右子树组成,这里要求左子树和右子树互不相交,且同为二叉树,很显然,这个定义是递归形式的。

满二叉树

除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树

结点总数=2^n-1 n为层数

完全二叉树
  • 若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树
  • 一维数组可以作为完全二叉树的存储结构,堆排序使用的数据结构就是完全二叉树
前中后序遍历

遍历方式

  • 前序遍历:root -> left -> right 【根结点放在前面】
  • 中序遍历:left -> root -> right 【根结点放在中间】
  • 后续遍历:left ->right -> root 【根结点放在后面】
  • 层序遍历:按照层次遍历

image-20220830125904314

代码实现

package com.yhs.tree;

public class BinaryTreeDemo {
    public static void main(String[] args) {
        //创建一个二叉树
        BinaryTree binaryTree = new BinaryTree();
        //创建需要的结点
        HeroNode root = new HeroNode(1, "乔丹");
        HeroNode node2 = new HeroNode(2, "詹姆斯");
        HeroNode node3 = new HeroNode(3, "约翰逊");
        HeroNode node4 = new HeroNode(4, "库里");
        HeroNode node5 = new HeroNode(5, "邓肯");

        //一、手动创建二叉树
        root.setLeft(node2);
        root.setRight(node3);
        node3.setRight(node4);
        node3.setLeft(node5);
        binaryTree.setRoot(root);

        //测试
        System.out.println("========前序遍历======");
        binaryTree.preOrder();
        System.out.println("========中序遍历======");
        binaryTree.infixOrder();
        System.out.println("========后序遍历======");
        binaryTree.rearOrder();
    }
}

//定义一个BinaryTree二叉树
class BinaryTree {
    private HeroNode root;

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    //前序遍历
    public void preOrder() {
        if (this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空无法遍历");
        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.root != null) {
            this.root.infixOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    //后续遍历
    public void rearOrder() {
        if (this.root != null) {
            this.root.rearOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }
}

class HeroNode {
    private int no;
    private String name;
    private HeroNode left;
    private HeroNode right;

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    //编写前序遍历方法
    public void preOrder() {
        //先输出父节点
        System.out.println(this);
        //递归向左子树前序遍历
        if (this.left != null) {
            this.left.preOrder();
        }
        //递归向右子树前序遍历
        if (this.right != null) {
            this.right.preOrder();
        }
    }

    //中序遍历方法
    public void infixOrder() {
        //先递归向左子树中序遍历
        if (this.left != null) {
            this.left.infixOrder();
        }
        //输出父节点
        System.out.println(this);
        //再递归向右子树中序遍历
        if (this.right != null) {
            this.right.infixOrder();
        }
    }

    //后续遍历
    public void rearOrder() {
        //先递归向左子树中序遍历
        if (this.left != null) {
            this.left.rearOrder();
        }
        //再递归向右子树中序遍历
        if (this.right != null) {
            this.right.rearOrder();
        }
        //最后输出父节点
        System.out.println(this);
    }
}
前中后序查找

image-20220830142141519

代码实现

//前序遍历查找
public HeroNode preOrderSearch(int no) {
    System.out.println("前序....");
    //比较当前结点
    if (this.no == no) {
        return this;
    }
    //1. 判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
    //2. 如果左递归前序查找,找到结点,则返回
    HeroNode resNode = null;
    if (this.left != null) {
        resNode = this.left.preOrderSearch(no);
    }
    if (resNode != null) {
        return resNode;
    }
    //1. 左递归前序查找,找到结点,则返回,否则继续判断
    //2. 当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
    if (this.right != null) {
        resNode = this.right.preOrderSearch(no);
    }
    return resNode;
}


//中序遍历查找
public HeroNode infixOrderSearch(int no) {
    //判断当前结点的左节点是否为空,如果不为空,则递归查找
    HeroNode resNode = null;
    if (this.left != null) {
        resNode = this.left.infixOrderSearch(no);
    }
    if (resNode != null) {
        return resNode;
    }
    System.out.println("中序查找...");
    if (this.no == no) {
        return this;
    }
    //否则计息进行右递归的中序查找
    if (this.right != null) {
        resNode = this.right.infixOrderSearch(no);
    }
    return resNode;
}

//后续遍历查找
public HeroNode rearOrderSearch(int no) {
    //判断当前结点的左节点是否为空,如果不为空,递归查找
    HeroNode resNode = null;
    if (this.left != null) {
        resNode = this.left.rearOrderSearch(no);
    }
    if (resNode != null) {
        return resNode;
    }
    if (this.right != null) {
        resNode = this.right.rearOrderSearch(no);
    }
    if (resNode != null) {
        return resNode;
    }
    System.out.println("后序查找...");
    if (this.no == no) {
        return this;
    }
    return resNode;
}
二叉树删除结点

image-20220830204054543

代码实现

//删除
public void deleteNode(int no) {
    //1. 当当前结点的左子结点不为空,并且左子结点就是要删除的结点,就将this.left = null,并结束递归删除
    if (this.left != null && this.left.no == no) {
        this.left = null;
        return;
    }
    //2.  当当前结点的右子结点不为空,并且右子结点就是要删除的结点,就将this.right = null,并结束递归删除
    if (this.right != null && this.right.no == no) {
        this.right = null;
        return;
    }
    //3. 需要向左子树进行递归删除
    if (this.left != null) {
        this.left.deleteNode(no);
    }
    //4. 向右子树递归删除
    if (this.right != null) {
        this.right.deleteNode(no);
    }
}
顺序存储二叉树
  1. 顺序存储二叉树通常是完全二叉树
  2. 第n个元素的左子结点 => 2 * n + 1
  3. 第n个元素的右子节点 => 2 * n + 2
  4. 第n个元素的父节点为 (n-1) /2
  5. n 表示二叉树中的第几个元素(按 0 开始编号 如图所示)

image-20220831132510259

package com.yhs.tree;

public class ArrBinaryTreeDemo {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7};
        //创建一个ArrBinaryTree
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        arrBinaryTree.preOrder();

    }
}
//编写一个ArrayBinaryTree ,实现顺序存储二叉树遍历
class ArrBinaryTree {
    private int[] arr; //存储数据结点的数组

    public ArrBinaryTree(int[] arr) {
        this.arr = arr;
    }

    //重载方法
    public void preOrder() {
        this.preOrder(0);
    }

    //编写一个方法,完成顺序存储二叉树的前序遍历
    //index 表示数组下标
    public void preOrder(int index) {
        //如果数组为空,或者arr.length = 0;
        if (arr == null || arr.length == 0) {
            System.out.println("数组为空,不能按照二叉树的前序遍历");
        }
        //输出当前这个元素
        System.out.println(arr[index]);
        //向左递归遍历
        if (index * 2 + 1 < arr.length) {
            preOrder(2 * index + 1);
        }
        //向右递归遍历
        if (index * 2 + 2 < arr.length) {
            preOrder(2 * index + 2);
        }
    }
}
线索化二叉树

在介绍线索化二叉树之前,我们先看一个案例,我先画一棵二叉树,如图1所示:

5cf0f56a3952078ab507ac03f6cf4ef2.png

如果我们对图1中的二叉树进行中序遍历时,结果为:4 2 5 1 3 6 ,我们认真观察图1中的二叉树时,发现 4 5 6 节点的左右指针没有用上,3节点的左指针也没用上;如果我们希望利用到各个节点的左右指针,让各个节点可以指向自己的前后节点,那我们就使用线索化二叉树。

线索化二叉树的基本介绍;

(1)前驱节点:就是以中序遍历方式遍历二叉树,某一个节点的前一个节点,例如图1中的5节点,看图1中的中序遍历结果:4 2 5 1 3 6 ,5节点的前面是2节点,所以5节点的前驱节点是2节点;一个节点有前驱节点的前提是该节点的左子节点是空的并且该节点有前一个节点,例如5节点的左子节点就为空且中序遍历出来时,5节点就有前一个节点,而4节点没有前一个节点,所以没有前驱节点;通常前驱节点是该节点的父节点或者父节点的父节点,依此类推。

(2)后继节点:就是以中序遍历方式遍历二叉树,某一个节点的后一个节点,例如图1中的5节点,看图1中的中序遍历结果:4 2 5 1 3 6 ,5节点的后面是1节点,所以5节点的后继节点是1节点;一个节点有后继节点的前提是该节点的右子节点是空的并且该节点后面有一个节点,例如5节点的右子节点就为空且中序遍历出来时,5节点就有后一个节点,而6节点的右子节点虽然为空但是后面没有节点了,所以6节点没有后继节点;通常后继节点是该节点的父节点或者父节点的父节点,依此类推。

(3)n 个结点的二叉链表中含有n+1(公式2n-(n-1)=n+1)个空指针域;利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")。

(4)这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树。

好,我们将图1的二叉树加上指向前驱节点的指针和指向后继节点的指针,然后就得到如下图2的中序线索二叉树;

img

中序线索化

package com.yhs.tree;

public class ThreadedBinaryTreeDemo {
    public static void main(String[] args) {
        //测试中序线索二叉树功能
        DemoNode root = new DemoNode(1, "鸣人");
        DemoNode node2 = new DemoNode(3, "佐助");
        DemoNode node3 = new DemoNode(6, "小樱");
        DemoNode node4 = new DemoNode(8, "雏田");
        DemoNode node5 = new DemoNode(10, "我爱罗");
        DemoNode node6 = new DemoNode(14, "大蛇丸");

        //手动创建二叉树
        root.setLeft(node2);
        root.setRight(node3);
        node2.setLeft(node4);
        node2.setRight(node5);
        node3.setLeft(node6);

        //测试中序线索化
        TBinaryTree tBinaryTree = new TBinaryTree();
        tBinaryTree.setRoot(root);
        tBinaryTree.threadedNode();

        //测试【以10号结点】
        DemoNode node5Left = node5.getLeft();
        System.out.println("前驱结点:" + node5Left);
        DemoNode node5Right = node5.getRight();
        System.out.println("后继结点:" + node5Right);

        //用线索化的方式遍历
        tBinaryTree.threadedList();


    }
}

//TBinaryTree 实现了线索化功能的二叉树
class TBinaryTree {
    private DemoNode root;

    //为了实现线索化,需要创建指向当前结点的前驱结点的指针
    //在递归进行线索化时,pre 总是保留前一个结点
    private DemoNode pre = null;

    public void setRoot(DemoNode root) {
        this.root = root;
    }

    //重载方法
    public void threadedNode() {
        this.threadedNode(root);
    }

    //遍历线索化二叉树
    public void threadedList() {
        //定义一个变量,存储当前遍历的结点,从root开始
        DemoNode node = root;
        while (node != null) {
            //循环找到leftType==1的结点
            //当leftType==1时,说明该结点是按照线索化处理的有效结点
            while (node.getLeftType() == 0) {
                node = node.getLeft();
            }
            //打印当前结点
            System.out.println(node);
            //如果当前结点的右指针指向的是后继结点,就一直输出
            while (node.getRightType() == 1) {
                //获取到当前结点的后继结点
                node = node.getRight();
                System.out.println(node);
            }
            //替换这个遍历的结点
            node = node.getRight();
        }
    }



    //编写对二叉树进行中序线索化的方法
    public void threadedNode(DemoNode node) {
        //如果node == null 不能线索化
        if (node == null) {
            return;
        }
        //(一)先线索化左子树
        threadedNode(node.getLeft());
        //(二)线索化当前结点
        //1. 处理当前结点的前驱结点
        if (node.getLeft() == null) {
            //让当前结点的左指针指向前驱结点
            node.setLeft(pre);
            //修改当前结点的左指针的类型
            node.setLeftType(1);//指向前驱结点
        }
        //2. 处理后继结点
        if (pre != null && pre.getRight() == null) {
            //让前驱结点的右指针指向当前结点
            pre.setRight(node);
            //修改前驱结点的右指针类型
            pre.setRightType(1);
        }
        //3. 每处理一个结点后,让当前结点是下一个结点的前驱结点
        pre = node;
        //(三)再线索化右子树
        threadedNode(node.getRight());
    }
}


//创建CatNode结点
class DemoNode {
    private int no;
    private String name;
    private DemoNode left;
    private DemoNode right;

    //说明
    //1. 如果leftType == 0 表示指向的是左子树,如果 == 1 表示指向前驱结点
    //2. 如果rightType == 0 表示指向的是右子树,如果 == 1 表示指向后继结点
    private int leftType;
    private int rightType;

    public int getLeftType() {
        return leftType;
    }

    public void setLeftType(int leftType) {
        this.leftType = leftType;
    }

    public int getRightType() {
        return rightType;
    }

    public void setRightType(int rightType) {
        this.rightType = rightType;
    }

    public DemoNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public DemoNode getLeft() {
        return left;
    }

    public void setLeft(DemoNode left) {
        this.left = left;
    }

    public DemoNode getRight() {
        return right;
    }

    public void setRight(DemoNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "DemoNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}

前序线索化

//编写对二叉树进行前序线索化的方法
public void preThreadNode(DemoNode node) {
    if (node == null) {
        return;
    }
    //(一)线索化当前结点
    if (node.getLeft() == null) {
        node.setLeft(pre);
        node.setLeftType(1);
    }
    if (pre != null && pre.getRight() == null) {
        pre.setRight(node);
        pre.setRightType(1);
    }
    pre = node;
    //(二)线索化左子树
    if (node.getLeftType() == 0) {
        preThreadNode(node.getLeft());
    }
    if (node.getRightType() == 0) {
        preThreadNode(node.getRight());
    }
}

前序线索化遍历

//遍历线索化二叉树[前序]
public void preThreadedList() {
    DemoNode node = root;
    while (node != null) {
        while (node.getLeftType() == 0) {
            System.out.println(node);
            node = node.getLeft();
        }
        System.out.println(node);
        while (node.getRightType() == 1) {
            node = node.getRight();
            System.out.println(node);
        }
        node = node.getRight();
    }
}

赫夫曼树

基本概念

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近

  • 路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
  • 结点的路径长度:两结点间路径上的分支数。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWFVOfk1MRg==,size_20,color_FFFFFF,t_70,g_se,x_16

  • 树的路径长度:从树根到每一个结点的路径长度之和。记作TL
  • 结点树目相同的二叉树中,完全二叉树是路径长度最短的二叉树

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWFVOfk1MRg==,size_20,color_FFFFFF,t_70,g_se,x_16

  • 权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
  • 1结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
  • 树的带权路径长度:树中所有叶子节点的带权路径长度之和。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWFVOfk1MRg==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWFVOfk1MRg==,size_20,color_FFFFFF,t_70,g_se,x_16

满二叉树不一定是最优二叉树

  • 哈夫曼树中权越大的叶子离根越近
  • 具有相同带权结点的哈夫曼树不唯一

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAWFVOfk1MRg==,size_20,color_FFFFFF,t_70,g_se,x_16

代码实现
import java.util.ArrayList;
import java.util.Collections;

public class HuffmanTree {
    public static void main(String[] args) {
        int[] arr = {13, 7, 8, 3, 29, 6, 1};
        Node root = creatHuffmanTree(arr);
        pre(root);
    }

    //编写一个前序遍历的方法
    public static void pre(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("空树一颗,不能遍历");
        }
    }

    /**
     * 创建赫夫曼树
     *
     * @param arr 需要创建成哈夫曼树的数组
     * @return 创建号后的赫夫曼树的根结点
     */
    public static Node creatHuffmanTree(int[] arr) {
        ArrayList<Node> nodes = new ArrayList<>();
        //1. 遍历arr数组
        //2. 将arr每个元素构成一个Node
        //将数组的元素依次取出构建成nodes放入集合
        for (int value : arr) {
            nodes.add(new Node(value));
        }

        while (nodes.size() > 1) {
            //从小到达排序
            Collections.sort(nodes);
            System.out.println(nodes);

            //取出根结点权值最小的两颗二叉树
            //(1)取出权值最小的结点(二叉树)
            Node leftNode = nodes.get(0);
            //(2)取出权值第二小的结点(二叉树)
            Node rightNode = nodes.get(1);
            //(3)构建一颗新的二叉树
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;
            //(4)从ArrayList删除处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //(5)将parent加入到nodes
            nodes.add(parent);
        }
        //返回赫夫曼树的root结点
        return nodes.get(0);
    }
}

class Node implements Comparable<Node> {
    public int value; //权值
    public Node left;
    public Node right;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    @Override
    public int compareTo(Node o) {
        //从小到大排序
        return this.value - o.value;
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}
赫夫曼编码—数据压缩

创建赫夫曼树

package com.yhs.huffmantree;

import java.util.*;

public class HuffmanCode {
    public static void main(String[] args) {
        String str = "i like like like java do you like a java";
        byte[] contentBytes = str.getBytes();
        System.out.println(contentBytes.length); //40

        List<CNode> nodes = getNodes(contentBytes);
        System.out.println(nodes);

        //测试,创建的二叉树
        System.out.println("赫夫曼树");
        CNode huffmanTreeRoot = creatHuffmanTree(nodes);
        System.out.println("前序遍历");
        pre(huffmanTreeRoot);
    }

    //前序遍历方法
    public static void pre(CNode root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("空树一颗...");
        }
    }

    /**
     * @param bytes 接收字节数组
     * @return 返回的就是 List 形式 [Node[date=97, weight=5], Node[date=32, weight=9]...
     */
    public static List<CNode> getNodes(byte[] bytes) {
        //1. 创建一个ArrayList
        ArrayList<CNode> cNodes = new ArrayList<>();

        //遍历bytes, 统计每一个byte出现的次数-> map[key, value]
        HashMap<Byte, Integer> counts = new HashMap<>();
        for (Byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }
        //把每个键值对转成一个CNode 对象,并加入到cNodes集合
        //遍历
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            cNodes.add(new CNode(entry.getKey(), entry.getValue()));
        }
        return cNodes;
    }

    //可以通过List 创建对应的赫夫曼树
    public static CNode creatHuffmanTree(List<CNode> nodes) {

        while (nodes.size() > 1) {
            //排序:从小到大
            Collections.sort(nodes);
            //取出第一棵最小的二叉树
            CNode leftNode = nodes.get(0);
            //取出第二棵最小的二叉树
            CNode rightNode = nodes.get(1);
            //创建一棵新的二叉树,它的根结点没有data,只有权值
            CNode parent = new CNode(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            //已经处理的两棵二叉树从nodes删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树,加入到nodes
            nodes.add(parent);
        }
        //nodes 最后的结点就是 哈夫曼树的根结点
        return nodes.get(0);
    }
}

//创建Node
class CNode implements Comparable<CNode> {
    Byte data;
    int weight;
    CNode left;
    CNode right;

    public CNode(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public int compareTo(CNode o) {
        //从小到大排序
        return this.weight - o.weight;
    }

    @Override
    public String toString() {
        return "CNode{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

生成赫夫曼编码

//生成赫夫曼树对应的赫夫曼编码
//思路:
//1. 将赫夫曼编码存放到Map<Byte, String>形式
//   32->01 97->100 100->11000等等
static Map<Byte, String> huffmanCodes = new HashMap<>();
//2. 在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();

    /**
     * 功能: 将传入的node结点的所有叶子结点的赫夫曼编码得到,并传入到huffmanCodes集合
     * @param node 传入的结点
     * @param code 路径:左子节点 0,右子结点 1
     * @param stringBuilder 用于拼接路径
     */
public static void getCodes(DogNode node, String code, StringBuilder stringBuilder) {
    StringBuilder stringBuilder02 = new StringBuilder(stringBuilder);
    //将code 加入到stringBuilder02
    stringBuilder02.append(code);
    if (node != null) { //如果node == null不处理
        //判断当前node是叶子结点还是非叶子结点
        if (node.data == null) { //非叶子结点
            //递归处理
            //向左
            getCodes(node.left, "0", stringBuilder02);
            //向右
            getCodes(node.right, "1", stringBuilder02);
        } else { //叶子结点
            huffmanCodes.put(node.data, stringBuilder02.toString());
        }
    }
}

   //为了调用方便,重载getCodes
    private static Map<Byte, String> getCodes(DogNode root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left, "0", stringBuilder);
        //处理root的右子树
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

赫夫曼编码字节数组

private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
    //1. 利用huffmanCodes 将 bytes 转成 赫夫曼编码对应的字符串
    StringBuilder stringBuilder = new StringBuilder();
    //遍历byte数组
    for (byte b : bytes) {
        stringBuilder.append(huffmanCodes.get(b));
    }
    //将 "10101000101111110..." 转成 byte[]

    //统计返回 byte[] huffmanCodeBytes 长度
    //一句话: int len = (stringBuilder.length() + 7) / 8;
    int len;
    if (stringBuilder.length() % 8 == 0) {
        len = stringBuilder.length() / 8;
    } else {
        len = stringBuilder.length() / 8 + 1;
    }
    //创建存储压缩后的byte数组
    byte[] huffmanCodeBytes = new byte[len];
    int index = 0;
    for (int i = 0; i < stringBuilder.length(); i += 8) {
        String strByte;
        if (i + 8 > stringBuilder.length()) {
            strByte = stringBuilder.substring(i);
        } else {
            strByte = stringBuilder.substring(i, i + 8);
        }
        //将strByte 转成一个byte,放入到huffmanCodeBytes
        huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
        index++;
    }
    return huffmanCodeBytes;
}

赫夫曼字节数组封装

//使用一个方法,将前面的方法封装起来,便于调用
public static byte[] huffmanZip(byte[] bytes) {
    List<DogNode> dogNodes = getNode(bytes);
    //根据 dogNodes 创建赫夫曼树
    DogNode huffmanTreeRoot = create(dogNodes);
    //对应的赫夫曼编码(根据赫夫曼树)
    Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
    //根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
    byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
    return huffmanCodeBytes;
}
赫夫曼编码—数据解码
//完成数据解压
    //思路
    //1. 将huffmanCodeBytes [-88, -65, -56, -65, -56...]
    //   重新先转成赫夫曼编码对应的二进制字符串 "1010100010111..."
    //2. 赫夫曼编码对应的二进制的字符串"1010100010111..." -> 对照 赫夫曼编码 -> "i like like like java do you like a java"

    public static byte[] decode(byte[] huffmanBytes, Map<Byte, String> huffmanCodes) {
        // 首先转化成二进制符号,然后拼接成字符串-注意末尾byte的处理
        StringBuilder str = new StringBuilder();
//        boolean flag;
        byte b;
        for (int i = 0; i < huffmanBytes.length - 1; i++) {
            //判断是否被8整除,富国整除就不用判断最后一个数据,全部需要补位截取
            b = huffmanBytes[i];
            str.append(byteToBitString(true, b));
        }
        // 单独处理最后一位byte
        b = huffmanBytes[huffmanBytes.length - 1];
        String lastByteStr = byteToBitString(false, b);
        // 如果长度相等,直接拼接
        if (str.length() + lastByteStr.length() == huffmanBytes.length) {
            str.append(lastByteStr);
        } else {
            // 如果长度不够,那就先补0,直到总长度相等,再拼接
            while (str.length() + lastByteStr.length() < huffmanBytes.length) {
                str.append(0);
            }
            str.append(lastByteStr);
        }
        System.out.println("解码后的赫夫曼编码字符串为: " + str.toString() + "\n长度为: " + str.length());

        //把字符串按照指定的赫夫曼编码进行解码
        //把赫夫曼编码表进行调换,因为要反向查询 a->100 100->a
        Map<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }

        //创建一个集合,存放byte
        ArrayList<Byte> list = new ArrayList<>();
        for (int i = 0; i < str.length();) {
            int count = 1;
            // 遍历查找的结束标识
            boolean loop = true;
            String key = null;

            while (loop && (i+count) <= str.length()) {
                //截取字符串,进行map中key值比较
                key = str.substring(i, i + count); //i 不动,让count走
                if (map.containsKey(key)) {
                    loop = false;
                } else {
                    // 没有的话继续遍历后移
                    count++;
                }
            }
            list.add(map.get(key));
            i += count; //i 直接移动到count
        }
        //当for循环结束后,我们list中存放了所有字符 "i like like like java do you like a java"

        //把list中的数据放入到byte[] 并返回
        byte[] bytes = new byte[list.size()];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = list.get(i);
        }
        return bytes;
    }

    /**
     * 首先需要一个将byte转化为电脑储存的补码二进制形式的方法
     *
     * @param flag 标志是否需要补高位,true需要补,false不需要补
     * @param b    传入的 byte
     * @return 是该 b 对应的二进制的字符串(注意是按补码返回)
     */
    public static String byteToBitString(boolean flag, byte b) {
        // Integer中有直接转化成二进制的方法
		// 转化类型
        int temp = b;
        // 转化
		// 正数转化呢,只有后几位,---所以需要补位0 补齐8 位
        if (flag) {
            temp |= 256; //按位或 256 -> 1 0000 0000 | 0000 0001 => 1 0000 0001
        }
        String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
        if (flag || temp < 0) {
            //注意!负数转化完是int类型32位的二进制补码--所以需要取出后8位
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }
文件压缩
//编写方法,将一个文件进行压缩
public static void zipFile(String srcFile, String dstFile) {
    //创建文件的输入流
    FileInputStream fis = null;
    //创建输出流
    OutputStream os = null;
    ObjectOutput output = null;
    try {
        fis = new FileInputStream(srcFile);
        //创建一个和源文件大小一样的byte[]
        byte[] b = new byte[fis.available()];
        //读取文件
        fis.read(b);
        //直接对源文件压缩
        byte[] huffmanBytes = huffmanZip(b);
        //创建文件的输出流,存放压缩文件
        os = new FileOutputStream(dstFile);
        //创建一个和文件输出流相关联的ObjectOutputStream
        output = new ObjectOutputStream(os);
        //把 赫夫曼编码后的字节数组写入压缩文件
        output.writeObject(huffmanBytes);

        //这里以对象的方式写入 赫夫曼编码,是为了以后恢复源文件时使用
        //注意 一定要把赫夫曼编码写入压缩文件
        output.writeObject(huffmanCodes);

    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        try {
            fis.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
文件解压
public static void unZipFile(String zipFile, String dstFile) {
    //定义文件输入流
    InputStream is = null;
    //定义一个对象输入流
    ObjectInputStream ois = null;
    //定义文件输出流
    OutputStream os = null;

    try {
        //创建文件输入流
        is = new FileInputStream(zipFile);
        //创建一个和 is关联的对象输入流
        ois = new ObjectInputStream(is);
        //读取byte数组 huffmanBytes
        byte[] huffmanBytes = (byte[]) ois.readObject();
        //读取赫夫曼编码表
        Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();

        //解码
        byte[] bytes = decode(huffmanBytes, huffmanCodes);
        //将bytes 数组写入到目标文件
        os = new FileOutputStream(dstFile);
        //写入到dstFile文件
        os.write(bytes);
    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        try {
            os.close();
            ois.close();
            is.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}
压缩文件注意事项
  1. 如果文件本身就是经过压缩处理的,那么使用赫夫曼比那吗再压缩效率不会有明显变化,比如视频、ppt等文件
  2. 赫夫曼编码是按字节来处理的,因此可以处理所有 文件【二进制文件,文本文件】
  3. 如果一个文件中的内容,重复数据不多,压缩效果也不会很明显

二叉排序树

基本概念

二叉排序树(BST:Binary Sort Tree)对于二叉排序树的任意一个非叶子节点,要求左子节点的值均小于根节点的值,右子节点的值均大于根节点的值

特别说明:如果有相同的值,可以将该节点放在左子节点或者右子节点

二叉排序树的创建和遍历
public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9};
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环地添加节点到二叉排序树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }
        //中序遍历二叉排序树
        System.out.println("中序遍历二叉排序树");
        binarySortTree.infixOrder();
    }
}

//创建二叉排序树
class BinarySortTree {
    private Node root;

    //添加节点的方法
    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

    //中序遍历
    public void infixOrder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("空树一棵,无法遍历");
        }
    }
}


//创建Node节点
class Node {
    int value;
    Node left;
    Node right;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //添加节点的方法
    //递归的形式添加节点,注意需要满足二叉排序树的要求
    public void add(Node node) {
        if (node == null) {
            return;
        }
        if (node.value < this.value) {
            if (this.left == null) {
                this.left = node;
            } else {
                this.left.add(node);
            }
        } else {
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }
}
二叉排序树的删除

查找要删除的节点和要删除节点的双亲结点

//创建Node节点
class Node {
    int value;
    Node left;
    Node right;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }


    /**
     * 查找要删除的节点
     *
     * @param value 希望删除节点的值
     * @return 如果找到返回该节点,否则返回null
     */
    public Node search(int value) {
        if (this.value == value) {
            return this;
        } else {
            if (value < this.value) {
                if (this.left != null) {
                    return this.left.search(value);
                } else {
                    return null;
                }
            } else {
                if (this.right != null) {
                    return this.right.search(value);
                } else {
                    return null;
                }
            }
        }
    }

    /**
     * 查找要删除节点的父节点
     *
     * @param value
     * @return 要删除的节点的父节点,如果没有就返回null
     */
    public Node searchParent(int value) {
        //如果当前节点就是要删除的节点的父节点,就返回
        if ((this.left != null && this.left.value == value) ||
                (this.right != null && this.right.value == value)) {
            return this;
        } else {
            //如果查找的值小于当前节点的值,并且当前节点的值不为空
            if (value < this.value && this.left != null) {
                return this.left.searchParent(value); //向左子树递归寻找
            } else if (value >= this.value && this.right != null) {
                return this.right.searchParent(value); //向右子树递归寻找
            } else {
                return null;
            }
        }
    }


    //添加节点的方法
    //递归的形式添加节点,注意需要满足二叉排序树的要求
    public void add(Node node) {
        if (node == null) {
            return;
        }
        if (node.value < this.value) {
            if (this.left == null) {
                this.left = node;
            } else {
                this.left.add(node);
            }
        } else {
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }
}

分三种情况进行删除

  1. 删除叶子结点
  2. 删除只有一棵子树的结点
  3. 删除有两棵子树的结点
public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环地添加节点到二叉排序树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }
        //中序遍历二叉排序树
        System.out.println("中序遍历二叉排序树");
        binarySortTree.infixOrder();

        //测试删除叶子节点
        binarySortTree.delNode(2);
        System.out.println("删除节点后");
        binarySortTree.infixOrder();

        //测试删除有一个子树的节点
        binarySortTree.delNode(1);
        System.out.println("删除有一个子树的节点后");
        binarySortTree.infixOrder();

        //删除有两棵子树的节点
        System.out.println("删除有两棵子树的节点");
        binarySortTree.delNode(10);
        binarySortTree.infixOrder();

    }
}

//创建二叉排序树
class BinarySortTree {
    private Node root;

    //查找要删除的节点
    public Node search(int value) {
        if (root == null) {
            return null;
        } else {
            return root.search(value);
        }
    }

    //查找父节点
    public Node searchParent(int value) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(value);
        }
    }

    //(1)找到直接后继节点(targetNode右子树值最小的节点)
    public int delRightTreeMin(Node node) {
        Node temp = node;
        //循环查找左子树,直到找到值最小的节点
        while (temp.left != null) {
            temp = temp.left;
        }
        //删除最小节点
        delNode(temp.value);
        //返回最小节点对应的值
        return temp.value;
    }

    //(2)找到直接前驱节点(targetNode左子树值最大的节点)
    public int delLeftTreeMax(Node node) {
        Node temp = node;
        while (temp.right != null) {
            temp = temp.right;
        }
        delNode(temp.value);
        return temp.value;
    }


    //删除叶子节点
    public void delNode(int value) {
        if (root == null) {
            return;
        } else {
            //1. 需要先找到要删除的节点 targetNode
            Node targetNode = search(value);
            //如果没有找到要删除的节点
            if (targetNode == null) {
                return;
            }
            //如果发现这棵二叉排序树只有一个节点
            if (root.left == null && root.right == null) {
                root = null;
                return;
            }

            //去找到targetNode的父节点
            Node parent = searchParent(value);
            //如果要删除的节点是叶子节点
            if (targetNode.left == null && targetNode.right == null) {
                //判断targetNode是父节点的左子节点还是右子节点
                if (parent.left != null && parent.left.value == value) { //左子节点
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value) { //右子节点
                    parent.right = null;
                }
            } else if (targetNode.left != null && targetNode.right != null) { //删除有两棵子树的节点
                int minValue = delRightTreeMin(targetNode.right);
                targetNode.value = minValue;
            } else { //删除只有一棵子树的节点
                //如果要删除的节点有左子节点
                if (targetNode.left != null) {
                    if (parent != null) {
                        //如果targetNode是parent的左子节点
                        if (parent.left.value == value) {
                            parent.left = targetNode.left;
                        } else { //targetNode 是 parent 的右子节点
                            parent.right = targetNode.left;
                        }
                    } else {
                        root = targetNode.left;
                    }
                } else { //要删除的节点有右子节点
                    if (parent != null) {
                        //如果targetNode是parent的左子节点
                        if (parent.left.value == value) {
                            parent.left = targetNode.right;
                        } else { //如果targetNode是parent的右子节点
                            parent.right = targetNode.right;
                        }
                    } else {
                        root = targetNode.right;
                    }
                }
            }
        }
    }


    //添加节点的方法
    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

    //中序遍历
    public void infixOrder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("空树一棵,无法遍历");
        }
    }
}

平衡二叉树

为什么使用平衡二叉树

二叉树能提高查询的效率 O(logn),但是当你插入 {1,2,3,4,5,6} 这种数据的时候,你的二叉树就像一个「链表」一样,搜索效率变为 O(n)

image-20220911133000536

于是在 1962 年,一个姓 AV 的大佬(G. M. Adelson-Velsky) 和一个姓 L 的大佬( Evgenii Landis)提出「平衡二叉树」(AVL) 。

判断平衡二叉树

判断「平衡二叉树」的 2 个条件:

  • 1. 是「二叉排序树」
  • 2. 任何一个节点的左子树或者右子树都是「平衡二叉树」(左右高度差小于等于 1)
平衡二叉树的插入

平衡二叉树的插入过程的前半部分与二叉排序树相同,但在新结点插入后,若造成查找路径上的某个结点不再平衡,则需要做出相应的调整。可将调整的规律归纳为下列四种情况

(1)RR型失衡(左单旋转)

前提条件:右子树的高度减去左子树的高度大于1,即:rightHeight() - leftHeight() > 1

在这里插入图片描述

(1)创建一个新的结点newNode,使其值等于当前根结点的值;
(2)将新结点的左子树设置为当前结点的左子树:newNode.left=left;
(3)将新结点的右子树设置为当前结点的右子树的左子树:newNode.right =right.left;
(4)将当前结点的值换为右子结点的值:value=right.value;
(5)将当前结点的右子树设置成右子树的右子树:right=right.right;
(6)将当前结点的左子树设置成新结点:left=newNode。

(2)LL平衡旋转(右单旋转)

前提条件:左子树的高度减去右子树的高度大于1,即:leftHeight() - rightHeight() > 1

在这里插入图片描述

(1)创建一个新的结点newNode,使其值等于当前根结点的值;
(2)将新结点的右子树设置为当前结点的右子树:newNode.right=right;
(3)将新结点的左子树设置为当前结点的左子树的右子树:newNode.left =left.right;
(4)将当前结点的值换为左子结点的值:value=left.value;
(5)将当前结点的左子树设置成左子树的左子树:left=left.left;
(6)将当前结点的右子树设置成新结点:right=newNode。

(3)LR平衡旋转(先左后右双旋转)

前提条件:符合RR平衡旋转(左单旋转)前提下,当前结点的左子树的右子树高度大于它的左子树的左子树的高度,即:left.rightHeight()>left.leftHeight()

(1)先对当前结点的左结点进行左单旋转;
(2)再对当前结点进行右单旋转即可。

(4)RL平衡旋转(先右后左双旋转)

前提条件:符合LL平衡旋转(右单旋转)前提下,当前结点的右子树的左子树的高度大于它的右子树的右子树的高度,即: right.leftHeight()>right.rightHeight()

(1)先对当前结点的右结点进行右单旋转;
(2)再对当前结点进行左单旋转即可。

完整代码实现
package com.yhs.avl;

public class AVLTreeDemo {
    public static void main(String[] args) {
//        int[] arr = {4,3,6,5,7,8};
//        int[] arr = {10,12,8,9,7,6};
//        int[] arr = {10, 11, 7, 6, 8, 9};
        int[] arr = {30, 12, 10, 15, 17, 18, 19, 20, 39, 36, 37, 40};
        AVLTree avlTree = new AVLTree();
        //添加结点
        for (int j : arr) {
            avlTree.add(new AVLNode(j));
        }

        System.out.println("=========旋转中序遍历======");
        avlTree.infix();

        System.out.println("旋转->树的高度 " + avlTree.getRoot().height());
        System.out.println("旋转->左子树高度 " + avlTree.getRoot().leftHeight());
        System.out.println("旋转->右子树高度 " + avlTree.getRoot().rightHeight());
        System.out.println("当前根结点= " + avlTree.getRoot());
    }
}

//创建AVLTree
class AVLTree {
    private AVLNode root;

    public void add(AVLNode node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

    public void infix() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("空树...");
        }
    }

    public AVLNode getRoot() {
        return root;
    }
}


class AVLNode {
    int value;
    AVLNode left;
    AVLNode right;

    public AVLNode(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //返回左子树的高度
    public int leftHeight() {
        if (left == null) {
            return 0;
        } else {
            return left.height();
        }
    }

    //返回右子树的高度
    public int rightHeight() {
        if (right == null) {
            return 0;
        }
        return right.height();
    }

    //返回以该节点为根结点的树的高度
    public int height() {
        return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
    }

    //左旋转的方法
    private void leftRotate() {
        //创建新结点,以当前根结点的值创建
        AVLNode newNode = new AVLNode(this.value);
        //新结点的左子树设置为当前结点的左子树
        newNode.left = this.left;
        //把新结点的右子树设置为当前结点的右子树的左子树
        newNode.right = this.right.left;
        //把当前结点的值换为当前结点的右子结点的值
        this.value = this.right.value;
        //把当前结点的右子树设置为当前结点右子树的右子树
        this.right = this.right.right;
        //把当前结点的左子树设置为新节点
        this.left = newNode;
    }

    //右旋转的方法
    private void rightRotate() {
        //创建新结点,以当前根结点的值创建
        AVLNode newNode = new AVLNode(this.value);
        //新节点的右子树设置为当前结点右子树
        newNode.right = this.right;
        //新结点的左子树设置为当前结点左子树的右子树
        newNode.left = this.left.right;
        //把当前结点的值换为左子结点的值
        this.value = this.left.value;
        //把当前结点的左子树设置为当前结点左子树的左子树
        this.left = this.left.left;
        //把当前结点的右子树设置为新结点
        this.right = newNode;
    }


    public void add(AVLNode node) {
        if (node == null) {
            return;
        }
        if (node.value < this.value) {
            if (this.left == null) {
                this.left = node;
            } else {
                this.left.add(node);
            }
        } else {
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }

        //当添加完一个结点后,如果(右子树的高度-左子树高度)> 1,左旋转
        if (rightHeight() - leftHeight() > 1) {
            //如果它的右子树的左子树的高度大于它右子树的右子树高度
            if (this.right != null && this.right.leftHeight() > this.right.rightHeight()) {
                //先对当前结点的右子树进行右旋转
                this.right.rightRotate();
                //然后再对当前结点进行左旋转
                this.leftRotate();
            } else {
                leftRotate(); //左旋转
            }
            return;
        }

        //当添加完一个结点后,如果(左子树的高度-右子树高度)> 1,右旋转
        if (leftHeight() - rightHeight() > 1) {
            //如果它的左子树的右子树高度大于它左子树的左子树的高度
            if (this.left != null && this.left.rightHeight() > this.left.leftHeight()) {
                //先对当前结点的左子树进行左旋转
                this.left.leftRotate();
                //再对当前结点进行右旋转
                this.rightRotate();
            } else {
                rightRotate(); //右旋转
            }
        }
    }

    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }
}

B树

B-tree树即B树,B即Balanced

2-3树

基本介绍

  1. 2-3树的所有叶子节点都在同一层(所有B树都满足这个条件)
  2. 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点
  3. 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
  4. 2-3树是有二节点和三节点构成的树

B+树

  1. B+树内部有两种结点,一种是索引结点,一种是叶子结点。
  2. B+树的索引结点并不会保存记录,只用于索引,所有的数据都保存在B+树的叶子结点中。而B树则是所有结点都会保存数据。
  3. B+树的叶子结点都会被连成一条链表。叶子本身按索引值的大小从小到大进行排序。即这条链表是 从小到大的。多了条链表方便范围查找数据。
  4. B树的所有索引值是不会重复的,而B+树 非叶子结点的索引值 最终一定会全部出现在 叶子结点中。

为什么要有B+树

B树好处:

  • B树的每一个结点都包含key(索引值) 和 value(对应数据),因此方位离根结点近的元素会更快速。(相对于B+树)

B树的不足:

  • 不利于范围查找(区间查找),如果要找 0~100的索引值,那么B树需要多次从根结点开始逐个查找。

  • 而B+树由于叶子结点都有链表,且链表是以从小到大的顺序排好序的,因此可以直接通过遍历链表实现范围查找。

B*树

基本概念

在线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继。在树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关

注意:线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。

图创建

import java.util.ArrayList;
import java.util.Arrays;

public class Graph {

    private ArrayList<String> vertexList; //顶点集合
    private int[][] edges; //存储图对应的邻接矩阵
    private int numOfEdge; //表示边的数量

    public static void main(String[] args) {
        //测试创建图
        int n = 5; //顶点数目
        String[] Vertexes = {"A", "B", "C", "D", "E"};
        //创建图对象
        Graph graph = new Graph(n);
        //循环添加顶点
        for (String vertex : Vertexes) {
            graph.insertVertex(vertex);
        }
        //添加边
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 2, 1);
        graph.insertEdge(1, 2, 1);
        graph.insertEdge(1, 3, 1);
        graph.insertEdge(1, 4, 1);

        //显示邻接矩阵
        graph.showGraph();
    }

    //构造器
    public Graph(int n) {
        //初始化矩阵和vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
        numOfEdge = 0;
    }

    //图中常用方法
    //1. 返回顶点的数量
    public int getVertexNum() {
        return vertexList.size();
    }
    //2. 得到边的数目
    public int getEdgNum() {
        return numOfEdge;
    }
    //3.返回节点i(下标)对应的数据 0 -> "A", 1 -> "B"...
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }
    //4. 返回v1 和 v2 的权值
    public int getWeight(int v1, int v2) {
        return edges[v1][v2];
    }
    //5. 显示图对应的矩阵
    public void showGraph() {
        for (int[] link : edges) {
            System.err.println(Arrays.toString(link));
        }
    }


    //插入顶点
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    /**
     * 添加边
     * @param v1 表示顶点下标
     * @param v2 表示顶点下标
     * @param weight 表示权值
     */
    public void insertEdge(int v1, int v2, int weight) {
        //无向图
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdge++;
    }
}

图遍历

深度优先遍历(Depth First Search)也称为深度优先搜索,简称DFS

DFS算法
  1. 深度优先搜索类似于树的先序遍历,它的基本思想如下:首先访问图中某一起始顶点V,然后由V出发,访问与V相邻且未被访问的任一顶点O1,再访问O1邻接且未被访问的任一顶点…重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止
  2. 这种访问策略是优先纵向挖掘深入,而不是对一个节点所有邻接节点进行横向访问
  3. 深度优先搜索是一个递归的过程
import java.util.ArrayList;
import java.util.Arrays;

public class Graph {

    private ArrayList<String> vertexList; //顶点集合
    private int[][] edges; //存储图对应的邻接矩阵
    private int numOfEdge; //表示边的数量

    //定义数组boolean[] 记录某个节点是否被访问
    private boolean[] isVisited;


    public static void main(String[] args) {
        //测试创建图
        int n = 5; //顶点数目
        String[] Vertexes = {"A", "B", "C", "D", "E"};
        //创建图对象
        Graph graph = new Graph(n);
        //循环添加顶点
        for (String vertex : Vertexes) {
            graph.insertVertex(vertex);
        }
        //添加边
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 2, 1);
        graph.insertEdge(1, 2, 1);
        graph.insertEdge(1, 3, 1);
        graph.insertEdge(1, 4, 1);

        //显示邻接矩阵
        graph.showGraph();

        //测试dfs遍历
        System.out.println("深度遍历");
        graph.dfs();
    }

    //构造器
    public Graph(int n) {
        //初始化矩阵和vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
        numOfEdge = 0;
        isVisited = new boolean[n];
    }

    //得到第一个邻接节点的下标w
    public int getFirstNeighbor(int curIndex) {
        for (int j = 0; j < vertexList.size(); j++) {
            if (edges[curIndex][j] > 0) { //大于0说明下一个邻接点存在
                return j;
            }
        }
        return -1;
    }

    //根据前一个邻接节点的下标来获取下一个邻接节点
    public int getNextNeighbor(int v1, int v2) {
        for (int j = v2 + 1; j < vertexList.size(); j++) {
            if (edges[v1][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    //深度优先遍历算法DFS
    //i 第一次是 0
    private void dfs(boolean[] isVisited, int i) {
        //首先访问该节点
        System.out.print(getValueByIndex(i) + "->");
        //将该节点设置为已访问
        isVisited[i] = true;
        //查找节点i的第一个邻接点
        int w = getFirstNeighbor(i);
        while (w != -1) {
            if (!isVisited[w]) {
                dfs(isVisited, w);
            }
            //如果w已被访问
            w = getNextNeighbor(i, w);
        }
    }

    //对dfs进行重载,遍历我们所有的节点,并进行dfs
    public void dfs() {
        //遍历所有节点,进行dfs
        for (int i = 0; i < getVertexNum(); i++) {
           if (! isVisited[i]) {
               dfs(isVisited, i);
           }
        }
    }


    //图中常用方法
    //1. 返回顶点的数量
    public int getVertexNum() {
        return vertexList.size();
    }

    //2. 得到边的数目
    public int getEdgNum() {
        return numOfEdge;
    }

    //3.返回节点i(下标)对应的数据 0 -> "A", 1 -> "B"...
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    //4. 返回v1 和 v2 的权值
    public int getWeight(int v1, int v2) {
        return edges[v1][v2];
    }

    //5. 显示图对应的矩阵
    public void showGraph() {
        for (int[] link : edges) {
            System.err.println(Arrays.toString(link));
        }
    }


    //插入顶点
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }
    /**
     * 添加边
     *
     * @param v1     表示顶点下标
     * @param v2     表示顶点下标
     * @param weight 表示权值
     */
    public void insertEdge(int v1, int v2, int weight) {
        //无向图
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdge++;
    }
}
BFS算法

广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。

如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了。
广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。

//对一个节点进行广度优先遍历的方法
private void bfs(boolean[] isVisited, int i) {
    int u; //表示头节点对应的下标
    int w; //邻接节点的下标
    //记录节点访问顺序的队列
    LinkedList queue = new LinkedList();
    //访问节点,输出节点信息
    System.out.print(getValueByIndex(i) + "=>");
    //标记为已访问
    isVisited[i] = true;
    //将节点加入队列
    queue.addLast(i);

    while (!queue.isEmpty()) {
        //取出队列的头节点下标
        u = (Integer) queue.removeFirst();
        //得到第一个邻接点的下标w
        w = getFirstNeighbor(u);
        while (w != -1) {
            if (!isVisited[w]) {
                System.out.print(getValueByIndex(w) + "=>");
                //标记已经访问
                isVisited[w] = true;
                //入队
                queue.addLast(w);
            }
            //以u为当前节点找到下一个邻接点
            w = getNextNeighbor(u, w);
        }
    }
}

//遍历所有节点,都进行广度优先搜索
public void bfs() {
    isVisited = new boolean[vertexList.size()];
    for (int i = 0; i < getVertexNum(); i++) {
        if (! isVisited[i]) {
            bfs(isVisited, i);
        }
    }
}

概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值