算法题总结(六)——链表

虚拟头结点

链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。

每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题

常用方法:

1、虚拟头结点

2、双指针

#206、反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

采用双指针 头插法


class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre =null;
        ListNode cur=head;
        ListNode tmp=null;//存储下一个结点

        while(cur!=null)
        {
            tmp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=tmp;
        }
        return pre;
    }
}

#24、两两交换链表中的结点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]

要使用一个虚拟头结点


class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode vhead = new ListNode(-1);
        vhead.next=head;
        ListNode cur=vhead;
        ListNode temp=null; //记录后面的链,防止断链
        ListNode firstNode=null;
        ListNode secondNode=null;
        while(cur.next!=null && cur.next.next!=null)
        {
            firstNode=cur.next;
            secondNode=cur.next.next;
            temp=cur.next.next.next;
            //交换
            cur.next=secondNode;
            firstNode.next=temp;
            secondNode.next=firstNode;
            cur=firstNode;
        }
        return vhead.next;
    }
}

#19、删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

当fastIndex指向最后一个结点时,和倒数第n个结点之间有n-1个间隔,所以fastIndex比slowIndex相比,多走了n-1步,由于让slowIndex指向被删除结点的前一个方便删除,因此fastIndex比slowIndex多走n步。


class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //设置一个虚拟结点,方便删除头结点的情况
        ListNode vhead =new ListNode(-1);
        vhead.next=head;
        ListNode slowIndex=vhead;
        ListNode fastIndex=vhead;
        //fastIndex比slowIndex多走n步
        for(int i=0;i<n;i++)
        {
            fastIndex=fastIndex.next;
        }
        //让fastIndex指向最后一个结点
        while(fastIndex.next!=null)
        {
            fastIndex=fastIndex.next;
            slowIndex=slowIndex.next;
        }
        //slowIndex指向被删除结点的前一个
        slowIndex.next=slowIndex.next.next;

        return vhead.next;
    }
}

#07、链表相交

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

先找到长度差,然后移动到距离末尾同一位置处,再一起同时移动


public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {

        ListNode curA =headA;
        ListNode curB =headB;
        int lenA=0;
        int lenB=0;
        while(curA!=null)
        {
            lenA++;
            curA=curA.next;
        }
        while(curB!=null)
        {
            lenB++;
            curB=curB.next;
        }
        curA=headA;
        curB=headB;
        if(lenA>lenB)
        {

            int num=lenA-lenB;
            for(int i=0;i<num;i++)
            {
                curA=curA.next;
            }
        }
        else{
            int num=lenB-lenA;
            for(int i=0;i<num;i++)
            {
                curB=curB.next;
            }

        }
        while(curA!=curB)
        {
            curA=curA.next;
            curB=curB.next;
        }
        return curA;

    }
}

#142、环形链表二

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

1、判断链表是否有环:

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。因为如果有环,相对slow来说,fast是一步一步靠近slow的,所以一定会相遇,而且是在环中相遇!

2、有环的情况下,如何找到这个环的入口:

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z,

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

public class Solution {
    public ListNode detectCycle(ListNode head) {

        ListNode fast=head;
        ListNode slow=head;
        while(fast!=null && fast.next!=null)  //因为下面会fast.next.next,所以这里要判断fast.next   fast跑的快,只需要判断fast就可以
        {
            slow=slow.next;
            fast=fast.next.next;  //每次快指针是慢指针的两倍
            if(slow==fast)  //相遇说明有环
            {
                ListNode index1 =fast;
                ListNode index2 =head;
                //从头结点出发一个指针,从相遇节点 也出发一个指针,
                //这两个指针每次只走一个节点, 
                //那么当这两个指针相遇的时候就是 环形入口的节点。
                while(index1!=index2)
                {
                    index1=index1.next;
                    index2=index2.next;
                }
                return index1;

            }
        }

        return null;

    }
}

#25、K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例 2:

输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

提示:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

步骤分解:

1、链表分区为已翻转部分+待翻转部分+未翻转部分

2、每次翻转前,要确定翻转链表的范围,这个必须通过 k 此循环来确定。

3、需记录翻转链表前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来

4、初始需要两个变量 pre 和 end,pre 代表待翻转链表的前驱,end 代表待翻转链表的末尾

5、经过k此循环,end 到达末尾,记录待翻转链表的后继 next = end.next

6、翻转链表,然后将三部分链表连接起来,然后重置 pre 和 end 指针,然后进入下一次循环

7、特殊情况,当翻转部分长度不足 k 时,在定位 end 完成后,end==null,已经到达末尾,说明题目已完成,直接返回即可

8、时间复杂度为 O(n∗K),最好的情况为 O(n),最差的情况未 O(n2)

9、空间复杂度为 O(1)O(1)O(1) 除了几个必须的节点指针外,我们并没有占用其他空间

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy=new ListNode(0);//头结点
        dummy.next=head;

        ListNode pre=dummy;   //用来记录每组的前一个结点
        ListNode end=dummy;   //用来找每组的末尾

        while(end.next!=null)  //当不是末尾元素时
        {
            for(int i=0;i<k && end!=null;i++)  //找到每组的最后一个元素
            end=end.next;
            if(end==null)  //已到达末尾,说明已经结束
                break;
            ListNode start=pre.next; //每组的第一个
            ListNode next=end.next;  //下一组的第一个,用来相连接
            end.next=null; //让每一组的末尾断开
            pre.next=reverse(start); //每一组进行翻转
            start.next=next;
            pre=start;
            end=pre;    //pre,end都让其指向下一组的前一个结点

        }
        return dummy.next;
    }

    //翻转一个链表,翻转完之后 head是指向尾节点的
    public ListNode reverse(ListNode head) 
    {
        ListNode pre=null;
        ListNode cur=head;
        ListNode temp=null;  //用来记录链
        while(cur!=null)
        {
            temp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=temp;
        }
        return pre;

    }



}

#138、随机链表的复制

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。

你的代码 接受原链表的头节点 head 作为传入参数。

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

思路:

本题要求我们对一个特殊的链表进行深拷贝。如果是普通链表,我们可以直接按照遍历的顺序创建链表节点。而本题中因为随机指针的存在,当我们拷贝节点时,「当前节点的随机指针指向的节点」可能还没创建,因此我们需要变换思路。一个可行方案是,我们利用回溯的方式,让每个节点的拷贝操作相互独立。对于当前节点,我们首先要进行拷贝,然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝,拷贝完成后将创建的新节点的指针返回,即可完成当前节点的两指针的赋值。

具体地,我们用哈希表记录每一个节点对应新节点的创建情况。遍历该链表的过程中,我们检查「当前节点的后继节点」和「当前节点的随机指针指向的节点」的创建情况。如果这两个节点中的任何一个节点的新节点没有被创建,我们都立刻递归地进行创建。当我们拷贝完成,回溯到当前层时,我们即可完成当前节点的指针赋值。注意一个节点可能被多个其他节点指向,因此我们可能递归地多次尝试拷贝某个节点,为了防止重复拷贝,我们需要首先检查当前节点是否被拷贝过,如果已经拷贝过,我们可以直接从哈希表中取出拷贝后的节点的指针并返回即可。

在实际代码中,我们需要特别判断给定节点为空节点的情况。

class Solution {
    //记录已经复制的结点
    Map<Node,Node> cacheNode =new HashMap<>();
    public Node copyRandomList(Node head) {
    if(head==null)
        return null;
    //如果此结点没有复制
    if(!cacheNode.containsKey(head))
    {
        Node newhead =new Node(head.val);
        cacheNode.put(head,newhead);
        newhead.next=copyRandomList(head.next);
        newhead.random=copyRandomList(head.random);
    }
    //已经复制了就返回复制的结点
    return cacheNode.get(head);

}
}

#148、排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

示例 1:

输入:head = [4,2,1,3]
输出:[1,2,3,4]

你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

归并排序的时间复杂度是O(nlogn)

class Solution {
    public ListNode sortList(ListNode head) {
        if(head==null)
            return null;
        int length=0;
        //得到链表的长度
        ListNode cur=head;
        while(cur!=null)
        {
            length++;
            cur=cur.next;
        }
        //建立头结点
        ListNode dummy = new ListNode(0);
        dummy.next=head;
        //控制每次排序的步长,每两组进行合并
        for(int subLength=1;subLength<length;subLength*=2)
        {
            ListNode prev=dummy;  //用来连接
            ListNode curr=dummy.next;  //用来遍历
            while(curr!=null)
            {
                ListNode head1=curr; //第一组的第一个结点
                //寻找第一组的末尾,要么是第k个,要么不足k,是最后一个
                for(int i=1;i<subLength && curr.next!=null;i++)
                {
                    curr=curr.next;
                }
                ListNode head2=curr.next; //第二组的第一个结点
                curr.next=null; //把第一组的末尾指向空
                curr=head2;   //第二组的第一个结点可能为null,所以下面要判断
                //寻找第二组的末尾,要么是第k个,要么不足k,是最后一个
                for(int i=1;i<subLength &&curr!=null &&curr.next!=null;i++)  //这里要判断一下curr!=null ,因为curr是有可能等于0的
                {
                    curr=curr.next;
                }
                ListNode next =null;  //记录后面的链
                if(curr!=null) //排除curr为null的情况
                {
                    next=curr.next; //记录后面的链
                    curr.next=null;//第二组的末尾指向空

                }
                prev.next=merge(head1,head2); //把每两个组合并并和前面的连接起来
                while(prev.next!=null)
                {
                    prev=prev.next;
                }
                curr=next;
            }

        }
        return dummy.next;
    }

    public ListNode merge(ListNode head1,ListNode head2)
    {
        ListNode dummy=new ListNode(0);
        ListNode temp=dummy,temp1=head1,temp2=head2;
        while(temp1!=null && temp2!=null)
        {
            if(temp1.val<=temp2.val)
            {
                temp.next=temp1;
                temp1=temp1.next;
            }
            else
            {
                temp.next=temp2;
                temp2=temp2.next;
            }
            temp=temp.next;
        }
        if(temp1!=null)
            temp.next=temp1;
        if(temp2!=null)
            temp.next=temp2;

        return dummy.next;

    }

}

25和148题都是每次取出一定大小的链,进行断链。

#23、合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

顺序合并

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {

        ListNode ans=null;
        for(int i=0;i<lists.length;i++)
        {
            ans=merge(ans,lists[i]);
        }
        return ans;

    }
    public ListNode merge(ListNode head1,ListNode head2)
    {
        ListNode dummy=new ListNode(0);
        ListNode temp=dummy;
        ListNode temp1=head1,temp2=head2;
        while(temp1!=null && temp2!=null)
        {
            if(temp1.val<=temp2.val)
            {
                temp.next=temp1;
                temp1=temp1.next;
            }
            else
            {
                temp.next=temp2;
                temp2=temp2.next;
            }
            temp=temp.next;
        }
        if(temp1!=null)
            temp.next=temp1;
        else if(temp2!=null)
            temp.next=temp2;

        return dummy.next;
    }
}

#146、LRU缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

思路

使用哈希表+双向链表实现

访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1)时间内完成。

在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样可以很方便的对头尾结点进行操作。

class LRUCache {

    //定义一个双向链表的结点
    class DlinkedNode{
        int key;
        int value;
        DlinkedNode prev;
        DlinkedNode next;  //前驱和后继
        public DlinkedNode(){}
        public DlinkedNode(int _key,int _value)
        {
            key=_key;
            value=_value;
        }
    }

    private Map<Integer,DlinkedNode> cache =new HashMap<Integer,DlinkedNode>();
    private int size;
    private int capacity;
    private DlinkedNode head,tail; //虚拟头结点和尾节点

    //构造函数
    public LRUCache(int capacity) {
        this.size=0;
        this.capacity=capacity;
        head=new DlinkedNode();
        tail=new DlinkedNode();
        head.next=tail;
        tail.prev=head;
    }

    public int get(int key) {
        DlinkedNode node =cache.get(key);
        if(node==null)
            return -1;
        moveToHead(node);  //先通过哈希定位,在移动到头部
        return node.value;
    }

    public void put(int key, int value) {
        DlinkedNode node=cache.get(key);
        if(node==null)  //如果不存在,新建一个结点
        {
            DlinkedNode newNode =new DlinkedNode(key,value);
            cache.put(key,newNode);
            addTohead(newNode);
            size++;
            if(size>capacity)  //超出容量
            {
                //删除尾节点
                DlinkedNode tail =removeTail();
                cache.remove(tail.key);  //哈希表中删除对应的项
                size--;
            }
        }
        else 
        {  //如果node存在,修改value后移动到头部
            node.value=value;
            moveToHead(node);
        }
    }

    //头部添加一个结点
    private void addTohead(DlinkedNode node)
    {
        node.prev=head;
        node.next=head.next;
        head.next.prev=node;
        head.next=node;
    }

    //移除一个结点
    private void removeNode(DlinkedNode node)
    {
        node.next.prev=node.prev;
        node.prev.next=node.next;
    }

    //把一个结点移动到头部
    private void moveToHead(DlinkedNode node)
    {
        removeNode(node);
        addTohead(node);
    }

    //删除尾部结点
    private DlinkedNode removeTail()
    {
        DlinkedNode res=tail.prev;
        removeNode(res);
        return res;
    }

#234、回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

示例 1:

输入:head = [1,2,2,1]
输出:true

示例 2:

输入:head = [1,2]
输出:false

提示:

  • 链表中节点数目在范围[1, 105] 内
  • 0 <= Node.val <= 9

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

即用O(n) 时间复杂度和 O(1) 空间复杂度判断链表是否回文。

先找到中间结点,把后半部分翻转,然后逐一判断。

class Solution {
    public boolean isPalindrome(ListNode head) {
        int len=0;
        ListNode cur=head;
        while(cur!=null)  //统计结点的个数
        {
            len++;
            cur=cur.next;
        }
        cur=head;
        if(len%2==0) //如果结点数为偶数
        {
            for(int i=1;i<len/2;i++)
            {
                cur=cur.next;
            }

        }
        else //如果结点数为偶数
        {
            for(int i=1;i<len/2+1;i++)
            {
                cur=cur.next;
            }

        }
        //把后半部分翻转
        ListNode index1=head;
        ListNode index2=reverse(cur.next);
        while(index2!=null)
        {
            if(index1.val!=index2.val)
            {
                return false;
            }
            index1=index1.next;
            index2=index2.next;
        }
        return true;

    }
    public ListNode reverse(ListNode head)
    {
        ListNode cur=head;
        ListNode pre=null;
        ListNode tmp=null;
        while(cur!=null)
        {
            tmp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=tmp;
        }
        return pre;
    }
}

#2、两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode pre=new ListNode(0);  //头结点
        ListNode cur=pre; //可移动的指针,往后遍历,形成链表
        int carry=0;
        while(l1!=null || l2!=null) //只要有一个不为空就行
        {
            int x=l1==null?0:l1.val;
            int y=l2==null?0:l2.val;
            int sum=x+y+carry;
            carry=sum/10;
            sum=sum%10;
            cur.next=new ListNode(sum);
            cur=cur.next;
            if(l1!=null)
                l1=l1.next;
            if(l2!=null)
                l2=l2.next;
        }
        if(carry==1) //最后还有一个进位
            cur.next=new ListNode(1);

        return pre.next;
    
    }
}

双指针法总结

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新 新数组下标的位置

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。

数组中的双指针

链表双指针:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值