数据结构之链表

链表是一种常见的数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。

链表与数组相比具有以下特点:

  1. 链式存储:链表的节点在内存中不需要连续的存储空间,每个节点通过指针将其与下一个节点连接起来。

  2. 动态性:链表的长度可以根据需要进行动态调整。可以方便地在链表头部或尾部插入或删除节点,不需要像数组那样移动其他元素。

  3. 随机访问困难:链表中的元素不具有直接的下标访问方式,需要从头节点开始按顺序遍历到目标节点,因此访问效率较低。

  4. 空间效率相对较低:相对于数组,链表需要额外的指针空间来存储节点间的连接关系,会占用一定的额外存储空间。

链表常见的类型有单链表、双链表和循环链表。

  • 单链表(Single Linked List):每个节点只有一个指向下一个节点的指针。

  • 双链表(Double Linked List):每个节点同时有指向下一个节点和上一个节点的指针,可以实现双向遍历。

  • 循环链表(Circular Linked List):尾节点指向头节点,形成一个环状结构。

链表适用于频繁插入和删除的场景下,尤其是在事先不知道数据大小的情况下。它可以方便地动态调整存储空间,并且插入和删除操作的效率较高。然而,链表的缺点是访问效率相对较低,无法通过下标直接访问元素,需要顺序遍历。因此,在需要快速随机访问元素的情况下,数组可能更合适。

1单链表

 此结构为链式存储:单链表中的元素通过指针将它们连接起来,每个节点包含一个数据域和一个指向下一个节点的指针。也就是说它不连续,无顺序,想找下一个乖乖靠指针噢。

为了方便理解 我们假想一个结构:
在这里插入图片描述

1.因为链表节点的存储位置没有连续要求,所以每个节点可以在内存的任意位置。这意味着链表节点可能分布在不同的内存区域中,不需要连续的物理空间。这也是链表的一个优点,因为它允许动态地分配和释放节点,提供了更灵活的存储空间管理方式。所以他的实际样子我们很难表达,也不知道散落成什么样子,只能假想成这个中规中矩的样子;同时我们应该明白数据结构的美难以刻画,只可意会。

2.一个数据一个指针共同构成了一个节点。通过节点的数据和指针的组合,链表可以存储和操作各种类型的数据,同时保持节点之间的链接关系。

到这里,我们开始着手用程序刻画这种关系;节点呢通常就用结构体实例化来表达,我们先创建一个结构体:

typedef int ListDataType; //方便修改定义的类型
struct SListNode
{
	ListDataType data;
	struct SListNode* index;
};
typedef struct SListNode ListNode;  //老写struct....太麻烦了,给他取个别名。

接下来的话同样测试是老三样:初始化、打印、存数据

1.1 初始化

1.这里我们只需要将指针index指向空即可(不是指向无效地址)
ListNode* plist = NULL;
2.数据data就不必去管:基本数据类型(如整数、字符等),如果你不为其赋初值,它们会被自动初始化为默认值,根据不同的编程语言和编译器而定。通常情况下,这样的默认值是零值(例如 0 或者 ‘\0’)

1.2 打印

打印就得从头到尾遍历一遍,首先虽然链表不支持随机访问,但是头结点我们是知道的,然后顺着头结点的指针找到下一个节点,再通过…,那这里的遍历其实就是顺藤摸瓜。我们使用循环,先建立一个指针指向头结点,然后“顺藤摸瓜”,直到我们摸到了“NULL”(因为最后一个节点的指针指向的是NULL)就打印完成。
注意一下,程序里面的head指针是指向链表头结点的,也是已知的。

void ListPrint(ListNode* head)
{
	ListNode* cur = head;     //创建指针指向头结点
	while (cur != NULL)      
	{
		printf("%d->", cur->data);
		cur = cur->index;
	}
	printf("NULL\n");
}

1.3存数据-尾插

在这里我们要考虑的问题有两个:
1.建立新节点存储数据时候的增容问题。
2.新节点和链表末尾节点的链接问题,主要是要找到最后一个节点的位置。
首先问题一,这里是要分配指定字节大小的空间,得用malloc了吧,开辟以后节点里的data就是新数据了,指针即将成为链表的末位指针,该指向NULL。考虑到头插等一些接口也是要扩容,不如索性写一个扩充节点的函数:

ListNode* AddListNode(ListDateType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));  //开辟一个新节点
	newnode->data = x;      //存入新数据
	newnode->index = NULL;      

	return newnode;
}

接下来我们解决问题二,让扩容前的链表末尾节点的指针指向新节点,我们首先就要找到旧的末尾节点,也没啥好办法,还得是遍历啊,不过这时候我们考虑一下两种情况:1.还没存到数据,那就简单了,把新开辟的节点作为第一个节点就好了。2.就是已经存过数据了,就遍历。
下面就是程序,这里注意一下,你要把一级指针传过来,那形参是二级指针。因为在C语言中,传递指针参数时,是按值传递的方式。如果你的函数中只使用一级指针,函数内对指针的修改不会影响到原来的指针。
而如果你使用二级指针,函数中对指针的修改会直接影响外部传入的指针。

void ListPushBack(ListNode**phead, ListDateType x)  //尾插
{
	ListNode*newnode = AddListNode(x);

	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		//找尾节点的指针
		ListNode* tail = *phead;
		while (tail->index != NULL)
		{
			tail = tail->index;
		}

		//尾节点,链接新节点
		tail->index = newnode;
	}

}

1.4 存数据-头插

头插相比于尾插就容易一点,因为我们不需要遍历去找节点了,我们只需要已知的头结点指针head,暂且称为头指针吧,我们要做的就是,让头指针指向新的节点,让新节点里面的指针指向旧链表的头节点:

void ListPushFront(ListNode**phead, ListDateType x)  //头插
{
	ListNode*newnode = AddListNode(x);   //跟头插一样,增个节点

	newnode->index = *phead;
	*phead = newnode;
}

1.5 删数据-尾删

在进行链表的尾删操作时,我们需要释放最后一个节点的内存空间,同时还需要更新倒数第二个节点的 index 指针,将其指向空,以确保链表仍然保持正确的结构。还是得遍历找到最后的节点指针;尾删呢,我们分为三种情况:1.链表为空;2.只有一个节点;3.多于一个节点;

void ListNodePopBack(ListNode**head)      //尾删
{
	//3种情况  1.空     2.一个节点    3.大于一个节点
	if (*head == NULL)
	{
		return;   //直接结束
	}
	else if ((*head)->index == NULL)
	{
		free(*head);
		*head = NULL;
	}
	else
	{
		ListNode* prev = NULL;
		ListNode*tail = *head;
		while (tail->index != NULL)    //让prev成为倒数第二个节点的指针
		{
			prev = tail;
			tail = tail->index;
		}
		free(tail);
		prev->index = NULL;
	}
}

1.6 删数据-头删

这个又轻松了,free掉*head的空间,让链表第二个节点成为第一个:

void ListNodePopFront(ListNode**head)     //头删
{
	ListNode* next = (*head)->index;
	free(*head);
	*head = next;
}

1.7 在链表指定位置插入

就比如:我想在3的前面插入7,那我是不是应该先找到3所在的节点:
在这里插入图片描述
我们先在头结点处创建一个指针从首位向后面遍历,直到遇到我们想要的数值。

ListNode* ListFind(ListNode* head, ListDateType x)
{
	ListNode* cur = head;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->index;
	}
	return NULL;
}

要注意这个返回的是指针,而非数值,我们可以这样验证:

      ListNode* pNode = ListFind(plist, 5);
	printf("指针的值为:%p\n", (void*)pNode);

当找到的时候,会返回指针地址,找不到就返回0;像这样:
在这里插入图片描述
好了,想要的位置我们已经找到了,接下来应该插入我们想要输入的数据了;我们要考虑一种情况:如果我们就想要头插,是不是直接就用之前的程序就可以了。接下来我们分析一下:我们在头结点创建一个指针prev,然后让这个指针遍历到2所在节点的位置,这时候我们只要让prev->index=newnode;
让newnode->index=pos就可以了
在这里插入图片描述

ListNode*ListInsert(ListNode** head, ListNode* pos, ListDateType x)//在pos位置的前面插入数据x
{
	if (pos == *head)
	{
		ListPushFront(head, x);
	}
	else
	{
		ListNode*newnode = AddListNode(x);
		ListNode*prev = *head;
		while (prev->index!=pos)
		{
			prev = prev->index;
		}
		prev->index = newnode;
		newnode->index = pos;
	}
}

另外说一下这个程序为什么第一个形参用二级指针,第一个形参用一级指针:
这是因为传递链表头节点时,我们需要修改链表头的指向,即需要修改 head 指针本身的值。而对于 pos 参数,我们仅需要获取它的值,不需要修改它的指向。

当我们传递一个一级指针作为参数时,函数内部的修改只会影响到这个指针的副本,不会影响到原始的指针。如果我们想要修改原始指针,就需要传递指针的指针(即二级指针)作为参数,函数内部通过二级指针修改一级指针指向的值。

1.8 删除指定位置的数据

在这里插入图片描述
我们的思路应该是
(1).先在头节点处创建一个指针,遍历到pos(指定位置);
(2).然后让prev节点处的index指向4处节点:prev->index->pos->index;
(3).然后free掉pos所在空间;
注意2和3顺序可不能颠倒,如果先进行3,那pos->index就没了,也就是说pos位置后面的节点就找不着了。

void ListErase(ListNode**head, ListNode* pos)
{
	if (pos == *head)
	{
		ListNodePopFront(head);
	}
	else
	{
		ListNode*prev = *head;
		while (prev->index != pos)
		{
			prev = prev->index;
		}
		prev->index = pos->index;
		free(pos);
	}
}

1.9完整程序

1.头文件程序

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

typedef int ListDateType;
struct SListNode
{
	ListDateType data;
	struct SListNode* index;
};
typedef struct SListNode ListNode;

void ListPrint(ListNode* head);  //打印

void ListPushBack(ListNode**head, ListDateType x);  //尾插

void ListPushFront(ListNode**head, ListDateType x);  //头插

void ListNodePopBack(ListNode**head);      //尾删
void ListNodePopFront(ListNode**head);     //头删

ListNode* ListFind(ListNode* head, ListDateType x); //找到数据所在位置的指针

ListNode*ListInsert(ListNode* head, ListNode* pos, ListDateType x);//在pos位置的前面插入数据x

void ListErase(ListNode**head, ListNode* pos);

2.源文件(函数程序)

#include"List.h"
void ListPrint(ListNode* head)
{
	ListNode* cur = head;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->index;
	}
	printf("NULL\n");
}

ListNode* AddListNode(ListDateType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->index = NULL;

	return newnode;
}
void ListPushBack(ListNode**phead, ListDateType x)  //尾插
{
	ListNode*newnode = AddListNode(x);

	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		//找尾节点的指针
		ListNode* tail = *phead;
		while (tail->index != NULL)
		{
			tail = tail->index;
		}

		//尾节点,链接新节点
		tail->index = newnode;
	}

}

void ListPushFront(ListNode**phead, ListDateType x)  //头插
{
	ListNode*newnode = AddListNode(x);   //跟头插一样,增个节点

	newnode->index = *phead;
	*phead = newnode;
}

void ListNodePopBack(ListNode**head)      //尾删
{
	//3种情况  1.空     2.一个节点    3.大于一个节点
	if (*head == NULL)
	{
		return;   //直接结束
	}
	else if ((*head)->index == NULL)
	{
		free(*head);
		*head = NULL;
	}
	else
	{
		ListNode* prev = NULL;
		ListNode*tail = *head;
		while (tail->index != NULL)
		{
			prev = tail;
			tail = tail->index;
		}
		free(tail);
		prev->index = NULL;
	}
}
void ListNodePopFront(ListNode**head)     //头删
{
	ListNode* next = (*head)->index;
	free(*head);
	*head = next;
}

ListNode* ListFind(ListNode* head, ListDateType x)
{
	ListNode* cur = head;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->index;
	}
	return NULL;
}

ListNode*ListInsert(ListNode** head, ListNode* pos, ListDateType x)//在pos位置的前面插入数据x
{
	if (pos == *head)
	{
		ListPushFront(head, x);
	}
	else
	{
		ListNode*newnode = AddListNode(x);
		ListNode*prev = *head;
		while (prev->index!=pos)
		{
			prev = prev->index;
		}
		prev->index = newnode;
		newnode->index = pos;
	}
}


//删除pos位置的值
void ListErase(ListNode**head, ListNode* pos)
{
	if (pos == *head)
	{
		ListNodePopFront(head);
	}
	else
	{
		ListNode*prev = *head;
		while (prev->index != pos)
		{
			prev = prev->index;
		}
		prev->index = pos->index;
		free(pos);
	}
}

3.部分测试

#include"List.h"

void _testList1()
{
	ListNode* plist = NULL;
	ListPushBack(&plist, 1);
	ListPushBack(&plist, 2);
	ListPushBack(&plist, 3);
	ListPushBack(&plist, 4); 
	ListPushBack(&plist, 5);
	ListPushBack(&plist, 6);
	ListPushFront(&plist, 0);
	ListPrint(plist);
	/*ListNode* pos = ListFind(plist, 3);
		ListInsert(&plist, pos, 30);

	ListPrint(plist);*/
	/*ListInsert(&plist, pos, 7);
	ListPrint(plist);*/
	/*ListNode* pNode = ListFind(plist, 3);
	printf("指针的值为:%p\n", (void*)pNode);
	ListNode* ppNode = ListFind(plist, 45);
	printf("指针的值为:%p\n", (void*)ppNode);*/


}

int main()
{
	_testList1();
	return 0;
}

2.双链表

链表的种类有多种,常用的分为单链表和双链表。又有带头、循环的buff,那我们这样分有8种:
在这里插入图片描述
这里我们把buff叠满,讲一下双向带头循环链表;

2.1 结构介绍

双向带头循环链表是一种特殊类型的链表,它具有以下特点:

  1. 双向性(Doubly Linked):每个节点除了包含指向下一个节点的指针,还有指向前一个节点的指针。这使得在链表中可以方便地进行向前和向后的遍历和操作。

  2. 带头节点(Head Node):在链表的头部添加一个额外的节点作为头节点。头节点不存储数据,仅用于指向第一个节点,这样可以简化链表的操作并且更容易进行插入和删除操作。

  3. 循环性(Circular):链表的最后一个节点的指针指向头节点,形成一个循环结构。这样可以方便实现循环遍历链表,即当遍历到最后一个节点时,直接跳转到头节点进行下一次遍历。
    在这里插入图片描述

双向带头循环链表可以充分利用双向指针的特点,使得在链表中进行插入、删除和遍历操作更加高效和方便。它适用于需要频繁在链表中进行插入和删除操作,并且需要双向遍历链表的场景。
根据我们的图我们先整一个结构体:

typedef int ListDataType;
typedef struct ListNode
{
	struct  ListNode* next;
	struct  ListNode* prev;
	ListDataType data;
}ListNode;

2.2 初始化

我们看到结构体三个元素,图中还有“带头的”,我们的初始化肯定很针对指针这个复杂的东西了,这时候应该只有一个“头”了(如下图),在此之前我们还是先弄一个增容的函数,不然没东西初始化给我就尴尬了。那我们还是让增容里面的指针指向NULL(注意NULL和无效的区别,这里NULL是为了防止无效)
在这里插入图片描述

ListNode* AddListNode(ListDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

增容以后我们要初始化成图中的样子,就是两个指针都指向自己:

ListNode* ListInit()
{
	ListNode*head = AddListNode(0);
	head->next = head;
	head->prev = head;

	return head;
}

2.3 销毁

销毁其实就是顺着head头指针找到其他指针并销毁,这里我们知道这是一个环形链表,我们不能先销毁head指针,这样就找不到其他节点了,所以我们先留着,反正是环形结构,总会遇见的,不如留到最后,刚好也可以作为循环结束的条件,所以用while比较好吧

void ListDestory(ListNode*head)
{
	assert(head);
	ListNode* index = head->next;
	while (index != head)
	{
		ListNode*next = index->next;
		free(index);
		index = next;

	}
	free(head);
	head = NULL;
}

至于这里为什么先用一个断言呢,是因为防止传入空指针,导致无法进入循环,而程序又不报错,不过也要注意断言通常在调试和开发阶段使用,它的目的是帮助程序员发现并修复错误。在发布版本的生产环境中,断言通常会被禁用或移除,以避免中断程序的执行。

2.4 打印

打印和前面的销毁都是需要遍历的,只不过销毁是找到指针并free掉,而打印是找到存储的数据打印出来,道理是一样的。

void ListPrint(ListNode*head)
{
	assert(head);

	ListNode* index = head->next;
	while (index != head)
	{
		printf("%d ", index->data);
		index = index->next;
	}
	printf("\n");
}

2.5 尾插

在这里插入图片描述
图中a4所在的节点就是咱们新加的,新开辟的节点要加入到链表中,就得适应链表中指针的规则。我们按照图中的指针走向进行定义就可以了:

void ListPushBack(ListNode*head, ListDataType x)
{
	assert(head);

	ListNode*tail= head->prev;
	ListNode*newnode = AddListNode(x);

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = head;
	head->prev = newnode;
}

2.6 头插

在这里插入图片描述
这个程序呢,其实也是看图写就好了:

void ListPushFront(ListNode*head, ListDataType x)
{
	assert(head);

	ListNode* newnode = AddListNode(x);
	newnode->next = head->next;
	head->next = newnode;
	newnode->prev = head;
}

2.7 尾删/头删

这个链表的删除其实就是指针的销毁和指针的改向;

//尾删
void ListPopBack(ListNode*head)
{
	assert(head->next != head);

	ListNode*tail = head->prev;
	ListNode*prev = tail->prev;

	prev->next = head;
	head->prev = prev;

	free(tail);
	tail = NULL;

	/*assert(head);
	ListErase(head->next);*/
}
//头删
void ListPopFront(ListNode*head)
{
	assert(head->next != head);

	ListNode* first = head->next;
	ListNode*second = first->next;
	head->next = second;
	second->prev = head;

	free(first);
	first = NULL;

	/*assert(head);
	ListErase(head->next);*/
}

2.8 查找

这个查找还是需要遍历一下去找到我们想要的值,当然返回的是指向该节点的指针。如果没有找到的话就返回空;

ListNode* ListFind(ListNode*head, ListDataType x)
{
	assert(head);
	ListNode* index = head->next;
	while (index != head)
	{
		if (index->data == x)
		{
			return index;
		}
		index = index->next;
	}
	return NULL;
}

注意因为返回的是指针,我们测试的时候可以测试指针的地址,如果找不到就返回0,找到的话会有一个地址,这样测试:

ListNode* pos=ListFind(list, 3);
	printf("指针的值为:%p\n", (void*)pos);

2.9 在任意位置(pos查找)之前插入

在这里插入图片描述
这次咱们还是看图写程序,断言pos是防止pos为空

void ListInsert(ListNode* pos, ListDataType x)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode*newnode = AddListNode(x);

	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;

}

2.10 删除pos位置的值

这里也是先使用查找函数找到pos的位置,然后整理指针的指向问题,注意不要先free掉pos,因为pos如果先没了,那他后面的节点就“失联”了。

void ListErase(ListNode*pos)
{
	assert(pos);

	ListNode*prev = pos->prev;
	ListNode*next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);
}

1.12完整程序

1.12.1头文件
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

typedef int ListDataType;

//带头双向循环==任意位置插入删除数据都是0(1)
typedef struct ListNode
{
	struct  ListNode* next;
	struct  ListNode* prev;
	ListDataType data;
}ListNode;

ListNode* ListInit();
void ListDestory(ListNode*head);
void ListPrint(ListNode*head);

void ListPushBack(ListNode*head, ListDataType x);
void ListPushFront(ListNode*head, ListDataType x);

void ListPopBack(ListNode*head);
void ListPopFront(ListNode*head);

ListNode* ListFind(ListNode*head0, ListDataType x);
void ListInsert(ListNode* pos, ListDataType x);
void ListErase(ListNode*pos);
1.12.2源文件
#include"DouList.h"


ListNode* AddListNode(ListDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

ListNode* ListInit()
{
	ListNode*head = AddListNode(0);
	head->next = head;
	head->prev = head;

	return head;
}
void ListDestory(ListNode*head)
{
	assert(head);
	ListNode* index = head->next;
	while (index != head)
	{
		ListNode*next = index->next;
		free(index);
		index = next;

	}
	free(head);
	head = NULL;
}
void ListPrint(ListNode*head)
{
	assert(head);

	ListNode* index = head->next;
	while (index != head)
	{
		printf("%d ", index->data);
		index = index->next;
	}
	printf("\n");
}

void ListPushBack(ListNode*head, ListDataType x)
{
	assert(head);

	ListNode*tail= head->prev;
	ListNode*newnode = AddListNode(x);

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = head;
	head->prev = newnode;

	//ListInsert(head, x);
}
void ListPushFront(ListNode*head, ListDataType x)
{
	assert(head);

	ListNode* newnode = AddListNode(x);
	newnode->next = head->next;
	head->next = newnode;
	newnode->prev = head;

	//ListInsert(head->next, x);
}



void ListPopBack(ListNode*head)
{
	assert(head->next != head);

	ListNode*tail = head->prev;
	ListNode*prev = tail->prev;

	prev->next = head;
	head->prev = prev;

	free(tail);
	tail = NULL;

	/*assert(head);
	ListErase(head->next);*/
}
void ListPopFront(ListNode*head)
{
	assert(head->next != head);

	ListNode* first = head->next;
	ListNode*second = first->next;
	head->next = second;
	second->prev = head;

	free(first);
	first = NULL;

	/*assert(head);
	ListErase(head->next);*/
}

ListNode* ListFind(ListNode*head, ListDataType x)
{
	assert(head);
	ListNode* index = head->next;
	while (index != head)
	{
		if (index->data == x)
		{
			return index;
		}
		index = index->next;
	}
	return NULL;
}

//pos位置之前插入x
void ListInsert(ListNode* pos, ListDataType x)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode*newnode = AddListNode(x);

	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;

}
void ListErase(ListNode*pos)
{
	assert(pos);

	ListNode*prev = pos->prev;
	ListNode*next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);
}
1.12.3 测试文件(部分)
#include"DouList.h"
void TestList()
{
	ListNode* list = ListInit();
	ListPushBack(list, 1);
	ListPushBack(list, 2);
	ListPushBack(list, 3);
	ListPushBack(list, 4);
	ListPrint(list);

	ListPushBack(list, 5);
	ListPushFront(list, 0);
	ListPrint(list);

	ListPopBack(list);
	ListPopFront(list);
	ListPrint(list);


	ListNode* pos=ListFind(list, 3);
	printf("指针的值为:%p\n", (void*)pos);

	ListDestory(list);
}

int main()
{
	TestList();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值