【数据结构初阶】单链表&双向带头循环链表

本文详细介绍了单链表和双向带头循环链表的概念、结构及实现方法,包括动态申请节点、插入、删除、查找等基本操作,并提供了C语言的代码实现。文章强调了链表结构的特性,如逻辑连续、物理非连续,以及不同链表结构在实际应用中的选择。
摘要由CSDN通过智能技术生成

⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:数据结构初阶
⭐代码仓库:Data Structure
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!


前言

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。


一、链表

1、链表的结构

在这里插入图片描述
注意:1.链式结构在逻辑上是连续的,但在物理上不一定连续。
2.现实中的结点一般都是从堆上申请出来的。
3.从堆上申请空间,是按照一定策略来分配的,两次申请的空间可能连续也可能不连续。

2、链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来我们就讲解一下这两种最常用的链表结构。


一、单链表的实现

1、概念

无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。


2、实现

(1)三板斧

老规矩,三板斧是必要的,我们进行书写代码的时候,是需要三板斧,也就是创建两个源文件(SList.c、test.c)和一个头文件(SList.h),有利于我们代码的封装,更加地简洁明了。
在这里插入图片描述


(2)单链表结构

在这里插入图片描述
大家看了这个结构,是不是感觉很不可思议,这结构体里咋还套一个自身结构体,怎么跟递归一样呢?其实,我们讲的单链表就像一个火车一样,车厢内部前半截存数据,后半截存地址,存的就是下个结构体的地址,我们形象地理解为指针指向下一个结点。


(3)动态申请一个结点

这个函数是为了我们后续需要申请newnode结点所创立的,进行函数的封装更加便捷。
这里我们用到了malloc来动态申请一个结点的结构体字节大小,同时也是为了防止野指针,将新结点的next置为空。
在这里插入图片描述


(4)尾插

尾插,顾名思义是要在尾部插入一个新的结点,看似很难,不知道末尾结点在哪,所以这就需要我们联想到之前写的顺序表的实现了,顺序表的结构是+1+1一直到加到末尾,也就是正好到size了。那我们这个单链表也一样,我们遍历链表到尾部为NULL即可。
在这里插入图片描述


(5)头插

头插其实并不难,我们新建立一个newnode的结点,将它的next指向头,头被newnode替代。
在这里插入图片描述


(6)小知识(二级指针)

这里大家可能会疑惑,为什么这里用的是二级指针,这么恐怖??那我们就要好好聊一聊指针了。众所周知,一级指针是接受传的参数的地址,而二级指针是接受的是一级指针的地址,我们由前面知道链表是有两个域的,一个数据域,一个指针域,指针域存的是地址,我们要改变整个结构体的结构,也就是说我将结点插入以后,指针存放地址需要改变,也就是一级指针的地址发生改变,那么就需要传一级指针的地址过去,接收的就是二级指针了。


(7)尾删

这里我们用到了指针的技巧,我们新创立两个指针,prev和tail,tail往后遍历,prev去存tail,到最后释放尾结点,prev的next置为空,我们上图解释:
在这里插入图片描述
那我们上代码:
在这里插入图片描述


(8)头删

头删那可是太简单了,找一个结点Next记录一下头的next,直接将头free掉,然后新的头为那个记录原来头的next的结点Next了。
在这里插入图片描述


(9)查找

查找那可是太简单了,我们知道一个值,直接遍历链表进行对比值即可,匹配到了就返回找到的结点,匹配不到就继续找,找不到就返回空即可。
在这里插入图片描述


(10)指定插入(前和后)

这里指定插入往前插入和往后插入我么放在一起进行讲解,是因为这两个插入的原理大致是相同的,只需要做一点点小的变换即可。
先讲一讲前插,前插相对于后插来讲是比较复杂的,因为需要考虑头结点的问题,如果遇到头结点则就是头插,不是头结点的话就需要进行遍历链表,找到我们需要插入的那个位置,再它之前进行插入,我们看图分析:
在这里插入图片描述
上代码:
在这里插入图片描述
再讲讲后插,后插看似很简单,因为它不需要考虑头结点,直接往后插入即可,但是呢,看似简单实则细节,尾插的话是需要考虑指向的顺序的,我们需要新结点先指向我们需要插入结点的next,再将要擦黑的结点的next指向新的结点,为什么要考虑顺序呢?原因是因为我们只记录了需要被插入结点的位置,如果我们先进行被插入结点指向新结点然后再新结点指向需要被插入结点的下一个结点,发现原本要被插入的结点的next变成newnode了,找不到原本链表中的下一个结点了!所以这就是顺序的重要性,我们看图:
在这里插入图片描述
上代码:
在这里插入图片描述


(11)指定删除(前和后)

删除结点之前和删除结点之后是大致类似的,我们放在一起讲。
指定删除当前结点其实很简单,只需要前一个结点指向后后面结点即可,我们上图和上代码:
在这里插入图片描述
在这里插入图片描述
删除后面一个其实也很简单,我们只需要记录当前结点以及后一个结点,当前结点直接指向后结点即可。
在这里插入图片描述
在这里插入图片描述


(12)销毁链表

其实就是将链表清空即可,我们只需要遍历链表将该结点前一个进行free即可,直接上代码:
在这里插入图片描述


3、原码

SList.h:

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

typedef int SLTDataType;

typedef struct SListNode {
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

//打印
void SListPrint(SLTNode* phead);
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SListPopBack(SLTNode** pphead);
//头删
void SListPopFront(SLTNode** pphead);
//查找
SLTNode* SListFind(SLTNode* pphead, SLTDataType x);
//指插(在pos位置之前去插入一个节点)
void SListInsertFront(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//指插2.0(在pos位置之后去插入一个节点)
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//指删
void SListEraseFront(SLTNode** pphead, SLTNode* pos);
//指删2.0
void SListEraseAfter(SLTNode* pos);
//销毁
void SListDestory(SLTNode** pphead);

SList.c:

#include"SList.h"

//增加一个节点
SLTNode* BuyListNode(SLTDataType x) {
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
}

//打印
void SListPrint(SLTNode* phead) {
	SLTNode* cur = phead;
	while (cur != NULL) {
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	/*SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;*/

	SLTNode* newnode = BuyListNode(x);

	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {
		//找到尾结点
		SLTNode* tail = *pphead;
		//往后遍历
		while (tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

//头插
void SListPushFront(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	/*SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;*/

	SLTNode* newnode = BuyListNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}

//尾删
void SListPopBack(SLTNode** pphead) {
	assert(pphead);
	/*if (*pphead == NULL) {
		return;
	}*/
	assert(*pphead != NULL);

	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next != NULL) {
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		prev->next = NULL;

		/*SLTNode* tail = *pphead;
		while (tail->next->next != NULL) {
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;*/
	}
}

//头删
void SListPopFront(SLTNode** pphead) {
	assert(pphead);
	/*if (*pphead == NULL) {
		return;
	}*/
	assert(*pphead != NULL);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

//查找
SLTNode* SListFind(SLTNode* pphead, SLTDataType x) {
	SLTNode* cur = pphead;
	while (cur) {
		if (cur->data == x) {
			return cur;
		}
		else {
			cur = cur->next;
		}
	}
	return NULL;
}

//指插(在pos位置之前去插入一个节点)
void SListInsertFront(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
	assert(pos);
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	if (*pphead == pos) {
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else {
		//找到pos前一个位置
		SLTNode* posprev = *pphead;
		while (posprev->next != pos) {
			posprev = posprev->next;
		}
		posprev->next = newnode;
		newnode->next = pos;
	}
}

//指插2.0(在pos位置之后去插入一个节点)
void SListInsertAfter(SLTNode* pos, SLTDataType x) {
	assert(pos);
	SLTNode* newnode = BuyListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

//指删
void SListEraseFront(SLTNode** pphead, SLTNode* pos) {
	assert(pos);
	assert(pphead);
	if (*pphead == pos) {
		/**pphead = pos->next;
		free(pos);*/
		SListPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

//指删2.0
void SListEraseAfter(SLTNode* pos) {
	assert(pos->next);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
	next = NULL;
}


//销毁
void SListDestory(SLTNode** pphead) {
	SLTNode* cur = *pphead;
	while (cur) {
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

test.c:

#include"SList.h"

void TestSList1() {
	SLTNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 5);
	SListPrint(plist);

	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPrint(plist);

	
}

//void TestSList2() {
//	SLTNode* plist = NULL;
//	SListPushBack(&plist, 1);
//	SListPushBack(&plist, 2);
//	SListPushBack(&plist, 2);
//	SListPushBack(&plist, 2);
//	SListPushBack(&plist, 5);
//	SListPrint(plist);
//
//	SLTNode* pos = SListFind(plist, 2);
//	int i = 1;
//	while (pos) {
//		printf("第%d个节点为:%p->%d\n", i, pos, pos->data);
//		pos = SListFind(pos->next, 2);
//		i++;
//	}
//
//	//修改(查找到元素并修改)
//	pos = SListFind(plist, 5);
//	if (pos) {
//		pos->data = 50;
//	}
//	SListPrint(plist);
//
//}
//
//void TestSList3() {
//	SLTNode* plist = NULL;
//	SListPushBack(&plist, 1);
//	SListPushBack(&plist, 2);
//	SListPushBack(&plist, 3);
//	SListPushBack(&plist, 4);
//	SListPushBack(&plist, 5);
//	SListPrint(plist);
//
//	SLTNode* pos = SListFind(plist, 5);
//	if (pos) {
//		SListInsert(&plist, pos, 50);
//	}
//	SListPrint(plist);
//
//	pos = SListFind(plist, 1);
//	if (pos) {
//		SListInsert(&plist, pos, 10);
//	}
//	SListPrint(plist);
//
//}
//
//void TestSList4() {
//	SLTNode* plist = NULL;
//	SListPushBack(&plist, 1);
//	SListPushBack(&plist, 2);
//	SListPushBack(&plist, 3);
//	SListPushBack(&plist, 4);
//	SListPushBack(&plist, 5);
//	SListPrint(plist);
//
//	SLTNode* pos = SListFind(plist, 1);
//	if (pos) {
//		SListInsertAfter(pos, 10);
//	}
//	SListPrint(plist);
//	
//
//}
//
//void TestSList5() {
//	SLTNode* plist = NULL;
//	SListPushBack(&plist, 1);
//	SListPushBack(&plist, 2);
//	SListPushBack(&plist, 3);
//	SListPushBack(&plist, 4);
//	SListPushBack(&plist, 5);
//	SListPrint(plist);
//
//	SLTNode* pos = SListFind(plist, 4);
//	if (pos) {
//		SListEraseFront(&plist, pos);
//	}
//	SListPrint(plist);
//}
//
//void TestSList6() {
//	SLTNode* plist = NULL;
//	SListPushBack(&plist, 1);
//	SListPushBack(&plist, 2);
//	SListPushBack(&plist, 3);
//	SListPushBack(&plist, 4);
//	SListPushBack(&plist, 5);
//	SListPrint(plist);
//
//	SListDestory(&plist);
//	SListPrint(plist);
//
//}


void menu() {
	printf("\t\t*******************************\t\t\n");
	printf("\t\t*******1.尾插      2.头插*******\t\t\n");
	printf("\t\t*******3.尾删      4.头删*******\t\t\n");
	printf("\t\t*******5.指插(前)  6.指插(后)***\t\t\n");
	printf("\t\t*******7.指删(前)  8.指删(后)***\t\t\n");
	printf("\t\t*******9.查找      10.打印******\t\t\n");
	printf("\t\t*******11.清空     0.退出*******\t\t\n");
	printf("\t\t********************************\t\t\n");
}
//void TestSList7() {
//	menu();
//	int input = 0;
//	scanf("%d\n", &input);
//	do {
//		
//	} while (input);
//}
int main() {
	TestSList1();
	//TestSList2();
	//TestSList3();
	//TestSList4();
	//TestSList5();
	//TestSList6();
	//TestSList7();

	return 0;
}

二、双向带头循环链表的实现

1、概念

带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。


2、实现

(1)三板斧

三板斧那是肯定必要的,正如我们前面说的,那我们就直接进入正题,直接设立三板斧:
在这里插入图片描述


(2)双向带头循环链表结构

在这里插入图片描述
在这里插入图片描述
有了前面的基础,我们知道结点前prev和结点后next两个结构体指针的,也就迎刃而解写出这种结构。


(3)增加结点

增加一个新结点,也就是说我们新结点的前后都为空,防止野指针,我们直接看代码:
在这里插入图片描述


(4)初始化

初始化结点也就是将这个链表的结构做出来,我们知道这个链表是双向循环带头链表,是有一个头结点的,所以我们单纯进行结点前指向自己,结点后指向自己,如下结构图:
在这里插入图片描述
在这里插入图片描述
注意:那个-1是可以随便值的,我们这个初始化的是头结点,是不作为存储数据的结点,仅仅是一个哨兵位的结点,也就是说,它只负责站岗,不负责管事儿。


(5)小知识(一级指针)

我们在单链表的时候是进行二级指针的接收,是因为改变了链表的结构,可是我们的双向带头循环链表似乎增加结点删除结点没有改变哨兵位啊!这有个初始化整出来一个固定的哨兵位的头结点,我们进行增删改查的时候发现根本没有改变哨兵位的头结点,只要不经过改变,那就不需要二级指针进行接收。
如果大家伙还不是很理解,可以这样形象地理解,有哨兵位的结点我们知道这个哨兵位的结点的地址,由此能够找到其他结点的地址,我增删改查操作并没有改变头结点哨兵位的地址,说明不需要传二级指针;而单链表我们没有带头,只是想去找第一个结点的地址并进行增删改查,找地址中的地址那就需要二级指针。


(6)尾插

这个链表看起来是那么地难啊!可是大家细心一点能发现,这个练笔不需要找尾结点,因为phead的prev即是尾结点,是不是很方便?那我们就结合图分析一下尾插然后直接进行尾插的代码操作!
在这里插入图片描述
在这里插入图片描述


(7)尾删

尾删是根据头结点的prev找到尾结点,再找到尾结点的前一个结点,也就是prevtail,将头结点指向prevtail,prevtail的next指向头结点即可,但这里需要判断一下链表是否为空。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


(8)头插

头插看起来似乎很简单,但要考虑顺序哦,先进行的是newnode与phead->next互相指,再phead与newnode互指即可。
在这里插入图片描述
在这里插入图片描述


(9)头删

先定义first为phead->next,直接将phead指向first->next即可,最后再将first释放结点即可。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


(10)查找

定义头结点的下一个存有数据的结点,遍历数组,找到数据相同的结点并直接返回结点位置。
在这里插入图片描述


(11)指定插入

我们找到pos这个结点并定义prev为pos->prev,然后跟新结点进行连接即可。

在这里插入图片描述
在这里插入图片描述


(12)指定删除

记录需要删除的结点的前一个结点prev和后一个结点later,直接将prev和later进行连接即可。
在这里插入图片描述
在这里插入图片描述


(13)销毁链表

即清空链表,只需要将链表每一个结点进行清空即可,包括头结点。
在这里插入图片描述


3、原码

SList.h:

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

typedef int LTDataType;

typedef struct ListNode 
{
	struct ListNode* next;//后
	struct ListNode* prev;//前

	LTDataType data;//数据
}LTNode;

//初始化
LTNode* LTInit();

//尾插
void LTPushBack(LTNode* phead, LTDataType x);

//尾删
void LTPopBack(LTNode* phead);

//销毁链表
void LTDestroy(LTNode* phead);

//打印
void LTPrint(LTNode* phead);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//头插
void LTPushFront(LTNode* phead, LTDataType x);

//头删
void LTPopFront(LTNode* phead);

//指定前插
void LTInsert(LTNode* pos, LTDataType x);

//指定删除
void LTErase(LTNode* pos);

//查找
LTNode* Find(LTNode* phead, LTDataType x);

Slist.c:

#include"Slist.h"

//增加结点
LTNode* BuyListNode(LTDataType x) {
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL) {
		perror("malloc::newnode");
		return NULL;
	}
	newnode->next = NULL;
	newnode->prev = NULL;
	newnode->data = x;

	return newnode;
}

//初始化
LTNode* LTInit() {
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

//判断链表是否为空
bool LTEmpty(LTNode* phead) {
	assert(phead);
	//if (phead->next == phead) {
	//	return true;
	//}
	//else{
	//	return false;
	//}
	return phead->next == phead;//空就是真
}


//打印
void LTPrint(LTNode* phead) {
	assert(phead);
	LTNode* cur = phead->next;
	printf("head<=>");
	while (cur != phead) {
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//销毁链表
void LTDestroy(LTNode* phead) {
	assert(phead);
	LTNode* cur = phead->next;
	LTNode* next = cur->next;
	while (cur != phead) {
		free(cur);
		cur = next;
		next = next->next;
	}
	free(phead);
	phead = NULL;
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x) {
	assert(phead);

	//不管链表为空还是不为空都可以插入
	LTNode* newnode = BuyListNode(x);
	LTNode* tail = phead->prev;

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

//尾删
void LTPopBack(LTNode* phead) {
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;

	tailprev->next = phead;
	phead->prev = tailprev;
	free(tail);
	tail = NULL;
	//LTErase(phead->prev);
}

//头插
void LTPushFront(LTNode* phead, LTDataType x) {
	assert(phead);

	LTNode* newnode = BuyListNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;

	phead->next = newnode;
	newnode->prev = phead;
}

//头删
void LTPopFront(LTNode* phead) {
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* first = phead->next;
	phead->next = first->next;
	first->next->prev = phead;
	free(first);
	first = NULL;

	//LTErase(phead->next);
}

//指定前插
void LTInsert(LTNode* pos, LTDataType x) {
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);
	//prev  newnode  pos
	prev->next = newnode;
	newnode->prev = prev;

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

//指定删除
void LTErase(LTNode* pos) {
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* later = pos->next;
	//prev pos later
	prev->next = later;
	later->prev = prev;
	free(pos);
	pos = NULL;
}

//查找
LTNode* Find(LTNode* phead, LTDataType x) {
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead) {
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

Test.c:

#include"Slist.h"


LTTest1() {
	LTNode* phead = LTInit();
	LTPushBack(phead, 1);
	LTPushBack(phead, 2);
	LTPushBack(phead, 3);
	LTPushBack(phead, 4);
	LTPrint(phead);

	LTPopBack(phead);
	LTPopBack(phead);
	//LTPopBack(phead);
	LTPrint(phead);

	LTPushFront(phead, 1);
	LTPushFront(phead, 2);
	LTPushFront(phead, 3);
	LTPushFront(phead, 4);
	LTPrint(phead);

	LTPopFront(phead);
	LTPopFront(phead);
	LTPopFront(phead);
	LTPrint(phead);

}

void LTTest2() {
	LTNode* phead = LTInit();
	LTPushBack(phead, 1);
	LTPushBack(phead, 2);
	LTPushBack(phead, 3);
	LTPushBack(phead, 4);
	LTPrint(phead);

	LTNode* pos = Find(phead, 4);


}

int main() {
	//LTTest1();
	LTTest2();
	//LTTest3();

	return 0;
}

总结

关于单链表与带头双向循环链表的优缺点在上文中已经展示过了。
在这里插入图片描述


客官,来个三连支持一下吧!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

The.Matrix.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值