单循环链表(不带表头)的C语言实现(详细)



前言


尾指针


尾指针的定义

尾指针是一个指向表尾的指针,可以用它来找到整个链表。

尾指针的作用

为什么不像单链表一样使用头指针呢?因为单循环链表和单链表一样,是单向的。找到后继结点很方便,需要O(1)的复杂度,找到前驱结点却很难,需要O(n)的复杂度。
尾指针指向的是尾结点,它的后继结点就是第一个数据结点(本文的链表没有头结点,如果有头结点,那么尾结点的后继结点是头结点)。所以,尾指针可以做到头指针的所有功能,而且尾指针还可以很方便找到表尾,和表尾相关的操作(比如合并2个单循环链表)用尾指针就很方便。

包含相关声明的头文件


将单循环链表包含的相关头文件,函数声明,结构体定义,宏定义等放到一个叫circular_linked_list.h的头文件中,是很好的管理思路。
想要使用单链表,只需要包含头文件circular_linked_list.h。

#include "circular_linked_list.h"

下面是完整的声明

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

typedef int ElemType;

struct Node;
typedef struct Node* PtrToNode;
typedef PtrToNode CircList;			// 用来表示循环链表尾指针
typedef PtrToNode Position;			// 用来表示循环链表结点地址
// 循环链表结点定义
typedef struct Node {
	ElemType element;
	Position next;
}Node;

void Init(CircList* ppRear);
bool IsEmpty(const CircList rear);
Position CreateNode(const ElemType elem);
Position GetElem(const CircList rear, int pos);
Position GetElemPrevious(const CircList rear, int pos);
void InsertElem(CircList* ppRear, int pos, ElemType elem);
void PushFront(CircList* ppRear, ElemType elem);
void PushBack(CircList* ppRear, ElemType elem);
ElemType DeleteElem(CircList* ppRear, int pos);
ElemType PopFront(CircList* ppRear);
ElemType PopBack(CircList* ppRear);
void ModifyElem(CircList rear, int pos, ElemType elem);
int GetLength(const CircList rear);
void Clear(CircList* ppRear);

Position LocateElem(const CircList rear, ElemType elem);
int LocatePos(const CircList rear, ElemType elem);
void RemoveElem(CircList* ppRear, ElemType elem);
void HeadInsert(CircList* ppRear);
void TailInsert(CircList* ppRear);
void Print(const CircList rear);


何时需要2级指针

在单链表中,有头结点时,插入,删除等相关操作,都不需要进行特殊处理。因为处理表头时,不需要直接改变头指针,只需要改变头结点。
但是,在单循环链表中,不管是插入还是删除,做表尾的相关操作时,都需要改变尾指针的指向,所以就需要2级指针。
简单总结:只要尾结点改变了,都需要2级指针



单循环链表的定义


单循环链表是在单链表的基础上做修改,有2个要点:
1.让表尾的next指针域不要指向NULL,而是指向头结点。
2.不要定义头指针,而是定义尾指针。尾指针指向表尾(尾结点)。
因为能找到表尾就能找到表头。头指针的作用,尾指针可以完美替代,而且当需要找表尾时,尾指针只需要O(1)的时间复杂度,而头指针需要O(n)。

和单链表一样,单循环链表的定义不是定义整个链表,而是定义单个结点(用结构体实现)。
创建单循环链表时,只需要将这些节点连接起来即可。

typedef int ElemType;				// 可以创建任何数据类型的链表,只需要修改这一行代码

struct Node;						// 先定义结点的结构体,但是不创建模板
typedef struct Node* PtrToNode;		// 指向Node结构体的指针有2个应用场景,下面2行代码分开定义
typedef PtrToNode CircList;			// 定义循环链表尾指针
typedef PtrToNode Position;			// 定义循环链表结点地址
// 循环链表结点定义
typedef struct Node {
	ElemType element;
	Position next;
}Node;

定义结点的代码大同小异,我只是用typedef把struct Node*改成了Position,这样命名更贴切一些。
这种命名方式是借鉴了经典书籍《数据结构与算法分析——C语言描述》里的代码。



单循环链表的基本通用操作


1.初始化单循环链表

由于我写的是没头结点的链表,所以初始化只需要设置尾指针为NULL。

void Init(CircList* ppRear)
{
	*ppRear = NULL;		
}

2.单循环链表是否为空

没头结点的链表判断是否为空表,是检测rear是否为NULL。

bool IsEmpty(const CircList rear)
{
	return rear == NULL;
}

3.创建一个新结点

因为插入的相关操作都需要创建一个新结点,所以可以写成一个函数实现复用

Position CreateNode(const ElemType elem)
{
	Position newNode = (Position)malloc(sizeof(Node));
	// 异常处理
	if (newNode == NULL)
	{
		puts("The creation of new node failed!");
		exit(EXIT_FAILURE);
	}
	
	newNode->element = elem;
	newNode->next = NULL;			// 因为新结点暂时没连接链表,所以next暂时设置为NULL
	return newNode;
}

4.查找指定位置上的元素

查询操作的异常处理是:
1.空表异常处理
2.pos范围越界异常处理

查询比较值得注意的特殊情况是:
1.查询的位置是尾结点,程序虽然会从if(cur == rear) break; 这里跳出,但是判定越界的if语句是,if (count < pos),如果查询的是表尾,count会等于pos,并不会被判断为越界。所以,程序可以通过查询尾结点的情况。
2.pos为0,此时count 和pos一开始都为0,程序不会进入循环,会直接返回cur,cur的初始值为rear。所以会返回尾指针,也和预计吻合。

/*
 * 查找循环链表指定位置元素的地址,pos的范围:[0,length],pos为0则返回尾指针
 * 如果指定位置超出范围,就打印提示信息,并终止程序
 * 空表不能查询,需要做异常处理
 */
Position GetElem(const CircList rear, int pos)
{
	// 左边的非法范围异常处理
	if (pos < 0)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	
	// 空表的异常处理
	if (IsEmpty(rear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	// 查询操作核心代码
	int count = 0;
	Position cur = rear;		// 保存指定位置元素的地址,初始时指向尾结点
	while (count < pos)
	{
		cur = cur->next;
		count++;
		if (cur == rear)
			break;
	}
	
	// 右边的非法范围异常处理
	if (count < pos)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	return cur;
}

5.查找指定位置元素的前驱元

该函数是为删除函数使用的,可以让删除函数更加模块化。
注意特殊情况,当原链表只有1个结点的时候,返回pos=1的前驱结点,就是返回该结点。
因为1个结点的循环链表,它的前驱结点就是它自己。

/*
 * GetElemPrevious函数用于返回当前位置的直接前驱
 * 该函数可以用于删除操作
 * pos的合法范围为[1,length]
 * 当pos为1时,返回尾结点
 */
Position GetElemPrevious(const CircList rear, int pos)
{
	// 空表的异常处理
	if (IsEmpty(rear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}

	// 左边的非法范围异常处理
	if (pos <= 0)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}

	Position prec = rear;
	int count = 0;
	while (count < pos - 1)
	{
		prec = prec->next;
		count++;
		// 如果找到尾结点了,说明要找的前驱结点越界了
		if (prec == rear)
		{
			puts("The position is out of range!");
			exit(EXIT_FAILURE);
		}
	}
	return prec;
}

6.在指定位置插入元素

异常情况有1个:pos越界异常。
特殊情况有1个:空表的插入,需要让新结点的next域指向它自己。空表的插入只有pos=1合法,pos>1需要单独做范围越界处理。
正常插入操作分为3步:
1.创建新结点。
2.找到被插入位置的前驱结点。
3.让新结点连接上链表。
如果是对表尾插入,需要移动尾指针到下一位。

/*
 * 在指定位置上插入元素(也就是把新结点插入到指定位置结点的前面)
 * 空表的插入需要做特殊处理,因为这是没头结点的版本
 * pos的合法范围是[1,length+1],GetElem函数会处理越界异常
 * 如果在表尾插入,需要移动尾结点
 */
void InsertElem(CircList* ppRear, int pos, ElemType elem)
{
	Position newNode = CreateNode(elem);
	// 空表的插入需要特殊化处理
	if (IsEmpty(*ppRear))
	{
		// 如果pos为1,则可以将新结点插入空表,否则就是越界
		if (pos == 1)
		{
			*ppRear = newNode;
			newNode->next = newNode;
			return;
		}
		else
		{
			puts("The position is out of range!");
			exit(EXIT_FAILURE);
		}
	}
	// GetElem函数会做指定位置范围越界的检测
	Position prec = GetElem(*ppRear, pos - 1);
	// 插入操作核心代码
	newNode->next = prec->next;
	prec->next = newNode;
	/*
	 * 如果在表尾后插入元素(pos=length+1),需要移动尾指针
	 * 而prec==尾指针包含两种情况,表头插入或者表尾插入
	 * 表头插入时,pos为1,所以需要剔除这种情况
	 */
	if (prec == *ppRear && pos != 1)
		*ppRear = (*ppRear)->next;		//在表尾插入,需要让尾指针往后移动一位
}

7.在头部插入元素

头插其实非常简单,就是在表尾后插入一个新结点。
绝大多数情况,尾指针是不用改变的。
但是对空表进行头插时,需要改变尾指针(所以需要传入二级指针的参数)。

/*
 * 在表头前插入结点,等价于在尾结点后插入元素,因为循环链表的表尾的后继结点就是表头
 * 当表为空时,需要做特殊处理
 */
void PushFront(CircList* ppRear, ElemType elem)
{
	Position newNode = CreateNode(elem);
	// 空表的头插需要特殊处理
	if (IsEmpty(*ppRear))
	{
		newNode->next = newNode;
		*ppRear = newNode;
		return;
	}
	// 头插核心操作
	newNode->next = (*ppRear)->next;
	(*ppRear)->next = newNode;
}

8.在尾部插入元素

头插和尾插的位置其实是一样的,都是在原来尾结点的下一位插入新结点
区别只有一点,尾插需要移动尾指针到下一位。这样,新结点就变成表尾了。

/*
 * 在表尾插入结点,尾指针一定要往后移动一位
 * 当表为空时,需要做特殊处理
 */
void PushBack(CircList* ppRear, ElemType elem)
{
	Position newNode = CreateNode(elem);
	// 空表的头插需要特殊处理
	if (IsEmpty(*ppRear))
	{
		newNode->next = newNode;
		*ppRear = newNode;
		return;
	}
	// 尾插核心操作
	newNode->next = (*ppRear)->next;
	(*ppRear)->next = newNode;
	*ppRear = (*ppRear)->next;
}

9.删除指定位置上的元素

删除指定位置的元素是实现较为复杂的操作。
有2个异常情况:
1.空表删除异常。
2.查找位置越界异常。
有2个特殊情况:
1.如果原链表只有1个元素,需要做清空链表的操作。
2.如果删除元素是尾结点,需要移动尾指针到前一位。
基本的删除操作分为2步:
1.找到被删除结点的前驱结点,可以利用单独的函数GetElemPrevious来实现,让程序更加模块化,越界异常和空表异常都由它完成。
2.执行删除的核心操作(让被删除结点的前驱直接连接后继,并且释放被删除结点的空间)。
有的应用场景,可能会使用被删除结点的值,所以我将这个值作为返回值返回。

/ 删除单循环链表指定位置上的元素,并返回该元素的值,位置范围为:[1,length]
ElemType DeleteElem(CircList* ppRear, int pos)
{
	Position prec = GetElemPrevious(*ppRear, pos);
	Position cur = prec->next;
	ElemType deleteElem = cur->element;
	// 如果要删除结点是尾结点
	// 分为循环链表有1个元素和多个元素的情况
	if (cur == *ppRear)
	{
		if (pos == 1)
		{
			Clear(ppRear);
			return deleteElem;
		}
		else
			*ppRear = prec;
	}

	// 删除核心操作
	prec->next = cur->next;
	free(cur);
	return deleteElem;
}

10.在头部删除元素

头删操作非常简单。
异常处理只有一个,空表异常处理。
特殊情况是只有一个元素的链表的头删。
核心删除操作很简单,就是让尾结点指向头结点的后继结点,并且释放头结点。

// 删除第一个数据结点,并且返回被删除结点的数据域
ElemType PopFront(CircList* ppRear)
{
	// 空表的异常处理
	if (IsEmpty(*ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	Position cur = (*ppRear)->next;
	ElemType deleteElem = cur->element;
	
	// 对于只有一个元素的链表,需要做特殊处理
	if (cur == *ppRear)
	{
		Clear(ppRear);
		return deleteElem;
	}
	
	// 头删的核心操作
	(*ppRear)->next = cur->next;
	free(cur);
	return deleteElem;
}

11.在尾部删除元素

尾删相对于头删要复杂一点点。
有一个异常情况,空表异常。
有一个特殊情况,原链表只有一个元素。
删除核心操作分为这几步:
1.找尾结点的前驱结点。
2.直接连接尾结点的前驱和后继结点。
3.释放尾结点的空间。
4.让尾指针往前移动一位。
5.返回被删除元素的值,有的场景可能会用。
加粗部分是相比头删多出来的2步。

// 删除循环单链表的尾结点,并返回被删除结点的数据域
ElemType PopBack(CircList* ppRear)
{
	// 空表的异常处理
	if (IsEmpty(*ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	ElemType deleteElem = (*ppRear)->element;
	// 如果原链表只有一个元素
	if ((*ppRear)->next == *ppRear)
	{
		Clear(ppRear);
		return deleteElem;
	}
	
	// 寻找尾结点的前驱结点
	Position prec = (*ppRear)->next;
	while (prec->next != *ppRear)
		prec = prec->next;
		
	// 尾删核心操作
	prec->next = (*ppRear)->next;
	free(*ppRear);
	*ppRear = prec;
	return deleteElem;
}

12.修改指定位置上元素的值

修改指定位置元素的值的函数很容易。
唯一需要注意的是,我设计的GetElem函数是为插入函数服务的。它不对空表和pos=0做异常处理,所以,修改函数需要自己单独做异常处理。

// 修改指定位置上元素的值,pos的合法范围为:[1,length]
void ModifyElem(CircList rear, int pos, ElemType elem)
{
	// 做空表异常处理
	if (IsEmpty(head))
	{
		puts("The double circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	// 后面的GetElem函数不会做pos=0的非法范围检查,需要单独处理
	if (pos == 0)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	Position cur = GetElem(rear, pos);
	cur->element = elem;
}

13.计算单循环链表长度

特殊情况是空表,length为0。
计算长度就是用一个计数变量,每循环一次自增1。

// 计算链表长度,如果是空表就返回0
int GetLength(const CircList rear)
{
	// 如果是空表,返回0
	if (IsEmpty(rear))
		return 0;
	int length = 0;
	Position cur = rear;
	// 每移动一次,length+1,如果移动到表尾,返回length
	do
	{
		cur = cur->next;
		length++;
	}
	while (cur != rear);
	return length;
}

14.清空单循环链表

清空链表很简单,注意需要succ指针保存当前被释放结点的后继节点。
免得释放当前结点后,丢失链表。

void Clear(CircList* ppRear)
{
	// 空表异常处理
	if (IsEmpty(ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	Position cur = (*ppRear)->next;	// 从第一个结点开始释放
	while (cur != *ppRear)			// 在循环内释放除尾结点之外的其他结点
	{
		Position succ = cur->next;	// 要保存当前要释放结点的后继结点,保证能找到链表的后续部分
		free(cur);
		cur = succ;
	}
	free(cur);						// 最后释放尾结点
	*ppRear = NULL;					// 让尾指针指向NULL
}

非通用操作(只适用于整型单循环链表)


1.查找和指定值相同的第一个结点

LocateElem函数看起来很简单其实有一个细节。
循环有可能会遍历整个链表(除非提前找到值),但是循环链表遍历的循环判断条件是有细节的。
1.必须从第一个数据结点开始找起,不能从尾结点找起,因为函数是返回和指定值相同的第一个元素的地址
2.循环链表遍历一遍的标志是再次到相同结点。所以,一定要先做值是否相同的判断,然后立刻将cur移动到下一位,最后做while循环的条件判断。这样程序在跳出循环时,必定已经遍历了一遍循环链表。

// 返回循环单链表内和指定值相同的第一个元素的地址,如果没有找到就做异常处理
Position LocateElem(const CircList rear, ElemType elem)
{
	// 空表的异常处理
	if (IsEmpty(rear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}

	Position cur = rear->next;
	// 从头结点开始检查,找到就直接返回该节点
	// 如果再次找到头结点,说明链表没有和指定值相同的结点,就跳出循环,在循环外做异常处理
	do
	{
		if (cur->element == elem)
			return cur;
		cur = cur->next;
	} while (cur != rear->next);
	// 如果跳出循环,说明没找到,需要做异常处理
	puts("The value is not in the list!");
	exit(EXIT_FAILURE);
}

2.返回和指定值相同的第一个结点的序号

查找指定值的序号和查找指定值的元素地址思路是一样的,查序号只是多一个计数的变量pos。
我修改了一下循环判断的条件,思路是一样的。
我把是否和指定值相同的判断放到了循环条件里面。
把再次找到头结点的条件放到了if里面,如果if成立,就认为没找到,就返回0。

/* 
 * 返回和指定值相同的第一个结点的序号
 * 如果没有找到,就返回0
 */
int LocatePos(const CircList rear, ElemType pos)
{
	// 空表的异常处理
	if(IsEmpty(rear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}

	Position cur = rear->next;
	int count = 1;
	while (cur->element != elem)
	{
		cur = cur->next;
		count++;
		if (cur == rear->next)
			return 0;
	}
	return count;
}

3.删除和指定值相同的第一个结点

循环链表的删除操作都不简单。
有2个异常情况:
1.空表的异常。
2.查找不到指定值。
有2个特殊情况:
1.原链表只有一个结点。
2.被删除结点是尾结点。
删除的基本操作分为2步:
1.找被删除结点的前驱结点。
2.核心删除操作(连接被删除结点的前驱和后继,释放被删除结点的空间)。

/*
 * 删除单循环链表内和指定值相同的第一个元素
 * 如果没找到和指定值相同的元素,则打印提示信息,并终止程序
 */
void RemoveElem(CircList* ppRear, ElemType elem)
{
	// 处理空表的异常
	if (IsEmpty(*ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	// 如果只有一个结点
	if ((*ppRear)->next == *ppRear)
	{
		// 如果该结点的值就是要删除的指定值,就清空链表
		if ((*ppRear)->element == elem)
		{
			Clear(ppRear);
			return;
		}
		// 否则就做没查找到的异常处理
		else
		{
			puts("The value is not in the list!");
			exit(EXIT_FAILURE);
		}
	}

	// 找被删除结点的前驱元
	Position prec = *ppRear;
	while (prec->next->element != elem)
	{
		prec = prec->next;
		// 如果找到了尾结点,说明链表内没有指定值,就处理异常
		if (prec == *ppRear)
		{
			puts("The value is not in the list!");
			exit(EXIT_FAILURE);
		}
	}
	Position cur = prec->next;
	// 如果被删除结点是尾结点,需要让尾指针前移一位
	if (cur == *ppRear)
		*ppRear = prec;

	// 删除操作
	prec->next = cur->next;
	free(cur);
}

4.自制输入函数(为了头插和尾插方便)

将键盘输入的一个整数赋值给num。
如果输入的是整数,就返回true。
如果输入的是非数字字符,就清空缓冲区,并返回false。

/*
 * 自制的输入函数,讲输入的整数存储到指定的整型变量中
 * 如果输入的是整数,就返回true;如果输入非数字字符,就返回false,并清空缓存区
 */
bool InputInteger(int* num)
{
	int ok;					// ok用于存储scanf函数成功读入的整数个数
	// 输入num的值,如果合法,ok为1,否则ok不为1
	ok = scanf("%d", num);
	// 如果输入的字符非法,利用下面代码去掉缓冲区里多余的字符
	if (ok != 1)
	{
		while (getchar() != '\n')
			continue;
	}
	// 如果输入的是整数,就返回true,如果输入非法字符,就返回false
	return ok == 1;
}

5.头插法建立整型单循环链表

头插法会将新结点插入到第一个结点的前面。
头插法输入数据的顺序会和链表的数据顺序相反。
输入整数的函数是我内置的InputInteger函数,它可以输入任意的整数,比直接使用scanf函数好。
注意,第一个结点的头插要进行特殊处理。

/*
 * 使用头插法建立整型单循环链表,输入数据会依次插入链表的头部
 * 所以,输入数据的顺序和链表中的数据顺序会相反
 * 注意,由于没有头结点,第一个结点的插入要特殊处理
 * 如果一开始的输入就是非法字符,就进行异常处理
 */
void HeadInsert(CircList* ppRear)
{
	int value;
	Position newNode;

	puts("Input a series of integers(enter q to quit):");
	// 第一个结点的头插要特殊处理
	if (InputInteger(&value))
	{
		newNode = CreateNode(value);
		newNode->next = newNode;
		*ppRear = newNode;
	}
	else
	{
		puts("Please enter valid numbers!");
		exit(EXIT_FAILURE);
	}
	
	// 第二个结点以及更多结点的头插
	while(InputInteger(&value))
	{
		newNode = CreateNode(value);
		newNode->next = (*ppRear)->next;
		(*ppRear)->next = newNode;
	}
}

6.尾插法建立整型单循环链表

尾插法是将新结点依次插入链表尾部,输入数据顺序和链表的实际顺序一致。
由于每次插入改变了尾结点,所以需要修改尾指针。
由于没有头结点,所以第一个结点的尾插需要特殊处理

/*
 * 使用尾插法建立整型单循环链表,输入数据会依次插入链表的尾部
 * 所以,输入数据的顺序和链表中的数据顺序相同
 * 注意,由于没有头结点,第一个结点的插入要特殊处理
 */
void TailInsert(CircList* ppRear)
{
	int value;
	Position newNode;

	puts("Input a series of integers(enter q to quit):");
	// 第一个结点的尾插要特殊处理
	if (InputInteger(&value))
	{
		newNode = CreateNode(value);
		newNode->next = newNode;
		*ppRear = newNode;
	}
	else
	{
		puts("Please enter valid numbers!");
		exit(EXIT_FAILURE);
	}

	// 第二个结点以及更多结点的尾插
	while (InputInteger(&value))
	{
		newNode = CreateNode(value);
		newNode->next = (*ppRear)->next;
		(*ppRear)->next = newNode;
		*ppRear = newNode;					// 尾插改变了尾结点,所以尾指针需要移动
	}
}

7.打印整型单循环链表

打印循环链表的操作很简单,只需要让链表从第一个结点开始,遍历一遍即可。
注意,当链表遍历到尾结点时跳出循环,还需要加一个输出语句打印尾结点。

/*
 * 依次打印链表的元素,最后一个元素连接head
 * 比如1->2->3->head,->head表明链表是一个环
 * 如果链表为空,就直接打印NULL
 */
void Print(const CircList rear)
{
	if (!IsEmpty(rear))
	{
		Position cur = rear->next;
		while (cur != rear)
		{
			printf("%d->", cur->element);
			cur = cur->next;
		}
		printf("%d->head\n", cur->element);
	}
	else
	{
		puts("NULL");
	}
}
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值