peek java linkedlist_JAVA提高十一:LinkedList深入分析

/*** LinkedList底层使用双向链表,实现了List和deque。实现所有的可选List操作,并可以只有所有元素(包括空值)

* 其大小理论上仅受内存大小的限制

*

* 所有的操作都可以作为一个双联列表来执行(及对双向链表操作)。

* 把对链表的操作封装起来,并对外提供看起来是对普通列表操作的方法。

* 遍历从起点、终点、或指定位置开始

* 内部方法,注释会描述为节点的操作(如删除第一个节点),公开的方法会描述为元素的操作(如删除第一个元素)

*

* LinkedList不是线程安全的,如果在多线程中使用(修改),需要在外部作同步处理。

*

* 需要弄清元素(节点)的索引和位置的区别,不然有几个地方不好理解,具体在碰到的地方会解释。

*

* 迭代器可以快速报错*/

public class LinkedList extends AbstractSequentialList implements List, Deque, Cloneable, java.io.Serializable

{//容量

transient int size = 0;//首节点

transient Nodefirst;//尾节点

transient Nodelast;//默认构造函数

publicLinkedList() {

}//通过一个集合初始化LinkedList,元素顺序有这个集合的迭代器返回顺序决定

public LinkedList(Collection extends E>c) {this();

addAll(c);

}//使用对应参数作为第一个节点,内部使用

private voidlinkFirst(E e) {final Node f = first;//得到首节点

final Node newNode = new Node<>(null, e, f);//创建一个节点

first = newNode; //设置首节点

if (f == null)

last= newNode; //如果之前首节点为空(size==0),那么尾节点就是首节点

elsef.prev= newNode; //如果之前首节点不为空,之前的首节点的前一个节点为当前首节点

size++; //长度+1

modCount++; //修改次数+1

}//使用对应参数作为尾节点

voidlinkLast(E e) {final Node l = last; //得到尾节点

final Node newNode = new Node<>(l, e, null);//使用参数创建一个节点

last = newNode; //设置尾节点

if (l == null)

first= newNode; //如果之前尾节点为空(size==0),首节点即尾节点

elsel.next= newNode; //如果之前尾节点不为空,之前的尾节点的后一个就是当前的尾节点

size++;

modCount++;

}//在指定节点前插入节点,节点succ不能为空

void linkBefore(E e, Nodesucc) {final Node pred = succ.prev;//获取前一个节点

final Node newNode = new Node<>(pred, e, succ);//使用参数创建新的节点,向前指向前一个节点,向后指向当前节点

succ.prev = newNode;//当前节点指向新的节点

if (pred == null)

first= newNode;//如果前一个节点为null,新的节点就是首节点

elsepred.next= newNode;//如果存在前节点,那么前节点的向后指向新节点

size++;

modCount++;

}//删除首节点并返回删除前首节点的值,内部使用

private E unlinkFirst(Nodef) {final E element = f.item;//获取首节点的值

final Node next = f.next;//得到下一个节点

f.item = null;

f.next= null; //便于垃圾回收期清理

first = next; //首节点的下一个节点成为新的首节点

if (next == null)

last= null; //如果不存在下一个节点,则首尾都为null(空表)

elsenext.prev= null;//如果存在下一个节点,那它向前指向null

size--;

modCount++;returnelement;

}//删除尾节点并返回删除前尾节点的值,内部使用

private E unlinkLast(Nodel) {final E element = l.item;//获取值

final Node prev = l.prev;//获取尾节点前一个节点

l.item = null;

l.prev= null; //便于垃圾回收期清理

last = prev; //前一个节点成为新的尾节点

if (prev == null)

first= null; //如果前一个节点不存在,则首尾都为null(空表)

elseprev.next= null;//如果前一个节点存在,先后指向null

size--;

modCount++;returnelement;

}//删除指定节点并返回被删除的元素值

E unlink(Nodex) {//获取当前值和前后节点

final E element =x.item;final Node next =x.next;final Node prev =x.prev;if (prev == null) {

first= next; //如果前一个节点为空(如当前节点为首节点),后一个节点成为新的首节点

} else{

prev.next= next;//如果前一个节点不为空,那么他先后指向当前的下一个节点

x.prev = null; //方便gc回收

}if (next == null) {

last= prev; //如果后一个节点为空(如当前节点为尾节点),当前节点前一个成为新的尾节点

} else{

next.prev= prev;//如果后一个节点不为空,后一个节点向前指向当前的前一个节点

x.next = null; //方便gc回收

}

x.item= null; //方便gc回收

size--;

modCount++;returnelement;

}//获取第一个元素

publicE getFirst() {final Node f = first;//得到首节点

if (f == null) //如果为空,抛出异常

throw newNoSuchElementException();returnf.item;

}//获取最后一个元素

publicE getLast() {final Node l = last;//得到尾节点

if (l == null) //如果为空,抛出异常

throw newNoSuchElementException();returnl.item;

}//删除第一个元素并返回删除的元素

publicE removeFirst() {final Node f = first;//得到第一个节点

if (f == null) //如果为空,抛出异常

throw newNoSuchElementException();returnunlinkFirst(f);

}//删除最后一个元素并返回删除的值

publicE removeLast() {final Node l = last;//得到最后一个节点

if (l == null) //如果为空,抛出异常

throw newNoSuchElementException();returnunlinkLast(l);

}//添加元素作为第一个元素

public voidaddFirst(E e) {

linkFirst(e);

}//店家元素作为最后一个元素

public voidaddLast(E e) {

linkLast(e);

}//检查是否包含某个元素,返回bool

public booleancontains(Object o) {return indexOf(o) != -1;//返回指定元素的索引位置,不存在就返回-1,然后比较返回bool值

}//返回列表长度

public intsize() {returnsize;

}//添加一个元素,默认添加到末尾作为最后一个元素

public booleanadd(E e) {

linkLast(e);return true;

}//删除指定元素,默认从first节点开始,删除第一次出现的那个元素

public booleanremove(Object o) {//会根据是否为null分开处理。若值不是null,会用到对象的equals()方法

if (o == null) {for (Node x = first; x != null; x =x.next) {if (x.item == null) {

unlink(x);return true;

}

}

}else{for (Node x = first; x != null; x =x.next) {if(o.equals(x.item)) {

unlink(x);return true;

}

}

}return false;

}//添加指定集合的元素到列表,默认从最后开始添加

public boolean addAll(Collection extends E>c) {return addAll(size, c);//size表示最后一个位置,可以理解为元素的位置分别为1~size

}//从指定位置(而不是下标!下标即索引从0开始,位置可以看做从1开始,其实也是0)后面添加指定集合的元素到列表中,只要有至少一次添加就会返回true//index换成position应该会更好理解,所以也就是从索引为index(position)的元素的前面索引为index-1的后面添加!//当然位置可以为0啊,为0的时候就是从位置0(虽然它不存在)后面开始添加嘛,所以理所当前就是添加到第一个位置(位置1的前面)的前面了啊!//比如列表:0 1 2 3,如果此处index=4(实际索引为3),就是在元素3后面添加;如果index=3(实际索引为2),就在元素2后面添加。//原谅我的表达水平,我已经尽力解释了...

public boolean addAll(int index, Collection extends E>c) {

checkPositionIndex(index);//检查索引是否正确(0<=index<=size)

Object[] a = c.toArray(); //得到元素数组

int numNew = a.length; //得到元素个数

if (numNew == 0) //若没有元素要添加,直接返回false

return false;

Nodepred, succ;if (index == size) { //如果是在末尾开始添加,当前节点后一个节点初始化为null,前一个节点为尾节点

succ = null; //这里可以看做node(index),不过index=size了(index最大只能是size-1),所以这里的succ只能=null,也方便后面判断

pred = last; //这里看做noede(index-1),当然实现是不能这么写的,看做这样只是为了好理解,所以就是在node(index-1的后面开始添加元素)

} else { //如果不是从末尾开始添加,当前位置的节点为指定位置的节点,前一个节点为要添加的节点的前一个节点

succ = node(index); //添加好元素后(整个新加的)的后一个节点

pred = succ.prev; //这里依然是node(index-1)

}//遍历数组并添加到列表中

for(Object o : a) {

@SuppressWarnings("unchecked")

E e=(E) o;

Node newNode = new Node<>(pred, e, null);//创建一个节点,向前指向上面得到的前节点

if (pred == null)

first= newNode; //若果前节点为null,则新加的节点为首节点

elsepred.next= newNode;//如果存在前节点,前节点会向后指向新加的节点

pred = newNode; //新加的节点成为前一个节点

}if (succ == null) {//pred.next = null//加上这句也可以更好的理解

last = pred; //如果是从最后开始添加的,则最后添加的节点成为尾节点

} else{

pred.next= succ; //如果不是从最后开始添加的,则最后添加的节点向后指向之前得到的后续第一个节点

succ.prev = pred; //当前,后续的第一个节点也应改为向前指向最后一个添加的节点

}

size+=numNew;

modCount++;return true;

}//清空表

public voidclear() {//方便gc回收垃圾

for (Node x = first; x != null; ) {

Node next =x.next;

x.item= null;

x.next= null;

x.prev= null;

x=next;

}

first= last = null;

size= 0;

modCount++;

}//获取指定索引的节点的值

public E get(intindex) {

checkElementIndex(index);returnnode(index).item;

}//修改指定索引的值并返回之前的值

public E set(intindex, E element) {

checkElementIndex(index);

Node x =node(index);

E oldVal=x.item;

x.item=element;returnoldVal;

}//指定位置后面(即索引为这个值的元素的前面)添加元素

public void add(intindex, E element) {

checkPositionIndex(index);if (index ==size)

linkLast(element);//如果指定位置为最后,则添加到链表最后

else //如果指定位置不是最后,则添加到指定位置前

linkBefore(element, node(index));

}//删除指定位置的元素,

public E remove(intindex) {

checkElementIndex(index);returnunlink(node(index));

}//检查索引是否超出范围,因为元素索引是0~size-1的,所以index必须满足0<=index

private boolean isElementIndex(intindex) {return index >= 0 && index

}//检查位置是否超出范围,index必须在index~size之间(含),如果超出,返回false

private boolean isPositionIndex(intindex) {return index >= 0 && index <=size;

}//异常详情

private String outOfBoundsMsg(intindex) {return "Index: "+index+", Size: "+size;

}//检查元素索引是否超出范围,若已超出,就抛出异常

private void checkElementIndex(intindex) {if (!isElementIndex(index))throw newIndexOutOfBoundsException(outOfBoundsMsg(index));

}//检查位置是否超出范围,若已超出,就抛出异常

private void checkPositionIndex(intindex) {if (!isPositionIndex(index))throw newIndexOutOfBoundsException(outOfBoundsMsg(index));

}//获取指定位置的节点

Node node(intindex) {//如果位置索引小于列表长度的一半(或一半减一),从前面开始遍历;否则,从后面开始遍历

if (index < (size >> 1)) {

Node x = first;//index==0时不会循环,直接返回first

for (int i = 0; i < index; i++)

x=x.next;returnx;

}else{

Node x =last;for (int i = size - 1; i > index; i--)

x=x.prev;returnx;

}

}//获取指定元素从first开始的索引位置,不存在就返回-1//不能按条件双向找了,所以通常根据索引获得元素的速度比通过元素获得索引的速度快

public intindexOf(Object o) {int index = 0;if (o == null) {for (Node x = first; x != null; x =x.next) {if (x.item == null)returnindex;

index++;

}

}else{for (Node x = first; x != null; x =x.next) {if(o.equals(x.item))returnindex;

index++;

}

}return -1;

}//获取指定元素从first开始最后出现的索引,不存在就返回-1//但实际查找是从last开始的

public intlastIndexOf(Object o) {int index =size;if (o == null) {for (Node x = last; x != null; x =x.prev) {

index--;if (x.item == null)returnindex;

}

}else{for (Node x = last; x != null; x =x.prev) {

index--;if(o.equals(x.item))returnindex;

}

}return -1;

}//提供普通队列和双向队列的功能,当然,也可以实现栈,FIFO,FILO//出队(从前端),获得第一个元素,不存在会返回null,不会删除元素(节点)

publicE peek() {final Node f =first;return (f == null) ? null: f.item;

}//出队(从前端),不删除元素,若为null会抛出异常而不是返回null

publicE element() {returngetFirst();

}//出队(从前端),如果不存在会返回null,存在的话会返回值并移除这个元素(节点)

publicE poll() {final Node f =first;return (f == null) ? null: unlinkFirst(f);

}//出队(从前端),如果不存在会抛出异常而不是返回null,存在的话会返回值并移除这个元素(节点)

publicE remove() {returnremoveFirst();

}//入队(从后端),始终返回true

public booleanoffer(E e) {returnadd(e);

}//入队(从前端),始终返回true

public booleanofferFirst(E e) {

addFirst(e);return true;

}//入队(从后端),始终返回true

public booleanofferLast(E e) {

addLast(e);//linkLast(e)

return true;

}//出队(从前端),获得第一个元素,不存在会返回null,不会删除元素(节点)

publicE peekFirst() {final Node f =first;return (f == null) ? null: f.item;

}//出队(从后端),获得最后一个元素,不存在会返回null,不会删除元素(节点)

publicE peekLast() {final Node l =last;return (l == null) ? null: l.item;

}//出队(从前端),获得第一个元素,不存在会返回null,会删除元素(节点)

publicE pollFirst() {final Node f =first;return (f == null) ? null: unlinkFirst(f);

}//出队(从后端),获得最后一个元素,不存在会返回null,会删除元素(节点)

publicE pollLast() {final Node l =last;return (l == null) ? null: unlinkLast(l);

}//入栈,从前面添加

public voidpush(E e) {

addFirst(e);

}//出栈,返回栈顶元素,从前面移除(会删除)

publicE pop() {returnremoveFirst();

}/*** Removes the first occurrence of the specified element in this

* list (when traversing the list from head to tail). If the list

* does not contain the element, it is unchanged.

*

*@paramo element to be removed from this list, if present

*@return{@codetrue} if the list contained the specified element

*@since1.6*/

public booleanremoveFirstOccurrence(Object o) {returnremove(o);

}/*** Removes the last occurrence of the specified element in this

* list (when traversing the list from head to tail). If the list

* does not contain the element, it is unchanged.

*

*@paramo element to be removed from this list, if present

*@return{@codetrue} if the list contained the specified element

*@since1.6*/

public booleanremoveLastOccurrence(Object o) {if (o == null) {for (Node x = last; x != null; x =x.prev) {if (x.item == null) {

unlink(x);return true;

}

}

}else{for (Node x = last; x != null; x =x.prev) {if(o.equals(x.item)) {

unlink(x);return true;

}

}

}return false;

}/*** Returns a list-iterator of the elements in this list (in proper

* sequence), starting at the specified position in the list.

* Obeys the general contract of {@codeList.listIterator(int)}.

*

* The list-iterator is fail-fast: if the list is structurally

* modified at any time after the Iterator is created, in any way except

* through the list-iterator's own {@coderemove} or {@codeadd}

* methods, the list-iterator will throw a

* {@codeConcurrentModificationException}. Thus, in the face of

* concurrent modification, the iterator fails quickly and cleanly, rather

* than risking arbitrary, non-deterministic behavior at an undetermined

* time in the future.

*

*@paramindex index of the first element to be returned from the

* list-iterator (by a call to {@codenext})

*@returna ListIterator of the elements in this list (in proper

* sequence), starting at the specified position in the list

*@throwsIndexOutOfBoundsException {@inheritDoc}

*@seeList#listIterator(int)*/

public ListIterator listIterator(intindex) {

checkPositionIndex(index);return newListItr(index);

}private class ListItr implements ListIterator{private NodelastReturned;private Nodenext;private intnextIndex;private int expectedModCount =modCount;

ListItr(intindex) {//assert isPositionIndex(index);

next = (index == size) ? null: node(index);

nextIndex=index;

}public booleanhasNext() {return nextIndex

}publicE next() {

checkForComodification();if (!hasNext())throw newNoSuchElementException();

lastReturned=next;

next=next.next;

nextIndex++;returnlastReturned.item;

}public booleanhasPrevious() {return nextIndex > 0;

}publicE previous() {

checkForComodification();if (!hasPrevious())throw newNoSuchElementException();

lastReturned= next = (next == null) ?last : next.prev;

nextIndex--;returnlastReturned.item;

}public intnextIndex() {returnnextIndex;

}public intpreviousIndex() {return nextIndex - 1;

}public voidremove() {

checkForComodification();if (lastReturned == null)throw newIllegalStateException();

Node lastNext =lastReturned.next;

unlink(lastReturned);if (next ==lastReturned)

next=lastNext;elsenextIndex--;

lastReturned= null;

expectedModCount++;

}public voidset(E e) {if (lastReturned == null)throw newIllegalStateException();

checkForComodification();

lastReturned.item=e;

}public voidadd(E e) {

checkForComodification();

lastReturned= null;if (next == null)

linkLast(e);elselinkBefore(e, next);

nextIndex++;

expectedModCount++;

}public void forEachRemaining(Consumer super E>action) {

Objects.requireNonNull(action);while (modCount == expectedModCount && nextIndex

action.accept(next.item);

lastReturned=next;

next=next.next;

nextIndex++;

}

checkForComodification();

}final voidcheckForComodification() {if (modCount !=expectedModCount)throw newConcurrentModificationException();

}

}//节点的数据结构,包含前后节点的引用和当前节点

private static class Node{

E item;

Nodenext;

Nodeprev;

Node(Node prev, E element, Nodenext) {this.item =element;this.next =next;this.prev =prev;

}

}//返回迭代器

public IteratordescendingIterator() {return newDescendingIterator();

}//因为采用链表实现,所以迭代器很简单

private class DescendingIterator implements Iterator{private final ListItr itr = newListItr(size());public booleanhasNext() {returnitr.hasPrevious();

}publicE next() {returnitr.previous();

}public voidremove() {

itr.remove();

}

}

@SuppressWarnings("unchecked")private LinkedListsuperClone() {try{return (LinkedList) super.clone();

}catch(CloneNotSupportedException e) {throw newInternalError(e);

}

}/*** Returns a shallow copy of this {@codeLinkedList}. (The elements

* themselves are not cloned.)

*

*@returna shallow copy of this {@codeLinkedList} instance*/

publicObject clone() {

LinkedList clone =superClone();//Put clone into "virgin" state

clone.first = clone.last = null;

clone.size= 0;

clone.modCount= 0;//Initialize clone with our elements

for (Node x = first; x != null; x =x.next)

clone.add(x.item);returnclone;

}/*** Returns an array containing all of the elements in this list

* in proper sequence (from first to last element).

*

*

The returned array will be "safe" in that no references to it are

* maintained by this list. (In other words, this method must allocate

* a new array). The caller is thus free to modify the returned array.

*

*

This method acts as bridge between array-based and collection-based

* APIs.

*

*@returnan array containing all of the elements in this list

* in proper sequence*/

publicObject[] toArray() {

Object[] result= newObject[size];int i = 0;for (Node x = first; x != null; x =x.next)

result[i++] =x.item;returnresult;

}/*** Returns an array containing all of the elements in this list in

* proper sequence (from first to last element); the runtime type of

* the returned array is that of the specified array. If the list fits

* in the specified array, it is returned therein. Otherwise, a new

* array is allocated with the runtime type of the specified array and

* the size of this list.

*

*

If the list fits in the specified array with room to spare (i.e.,

* the array has more elements than the list), the element in the array

* immediately following the end of the list is set to {@codenull}.

* (This is useful in determining the length of the list only if

* the caller knows that the list does not contain any null elements.)

*

*

Like the {@link#toArray()} method, this method acts as bridge between

* array-based and collection-based APIs. Further, this method allows

* precise control over the runtime type of the output array, and may,

* under certain circumstances, be used to save allocation costs.

*

*

Suppose {@codex} is a list known to contain only strings.

* The following code can be used to dump the list into a newly

* allocated array of {@codeString}:

*

*

 
 

* String[] y = x.toArray(new String[0]);

*

* Note that {@codetoArray(new Object[0])} is identical in function to

* {@codetoArray()}.

*

*@parama the array into which the elements of the list are to

* be stored, if it is big enough; otherwise, a new array of the

* same runtime type is allocated for this purpose.

*@returnan array containing the elements of the list

*@throwsArrayStoreException if the runtime type of the specified array

* is not a supertype of the runtime type of every element in

* this list

*@throwsNullPointerException if the specified array is null*/@SuppressWarnings("unchecked")public T[] toArray(T[] a) {if (a.length

a=(T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);int i = 0;

Object[] result=a;for (Node x = first; x != null; x =x.next)

result[i++] =x.item;if (a.length >size)

a[size]= null;returna;

}private static final long serialVersionUID = 876323262645176354L;/*** Saves the state of this {@codeLinkedList} instance to a stream

* (that is, serializes it).

*

*@serialDataThe size of the list (the number of elements it

* contains) is emitted (int), followed by all of its

* elements (each an Object) in the proper order.*/

private voidwriteObject(java.io.ObjectOutputStream s)throwsjava.io.IOException {//Write out any hidden serialization magic

s.defaultWriteObject();//Write out size

s.writeInt(size);//Write out all elements in the proper order.

for (Node x = first; x != null; x =x.next)

s.writeObject(x.item);

}/*** Reconstitutes this {@codeLinkedList} instance from a stream

* (that is, deserializes it).*/@SuppressWarnings("unchecked")private voidreadObject(java.io.ObjectInputStream s)throwsjava.io.IOException, ClassNotFoundException {//Read in any hidden serialization magic

s.defaultReadObject();//Read in size

int size =s.readInt();//Read in all elements in the proper order.

for (int i = 0; i < size; i++)

linkLast((E)s.readObject());

}/*** Creates a late-binding

* and fail-fast {@linkSpliterator} over the elements in this

* list.

*

*

The {@codeSpliterator} reports {@linkSpliterator#SIZED} and

* {@linkSpliterator#ORDERED}. Overriding implementations should document

* the reporting of additional characteristic values.

*

* @implNote

* The {@codeSpliterator} additionally reports {@linkSpliterator#SUBSIZED}

* and implements {@codetrySplit} to permit limited parallelism..

*

*@returna {@codeSpliterator} over the elements in this list

*@since1.8*/@Overridepublic Spliteratorspliterator() {return new LLSpliterator(this, -1, 0);

}/**A customized variant of Spliterators.IteratorSpliterator*/

static final class LLSpliterator implements Spliterator{static final int BATCH_UNIT = 1 << 10; //batch array size increment

static final int MAX_BATCH = 1 << 25; //max batch array size;

final LinkedList list; //null OK unless traversed

Node current; //current node; null until initialized

int est; //size estimate; -1 until first needed

int expectedModCount; //initialized when est set

int batch; //batch size for splits

LLSpliterator(LinkedList list, int est, intexpectedModCount) {this.list =list;this.est =est;this.expectedModCount =expectedModCount;

}final intgetEst() {int s; //force initialization

final LinkedListlst;if ((s = est) < 0) {if ((lst = list) == null)

s= est = 0;else{

expectedModCount=lst.modCount;

current=lst.first;

s= est =lst.size;

}

}returns;

}public long estimateSize() { return (long) getEst(); }public SpliteratortrySplit() {

Nodep;int s =getEst();if (s > 1 && (p = current) != null) {int n = batch +BATCH_UNIT;if (n >s)

n=s;if (n >MAX_BATCH)

n=MAX_BATCH;

Object[] a= newObject[n];int j = 0;do { a[j++] = p.item; } while ((p = p.next) != null && j

current=p;

batch=j;

est= s -j;return Spliterators.spliterator(a, 0, j, Spliterator.ORDERED);

}return null;

}public void forEachRemaining(Consumer super E>action) {

Node p; intn;if (action == null) throw newNullPointerException();if ((n = getEst()) > 0 && (p = current) != null) {

current= null;

est= 0;do{

E e=p.item;

p=p.next;

action.accept(e);

}while (p != null && --n > 0);

}if (list.modCount !=expectedModCount)throw newConcurrentModificationException();

}public boolean tryAdvance(Consumer super E>action) {

Nodep;if (action == null) throw newNullPointerException();if (getEst() > 0 && (p = current) != null) {--est;

E e=p.item;

current=p.next;

action.accept(e);if (list.modCount !=expectedModCount)throw newConcurrentModificationException();return true;

}return false;

}public intcharacteristics() {return Spliterator.ORDERED | Spliterator.SIZED |Spliterator.SUBSIZED;

}

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值