数据结构——稀疏数组、队列、链表、栈、哈希表、树(包括堆)、图

一、数据结构的概述

数据结构分为:线性结构、非线性结构;

线性结构分为:顺序存储结构(数组)、链式存储结构(链表)。

线性结构

1) 线性结构是最常用的数据结构,特点是数据元素之间存在一对一的线性关系

2) 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的

3) 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息

4) 线性结构常见的有:数组、队列、链表和栈

非线性结构

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

二、稀疏数组(sparse array)

引入

举例:编写的五子棋程序中,有存盘退出和续上盘的功能。0表示没有棋子,1表示黑子,2表示蓝子,该二维数组的很多值是默认值 0, 因此记录了很多没有意义的数据.,此时可以用稀疏数组来保存信息。

稀疏数组简介

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

1) 记录数组一共有几行几列,有多少个不同的值

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

稀疏数组应用及生成分析

1) 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)

2) 把稀疏数组存盘,并且可以从新恢复原来的二维数组数 

分析如下:

代码实现
package com.study.sparsearray;


public class SparseArray {
    public static void main(String[] args) {
        //先创建原始二维数组 11*11  0:没有棋子 1:黑子 :蓝子
        int chessArray1[][] = new int[11][11];
        chessArray1[1][2] = 1;//黑子
        chessArray1[2][3] = 2;//蓝子
        //输出原始二维数组
        //普通for循环
//        for(int i = 0; i < chessArray1.length; i++) {
//            for (int j = 0; j < chessArray1[i].length; j++) {
//                System.out.print(chessArray1[i][j] + "\t");
//            }
//            System.out.println();
//        }
        //增强for循环
        System.out.println("原始二维数组");
        for (int[] row : chessArray1) {
            for (int data : row) {
                System.out.printf("%d\t", data);
            }
            System.out.println();
        }
        //将二维数组转成稀疏数组
        //1.先遍历二维数组,得到非0数据的个数
        int sum = 0;//记录非0个数
        for (int i = 0; i < chessArray1.length; i++) {
            for (int j = 0; j < chessArray1[i].length; j++) {
                if (chessArray1[i][j] != 0) {
                    sum++;
                }
            }
        }
        System.out.println("非0值的个数 sum = " + sum);

        //创建稀疏数组
        int[][] sparseArr = new int[sum + 1][3];
        //给稀疏数组赋值
        int sizeRow = chessArray1.length;
        int sizeCol = chessArray1[0].length;
        sparseArr[0][0] = sizeRow;
        sparseArr[0][1] = sizeCol;
        sparseArr[0][2] = sum;
        //遍历二维数组,将非0值存放到稀疏数组中
        int count = 0;//记录是第几个非0数据
        for (int i = 0; i < chessArray1.length; i++) {
            for (int j = 0; j < chessArray1[i].length; j++) {
                if (chessArray1[i][j] != 0) {
                    count++;
                    sparseArr[count][0] = i;
                    sparseArr[count][1] = j;
                    sparseArr[count][2] = chessArray1[i][j];
                }
            }
        }
        //输出稀疏数组
        System.out.println("==========稀疏数组======");
        for (int i = 0; i < sparseArr.length; i++) {
            for (int j = 0; j < sparseArr[i].length; j ++) {
                System.out.print(sparseArr[i][j] + "\t");
            }
            System.out.println();
        }

        //用稀疏数组恢复原始二维数组
        //读取稀疏数组第一行
        int[][] chessArray2 = new int[sparseArr[0][0]][sparseArr[0][1]];
        //循环遍历稀疏数组
        for (int i = 1; i < sparseArr.length; i++) {//从稀疏数组第2行开始遍历
            chessArray2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];

        }

        //增强for循环
        System.out.println("恢复后的原始二维数组");
        for (int[] row : chessArray2) {
            for (int data : row) {
                System.out.printf("%d\t", data);
            }
            System.out.println();
        }

    }
}

三、队列(Queue)

队列简介

1) 队列是一个有序列表,可以用数组或是链表来实现。

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

数组模拟队列

队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队 列的最大容量。

因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标, front 会随着数据输出而改变,而 rear 则是随着数据输入而改变,如图所示:

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

思路分析

1) 将尾指针往后移:rear+1 , 当 front == rear 【空】

2) 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。 rear == maxSize - 1[队列满]

代码实现
package com.study.queue;

import java.util.Scanner;


public class ArrayQueueDemo {
    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): 查看队列头的数据");
            key = scanner.next().charAt(0);//接收字符
            switch (key) {
                case 's':
                    arrayQueue.showQueue();
                    break;
                case 'a':
                    System.out.println("请输入一个数字");
                    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 head = arrayQueue.headQueue();
                        System.out.printf("队列头的数据是:%d\n", head);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());;
                    }
                    break;
                case 'e':
                    scanner.close();//要把scanner关掉
                    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;//指向队列头部,指向队列头的前后一个位置
        rear = -1;//指向队列尾部,指向队列尾部的数据(即就是 队列的最后一个数据)
    }

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

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

    //添加数据到队列
    public void addQueue(int n) {
        //首先判断队列是否满
        if (isFull()) {
            System.out.println("队列满,不能加入数据了");
            return;
        }
        rear++;//让尾部 rear 后移
        arr[rear] = n;//将数据n添加到队列arr中
    }

    //获取队列的数据,即 出队列
    public int getQueue() {
        //先判断队列是否为空
        if (isEmpty()) {
            throw new RuntimeException("队列空,不能取数据");
        }
        front++;
        return arr[front];
    }

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

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

问题分析并优化

1) 目前数组使用一次就不能用, 没有达到复用的效果

2) 将这个数组使用算法,改进成一个环形的队列 取模:%

数组模拟环形队列

思路分析

对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取模的方式来实现即可)

分析说明:

1) 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的 时候需要注意 (rear + 1) % maxSize == front 满]

2) rear == front [空]

3) 分析示意图:

代码实现

package com.study.queue;

import java.util.Scanner;


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

        //测试
        System.out.println("===============测试数组模拟环形队列的案例======");
        CircleArray circleArray = new CircleArray(4);//4其实有效数据最大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): 查看队列头的数据");
            key = scanner.next().charAt(0);//接收字符
            switch (key) {
                case 's':
                    circleArray.showQueue();
                    break;
                case 'a':
                    System.out.println("请输入一个数字");
                    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 head = circleArray.headQueue();
                        System.out.printf("队列头的数据是:%d\n", head);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                        ;
                    }
                    break;
                case 'e':
                    scanner.close();//要把scanner关掉
                    loop = false;
                    break;
                default:
                    break;
            }
        }
        System.out.println("===========程序退出===============");
    }

}

class CircleArray {
    private int maxSize;//表示数组的最大容量
    private int front;//队列 头,调整了,指向队列的第一个元素,arr[front]是第一个元素,front初始值=0
    private int rear;//队列 尾,指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定,rear=0初始
    private int[] arr;//该数组用于存放数据,模拟队列

    public CircleArray(int arrMaxSize) {
        maxSize = arrMaxSize;
        arr = new int[maxSize];
        front = 0;
        rear = 0;//默认就是0,可以不写
    }

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

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

    //添加数据到队列
    public void addQueue(int n) {
        if (isFull()) {
            System.out.println("队列满,不能再加~");
            return;
        }
        //直接将数据加入
        arr[rear] = n;
        //将rear后移
        rear = (rear + 1) % maxSize;
    }

    //取出数据
    public int getQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列空,不能取数据");
        }
        //要分析:front指向队列的第一个元素,
        //1.先把front对应的值保存到一个临时变量,
        //2.将front后移
        //3.将临时保存的变量返回
        int value = arr[front];
        front = (front + 1) % maxSize;
        return value;
    }

    //显示队列的所有数据
    public void showQueue() {
        if (isEmpty()) {
            System.out.println("队列空~");
            return;
        }
        //思路:从front开始遍历,遍历多少个元素?
        for (int i = front; i < front + size(); i++) {
            System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
        }

    }

    //方法:求出当前队列有效数据的个数
    public int size() {
        return (rear + maxSize - front) % maxSize;
    }

    //求出当前队列的头数据
    public int headQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列空");
        }
        return arr[front];

    }
}

四、链表(linked list)

链表简介

链表分为带头节点的链表和不带头节点的链表。链表是有序的列表,但是它在内存中是存储如下:

1) 链表是以节点的方式来存储,是链式存储

2) 每个节点包含 data 域, next 域:指向下一个节点.

3) 如图:发现链表的各个节点不一定是连续存储.

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

单向链表应用实例

使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作。

(1)添加节点

1) 第一种方法在添加英雄时,直接添加到链表的尾部

2)第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)

(2) 修改节点

思路

        (1) 先找到该节点,通过遍历,

        (2) temp.name = newHeroNode.name ; temp.nickname= newHeroNode.nickname

(3)删除节点

代码实现

package com.study.linkedlist;

import java.util.Stack;


public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //测试
        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");

        SingleLinkedList singleLinkedList = new SingleLinkedList();
//        singleLinkedList.add(hero1);
//        singleLinkedList.add(hero2);
//        singleLinkedList.add(hero3);
//        singleLinkedList.add(hero4);

        //加入英雄,按照编号的顺序
        singleLinkedList.addByOrder(hero1);
        singleLinkedList.addByOrder(hero4);
        singleLinkedList.addByOrder(hero2);
        singleLinkedList.addByOrder(hero3);
        singleLinkedList.list();

        //测试修改节点的代码
        HeroNode newHeroNode = new HeroNode(2, "小卢", "玉麒麟~~");
        singleLinkedList.update(newHeroNode);
        //显示
        System.out.println("===============修改后================");
        singleLinkedList.list();

        //删除节点
        System.out.println("========删除节点后=================");
        singleLinkedList.del(2);
        singleLinkedList.list();

        //测试 获取单链表的节点的个数
        System.out.println("有效的节点个数:" + getLength(singleLinkedList.getHead()));

        //测试 是否得到了倒数第k个节点
        int k = 4;
        System.out.println("倒数第 " + k + " 个节点是:" +
                findLastIndexNode(singleLinkedList.getHead(), k));

        //测试 翻转链表
        System.out.println("=============翻转结果============");
        reverseList(singleLinkedList.getHead());
        singleLinkedList.list();

        //测试 逆序打印 单链表
        System.out.println("===============逆序打印单链表,用栈===========");
        reversePrint(singleLinkedList.getHead());
    }

    //使用栈的方式 将链表的各个节点压入到栈中,然后利用栈的 先进后出 的特点,实现逆序打印
    public static void reversePrint(HeroNode head) {
        if(head.next == null) {
            return;//空链表无法打印
        }
        //创建一个栈,将各个节点压入栈中
        Stack<HeroNode> stack = new Stack<>();
        HeroNode cur = head.next;
        //将链表的所有节点 压入栈中
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;//后移,从而压入下一个节点
        }
        //将栈中的节点进行打印,pop出栈
        while (stack.size() > 0) {
            System.out.println(stack.pop());//stack特点是先进后出
        }
    }


    //将单链表进行反转
    public static void reverseList(HeroNode head) {
        //如果当前链表为空或者1个,无需翻转
        if (head.next == null || head.next.next == null) {
            return;
        }
        //定义一个辅助指针(变量),帮助我们遍历原来的链表
        HeroNode cur = head.next;//遍历原先的链表
        HeroNode next = null;//指向当前节点cur的下一个节点
        HeroNode reverseHead = new HeroNode(0, "", "");
        //遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
        while (cur != null) {
            next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
            cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的最前端
            reverseHead.next = cur;
            cur = next;//让cur后移
        }
        //将head.next指向 reverseHead.next,实现单链表的翻转
        head.next = reverseHead.next;

    }

    //查找单链表中的倒数第k个节点
    /*
        思路:
        1.编写一个方法,接收head节点,同时接收一个index
        2.index表示:倒数第index个节点
        3.先把链表从头到尾遍历一下,得到链表的总长度 调用getLength(HeroNode head)方法
        4.得到size后,从链表的第一个开始遍历,遍历size-index个,就可以得到
        5.如果找到了,就返回该节点,否则返回空
     */
    public static HeroNode findLastIndexNode(HeroNode head, int index) {
        //判断链表为空,返回空
        if (head.next == null) {
            return null;
        }
        //第一次遍历 得到链表的长度(节点的个数)
        int size = getLength(head);
        //第二次遍历 到 size-index 位置,就是倒数的第k个节点
        //先做数据校验,index 是否合理
        if (index <= 0 || index > size) {
            return null;
        }
        //定义一个辅助变量
        HeroNode cur = head.next;
        for (int i = 0; i < size - index; i++) {
            cur = cur.next;
        }
        return cur;
    }


    //方法:获取单链表的节点的个数,如果是带头节点的链表,需要不统计头节点

    /**
     * @param head 链表的头节点
     * @return 返回的是有效节点的个数
     */
    public static int getLength(HeroNode head) {
        if (head.next == null) {
            return 0;
        }
        int length = 0;
        HeroNode cur = head.next;//不统计头节点
        while (cur != null) {
            length++;
            cur = cur.next;//遍历
        }
        return length;
    }
}

//定义SingleLinkedList 管理英雄
class SingleLinkedList {
    //先初始化一个头节点,头节点不能动,不存放具体数据,只是代表头
    private HeroNode head = new HeroNode(0, "", "");

    public HeroNode getHead() {
        return head;
    }

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

    //第2种方式:添加英雄时,根据排名将英雄插入到指定位置
    //如果这个排名存在,则添加失败,给出提示
    public void addByOrder(HeroNode heroNode) {
        //头节点不能动,仍然通过辅助变量temp来找到需要添加的位置
        //因为是单链表,因此找的temp位于添加位置的前一个节点,否则加入不了
        HeroNode temp = head;
        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 == true) {//不能添加,说明编号已经存在
            System.out.println("准备插入的英雄的编号已经存在,不能再加入了 " + heroNode.no);
        } else {
            //插入到链表中,temp的后边
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }


    //修改节点的信息,根据编号来修改,即no编号不能改
    //1.根据newHeroNode的no来修改
    public void update(HeroNode newHeroNode) {
        //先判断链表是否空
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }
        //找到需要修改的节点,根据no编号来找
        //先定义一个辅助变量
        HeroNode temp = head.next;
        boolean flag = false;//表示是否找到该节点
        while (true) {
            if (temp == null) {
                break;//到链表的最后了,遍历完链表了
            }
            if (temp.no == newHeroNode.no) {
                //找到了
                flag = true;
                break;
            }
            temp = temp.next;
        }
        //根据flag判断是否找到要修改的节点
        if (flag) {
            temp.name = newHeroNode.name;
            temp.nickName = newHeroNode.nickName;
        } else {//没有找到要修改的节点
            System.out.println("没有找到编号为 " + newHeroNode.no + " 的节点");
        }
    }

    //删除节点
    //1.head不能动,需要temp辅助节点找到待删除节点的前一个节点
    //2.比较时,temp.next.no和需要删除的节点的no进行比较
    public void del(int no) {
        HeroNode temp = head;
        boolean flag = false;//表示是否找到待删除节点的前一个节点
        while (true) {
            if (temp.next == null) {//已经到链表最后
                break;
            }
            if (temp.next.no == no) {
                //找到了待删除节点的前一个节点temp
                flag = true;
                break;
            }
            temp = temp.next;//temp后移
        }
        //判断flag
        if (flag) {
            //可以删除
            temp.next = temp.next.next;
        } else {
            System.out.println("无法删除节点 " + no);
        }
    }


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


//定义一个HeroNode,每个HeroNode对象就是一个节点
class HeroNode {
    public int no;
    public String name;
    public String nickName;
    public HeroNode next;//指向下一个节点

    //构造器
    public HeroNode(int hNo, String hName, String hNickName) {
        this.no = hNo;
        this.name = hName;
        this.nickName = hNickName;
    }
    //为了显示方便,重写toString

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

单链表面试题

1) 求单链表中有效节点的个数

    //方法:获取单链表的节点的个数,如果是带头节点的链表,需要不统计头节点

    /**
     * @param head 链表的头节点
     * @return 返回的是有效节点的个数
     */
    public static int getLength(HeroNode head) {
        if (head.next == null) {
            return 0;
        }
        int length = 0;
        HeroNode cur = head.next;//不统计头节点
        while (cur != null) {
            length++;
            cur = cur.next;//遍历
        }
        return length;
    }
}

2)查找单链表中的倒数第 k 个结点

    //查找单链表中的倒数第k个节点
    /*
        思路:
        1.编写一个方法,接收head节点,同时接收一个index
        2.index表示:倒数第index个节点
        3.先把链表从头到尾遍历一下,得到链表的总长度 调用getLength(HeroNode head)方法
        4.得到size后,从链表的第一个开始遍历,遍历size-index个,就可以得到
        5.如果找到了,就返回该节点,否则返回空
     */
    public static HeroNode findLastIndexNode(HeroNode head, int index) {
        //判断链表为空,返回空
        if (head.next == null) {
            return null;
        }
        //第一次遍历 得到链表的长度(节点的个数)
        int size = getLength(head);
        //第二次遍历 到 size-index 位置,就是倒数的第k个节点
        //先做数据校验,index 是否合理
        if (index <= 0 || index > size) {
            return null;
        }
        //定义一个辅助变量
        HeroNode cur = head.next;
        for (int i = 0; i < size - index; i++) {
            cur = cur.next;
        }
        return cur;
    }

3)单链表的反转

    //将单链表进行反转
    public static void reverseList(HeroNode head) {
        //如果当前链表为空或者1个,无需翻转
        if (head.next == null || head.next.next == null) {
            return;
        }
        //定义一个辅助指针(变量),帮助我们遍历原来的链表
        HeroNode cur = head.next;//遍历原先的链表
        HeroNode next = null;//指向当前节点cur的下一个节点
        HeroNode reverseHead = new HeroNode(0, "", "");
        //遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
        while (cur != null) {
            next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
            cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的最前端
            reverseHead.next = cur;
            cur = next;//让cur后移
        }
        //将head.next指向 reverseHead.next,实现单链表的翻转
        head.next = reverseHead.next;

    }

4)从尾到头打印单链表

【要求方式 1:反向遍历 。 方式 2:Stack 栈

    //使用栈的方式 将链表的各个节点压入到栈中,然后利用栈的 先进后出 的特点,实现逆序打印
    public static void reversePrint(HeroNode head) {
        if(head.next == null) {
            return;//空链表无法打印
        }
        //创建一个栈,将各个节点压入栈中
        Stack<HeroNode> stack = new Stack<>();
        HeroNode cur = head.next;
        //将链表的所有节点 压入栈中
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;//后移,从而压入下一个节点
        }
        //将栈中的节点进行打印,pop出栈
        while (stack.size() > 0) {
            System.out.println(stack.pop());//stack特点是先进后出
        }
    }

双向链表

双向链表的操作分析和实现

使用带 head 头的双向链表实现 –水浒英雄排行榜

管理单向链表的缺点分析:

1) 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。

2) 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除 时节点,总是找到 temp,temp 是待删除节点的前一个节点。

双向链表的遍历,添加,修改,删除

1) 遍历方法和单链表一样,只是可以向前,也可以向后查找

2) 添加 (默认添加到双向链表的最后)

        (1) 先找到双向链表的最后这个节点

        (2) temp.next = newHeroNode

        (3) newHeroNode.pre = temp;

3) 修改 思路和 原来的单向链表一样.

4) 删除

        (1) 因为是双向链表,因此,我们可以实现自我删除某个节点

        (2) 直接找到要删除的这个节点,比如 temp

        (3) temp.pre.next = temp.next

        (4) temp.next.pre = temp.pr

代码实现
package com.study.linkedlist;


public class DoubleLinkedListDemo {
    public static void main(String[] args) {
        //测试
        System.out.println("=======双向链表测试==========");
        HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨");
        HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟");
        HeroNode2 hero3 = new HeroNode2(3, "吴用", "智多星");
        HeroNode2 hero4 = new HeroNode2(4, "林冲", "豹子头");

        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();

//        doubleLinkedList.add(hero1);
//        doubleLinkedList.add(hero2);
//        doubleLinkedList.add(hero3);
//        doubleLinkedList.add(hero4);
//
//        doubleLinkedList.list();

        //按顺序添加
        doubleLinkedList.addByOrder(hero2);
        doubleLinkedList.addByOrder(hero4);
        doubleLinkedList.addByOrder(hero3);
        doubleLinkedList.addByOrder(hero1);
        System.out.println("===========按顺序=============");
        doubleLinkedList.list();

        //修改
          HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "嗯嗯嗯");
        doubleLinkedList.update(newHeroNode);
        System.out.println("============修改后的结果=============");
        doubleLinkedList.list();
        //删除
        doubleLinkedList.del(3);
        System.out.println("============删除后的结果=============");
        doubleLinkedList.list();
    }
}


//创建一个双向链表类
class DoubleLinkedList {
    //初始化一个头节点
    private HeroNode2 head = new HeroNode2(0, "", "");

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

    //添加 默认添加到双向链表的最后
    public void add(HeroNode2 heroNode) {
        //思路:当不考虑编号顺序时,
        //1.找到当前链表的最后节点
        //2.将最后这个节点的next 指向 新的节点
        //因为head节点不能动,因此需要一个辅助变量temp
        HeroNode2 temp = head;
        //遍历链表,找到最后
        while (true) {
            //找到链表的最后
            if (temp.next == null) {
                break;
            }
            //如果没有找到最后,将temp后移
            temp = temp.next;//指向最后
        }
        //当退出while循环时,temp指向链表最后
        temp.next = heroNode;
        heroNode.pre = temp;//完成连接
    }
    //第二种添加方式:按照编号顺序添加
    public void addByOrder(HeroNode2 heroNode2) {
        HeroNode2 temp =head;
        boolean flag = false;
        while (true) {
            if(temp.next == null) {
                break;
            }
            if(temp.next.no > heroNode2.no) {
                break;//找到了
            } else if(temp.next.no == heroNode2.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if(flag == true) {
            System.out.println("不能再加");
        } else {
            temp.next = heroNode2;
            heroNode2.pre = temp;//完成连接
        }
    }

    //修改
    //修改节点的信息,根据编号来修改,即no编号不能改
    //1.根据newHeroNode的no来修改
    public void update(HeroNode2 newHeroNode) {
        //先判断链表是否空
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }
        //找到需要修改的节点,根据no编号来找
        //先定义一个辅助变量
        HeroNode2 temp = head.next;
        boolean flag = false;//表示是否找到该节点
        while (true) {
            if (temp == null) {
                break;//到链表的最后了,遍历完链表了
            }
            if (temp.no == newHeroNode.no) {
                //找到了
                flag = true;
                break;
            }
            temp = temp.next;
        }
        //根据flag判断是否找到要修改的节点
        if (flag) {
            temp.name = newHeroNode.name;
            temp.nickName = newHeroNode.nickName;
        } else {//没有找到要修改的节点
            System.out.println("没有找到编号为 " + newHeroNode.no + " 的节点");
        }
    }

    //删除节点
    //对双线链表,可以直接找到要删除的这个节点,找到后自我删除
    public void del(int no) {
        //判断当前链表是否为空
        if(head.next == null) {
            System.out.println("链表为空,无法删除");
            return;
        }
        HeroNode2 temp = head.next;//辅助指针
        boolean flag = false;//表示是否找到待删除节点的前一个节点
        while (true) {
            if (temp == null) {//已经到链表最后
                break;
            }
            if (temp.no == no) {
                //找到了待删除节点的前一个节点temp
                flag = true;
                break;
            }
            temp = temp.next;//temp后移
        }
        //判断flag
        if (flag) {
            //可以删除
            temp.pre.next = temp.next;
            //如果是最后一个节点,就不需要下面这句,否则会空指针异常
            if(temp.next != null) {
                temp.next.pre = temp.pre;
            }
        } else {
            System.out.println("无法删除节点 " + no);
        }
    }


    //遍历双向链表
    public void list() {
        //先判断链表是否为空
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }
        //因为头节点不能动,因此需要辅助变量来遍历
        HeroNode2 temp = head.next;
        while (true) {
            //判断是否到链表最后
            if (temp == null) {
                System.out.println("链表到最后了");
                break;//结束
            }
            //输出节点信息
            System.out.println(temp);
            //将temp后移
            temp = temp.next;//后移
        }
    }
}

//定义一个HeroNode,每个HeroNode对象就是一个节点
class HeroNode2 {
    public int no;
    public String name;
    public String nickName;
    public HeroNode2 next;//指向下一个节点,默认null
    public HeroNode2 pre;//指向前一个节点,默认null

    //构造器
    public HeroNode2(int hNo, String hName, String hNickName) {
        this.no = hNo;
        this.name = hName;
        this.nickName = hNickName;
    }
    //为了显示方便,重写toString

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

单向环形链表应用

Josephu(约瑟夫、约瑟夫环) 问题

Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数 到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由 此产生一个出队编号的序列。

提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结 点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直 到最后一个结点从链表中删除算法结束。

约瑟夫问题的示意图

思路图解

代码实现
package com.study.linkedlist;


public class Josephu {
    public static void main(String[] args) {
        //测试 构建环形链表  遍历环形链表
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        circleSingleLinkedList.addBoy(5);//加入5个小孩节点
        circleSingleLinkedList.showBoy();

        //测试小孩出圈
        circleSingleLinkedList.countBoy(1, 2, 5);
    }
}

//创建环形单向链表
class CircleSingleLinkedList {
    //先创建一个first节点,当前不知道谁是
    private Boy first = null;//先初始化

    //添加小孩节点。构建成一个环形链表
    public void addBoy(int nums) {//nums表示一共几个小孩
        //nums数据校验
        if (nums < 1) {
            System.out.println("nums值不正确");
            return;
        }
        Boy curBoy = null;//辅助指针,帮助构建环形链表
        //使用for循环创建环形链表
        for (int i = 1; i <= nums; i++) {
            //根据编号,创建小孩节点
            Boy boy = new Boy(i);//
            //如果是第一个小孩
            if (i == 1) {
                first = boy;
                first.setNext(first);//构成环状
                curBoy = first;//curBoy指向第一个小孩先
            } else {
                curBoy.setNext(boy);
                boy.setNext(first);
                curBoy = boy;
            }
        }

    }

    //遍历当前的环形链表
    public void showBoy() {
        //先判断链表是否为空
        if (first == null) {
            System.out.println("没有任何小孩");
            return;
        }
        //因为first不能动,因此使用辅助指针完成遍历
        Boy curBoy = first;
        while (true) {
            System.out.printf("小孩的编号 %d \n", curBoy.getNo());
            if (curBoy.getNext() == first) {//说明遍历完毕
                break;
            }
            curBoy = curBoy.getNext();//curBoy后移
        }
    }
    //根据用户输入,计算小孩出圈的顺序

    /**
     * @param startNo  表示从第几个小孩开始数数
     * @param countNum 表示数几下
     * @param nums     表示最初有多少个小孩在圈中
     */
    public void countBoy(int startNo, int countNum, int nums) {
        //先对数据进行校验
        if (first == null || startNo < 1 || startNo > nums) {
            System.out.println("参数输入有误,请重新输入");
            return;
        }
        //创建辅助指针,帮助完成小孩出圈
        Boy helper = first;
        //辅助指针helper指向环形链表的最后这节点
        while (true) {
            if (helper.getNext() == first) {//说明helper指向最后小孩节点
                break;
            }
            helper = helper.getNext();
        }
        //小孩报数前,先让first和helper移动k-1次
        for (int j = 0; j < startNo - 1; j++) {
            first = first.getNext();
            helper = helper.getNext();
        }
        //报数,first和helper移动m-1次,然后出圈,是循环操作,直到圈中只有一个节点
        while (true) {
            if (helper == first) {//说明圈中只有一个节点了
                break;
            }
            //先移动m-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());
    }
}


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

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

        public int getNo() {
            return no;
        }

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

        public Boy getNext() {
            return next;
        }

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

五、栈(stack)

栈的介绍

1) 栈的英文为(stack)

2) 栈是一个先入后出(FILO-First In Last Out)的有序列表。

3) 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的 一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)

4) 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元 素最先删除,最先放入的元素最后删除

5) 图解方式说明出栈(pop)和入栈(push)的概念

栈的应用

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

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

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

4) 二叉树的遍历。

5) 图形的深度优先(depth 一 first)搜索法。

用数组模拟栈的使用

思路分析

代码实现

package com.study.stack;

import java.util.Scanner;

/**
 * @author 
 * @version 1.0
 * 数组模拟栈
 */
public class ArrayStackDemo {
    public static void main(String[] args) {
        //测试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.println("===================请输入你的选择:==========================");
            key = scanner.next();
            switch (key) {
                case "show":
                    stack.list();
                    break;
                case "push":
                    System.out.println("请输入一个数:");
                    int value = scanner.nextInt();
                    stack.push(value);
                    break;
                case "pop":
                    try {
                        int res = stack.pop();
                        System.out.println("出栈的数据是:" + res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());;
                    }
                    break;
                case "exit":
                    scanner.close();
                    loop = false;
                default:
                    break;
            }
        }
        System.out.println("程序退出");
    }
}

//定义一个ArrayStack表示栈
class ArrayStack {
    private int maxSize;
    private int[] stack;//栈的数据存放在此
    private int top = -1;//栈顶初始化 -1 没有数据
    public ArrayStack(int maxSize) {
        this.maxSize = maxSize;
        stack = new int[this.maxSize];
    }
    //方法 判断栈满
    public boolean isFull() {
        return top == maxSize - 1;
    }
    //判断栈空
    public boolean isEmpty() {
        return top == -1;
    }
    //入栈
    public void push(int value) {
        //先判断栈是否满
        if(isFull()) {
            System.out.println("栈满");
            return;
        }
        top++;
        stack[top] = value;
    }
    //出栈 将栈顶数据返回
    public int pop() {
        //先判断栈是否空
        if(isEmpty()) {
            //异常提示
            throw new RuntimeException("栈空,无数据");
        }
        int value = stack[top];
        top--;
        return value;//返回值
    }
    //显示栈  即 遍历栈,遍历时需要从栈顶开始显示数据
    public void list() {
        if(isEmpty()) {
            System.out.println("没有数据,栈空");
            return;
        }
        for (int i = top; i >= 0; i--) {
            System.out.printf("stack[%d]=%d\n", i, stack[i]);
        }
    }
}

栈实现综合计算机

表达式介绍——前缀、中缀、后缀

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

2)中缀表达式:是常见的表达式,我们平时看到的如 1+2*3,对于中缀表达式,计算机在计算时要判断运算符的优先级,因此比较麻烦。

3)后缀表达式,又叫逆波兰表达式,运算符位于操作符之后。

1.中缀表达式的实现

思路分析

代码实现
package com.study.stack;

/**
 * @author 
 * @version 1.0
 * 用栈来做表达式计算器
 */
public class Calculator {
    public static void main(String[] args) {
        //进行计算实现
        String expression = "70+20*6-4";
        //创建两个栈 数栈 符号栈
        ArrayStack2 numStack = new ArrayStack2(10);
        ArrayStack2 operStack = new ArrayStack2(10);
        //定义需要的变量
        int index = 0;//索引 扫描表达式
        int num1 = 0;
        int num2 = 0;
        int oper = 0;//运算符
        int res = 0;//计算结果
        char ch = ' '; //将每次扫描得到的char保存到ch
        String keepNum = "";//拼接多位数
        //开始 while 语句循环扫描expression
        while (true) {
            //先依次得到expression里的每个字符
            ch = expression.substring(index, index + 1).charAt(0);//取出单个字符
            //判断
            if (operStack.isOper(ch)) {//如果是运算符
                //继续判断 当前的符号栈 是否为空
                if (!operStack.isEmpty()) {
                    //符号栈不为空,需要判断优先级
                    //若当前优先级 <= 栈里,就把栈里pop出一个符号,pop出两个数字
                    if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
                        num1 = numStack.pop();
                        num2 = numStack.pop();
                        oper = operStack.pop();
                        res = numStack.cal(num1, num2, oper);
                        //把运算的结果入数栈
                        numStack.push(res);
                        //把当前操作符入符号栈
                        operStack.push(ch);
                    } else {
                        //操作符优先级 > 栈中符号优先级,直接入栈
                        operStack.push(ch);
                    }
                } else {
                    //如果为空,直接入符号栈
                    operStack.push(ch);
                }
            } else {
                //数栈,直接入数栈
//                numStack.push(ch - 48);//将字符转成数字
                //要考虑多位数
                //1.当处理多位数时,不能发现是一个数就立即入栈,
                //2.处理数时,需要向expression表达式的后再看一位,若是数,就继续扫描,若是符号,可以入栈
                //3.需要定义字符串变量,用于拼接

                //处理多位数
                keepNum += ch;
                //若ch已经是表达式最后一位,不需要判断了,直接入栈
                if (index == expression.length() - 1) {
                    numStack.push(Integer.parseInt(keepNum));
                } else {

                    //判断下一位字符是否是数字
                    if (operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))) {
                        //后一位是操作符,直接入数栈,要把字符串keepNum转成Int
                        numStack.push(Integer.parseInt(keepNum));
                        //注意!!!!!要清空keepNum
                        keepNum = "";//拼接多位数
                    }
                }

            }
            //让index + 1 ,并判断是否扫描到expression的最后
            index++;
            if (index >= expression.length()) {
                break;//扫描结束了
            }
        }
        //表达式扫描完毕,顺序pop 并运行
        while (true) {
            //如果符号栈为空,则计算到最后的结果,数栈中就只有一个数字,就是计算结果
            if (operStack.isEmpty()) {
                break;
            }
            num1 = numStack.pop();
            num2 = numStack.pop();
            oper = operStack.pop();
            res = numStack.cal(num1, num2, oper);
            numStack.push(res);//入栈 结果
        }
        System.out.printf("表达式 %s = %d ", expression, numStack.pop());
    }
}

//创建栈,扩展相关更能
class ArrayStack2 {
    private int maxSize;
    private int[] stack;//栈的数据存放在此
    private int top = -1;//栈顶初始化 -1 没有数据

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

    //增加啊一个方法,可以返回当前栈顶的值,但是不是出栈,只是看一下
    public int peek() {
        return stack[top];//lol一眼
    }

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

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

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

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

    //显示栈  即 遍历栈,遍历时需要从栈顶开始显示数据
    public void list() {
        if (isEmpty()) {
            System.out.println("没有数据,栈空");
            return;
        }
        for (int i = top; i >= 0; i--) {
            System.out.printf("stack[%d]=%d\n", i, stack[i]);
        }
    }

    //扩展功能
    //返回运算符的优先级,优先级是程序员确定的,我们假定用数字表示优先级
    // 返回数字越大,优先级越高
    public int priority(int oper) {
        if (oper == '*' || oper == '/') {
            return 1;
        } else if (oper == '+' || oper == '-') {
            return 0;
        } else {//我们假定目前表达式只有 +-*/
            return -1;
        }
    }

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

    //计算方法
    public int cal(int num1, int num2, int oper) {//char int 可以相互转换
        int res = 0;//用于存放计算结果
        switch (oper) {
            case '+':
                res = num1 + num2;
                break;
            case '-':
                res = num2 - num1;//注意顺序
                break;
            case '*':
                res = num1 * num2;
                break;
            case '/':
                res = num2 / num1;
                break;
            default:
                break;
        }
        return res;
    }
}

2.后缀表达式的实现

即完成逆波兰计算机。

1) 输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果

2) 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。

思路分析

例如: (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.study.stack;

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

/**
 * @author 
 * @version 1.0
 * 逆波兰表达式 后缀表达式
 */
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 = toInfixExpressionList(expression);
        System.out.println(infixExpressionList);//已经将中缀表达式转为list集合里,便于后续遍历

        System.out.println("==============后缀表达式==================");
        List<String> suffixExpressionList = parseSuffixExpressionList(infixExpressionList);
        System.out.println(suffixExpressionList);

        System.out.println("计算结果= " + calculate(suffixExpressionList));

        ///
        //先定义一个逆波兰表达式
        //(3+4)×5-6  ----->   3 4 + 5 × 6 -
//        String suffixExpression = "30 4 + 5 * 6 -";
        //1.先将suffixExpression放到 ArrayList中,用集合取数更方便
        //2.将ArrayList传递给一个方法,遍历ArrayList 配合栈,完成计算

//        List<String> rpnList = getListString(suffixExpression);
//        System.out.println("rpnList = " + rpnList);
//
//        int res = calculate(rpnList);
//        System.out.printf("后缀表达式 %s 的计算结果 = %d", suffixExpression, res);

    }
    //方法:将得到的中缀表达式对应的List ===> 转为后缀表达式对应的List
    // 即 将ArrayList[1,+,(,(,2,+,3,),*,4,),-,5]==> ArrayList[1,2,3,+,4,*,+,5,-]
    public static List<String> parseSuffixExpressionList(List<String> ls) {
        //定义两个栈
        Stack<String> s1 = new Stack<>();//符号栈
//        Stack<String> s2 = new Stack<>();//存储中间结果的栈---s2没有出栈操作,因此可以用List替代
        List<String> s2 = new ArrayList<String>();

        //遍历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的优先级 <= 栈顶运算符的优先级,将栈顶的运算符弹出并加入s2中,再与新的栈顶比较
                //需要构造一个方法,比较优先级高低----增加一个类
                while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
                    s2.add(s1.pop());
                }
                //把当前扫描到的item压入到s1
                s1.push(item);
            }
        }
        //一次将s1中剩余的运算符加入到s中
        while (s1.size() != 0) {
            s2.add(s1.pop());
        }
        return s2;//因为是存放到List中,因此按顺序输出就是对应的后缀表达式的list
    }



    //方法:将中缀表达式转成List
    //s = "1+((2+3)*4)-5"
    public static List<String> toInfixExpressionList(String s) {
        //定义一个List存放中缀表达式对应的数据
        List<String> ls = new ArrayList<String>();
        int i = 0;//索引,指针,用于遍历中缀表达式字符串
        String str;//用来做多位数拼接
        char c;//每遍历到一个字符就放入到c中
        do {
            //如果c是非数字,就加到ls中
            if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) {
                ls.add("" + c);
                i++;//i后移
            } else {//如果c是一个数字,则要考虑多位数的情况
                str = "";//置空
                while (i < s.length() && (c = s.charAt(i)) >= 48 && (c = s.charAt(i)) <= 57) {
                    str += c;//拼接
                    i++;//i后移继续判断
                }
                ls.add(str);
            }
        } while (i < s.length());
        return ls;//返回ls集合
    }

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

    //按流程完成对逆波兰表达式的运算
    /*
        1.从左至右扫描,遇到数字,将数字压入栈;
        2.遇到符号,弹出栈顶的两个数字,用运算符对其做运算,将计算结果入栈
        3.重复上述过程,直到表达式最右端
     */
    public static int calculate(List<String> ls) {
        //创建一个栈
        Stack<String> stack = new Stack<>();
        //遍历ls
        for (String item : ls) {
            //判断是否是数,使用正则表达式判断
            if (item.matches("\\d+")) {//匹配多位数
                //item为数,入栈
                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(res + "");//把整数转成字符串
            }
        }
        //for循环结束后,留在栈中的数据就是运算结果
        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;
    }

}

中缀表达式转后缀表达式

步骤

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 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式

举例

将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下

因此结果为 :"1 2 3 + 4 × + 5 -".

由于篇幅太长,后续几种数据结构的介绍放在其他文章里啦~

六、哈希表

具体介绍见数据结构——哈希表-CSDN博客

七、树

具体介绍见数据结构——树(二叉树、顺序存储树、线索化二叉树)-CSDN博客,以及数据结构——树结构的应用(堆、赫夫曼树、赫夫曼编码、二叉排序树、平衡二叉树)-CSDN博客以及多路查找树——多叉树、B树、B+树、B*树-CSDN博客
 

八、图

具体介绍见数据结构——图-CSDN博客

总结

本文针对常见的数据结构进行介绍,主要包括:稀疏数组、队列、链表、栈、哈希表、树、图,对他们的构造原理和应用进行了详述。

  • 61
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值