LinkedList与链表(1万多字超级详细版本)

        一. ArrayList的缺陷

        上个博客已经熟悉了ArrayList的使用,并且进行了简单模拟实现。通过源码知道,ArrayList底层使用数组来存储元素:

public class ArrayList < E > extends AbstractList < E >
implements List < E > , RandomAccess , Cloneable , java . io . Serializable
{
// ...
// 默认容量是 10
private static final int DEFAULT_CAPACITY = 10 ;
//...
// 数组:用来存储元素
transient Object [] elementData ; // non-private to simplify nested class access
// 有效元素个数
private int size ;
public ArrayList ( int initialCapacity ) {
if ( initialCapacity > 0 ) {
this . elementData = new Object [ initialCapacity ];
} else if ( initialCapacity == 0 ) {
this . elementData = EMPTY_ELEMENTDATA ;
} else {
throw new IllegalArgumentException ( "Illegal Capacity: " +
initialCapacity );
}
}
// ...
        由于其底层是一段连续空间,当 ArrayList 任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后 搬移,时间复杂度为 O(n) ,效率比较低,因此 ArrayList 不适合做任意位置插入和删除比较多的场景 。因此: java 集合中又引入了LinkedList ,即链表结构。

二. 链表

2.1 链表的概念及结构

        链表是一种物理存储结构上非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接 次序实现的 。
        实际中链表的结构非常多样,以下情况组合起来就有8 种链表结构:
1. 单向或者双向

2. 带头或者不带头


3. 环或者非循环

        虽然有这么多的链表的结构,但是我们重点掌握两种:
        
        1.无头单向非循环链表 结构简单 ,一般不会单独用来存数据。实际中更多是作为 其他数据结构的子结构 ,如哈希桶、图的邻接表等等。另外这种结构在笔试面试 中出现很多。
        2.无头双向链表 :在 Java 的集合框架库中 LinkedList 底层实现就是无头双向循环链表。

2.2 链表的实现

        


2.2.1 先创建一个LinkedList链表

        

        一般的都是通过一个内部类来实现链表的创建的,显得更加的整齐。

        

        自己写一个接口来实现存放这些方法。

        

        这是实例化来创建了内容对象,通过next来实现链表的连接。head是头。

        

        就是通过如图形式连接的。


        2.2.2 display方法

        

        通过创建一个singerLinkedlist对象来指向head,也就是第一个元素,通过while语句让origal指针指向空值时结束。origal=origal.next,是用来移动指针的。

        结果:


2.2.3 addFirst方法

        

        通过cur创建一个singerLinkedlist对象,存入你输入的数据,把head的地址放在cur.next当中,如下图:

        

        然后再改变一下head指针,让它指向增加之后的第一个元素。


2.2.4 addLast方法

        

        先判断一下,指针指向的内容是否为null,如果为null说明链表中不存在元素,第一个也就是最后一个,所以直接创建一个对象,让head指针指向它们。

        如果不为空,先创建一个对象来存储你要增加的值,然后再创建一个指针也指向head指针指向的内容。通过while语句先指向最后一个位置,然后让最后一个位置的next原来为空现在把你增加的数据的地址。

        只是执行完while语句的效果,然后cur1.next=cur,就是下图的效果。

        


        2.2.5addIndex方法

        

        在指定的位置插入你要插入的元素。

        fushuyichang是我们自己写的一个异常。

        

        如图,如果你输入一个不合法的数,就会报出输入异常的提醒。如果你要在首尾位置插入,就直接调用我们原来写的方法。

        下面是我们从第一个位置到达倒数第二个位置插入的方法,先通过一个len方法来记录下第二个元素的下标,通过while语句来找到我们要插入的位置。然后通过一个temp来存下我们原来的位置的next,然后把你添加元素的地址放到当前位置的next当中,让后再把原来位置的next放到你添加的这个元素的next当中。

        或者通过下面这个方式:

        

        这样也可以成功的插入元素,不用引入第三个变量。


2.2.6 contains方法        

        不要在意len没有什么用,我忘记删除了,这个很简单能看懂,就是通过cur=cur.next的方式来移动指针,通过cur.val的值与key的值进行比较来判断是否找到该值,没有找到就返回false。


        2.2.7 remove方法
        第一种

        通过双指针,一个就按照正常的顺序往后走,一个是指向它的前一个位置,当找到了该元素后,通过prev是指向前一个元素的,当cur指针找到咱要删除的元素,通过prev.next=cur.next的方式删除掉它,很巧妙,当你删除的是第一个元素时,进去prev.next就是空值了,因为prev谁也没指向,它就是一个空指针,然后直接改变head也就是头元素就删除了,删除后面的就是prev永远就不会为null了,然后通过prev.next=cur.next的方式删除了。

        prev.next=cur.next的过程如下图:

        原来的

        当我们要删除18时,如图所示,cur找到了18,cur找到了该值,prev指向它的前一个位置,然后进行我们的操作。

        执行完之后的。

        这样就直接把0x77就跳过去了。


第二种

        

        先找到你要删除的前一个,改变你要删除的前一个的next,把你要删除那个元素的下一个元素的地址放进去,也可以达到删除的效果,和上面的一样,就是它定义了两个变量我们这里定义了一个。

        大家思考一下这个是否可以达到删除的效果?

        答案是不能的,你的prev看似是存放着cur.next,看似和上面的一样,但是这是指针,你这相当于把prev指针指向了cur.next那个地址的值,并不是改变了,你的cur的next的值,下面的表示的是有改变了prev的指向的地址,并没有断开连接。


两种方法的区别:        

        第一段代码中,当满足 key - 1 == len 的条件时,执行的删除操作是 prev = cur.next.next; ,这种写法存在错误,因为它只是将 prev 指向了 cur.next.next ,但并没有将 cur.next 与链表中的后续节点正确断开连接,无法实现删除节点的效果。

        第二段代码中,当满足条件时,执行的删除操作是 cur.next = cur.next.next; ,这是正确的删除节点操作,将当前节点(cur)的下一个节点(cur.next)指向其下下个节点(cur.next.next),从而实现了删除 cur.next 节点的效果。


        这里的cur.next直接使用和赋值给prev到底有什么区别

        

        当将 cur.next 赋值给 prev 时(如第一段代码):

 
singerLinkedlist prev = cur.next;
prev = cur.next.next;

        这里只是改变了 prev 的引用,让它指向了 cur.next.next ,但并没有改变链表的结构。原来 cur.next 所指向的节点仍然在链表中,没有被真正删除或断开连接。

        

而直接对 cur.next 进行赋值(如第二段代码):

 
cur.next = cur.next.next;
 

        这直接修改了链表中当前节点(cur)的下一个节点的引用,将其指向了下下个节点,从而实现了删除当前节点的下一个节点(cur.next)的效果,改变了链表的结构。

 

        总的来说,第一段代码中的操作没有对链表进行有效的删除节点操作,而第二段代码中的操作正确地实现了删除节点。

        


        第三种

        

        这是通过输入你要删除的值来删除的,不是通过下标。

        


        2.2.8 removeAllKey方法

        

        通过双指针,先把除了第一项的其他符合要求的项数全部删除。最后再判断一下第一项是否符合删除的条件。


        2.2.9 size方法

        

        


        2.2.10 clear方法

        

        这种是破坏了链表结构打印的,通过cur指向head,然后cur1指向cur,通过cur=cur.next的形式来使指针向后移动,通过cur1.next=null的形式使表的结构断开,只剩下第一个了,让head=null就行了。

        原来的链表结构。

        while执行后的

        就可以了。

       

        或者

        

        咱是通过head指针打印的,直接让head指针变为0就可以了。


        2.3 面试题

        2.3.1 删除链表中等于给定值 val 的所有节点。

        要求就是

        

        答案

        通过两个指针的形式,如果一直相等,就让prev一直指向第一个元素的位置不懂,直到最后一个元素的next为null赋值给prev,此时就剩一个头head了,如果中间有不相等的,就让prev指向那个不相等的,把他保留下来。

        源代码:

    public ListNode removeElements(ListNode head, int val) {

        if(head==null){

            return head;

        }

        ListNode cur=head.next;

        ListNode cur1=head;

        while(cur!=null){

            if(cur.val==val){

               cur1.next=cur.next;

               cur=cur.next;

               }

                else{

                    cur1=cur;

                    cur=cur.next;

                }

            }

            if(head.val==val){

                head=head.next;

            }

        return head;

    }


        2.3.2      翻转链表

        

        cur和cur1的作用都是记录原来它的next的值,防止原来的next发生变化,你再让cur=cur.next,他会陷入无限循环。

        源代码:

public ListNode reverseList(ListNode head) {

        if(head==null){

            return  head;

        }

        ListNode cur1=head.next;

        head.next=null;

        while(cur1!=null){

            ListNode prev=cur1.next;

            cur1.next=head;

            head=cur1;

            cur1=prev;

        }

        return head;

    }


2.3.3 链表的中间结点

        

要求:一次遍历就要完成,不能用len来记录次数。

        此时我们就要定义两个引用了。

        

        一个fast引用,走得快,slow引用走的慢,路程是一样的,fast是slow的二倍,所以fast到终点的时候,slow才走到中间位置。

        为什么要有两个限制条件呢?

        第二个限制条件是,当有5个数的时候,要打印后三个,fast到第三个时,fast到第二个了,判断还是符合,fast走到第五个位置了,此时slow走到第三个位置,此时就要打印了,如果只有第一个条件的话,此时还要向后走,slow就会走到第四个位置了,导致打印结果出错。

        第一个限制条件的作用,光有第二个限制条件也是不行的,也不能交换位置,例如,当有四个元素时,fast走到第三个位置,slow到了第二个位置,fast走到第五个位置,此时为null,你要是直接用fast.next此时fast为空,无法访问它的next会报空指针异常。

        源代码:

public ListNode middleNode(ListNode head) {

        if(head==null){

            return head;

        }

        ListNode fast=head;

        ListNode slow=head;

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

            fast=fast.next.next;

            slow=slow.next;

        }

        return slow;

    }


2.3.4 打印倒数的节点

        要求:输入一个链表,输出该链表中倒数第k个结点及其往后的节点。

        要求用一个while语句遍历一次解决。

        

        通过len控制先不遍历slow,直到我们走到合适的位置,你要打印倒数后两个的话,fast指向最后一个的时候,slow要指向倒数第二个,也就是图中的效果。

        源代码:
public void findlast(int k){
    singerLinkedlist fast=head;
    singerLinkedlist slow=head;
    int len=1;
    while (fast!=null&&fast.next!=null){
        if(len<k){
            len++;
            fast=fast.next;
            continue;
        }
        fast=fast.next;
        slow=slow.next;
    }
    System.out.println(slow.val);
}}

        2.3.5 判断回文

        题目要求:

        答案

        先把这个链表中的值加入放到数组当中,通过数组来判断是否是回文。

        源代码1:
public boolean check(singerLinkedlist head){
 int[] arr=new int[10];
 singerLinkedlist cur=head;
 int i=0;
 int sum=0;
 while (cur!=null){
     arr[i]=cur.val;
     cur=cur.next;
     sum++;
     i++;
 }
    for (int j = 0; j < sum/2; j++) {
        if (arr[j]==arr[sum-1]){

        }
        else {
            return false;
        }
        sum--;
    }
    return true;
}

解法二:

                通过快慢指针,快指针指向最后,慢指针指到中间位置,此时再通过下面的while循环翻转一下后半部分的内容,如果它是奇数,slow指针和cur指针会在中间位置相遇,此时就结束循环了,如果是偶数,通过if语句结束循环,此时到中间cur.next就是slow。

源代码2:
public boolean panduan(singerLinkedlist head){
    singerLinkedlist fast=head;
    singerLinkedlist slow=head;
    int len=0;
    while (fast!=null&&fast.next!=null){
        fast=fast.next.next;
        slow=slow.next;
    }
    singerLinkedlist cur=head;
    singerLinkedlist cur1=slow.next;
    while (cur1!=null){
        singerLinkedlist curn=cur1.next;
        cur1.next=slow;
        slow=cur1;
        cur1=curn;
    }
    while (slow!=cur){
        if (cur.next==slow){
            break;
        }
        if (cur.val!=slow.val){
            return false;
        }
        slow=slow.next;
        cur=cur.next;
    }
    return true;
}

2.3.6 拼接链表

        

        

        答案:

        

        先创建一个对象,它的头是-1,让tmp指向newlist,通过while判断谁的小,就把谁接在tmp的后面,最后head和head2的长度不一定一样,所以,判断二者谁不为空就把谁后面的全部拼在tmp的后面。

源代码:
public singerLinkedlist hebing(singerLinkedlist head,singerLinkedlist head2){
                    singerLinkedlist newlist=new singerLinkedlist(-1);
                    singerLinkedlist tmp=newlist;
                    while (head!=null&&head2!=null){
                        if (head.val<head2.val){
                            tmp.next=head;
                            head=head.next;
                            tmp=tmp.next;
                        }
                        else {
                            tmp.next=head2;
                            head2=head2.next;
                            tmp=tmp.next;
                        }
                    }
                    if (head!=null){
                        tmp=head;
                    }
                    if (head2!=null){
                        tmp=head2;
                    }
                    return newlist.next;
}

2.3.7 链表分割

        答案:

        解法一:

        

        创建两个对象一个是放小于x的一个放大于等于x的,通过while循环把它们分别放在不同的对象当中,可能全部都大于不存在小的,此时就直接返回prev1的next因为它的第一个是-1,不能要,如果小于的,可以再判断一下bigcur是否为空,如果是空的话就直接返回prev 的next,二者都不是空值再拼接,最后一个if语句是把最后一个元素的next设置为空值,因为如果最后一个数小于x,放在了前面,此时的最后一个next中放的还是原来的next值,此时就会形成一个闭环,无限循环输出。

源代码1:

public singerLinkedlist paixu(singerLinkedlist pHead,int x){
        // write code here
        singerLinkedlist littlecur=new singerLinkedlist(-1);
        singerLinkedlist bigcur=new singerLinkedlist(-1);
        singerLinkedlist prev=littlecur;
        singerLinkedlist prev1=bigcur;
        singerLinkedlist cur=pHead;
        while(cur!=null){
            if(cur.val<x){
                littlecur.next=cur;
                cur=cur.next;
                littlecur=littlecur.next;
            }
            else{
                bigcur.next=cur;
                cur=cur.next;
                bigcur=bigcur.next;
            }
        }
        if (littlecur==null){
            return prev1.next;
        }
        littlecur.next=prev1.next;
        if (littlecur!=null){
            bigcur.next=null;
        }
        return prev.next;
    }

解法二:

        

        先创建四个空指针。

        然后把两种值一种大于等于x的,一种小于x的分别用不同的指针指向它们,首先就是先判断s1是否为空,如果不为空,说明执行了else的语句,此时s1就指向了这个小于x的头元素,作用就是找头的,不为空执行上面的,让s2动,s1不动,结束之后,s2就指向了小于x的最后一个元素,同理s4也指向了最大值的最后一个元素,判断s2是否为空,为空证明没有小于x的值,此时直接返回s4,否则直接让s2的next拼上大于等于x的第一个,就是如图效果,通过最后一个if语句将最后一个元素弄为空值。

源代码2:

public singerLinkedlist paixu2(singerLinkedlist pHead,int x){
    singerLinkedlist s1=null;
    singerLinkedlist s2=null;
    singerLinkedlist s3=null;
    singerLinkedlist s4=null;
    singerLinkedlist cur=pHead;
    while (cur!=null){
        if(cur.val<x){
            if (s1!=null){
                s2.next=cur;
                cur=cur.next;
                s2=s2.next;
            }
            else {
                s1=s2=cur;
                cur=cur.next;
            }
        }
        else {
            if (s4!=null){
                s3.next=cur;
                cur=cur.next;
                s3=s3.next;
            }
            else {
                s3=s4=cur;
                cur=cur.next;
            }
        }
    }
    if (s2==null){
        return s3;
    }//访问next了就要考虑他是否为空。
    s2.next=s4;
    if (s2!=null&&s3!=null){
        s3.next=null;
    }//因为当s2为空时说明全部都大于x,此时也就不存在什么问题,当s3为空时,说明全是小于x的,也不会存在问题,但是当s2不为空的时候,当最后一个是小于x的,
    // 它的前面有大于x的,此时最后一个去前面了,但是此时的最后一个还是有next的此时就会陷入循环当中。
    return s1;
}
2.3.8 相交链表

        

        先讲一下这里的相交不是我们创痛意义上的相交。

        

        不是有交点的意思,而是只要有交点两个链表的后面全部得相等,是Y字型相交而不是我们的X型相交。

源代码:

public class Solution {

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {

        ListNode cur1=headA;

        ListNode cur2=headB;

        int len1=0;

        int len2=0;

        while(cur1!=null){

            len1++;

            cur1=cur1.next;

        }

        while(cur2!=null){

            len2++;

            cur2=cur2.next;

        }

        cur1=headA;

        cur2=headB;

        int len=len1-len2;

        if(len<0){

            cur1=headB;

            cur2=headA;

            len=len2-len1;

        }

        while(len!=0){

            cur1=cur1.next;

            len--;

        }

        while(cur1!=cur2){

            cur1=cur1.next;

            cur2=cur2.next;

        }

        if(cur1==null){

            return null;

        }

        return cur1;

    }

}

思路:

        思路就是,因为后面的要全部相等,两个链表的长度不一定相等,我们得先保证两个链表后面走的长度是一样的,所以我们就先让那个长的链表先走,保证它俩的长度一样,我们让cur1指向长链表让cur2指向短链表,我们不知道谁长,可以通过len1和len2来分别记录长度,再通过差值判断谁长,让cur1指向长的,再让cur1先走差值步,此时才可以判断是否相交,我们要的是相交的那个点,通过cur1!=cur2,如果相等了,说明就找到相交点了,下面的if语句如果cur1也就是长的为空了,说明没有找到相交点,此时返回null,否则就返回cur1说明找到了相交点。


2.3.9:环形链表

        答案:

        

        还是通过快慢指针的方式,让快指针走两步,慢指针走一步,此时二者的距离一直在缩小,如果是环形的话,总是会相遇的,如果fast==slow说明相遇了,就返回true否则就返回false。

        可能会有疑问,fast能不能一次走三步,slow走一步,这是不能的,例如一个两个元素呈环形的链表,它就一直会错过,如果不成环形链表,还会发生越界情况。

源代码:

    public boolean hasCycle(ListNode head) {

        ListNode fast=head;

        ListNode slow=head;

        if(head==null||head.next==null){

            return false;

        }

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

            fast=fast.next.next;

            slow=slow.next;

            if(fast==slow){

                return true;

            }

        }

        return false;

    }

2.3.10 环形链表 II

        

        答案:

        

        思路:

        

源代码:

public ListNode detectCycle(ListNode head) {

        ListNode fast=head;

        ListNode slow=head;

        ListNode cur=head;

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

            fast=fast.next.next;

            slow=slow.next;

            if(fast==slow){

                break;

            }

        }

        if(fast==null||fast.next==null){

            return null;

        }

        while(cur!=slow){

            cur=cur.next;

            slow=slow.next;

        }

        return cur;

    }


        三.双向链表

        3.1 什么是双向链表

        双向链表(Doubly Linked List)是链表的一种。

        在双向链表中,每个节点不仅包含数据和指向下一个节点的指针(称为“后继指针”),还包含指向前一个节点的指针(称为“前驱指针”)。

        这使得双向链表在某些操作上比单向链表更具优势。例如,在双向链表中,可以直接从尾部向头部遍历,而单向链表只能从头部向尾部遍历。

        3.2 双向链表的优点

        它解决了单向链表只能访问后一个元素的缺点,加了一个prev来表示前面的元素。

        如下图所示:

        加了一个prev来表示前一个变量,和一个last表示指向最后一个变量,使它既可以向前访问也可以向后访问,更加的灵活。

        3.3 双向链表中的方法

        3.3.1 display方法

        

        和上面的单项链表差不多一样。

         源代码:

        

public void display() {
shuangxiang cur=head;
while (cur!=null){
    System.out.print(cur.val+"  ");
    cur=cur.next;
}
    System.out.println();
}


3.3.2 size方法

        

        和单项链表的一样

        源代码:
public int size() {
    int count=0;
    shuangxiang cur=head;
    while (cur!=null){
        count++;
        cur=cur.next;
    }
    return count;
}


3.3.3 addfrist方法

        先判断是否为空,为空就直接返回,然后创建两个指针分别指向cur和cur1,直接改变第一个元素的next和prev即可,再移动一下头指针head即可。

        源代码:
public void addFirst(int data) {
    shuangxiang l6=new shuangxiang(data);
    if (head==null){
        head=l6;
        last=l6;
        return;
    }
    shuangxiang cur=head;
    shuangxiang cur1=last;
  cur.prev=l6;
  l6.next=cur;
  l6.prev=null;
  head=l6;
}


3.3.4 addlast方法

                还是先判断是否为空,不为空就对最后一个元素进行操作。

        源代码:
public void addLast(int data) {
shuangxiang q6=new shuangxiang(data);
if (head==null){
    head=last=q6;
    return;
}
last.next=q6;
q6.prev=last;
last=q6;
}


3.3.5 addIndex方法

        

        还是先判断是否为空,然后如果他要在第一个元素处添加,就直接调用addfrist方法,如果最后处添加,就调用addlast方法,然后用len来记录index的值,然后让prev指向原来位置的元素,让cur指向原来的前面的哪个元素,然后cur3和cur2的作用分别是记录原来的next和prev的值,使他接在后面。

        源代码:
public void addIndex(int index, int data) {
shuangxiang l6=new shuangxiang(data);
shuangxiang cur=head;
shuangxiang prev=last;
if (index>size()-1||index<0){
    throw new fushuyichang("输入异常,超出范围");
}
if (index==size()-1){
    addLast(data);
    return;
}
if (index==0){
    addFirst(data);
    return;
}
int len=index;
while (len!=0){
    prev=prev.prev;
    len--;
}
while (len+1!=index){
    cur=cur.next;
    len++;
}
shuangxiang cur2=cur.next;
shuangxiang cur3=prev.prev;
cur.next=l6;
l6.next=cur2;
prev.prev=l6;
l6.prev=cur3;
}


3.3.6 remove方法

        

        删除元素第一次出现的位置,然后先判断首尾是否为要删除的元素,然后再找中间的元素,通过找到要删除的元素的前一个元素,然后对他进行操作,即可删除该元素。

        源代码:
public void remove(int key) {
shuangxiang cur=head;
shuangxiang prev=last;
if (prev.val==key){
    shuangxiang prev2=prev.prev;
    prev.prev=null;
    prev2.next=null;
    return;
}
if (head.val==key){
    shuangxiang cur5=cur.next;
    cur.next=null;
    cur5.prev=null;
    head=cur5;
    return;
}
while (cur!=null){
    if (cur.next.val==key){
        cur.next=cur.next.next;
        cur.next.prev=cur;
        break;
    }
    cur=cur.next;
}
}


3.3.7 removeAllkey方法

        

        删除出现的全部元素,首先设立几个指针分别指向不同的对象,然后通过while语句先找到你要删除的前一个元素,然后对它进行操作,使其删除它,最后还要判断头元素是否相等,相等就直接让他等于后一个删除掉它。

        源代码:
public void removeAllKey(int key) {
    shuangxiang cur=head;
    shuangxiang cur3=head.next;
    while (cur3!=null){
        if (cur3.val==key){
       cur.next=cur.next.next;
       cur3=cur3.next;
       cur.next.prev=cur;
        }
        else {
            cur=cur3;
            cur3=cur3.next;
        }
    }
    if (head.val==key){
        head=head.next;
    }
}


2.3.8 contains方法

        

        和单向的一样,就是找是否存在这个元素。

        

        源代码:
public boolean contains(int key) {
    shuangxiang cur=head;
    while (cur!=null){
        if (cur.val==key){
            return true;
        }
        cur=cur.next;
    }
    return false;
}


四.ArrayListLinkedList的区别

        

五.结束语

        感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。  感谢大家的一键三连。

  • 36
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值