【数据结构】AVL树

前言

在C++的STL标准库中的set、map、multiset和multimap的底层实现都是二叉搜索树。

而进行搜索数据的方式有:

  1. 暴力搜索。
  2. 二分搜索(缺陷:二分查找需要有序数组,伴随着插入,维护的成本较高)
  3. 二叉搜索树(缺陷:在极端场景下会退化成链表,其效率无法保证)
  4. 二叉平衡搜索树
  5. 多叉平衡搜索树:B树
  6. 哈希表

由于二叉搜索树的自身缺陷,假如往树中插入的元素有序或者接近有序,二叉树就会退化成为单支树,时间复杂度为O(N),因此map、set等关联式容器的底层结构式对二叉树进行了平衡处理,及采用平衡树来实现。而AVL树的实现就是二叉平衡平衡树。

AVL树的概念

        二叉搜索树虽然可以缩短查找的效率,但是如果数据有序或者接近有序,二叉搜索树就会退化成为单支树,查找元素相当于在顺序表里查找,效率低下。

        在1962年,俩位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis发明了一种解决上述问题的方法:当向二叉搜索树中插入新节点后,如果可以保证每一个节点的左右子树高度之差的绝对值不超过1(需要对树中的节点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者式空树,或者是具有以下性质的二叉搜索树:

  • 其左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

平衡因子的计算方式:平衡因子=右子树高度-左子树高度

AVL树的本质是高度平衡二叉搜索树,其二叉树只有在满二叉树的情况,平衡因子才会相等,否则进行增加与删除操作的时候,平衡因子肯定会被改变。

如果一棵二叉搜索树是高度平衡的,其就是AVL树。

如果有n个节点,其高度可以保持在O(logN),搜索时间复杂度O(logN)。

  • 当为满二叉树:2^h-1=N
  • AVL树:2^h-x= N(x的范围:1<x<2^(h-1))

AVL树的模拟实现

AVL树的基本框架

template<class k, class v>
struct AVLTreeNode
{
	//构造
	AVLTreeNode(const k& date)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_kv(kv)
		,_bf(0)
	{}

	AVLTreeNode<k, v>* _left;
	AVLTreeNode<k, v>* _right;
	AVLTreeNode<k, v>* _parent;
	pair<k,v> _kv;
	int _bf;//平衡因子
};

template<class k, class v>
class AVLTree
{
	typedef AVLTreeNode<k,v> Node;
	AVLTree()
		:_node(nullptr)
	{}

private:
	Node* _root;
};

AVL树的插入

        AVL树就是在二叉搜索树的基础之上引入了平衡因子,所以在插入节点的同时可以参考AVL树,然后通过结合平衡因子来解决问题。

其核心步骤分为三步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子
  3. 根据平衡因子观察是否达到平衡,如果没有则进行旋转。

插入新节点

	bool Insert(const pair<k,v>& kv)
	{
		//插入新节点
		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->_left;
			}
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		cur = new Node<kv>;
		if (cur->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			assert(false);
		}
		cur->_parent = parent;
		//计算平衡因子
	}

平衡因子的计算

        当新的节点插入之后,AVL树的平衡性可能会被破坏,此时就需要更新破坏因子,并检测是否破坏了AVL树的平衡性。

  • 平衡因子=右子树的高度-左子树的高度。

观察这棵已经是AVL树的每一个平衡因子,当cur插入之后,parent的平衡因子一定会被调整,在插入cur之前,parent的平衡因子一共会分成三种情况:-1/0/1。

当插入cur之后,可能出现的结果如下:

  1. 新增在左,parent的平衡因子减1。
  2. 新增在右,parent的平衡因子加1。
  3. 更新后parent的平衡因子==0,说明parent所在的子树高度不变,不会影响祖先,不用再继续沿着root的路径往上更新。
  4. 更新后parent的平衡因子==1 or ==-1,说明parent所在的子树高度变化,会对祖先造成影响,需要再继续沿着到root的路径往上更新。
  5. 更新后parent的平衡因子==2 or ==-2,说明parent所在的子树高度已经变化,并且不平衡,需要立刻对parent所在的子树进行旋转,让其平衡。
  6. 平衡因子向上更新到根节点——>插入结束。

我们在更新平衡因子的时候可以发现,平衡因子相当于是用于检测这棵AVL树的平衡的,平衡因子的区间是[-2,2]。

	bool Insert(const pair<k,v>& kv)
	{
		//插入新节点
		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->_left;
			}
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		cur = new Node<kv>;
		if (cur->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			assert(false);
		}
		cur->_parent = parent;
		//更新平衡因子
		while (parent)
		{
			//第一、二步:更新parent的平衡因子
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else if (cur == parent->_right)
			{
				parent->_bf++;
			}
			//第三步:更新后parent的平衡因子==0
			if (parent->_bf == 0)
			{
				break;
			}
			//第四步:更新后parent的平衡因子==1 or ==-1
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = cur->_parent;
			}
			//第五步:更新后parent的平衡因子==2 or ==-1
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//旋转
				break;
			}
			else
			{
				assert(false);
			}
		}
	}

AVL树的旋转

        在一棵原本是是AVL平衡树中插入一个节点,可能会造成不平衡,此时必须调整树的结构,使之平衡。

        根据节点的插入位置的不同,AVL树的旋转会分为4种:

  1. 新节点插入较高左子树的左侧——右单旋
  2. 新节点插入较高右子树的右侧——左单旋
  3. 新节点插入较高左子树的右侧——先左单旋再右单旋
  4. 新节点插入较高右子树的左侧——先右单旋再左单旋

下面将进行详细讲解:

首先,旋转之前需要注意的俩个问题:

  • 保持其是搜索树
  • 变成平衡树且降低这个子树的高度。
左单旋 

在上述的情况中,h的可能值为0,1,2,3,4...,a,b,c是符合AVL树的子树。

当h==2的时候,a和b是x,y,z中的一种,c只能是z,然后在这种情况下,才会导致M、N之间的旋转。

在插入前的可能形状的组合:3*3*1=9种;新元素可能插入的位置存在4种;这样当h==2的时候,合计情况有36种。

【注意】我们在插入新元素之后,会导致parent的平衡因子成为2,cur的平衡因子成为1,在这种情况下,需要左单旋转操作。

在进行左单旋操作的核心操作是:

  1. 将b转移到M的右子树之下;
  2. 如果b不等于空,将b的父亲转换为M;如果b为空,则不进行操作;
  3. 将M转移到N的左子树之下;
  4. 记录M的父亲
  5. 将M的父亲转换为N;
  6. 将记录的N的原父转换为N的父亲;并判断M是原父的左子还是右子。

这里可以总结:需要进行改变的数据一共有:parent的parent的孩子的指向,parent的右孩子的指向,parent的父亲指向,cur的左孩子指向,cur的父亲指向,cur的左孩子的父亲指向。

代码展示:

//左单旋
void RotateL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curleft = cur->_left;

	parent->_right = curleft;
	if (curleft)
	{
		curleft->_parent = parent;
	}
	cur->_left = parent;
	Node* pparent = parent->_parent;
	parent->_parent = cur;
	if (pparent == nullptr)
	{
		_root = cur;
		cur->_parent = nullptr;
	}
	else
	{
		if (pparent->_left == parent)
		{
			pparent->_left = cur;
		}
		else //if(pparent->_right == parent)
		{
			pparent->_right = cur;
		}
		cur->_parent = pparent;
	}
	parent->_bf = cur->_bf = 0;
}

 

右单旋

右单旋与左单旋的方式类似,方式如图:

代码展示:

//右单旋
void RotateR(Node* parent)
{
	Node* cur = parent->_left;
	Node* curright = cur->_right;
	parent->_left = curright;
	if (curright)
	{
		curright->_parent = parent;
	}
	cur->_right = parent;
	Node* pparent = parent->_parent;
	parent->_parent = cur;
	if (pparent == nullptr)
	{
		_root = cur;
		cur->_parent = nullptr;
	}
	else
	{
		if (pparent->_left == parent)
		{
			pparent->_left = cur;
		}
		else
		{
			pparent->_right = cur;
		}
		cur->_parent = pparent;
	}
	parent->_bf = cur->_bf = 0;
}
右左旋转

        在了解完左单旋和右单选之后,我们基本对这俩种单旋转方式有了基本的认识,这俩种单旋的方式都是直线插入的,而在进行折线插入的时候需要实现另外一种方法。

【注意】我们可以在c位置的左子树位置或右子树位置添加节点。

当h==2的时候:

a和d是x,y,z中的任意一棵子树,c只能是z这棵子树;

这样合计下来:一共有3*3*4=36种情况。(3*3是a,d之前的组合;4是当h等于2的时候,可以新增的数据个数)。

此时可以发现,使用单旋的方式已经不能解决问题,需要使用双旋转的方式来解决问题:

  1. 以60为旋转点,进行右单旋转
  2. 以30为旋转点,进行左单旋转

右左双旋转的结果本质上是:

  • 60为根
  • 60的左边是30
  • 60的右边是90
  • 60的左边给了30的右边
  • 60的右边给了30的左边

通过对图例的了解,当h==0的时候,60的左右子树都为空,即60是新增的元素;当h>0的时候,在60的左子树或者60的右子树新增元素,才会构成右左旋转。

处理节点的平衡因子的时候,可以将平衡因子分成3份,判断的主要条件是60在旋转之前的的平衡因子:

  • 当60旋转之前的平衡因子为0,则旋转之后30,60,90的平衡因子都为0;
  • 当60旋转之前的平衡因子为1,则旋转之后60,90的平衡因子为0,30的平衡因子为-1;
  • 当60旋转之前的平衡因子为-1,则旋转之后60,30的平衡因子为0,90的平衡因子为1;

则进行代码展示:

	//左右旋转
	void RotateLR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		int bf = curright->_bf;

		RotateL(cur);
		RotateR(parent);

		if (bf == 0)
		{
			curright->_bf = 0;
			parent->_bf = 0;
			cur->_bf = 0;
		}
		else if (bf == 1)
		{
			curright->_bf = 0;
			parent->_bf = -1;
			cur->_bf = 0;
		}
		else if (bf == -1)
		{
			curright->_bf = 0;
			parent->_bf = 0;
			cur->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}

 

左右旋转

左右旋转与右左旋转的思路相同,即:先对30进行左单选,然后再对90进行右单旋,旋转之后考虑平衡因子的更新。

处理节点的平衡因子的时候,可以将平衡因子分成3份,判断的主要条件是60在旋转之前的的平衡因子:

  • 当60旋转之前的平衡因子为0,则旋转之后30,60,90的平衡因子都为0;
  • 当60旋转之前的平衡因子为1,则旋转之后30,60的平衡因子为0,30的平衡因子为-1;
  • 当60旋转之前的平衡因子为-1,则旋转之后60,90的平衡因子为0,90的平衡因子为1;

则进行代码展示:

//右左旋转
void RotateRL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curleft = cur->_left;
	int bf = curleft->_bf;

	RotateR(cur);
	RotateL(parent);

	if (bf == 0)
	{
		curleft->_bf = 0;
		parent->_bf = 0;
		cur->_bf = 0;
	}
	else if (bf == 1)
	{
		curleft->_bf = 0;
		parent->_bf = 0;
		cur->_bf = -1;
	}
	else if (bf == -1)
	{
		curleft->_bf = 0;
		parent->_bf = 0;
		cur->_bf = 1;
	}
	else
	{
		assert(false);
	}
}

AVL树的验证

       AVL树由于是再二叉搜索树的基础之上加入了平衡性的因素,因此如果需要验证AVL树,可以分为俩步:

1.验证是否为二叉搜索树:

如果中序遍历可以得到一个有序的序列,就可以说明是二叉搜索树。

2.验证是否为平衡树

  • 每一个节点子树的高度差的绝对值不会超过1(注意节点中如果没有平衡因子)
  • 节点的平衡因子的计算是否正确
	int Height(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

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

 

AVL树的删除

        由于AVL树也是二叉搜索树,可以按照二叉搜索树的方式将节点删除,步骤如下:

  1. 按照搜索树的规则查找节点删除
  2. 更新平衡因子,当cur为0的时候,更新parent的平衡因子
  3. 出现异常旋转。

AVL树的性能

        AVL树是一棵绝对平衡的二叉搜索树,要求每一个节点的左右子树高度之差的绝对值不能超过1,这样可以保证在查询的时候具有高效性,时间复杂度为logN。

        但是对于AVL树的一些结构修改的操作性能是非常低的,就比如:插入时需要维护其平衡,旋转的次数比较多,更差的是在删除的时候,有可能一直让旋转持续到根的位置。

代码展示

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

template<class k, class v>
struct AVLTreeNode
{
	//构造
	AVLTreeNode(const pair<k,v>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_kv(kv)
		,_bf(0)
	{}

	AVLTreeNode<k, v>* _left;
	AVLTreeNode<k, v>* _right;
	AVLTreeNode<k, v>* _parent;
	pair<k,v> _kv;
	int _bf;//平衡因子
};

template<class k, class v>
class AVLTree
{
public:
	typedef AVLTreeNode<k,v> Node;
	AVLTree()
		:_root(nullptr)
	{}

	bool Insert(const pair<k,v>& kv)
	{
		//插入新节点
		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->_left;
			}
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			assert(false);
		}
		cur->_parent = parent;
		//更新平衡因子
		while (parent)
		{
			//第一、二步:更新parent的平衡因子
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else if (cur == parent->_right)
			{
				parent->_bf++;
			}
			//第三步:更新后parent的平衡因子==0
			if (parent->_bf == 0)
			{
				break;
			}
			//第四步:更新后parent的平衡因子==1 or ==-1
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = cur->_parent;
			}
			//第五步:更新后parent的平衡因子==2 or ==-1
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//旋转
				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)
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				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* pparent = parent->_parent;
		parent->_parent = cur;
		if (pparent == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = cur;
			}
			else //if(pparent->_right == parent)
			{
				pparent->_right = cur;
			}
			cur->_parent = pparent;
		}
		parent->_bf = cur->_bf = 0;
	}
	//右单旋
	void RotateR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		parent->_left = curright;
		if (curright)
		{
			curright->_parent = parent;
		}
		cur->_right = parent;
		Node* pparent = parent->_parent;
		parent->_parent = cur;
		if (pparent == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = cur;
			}
			else
			{
				pparent->_right = cur;
			}
			cur->_parent = pparent;
		}
		parent->_bf = cur->_bf = 0;
	}
	//左右旋转
	void RotateLR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		int bf = curright->_bf;

		RotateL(cur);
		RotateR(parent);

		if (bf == 0)
		{
			curright->_bf = 0;
			parent->_bf = 0;
			cur->_bf = 0;
		}
		else if (bf == 1)
		{
			curright->_bf = 0;
			parent->_bf = -1;
			cur->_bf = 0;
		}
		else if (bf == -1)
		{
			curright->_bf = 0;
			parent->_bf = 0;
			cur->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}
	//右左旋转
	void RotateRL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		int bf = curleft->_bf;

		RotateR(cur);
		RotateL(parent);

		if (bf == 0)
		{
			curleft->_bf = 0;
			parent->_bf = 0;
			cur->_bf = 0;
		}
		else if (bf == 1)
		{
			curleft->_bf = 0;
			parent->_bf = 0;
			cur->_bf = -1;
		}
		else if (bf == -1)
		{
			curleft->_bf = 0;
			parent->_bf = 0;
			cur->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}

	int Height(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)
		{
			cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;
			return false;
		}
		
		return abs(rightHeight - leftHeight) < 2
			&& IsBalance(root->_left)
			&& IsBalance(root->_right);
				
	}
private:
	Node* _root;
};

  • 128
    点赞
  • 124
    收藏
    觉得还不错? 一键收藏
  • 112
    评论
AVL树是一种自平衡二叉搜索树,它的平衡因子(左子树高度减去右子树高度)在任何时候都是-1、0或1。当插入或删除节点后,如果AVL树失去平衡,则需要通过旋转来重新平衡它。 在Python中实现AVL树,可以使用节点类和AVL树类。节点类包括节点值、左子树、右子树、高度和平衡因子等属性。AVL树类包括根节点、插入节点、删除节点、旋转方法和平衡方法等方法。 下面是一个简单的Python实现AVL树的代码示例: ```python class Node: def __init__(self, val): self.val = val self.left = None self.right = None self.height = 1 self.balance = 0 class AVLTree: def __init__(self): self.root = None def insert(self, val): self.root = self._insert(self.root, val) def _insert(self, node, val): if not node: return Node(val) if val < node.val: node.left = self._insert(node.left, val) else: node.right = self._insert(node.right, val) node.height = 1 + max(self.get_height(node.left), self.get_height(node.right)) node.balance = self.get_balance(node) if node.balance > 1 and val < node.left.val: return self.right_rotate(node) if node.balance < -1 and val > node.right.val: return self.left_rotate(node) if node.balance > 1 and val > node.left.val: node.left = self.left_rotate(node.left) return self.right_rotate(node) if node.balance < -1 and val < node.right.val: node.right = self.right_rotate(node.right) return self.left_rotate(node) return node def delete(self, val): self.root = self._delete(self.root, val) def _delete(self, node, val): if not node: return node if val < node.val: node.left = self._delete(node.left, val) elif val > node.val: node.right = self._delete(node.right, val) else: if not node.left or not node.right: temp = node.left if node.left else node.right if not temp: node = None else: node = temp else: temp = self.get_min(node.right) node.val = temp.val node.right = self._delete(node.right, temp.val) if not node: return node node.height = 1 + max(self.get_height(node.left), self.get_height(node.right)) node.balance = self.get_balance(node) if node.balance > 1 and self.get_balance(node.left) >= 0: return self.right_rotate(node) if node.balance < -1 and self.get_balance(node.right) <= 0: return self.left_rotate(node) if node.balance > 1 and self.get_balance(node.left) < 0: node.left = self.left_rotate(node.left) return self.right_rotate(node) if node.balance < -1 and self.get_balance(node.right) > 0: node.right = self.right_rotate(node.right) return self.left_rotate(node) return node def right_rotate(self, node): left_child = node.left node.left = left_child.right left_child.right = node node.height = 1 + max(self.get_height(node.left), self.get_height(node.right)) left_child.height = 1 + max(self.get_height(left_child.left), self.get_height(left_child.right)) node.balance = self.get_balance(node) left_child.balance = self.get_balance(left_child) return left_child def left_rotate(self, node): right_child = node.right node.right = right_child.left right_child.left = node node.height = 1 + max(self.get_height(node.left), self.get_height(node.right)) right_child.height = 1 + max(self.get_height(right_child.left), self.get_height(right_child.right)) node.balance = self.get_balance(node) right_child.balance = self.get_balance(right_child) return right_child def get_height(self, node): if not node: return 0 return node.height def get_balance(self, node): if not node: return 0 return self.get_height(node.left) - self.get_height(node.right) def get_min(self, node): while node.left: node = node.left return node ``` 这个实现包括插入、删除、旋转和平衡等基本操作。你可以按需调用这些方法,来实现你的具体需求。
评论 112
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值