小肥柴慢慢手写数据结构(C篇)(4-1 队列Queue)

目录

4-1 队列概念

如下图,(一般)队列相当于一个管道,元素类似于管道里的小球;规定仅允许从管道的末尾放入小球,仅允许从管道的头部取出小球;这样就自然形成了一个“先进先出的结构”(FIFO),让所有小球在管道中排队。
在这里插入图片描述
由于需要存储“一组”数据,队列所以既可以选择使用链表实现,也可以选择使用数组实现;其中链表实现相对简单,因为只需要一个单向链表,保留删除头结点(出队列)和尾插(入队列)两个操作就能复用之前的很多代码了;若使用数组实现则有一个容积问题和回转问题,下面我们分别实现这两套方案的代码。

4-2 基于链表的实现

关键操作:
(1)Enqueue( ) 入队列 :链表尾插
(2)Dequeue( ) 出队列 :删除头结点
上代码:
(1)QueueLink.h
这里我们简单实现了链表中尾插和删头两个操作,也可以复用之前实现的单链表;所有函数名字清晰,不做解释。

typedef int ElementType;
#ifndef _Queue_Link_h
#define _Queue_Link_h
typedef struct Node{
	ElementType data;
	struct Node *next;
}Node;

struct QueueLink{
	Node *Front;
	Node *Rear;
};

typedef struct QueueLink *Queue; 

int IsEmpty( Queue Q );
Queue CreateQueue( );
void DisposeQueue( Queue Q );
void Enqueue( ElementType X, Queue Q );
ElementType Front( Queue Q );
void Dequeue( Queue Q );
ElementType FrontAndDequeue( Queue Q );
#endif

(2)QueueLink.c

#include <stdio.h>
#include <stdlib.h>

#include "QueueLink.h"

int IsEmpty( Queue Q ){
	return Q->Front == NULL;
}

Queue CreateQueue( ){
    Queue Q = malloc(sizeof(struct QueueLink));
    if( Q == NULL )
    	printf( "CreateQueue: Out of space!!!\n" );
	else
		Q->Front = Q->Rear = NULL;
	return Q;
}

void Enqueue( ElementType X, Queue Q ){
	Node *node = (Node *)malloc(sizeof(Node));
	if(node == NULL)
		printf( "Enqueue: Out of space!!!\n" );
	else{
		node->data = X;
		node->next = NULL;
		if(Q->Rear==NULL){
			Q->Front = node;
			Q->Rear = node;
		} else {
			Q->Rear->next = node;
			Q->Rear = node;
		}
	}
}

void Dequeue( Queue Q ){
    if( IsEmpty( Q ) )
        printf( "Empty queue\n" );
    else {
    	Node * node = Q->Front;
		if(Q->Front == Q->Rear)
			Q->Front = Q->Rear = NULL;
		else
			Q->Front = Q->Front->next;
		free(node);
    }
}

ElementType Front( Queue Q ){
    if( !IsEmpty( Q ) )
        return Q->Front->data;
    printf( "Empty queue\n" );
    return 0;
}

ElementType FrontAndDequeue( Queue Q ){
	int data = 0;
    if( IsEmpty( Q ) )
        printf( "Empty queue\n" );
    else {
    	Node *node = Q->Front;
    	data = node->data;
		if(Q->Front == Q->Rear)
			Q->Front = Q->Rear = NULL;
		else
			Q->Front = Q->Front->next;
		free(node);
    }
    return data;
}

void DisposeQueue( Queue Q ){
	if(Q != NULL){
		Node *cur = Q->Front;
		while(cur->next != NULL){
			Node *node = cur;
			free(node);
			cur = cur->next;
		}
		Q->Front = NULL;
		Q->Rear = NULL;
		free(Q);
		Q = NULL;
	}
}

void showQueue(Queue Q){
	Node *cur = Q->Front;
	while(cur->next != NULL){
		printf(" %d", cur->data);
		cur = cur->next;
	}
	printf(" %d\n", cur->data);
}

这里的dequeue( )也可以改为返回被删除的头结点元素,按需实现。
(3)测试文件Main.c

#include <stdio.h>
#include <stdlib.h>

#include "QueueLink.h"

int main(int argc, char *argv[]) {
	int i, size = 5;
	Queue Q = CreateQueue();
	printf( "Enqueue start:\n" );
	for(i=0; i<size; i++)
		Enqueue(i+1, Q);
	
	printf("\nShow Queue: ");
	showQueue(Q);
	
	printf("\nFront Element:%d", Front(Q));
	printf("\nShow Queue: ");
	showQueue(Q);
	
	printf("\nFrontAndDequeue Element:%d", FrontAndDequeue(Q));
	printf("\nShow Queue: ");
	showQueue(Q);
	
	printf("\nDequeue:");
	Dequeue(Q);
	printf("\nShow Queue: ");
	showQueue(Q);
	
	DisposeQueue(Q);
	
	return 0;
}

【测试结果】
在这里插入图片描述

4-3 基于数组的实现(循环队列)

对于基于数组的队列来讲,有以下几个问题需要明确:(对比ArrayList,其实也可以用ArrayList为蓝本来实现队列)
(1)标记数组中最后一个有效元素的索引为rear,此时如果添加新元素,那么rear++,且将新元素放入已经更新的arr[rear]中;
(2)标记数组中第一个有效元素的索引为front,此时如果删除元素,则front++,也就是“避开”了旧头元素;
在这里插入图片描述
(3)假设设定数组的容量是capacity,如果在元素填充满后继续添加元素,那么必然rear出现“回绕”,也就是重新从index=0开始,后续继续添加就是覆盖了。
(4)同理如果rear<front,说明已经回绕了,假设front==size-1,此时出队列front也需要回绕。

我认为初学者并不需要画出环形图来表达以上过程,一个简单的线形图足矣帮助理解问题,特别是对两个基准front和rear的移动,综上分析:
(1)对front和rear两个标记,解决回绕是一个共性问题;
(2)循环队列的核心就是front和rear;
(3)检测队列为空是非常重要的;
(4)每个人对front和rear的实现细节上可能存在差异。
接下来上代码:
(1)QueueArray.h

typedef int ElementType;
#ifndef _Queue_Array_h
#define _Queue_Array_h

struct QueueRecord{
    int Capacity;
    int Front;
    int Rear;
    int Size;
    ElementType *Array;
};
typedef struct QueueRecord *Queue;

int IsEmpty( Queue Q );
int IsFull( Queue Q );
Queue CreateQueue( int MaxElements );
void DisposeQueue( Queue Q );
void MakeEmpty( Queue Q );
void Enqueue( ElementType X, Queue Q );
ElementType Front( Queue Q );
void Dequeue( Queue Q );
ElementType FrontAndDequeue( Queue Q );

#endif  /* _Queue_h */

(2)QueueArray.c

#include <stdio.h>
#include <stdlib.h>

#include "QueueArray.h"

#define MinQueueSize ( 5 )

int IsEmpty( Queue Q ){
	return Q->Size == 0;
}

int IsFull( Queue Q ){
    return Q->Size == Q->Capacity;
}

Queue CreateQueue( int MaxElements ){
    Queue Q = NULL;
    if( MaxElements < MinQueueSize ){
    	printf("Queue size is too small\n");
    	return NULL;
	}

    Q = malloc( sizeof( struct QueueRecord ) );
    if( Q == NULL ){
    	printf( "Out of space!!!\n" );
    	return NULL;
	}

    Q->Array = malloc( sizeof( ElementType ) * MaxElements );
    if( Q->Array == NULL ){
	    printf( "Out of space!!!\n" );
	    free(Q);
	    return NULL;
	}

    Q->Capacity = MaxElements;
    MakeEmpty( Q );

    return Q;
}

void MakeEmpty( Queue Q ){
    Q->Size = 0;
    Q->Front = 1;
    Q->Rear = 0;
}

void DisposeQueue( Queue Q ){
    if( Q != NULL ){
        free( Q->Array );
        free( Q );
    }
}


static int Succ( int Value, Queue Q ){
    if( ++Value == Q->Capacity )
        Value = 0;
    return Value;
}

void Enqueue( ElementType X, Queue Q ){
    if( IsFull( Q ) ){
    	printf( "Full queue\n" );
	} else {
        Q->Size++;
        Q->Rear = Succ( Q->Rear, Q );
        Q->Array[ Q->Rear ] = X;
    }
}

ElementType Front( Queue Q ){
    if( !IsEmpty( Q ) )
        return Q->Array[ Q->Front ];
    printf( "Empty queue\n" );
    return 0;
}

void Dequeue( Queue Q ){
    if( IsEmpty( Q ) )
        printf( "Empty queue\n" );
    else {
        Q->Size--;
        Q->Front = Succ( Q->Front, Q );
    }
}

ElementType FrontAndDequeue( Queue Q ){
    ElementType X = 0;

    if( IsEmpty( Q ) )
        printf( "Empty queue\n" );
    else {
        Q->Size--;
        X = Q->Array[ Q->Front ];
        Q->Front = Succ( Q->Front, Q );
    }
    return X;
}

void showQueue(Queue Q){
	int cur = Q->Front;
	while(cur != Q->Rear){
		printf(" %d", Q->Array[cur]);
		cur = Succ(cur, Q);
	}
	printf(" %d\n", Q->Array[cur]);
}

有几个细节需要思考:
<1> 对于回绕,我们抽象出一个共性工具函数Succ()

static int Succ( int Value, Queue Q ){
    if( ++Value == Q->Capacity )
        Value = 0;
    return Value;
}

这里入参value其实就是index,如果++之后到达capacity,就应该回绕了(value=0);否则什么都不做。
<2> 初始化函数CreateQueue( )中,使用MakeEmpty( )来初始化front和rear

void MakeEmpty( Queue Q ){
    Q->Size = 0;
    Q->Front = 1;
    Q->Rear = 0;
}

用size来判断empty和full

int IsEmpty( Queue Q ){
	return Q->Size == 0;
}

int IsFull( Queue Q ){
    return Q->Size == Q->Capacity;
}

而front和rear不参与以上两个函数,这里是一个小trick:原书作者希望使用rear=front-1来判空,以满足某些程序设计人员依靠基准来隐式推算类似队列大小等数据,但实际上我们的模仿黑皮书的代码并不是完美的,市面上还有很多别的形式的实现,特别对于回绕,使用%计算:
1> 首先front=rear=0初始化;
2> 接着使用取模运算实现回转

Q->rear = (Q->rear+1)%MAXSIZE; //入队
Q->front = (Q->front+1)%MAXSIZE; //出队

3)还能使用两个基准计算元素个数,从而不用维护size属性(有优势也有劣势,优势就是节省了一点点空间,劣势就是每次获取size都要重新计算一遍)

//返回长度
int getLenth(LoopQueue *Q) {
	return (Q->rear - Q->front + MAXSIZE)%MAXSIZE;
}

有兴趣的同学,可以参考文献[1]重新改写下,万事皆有两面性,建议再看看参考文献[2],也重写一遍;还是那句话:根据具体需要适度改进数据结构。且后续使用数据结构的时候,除非性能压榨,否则是不会改动已经设计好的数据结构的;当然我对黑皮书中这段代码的描述也并不是很满意,如果说不使用%运算是因为其相对效率不高(真的只是相对的,并没有那么不堪),那这种设计还是可以接受的。
(3)Main.c

#include <stdio.h>
#include <stdlib.h>

#include "QueueArray.h"

int main(int argc, char *argv[]) {
	int i, size = 5;
	Queue Q = CreateQueue(size);
	printf( "Enqueue start:\n" );
	for(i=0; i<size; i++)
		Enqueue(i+1, Q);
		
	printf("\nShow Queue: ");
	showQueue(Q);
	
	printf("Front Element:%d\n", Front(Q));
	printf("\nShow Queue: ");
	showQueue(Q);
	
	printf("FrontAndDequeue Element:%d\n", FrontAndDequeue(Q));
	printf("\nShow Queue: ");
	showQueue(Q);
	
	printf("Dequeue:\n");
	Dequeue(Q);
	printf("\nShow Queue: ");
	showQueue(Q);
	
	return 0;
}

【测试结果】
在这里插入图片描述

4-4 严版教材的实现

1.单链队列
(1)c3-2.h

 // c3-2.h 单链队列--队列的链式存储结构
 typedef struct QNode
 {
   QElemType data;
   QNode *next;
 }*QueuePtr;

 struct LinkQueue
 {
   QueuePtr front,rear; // 队头、队尾指针
 };

(2)bo3-2.cpp,相关实现也非常简单

 // bo3-2.cpp 链队列(存储结构由c3-2.h定义)的基本操作(9个)
 Status InitQueue(LinkQueue &Q)
 { // 构造一个空队列Q
   if(!(Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode))))
     exit(OVERFLOW);
   Q.front->next=NULL;
   return OK;
 }

 Status DestroyQueue(LinkQueue &Q)
 { // 销毁队列Q(无论空否均可)
   while(Q.front)
   {
     Q.rear=Q.front->next;
     free(Q.front);
     Q.front=Q.rear;
   }
   return OK;
 }

 Status ClearQueue(LinkQueue &Q)
 { // 将Q清为空队列
   QueuePtr p,q;
   Q.rear=Q.front;
   p=Q.front->next;
   Q.front->next=NULL;
   while(p)
   {
     q=p;
     p=p->next;
     free(q);
   }
   return OK;
 }

 Status QueueEmpty(LinkQueue Q)
 { // 若Q为空队列,则返回TRUE,否则返回FALSE
   if(Q.front==Q.rear)
     return TRUE;
   else
     return FALSE;
 }

 int QueueLength(LinkQueue Q)
 { // 求队列的长度
   int i=0;
   QueuePtr p;
   p=Q.front;
   while(Q.rear!=p)
   {
     i++;
     p=p->next;
   }
   return i;
 }

 Status GetHead(LinkQueue Q,QElemType &e)
 { // 若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR
   QueuePtr p;
   if(Q.front==Q.rear)
     return ERROR;
   p=Q.front->next;
   e=p->data;
   return OK;
 }

 Status EnQueue(LinkQueue &Q,QElemType e)
 { // 插入元素e为Q的新的队尾元素
   QueuePtr p;
   if(!(p=(QueuePtr)malloc(sizeof(QNode)))) // 存储分配失败
     exit(OVERFLOW);
   p->data=e;
   p->next=NULL;
   Q.rear->next=p;
   Q.rear=p;
   return OK;
 }

 Status DeQueue(LinkQueue &Q,QElemType &e)
 { // 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR
   QueuePtr p;
   if(Q.front==Q.rear)
     return ERROR;
   p=Q.front->next;
   e=p->data;
   Q.front->next=p->next;
   if(Q.rear==p)
     Q.rear=Q.front;
   free(p);
   return OK;
 }

 Status QueueTraverse(LinkQueue Q,void(*vi)(QElemType))
 { // 从队头到队尾依次对队列Q中每个元素调用函数vi()。一旦vi失败,则操作失败
   QueuePtr p;
   p=Q.front->next;
   while(p)
   {
     vi(p->data);
     p=p->next;
   }
   printf("\n");
   return OK;
 }

2.循环队列
(1)c3-3.h 队列的顺序存储结构(可用于循环队列和非循环队列)

 #define MAXQSIZE 5 // 最大队列长度(对于循环队列,最大队列长度要减1)
 struct SqQueue
 {
   QElemType *base; // 初始化的动态分配存储空间
   int front; // 头指针,若队列不空,指向队列头元素
   int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
 };

(2)bo3-3.cpp 循环队列,这里多了一个base基址,给后续拓展操作流出尾巴

 // bo3-3.cpp 循环队列(存储结构由c3-3.h定义)的基本操作(9个)
 Status InitQueue(SqQueue &Q)
 { // 构造一个空队列Q
   Q.base=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
   if(!Q.base) // 存储分配失败
     exit(OVERFLOW);
   Q.front=Q.rear=0;
   return OK;
 }

 Status DestroyQueue(SqQueue &Q)
 { // 销毁队列Q,Q不再存在
   if(Q.base)
     free(Q.base);
   Q.base=NULL;
   Q.front=Q.rear=0;
   return OK;
 }

 Status ClearQueue(SqQueue &Q)
 { // 将Q清为空队列
   Q.front=Q.rear=0;
   return OK;
 }

 Status QueueEmpty(SqQueue Q)
 { // 若队列Q为空队列,则返回TRUE,否则返回FALSE
   if(Q.front==Q.rear) // 队列空的标志
     return TRUE;
   else
     return FALSE;
 }

 int QueueLength(SqQueue Q)
 { // 返回Q的元素个数,即队列的长度
   return(Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
 }

 Status GetHead(SqQueue Q,QElemType &e)
 { // 若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR
   if(Q.front==Q.rear) // 队列空
     return ERROR;
   e=*(Q.base+Q.front);
   return OK;
 }

 Status EnQueue(SqQueue &Q,QElemType e)
 { // 插入元素e为Q的新的队尾元素
   if((Q.rear+1)%MAXQSIZE==Q.front) // 队列满
     return ERROR;
   Q.base[Q.rear]=e;
   Q.rear=(Q.rear+1)%MAXQSIZE;
   return OK;
 }

 Status DeQueue(SqQueue &Q,QElemType &e)
 { // 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK;否则返回ERROR
   if(Q.front==Q.rear) // 队列空
     return ERROR;
   e=Q.base[Q.front];
   Q.front=(Q.front+1)%MAXQSIZE;
   return OK;
 }

 Status QueueTraverse(SqQueue Q,void(*vi)(QElemType))
 { // 从队头到队尾依次对队列Q中每个元素调用函数vi().一旦vi失败,则操作失败
   int i;
   i=Q.front;
   while(i!=Q.rear)
   {
     vi(*(Q.base+i));
     i=(i+1)%MAXQSIZE;
   }
   printf("\n");
   return OK;
 }

(3)顺序队列:这个扩容操作一言难尽

 // bo3-4.cpp 顺序队列(非循环,存储结构由c3-3.h定义)的基本操作(9个)
 Status InitQueue(SqQueue &Q)
 { // 构造一个空队列Q
   Q.base=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
   if(!Q.base) // 存储分配失败
     exit(OVERFLOW);
   Q.front=Q.rear=0;
   return OK;
 }

 Status DestroyQueue(SqQueue &Q)
 { // 销毁队列Q,Q不再存在
   if(Q.base)
     free(Q.base);
   Q.base=NULL;
   Q.front=Q.rear=0;
   return OK;
 }

 Status ClearQueue(SqQueue &Q)
 { // 将Q清为空队列
   Q.front=Q.rear=0;
   return OK;
 }

 Status QueueEmpty(SqQueue Q)
 { // 若队列Q为空队列,则返回TRUE,否则返回FALSE
   if(Q.front==Q.rear) // 队列空的标志
     return TRUE;
   else
     return FALSE;
 }

 int QueueLength(SqQueue Q)
 { // 返回Q的元素个数,即队列的长度
   return(Q.rear-Q.front);
 }

 Status GetHead(SqQueue Q,QElemType &e)
 { // 若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR
   if(Q.front==Q.rear) // 队列空
     return ERROR;
   e=*(Q.base+Q.front);
   return OK;
 }

 Status EnQueue(SqQueue &Q,QElemType e)
 { // 插入元素e为Q的新的队尾元素
   if(Q.rear>=MAXQSIZE)
   { // 队列满,增加1个存储单元
     Q.base=(QElemType *)realloc(Q.base,(Q.rear+1)*sizeof(QElemType));
     if(!Q.base) // 增加单元失败
       return ERROR;
   }
   *(Q.base+Q.rear)=e;
   Q.rear++;
   return OK;
 }

 Status DeQueue(SqQueue &Q,QElemType &e)
 { // 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR
   if(Q.front==Q.rear) // 队列空
     return ERROR;
   e=Q.base[Q.front];
   Q.front=Q.front+1;
   return OK;
 }

 Status QueueTraverse(SqQueue Q,void(*vi)(QElemType))
 { // 从队头到队尾依次对队列Q中每个元素调用函数vi()。一旦vi失败,则操作失败
   int i;
   i=Q.front;
   while(i!=Q.rear)
   {
     vi(*(Q.base+i));
     i++;
   }
   printf("\n");
   return OK;
 }

(4)还有链栈结构,但我觉得这个结构并不是那么的实用,就不贴出来了,其实上面的非循环队列的实现,是没有必要封装的那么复杂的,完全可以用最简单数组和两个基准去实现其核心功能,简单的才是最好的,这也是我看过Linux内核代码以及对《啊哈,算法》这本书作者思想的理解,数据结构本身就是为了更好的实现程序算法而服务的,千万不能陷入为了数据结构和设计数据结构的小路。

4-5 Linux代码中Queue案例

这里的典型例子就是kfifo,网上大多数帖子都已2.6版本为例做代码讲解。具体信息如下(截图,侵删):
在这里插入图片描述
相关代码如下:

//根据给定buffer创建一个kfifo
  struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size,
                  gfp_t gfp_mask, spinlock_t *lock);
  //给定size分配buffer和kfifo
  struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask,
                   spinlock_t *lock);
  //释放kfifo空间
  void kfifo_free(struct kfifo *fifo);
  //向kfifo中添加数据
  unsigned int kfifo_put(struct kfifo *fifo,
                 const unsigned char *buffer, unsigned int len);

  //从kfifo中取数据
  unsigned int kfifo_get(struct kfifo *fifo,
                 unsigned char *buffer, unsigned int len);
 //获取kfifo中有数据的buffer大小 
 unsigned int kfifo_len(struct kfifo *fifo);

实际到了3.9中:

/**
 * kfifo_init - initialize a fifo using a preallocated buffer
 * @fifo: the fifo to assign the buffer
 * @buffer: the preallocated buffer to be used
 * @size: the size of the internal buffer, this have to be a power of 2
 *
 * This macro initialize a fifo using a preallocated buffer.
 *
 * The numer of elements will be rounded-up to a power of 2.
 * Return 0 if no error, otherwise an error code.
 */
#define kfifo_init(fifo, buffer, size) \
({ \
	typeof((fifo) + 1) __tmp = (fifo); \
	struct __kfifo *__kfifo = &__tmp->kfifo; \
	__is_kfifo_ptr(__tmp) ? \
	__kfifo_init(__kfifo, buffer, size, sizeof(*__tmp->type)) : \
	-EINVAL; \
})

进一步看细节

int __kfifo_init(struct __kfifo *fifo, void *buffer,
		unsigned int size, size_t esize)
{
	size /= esize;

	size = roundup_pow_of_two(size);

	fifo->in = 0;
	fifo->out = 0;
	fifo->esize = esize;
	fifo->data = buffer;

	if (size < 2) {
		fifo->mask = 0;
		return -EINVAL;
	}
	fifo->mask = size - 1;

	return 0;
}

这里的in和out不就是对应rear和front吗?

unsigned int __kfifo_in(struct __kfifo *fifo,
		const void *buf, unsigned int len)
{
	unsigned int l;

	l = kfifo_unused(fifo);
	if (len > l)
		len = l;

	kfifo_copy_in(fifo, buf, len, fifo->in);
	fifo->in += len;
	return len;
}

其实还有一个常见的例子:Workqueue,基于链表的queue,有兴趣的同学可以自己查看下参考文献[7]/[8]。

小结:使用queue的目的就是让元素(事件/作业/任务)排队等待处理,因为它仅有一个出口和一个入口。

[1] 数据结构(C语言)-循环队列基本操作
[2] 数据结构(C语言)-链队列基本操作
[3] Linux内核中常用的数据结构和算法
[4] 0Linux 内核:匠心独运之无锁环形队列
[5] Linux内核数据结构之kfifo详解
[6] linux内核之Kfifo环形队列
[7] Linux-workqueue讲解(Linux3.9内核)
[8] Linux内核中的Workqueue机制分析

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值