C/C++ 程序员必须会的《二叉树》

二叉树的概念

二叉树犹如我们的族谱一般!

他也像是一颗倒立的大树!

树状图是一种数据结构,它是由 n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;除了根结点外,每个子结点可以分为多个不相交的子树;

专业术语中文描述
Root根节点一棵树的顶点
Child孩子节点一个节点含有的子树的根节点称为该节点的子节点
Leaf叶子节点没有孩子的节点
Degree一个节点包含的子树的数量
Edge一个节点与另外一个节点的连接
Depth深度根节点到这个节点经过的边的数量
Height节点高度从当前节点到叶子节点形成路径中边的数量
Level层级节点到根节点最长路径的边的总和
Path路径一个节点和另一个节点之间经过的边和 Node 的序列

如下图就是一颗二叉树:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


二叉树的原理

二叉树是一个每个结点最多只能有两个分支的树,左边的分支称之为左子树,右边的分支称之为右子树。
在这里插入图片描述

二叉树的三条限定公式:

公式
(1) 在非空二叉树中,第 i - 1 层的结点总数不超过2^i-1(2的i-1次方), i >= 1;
(2) 深度为 h - 1 的二叉树最多有2^h - 1(2的h次方 - 1) 个结点(h>=1),最少有 h 个结点;
(3) 对于任意一棵二叉树,如果其叶结点数为 N0,而度数为 2 的结点总数为 N2,则 N0 = N2 + 1;

二叉树的分类

一、完全二叉树

若设二叉树的高度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层有叶子节点,并且叶子结点都是从左到右依次排布,这就是完全二叉树(就是完全二叉树)。
在这里插入图片描述

二、满二叉树

除了叶结点外每一个结点都有左右子节点且叶子结点都处在最底层的二叉树。

在这里插入图片描述

三、平衡二叉树

又被称为 AVL 树,它是一颗空树或左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
在这里插入图片描述

四、二叉搜索树

这也是二叉树中最常用的,也是我们重点讲的对象!

二叉搜索树又称二叉查找树、二叉排序树(Binary Sort Tree)。它是一颗空树或是满足下列性质的二叉树:

性质
(1)若左子树不空,则左子树上所有节点的值均小于或等于它的根节点的值;
(2)若右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
(3)左、右子树也分别为二叉排序树。

在这里插入图片描述

五、红黑树

是每个节点都带有颜色属性(颜色为红色或黑色)的自平衡二叉查找树,满足下列性质:

性质
(1)节点是红色或黑色;
(2)根节点是黑色;
(3)所有叶子节点都是黑色;
(4)每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点;
(5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

在这里插入图片描述


二叉搜索树的算法实现

二叉搜索树的原理:
一堆数据中,以任意一个数作为树根,之后的所有数,都与树根作比较,大于树根,存储在树根的右子节点上,如果右子节点已有节点,那么再与其进行比较,直到找到一个父节点的子节点为NULL时,将该数插入在当前位置。小于树根,则存储在树根的左子节点上…

例如,下图就是一颗二叉搜索树(二叉排序树):
在这里插入图片描述

二叉树一般采用链式存储方式:每个结点包含两个指针域,指向两个孩子结8点,还包含一个数据,存储结点信息。
在这里插入图片描述


节点结构体的定义:
#define NODE_MAX 1024

#define IsLess(a, b) (a < b)	// 用于两数判断
#define IsEqual(a, b) (a == b)

typedef int ElemType;

typedef struct _Bnode {
	ElemType data;
	struct _Bnode* lchild;	// 左子节点
	struct _Bnode* rchild;	// 右子节点
}Bnode, * Btree;

二叉搜索树插入节点

将要插入的结点 e,与节点 root 节点进行比较,若小于则去到左子树进行比较,若大于则去到右子树进行比较,重复以上操作直到找到一个空位置用于放置该新节点。

bool insertBtree(Bnode **root, Bnode *node) {
	Bnode *tem = NULL;	// 用于遍历
	Bnode *parent = NULL;	// 用于保存待插入位置的父节点
	bool isLchild = true;	// true:插入左子节点;false:插入右子节点


	if (!node) {	// 如果子节点为空,直接返回
		return false;
	} else {	// 它的左右子节点置为NULL
		node->lchild = NULL;
		node->rchild = NULL;
	}
		
		
		
	if (*root) {	// 如果存在根节点
		tem = *root;	// 使用tem保存根节点,以便后续遍历
	} else {	
		*root = node;	// 将子树节点赋值根节点
		return true;
	}


	while (tem) {
		parent = tem;	// 保存父节点

		if (IsLess(node->data, tem->data)) {	// 如果子节点小于父节点
			tem = tem->lchild;		// 子节点将插在父节点的左子节点上
			isLchild = true;
		} else {
			tem = tem->rchild;		// 子节点将插在父节点的右子节点上
			isLchild = false;
		}
	}


	//找到空位置后,进行插入
	if (isLchild) {	
		parent->lchild = node;	// 插入左子节点
	} else {
		parent->rchild = node;	// 插入右子节点
	}

	
	return true;
}

二叉搜索树删除节点

将要删除的节点的值,与节点 root 节点进行比较,若小于则去到左子树进行比较,若大于则去到右子树进行比较,重复以上操作直到找到一个节点的值等于删除的值,则将此节点删除。删除时有 4 中情况须分别处理:

1. 删除节点不存在左右子节点,即为叶子节点,直接删除;

在这里插入图片描述

2. 删除节点存在左子节点,不存在右子节点,直接把左子节点替代删除节点;

在这里插入图片描述

3. 删除节点存在右子节点,不存在左子节点,直接把右子节点替代删除节点;

在这里插入图片描述

4. 删除节点存在左右子节点,则取左子树上的最大节点或右子树上的最小节点替换删除节点。

在这里插入图片描述

可以使用两种方式实现删除:递归实现 和 非递归实现!

递归实现删除:

// 递归方式寻找左子节点最大值
int recursionFindMax(Bnode* root) {
	if (!root) {
		exit(-1);
	}

	if (!root->rchild) {
		return root->data;
	}

	return recursionFindMax(root->rchild);
}

// 递归方式删除值
Bnode * recursionDeleteBtree(Bnode *root, ElemType key, Bnode *&deleteBtree) {
	if (!root) {	// 递归出口:没有找到删除的值
		return NULL;
	}

	// 结点的值大于删除的值
	if (IsLess(key, root->data)) {
		root->lchild = recursionDeleteBtree(root->lchild, key, deleteBtree);
		return root;
	}

	// 结点的值小于删除的值
	if (IsLess(root->data, key)) {
		root->rchild = recursionDeleteBtree(root->rchild, key, deleteBtree);
		return root;
	}

	// 获取待删除的节点
	deleteBtree = root;

	// 删除节点不存在左右子节点,即为叶子节点,直接删除
	if (!root->lchild && !root->rchild) return NULL;

	// 删除节点只存在右子节点,直接用右子节点取代删除节点
	if (root->rchild && !root->lchild) return root->rchild;

	// 删除节点只存在左子节点,直接用左子节点取代删除节点
	if (root->lchild && !root->rchild) return root->lchild;

	// 删除节点存在左右子节点,直接用左子节点的最大值取代删除节点
	int var = recursionFindMax(root->lchild);
	root->data = var;
	root->lchild = recursionDeleteBtree(root->lchild, var, deleteBtree);
	return root;
}

非递归方式删除:

// 非递归方式
Bnode *findMax(Bnode *root, ElemType &var) {
	Bnode *praent = root;	// 保存最大左子节点的父节点

	if (!root) {
		exit(-1);
	}

	// 找到左子节点的最大值
	while (root->rchild) {
		praent = root;		// 保存父节点
		root = root->rchild;
	}

	if (praent != root) {
		praent->rchild = NULL;	// 断开其与右子节点的关系
	}
	

	var = root->data;

	return root;
}


// 非递归方式删除
Bnode *deleteBtree(Bnode *root, ElemType key) {
	Bnode *tem = root;
	Bnode *praent = NULL;	// 保存待删除节点的父节点
	bool isLchild = true;	// true:左子节点  false:右子节点

	if (!root) {
		return NULL;
	}

	while (tem) {
		if (IsLess(key, tem->data)) {
			praent = tem;		// 保存父节点
			tem = tem->lchild;
			isLchild = true;	// 左子节点	
			continue;
		}


		if (IsLess(tem->data, key)) {
			praent = tem;
			tem = tem->rchild;
			isLchild = false;	// 右子节点
			continue;
		}


		// 删除节点不存在左右子节点,即为叶子节点,直接删除
		if (!tem->lchild && !tem->rchild) {
			if (tem != root) {	// 如果删除节点不是根节点
				if (isLchild) {
					praent->lchild = NULL;	// 断开其与左子节点关系,方便释放
				} else {
					praent->rchild = NULL;	// 断开关系
				}
			}
			return tem;
		}

		// 删除节点只存在右子节点,直接用右子节点取代删除节点
		if (tem->rchild && !tem->lchild) {
			praent->rchild = tem->rchild;
			return tem;
		}


		// 删除节点只存在左子节点,直接用左子节点取代删除节点
		if (tem->lchild && !tem->rchild) {
			praent->lchild = tem->lchild;
			return tem;
		}


		// 删除节点存在左右子节点,直接用左子节点的最大值取代删除节点
		ElemType var;
		praent = findMax(tem->lchild, var);
		if (praent == tem->lchild) tem->lchild = NULL;	// 断开关系
		tem->data = var;
		return praent;
	}

	return NULL;
}

建议使用非递归方式实现删除!


二叉搜索树搜索(查找)

一样有两种方式实现查找:递归方式 和 非递归方式

递归方式实现查找:

// 递归方式查找
Bnode *queryByRec(Bnode *root, ElemType key) {

	if (!root || IsEqual(root->data, key)) {
		return root;
	} else if (IsLess(key, root->data)) {
		return queryByRec(root->lchild, key);
	} else {
		return queryByRec(root->rchild, key);
	}

}

非递归方式实现查找:

// 非递归方式查找
Bnode *queryByLoop(Bnode *root, ElemType key) {

	if (root) {
		while (root && !IsEqual(root->data, key)) {
			if (IsLess(key, root->data)) {
				root = root->lchild;
			} else {
				root = root->rchild;
			}
		}

		return root;
	}

	return NULL;
	
}

二叉树的遍历

二叉树的遍历是指从根结点出发,按照某种次序依次访问所有结点,使得每个结点被当且访问一次。共分为四种方式:

一、前序遍历

先访问根节点,然后前序遍历左子树,再前序遍历右子树。

在这里插入图片描述
上图遍历结果为:19、7、5、11、15、25、21、61

前序遍历使用递归方式实现:

// 递归方式正序输出
void PreOrderRec(Bnode* root){
	if (!root) {
		return;
	}

	cout << "-" << root->data << "- ";
	PreOrderRec(root->lchild);
	PreOrderRec(root->rchild);
}

前序遍历使用 栈 实现:

具体过程:

  1. 首先申请一个新的栈,记为 stack;

  2. 将头结点(根节点) head 压入 stack 中;

  3. 每次从 stack 中弹出栈顶节点,记为 cur,然后打印 cur 值,如果 cur 右孩子不为空,则将右孩子压入栈中;如果 cur 的左孩子不为空,将其压入 stack 中;

重复步骤 3,直到 stack 为空.

// 非递归方式正序输出
void PreOrder(Bnode *root) {
	if (!root) {
		return;
	}

	Bnode cur;		
	Stack stack;
	initStack(stack);
	insertValue(stack, *root);	// 根节点入栈

	// 栈不为空,继续执行
	while (!isStackEmpty(stack)) {
		popValue(stack, cur);	// 出栈
		cout << cur.data << " ";

		if (cur.rchild) {	// 右子节点不为空,入栈
			insertValue(stack, *cur.rchild);
		}

		if (cur.lchild) {	// 左子节点不为空,入栈
			insertValue(stack, *cur.lchild);
		}
	}

	clearStack(stack);	// 释放栈的内存

}

=

=

=

二、中序遍历

先访问根节点的左子树,然后访问根节点,最后遍历右子树
在这里插入图片描述

上图遍历结果为:5、7、11、15、19、21、25、61

=

=

=

三、后序遍历

从左到右,先叶子后节点的方式遍历访问左右子树,最后访问根节点
在这里插入图片描述

上图遍历结果为:5、15、11、7、21、61、25、19

=

=

=

四、层序遍历

从根节点从上往下逐层遍历,在同一层,按从左到右的顺序对节点逐个访问。
在这里插入图片描述

上图遍历结果为:19、7、25、5、11、21、61、15

具体代码实现有兴趣的可以自己试一下!


全部测试代码:

tree.h

#pragma once

#define NODE_MAX 1024

#define IsLess(a, b) (a < b)
#define IsEqual(a, b) (a == b)

typedef int ElemType;

typedef struct _Bnode {
	ElemType data;
	struct _Bnode* lchild;	// 左子节点
	struct _Bnode* rchild;	// 右子节点
}Bnode, * Btree;

stack.h

#pragma once
#include <cstddef>
#include "tree.h"


#define StackMax 128

typedef struct TreeStack {
	Bnode *top;
	Bnode *base;
}Stack;


bool initStack(Stack &stack);
bool isStackEmpty(Stack &stack);
bool isStackfull(Stack &stack);
bool insertValue(Stack &stack, Bnode &val);
bool popValue(Stack &stack, Bnode &val);
void clearStack(Stack &stack);





bool initStack(Stack &stack) {

	stack.base = new Bnode[StackMax];
	if (!stack.base) return false;

	stack.top = stack.base;

	return true;
}


bool isStackEmpty(Stack &stack) {

	if (stack.top == stack.base) {
		return true;
	}

	return false;
}



bool isStackfull(Stack &stack) {

	if ((stack.top - stack.base) == StackMax) {
		return true;
	}

	return false;
}


bool insertValue(Stack &stack, Bnode &val) {

	if (isStackfull(stack)) {
		return false;
	}

	*stack.top = val;
	stack.top++;

	return true;
}


bool popValue(Stack &stack, Bnode &val) {

	if (isStackEmpty(stack)) {
		return false;
	}

	stack.top--;
	val = *stack.top;

	return true;
}


void clearStack(Stack &stack) {

	delete stack.base;
	stack.base = NULL;
	stack.top = NULL;
}

main.cpp

#include <iostream>
#include <Windows.h>
#include "stack.h"


using namespace std;

// 插入子树节点
bool insertBtree(Bnode **root, Bnode *node);	// 参数一:根节点;参数二:子树节点


/* 递归方式:*/
// 找到左子节点的最大值
static int recursionFindMax(Bnode *root);
// 删除节点
Bnode * recursionDeleteBtree(Bnode *root, ElemType key, Bnode *&deleteBtree);		// 参数一:根节点;参数二:删除的值;参数三:保存删除节点地址返回释放


/* 非递归方式:*/
// 找到左子节点的最大值
static Bnode *findMax(Bnode *root, ElemType &var);
// 删除节点
Bnode *deleteBtree(Bnode *root, ElemType key);



/* 递归方式:*/
// 查找节点
Bnode *queryByRec(Bnode *root, ElemType key);


/* 非递归方式:*/
// 查找结点
Bnode *queryByLoop(Bnode *root, ElemType key);



/* 递归方式正序遍历 */
void PreOrderRec(Bnode *root);


/* 非递归方式正序遍历 */
void PreOrder(Bnode *root);



bool insertBtree(Bnode **root, Bnode *node) {
	Bnode *tem = NULL;	// 用于遍历
	Bnode *parent = NULL;	// 用于保存待插入位置的父节点
	bool isLchild = true;	// true:插入左子节点;false:插入右子节点


	if (!node) {	// 如果子节点为空,直接返回
		return false;
	} else {	// 它的左右子节点置为NULL
		node->lchild = NULL;
		node->rchild = NULL;
	}
		
		
		
	if (*root) {	// 如果存在根节点
		tem = *root;	// 使用tem保存根节点,以便后续遍历
	} else {	
		*root = node;	// 将子树节点赋值根节点
		return true;
	}


	while (tem) {
		parent = tem;	// 保存父节点

		if (IsLess(node->data, tem->data)) {	// 如果子节点小于父节点
			tem = tem->lchild;		// 子节点将插在父节点的左子节点上
			isLchild = true;
		} else {
			tem = tem->rchild;		// 子节点将插在父节点的右子节点上
			isLchild = false;
		}
	}


	if (isLchild) {
		parent->lchild = node;	// 插入左子节点
	} else {
		parent->rchild = node;	// 插入右子节点
	}

	
	return true;
}


// 递归方式寻找左子节点最大值
int recursionFindMax(Bnode* root) {
	if (!root) {
		exit(-1);
	}

	if (!root->rchild) {
		return root->data;
	}

	return recursionFindMax(root->rchild);
}

// 递归方式删除值
Bnode * recursionDeleteBtree(Bnode *root, ElemType key, Bnode *&deleteBtree) {
	if (!root) {	// 递归出口:没有找到删除的值
		return NULL;
	}

	// 结点的值大于删除的值
	if (IsLess(key, root->data)) {
		root->lchild = recursionDeleteBtree(root->lchild, key, deleteBtree);
		return root;
	}

	// 结点的值小于删除的值
	if (IsLess(root->data, key)) {
		root->rchild = recursionDeleteBtree(root->rchild, key, deleteBtree);
		return root;
	}

	// 获取待删除的节点
	deleteBtree = root;

	// 删除节点不存在左右子节点,即为叶子节点,直接删除
	if (!root->lchild && !root->rchild) return NULL;

	// 删除节点只存在右子节点,直接用右子节点取代删除节点
	if (root->rchild && !root->lchild) return root->rchild;

	// 删除节点只存在左子节点,直接用左子节点取代删除节点
	if (root->lchild && !root->rchild) return root->lchild;

	// 删除节点存在左右子节点,直接用左子节点的最大值取代删除节点
	int var = recursionFindMax(root->lchild);
	root->data = var;
	root->lchild = recursionDeleteBtree(root->lchild, var, deleteBtree);
	return root;
}




// 非递归方式
Bnode *findMax(Bnode *root, ElemType &var) {
	Bnode *praent = root;	// 保存最大左子节点的父节点

	if (!root) {
		exit(-1);
	}

	// 找到左子节点的最大值
	while (root->rchild) {
		praent = root;		// 保存父节点
		root = root->rchild;
	}

	if (praent != root) {
		praent->rchild = NULL;	// 断开其与右子节点的关系
	}
	

	var = root->data;

	return root;
}


// 非递归方式删除
Bnode *deleteBtree(Bnode *root, ElemType key) {
	Bnode *tem = root;
	Bnode *praent = NULL;	// 保存待删除节点的父节点
	bool isLchild = true;	// true:左子节点  false:右子节点

	if (!root) {
		return NULL;
	}

	while (tem) {
		if (IsLess(key, tem->data)) {
			praent = tem;		// 保存父节点
			tem = tem->lchild;
			isLchild = true;	// 左子节点	
			continue;
		}


		if (IsLess(tem->data, key)) {
			praent = tem;
			tem = tem->rchild;
			isLchild = false;	// 右子节点
			continue;
		}


		// 删除节点不存在左右子节点,即为叶子节点,直接删除
		if (!tem->lchild && !tem->rchild) {
			if (tem != root) {	// 如果删除节点不是根节点
				if (isLchild) {
					praent->lchild = NULL;	// 断开其与左子节点关系,方便释放
				} else {
					praent->rchild = NULL;	// 断开关系
				}
			}
			return tem;
		}

		// 删除节点只存在右子节点,直接用右子节点取代删除节点
		if (tem->rchild && !tem->lchild) {
			praent->rchild = tem->rchild;
			return tem;
		}


		// 删除节点只存在左子节点,直接用左子节点取代删除节点
		if (tem->lchild && !tem->rchild) {
			praent->lchild = tem->lchild;
			return tem;
		}


		// 删除节点存在左右子节点,直接用左子节点的最大值取代删除节点
		ElemType var;
		praent = findMax(tem->lchild, var);
		if (praent == tem->lchild) tem->lchild = NULL;	// 断开关系
		tem->data = var;
		return praent;
	}

	return NULL;
}



// 递归方式查找
Bnode *queryByRec(Bnode *root, ElemType key) {

	if (!root || IsEqual(root->data, key)) {
		return root;
	} else if (IsLess(key, root->data)) {
		return queryByRec(root->lchild, key);
	} else {
		return queryByRec(root->rchild, key);
	}

}



// 非递归方式查找
Bnode *queryByLoop(Bnode *root, ElemType key) {

	if (root) {
		while (root && !IsEqual(root->data, key)) {
			if (IsLess(key, root->data)) {
				root = root->lchild;
			} else {
				root = root->rchild;
			}
		}

		return root;
	}

	return NULL;
	
}


// 递归方式正序输出
void PreOrderRec(Bnode* root){
	if (!root) {
		return;
	}

	cout << "-" << root->data << "- ";
	PreOrderRec(root->lchild);
	PreOrderRec(root->rchild);
}


// 非递归方式正序输出
void PreOrder(Bnode *root) {
	if (!root) {
		return;
	}

	Bnode cur;		
	Stack stack;
	initStack(stack);
	insertValue(stack, *root);	// 根节点入栈

	while (!isStackEmpty(stack)) {
		popValue(stack, cur);	// 出栈
		cout << cur.data << " ";

		if (cur.rchild) {	// 右子节点不为空,入栈
			insertValue(stack, *cur.rchild);
		}

		if (cur.lchild) {	// 左子节点不为空,入栈
			insertValue(stack, *cur.lchild);
		}
	}

	clearStack(stack);

}




int main(void) {
	int test[] = { 19, 7, 25, 5, 11, 15, 21, 61 };
	Bnode *root = NULL;
	Bnode *node = NULL;


	node = new Bnode;
	node->data = test[0];

	insertBtree(&root, node);


	for (int i = 1; i < sizeof(test) / sizeof(test[0]); i++) {
		node = new Bnode;
		node->data = test[i];
		insertBtree(&root, node);
	}

	// 打印二叉搜索树
	cout << "二叉树生成后打印:" << endl;
	PreOrderRec(root);
	cout << endl;


	// 递归方式删除
	node = new Bnode;
	node = NULL;
	recursionDeleteBtree(root, 15, node);
	if (node) {
		cout << "删除" << node->data << "成功" << endl;
		delete node;
	} else {
		cout << "没有该值,删除失败!" << endl;
	}

	cout << "删除15后打印:" << endl;
	PreOrderRec(root);
	cout << endl;



	// 非递归方式删除
	node = new Bnode;
	node = NULL;
	node = deleteBtree(root, 19);
	if (node) {
		cout << "删除11成功" << endl;
		delete node;
	} else {
		cout << "没有该值,删除11失败!" << endl;
	}

	cout << "删除11后打印:" << endl;
	PreOrderRec(root);
	cout << endl;





	// 递归方式查找
	node = new Bnode;
	node = NULL;
	node = queryByRec(root, 21);
	if (node) {
		cout << "查找成功,确实有" << node->data << endl;
	} else {
		cout << "查找失败,没有21该值!" << endl;
	}


	// 非递归方式查找
	node = queryByLoop(root, 61);
	if (node) {
		cout << "查找成功,确实有" << node->data << endl;
	} else {
		cout << "查找失败,没有61该值!" << endl;
	}


	// 非递归方式输出
	PreOrder(root);



	system("pause");
	return 0;
}

运行截图:
在这里插入图片描述


总结:
重在理解,代码其实很容易实现的,理解好就行!特别建议,项目开发中,能不用递归就不用递归!!!

笔记来自骑牛学院高级VIP课程!

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

cpp_learners

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

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

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

打赏作者

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

抵扣说明:

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

余额充值