数据结构与算法01

线性结构和非线性结构

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

稀疏数组和对列

1. 引例

  • 问题描述:编写的五子棋程序中,有存盘退出和续上盘的功能。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SwBArbhO-1589021541023)(E:\Java学习资料\Java进阶笔记\图片\001.png)]

  • 问题分析:因为该二维数组的很多值是默认值0,因此记录了很多没有意义的数据,所以我们应该用稀疏数组。

2. 稀疏数组SparseArray

基本介绍

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

  • 稀疏数组的处理方法是:

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

应用实例

使用稀疏数组,来保留类似引例中的二维数组(棋盘、地图等)。把稀疏数组存盘,并且可以重新恢复原来的二维数组。

  • 思路:

    • 二维数组转稀疏数组的思路

      1. 遍历原始的二维数组,得到有效数据的个数sum
      2. 根据sum就可以创建稀疏数组sparseArr int[sum+ 1][3]
      3. 将二维数组的有效数据数据存入到稀疏数组

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztGFl51X-1589021541025)(E:\Java学习资料\Java进阶笔记\图片\004.png)]

    • 稀疏数组转原始的二维数组的思路

      1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的chessArr2 = int[11][11]
      2. 在读取稀疏数组后几行的数据,并赋给原始的二维数组即可
  • 代码实现:

public class SparseArrayMain {
    public static void main(String[] args) {
        //这里我们的棋盘大小为11*11,1代表黑子,2代表蓝子,设置三个初始值
        int[][] arr1 = new int[11][11];
        arr1[1][2] = 1;
        arr1[2][3] = 2;
        arr1[4][5] = 2;

        //打印原来的数组
        System.out.println("原来的数组:");
        for (int i = 0; i < 11; i++) {
            for (int j = 0; j < 11; j++){
                System.out.printf("%d\t", arr1[i][j]);
            }
            System.out.println();
        }

        //计算有效数据个数
        int sum = 0;
        for (int i = 0; i < 11; i++) {
            for (int j = 0; j < 11; j++) {
                if(arr1[i][j] != 0){
                    sum++;
                }
            }
        }

        //创建稀疏数组
        int[][] sparseArr = new int[sum + 1][3];
        //给稀疏数组赋值
        //这里我们设置一个计数器来控制稀疏数组行数的自增
        int count = 0;
        for (int i = 0; i < 11; i++) {
            for (int j = 0; j < 11; j++) {
                if(arr1[i][j] != 0){
                    count++;
                    sparseArr[count][0] = i;
                    sparseArr[count][1] = j;
                    sparseArr[count][2] = arr1[i][j];
                }
            }
        }

        //打印稀疏数组
        System.out.println("稀疏数组:");
        for (int i = 0; i < sum + 1; i++) {
            for (int j = 0; j < 3; j++) {
                System.out.printf("%d\t", sparseArr[i][j]);
            }
            System.out.println();
        }

        //把稀疏数组还原为原来的数组
        int[][] arr2 = new int[11][11];
        //遍历稀疏数组进行赋值,给新数组进行赋值从第一行开始遍历
        for (int i = 1; i < sum + 1; i++) {
            for (int j = 0; j < 3; j++) {
                arr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][j];
            }
        }

        //打印还原后的数组
        System.out.println("还原后的数组:");
        for (int i = 0; i < 11; i++) {
            for (int j = 0; j < 11; j++){
                System.out.printf("%d\t", arr2[i][j]);
            }
            System.out.println();
        }
    }
}

-------------------------------

运行结果:
原来的数组:
0	0	0	0	0	0	0	0	0	0	0	
0	0	1	0	0	0	0	0	0	0	0	
0	0	0	2	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	2	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
稀疏数组:
0	0	0	
1	2	1	
2	3	2	
4	5	2	
还原后的数组:
0	0	0	0	0	0	0	0	0	0	0	
0	0	1	0	0	0	0	0	0	0	0	
0	0	0	2	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	2	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	0	

Process finished with exit code 0

  • 补充练习

    要求:

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

3. 队列

队列介绍

队列是一个有序列表,可以用数组或是链表来实现。
遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

数组模拟队列

  • 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量。因为队列的输出、输入是分别从前后端来处理,因此需要两个变量front及rear分别记录队列前后端的下标,front 会随着数据输出而改变,而rear则是随着数据输入而改变,如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ZC8nJZP-1589021541026)(E:\Java学习资料\Java进阶笔记\图片\005.png)]

  • 思路分析:当我们将数据存入队列时称为”addQueue",addQueue 的处理需要有两个步骤:

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

import java.util.Scanner;

public class ArrayQueueMain {
    public static void main(String[] args) {
        ArrayQueue queue = new ArrayQueue(3);
        Scanner sc = new Scanner(System.in);

        //为了方便演示,我们首先制作一个菜单
        boolean b = true;
        while (b){
            System.out.println("s(show): 打印队列");
            System.out.println("a(add): 添加元素");
            System.out.println("o(out): 取出元素");
            System.out.println("f(first): 获取队首元素");
            System.out.println("e(exit): 退出程序");

            System.out.println("请输入您要执行的操作:");
            char c = sc.next().charAt(0);
            switch (c){
                case 's':
                    System.out.println("队列元素如下:");
                    queue.show();
                    break;
                case  'a':
                    try {
                        System.out.println("请输入您要添加的元素值:");
                        int i = sc.nextInt();
                        queue.addQueue(i);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        break;
                    }
                case 'o':
                    try {
                        int i = queue.outQueue();
                        System.out.println("取出的元素值为:" + i);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        break;
                    }
                case 'f':
                    try {
                        int first = queue.getFirst();
                        System.out.println("对首元素为:" + first);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        break;
                    }
                case 'e':
                    b = false;
                    break;
                default :
                    System.out.println("输入错误");
                    b = false;
                    break;
            }
        }
        System.out.println("退出程序");
    }
}

class ArrayQueue{
    private int numMax;//数组队列的最多存储的元素个数
    private int fount;//队头指针
    private int rear;//队尾指针
    private int[] arr;//队列数组

    //创建队列的构造器
    public ArrayQueue(int numMax){
        arr = new int[numMax];
        this.numMax = numMax;
        fount = -1;
        rear = -1;
    }

    //判断队列是否为空
    public boolean isEmpty(){
        return fount == rear;
    }
    //判断队列是否满
    public boolean isFull(){
        return rear == (numMax - 1);
    }
    //入队列
    public void addQueue(int value) throws Exception {
        //判断队列是否为满,如果为满就抛出异常
        if(isFull()){
            throw new Exception("队列已满,无法加入数据");
        }
        //否则,加入数据
        rear++;
        arr[rear] = value;
    }
    //出队列,并返回取出元素的值
    public int outQueue() throws Exception {
        //判断队列是否为空,如果为空抛出异常
        if(isEmpty()){
            throw new Exception("队列为空,无法进行出队操作");
        }
        //否则,出对列
        fount++;
        return arr[fount];
    }
    //返回队头元素
    public int getFirst() throws Exception {
        //判断队列是否为空,如果为空抛出异常
        if(isEmpty()){
            throw new Exception("队列为空,无法进行出队操作");
        }
        //否则返回队首元素
        return arr[fount + 1];
    }
    //打印队列
    public void show(){
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}
  • 上面的代码中出现的问题及其优化方案
    1. 目前数组使用一次就不能使用了,没有达到复用的效果
    2. 将这个数组使用相关算法,改进成一个环形数组(使用取模的方法(%))

数组模拟环形队列

目的:解决上一节中代码出现的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfOSKCPQ-1589021541028)(E:\Java学习资料\Java进阶笔记\图片\005.png)]

思路分析:

  1. front 变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素,front的初始值为0
  2. rear变量的含义做一个调整:rear执行队列的最后一个元素的后一个位置,因为我们希望空出一个空间作为约定(这个空出来的位置不放置队列元素,也就是我们定义了一个有n个元素的数组,但队列中最多只能存放n-1个元素).rear的初始值为0。
  3. 当队列满时,条件是(rear + 1)%maxSize = front【满】
  4. 当队列为空的条件:rear == front【空】
  5. 当我们这样分析后,队列中有效的数据的个数:(rear + maxSize-front)%maxSize
  6. 这样我们就可以在原来的队列上修改得到一个环形队列

代码实现:

import java.util.Scanner;

public class ArrayCircleQueueMain {
    public static void main(String[] args) {
        CircleQueue queue = new CircleQueue(4);
        Scanner sc = new Scanner(System.in);

        //为了方便演示,我们首先制作一个菜单
        boolean b = true;
        while (b){
            System.out.println("s(show): 打印队列");
            System.out.println("a(add): 添加元素");
            System.out.println("o(out): 取出元素");
            System.out.println("f(first): 获取队首元素");
            System.out.println("e(exit): 退出程序");

            System.out.println("请输入您要执行的操作:");
            char c = sc.next().charAt(0);
            switch (c){
                case 's':
                    System.out.println("队列元素如下:");
                    try {
                        queue.show();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case  'a':
                    try {
                        System.out.println("请输入您要添加的元素值:");
                        int i = sc.nextInt();
                        queue.add(i);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        break;
                    }
                case 'o':
                    try {
                        int i = queue.out();
                        System.out.println("取出的元素值为:" + i);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        break;
                    }
                case 'f':
                    try {
                        int first = queue.getFirst();
                        System.out.println("对首元素为:" + first);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        break;
                    }
                case 'e':
                    b = false;
                    break;
                default :
                    System.out.println("输入错误");
                    b = false;
                    break;
            }
        }
        System.out.println("退出程序");
    }
}

class CircleQueue{
    private int maxSize;
    private int front;
    private int rear;
    private int[] arr;

    //创建队列
    public CircleQueue(int maxSize){
        this.maxSize = maxSize;
        arr = new int[maxSize];
        front = 0;
        rear = 0;
    }

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

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

    //入队
    public void add(int num) throws Exception {
        //判断队列是否为满
        if(isFull()){
            throw new Exception("队列为满,不能添加元素!!!");
        }
        //如果不为满,就添加元素到队列尾
        arr[rear] = num;
        rear = (rear + 1) % maxSize;
    }

    //出队
    public int out() throws Exception {
        //判断队列是否为空
        if(isEmpty()){
            throw new Exception("队列为空,不能取出元素");
        }
        //如果不为空,就取出队首元素
        int value = arr[front];
        front++;
        return value;
    }

    //查看队首元素
    public int getFirst() throws Exception {
        //判断队列是否为空
        if(isEmpty()){
            throw new Exception("队列为空,不能取出元素");
        }
        //如果不为空,返回队首元素
        return arr[front];
    }

    //打印队列
    public void show() throws Exception {
        //判断队列是否为空,如果为空,就无法打印
        if(isEmpty()){
            throw new Exception("队列为空");
        }
        //打印队列
        for(int i = front; i < front + queueSize(); i++){
            System.out.printf("arr[%d] = %d\n", i % maxSize, arr[i % maxSize]);
        }
    }

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

链表(Linked List)

单向链表

  1. 链表是以节点的方式来存储的,是链式存储
  2. 每个节点包含data域,next域(指向下一个节点)
  3. 链表的各个节点不一定是连续存储
  4. 链表分带头结点的链表和没有头节点的链表,根据实际的需求来确定

带头节点单链表的逻辑结构示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qiPoEqlG-1589021541029)(E:\Java学习资料\Java进阶笔记\图片\006)]

head节点:

  1. 不存放具体的数据
  2. 作用就是表示单链表的表头next

应用实例

使用带head头的单向链表实现水浒英雄排行榜管理

  1. 完成对英雄人物的增删改查操作
  2. 第一种方式,在添加英雄时,直接添加到链表的尾部
  3. 第二种方式,在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
第一种方式
  • 添加(创建)
    1. 先创建一个head头节点,作用就是表示单链表的头
    2. 后面我们每添加一个节点,就直接加入到链表的最后
  • 遍历
    1. 通过一个辅助变量帮助遍历整个链表

代码实现:

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //创建一个单链表
        SingleLinkedList list = new SingleLinkedList();
        //创建4个节点
        HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode node3 = new HeroNode(3, "吴用", "智多星");
        HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
        HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
        //给链表添加节点
        list.add(node1);
        list.add(node3);
        list.add(node4);
        list.add(node2);
        //打印链表
        list.show();
    }
}

class SingleLinkedList{
    //创建头节点

    HeroNode head = new HeroNode(0, null, null);

    //在链表尾部添加元素
    public void add(HeroNode heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;

        //遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
        while(true){
            if(temp.next == null){
                temp.next = heroNode;
                break;
            }
            temp = temp.next;
        }
    }

    //打印链表
    public void show(){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        while(true){
            if(temp.next == null){
                break;
            }
            System.out.println(temp.next);
            temp = temp.next;
        }
    }
}

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

    //创建节点
    public HeroNode(int no, String name, String nickname){
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

    @Override
    public String toString() {
        return "[" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                ']';
    }
}
-----------------------
运行结果:
[[no=1, name='宋江', nickname='及时雨']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
[no=2, name='卢俊义', nickname='玉麒麟']

Process finished with exit code 0

第二种方式

需要按照编号的顺序添加

  1. 首先找到新添加的节点的位置(通过辅助指针temp ),通过遍历
  2. 新的节点.next=temp.next
  3. 将temp.next=新的节点

代码实现:

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //创建一个单链表
        SingleLinkedList list = new SingleLinkedList();
        //创建4个节点
        HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode node3 = new HeroNode(3, "吴用", "智多星");
        HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
        HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
        //给链表添加节点
        list.addByOrder(node1);
        list.addByOrder(node3);
        list.addByOrder(node4);
        list.addByOrder(node2);
        list.addByOrder(node2);
        //打印链表
        list.show();
    }
}

class SingleLinkedList{
    //创建头节点

    HeroNode head = new HeroNode(0, null, null);

    //按no的顺序添加元素,如果已经存在,相同的no,就不添加
    public void addByOrder(HeroNode heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        //定义一个flag来标记是否存在相同的节点,默认为false
        boolean flag = false;

        //遍历链表,如果找到指定的位置,就跳出循环
        while(true){
            if(temp.next == null){//说明已经到达链表的尾部
                break;
            }
            if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
                break;
            } else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
                flag = true;
                break;
            }else {
                temp = temp.next;
            }
        }

        //跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
        if(flag){
            System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
        }else{
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    //打印链表
    public void show(){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        while(true){
            if(temp.next == null){
                break;
            }
            System.out.println(temp.next);
            temp = temp.next;
        }
    }
}

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

    //创建节点
    public HeroNode(int no, String name, String nickname){
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

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

------------------
运行结果:
no为2的节点已经存在,不可重复添加
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']

Process finished with exit code 0

修改节点的信息

根据no编号来修改,即no编号不能改

代码实现及测试

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //创建一个单链表
        SingleLinkedList list = new SingleLinkedList();
        //创建4个节点
        HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode node3 = new HeroNode(3, "吴用", "智多星");
        HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
        HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
        //给链表添加节点
        list.addByOrder(node1);
        list.addByOrder(node3);
        list.addByOrder(node4);
        list.addByOrder(node2);
        //list.addByOrder(node2);
        //打印链表
        list.show();
        HeroNode node5 = new HeroNode(2, "小卢", "小玉");
        System.out.println("修改后的链表为:");
        list.update(node5);//修改编号为2的节点的信息
        list.show();
    }
}

class SingleLinkedList{
    //创建头节点

    HeroNode head = new HeroNode(0, null, null);

   /* //在链表尾部添加元素
    public void add(HeroNode heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;

        //遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
        while(true){
            if(temp.next == null){
                temp.next = heroNode;
                break;
            }
            temp = temp.next;
        }
    }
*/
    //按no的顺序添加元素,如果已经存在,相同的no,就不添加
    public void addByOrder(HeroNode heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        //定义一个flag来标记是否存在相同的节点,默认为false
        boolean flag = false;

        //遍历链表,如果找到指定的位置,就跳出循环
        while(true){
            if(temp.next == null){//说明已经到达链表的尾部
                break;
            }
            if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
                break;
            } else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
                flag = true;
                break;
            }else {
                temp = temp.next;
            }
        }

        //跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
        if(flag){
            System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
        }else{
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    //修改节点
    public void update(HeroNode newHeroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head.next;
        //定义一个flag来标识找到与否,默认为false
        boolean flag =false;

        //遍历链表,在指定情况下跳出循环
        while (true){
            if(temp == null){//到达链表末尾
                break;
            }
            if(temp.no == newHeroNode.no){//说明找到了
                flag = true;
                break;
            }else{
                temp = temp.next;
            }
        }
        //跳出循环后,对flag进行判断
        if(flag){//找到了
            temp.name = newHeroNode.name;
            temp.nickname = newHeroNode.nickname;
        }else{
            System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
        }
    }

    //打印链表
    public void show(){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        while(true){
            if(temp.next == null){
                break;
            }
            System.out.println(temp.next);
            temp = temp.next;
        }
    }
}

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

    //创建节点
    public HeroNode(int no, String name, String nickname){
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

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

------------------

[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
修改后的链表为:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='小卢', nickname='小玉']
[no=3, name='吴用', nickname='智多星'] 
[no=4, name='林冲', nickname='豹子头']

Process finished with exit code 0
单链表节点的删除

思路:

  1. 我们先找到需要删除的这个节点的前一个节点temp
  2. temp.next = temp.next.next
  3. 被删除的节点,将不会有其他引用指向,会被垃圾回收机制回收

代码示例:

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //创建一个单链表
        SingleLinkedList list = new SingleLinkedList();
        //创建4个节点
        HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode node3 = new HeroNode(3, "吴用", "智多星");
        HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
        HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
        //给链表添加节点
        list.addByOrder(node1);
        list.addByOrder(node3);
        list.addByOrder(node4);
        list.addByOrder(node2);
        //list.addByOrder(node2);
        //打印链表
        list.show();
        list.delete(1);
        list.delete(4);
        list.delete(2);

        //打印删除后的
        System.out.println("删除后的链表为:");
        list.show();
    }
}

class SingleLinkedList{
    //创建头节点

    HeroNode head = new HeroNode(0, null, null);

   /* //在链表尾部添加元素
    public void add(HeroNode heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;

        //遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
        while(true){
            if(temp.next == null){
                temp.next = heroNode;
                break;
            }
            temp = temp.next;
        }
    }
*/
    //按no的顺序添加元素,如果已经存在,相同的no,就不添加
    public void addByOrder(HeroNode heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        //定义一个flag来标记是否存在相同的节点,默认为false
        boolean flag = false;

        //遍历链表,如果找到指定的位置,就跳出循环
        while(true){
            if(temp.next == null){//说明已经到达链表的尾部
                break;
            }
            if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
                break;
            } else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
                flag = true;
                break;
            }else {
                temp = temp.next;
            }
        }

        //跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
        if(flag){
            System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
        }else{
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    //修改节点
    public void update(HeroNode newHeroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head.next;
        //定义一个flag来标识找到与否,默认为false
        boolean flag =false;

        //遍历链表,在指定情况下跳出循环
        while (true){
            if(temp == null){//到达链表末尾
                break;
            }
            if(temp.no == newHeroNode.no){//说明找到了
                flag = true;
                break;
            }else{
                temp = temp.next;
            }
        }
        //跳出循环后,对flag进行判断
        if(flag){//找到了
            temp.name = newHeroNode.name;
            temp.nickname = newHeroNode.nickname;
        }else{
            System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
        }
    }

    //打印链表
    public void show(){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        if(head.next == null){
            System.out.println("链表为空");
        }
        while(true){
            if(temp.next == null){
                break;
            }
            System.out.println(temp.next);
            temp = temp.next;
        }
    }

    //删除节点
    public void delete(int no){
        HeroNode temp = head;
        boolean flag = false;
        while(true){
            if(temp.next == null){
                break;
            }
            if(temp.next.no == no){
                flag = true;
                break;
            }else{
                temp = temp.next;
            }
        }

        if(flag){
            temp.next = temp.next.next;
        }else{
            System.out.printf("要删除的%d节点不存在\n", no);
        }
    }
}

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

    //创建节点
    public HeroNode(int no, String name, String nickname){
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

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

----------------------------
运行结果:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
删除后的链表为:
[no=3, name='吴用', nickname='智多星']

Process finished with exit code 0

练习题

  1. 计算单链表的有效节点个数(如果是带头节点的链表,不统计头节点)
  2. 查找单链表中的倒数第k个节点
  3. 单链表的反转
  4. 从位到头打印单链表(用栈的方式)
  5. 合并两个有序的单链表,合并之后的链表依然有序(以后再实现)

代码实现及测试:

import java.util.Stack;

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //创建一个单链表
        SingleLinkedList list = new SingleLinkedList();
        //创建4个节点
        HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode node3 = new HeroNode(3, "吴用", "智多星");
        HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
        HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
        //给链表添加节点
        list.addByOrder(node1);
        list.addByOrder(node3);
        list.addByOrder(node4);
        list.addByOrder(node2);
        //list.addByOrder(node2);
        //打印链表
        list.show();
        list.delete(1);

        //打印删除后的
        System.out.println("删除后的链表为:");
        list.show();
        System.out.printf("节点的有效个数为:%d\n", getLength(list.getHead()));
        //查找倒数第二个元素
        HeroNode lastIndexNode = findLastIndexNode(list.getHead(), 2);
        System.out.println("倒数第二个元素为:" + lastIndexNode);
        //反转链表
        reverse(list.getHead());
        //打印反转后的链表
        System.out.println("打印反转后的链表:");
        list.show();
        //在不改变链表结构的前提下,从尾到头打印单链表
        System.out.println("从尾到头打印单链表,不改变链表结构");
        reverseShow(list.getHead());
    }

    //计算单链表节点的个数
    public static int getLength(HeroNode head) {//参数为头节点
        HeroNode temp = head.next;
        if (temp == null) {
            return 0;
        }
        int length = 0;
        while (temp != null) {
            length++;
            temp = temp.next;
        }
        return length;
    }

    //查找倒数第k个节点并返回
    //1.编写一个方法,接收head节点,同时接收一个index
    //2.index 表示的是倒数第index个节点
    //3.先把链表从头到尾遍历,得到链表的总长度getLength()
    //4.得到size后我们从链表的第一个开始遍历(size - index)个,就可以得到我们要找的节点
    //5.如果找到了,则返回该节点,否则返回null
    public static HeroNode findLastIndexNode(HeroNode head, int index){
        if(head.next == null){
            return null;
        }
        HeroNode temp = head.next;
        if(index <= 0 || index > getLength(head)){
            return null;
        }
        for(int i = 0; i < getLength(head) - index; i++){
            temp = temp.next;
        }
        return temp;
    }

    //单链表的反转
    //1.先定义一个节点 reverseHead = new HeroNode();
    //2.从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端
    //3.原来的链表的head.next = reverseHead.next
    public static void reverse(HeroNode head){
        //如果该链表为空或者只有一个节点,那么就不用反转
        if(head.next == null || head.next.next == null){
            return;
        }
        HeroNode reverseHead = new HeroNode(0, "", "");
        HeroNode cur = head.next;//指向当前节点
        HeroNode next = null;//指向当前节点的下一个节点
        while(cur != null){
            next = cur.next;
            cur.next = reverseHead.next;
            reverseHead.next = cur;
            cur = next;
        }

        head.next = reverseHead.next;
    }

    //逆序打印单链表,不改变链表的结构(使用栈)
    public static void reverseShow(HeroNode head){
        //如果链表为空,则不打印
        if(head.next == null){
            return;
        }

        Stack<HeroNode> heroNodes = new Stack<>();
        HeroNode cur = head.next;
        while(cur != null){
            heroNodes.push(cur);
            cur = cur.next;
        }
        //出栈,并打印
        while(heroNodes.size() > 0){
            System.out.println(heroNodes.pop());
        }
    }
}

class SingleLinkedList{
    //创建头节点

   private HeroNode head = new HeroNode(0, null, null);

    public HeroNode getHead() {
        return head;
    }

    /* //在链表尾部添加元素
        public void add(HeroNode heroNode){
            //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
            HeroNode temp = head;

            //遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
            while(true){
                if(temp.next == null){
                    temp.next = heroNode;
                    break;
                }
                temp = temp.next;
            }
        }
    */
    //按no的顺序添加元素,如果已经存在,相同的no,就不添加
    public void addByOrder(HeroNode heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        //定义一个flag来标记是否存在相同的节点,默认为false
        boolean flag = false;

        //遍历链表,如果找到指定的位置,就跳出循环
        while(true){
            if(temp.next == null){//说明已经到达链表的尾部
                break;
            }
            if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
                break;
            } else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
                flag = true;
                break;
            }else {
                temp = temp.next;
            }
        }

        //跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
        if(flag){
            System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
        }else{
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    //修改节点
    public void update(HeroNode newHeroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head.next;
        //定义一个flag来标识找到与否,默认为false
        boolean flag =false;

        //遍历链表,在指定情况下跳出循环
        while (true){
            if(temp == null){//到达链表末尾
                break;
            }
            if(temp.no == newHeroNode.no){//说明找到了
                flag = true;
                break;
            }else{
                temp = temp.next;
            }
        }
        //跳出循环后,对flag进行判断
        if(flag){//找到了
            temp.name = newHeroNode.name;
            temp.nickname = newHeroNode.nickname;
        }else{
            System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
        }
    }

    //打印链表
    public void show(){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode temp = head;
        if(head.next == null){
            System.out.println("链表为空");
        }
        while(true){
            if(temp.next == null){
                break;
            }
            System.out.println(temp.next);
            temp = temp.next;
        }
    }

    //删除节点
    public void delete(int no){
        HeroNode temp = head;
        boolean flag = false;
        while(true){
            if(temp.next == null){
                break;
            }
            if(temp.next.no == no){
                flag = true;
                break;
            }else{
                temp = temp.next;
            }
        }

        if(flag){
            temp.next = temp.next.next;
        }else{
            System.out.printf("要删除的%d节点不存在\n", no);
        }
    }
}

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

    //创建节点
    public HeroNode(int no, String name, String nickname){
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

    @Override
    public String toString() {
        return "[" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                ']';
    }
}
----------------------------
测试结果:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
删除后的链表为:
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
节点的有效个数为:3
倒数第二个元素为:[no=3, name='吴用', nickname='智多星']
打印反转后的链表:
[no=4, name='林冲', nickname='豹子头']
[no=3, name='吴用', nickname='智多星']
[no=2, name='卢俊义', nickname='玉麒麟']
从尾到头打印单链表,不改变链表结构
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']

Process finished with exit code 0

双向链表

分析:

  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.pre(要考虑temp是否是最后一个节点)
  5. 按顺序添加节点(以后实现)

代码实现:

public class DoubleLinkedListDemo {
    public static void main(String[] args) {
        DoubleLinkedList list = new DoubleLinkedList();

        HeroNode2 node1 = new HeroNode2(1, "宋江", "及时雨");
        HeroNode2 node3 = new HeroNode2(3, "吴用", "智多星");
        HeroNode2 node4 = new HeroNode2(4, "林冲", "豹子头");
        HeroNode2 node2 = new HeroNode2(2, "卢俊义", "玉麒麟");

        list.add(node1);
        list.add(node2);
        list.add(node3);
        list.add(node4);
        System.out.println("初始的链表: ");
        list.show();

        //修改
        HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "入云龙");
        list.update(newHeroNode);
        System.out.println("修改之后的链表:");
        list.show();

        //删除
        list.delete(1);
        System.out.println("删除之后的链表:");
        list.show();
    }
}


class DoubleLinkedList {
    //创建头节点
    private HeroNode2 head = new HeroNode2(0, null, null);

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

    //打印链表
    public void show(){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode2 temp = head;
        if(head.next == null){
            System.out.println("链表为空");
            return;
        }
        while(true){
            if(temp.next == null){
                break;
            }
            System.out.println(temp.next);
            temp = temp.next;
        }
    }

    //遍历链表并在末尾添加元素
    public void add(HeroNode2 heroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode2 temp = head;

        //遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
        while(true){
            if(temp.next == null){
                temp.next = heroNode;
                break;
            }
            temp = temp.next;
        }
        temp.next = heroNode;
        heroNode.pre = temp;
    }

    //修改节点,和单链表的方式一样
    public void update(HeroNode2 newHeroNode){
        //因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
        HeroNode2 temp = head.next;
        //定义一个flag来标识找到与否,默认为false
        boolean flag =false;

        //遍历链表,在指定情况下跳出循环
        while (true){
            if(temp == null){//到达链表末尾
                break;
            }
            if(temp.no == newHeroNode.no){//说明找到了
                flag = true;
                break;
            }else{
                temp = temp.next;
            }
        }
        //跳出循环后,对flag进行判断
        if(flag){//找到了
            temp.name = newHeroNode.name;
            temp.nickname = newHeroNode.nickname;
        }else{
            System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
        }
    }

    //删除某个节点
    public void delete(int no){
        HeroNode2 temp = head.next;
        boolean flag = false;
        while(true){
            if(temp == null){
                break;
            }
            if(temp.no == no){
                flag = true;
                break;
            }else{
                temp = temp.next;
            }
        }

        if(flag){
            temp.pre.next = temp.next;
            if(temp.next != null){
                temp.next.pre = temp.pre;
            }
        }else{
            System.out.printf("要删除的%d节点不存在\n", no);
        }
    }
}

class HeroNode2{
    public int no;
    public String name;
    public String nickname;
    public HeroNode2 next;
    public HeroNode2 pre;

    //创建节点
    public HeroNode2(int no, String name, String nickname){
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

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

---------------------

测试结果:
初始的链表: 
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
修改之后的链表:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='公孙胜', nickname='入云龙']
删除之后的链表:
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='公孙胜', nickname='入云龙']

Process finished with exit code 0

单向环形链表

以约瑟夫环为例。

约瑟夫环问题:

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

提示:

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

思路分析:

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

遍历环形链表:

  1. 先让一个辅助指针(变量)curBoy,指向first节点
  2. 然后通过一个while循环遍历该环形链表即可,curBoy.next == first表示结束

定义一个方法,生成指定的出列顺序,步骤如下:

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

    补充:小孩报数前,先让first和helper移动k - 1次

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

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

    first = first.next

    helper.next = first

    原来first指向的节点就会没有任何引用,会被回收

代码实现:

public class Jusephu {
    public static void main(String[] args) {
        //创建一个环形链表
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        //添加五个小孩
        circleSingleLinkedList.addBoys(5);
        //遍历
        circleSingleLinkedList.show();
        //我们指定有5个小孩,从第2个小孩开始报数,m = 2
        circleSingleLinkedList.jusephuNum(2, 2, 5);
    }
}

//创建环形链表
class CircleSingleLinkedList{
    private BoyNode first = null;

    //在这个环形链表中添加一定数量的小孩,参数为小孩的数量
    public void addBoys(int num){
        if(num < 1){
            System.out.println("输入不合法");
            return;
        }
        BoyNode curBoy = null;
        for (int i = 1; i <= num; i++) {
            BoyNode boyNode = new BoyNode(i);
            if(i == 1){
                first = boyNode;
                curBoy = boyNode;
                first.setNext(first);
            }else{
                curBoy.setNext(boyNode);
                boyNode.setNext(first);
                curBoy = curBoy.getNext();
            }
        }
    }

    //遍历环形链表
    public void show(){
        //判断链表是否为空
        if(first == null){
            System.out.println("链表为空");
            return;
        }
        //定义一个辅助变量
        BoyNode temp = first;
        while(true){
            System.out.println(temp.getNo());
            temp = temp.getNext();
            if(temp == first){
                return;
            }
        }
    }

    public void jusephuNum(int startNo, int countNum, int nums) {
        //对数据的正确性进行校验
        if (startNo < 1 || startNo > nums || countNum < 1 || countNum > nums) {
            return;
        }
        //创建一个helper指针,指向最后一个节点
        BoyNode helper = null;
        BoyNode temp = first;
        while (true) {
            if (temp.getNext() == first) {
                helper = temp;
                break;
            } else {
                temp = temp.getNext();
            }
        }

        //在进行报数前,我们需要把helper和first同时移动startNo - 1次
        for (int i = 0; i < startNo - 1; i++) {
            first = first.getNext();
            helper = helper.getNext();
        }
        //报数过程
        while (true) {
            if(first == helper){
                //此时链表中还有一个节点,跳出循环
                break;
            }

            //报数的过程,helper和first同时移动countNum - 1次,移动之后,first所指向的节点即是要出列的节点
            for (int i = 0; i < countNum - 1; i++) {
                first = first.getNext();
                helper = helper.getNext();
            }

            //让指定的小孩出链表
            System.out.printf("编号为%d的小孩出列\n", first.getNo());
            first = first.getNext();
            helper.setNext(first);
        }
        System.out.printf("最后出列的小孩的编号为:%d", first.getNo());
    }
}

//创建小孩节点
class BoyNode{
    private int no;
    private BoyNode next;

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

    public int getNo() {
        return no;
    }

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

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

    public BoyNode getNext() {
        return next;
    }

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

-----------------------
测试结果:
1
2
3
4
5
编号为3的小孩出列
编号为5的小孩出列
编号为2的小孩出列
编号为1的小孩出列
最后出列的小孩的编号为:4
Process finished with exit code 0

栈(Stack)

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

数组模拟栈

思路:

  1. 定义一个top来表示栈顶,初始化为-1
  2. 入栈的操作,当有数据加入到栈时,top++;stack[top] = data
  3. 出栈的操作,int value = stack[top]; top–; return value

代码实现:

import java.util.Scanner;

public class ArrayStackDemo {
    public static void main(String[] args) {
        ArrayStack stack = new ArrayStack(4);
        String key = "";
        boolean loop = true;
        Scanner sc = new Scanner(System.in);

        while(loop){
            System.out.println("exit: 退出程序");
            System.out.println("show: 显示栈数据");
            System.out.println("pop: 出栈");
            System.out.println("push: 入栈");
            System.out.println("请输入您要执行的操作:");
            key = sc.next();
            switch (key){
                case "exit":
                    loop = false;
                    break;
                case "show":
                    stack.list();
                    break;
                case "pop":
                    try {
                        int value = stack.pop();
                        System.out.println("出栈的数据为" + value);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    break;
                case "push":
                    System.out.println("请输入您要入栈的数据:");
                    int num = sc.nextInt();
                    stack.push(num);
                    break;
                default:
                    break;
            }
        }
        System.out.println("退出程序");
    }
}

class ArrayStack{
    private int maxSize;
    private int top = -1;
    private int[] arr;

    //构造方法
    public ArrayStack(int maxSize){
        this.maxSize = maxSize;
        arr = new int[this.maxSize];
    }

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

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

    //入栈
    public void push(int num){
        //判断栈是否满
        if(isFull()){
            System.out.println("栈满,无法入栈");
            return;
        }
        top++;
        arr[top] = num;
    }

    //出栈
    public int pop(){
        //判断栈是否为空
        if(isEmpty()){
            throw new RuntimeException("栈为空,无法出栈");
        }
        int v = arr[top];
        top--;
        return v;
    }

    //遍历栈(从栈顶开始显示数据)
    public void list(){
        //判断栈是否为空
        if(isEmpty()){
            System.out.println("栈为空");
            return;
        }
        for(int i = top; i >= 0; i--){
            System.out.printf("arr[%d] = %d\n", i, arr[i]);
        }
    }
}

链表模拟栈

(略)

栈实现综合计算器

只有加减乘除。

思路:

  1. 通过一个index值(索引),来遍历我们的表达式
  2. 如果我们发现是一个数字, 就直接入数栈
  3. 如果发现扫描到是一个符号,就分如下情况
    • 如果发现当前的符号栈为空,就直接入栈
    • 如果符号栈有操作符,就进行比较,如果当 前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数在从符号栈中pop出一个符号, 进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈,如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈.
  4. 当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行.
  5. 最后在数栈只有一个数字,就是表达式的结果

代码实现:

public class Calculator {
    public static void main(String[] args) {
        String expression = "100*2+3*3*5-3/1";
        //创建两个栈,一个数字栈,一个符号栈
        ArrayStack2 numStack = new ArrayStack2(10);
        ArrayStack2 operStack = new ArrayStack2(10);
        //定义一些操作数
        int index = 0;//用来扫描字符串
        int num1;
        int num2;
        char ch;
        int oper;
        String k = "";
        String p = "";//用来对表达式进行展望
        while (true){
            ch = expression.substring(index, index + 1).charAt(0);
            //判断是否为运算符
            if(operStack.isOper(ch)){
                //判断符号栈是否为空
                if(!operStack.isEmpty()){
                    //如果不为空
                    //判断当前操作符的优先级是否小于等于栈顶优先级
                    if(operStack.charClass(ch) <= operStack.charClass(operStack.peek())){
                        //数字栈中弹出两个数,符号栈中弹出栈定符号,进行运算
                        num1 = numStack.pop();
                        num2 = numStack.pop();
                        oper = operStack.pop();
                        int res = numStack.cal(num1, num2, oper);
                        numStack.push(res);
                        //运算符入栈
                        operStack.push(ch);
                    }else{
                        //直接入栈
                        operStack.push(ch);
                    }
                }else {
                    //符号栈为空,直接入栈
                    operStack.push(ch);
                }
            }else{
                //不是运算符
                k += ch;//如果下一位不是运算符,就会一直拼接下去
                if(index == expression.length() - 1){
                    numStack.push(Integer.parseInt(k));
                }else{
                    if(operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))){
                        numStack.push(Integer.parseInt(k));
                        //清空k
                        k = "";
                    }
                }
            }

            index++;
            if(index >= expression.length()){
                break;
            }
        }

        //对最后两个栈中的元素进行判断,如果运算符栈为空,那么数字栈就只剩一个数字,即为结果
        while(true){
            if(operStack.isEmpty()){
                break;
            }
            num1 = numStack.pop();
            num2 = numStack.pop();
            oper = operStack.pop();
            int res = numStack.cal(num1, num2, oper);
            numStack.push(res);
        }
        //打印运算结果
        System.out.println(expression + "=" + numStack.pop());
    }
}

class ArrayStack2{
    private int maxSize;
    private int top = -1;
    private int[] arr;

    //构造方法
    public ArrayStack2(int maxSize){
        this.maxSize = maxSize;
        arr = new int[this.maxSize];
    }

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

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

    //入栈
    public void push(int num){
        //判断栈是否满
        if(isFull()){
            System.out.println("栈满,无法入栈");
            return;
        }
        top++;
        arr[top] = num;
    }

    //出栈
    public int pop(){
        //判断栈是否为空
        if(isEmpty()){
            throw new RuntimeException("栈为空,无法出栈");
        }
        int v = arr[top];
        top--;
        return v;
    }

    //遍历栈(从栈顶开始显示数据)
    public void list(){
        //判断栈是否为空
        if(isEmpty()){
            System.out.println("栈为空");
            return;
        }
        for(int i = top; i >= 0; i--){
            System.out.printf("arr[%d] = %d\n", i, arr[i]);
        }
    }

    //查看栈定元素
    public int peek(){
        return arr[top];
    }

    //判断是否为运算符
    boolean isOper(char ch){
        return ch == '+' || ch == '-' || ch == '*' || ch == '/';
    }

    //返回运算符的优先级,优先级越高,返回值越大
    public int charClass(int ch){
        if(ch == '*' || ch == '/'){
            return 1;
        }else if(ch == '+' || ch == '-'){
            return 0;
        }else {
            return -1;
        }
    }

    //表达式运算
    public int cal(int num1, int num2, int oper){
        if(oper == '+'){
            return num1 + num2;
        }else if(oper == '-'){
            return num2 - num1;
        }else if(oper == '*'){
            return num1 * num2;
        }else if(oper == '/'){
            return num2 / num1;
        }else {
            return -1;
        }
    }
}

前缀、中缀、后缀表达式

前缀表达式(波兰表达式)

前缀表达式的计算机求值:

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

例如:(3+4)X5-6对应的前缀表达式就是- X +3456,针对前缀表达式求值步骤如下:

  1. 从右至左扫描,将6、5、4、3压入堆栈
  2. 遇到+运算符, 因此弹出3和4 (3为栈项元素,4为次项元素),计算出3+4的值,得7,再将7入栈
  3. 接下来是X运算符,因此弹出7和5,计算出7X5=35, 将35入栈
  4. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式

  1. 中缀表达式就是常见的运算表达式,如(3+4)X5-6
  2. 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)

后缀表达式

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

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

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

逆波兰计算器的实现

  1. 输入一个逆波兰表达式,使用栈(Stack), 计算其结果
  2. 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
  3. 思路分析
  4. 代码完成

代码实现:

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

public class PolandNotation {
    public static void main(String[] args) throws Exception {
        //先给定一个逆波兰表达式
        String notation = "31 4 + 5 * 6 -";
        //把给定的表达式存放在一个集合中
        ArrayList<String> list = toList(notation);
        System.out.println(list);
        //给定一个集合,返回计算结果
        int result = getResult(list);
        System.out.println("计算结果为:" + result);
    }

    public static ArrayList<String> toList(String notation){
        ArrayList<String> list = new ArrayList<>();
        String[] s = notation.split(" ");
        for(String e : s){
            list.add(e);
        }
        return list;
    }

    public static int getResult(ArrayList<String> list) throws Exception {
        Stack<String> stack = new Stack<>();
        int num1;
        int num2;

        for(String s : list){
            int res = 0;
            if(s.matches("\\d+")){//匹配多位数(正则表达式)
                stack.push(s);
            }else if(s.equals("+")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 + num1;
                stack.push("" + res);
            }else if(s.equals("-")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 - num1;
                stack.push("" + res);
            }else if(s.equals("*")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 * num1;
                stack.push("" + res);
            }else if(s.equals("/")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 / num1;
                stack.push("" + res);
            }else {
                throw new Exception("表达式有误!!!");
            }
        }
        return Integer.parseInt(stack.pop());
    }
}

中缀表达式转后缀表达式

思路分析:

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

  2. 从左至右扫描中缀表达式

  3. 遇到操作数时,将其压入s2

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

    1. 如果s1为空,或栈顶运算符为左括号”(“,则直接将次运算符入栈

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

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

  5. 遇到括号时:

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

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

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

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

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

代码实现:

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

public class PolandNotationPlus {
    public static void main(String[] args) throws Exception {
        String expression = "1+((2+3)*4)-5";

        //把中缀表达式字符串转换为一个存放各个数字和运算符的ArrayList
        ArrayList<String> infixList = infixToList(expression);
        System.out.println(infixList);

        //把存放中缀表达式的集合转换为后缀表达式集合
        ArrayList<String> list = infixToSuffixList(infixList);
        System.out.println(list);

        int result = getResult(list);
        System.out.println(result);
    }
    //把中缀表达式字符串转换为一个存放各个数字和运算符的ArrayList
    //方法如下:
    public static ArrayList<String> infixToList(String expression){
        ArrayList<String> list = new ArrayList<>();
        int i = 0;
        String key;
        char c;
        do{
            if((c = expression.charAt(i)) < 48 || (c = expression.charAt(i)) > 57){
                list.add("" + c);
                i++;
            }else{
                key = "";
                while(i < expression.length() && (c = expression.charAt(i)) >= 48 && (c = expression.charAt(i)) <= 57){
                    key += c;
                    i++;
                }
                list.add(key);
            }
        }while (i < expression.length());

        return list;
    }

    //把存放中缀表达式的集合转换为后缀表达式集合
    //方法如下
    public static ArrayList<String> infixToSuffixList(ArrayList<String> list){
        ArrayList<String> ls = new ArrayList<>();
        Stack<String> s1 = new Stack<>();

        for(String item : list){
            if(item.matches("\\d+")){
                ls.add(item);
            }else if(item.equals("(")){
                s1.push(item);
            }else if(item.equals(")")){
                while(!s1.peek().equals("(")){
                    ls.add(s1.pop());
                }
                s1.pop();
            }else{
                while (true){
                    if(s1.size() == 0 || s1.peek().equals("(")){
                        s1.push(item);
                        break;
                    }else if(Operation.oper(item) > Operation.oper(s1.peek())){
                        s1.push(item);
                        break;
                    }else{
                        ls.add(s1.pop());
                    }
                }
            }
        }
        while (s1.size() != 0){
            ls.add(s1.pop());
        }
        return ls;
    }

    //输入后缀表达式,得出结果
    public static int getResult(ArrayList<String> list) throws Exception {
        Stack<String> stack = new Stack<>();
        int num1;
        int num2;

        for(String s : list){
            int res = 0;
            if(s.matches("\\d+")){//匹配多位数(正则表达式)
                stack.push(s);
            }else if(s.equals("+")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 + num1;
                stack.push("" + res);
            }else if(s.equals("-")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 - num1;
                stack.push("" + res);
            }else if(s.equals("*")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 * num1;
                stack.push("" + res);
            }else if(s.equals("/")){
                num1 = Integer.parseInt(stack.pop());
                num2 = Integer.parseInt(stack.pop());
                res = num2 / num1;
                stack.push("" + res);
            }else {
                throw new Exception("表达式有误!!!");
            }
        }
        return Integer.parseInt(stack.pop());
    }
}

//比较运算符优先级的类
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 oper(String s){
        if(s.equals("+")){
            return ADD;
        }else if(s.equals("-")){
            return SUB;
        }else if(s.equals("*")){
            return MUL;
        }else{
            return DIV;
        }
    }
}

逆波兰计算器完整版实现

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

public class ReversePolishMultiCalc {

	 /**
     * 匹配 + - * / ( ) 运算符
     */
    static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";

    static final String LEFT = "(";
    static final String RIGHT = ")";
    static final String ADD = "+";
    static final String MINUS= "-";
    static final String TIMES = "*";
    static final String DIVISION = "/";

    /**
     * 加減 + -
     */
    static final int LEVEL_01 = 1;
    /**
     * 乘除 * /
     */
    static final int LEVEL_02 = 2;

    /**
     * 括号
     */
    static final int LEVEL_HIGH = Integer.MAX_VALUE;


    static Stack<String> stack = new Stack<>();
    static List<String> data = Collections.synchronizedList(new ArrayList<String>());

    /**
     * 去除所有空白符
     * @param s
     * @return
     */
    public static String replaceAllBlank(String s ){
        // \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
        return s.replaceAll("\\s+","");
    }

    /**
     * 判断是不是数字 int double long float
     * @param s
     * @return
     */
    public static boolean isNumber(String s){
        Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
        return pattern.matcher(s).matches();
    }

    /**
     * 判断是不是运算符
     * @param s
     * @return
     */
    public static boolean isSymbol(String s){
        return s.matches(SYMBOL);
    }

    /**
     * 匹配运算等级
     * @param s
     * @return
     */
    public static int calcLevel(String s){
        if("+".equals(s) || "-".equals(s)){
            return LEVEL_01;
        } else if("*".equals(s) || "/".equals(s)){
            return LEVEL_02;
        }
        return LEVEL_HIGH;
    }

    /**
     * 匹配
     * @param s
     * @throws Exception
     */
    public static List<String> doMatch (String s) throws Exception{
        if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
        if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");

        s = replaceAllBlank(s);

        String each;
        int start = 0;

        for (int i = 0; i < s.length(); i++) {
            if(isSymbol(s.charAt(i)+"")){
                each = s.charAt(i)+"";
                //栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
                if(stack.isEmpty() || LEFT.equals(each)
                        || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
                    stack.push(each);
                }else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
                    //栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
                    while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
                        if(calcLevel(stack.peek()) == LEVEL_HIGH){
                            break;
                        }
                        data.add(stack.pop());
                    }
                    stack.push(each);
                }else if(RIGHT.equals(each)){
                    // ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
                    while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
                        if(LEVEL_HIGH == calcLevel(stack.peek())){
                            stack.pop();
                            break;
                        }
                        data.add(stack.pop());
                    }
                }
                start = i ;    //前一个运算符的位置
            }else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
                each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
                if(isNumber(each)) {
                    data.add(each);
                    continue;
                }
                throw new RuntimeException("data not match number");
            }
        }
        //如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
        Collections.reverse(stack);
        data.addAll(new ArrayList<>(stack));

        System.out.println(data);
        return data;
    }

    /**
     * 算出结果
     * @param list
     * @return
     */
    public static Double doCalc(List<String> list){
        Double d = 0d;
        if(list == null || list.isEmpty()){
            return null;
        }
        if (list.size() == 1){
            System.out.println(list);
            d = Double.valueOf(list.get(0));
            return d;
        }
        ArrayList<String> list1 = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            list1.add(list.get(i));
            if(isSymbol(list.get(i))){
                Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
                list1.remove(i);
                list1.remove(i-1);
                list1.set(i-2,d1+"");
                list1.addAll(list.subList(i+1,list.size()));
                break;
            }
        }
        doCalc(list1);
        return d;
    }

    /**
     * 运算
     * @param s1
     * @param s2
     * @param symbol
     * @return
     */
    public static Double doTheMath(String s1,String s2,String symbol){
        Double result ;
        switch (symbol){
            case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
            case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
            case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
            case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
            default : result = null;
        }
        return result;

    }

    public static void main(String[] args) {
        //String math = "9+(3-1)*3+10/2";
        String math = "12.8 + (2 - 3.55)*4+10/5.0";
        try {
            doCalc(doMatch(math));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

.pop());
}
}
start = i ; //前一个运算符的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException(“data not match number”);
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));

    System.out.println(data);
    return data;
}

/**
 * 算出结果
 * @param list
 * @return
 */
public static Double doCalc(List<String> list){
    Double d = 0d;
    if(list == null || list.isEmpty()){
        return null;
    }
    if (list.size() == 1){
        System.out.println(list);
        d = Double.valueOf(list.get(0));
        return d;
    }
    ArrayList<String> list1 = new ArrayList<>();
    for (int i = 0; i < list.size(); i++) {
        list1.add(list.get(i));
        if(isSymbol(list.get(i))){
            Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
            list1.remove(i);
            list1.remove(i-1);
            list1.set(i-2,d1+"");
            list1.addAll(list.subList(i+1,list.size()));
            break;
        }
    }
    doCalc(list1);
    return d;
}

/**
 * 运算
 * @param s1
 * @param s2
 * @param symbol
 * @return
 */
public static Double doTheMath(String s1,String s2,String symbol){
    Double result ;
    switch (symbol){
        case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
        case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
        case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
        case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
        default : result = null;
    }
    return result;

}

public static void main(String[] args) {
    //String math = "9+(3-1)*3+10/2";
    String math = "12.8 + (2 - 3.55)*4+10/5.0";
    try {
        doCalc(doMatch(math));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值