【数据结构】&&【C++】平衡搜索二叉树的模拟实现(AVL树)

一.AVL树的性质

AVL树就是平衡搜索二叉树。因为搜索二叉树虽然可以提高查找效率,但当数据有序时,具有缺陷,有可能会退化成单支树,查找元素就相当于在线性表中查找。效率就降低变成O(N)级别。因此为了解决上面的问题,有人发明AVL树来解决上面的问题。
方法:当向二叉搜索树中插入新结点时,如果能保证每个结点的左右子树的高度差的绝对值不超过1.那么这样就可以形成一个高度平衡的树,因为这样可以降低树的高度,从而减少平均搜索长度。

【性质】:

1.AVL树可以是一颗空树
2.或者是一颗平衡的二叉搜索树
3.如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。

如何理解平衡?

1.它的子树都是AVL树
2.它的所有左右子树的高度差的绝对值不超过1.可以是-1,可以是0,可以是1.
3.左右子树的高度差也叫做平衡因子bf。
4.在每次插入结点之前,这颗树就是AVL树。

在这里插入图片描述

二.AVL树的模拟实现

①.AVL树结点的定义

AVL树其实就在搜索二叉树的基础上加了平衡因子。不过要注意的是这颗结点是三叉链,多出一个父指针。这个父指针是方便用来向上更新平衡因子的。
正常来说搜索树是K结构(存一个数据),不过也可以是KV结构的(存两个数据)。我们这里直接用KV结构。也就是结点里存的是两个数据一个K,一个V。
而我们可以直接用pair类来存这个两个数据。然后直接将pair类对象存在结点里。
所有结点里存的是pair类的数据。然后还有左右指针和父指针,还有平衡因子。

template <class K, class V>

struct NodeTree
{
	int _bf;//平衡因子
	pair<K, V> _kv;//结点里存的数据
	NodeTree<K, V>* _left;
	NodeTree<K, V>* _right;
	NodeTree<K, V>* _parent;//为什么要多增加一个父指针呢?为了后面往上更新平衡因子方便

	NodeTree(const pair<K, V>& kv)//构造函数
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{ }

};

②. AVL树的插入

AVL树的插入其实就是在搜索树的插入基础上加上了更新平衡因子这一步。
前面都是和搜索树的插入是基本一样的(区别在于还要调整父指针)

bool Insert(const pair<K, V>& kv)
	{
		//AVL树的插入就是搜索树的插入+更新平衡因子
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		//说明该二叉树不是空树,那么就进行比较找到位置
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				//记录结点的位置
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		//走到这里表明cur为空了,表明位置已经找到了
		cur = new Node(kv);

		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		//注意这个是三叉链,还要注意父指针
		cur->_parent = parent;

AVL最核心的步骤就在于平衡因子的调整!
当插入一个结点时,插入结点的平衡因子肯定为0。

1.当插入结点在父节点的右边时,那么父节点的平衡因子bf就要++。
2.当插入结点在于父节点的左边时,那么父节点的平衡因子bf就要–。

在这里插入图片描述

3.当父节点的平衡因子等于0时,这说明原来的父结点的平衡因子原来肯定是1或者-1,然后插入一个结点后,parent的平衡因子加加或者减减变成0了。弥补上parent的另外一个缺漏的结点。但要注意该子树的高度并没有变化。只是在另外一端加上一个结点。这个时候子树的高度没有改变,那么就不会影响上面的祖先的平衡因子的改变,就不需要往上更新。
4.当父结点的平衡因子等于1或者-1时,这说明原来的父节点的平衡因子一定是0,然后插入一个结点后,parent的平衡因子加加或者减减变成1或者-1。一旦平衡因子变成-1或者1,就说明这颗子树的高度改变了,子树的高度改变了就会影响祖先的高度,就会影响祖先的平衡因子,所有就要往上继续更新平衡因子。
5.向上更新平衡因子后,如果parent的平衡因子等于-2或者2,这说明这颗树"生病"了。不健康了,也就是parent所在的这颗子树高度严重不平衡了,正常的AVL树的高度差是的绝对值不超过1。那么我们就要对这颗子树进行调整。利用旋转让子树平衡。
6.当更新到根节点后,就不要再往上更新了。

在这里插入图片描述

③.平衡因子的更新

平衡因子的更新需要理解下面的更新思路:
在这里插入图片描述

        while (parent)//往上不断更新,直到更新到根结点,根结点上面就不需要更新了
		{
			if (cur == parent->_left)//当插入的结点在parent结点的左边时,parent的平衡因子减减
			{
				parent->_bf--;
			}
			else//当插入的结点在parent结点的左边时,parent的平衡因子加加
			{
				parent->_bf++;
			}
			if (parent->_bf == 0)//更新后,如果parent的平衡因子等于0,说明这颗子树的高度并没有改变,不需要更新平衡上面的平衡因子了。直接出去
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)//更新后,如果parent的平衡因子等于1或者-1,说明这棵树的高度发生变化,需要往上更新那写祖先结点的平衡因子
			{
				//说明该子树高度改变,会影响祖先,需要往上更新平衡因子
				cur = parent;
				parent = parent->_parent;
			
			}
			else if (parent->_bf == 2 || parent->_bf == -2)//更新后,如果parent的平衡因子等于-2或者2,说明这颗子树的高度已经严重不平衡了,需要我们使用旋转的手段来让它平衡!
			{
				//说明这个树生病了,需要调整,旋转,将树平衡
				//要求旋转后,仍然是一个颗搜索树
				// 旋转后平衡,高度变低
	            … …… …… ……
	            … …… …… ……
			else//如果还出现其他情况,就说明在插入之前这颗树就已经不平衡了,但还是要判断一下。
			{
					assert(false);
			}
			
		}
		return true;
	}

当parent的平衡因子为2或者-2,说明这个树生病了高度不平衡了,需要调整,旋转,将树平衡。
而旋转时需要注意下面的细节:
1.旋转后仍然保持是搜索树。
2.旋转后变平衡,高度会降低。

那怎么旋转呢?
旋转的情况有很多种,有要左单旋的,有需要右单旋的,甚至要双旋。
我们一一来分类研究:

④.左单旋

什么情况会发生左旋呢?

①当单纯的右边高时,当parent更新到2时就会发生左旋!
②即parent的平衡因子为2,cur的平衡因子是1,这样就是单纯的右边高。

在这里插入图片描述
所以左旋的核心就是两个步骤:

①核心就是让cur的左结点成为parent的右结点,
②让parent成为cur的左结点。

旋转细节:

①首先要找到cur的位置和cur的左节点curleft。
②将cur的左节点curleft成为parent的右结点,要注意要将curleft的父指针也要指向parent(在curleft结点存在的前提下)
③将parent成为cur的左结点,要注意将parent的父指针改成cur。
④最后cur成为父结点位置,也要注意调整cur的父指针

这样做是因为要求旋转后还要保持是搜索树!

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

左单旋之后,cur结点就会充当父结点,而旋转后高度会降低,并且cur和parent的平衡因子都为0!
不过要注意这里的图形可能只是一颗子树,而不是整棵树,所以当cur为父节点时,cur的父指针有两种情况,一种就是原来parent的位置就是根结点,即parent的父指针就是空。那么cur的父指针就是空。当原来的parent的位置不是根结点,原来的parent的父指针就是cur的父指针。

//    不平衡的树情况有很多种
	if (parent->_bf == 2 && cur->_bf == 1)//这种情况是单纯右边高,左单旋即可解决
	{
		RotateL(parent);
	}
	    void RotateL(Node* parent)//左单旋
	{
	
		Node* cur = parent->_right;
		Node* pp = parent->_parent;//要提前记录parent的父指针,因为最后cur位置成为父结点的位置,那么cur的父指针是谁呢?需要用pp来判断
		Node* curleft = cur->_left;
		parent->_right = curleft;
		if (curleft)//如果cur的左节点不存在那就不用弄
		{
			curleft->_parent = parent;//调整父指针
		}
		cur->_left = parent;
	
		parent->_parent = cur;//调整父指针
	
		
	
		if (parent==_root)
		{
			//那么这样cur就是根结点了
			_root = cur;
			cur->_parent = nullptr;
		}
		else//cur就不是根结点,pp是它的父指针,要根据原来parent的位置来确定cur在pp的左边还是右边
		{
			if (pp->_left == parent)
			{
				pp->_left = cur;
			}
			else
			{
				pp->_right = cur;
			}
	
			cur->_parent = pp;
		
		}
		cur->_bf = parent->_bf = 0;
			//旋转后cur和parent bf都为0
	}

⑤.右单旋

什么情况会发生右旋呢?

①当单纯的左边高时,当parent更新到-2时就会发生右旋!
②即parent的平衡因子为-2,cur的平衡因子是-1,这样就是单纯的左边高。

在这里插入图片描述

右旋的核心步骤

①将cur的右边给parent的左边。
②将parent作为cur的右边

右旋的细节:

①首先找到cur的位置,和cur的右结点curright。
②将cur的右边curright给parent的左边,要注意将curright的父指针也要指向parent.(curright结点存在的前提下)
③将parent作为cur的右边,要注意将parent的父指针也要指向cur。
④当cur变成父结点后,也要注意cur的父指针的调整。
⑤最后旋转后,cur和parent的平衡因子都为0。

在这里插入图片描述
右单旋之后,cur结点就会充当父结点,而旋转后高度会降低,并且cur和parent的平衡因子都为0!
不过要注意这里的图形可能只是一颗子树,而不是整棵树,所以当cur为父节点时,那cur的父指针是谁呢?有两种情况,一种就是原来parent的位置就是根结点,即parent的父指针就是空。那么cur的父指针就是空。当原来的parent的位置不是根结点,原来的parent的父指针就是cur的父指针。

else if (parent->_bf == -2 && cur->_bf == -1)//这种情况是单纯左边高,右单旋即可解决
	{
		RotateR(parent);
	}
void RotateR(Node* parent)//右单旋
	{
		Node* cur = parent->_left;
       Node* ppnode = parent->_parent;//提前记录parent的父指针,因为后面会修改parent的父指针位置
		Node* curright = cur->_right;
		
		parent->_left = curright;
		if (curright)
		{
			curright->_parent = parent;//调整curright的父指针
		}
		
		cur->_right = parent;
		parent->_parent = cur;//调整parentt的父指针

		if (ppnode == nullptr)
		{
			//说明cur就变成根节点了
			_root = cur;
			cur->_parent = nullptr;
		}
		else//说明ppnode就是cur的父指针,但需要根据parent和ppnode位置来判断
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;//调整cur的父指针
		}
		cur->_bf = parent->_bf = 0;
	}

⑥.双旋(左右旋/右左旋)

双旋发生在什么情况下呢?

①不是单纯一边高时,出现曲折,不是一条直线样子。
②比如parent的平衡因子为2,cur的平衡因子为-1时,就说明parent相对于cur在左边,而新增结点相对于cur是在右边,这样的情况就不是一条直线,就需要先使用左旋让这条曲线变成直线,也就是单纯的左边高,然后再使用右旋对这个直线进行旋转!
③比如parent的平衡因子是-2,cur的平衡因子为1,就说明parent相对于cur在右边,而新增结点相遇于cur在左边,所以需要先用右旋让曲折地方变直也就变成了单纯的右边高,然后再用左旋对对这个直线旋转平衡!

双旋的核心步骤:

①就是复用两个单旋的核心步骤
②要根据图形来判断先左旋还是先右旋
③根据曲折点的平衡因子来确定谁的平衡因子需要调整,因双旋就是复用了两个单旋,最后cur和parent的平衡因子都会变成0,这个显然不行的。

双旋的细节

①先对cur进行旋转,再对parent进行旋转。
②平衡因子的调整,是根据插入位置的平衡因子来决定的,当插入在右边时,插入点的平衡因子就为1,那么最后由于右边这个结点会分给cur。所以最好cur的平衡因子就为0,而parent右边就缺少结点,所以parent的平衡因子就变成-1.
当插入左边时,插入点的平衡因子就为-1,那么最好由于左边的结点会分给parent,那么parent的平衡因子就为0,而cur左边就缺少结点,cur的平衡因子就为1.那么如果当插入位置的平衡因子为0,那么就不需要调整了。

在这里插入图片描述
在这里插入图片描述
以上是右左双旋的情况,也就是当parent的平衡因子为2,cur的平衡因子为-1时会发生!

else if (parent->_bf == 2 && cur->_bf == -1)//这种情况就是不是单纯的一边高了,而是出现曲线,一边高一边低,需要使用双旋这里需要先对cur使用右旋,再对parent使用左旋
	{
		RotateRL(parent);
	
	}
void RotateRL(Node* parent)
	{

		//双旋后注意还要调整平衡因子,因为双旋后将cur parent的平衡因子都置0了不合理
		//根据curleft的平衡因子来确定谁的平衡因子需要调整
		Node* cur = parent->_right;
		Node* curleft = cur->_left;

		RotateR(parent->_right);//先对cur使用右旋
		RotateL(parent);//再对parent左旋
		if (curleft->_bf == 0)//根据插入位置的平衡因子来确定是否需要调整平衡因子。
		{
			curleft->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 0;
		}
		else if (curleft->_bf == -1)
		{
			cur->_bf = 1;
			parent->_bf = 0;
			curleft->_bf = 0;
		}
		else if (curleft->_bf == 1)
		{
			parent->_bf = -1;
			cur->_bf = 0;
			curleft->_bf = 0;
		}
		//双旋后,就可以直接break了,
		else
		{
			assert(false);
		}
	}

那么另一种的左右双旋呢?当parent的平衡因子为-2,cur的平衡因子为1时就会发生左右双旋!
在这里插入图片描述

else if (parent->_bf == -2 && cur->_bf == 1)//这种情况也是折线,不是单纯的一边高,需要使用双旋,先使用左旋再使用右旋
		{
			RotateLR(parent);

		}
		void RotateLR(Node* parent)
     	{
		
		//先记录位置再能旋转
		// 
		//最后还需要调整平衡因子
		Node* cur = parent->_left;
		Node* curright = cur->_right;

		RotateL(parent->_left);//先对cur使用左旋
		RotateR(parent);//再对parent使用右旋
		if (curright->_bf == 0)
		{
			//减少耦合关系
			cur->_bf = 0;
			parent->_bf = 0;
			curright->_bf = 0;
		}
		else if (curright->_bf == -1)
		{
			cur->_bf = 0;
			parent->_bf = 1;
			curright->_bf = 0;
		}
		else if (curright->_bf == 1)
		{
			cur->_bf = -1;
			parent->_bf = 0;
			curright->_bf = 0;
		}
		//双旋后,就可以直接break了,
	
	}

⑧.AVL树的删除

这里的删除也是在搜索树删除结点的基础上再进行更新平衡因子的,原理是一致的,如果有兴趣可以搞一搞,这里只将大概思路列出来:

①按照搜索树的删除进行处理
②更新平衡因子
③出现异常后,就进行旋转处理

⑨.检查是否是AVL树

我们可以通过一个函数来判断我们的这颗树是否是AVL树,那写一个什么样的函数判断呢?
根据AVL树的性质:树的左右子树的高度差绝对值不超过1.我们可以将每个子树的高度都计算出来,然后用右子树减去左子树来算这颗子高度差,然后与平衡因子来比较就可以看出来是否一致。


	int Heigh(Node* root)//用来检查树的高度的
	{
 		if (root == nullptr)
			return 0;

		int HeightL = Heigh(root->_left);
		int HeightR = Heigh(root->_right);

		return HeightL > HeightR ? HeightL + 1 : HeightR + 1;
	}

	bool _isbalance(Node* root)//用来检查树是否平衡的
	{
		if (root == nullptr)
			return true;

		int highL = Heigh(root->_left);
		int highR = Heigh(root->_right);

		if (highR - highL != root->_bf)
		{
			cout << "平衡因子异常:" <<root->_kv.first<<"-"<< root->_bf << " " << endl;
			return false;
		}
		return abs(highR-highL)<2
			&& _isbalance(root->_left)
			&& _isbalance(root->_right);

	}
	bool isbalance()
	{
		return _isbalance(_root);
	}

三.完整代码

#pragma once
#include <iostream>
using namespace std;
#include<assert.h>
//首先定义结点

template <class K, class V>

struct NodeTree
{
	int _bf;//平衡因子
	pair<K, V> _kv;//结点里存的数据
	NodeTree<K, V>* _left;
	NodeTree<K, V>* _right;
	NodeTree<K, V>* _parent;//为什么要多增加一个父指针呢?为了后面往上更新平衡因子方便

	NodeTree(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{ }

};

template <class K, class V>
class AVLTree
{

	typedef NodeTree<K, V> Node;
public:

	bool Insert(const pair<K, V>& kv)
	{
		//AVL树的插入就是搜索树的插入+更新平衡因子
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		//说明该二叉树不是空树,那么就进行比较找到位置
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				//记录结点的位置
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		//走到这里表明cur为空了,表明位置已经找到了
		cur = new Node(kv);

		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		//注意这个是三叉链,还要注意父指针
		cur->_parent = parent;
		//正常插入结点已经完成,接下来AVL树还需要更新平衡因子
		while (parent)//往上不断更新,直到更新到根结点,根结点上面就不需要更新了
		{
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				//说明该子树高度改变,会影响祖先,需要往上更新平衡因子
				cur = parent;
				parent = parent->_parent;
			
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//说明这个树生病了,需要调整,旋转,将树平衡
				//核心就是让cur的左结点成为parent的右结点,让parent成为cur的左结点。
				//要求旋转后,仍然是一个颗搜索树
				// 旋转后平衡,高度变低


				//不平衡的树情况有很多种
				if (parent->_bf == 2 && cur->_bf == 1)//这种情况是单纯右边高,左单旋即可解决
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)//这种情况是单纯左边高,右单旋即可解决
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)//这种情况就是不是单纯的一边高了,而是出现曲线,一边高一边低,需要使用双旋这里需要先对cur使用右旋,再对parent使用左旋
				{
					RotateRL(parent);

				}
				else if (parent->_bf == -2 && cur->_bf == 1)//这种情况也是折线,不是单纯的一边高,需要使用双旋,先使用左旋再使用右旋
				{
					RotateLR(parent);

				}
				//最后调整完break出去
				break;
			}
			else
				{
					assert(false);
				}
			
		}
		return true;
	}
	void RotateL(Node* parent)//左单旋
	{

		Node* cur = parent->_right;
		
		Node* curleft = cur->_left;
		parent->_right = curleft;
		if (curleft)
		{
			curleft->_parent = parent;
		}
		cur->_left = parent;
		Node* pp = parent->_parent;
		parent->_parent = cur;



		if (parent==_root)
		{
			//那么这样cur就是根结点了
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (pp->_left == parent)
			{
				pp->_left = cur;
			}
			else
			{
				pp->_right = cur;
			}

			cur->_parent = pp;
			//旋转后cur和parent bf都为0?
		}
		cur->_bf = parent->_bf = 0;
	}
	void RotateR(Node* parent)//右单旋
	{
		Node* cur = parent->_left;

		Node* curright = cur->_right;
		
		parent->_left = curright;
		if (curright)
		{
			curright->_parent = parent;
		}
		Node* ppnode = parent->_parent;
		cur->_right = parent;
		parent->_parent = cur;

		if (ppnode == nullptr)
		{
			//说明cur就变成根节点了
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		cur->_bf = parent->_bf = 0;
	}
	void RotateRL(Node* parent)
	{

		//双旋后注意还要调整平衡因子,因为双旋后将cur parent的平衡因子都置0了不合理
		//根据curleft的平衡因子来确定谁的平衡因子需要调整
		Node* cur = parent->_right;
		Node* curleft = cur->_left;

		RotateR(parent->_right);//先对cur使用右旋
		RotateL(parent);//再对parent左旋
		if (curleft->_bf == 0)
		{
			curleft->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 0;
		}
		else if (curleft->_bf == -1)
		{
			cur->_bf = 1;
			parent->_bf = 0;
			curleft->_bf = 0;
		}
		else if (curleft->_bf == 1)
		{
			parent->_bf = -1;
			cur->_bf = 0;
			curleft->_bf = 0;
		}
		//双旋后,就可以直接break了,
		else
		{
			assert(false);
		}
	}
	void RotateLR(Node* parent)
	{
		
		//先记录位置再能旋转
		// 
		//最后还需要调整平衡因子
		Node* cur = parent->_left;
		Node* curright = cur->_right;

		RotateL(parent->_left);//先对cur使用左旋
		RotateR(parent);//再对parent使用右旋
		if (curright->_bf == 0)
		{
			//减少耦合关系
			cur->_bf = 0;
			parent->_bf = 0;
			curright->_bf = 0;
		}
		else if (curright->_bf == -1)
		{
			cur->_bf = 0;
			parent->_bf = 1;
			curright->_bf = 0;
		}
		else if (curright->_bf == 1)
		{
			cur->_bf = -1;
			parent->_bf = 0;
			curright->_bf = 0;
		}
		//双旋后,就可以直接break了,
	
	}
	
	

	int Heigh(Node* root)//用来检查树的高度的
	{
 		if (root == nullptr)
			return 0;

		int HeightL = Heigh(root->_left);
		int HeightR = Heigh(root->_right);

		return HeightL > HeightR ? HeightL + 1 : HeightR + 1;
	}

	bool _isbalance(Node* root)//用来检查树是否平衡的
	{
		if (root == nullptr)
			return true;

		int highL = Heigh(root->_left);
		int highR = Heigh(root->_right);

		if (highR - highL != root->_bf)
		{
			cout << "平衡因子异常:" <<root->_kv.first<<"-"<< root->_bf << " " << endl;
			return false;
		}
		return abs(highR-highL)<2
			&& _isbalance(root->_left)
			&& _isbalance(root->_right);

	}
	bool isbalance()
	{
		return _isbalance(_root);
	}

private:
	Node* _root=nullptr;
};
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小陶来咯

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

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

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

打赏作者

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

抵扣说明:

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

余额充值