【C++】-二叉搜索树的详解(递归和非递归版本以及巧用引用)

在这里插入图片描述
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!


前言

今天我要来给大家讲解一下二叉树的一些进阶部分的知识,与其这样说不如说是为了红黑树和AVL树和红黑树做铺垫,本篇的内容相对来说理解起来比前面的简单,博主也会分两个版本给大家介绍,一个递归版本的,一个非递归版本的,两个会一个介绍的,话不多说,我们开始进入正文


一、什么是二叉搜索树?

这个树不像普通的树一样每颗结点都是杂乱无章的,他符合一个特性:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

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

通过上面这个树我们分析,每棵子树的根节点的值都是这颗子树的左子树中最大的,是这颗子树的右子树中最小的,我们在来看,以8这个根节点为例,它的左子树中最大的值是这颗子树的最右边的结点,而它的右子树中最小的结点是这颗子树的最左边的结点
在这里插入图片描述
大家先提前了解这个特点因为一会在删除的时候需要使用到这个特点。


我们发现将二叉搜索树进行中序遍历,就是一个有序的,所以接下来我们验证的时候也是将二叉搜索树按照中序打印出来进行验证

二、模拟实现

我们今天实现的二叉搜索树是没有重复数字的,这个到AVL树的时候才能解决。我们就实现插入,删除,查找的主要功能。

我们现将结点进行封装,这个封装在list的实现的时候也了解过了,来一个框架:

template<class k>
struct BSNode
{
	BSNode<k>* _left;
	BSNode<k>* _right;
	k _data;

	BSNode(const k& data=k())
		:_left(nullptr)
		,_right(nullptr)
		,_data(data)
	{}
};

template<class k>
class BSTree
{
	typedef BSNode<k> Node;
public:
	BSTree(){}
private:
	Node* _root;//根节点
};

2.1 中序遍历

我们进行中序遍历的时候,需要传根节点进去的,如果不封装一层,我们在类外面是没有访问到私有的_root的,所以要进行封装,递归版本的都要进行封装。

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}

2.2 插入操作

我们普通二叉树进行插入没啥意义,但是对于二叉搜索树进行插入有意义,它插入的位置是确定的,不是随便插入的。

非递归:

bool insert(const k& data)
	{
		if (_root == nullptr)//树为空的情况
		{
			_root = new Node(data);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;//为了记录将要插入位置的父节点,不然申请结点没有办法进行链接操作
		while (cur)//为了找到插入位置
		{
			parent = cur;
			if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(data);
		if (parent->_data > data)//判断插入到父节点的哪边
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}

		return true;
	}

在这里插入图片描述

递归版本:

bool InsertR(const k& data)
	{
		return _InsertR(_root, data);
	}
bool _InsertR(Node*& root, const k& data)//传引用的好处就是得到父节点的指针,不用关心当前插入位置结点是父节点的哪个结点
	{
		if (root == nullptr)
		{
			root = new Node(data);
			return true;
		}
		if (root->_data < data)
		{
			return _InsertR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _InsertR(root->_left, data);
		}
		else//相等的时候就不用插入了
		{
			return false;
		}
	}

这个巧妙的设计就是传引用进去了,因为我要通过父节点来确定链接关系,结果通过引用直接获得父节点指向的指针,将指针里面的内容修改成要插入结点的就行了,不需要保留父节点,也不需要判断位于父节点那边了
在这里插入图片描述

2.3查找操作

查找操作就比较简单
非递归:

bool find(const k& data)
	{
		if (_root == nullptr)
		{
			return false;
		}
		Node* cur = _root;
		while (cur)
		{
			if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else//找到了就返回真
			{
				return true;
			}
		}
		return false;//到这还没有返回,说明没有找到
	}

递归版本:

bool FindR(const k& data)
	{
		return _FindR(_root, data);
	}
bool _FindR(Node* root, const k& data)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (root->_data < data)
		{
			return _FindR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _FindR(root->_left, data);
		}
		else
		{
			return true;
		}
	}

2.4删除操作

这个操作也是最复杂的,情况也是最多的

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
在这里插入图片描述
我们看到这种情况其实是有三种小情况的,而要删除的结点没有左右孩子的情况,他的左右指针都是空,所以可以放在b,c情况里面

情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
在这里插入图片描述

情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除

在这里插入图片描述
非递归:

bool erase(const k& data)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				break;
			}
		}
		if (cur == nullptr)//没找到要删除的元素
		{
			return false;
		}

		if (cur->_left == nullptr)//情况b
		{
			if (cur == _root)
			{
				 _root= cur->_right;
			}
			else
			{
				if (parent->_left == cur)
				{
					parent->_left = cur->_right;
				}
				else
				{
					parent->_right = cur->_right;
				}
			}
			
		}
		else if (cur->_right == nullptr)//情况c
		{
			if (cur == _root)
			{
				 _root=cur->_left;
			}
			else
			{
				if (parent->_right == cur)
				{
					parent->_right= cur->_left;
				}
				else
				{
					parent->_left = cur->_left;
				}
			}
		}
		else//情况d
		{

			Node* pcur = cur->_left;//找左子树中最大值
			parent = cur;
			while (pcur->_right != nullptr)//找到左子树的最大值
			{
				parent = pcur;
				pcur = pcur->_right;
			}
			swap(pcur->_data, cur->_data);//替换
			if (parent->_left == pcur)//最右边的结点还有左子树·,但是没有右子树了
			{
				parent->_left = pcur->_left;
			}
			else//大部分都是这种情况
			{
				parent->_right =pcur->_left;
			}
			cur = pcur;
		}
		delete cur;
		return true;
	}

递归版本:

bool EraseR(const k& data)
	{
		return _EraseR(_root, data);
	}
bool _EraseR(Node*& root, const k& data)
	{
		if (root == nullptr)
			return false;
			
		if (root->_data < data)
		{
			return _EraseR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _EraseR(root->_left, data);
		}
		else//找到了删除的结点
		{
			Node* del = root;
			if (root->_left == nullptr)//左为空
			{
				 root = root->_right;//然后父亲指向我的右,不需要判断是父节点右还是左,传进来是什么就是什么,是父亲结点的指针的引用
			}
			else if (root->_right == nullptr)//右为空
			{
				root = root->_left;
			}
			else
			{
				Node* leftmax = root->_left;
				while (leftmax->_right)
				{
					leftmax = leftmax->_right;
				}
				swap(leftmax->_data, root->_data);

				return _EraseR(root->_left, data);//递归去删除替换后的结点
			}
			
			delete del;
			return true;
		}
	}

大家好好这个引用,节画画图来理解一下

2.5拷贝构造

这个需要一个一个的拷贝:两个版本是一样的

BSTree(const BSTree<k>& t)
	{
		_root = Copy(t._root);

	}
Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

2.6析构函数

~BSTree()
	{
		Destroy(_root);
	}

	void Destroy(Node*& root)
	{`在这里插入代码片`
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}

2.7赋值运算符

BSTree<k>& operator=(BSTree<k> t)
	{
		swap(_root, t._root);
		return *this;
	}

至此我们的两个版本的二叉搜索树就模拟实现完成了。

三、二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。

四、二叉搜索树的应用

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
    以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
    在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

我们上面实现的写法就是k模型

  1. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:

比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文英文单词与其对应的中文<word, chinese>就构成一种键值对;

在这里插入图片描述

再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对
在这里插入图片描述


在这里插入图片描述

我们来看测试代码:

#include<iostream>
using namespace std;
template<class k, class v >
struct BSNode
{
	BSNode<k,v>* _left;
	BSNode<k,v>* _right;
	k _data;
	v _value;

	BSNode(const k& data = k(), const v& value = v())
		:_left(nullptr)
		, _right(nullptr)
		, _data(data)
		,_value(value)
	{}
};


template<class k,class v>
class BSTreeRKV
{
	typedef BSNode<k,v> Node;
public:
	BSTreeRKV() {}

	BSTreeRKV(const BSTreeRKV<k,v>& t)
	{
		_root = Copy(t._root);
	}

	BSTreeRKV<k,v>& operator=(BSTreeRKV<k,v> t)
	{
		swap(_root, t._root);
		return *this;
	}


	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool InsertRKV(const k& data,const v& value)
	{
		return _InsertRKV(_root, data,value);
	}

	Node* FindRKV(const k& data)
	{
		return _FindRKV(_root, data);
	}

	bool EraseRKV(const k& data)
	{
		return _EraseRKV(_root, data);
	}

	~BSTreeRKV()
	{
		Destroy(_root);
	}
private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}


	bool _EraseRKV(Node*& root, const k& data)
	{
		if (root == nullptr)
			return false;
		if (root->_data < data)
		{
			return _EraseRKV(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _EraseRKV(root->_left, data);
		}
		else
		{
			Node* del = root;
			if (root->_left == nullptr)//左为空
			{
				root = root->_right;//然后父亲指向我的右,此不需要判断是父节点右还是左,传进来是什么就是什么,是父亲结点的指针的引用
			}
			else if (root->_right == nullptr)//右为空
			{
				root = root->_left;
			}
			else
			{
				Node* leftmax = root->_left;
				while (leftmax->_right)
				{
					leftmax = leftmax->_right;
				}
				swap(leftmax->_data, root->_data);

				return _EraseRKV(root->_left, data);
			}

			delete del;
			return true;

		}
	}

	Node* _FindRKV(Node* root, const k& data)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		if (root->_data < data)
		{
			return _FindRKV(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _FindRKV(root->_left, data);
		}
		else
		{
			return root;
		}
	}

	bool _InsertRKV(Node*& root, const k& data, const v& value)//传引用的好处就是得到父节点的指针,不用关心当前插入位置结点是父节点的哪个结点
	{
		if (root == nullptr)
		{
			root = new Node(data,value);
			return true;
		}
		if (root->_data < data)
		{
			return _InsertRKV(root->_right, data,value);
		}
		else if (root->_data > data)
		{
			return _InsertRKV(root->_left, data,value);
		}
		else
		{
			return false;
		}
	}


	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << ":"<<root->_value<<endl;
		_InOrder(root->_right);
	}
private:
	Node* _root;//根节点
};


void BSTRKV1()
{
	BSTreeRKV<string, string> b;
	b.InsertRKV("sort", "排序");
	b.InsertRKV("left", "左边");
	b.InsertRKV("right", "右边");
	string str;
	while (cin >> str)
	{
		auto* ret = b.FindRKV(str);
		if (ret != nullptr)
		{
			cout << ret->_value << endl;
		}
		else
		{
			cout << "没有此单词" << endl;
		}
	}
}

void BSTRKV2()
{
	BSTreeRKV<string, int> b;
	string str[] = { "苹果","香蕉","苹果","梨子","苹果","香蕉","梨子" };
	for (int i = 0; i < 7; i++)
	{
		auto* ret = b.FindRKV(str[i]);
		if (ret == nullptr)
		{
			b.InsertRKV(str[i], 1);
		}
		else
		{
			ret->_value++;
		}
	}

	b.InOrder();
}

五、非递归和递归的完整代码

非递归:

#include<iostream>
using namespace std;
template<class k>
struct BSNode
{
	BSNode<k>* _left;
	BSNode<k>* _right;
	k _data;

	BSNode(const k& data=k())
		:_left(nullptr)
		,_right(nullptr)
		,_data(data)
	{}
};

template<class k>
class BSTree
{
	typedef BSNode<k> Node;
public:
	BSTree(){}

	BSTree(const BSTree<k>& t)
	{
		_root = Copy(t._root);

	}

	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

	~BSTree()
	{
		Destroy(_root);
	}

	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}

	BSTree<k>& operator=(BSTree<k> t)
	{
		swap(_root, t._root);
		return *this;
	}
	bool insert(const k& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)//为了找到插入位置
		{
			parent = cur;
			if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(data);
		if (parent->_data > data)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}

		return true;
	}

	bool erase(const k& data)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				break;
			}
		}
		if (cur == nullptr)
		{
			return false;
		}

		if (cur->_left == nullptr)
		{
			if (cur == _root)
			{
				 _root= cur->_right;
			}
			else
			{
				if (parent->_left == cur)
				{
					parent->_left = cur->_right;
				}
				else
				{
					parent->_right = cur->_right;
				}
			}
			
		}
		else if (cur->_right == nullptr)
		{
			if (cur == _root)
			{
				 _root=cur->_left;
			}
			else
			{
				if (parent->_right == cur)
				{
					parent->_right= cur->_left;
				}
				else
				{
					parent->_left = cur->_left;
				}
			}
		}
		else
		{

			Node* pcur = cur->_left;//找左子树中最大值
			parent = cur;
			while (pcur->_right != nullptr)//找到最大值
			{
				parent = pcur;
				pcur = pcur->_right;
			}
			swap(pcur->_data, cur->_data);//替换
			if (parent->_left == pcur)//此时就左子树就一个结点
			{
				parent->_left = pcur->_left;
			}
			else//大部分都是这种情况
			{
				parent->_right =pcur->_left;
			}
			cur = pcur;
		}
		delete cur;
		return true;
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}


	bool find(const k& data)
	{
		if (_root == nullptr)
		{
			return false;
		}
		Node* cur = _root;
		while (cur)
		{
			if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}


private:
	Node* _root;//根节点
};

递归:

#include<iostream>
using namespace std;
template<class k>
struct BSNode
{
	BSNode<k>* _left;
	BSNode<k>* _right;
	k _data;

	BSNode(const k& data = k())
		:_left(nullptr)
		, _right(nullptr)
		, _data(data)
	{}
};
template<class k>
class BSTreeR
{
	typedef BSNode<k> Node;
public:
	BSTreeR() {}

	BSTreeR(const BSTreeR<k>& t)
	{
		_root = Copy(t._root);
		
	}

	BSTreeR<k>& operator=(BSTreeR<k> t)
	{
		swap(_root, t._root);
		return *this;
	}
	
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool InsertR(const k& data)
	{
		return _InsertR(_root, data);
	}

	bool FindR(const k& data)
	{
		return _FindR(_root, data);
	}

	bool EraseR(const k& data)
	{
		return _EraseR(_root, data);
	}

	~BSTreeR()
	{
		Destroy(_root);
	}
private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}

	bool _EraseR(Node*& root, const k& data)
	{
		if (root == nullptr)
			return false;
		if (root->_data < data)
		{
			return _EraseR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _EraseR(root->_left, data);
		}
		else
		{
			Node* del = root;
			if (root->_left == nullptr)//左为空
			{
				 root = root->_right;//然后父亲指向我的右,此不需要判断是父节点右还是左,传进来是什么就是什么,是父亲结点的指针的引用
			}
			else if (root->_right == nullptr)//右为空
			{
				root = root->_left;
			}
			else
			{
				Node* leftmax = root->_left;
				while (leftmax->_right)
				{
					leftmax = leftmax->_right;
				}
				swap(leftmax->_data, root->_data);

				return _EraseR(root->_left, data);
			}
			
			delete del;
			return true;
			
		}
	}

	bool _FindR(Node* root, const k& data)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (root->_data < data)
		{
			return _FindR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _FindR(root->_left, data);
		}
		else
		{
			return true;
		}
	}

	bool _InsertR(Node*& root, const k& data)//传引用的好处就是得到父节点的指针,不用关心当前插入位置结点是父节点的哪个结点
	{
		if (root == nullptr)
		{
			root = new Node(data);
			return true;
		}
		if (root->_data < data)
		{
			return _InsertR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _InsertR(root->_left, data);
		}
		else
		{
			return false;
		}
	}


	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}
private:
	Node* _root;//根节点
};

六、总结

我们二叉搜索树实现起来还是比较简单的,要考虑的东西并不是特别多,但是二叉搜索树有最坏的情况,所以我们后面学的AVL树和红黑树可以解决这问题,其次连哥哥版本的实现,递归版本的代码两少很多,尤其在删除的时候,巧妙的使用了引用,希望大家下去画画图,理解一下怎么使用,接下来我们将通过一篇博客用刷题的刷题的方式,带大家再来更好的学习二叉树相关的知识,我们下篇再见

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

橘柚!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值