【数据结构与算法】——一文带你理清线性表的基础(java代码演示)

前言
  🎄:CSDN的小伙伴们大家好,今天跟大家分享一个数据结构基础的版块——线性表。如果这篇文章对你有用,麻烦给我点个小赞以示鼓励吧🎄
  🏡:博客主页:空山新雨后的java知识图书馆
  ☔️:晚上下雨了,天气转凉。
  📝: 智者创造机会,强者把握机会,弱者等待机会。📝
  📖上一篇文章:spring框架学习第二部分📖
  👏欢迎大家一起学习,进步。加油👊



一、线性表的定义

1、定义

  • 线性表是具有相同特性的数据元素的一个有限序列

  • 其中数据元素的个数n定义为表的长度

  • 当n=0时称为空表

  • 将非空的线性表(n>0)记作:(a1,a2…a(n))

  • 这里的 数据元素a(i)(1<=i<=n)只是一个抽象的符号,其具体含义在不同的情况下可以不同

案例:
在这里插入图片描述

同一线性表中的元素必定具有相同的特性,数据元素之间的关系是线性关系。

2、线性表的逻辑特征

  • 在非空的线性表,有且仅有一个开始结点a1,他没有直接前趋,而仅有一个直接后继a2.
  • 有且仅有一个终端结点an,他没有直接后继,而仅有一个直接前趋a(n-1)。
  • 其余的内部结点ai(2<=i<=n-1)都有且仅有一个直接前趋a(i-1)和一个直接后继a(i+1)。
  • 线性表是一种典型的线性结构。

3、线性表的存储结构

​ 分为顺序存储结构,和链式存储结构

3.1 、线性表的顺序表示又称为顺序存储结构或顺序映像

​   顺序存储的定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构,简言之,逻辑上相邻,物理上也相邻

线性表的第一个元素a1所在的存储位置,称为线性表的起始位置基地址。

   存储地址不连续不能是顺序存储结构。

  重大优点:只要知道某一个元素的存储位置就可以计算其他元素的存储位置

例如:如果每一个元素占用8个存储单位,ai 存储位置是2000单元,则ai+1 的存储位置是(2008单元)

  假设线性表的每个元素需占用L个存储单元,则第i+1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系:

LOC(ai+1) = LOC(ai) + L

顺序表的特点总结

  1. ​ 以物理位置相邻表示逻辑关系

  2. ​ 任意元素均可随机存取

3.2、顺序表的实现

​ 代码实现:

package com.studySelf.Linearlist02.Sqlist;

import java.util.Iterator;

/**
 * @author wang
 * @packageName com.studySelf.Linearlist02.Sqlist
 * @className List
 * @date 2021/11/29 19:45
 */
public class Seqlist<T> extends Object implements Iterable<T>{
    /**存储元素的数组*/
    private Object[] element;
    /**记录当前顺序表中元素个数*/
    private int n;
    
    //构造方法
    public Seqlist(int capacity) {
        //初始化数组
       this.element = new Object[capacity];
       //初始化长度
       this.n = 0;
    }

    /**将一个线性表置为空表*/
    public void clear() {
        this.n = 0;
    }

    /**判断当前线性表是否为空表*/
    public boolean isEmpty() {
        return n==0;
    }
    /**获取线性表的长度*/
    public int length() {

        return n;
    }
    /**获取指定位置的元素*/
    public T get(int i) {
        return (T) element[i];
    }
    /**获取所有的元素*/
    public void getAll() {
        for(int i = 0;i<n;i++) {
            System.out.println(element[i]);
        }
    }

    /**向线性表中添加元素*/
    public void insert(T t) {
        //如果数组已经到达上限,即扩容
        if(n==element.length) {
            //扩容为原数组的两倍
            resize(2*element.length);
        }
        element[n++] = t;
    }

    /**在指定i索引处插入元素*/
    public void insert(int i,T t) {
        //如果数组已经到达上限,即扩容
        if(n==element.length) {
            //扩容为原数组的两倍
            resize(2*element.length);
        }
        //把索引i处的元素及其后面的元素依次向后移动一位
        for(int index = n;index>i;index--) {
            element[index] = element[index-1];
        }
        element[i] = t;
        //元素加一
        n++;
    }

    /**删除指定的位置i处的元素,并返回该元素*/
    public T remove(int i) {
        //记录索引i处的值
        T current = (T) element[i];

        //索引i后面元素依次向后移动一位即可
        for(int index = i;index < n-1 ;index++) {
            element[index] = element[index+1];
        }
        //元素个数-1
        n--;
        if(n>0 && n<element.length/4) {
            resize(element.length/2);
        }
        return current;
    }

    /**改变容量的方法*/
    public void resize(int newSize) {
        //记录原数组
        T[] temp = (T[]) element;
        //创建新数组
        element = new Object[newSize];
        //将旧数组的内容传到新数组
        for(int i = 0;i<n;i++) {
            element[i] = temp[i];
        }
    }

    /**查找t元素第一次出现的位置*/
    public int indexOf(T t) {
        for(int i =0;i<n;i++) {
            if(element[i].equals(t)) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public Iterator<T> iterator() {
        return new LIterator<T>();
    }
    private class LIterator<T> implements Iterator<T> {

        private int cur ;
        public LIterator() {
            //从0开始遍历
            this.cur = 0;
        }
        @Override
        //该方法表示是否含有下一个元素
        public boolean hasNext() {
            //当当前元素索引小于元素个数即索引等于或大于n时,证明顺序表中没有元素,返回false
            return cur<n;
        }

        @Override
        //下一个元素就是数组中下一个元素
        public T next() {
            return (T)element[cur++];
        }
    }
}


顺序表要注意容量可变问题

3.3、顺序表的时间复杂度

  get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);

  insert(int i,T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);

  remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);

  由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显

3.4、顺序表总结

顺序表的特点

​   1.利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致

​   2.在访问线性表时,可以快速的计算出任何一个元素的存储地址。因此可以粗略的认为,访问每个元素所花的时间相等

  这种存取元素的方法称为随机存取法

顺序表的优缺点

优点

​   1.存储密度大,(结点本身所占存储量/结点结构所占存储量)

​   2.可以随机存取表中任意元素

缺点

​   在插入、删除某一元素时,需要移动大量元素

​   浪费存储空间

​   属于静态存储形式、数据元素个数不能自由扩充

java中ArrayList实现

  java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。

4、线性表的链式表示和实现

​   定义:结点在存储器上的位置是任意的,即逻辑上相邻的元素在物理上不一定相邻;线性表的链式表示又称为非顺序映像链式映像

  实现:用一组物理位置任意的存储单元来存放线性表的数据元素,这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的,链表中的元素的逻辑次序和物理次序不一定相同。

  单链表可以由头指针唯一确定,因此单链表可以用头指针的名字来命名

4.1、与链式存储有关的术语:

  • 结点:数据元素的存储映像,由数据域和指针域两部分组成

  • 链表:n个结点由指针链组成一个链表,他是线性表的链式存储映像,称为线性表的链式存储结构

  • 单链表:结点只有一个指针域的链表,称为单链表或线性链表

  • 双链表:结点由两个指针域的链表,称为双链表

  • 循环链表:首尾相接的链表称为循环链表

  • 头指针:是指向链表中的第一个结点的指针

  • 首元结点:是指链表中存储的第一个元素a1的结点

  • 头结点:是指链表在首元结点之前附设的一个结点

如何表示空表:

​   1.无头结点时,头指针为空的表示空表

​   2.有头结点时,当头结点的指针域为空时,为空表

在链表设置头结点有什么好处?

1.便于首元结点的处理

​   首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无需做特殊处理

2.便于空表和非空表的统一处理

​   无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。

头结点的数据域中存储什么?

​   头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此结点不能计入链表长度值。

链表的存储结构的特点

​   1.结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在

物理上不一定相邻。


  2.访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等

4.2、单向链表

​   定义:单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。

单向链表代码实现:

package com.SequenceList.LinkListTest;

import java.util.Iterator;

/**
 * @author wang
 * @packageName com.SequenceList
 * @className LinkList
 * @date 2021/12/2 19:33
 * 单项链表API设计类
 */
public class LinkList<T> implements Iterable<T>{
    /**记录头结点*/
    private Node head;
    /**记录链表长度*/
    private int N;
    /**初始化头结点*/
    public LinkList(){
        //头结点为空,无指向
        head = new Node(null,null);
        N = 0;
    }
    /**清空链表*/
    public void clear() {
        //当头结点的指向为null和头结点的数据域也为空,数据的长度为0
        head.next = null;
        head.item = null;
        N =0;
    }
    /**获取链表的长度*/
    public int length() {
        //n就是链表的长度
        return N;
    }
    /**判断链表是否为空*/
    public boolean isEmpty() {
        return N == 0;
    }
     /**获取指定位置处i的元素*/
     public T get(int i) {
         //这里要注意i值超出范围即<0或>n
         if(i<0 || i>N) {
             throw new RuntimeException("您的索引有误");
         }

         //从头结点开始遍历
         //当遍历到指定元素i位置是结束,即是找到元素
        //定义n为头结点的下一个元素,即首元结点
         Node n = head.next;
         for(int index = 0;index < i;index++) {
             //每循环一次,就让n的数据为下一个结点,直到找到我们
             //需要的第i个元素位置
             n = n.next;
         }
         //找到之后返回该元素
         return (T) n.item;
     }
    /**向链表中添加元素t*/
    public void insert(T t) {
        //首先找到最后一个结点
        Node n = head;
        //当n.next等于null时,即代表我们找到了最后一个元素
        while(n.next != null) {
            //找到最后一个元素,将n指向最后一个元素
            n = n.next;
        }
        //创建一个新结点,新节点就是我们要插入的最后一个结点
        Node newNode = new Node(t,null);
        //当我们插入最后一个结点之后,那么原来的结点n就是倒数第二个节点,那么他就应该指向新结点
        n.next = newNode;
        //同时链表的长度不要忘了加一
        N++;
    }
    /**向指定位置i处,添加元素t*/
    public void insert(int i,T t) {
        //此处同样要注意索引问题
        if(i<0 || i>N) {
            throw new RuntimeException("您的索引有误");
        }
        //首先应该找到i位置之前的元素
        Node before = head;
        for(int index = 0;index <= i-1;index++) {
            before =before.next;
        }
        //那么找到位置i之前一个处的结点之后,也就找到了i位置的结点
        Node current = before.next;
        //此时构建一个新的结点,那么新节点的指向就是current位置的元素
        //新节点的被指向元素就应该是before位置元素
        Node newNode = new Node(t,current);
        before.next = newNode;
        //不要忘了长度加一哦
        N++;
    }
    /**删除指定位置i处的元素,并返回被删除的元素*/
    public T remove(int i) {
        //老规矩,索引问题
        if(i<0 || i>N) {
            throw new RuntimeException("您的索引有误");
        }
        //删除结点思路:
        //先找到当前位置之前一位结点
        Node before = head;
        for(int index = 0;index <=i-1;index++) {
            before = before.next;
        }

        //找到当前位置结点
        Node current = before.next;

        //前一个结点的指向为当前结点的下一个结点
        before.next = current.next;
        N--;
        return (T)current.item;
    }

    /**查找元素第一次在链表中出现的位置*/
    public int indexOf(T t) {
        Node n = head;
        for(int i = 0;n.next != null;i++)  {
            n = n.next;
            if(n.item.equals(t)){
                return i;
            }
        }
        return -1;
    }

    @Override
    public Iterator<T> iterator() {
        return new LIterator();
    }

    private class LIterator implements Iterator<T>{

        private Node n;
        public LIterator() {
            //让初始化结点位置在头结点
            n = head;
        }


        @Override
        public boolean hasNext() {
            //如果n结点指向下一个结点为null则表示链表结束
            return n.next!=null;
        }

        @Override
        public T next() {

            //当n结点初始化为头结点时,那么第一个结点应该就是下一个结点,以此类推
            n = n.next;
            //这里返回n结点的值即可
            return (T) n.item;
        }
    }

}

/**结点类*/
class Node<T>{
    //存储数据
    public T item;
    //指向下一个结点
    public Node next;


    public Node(T item,Node next){
        this.item = item;
        this.next = next;
    }

}

4.3、双向链表

  双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
代码实现:

package com.SequenceList.DoubleLinkedList;

import java.util.Iterator;

/**
 * @author wang
 * @packageName com.SequenceList.DoubleLinkedList
 * @className DoubleLinkedList
 * @date 2021/12/7 20:15
 * 双向链表类
 */
public class DoubleLinkedList<T> implements Iterable<T>{
    /**记录首结点*/
    private Node head;
    /**记录尾结点*/
    private Node last;
    /**记录元素个数*/
    private int N;
    /**初始化链表*/
    public  DoubleLinkedList() {
        //尾结点为null
        last=null;
        //头结点初始化为null
        head = new Node(null,null,null);
        //元素个数为0
        N = 0;
    }



    /**内部类Node类*/
    public class Node<T>{
        /**成员变量,存储数据*/
        public T item;
        /**下一个结点*/
        public  Node pre;
        /**上一个结点*/
        public Node next;

        /**构造方法*/
        public Node(T item, Node pre, Node next) {
            this.item = item;
            this.pre = pre;
            this.next = next;
        }
    }

    /**清空链表*/
    public void clear() {
        //链表尾结点为空,头结点所指也为空,个数为0
        last = null;
        head.next = last;
        head.pre =null;
        head.item =null;
        N =0;
    }

    /**获取链表的长度*/
    public int length() {
        //链表的长度就是元素的个数
        return N;
    }

    /**判断链表是否为空*/
    public boolean isEmpty() {
        return N==0;
    }
    /**插入元素t*/

    public void insert(T t) {
        //如果链表为空的情况
        if(isEmpty()) {
            //将尾结点的数据就是我们要插入的值,他的上一个结点就是头结点
            Node newNode = new Node(t,head,null);
            last = newNode;
            //那么头结点就该指向尾结点
            head.next = last;
        }else{
            //链表不为空的情况
            //存储一下旧的尾结点
            Node oldNode = last;
            Node node = new Node(t, oldNode, null);
            //旧的尾结点就应该指向新的尾结点
            oldNode.next = node;
            //尾结点重新为新结点
            last = node;
        }
        //链表中的元素的个数加一
        N++;
    }

    /**向指定位置i处插入元素t*/
    public void insert(int i,T t) {
        //防止索引越界的问题
            if(i<0 || i>=N) {
                throw new RuntimeException("您插入的位置有误");
            }
        //首先找到i位置的前一个元素
        Node pre = head;
        //从head开始
        for(int index = 0;index < i ;index++) {
            pre = pre.next;
        }
        //当找到第i个元素的前一个值时,pre 就是i位置的前一个元素
        //i位置的元素为前一个结点的下一个结点
        Node currentNode = pre.next;

        //构建新结点,其值为t,向前指向当前元素的前一个元素,后指向为当前元素
        Node newNode = new Node(t, pre, currentNode);
        //此时i位置的元素的前指向就应该是插入的这个元素
        currentNode.pre = newNode;
        //前一个元素的后指向就是该元素
        pre.next = newNode;
        N++;

    }
    /**获取指定位置i处的元素*/
    public T get(int i){
        //防止索引越界的问题
        if(i<0 || i>=N) {
            throw new RuntimeException("您插入的位置有误");
        }
        //首先将该结点设置为链表中第一个元素,不是头结点
        Node curr = head.next;
        //从head开始
        for(int index = 0;index < i ;index++) {
            //那么此时到i前一个元素停止时,该元素的下一个值就已经找到
            curr = curr.next;
        }
        return (T)curr.item;
    }
    /**获取指定元素在链表中第一次出现的位置*/
    public int indexOf(T t) {
        //设置一个头结点
        Node node = head;
        //当元素的后趋为null时,说明链表遍历完了,如果遍历完了还没找到,那么久说明没有元素t
        for(int i =0; node.next!= null;i++) {
            //如果找到了,就比对返回i的值
            node = node.next;
            if(node.item.equals(t)) {
                return i;
            }
        }
        return -1;
    }
    /**删除i处的元素,并返回该元素*/
    public T remove(int i) {
        //防止索引越界的问题
        if(i<0 || i>=N) {
            throw new RuntimeException("您删除的位置有误或者链表中不存在元素");
        }

        //寻找I位置的前一个元素
        Node pre = head;
        for(int index = 0;index < i;index++) {
            pre = pre.next;
        }
        //i位置的元素
        Node iNode = pre.next;
        //i位置的下一个元素
        Node iNextNode = iNode.next;

        //将i位置的前一个元素的下一个指向i位置的下一个元素
        pre.next = iNextNode;
        //将i位置的后一个元素的前指向为i位置的前一个元素
        iNextNode.pre = pre;
        //长度减一
        N--;
        return (T)iNode.item;
    }

    /**获取第一个元素*/
    public T getFirstNode() {
        if(isEmpty()) {
            return null;
        }
        return (T)head.next.item;
    }

    /**获取最后一个元素*/
    public T getLastNode() {
        if(isEmpty()) {
            return null;
        }
        return (T)last.item;
    }

    @Override
    public Iterator<T> iterator() {
        return new DIterator();
    }

    private class DIterator implements Iterator{

        private Node n ;

        public DIterator() {
            this.n = head;
        }
        @Override
        public boolean hasNext() {
            return n.next!= null;
        }

        @Override
        public Object next() {
            n = n.next;
            return n.item;
        }
    }
}

4.4、链表的复杂度分析

  get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n)

  insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);

  remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n)

  相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换。

  相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表增删操作比较多,建议使用链表

4.5、几个链表常见面试题

4.5.1、单链表的反转问题

单链表的反转,是面试中的一个高频题目。

需求:

原链表中数据为:1->2->3>4

反转后链表中数据为:4->3->2->1

反转API

public void reverse():对整个链表反转 

public Node reverse(Node curr):反转链表中的某个结点curr,并把反转后的curr结点返回

使用递归可以完成反转,递归反转其实就是从原链表的第一个存数据的结点开始,依次递归调用反转每一个结点,

直到把最后一个结点反转完毕,整个链表就反转完毕。

在这里插入图片描述

代码:

    /**单链表的反转*/
    public void reverse() {
        //当链表为空的时候,不需要反转
        if(N == 0) {
            return;
        }
        //如果链表不为空, 就递归调用重载方法
        reverse(head.next);
    }

    /**单链表反转的重载方法,
     * @param curr 当前遍历的结点
     * @return 反转后的当前结点的上一个结点
     * */

    public Node reverse(Node curr){
        //当到达最后一个元素的时候,
        if(curr.next == null) {
            //反转后,头结点应该指向原链表中的最后一个元素
            head.next = curr;
            return curr;
        }
        //当链表未达到最后一个元素的时候
        //递归的反转当前结点curr的下一个结点,返回值就是当前结点的上一个结点
        Node pre = reverse(curr.next);

        //把返回的结点的下一个结点变为当前结点
        pre.next = curr;
        //当前结点的下一个结点设为null
        curr.next = null;
        //返回当前结点
        return curr;
    }
4.5.2、快慢指针问题

​   定义:快慢指针指的是定义两个指针,这两个指针的移动速度一快一慢,以此来制造出自己想要的差值,这个差值可以让我们找到链表上相应的结点一般情况下,快指针的移动步长为慢指针的两倍

4.5.2.1、快慢指针之解决中间值

需求

​  请完善测试类Test中的getMid方法,可以找出链表的中间元素值并返回。

  利用快慢指针,我们把一个链表看成一个跑道,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以此来达到找到中间节点的目的。

  如下图,最开始,slow与fast指针都指向链表第一个节点,然后slow每次移动一个指针,fast每次移动两个指针。

在这里插入图片描述

那么最后dd,就是我们想要的值

package com.SequenceList.getMidValue;

/**
 * @author wang
 * @packageName com.SequenceList.getMidValue
 * @className Test
 * @date 2021/12/13 19:49
 */
public class Test {
    public static void main(String[] args) {
        Node<String> first = new Node<String>("aa", null);
        Node<String> second = new Node<String>("bb", null);
        Node<String> third = new Node<String>("cc", null);
        Node<String> fourth = new Node<String>("dd", null);
        Node<String> fifth = new Node<String>("ee", null);
        Node<String> six = new Node<String>("ff", null);
        Node<String> seven = new Node<String>("gg", null);

        //结点之间的指向
        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six; 
        six.next = seven;

        System.out.println(getMid(first));

    }

    /**查找中间值*/
    public static String getMid(Node<String> first) {
        //令快慢指针都从链表中第一个位置开始。
        Node<String> slow = first;
        Node<String> fast = first;
        //当fast指针到达链表尾时,(即为null)结束循环
        while(fast != null && fast.next != null) {
            //快指针,每一次移动两个元素
            fast = fast.next.next;
            //慢指针,每一次移动一个元素
            slow = slow.next;
        }
        //最后返回slow位置的值就是我们想要的值,记住返回item值,不是slow结点
        return slow.item;
    }


    //结点类
    private static class Node<T> {
        T item;
        Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

4.5.2.2、单链表是否有环问题

在这里插入图片描述

需求:

  请完善测试类Test中的isCircle方法,返回链表中是否有环。

  使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环。

过程:
在这里插入图片描述
在这里插入图片描述

代码:

package com.SequenceList.CircleTest;



/**
 * @author wang
 * @packageName com.SequenceList.CircleTest
 * @className IsOrNotCircle
 * @date 2021/12/13 20:15
 * 单链表是否有环问题
 */
public class IsOrNotCircle {
    public static void main(String[] args) {
        Node<String> first = new Node<String>("aa", null);
        Node<String> second = new Node<String>("bb", null);
        Node<String> third = new Node<String>("cc", null);
        Node<String> fourth = new Node<String>("dd", null);
        Node<String> fifth = new Node<String>("ee", null);
        Node<String> six = new Node<String>("ff", null);
        Node<String> seven = new Node<String>("gg", null);

        //结点之间的指向
        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;

        //制造环
        seven.next = third;

        System.out.println(isCircle(first));
    }


    /**求是否链表有环方法
     * 当链表中快指针与满指针指向同一个结点的时候,则代表链表有环,而没有指向
     * 同一个结点则代表无欢,因为无环二指针不可能指向相同结点*/
    public static boolean isCircle(Node first) {
        //定义快满指针
        Node<String> fast = first;
        Node<String> slow = first;
        //同样遍历循环,出循环就是fast为null,那么出了循环就说明该链表无环,返回false
        while(fast!= null && fast.next!= null) {
            //快慢指针正常走
            fast = fast.next.next;
            slow =slow.next;
            //如果两个指针相遇,说明有环,返回true
            if(fast.equals(slow)) {
                return true;
            }
        }
        return false;
    }
    private static class Node<T>{
        T item;

        Node next;

        private Node(T item,Node next) {
            this.item =item;
            this.next = next;
        }
    }
}


4.5.2.3、有环链表入口问题

需求:

  请完善Test类中的getEntrance方法,查找有环链表中环的入口结点。

  当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样为1,则慢指针与“新”指针相遇的地方就是环的入口。证明这一结论牵涉到数论的知识,这里略,只讲实现。

在这里插入图片描述

代码:

package com.SequenceList.CircleTest;

/**
 * @author wang
 * @packageName com.SequenceList.CircleTest
 * @className FindCircleEntryTest
 * @date 2021/12/14 19:18
 * 有环链表入口问题
 */
public class FindCircleEntryTest {
    public static void main(String []args) {
        Node<String> first = new Node<String>("aa", null);
        Node<String> second = new Node<String>("bb", null);
        Node<String> third = new Node<String>("cc", null);
        Node<String> fourth = new Node<String>("dd", null);
        Node<String> fifth = new Node<String>("ee", null);
        Node<String> six = new Node<String>("ff", null);
        Node<String> seven = new Node<String>("gg", null);

        //结点之间的指向
        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;

        //制造环
        seven.next = fourth;


        System.out.println(getEntrance(first).item);

    }


    private static Node getEntrance(Node<String> first) {
        //定义快满指针,以及临时指针
        Node<String> fast = first;
        Node<String> slow = first;
        Node<String> temp = null;

        while(fast != null && fast.next!=null) {
            fast = fast.next.next;
            slow = slow.next;

            //当找到快慢指针相遇之时,将临时指针赋值为first结点
            if(fast.equals(slow)) {
                temp = first;
                continue;
            }
            //判断如果temp指针不为空,那么说明已经成功赋值,快慢指针已经相遇
            if(temp != null){
                //让temp指针的移动速度与慢指针保持一致
                temp = temp.next;

                //当慢指针与临时指针相遇之时,这个临时指针所指就是我们要找的环的入口
                if(temp.equals(slow)) {
                    break;
                }
            }

        }
        return temp;
    }
    private static class Node<T> {
        T item;
        Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

5、循环链表

5.1、循环链表的定义

​   循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。
在这里插入图片描述

循环链表的构建

public class Test {
	public static void main(String[] args) throws Exception { 
		//构建结点 
    Node<Integer> first = new Node<Integer>(1, null); 		Node<Integer> second = new Node<Integer>(2, null); 		Node<Integer> third = new Node<Integer>(3, null); 		Node<Integer> fourth = new Node<Integer>(4, null); 		Node<Integer> fifth = new Node<Integer>(5, null); 		Node<Integer> six = new Node<Integer>(6, null); 		Node<Integer> seven = new Node<Integer>(7, null);
	//构建单链表
    first.next = second; 
    second.next = third;
    third.next = fourth;
    fourth.next = fifth;
    fifth.next = six; 
    six.next = seven; 
    //构建循环链表,让最后一个结点指向第一个结点
    seven.next = first;
	}
}

5.2、约瑟夫(Joseph)问题

问题:

  传说有这样一个故事,在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,从而逃过了这场死亡游戏 。

问题转换:

41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。

  • 1.编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;

  • 2.自退出那个人开始的下一个人再次从1开始报数,以此类推;

  • 3.求出最后退出的那个人的编号。

图示:
在这里插入图片描述

解题思路:

​ 1.构建含有41个结点的单向循环链表,分别存储1~41的值,分别代表这41个人;

​ 2.使用计数器count,记录当前报数的值;

​ 3.遍历链表,每循环一次,count++;

​ 4.判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0;

代码:

package com.SequenceList.joseph;

/**
 * @author wang
 * @packageName com.SequenceList.joseph
 * @className JosephTest
 * @date 2021/12/14 20:07
 * 约瑟夫问题
 */
public class JosephTest {

    /**
     * @param args
     * @Date 2021/12/15 19:29
     * @Param
     * @Return void
     * @MetodName main
     * @Author wang
     * @Description 约瑟夫问题测试
     */
    public static void main(String[] args) {
        //构建一个循环链表
        //记录首结点
        Node<Integer> first = null;
        //记录前一个结点
        Node<Integer> pre = null;
        int full = 41;
        for (int i = 1; i <= full; i++) {
            //第一个元素,我们让首结点为新结点
            //值为1,让前一个结点成为首结点
            if (i == 1) {
                first = new Node(i, null);
                pre = first;
                //然后跳出if继续循环
                continue;
            }
            //如果不是第一个元素
            Node<Integer> node = new Node<>(i, null);
            //pre的下一个结点 就是node
            pre.next = node;
            //让pre一直向后移动
            pre = node;

            //如果是最后一个元素
            if (i == 41) {
                //让最后一个结点指向头一个结点,完成循环
                pre.next = first;
            }
        }

        //使用count 记录报数的值
        int count = 0;
        //遍历链表,每循环一次,count++
        Node<Integer> n = first;
        Node<Integer> before = null;
        while (n != n.next) {
            count++;
            if (count == 3) {
                //判断count的值,如果等于3,打印该值并删除 ,把count重置为0
                before.next = n.next;
                System.out.print(n.item + ",");
                count = 0;
                n = n.next;
            } else {
                //如果不等于三,就往移指针
                before = n;
                n = n.next;
            }
        }
        //打印最后一个人
        System.out.println(n.item);
    }

    /**
     * 结点类
     */
    private static class Node<T> {
        T item;
        Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}
/*
 * 3,6,9,12,15,18,21,24,27,30,33,36,39,1,5,10,14,19,23,28,32,37,41,7,13,20,26,34,40,8,17,29,38,11,25,2,22,4,35,16,31
 *
 * */

二、栈和队列

2.1、栈的定义及运算:

​   定义:栈是限定在表的一端进行插入和删除运算的线性表,通常将插入、删除的一端称之为(表尾an)栈顶(top),另一端(表头a1)称之为栈底(bottom),不含元素的栈称之为空栈。

​   其逻辑结构为一对一关系

​   其存储结构用顺序栈或链式栈皆可

运算规则:

​   插入元素到栈顶(即表尾)的操作,称为入栈(压栈 = push(x))

​   从栈顶(表尾)删除最后一个元素的操作,称为出栈(弹栈 = pop(y))

在这里插入图片描述

​   其中栈的插入和删除只能在表尾进行,也就是后进先出(Last in First Out ),简称栈为LIFO表。

特点

​ - 1.栈和后面的队列的插入和删除都是只能在表的“端点”进行的线性表

​ - 2.栈的应用一般在问题求解的过程中有“后进先出”的特性,例如(数制转换,括号匹配的检验,行编辑程序,迷宫求解,表达式求值,八皇后问题,函数调用,递归调用的实现)

2.1.1、栈的实现类(代码):

package com.studySelf.Linearlist02.stack;

import java.util.Iterator;

/**
 * @author wang
 * @version 1.0
 * @packageName com.studySelf.Linearlist02.stack
 * @className Stack
 * @date 2021/12/20 19:32
 * @Description 栈使用链式存储实现类
 */
public class Stack<T> implements Iterable<T> {
    //记录首结点
    private Node<T> head;
    //栈中元素个数
    private int N;

    /**
     * @Date 2021/12/20 19:40
     * @Param
     * @Return void
     * @MetodName Stack
     * @Author wang
     * @Description 初始化栈
     */
    public Stack() {
        //初始化栈中结点,首结点无数据且不指向任何结点
        //元素个数为0
        this.head = new Node(null, null);
        this.N = 0;
    }

    /**
     * @Date 2021/12/20 19:40
     * @Param
     * @Return boolean
     * @MetodName isEmpty
     * @Author wang
     * @Description 判断栈是否为空
     */
    public boolean isEmpty() {
        return N == 0;
    }

    /**
     * @param t
     * @Date 2021/12/20 19:40
     * @Param
     * @Return void
     * @MetodName push
     * @Author wang
     * @Description 压栈,将元素插入栈中
     * 该方法,首先栈插入元素应当是插在首结点之后,因为他要遵循先进后出的原则,所以只需要让
     * 头结点指向新的结点,让新的结点指向旧的结点即可,完成插入操作
     */
    public void push(T t) {


        //找到首结点的下一个结点
        Node oldFirst = head.next;

        //创建一个新节点,内容为t,让新节点指向旧结点
        Node newNode = new Node(t, null);

        //让首结点指向新节点
        head.next = newNode;

        newNode.next = oldFirst;
        //元素个数自增
        N++;

    }

    /**
     * @Date 2021/12/20 19:45
     * @Param
     * @Return T
     * @MetodName pop
     * @Author wang
     * @Description 弹栈,取出数据的操作
     * 因为栈的先进后出的 原则,所以,每次弹出的数据总是栈中位于首结点之后的数据
     */
    public T pop() {
        //找到首结点的下一个元素
        Node oldNode = head.next;
        //这里注意结点可能为null值的情况,为null直接返回即可
        if (oldNode == null) {
            return null;
        }
        //将首结点的下一个元素指向第一个元素的下一个元素
        head.next = oldNode.next;
        //元素--
        N--;
        return (T) oldNode.item;
    }


    /**
     * @Date 2021/12/20 19:50
     * @Param
     * @Return int
     * @MetodName getNumInStack
     * @Author wang
     * @Description 获取栈中元素个数
     */
    public int getNumInStack() {
        return N;
    }

    /**
     * @Date 2021/12/20 19:59
     * @Param
     * @Return Iterator<T>
     * @MetodName iterator
     * @Author wang
     * @Description
     */
    @Override
    public Iterator<T> iterator() {
        return new SIterator();
    }

    private class SIterator implements Iterator<T> {
        private Node n;

        private SIterator() {
            this.n = head;
        }

        @Override
        public boolean hasNext() {

            return n.next != null;
        }

        @Override
        public T next() {
            n = n.next;
            return (T) n.item;
        }
    }


    private class Node<T> {
        //结点值
        private T item;
        //下一个结点
        private Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

2.1.2、括号匹配问题

问题描述

​   给定一个字符串,里边可能包含"()"小括号和其他字符,请编写程序检查该字符串的中的小括号是否成对出现。

例如:

“(上海)(长安)”:正确匹配

“上海((长安))”:正确匹配

“上海(长安(北京)(深圳)南京)”:正确匹配

“上海(长安))”:错误匹配

“((上海)长安”:错误匹配

解决思路

1.创建一个栈用来存储左括号 
2.从左往右遍历字符串,拿到每一个字符 
3.判断该字符是不是左括号,如果是,放入栈中存储 
4.判断该字符是不是右括号,如果不是,继续下一次循环
5.如果该字符是右括号,则从栈中弹出一个元素t; 
6.判断元素t是否为null,如果不是,则证明有对应的左括号,如果不是,则证明没有对应的左括号 
7.循环结束后,判断栈中还有没有剩余的左括号,如果有,则不匹配,如果没有,则匹配

图示


在这里插入图片描述

代码实例

package com.studySelf.Linearlist02.stack;

/**
 * @author wang
 * @version 1.0
 * @packageName com.studySelf.Linearlist02.stack
 * @className BracketMatchProblem
 * @date 2021/12/20 21:37
 * @Description 括号匹配问题
 */


public class BracketMatchProblem {

    /**
     * @Date  2021/12/20 22:11
     * @Param 
     * @param args
     * @Return void
     * @MetodName main
     * @Author wang
     * @Description 主方法
     */
    public static void main(String[] args) {

        String s = "()00(11)";
        boolean check = check(s);
        System.out.println(check);
    }
    
    /**
     * @Date  2021/12/20 22:10
     * @Param 
     * @param str
     * @Return boolean
     * @MetodName check
     * @Author wang
     * @Description 括号匹配问题方法
     */
    public static boolean check(String str) {
        //创建一个栈用来存储左括号
        Stack<String> chars = new Stack<String>();
        //2.从左往右遍历字符串,拿到每一个字符
        for (int i = 0; i < str.length(); i++) {
            //3.判断该字符是不是左括号,如果是,放入栈中存储
            String currChar = str.charAt(i) + "";
            if (currChar.equals("(")) {
                chars.push(currChar);
                //4.判断该字符是不是右括号,如果不是,继续下一次循 环
            } else if (currChar.equals(")")) {
                //5.如果该字符是右括号,则从栈中弹出一个元素pop;
                String pop = chars.pop();
                //6.判断元素pop是否为null,如果不是,则证明有对应的左括号,如果不是,则证明没有对应的 左括号
                if (pop != null) {
                    return true;
                } else {
                    return false;
                }
            }
        }

        //7.循环结束后,判断栈中还有没有剩余的左括号,如果有,则不匹配,如果没有,则匹配
        if (chars.size() == 0) {
            return true;
        } else {
            return false;
        }
    }
}

2.1.3、逆波兰表达式

概念:

中缀表达式:

  中缀表达式就是我们平常生活中使用的表达式,例如:1+3*2,2-(1+3)等等,

  中缀表达式的特点是:二元运算符总是置于两个操作数中间。
  中缀表达式是人们最喜欢的表达式方式,因为简单,易懂。

  但是对于计算机来说就不是这样了,因为中缀表达式的运算顺序不具有规律性。不同的运算符具有不同的优先级,如果计算机执行中缀表达式,需要解析表达式语义,做大量的优先级相关操作

  表达式互换关系如下[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iL0CXhR0-1651219332115)(D:\桌面\计算机\数据结构\线性表\栈\逆波兰表达式.png)]

  逆波兰表达式:逆波兰表达式是波兰逻辑学家J・卢卡西维兹(J・ Lukasewicz)于1929年首先提出的一种表达式的表示方法,

  后缀表达式的特点:运算符总是放在跟它相关的操作数之后

案例需求

​   给定一个只包含加减乘除四种运算的逆波兰表达式的数组表示方式,求出该逆波兰表达式的结果。

思路:

1.创建一个栈对象oprands存储操作数 
2.从左往右遍历逆波兰表达式,得到每一个字符串
3.判断该字符串是不是运算符,如果不是,把该该操作数压入oprands栈中 
4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2 
5.使用该运算符计算o1和o2,得到结果result 
6.把该结果压入oprands栈中 
7.遍历结束后,拿出栈中最终的结果返回

图示:

在这里插入图片描述

代码实例:

package com.studySelf.Linearlist02.stack;

/**
 * @author wang
 * @version 1.0
 * @packageName com.studySelf.Linearlist02.stack
 * @className ReversePolishNotation
 * @date 2021/12/20 22:31
 * @Description 逆波兰表达式问题
 */

public class ReversePolishNotation {
    /**
     * @Date  2021/12/20 22:47
     * @Param
     * @param args
     * @Return void
     * @MetodName main
     * @Author wang
     * @Description 主方法
     */
    public static void main(String[] args) {
        //9*(12-3) - 6/2  :78
        String[] notation = {"9", "12", "3", "-", "*", "6", "2", "/", "-"};
        int calculate = calculate(notation);
        System.out.println(calculate);
    }

    /**
     * @param notation
     * @Date 2021/12/20 22:43
     * @Param
     * @Return int
     * @MetodName calculate
     * @Author wang
     * @Description 逆波兰表达式实现方法
     */
    public static int calculate(String[] notation) {
        //创建栈对象
        Stack<Integer> oprands = new Stack<>();
        //2.从左往右遍历逆波兰表达式,得到每一个字符串
        for (int i = 0; i < notation.length; i++) {

            String curr = notation[i];
            //3.判断该字符串是不是运算符,如果不是,把该该操作数压入oprands栈中
            Integer o1;
            Integer o2;
            Integer result;
            switch (curr) {
                case "+":
                    //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2
                    o1 = oprands.pop();
                    o2 = oprands.pop();
                    //5.使用该运算符计算o1和o2,得到结果result
                    /**
                     * 这里需要注意的是o1,o2两个变量的顺序不能调换,因为栈弹出顺序是后进先出,而我们运算数
                     * 先进的一般是运算符前面那个数,主要是对减法和除法有影响,因此,统一将两个变量顺序交换即可
                     */
                    result = o2 + o1;
                    //6.把该结果压入oprands栈中
                    oprands.push(result);
                    break;

                case "-":
                    //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2
                    o1 = oprands.pop();
                    o2 = oprands.pop();
                    //5.使用该运算符计算o1和o2,得到结果result
                    result = o2 - o1;
                    //6.把该结果压入oprands栈中
                    oprands.push(result);
                    break;
                case "*":
                    //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2
                    o1 = oprands.pop();
                    o2 = oprands.pop();
                    //5.使用该运算符计算o1和o2,得到结果result
                    result = o2 * o1;
                    //6.把该结果压入oprands栈中
                    oprands.push(result);
                    break;
                case "/":
                    //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2
                    o1 = oprands.pop();
                    o2 = oprands.pop();
                    //5.使用该运算符计算o1和o2,得到结果result
                    result = o2 / o1;
                    //6.把该结果压入oprands栈中
                    oprands.push(result);
                    break;
                default:
                    //如果不是操作符,就压入栈中
                    oprands.push(Integer.parseInt(curr));
                    break;
            }
        }
        //7.遍历结束后,拿出栈中最终的结果返回,此时栈中仅仅只有最后一个结果,所以返回即可
        Integer result = oprands.pop();
        return result;
    }
}

2.2、队列的定义和特点

​   定义:队列(queue) 是一种先进先出(First In First Out —— FIFO)的线性表,在表一端插入(表尾)在另一端(表头)删除。

​   其逻辑结构为一对一关系

​   其存储结构为顺序队,或链队

2.2.1,队列的代码演示

package com.studySelf.Linearlist02.queue;

import java.util.Iterator;

/**
 * @author wang
 * @version 1.0
 * @packageName com.studySelf.Linearlist02.queue
 * @className Queue
 * @date 2021/12/21 19:44
 * @Description 链式队列类
 */
public class Queue<T> implements Iterable<T> {
    //首结点
    private Node head;
    //尾结点
    private Node last;

    //元素个数
    private int N;

    //初始化化队列
    public Queue() {
        this.head = new Node(null, null);
        this.last = null;
        this.N = 0;
    }

    public boolean isEmpty() {
        return N == 0;
    }

    public int qNum() {
        return N;
    }

    /**
     * @param t
     * @Date 2021/12/21 20:16
     * @Param
     * @Return void
     * @MetodName enQueue
     * @Author wang
     * @Description 入队列
     */
    public void enQueue(T t) {
        //如果尾结点为空的情况
        if (last == null) {
            //插入新的结点,使尾结点称为新结点
            last = new Node(t, null);
            //首结点指向下一个结点,这里的下一个结点就是尾结点
            head.next = last;
        } else {
            //当尾结点有值时,给定一个变量存储旧的尾结点
            Node oldNode = last;
            //创建一个新节点,并且他就是新的尾结点
            last = new Node(t, null);
            //给旧的尾结点的后面就是新的尾结点
            oldNode.next = last;
        }
        N++;
    }

    /**
     * @Date 2021/12/21 20:16
     * @Param
     * @Return T
     * @MetodName deQueue
     * @Author wang
     * @Description 出队列
     */
    public T deQueue() {
        //如果队列为空,返回null
        if (isEmpty()) {
            return null;
        }
        //找到第一个元素
        Node oldFirst = head.next;
        //将首结点后面一个元素指向旧的第一个元素的下一个元素即可完成删除
        head.next = oldFirst.next;
        N--;
        //如果删除到最后为空了,给尾结点赋值为null
        if (isEmpty()) {
            last = null;
        }
        //返回被删除的值
        return (T) oldFirst.item;
    }

    @Override
    public Iterator<T> iterator() {
        return new QIterator();
    }

    private class QIterator implements Iterator<T> {

        private Node n;

        private QIterator() {
            this.n = head;
        }

        @Override
        public boolean hasNext() {
            return n.next != null;
        }

        @Override
        public T next() {
            n = n.next;
            return (T) n.item;
        }
    }

    private class Node<T> {
        private T item;
        private Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

测试方法

package com.studySelf.Linearlist02.Test;

import com.studySelf.Linearlist02.queue.Queue;

/**
 * @author wang
 * @version 1.0
 * @packageName com.studySelf.Linearlist02.Test
 * @className QueueTest
 * @date 2021/12/21 20:07
 * @Description 队列测试类
 */
public class QueueTest {
    public static void main(String[] args) {
        Queue<String> q = new Queue<String>();
        q.enQueue("a");
        q.enQueue("b");
        q.enQueue("c");
        q.enQueue("d");
        for (String s : q) {
            System.out.println(s);
        }

        System.out.println("-------------");
        System.out.println("队列是否为空" + q.isEmpty());
        System.out.println("队列中元素个数" + q.qNum());

        System.out.println("取出元素1:" + q.deQueue());
        System.out.println("取出元素2:" + q.deQueue());
        System.out.println("取出元素3:" + q.deQueue());
        System.out.println("取出元素4:" + q.deQueue());
        System.out.println("取出元素5:" + q.deQueue());

        System.out.println("剩余元素");
        for (String s1 : q) {
            System.out.println(s1);
        }


    }
}
/*
输出结果
a
b
c
d
-------------
队列是否为空false
队列中元素个数4
取出元素1:a
取出元素2:b
取出元素3:c
取出元素4:d
取出元素5:nul
 */

  关于该部分的数据结构知识总结到这里,感谢各位能在五一劳动节阅读,本文呢大量的知识来源都来源于网上,博主我经过学习之后,总结的一篇博客,我学习的是黑马的视频教程,各位可以直接上B站搜索即可学习,祝各位小伙伴能够工作顺利,五一节日快乐!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

空山新雨后~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值