[C++进阶]---AVL树模拟实现

1.AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

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

在这里插入图片描述
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n) ,搜索时间复杂度O( l o g 2 n log_2 n log2n)

2.AVL树模拟实现

2.1AVL树节点的定义

struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;	//该节点的左孩子
	AVLTreeNode<K, V>* _right;	//该节点的右孩子
	AVLTreeNode<K, V>* _parent;	//该节点的双亲
	int _bf;//该节点的平衡因子

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

2.2AVL的插入

假设下面这棵树是我们最开始AVL树,接下来我们需要对其进行节点的插入,并针对不同的情况有不同的处理情况
在这里插入图片描述
1.新增节点在parent节点的左边,parent的平衡因子减一(减减)
在这里插入图片描述

2.新增节点在parent节点的右边,parent的平衡因子加一(加加)
3.更新parent平衡因子==0,说明parent所在的子树的高度不变,不会再影响祖先,不用继续沿着到root的路径继续往上更新,插入结束

在这里插入图片描述

4.更新后parent平衡因子==1 or -1,说明parent所在的子树的高度变化,会再影响祖先,需要继续沿着到root的路径继续往上更新
5.更新后parent平衡因子==2 or -2,说明parent所在的子树的高度变化且不平衡,对parent所在子树进行旋转,让子树平衡,插入结束(涉及旋转下面将分情况进行讲解)
在这里插入图片描述
6.更新到根节点,没有出现parent平衡因子==2 or -2的情况更新结束
在这里插入图片描述

2.3AVL树的旋转

旋转的时候涉及的问题:

旋转之后的树需要满足两个条件:1.保持这棵树是二叉搜索树;2.变成平衡树(AVL树),且降低这个子树的高度

2.3.1左单旋

当更新parent(30)节点的平衡因子为2cur(60)节点的平衡因子为1,造成parent(30)所在子树右边高,需要进行左单旋
在这里插入图片描述
以下是h等于0和h等于1的具象图:
在这里插入图片描述
根据上面两种右边子树比较高的情况,需要将cur的左子树链接成为parent的右子树,让parent链接成为cur的左子树。情况一:插入之前,AVL树高度为1;插入节点后,树高度为2,旋转之后,树高度为1且达到平衡,不用继续沿着到root的路径继续往上更新,插入结束;情况二:插入之前,AVL树高度为2;插入节点后,树高度为3;旋转之后,树高度为2且达到平衡,不用继续沿着到root的路径继续往上更新,插入结束。
代码如下:

void RotateL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		//curleft成为parent的右边
		parent->_right = curleft;
		//curleft可能为空
		if (curleft)
		{
			//改变curleft父指针的指向
			curleft->_parent = parent;
		}
		//parent成为cur的左边
		cur->_left = parent;
		//记录parent的父节点指针
		Node* ppnode = parent->_parent;
		//改变parent父指针的指向
		parent->_parent = cur;
		//判断parent节点是否为根节点
		if (parent == _root)
		{
			//cur成为根节点
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			//cur链接parent的父节点
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else if (ppnode->_right == parent)
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		parent->_bf = cur->_bf = 0;
	}

2.3.2右单旋

当更新后,parent(30)节点的平衡因子为-2cur(60)节点的平衡因子为-1,造成parent(30)所在子树左边高,需要进行右单旋
在这里插入图片描述
以下是h等于0和h等于1的具象图:
在这里插入图片描述
情况一(h等于0): 插入之前,AVL树高度为1;插入节点后,树高度为2,旋转之后,树高度为1且达到平衡,不用继续沿着到root的路径继续往上更新,插入结束;情况二(h等于1): 插入之前,AVL树高度为2;插入节点后,树高度为3;旋转之后,树高度为2且达到平衡,不用继续沿着到root的路径继续往上更新,插入结束。
分析: 根据上面两种左边子树比较高的情况,需要将cur的右子树链接成为parent的左子树,让parent链接成为cur的左子树。局部根节点parent已经出现向左边倾斜的情况,当插入节点时导致parent所在子树左边高且不平衡,经过旋转后,cur成为新的局部根节点,cur所在子树的高度与未插入节点前的高度一致,不会对上层节点造成影响,达到平衡状态(即cur的平衡因子为0)

代码如下:

void RotateR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		//curright链接成为parent的左子树
		parent->_left = curright;
		if (curright)
		{
			//curright不为空情况进行,改变其父指针指向
			curright->_parent = parent;
		}
		//parent链接成为cur的右子树
		cur->_right = parent;
		//记录parent父指针
		Node* ppnode = parent->_parent;
		//让parent父指针指向cur
		parent->_parent = cur;
		//cur与原parent指向父节点链接
		if (parent == _root)
		{
			//parent为根节点的情况
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			//parent为局部子树根节点
			//判断parent原先为局部左子树还是局部右子树
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		//更新parent\cur的平衡因子为0
		parent->_bf = cur->_bf = 0;
	}

2.3.3右左双旋

2.3.3.1旋转情况分析

当插入节点更新平衡因子后,parent(30)节点的平衡因子为2cur(60)节点的平衡因子为-1,造成parent(30)所在子树右边高,但右子树cur节点所在子树却左边高,这时候已经parent所在的子树并不是纯粹的右边高了,所以只进行左单旋不能解决问题,需要进行右左双旋。
在这里插入图片描述
以下是h等于0和h等于1的具象图:
仅仅进行左单旋操作:
在这里插入图片描述

从上面的图可以看出,若parent所在子树不是完全右边高的情况,对其进行左单旋操作不能使AVL树达到平衡

进行右左双旋操作:
在这里插入图片描述

parent(30)所在子树右边高,但右子树cur节点所在子树却左边高,像一个折线;需要先对以cur节点为根节点的子树进行右旋操作,然后对parent节点为根节点的子树进行左旋操作,经过双旋操作后,curleft代替parent节点成为新的(局部)根节点,解决了不平衡问题,使左右子树高度一致,达到平衡状态。

2.3.3.2平衡因子更新分析

在这里插入图片描述
问题: 双旋调整后,parent\cur\curleft三个节点的位置发生了变化,这三个节点的平衡因子是否都无脑改为0呢?答案:不是的!那么该这三个节点的平衡因子应该怎么修改呢?友友们不要着急,且和我一起慢慢分析吧!
h==0(curleft->_bf ==0)的情况:
在这里插入图片描述

分析: ①当curleft的平衡因子为0时,curleft没有节点给cur\parent节点,且cur\parent节点原本也无左右子树,所以cur\parent节点的平衡因子都更新为0

h==1(curleft->_bf ==-1)的情况:
在这里插入图片描述

分析: ②当curleft的平衡因子为-1curleft没有右节点可以链接成为cur节点的左子树,以cur节点为根节点右旋之后,造成cur节点所在子树右边偏高,所以cur节点的平衡因子更新为1curleft有左节点可以链接成为parent的右子树,以parent节点为根节点左旋之后,使parent节点左右子树高度一致达到平衡状态,所以parent的平衡因子也更新为0

h==1(curleft->_bf ==-1)的情况:
在这里插入图片描述

分析: ③当curleft的平衡因子为1curleft有右节点可以链接成为cur节点的左子树,以cur节点为根节点右旋之后,使cur节点左右子树高度一致达到平衡状态,所以cur节点的平衡因子更新为0curleft有左节点链接成为parent的右子树,以parent节点为根节点左旋之后,造成parent节点所在子树左边偏高,所以parent节点的平衡因子更新为-1

代码如下:

void RotateRL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		int bf = curleft->_bf;//旋转过程中,curleft的bf发生变化,需要提前记录

		//先以cur(parent->_right)为根节点右旋
		//再以parent为根节点左旋
		RotateR(cur);
		RotateL(parent);

		//针对不同curleft->_bf的情况进行处理
		if (bf == 0)
		{
			curleft->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			curleft->_bf = 0;
			cur->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
			curleft->_bf = 0;
			cur->_bf = 1;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

右左双旋平衡因子更新总结分析:
分析上面三种平衡因子更新的情况,可以看出右左双旋(先右旋,再左旋)结果的本质是:curleft的右边给了cur的左边,curleft的左边给了parent的左边,cur节点和parent分别成为curleft的右左子树。

2.3.4右左双旋

2.3.4.1旋转情况分析

当插入节点更新平衡因子后,parent(90)节点的平衡因子为-2cur(30)节点的平衡因子为1,造成parent(90)所在子树左边高,但其左子树cur节点所在子树却左边高,这时候已经parent所在的子树并不是纯粹的左边高了,所以只进行右单旋不能解决问题,需要进行右左双旋。
在这里插入图片描述
以下是h等于0和h等于1的具象图:
仅仅进行右单旋操作:
在这里插入图片描述

从上面的图可以看出,若parent所在子树不是完全左边高的情况,对其进行右单旋操作不能使AVL树达到平衡

进行左右双旋操作:
在这里插入图片描述

parent(90)所在子树左边高,但右子树cur节点所在子树却右边高,像一个折线;需要先对以cur节点为根节点的子树进行左旋操作,然后对parent节点为根节点的子树进行右旋操作,经过双旋操作后,curleft代替parent节点成为新的(局部)根节点,解决了不平衡问题,使左右子树高度一致,达到平衡状态。

2.3.4.2平衡因子更新分析

h==0(curright->_bf ==0)的情况:
在这里插入图片描述

分析: ①当curright的平衡因子为0时,curright没有节点给cur\parent节点,且cur\parent节点原本也无左右子树,所以cur\parent节点的平衡因子都更新为0

h==-1(curright->_bf ==-1)的情况:
在这里插入图片描述

分析: ②当curright的平衡因子为-1curright有左节点链接cur节点的右子树,以cur节点为根节点左旋之后,使cur节点左右子树高度一致达到平衡状态,所以cur的平衡因子也更新为0curright没有右节点链接成为parent的左子树,以parent节点为根节点右旋之后,造成parent节点所在子树右边偏高,所以parent节点的平衡因子更新为1

h==1(curright->_bf ==1)的情况:
在这里插入图片描述

分析: ③当curright的平衡因子为1curright没有左节点可以链接cur节点的右子树,以cur节点为根节点左旋之后,造成parent节点所在子树左边偏高,所以cur的平衡因子也更新为-1curright有右节点可以链接成为parent的左子树,以parent节点为根节点右旋之后,使parent节点左右子树高度一致达到平衡状态,所以parent节点的平衡因子更新为0

代码如下:

void RotateLR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		int bf = curright->_bf;

		//先以cur(parent->_left)为根节点左旋
		//再以parent为根节点右旋
		RotateL(parent->_left);
		RotateR(parent);
		
		//针对不同curright->_bf的情况进行处理
		if (bf == 0)
		{
			curright->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			curright->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			curright->_bf = 0;
			cur->_bf = -1;
			parent->_bf = 0;
		}
	}

左右双旋平衡因子更新总结分析:
分析上面三种平衡因子更新的情况,可以看出左右双旋(先左旋,再右旋)结果的本质是:curright的左边给了cur的右边,curleft的右边给了parent的左边,cur节点和parent分别成为curleft的左右子树。

注意: ① 双旋是先左(右)旋,然后再右(左)旋,单旋转的原理是一样的,所以可以直接对前面左(右)旋函数调用,然后对右(左)旋函数调用,可增加代码复用性;②双旋平衡因子单独分析进行更新,与单旋转函数可以起到解耦合的作用。

2.3.5AVL树的验证

我们已经实现了AVL树插入操作的功能,为了保证所写的代码正确,我们可以插入数据进行检验。

使用测试AVL树平衡的函数+进行数据的插入检验

	int Height()
	{
		return Height(_root);
	}
	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 << "Exception:" << root->_kv.first << "->" << root->_bf  << endl;
			assert(false);
			return false;
		}

		return abs(leftHeight - rightHeight) < 2 
			&& IsBalance(root->_left) 
			&& IsBalance(root->_right);
	}

测试1:

void test()
{
	//普通场景
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.insert(make_pair(e, e));
		cout << "Insert:" << e << "->" << t.IsBalance() << endl;
	}
	cout << "所有数据插入成功" << endl;
}

代码运行结果为:
在这里插入图片描述
测试2:

void test2()
{
	//插入随机数
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand()+i);
	}
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.insert(make_pair(e, e));
	}
	cout << "所有数据插入成功" << endl;
}

代码运行结果如下:
在这里插入图片描述
测试3:

void test2()
{
	//插入随机数
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand()+i);
	}
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.insert(make_pair(e, e));
	}
	cout << "Insert:" << t.IsBalance() << endl;
	cout << "所有数据插入成功" << endl;
}

代码运行结果如下:
在这里插入图片描述

3.AVL模拟实现源码

#include<iostream>
#include<assert.h>
using namespace std;
template<class K,class V>
struct AVLTreeNode
{
	//建议声明顺序与初始化顺序一样
	AVLTreeNode<K, V>* _left;	//该节点的左孩子
	AVLTreeNode<K, V>* _right;	//该节点的右孩子
	AVLTreeNode<K, V>* _parent;	//该节点的双亲
	pair<K, V> _kv;
	int _bf;//该节点的平衡因子

	AVLTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_bf(0)
	{}
};
template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool insert(const pair<K, V>& kv)
	{
		//插入第一个节点
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		//找到合适的位置插入连接
		Node* parent = nullptr;
		Node* cur = _root;
		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
		{
			parent->_right = cur;

		}
		cur->_parent = parent;
		//更新平衡因子
		while (parent)
		{
			if (parent->_left == cur)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}
			//parent所在子树高度不变,会影响祖先,不需要更新平衡因子,插入结束
			if (parent->_bf == 0)
			{
				break;
			}//parent所在子树高度变化,会影响祖先,继续沿着到root的路径更新平衡因子
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}//parent所在子树高度变化且不平衡,需要旋转parent所在子树
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//插入在parent右子树的右侧,造成parent右边高
				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);
			}
		}
	}
	void RotateL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		//curleft成为parent的右边
		parent->_right = curleft;
		//curleft可能为空
		if (curleft)
		{
			//改变curleft父指针的指向
			curleft->_parent = parent;
		}

		//parent成为cur的左边
		cur->_left = parent;
		//记录parent的父节点指针
		Node* ppnode = parent->_parent;
		//改变parent父指针的指向
		parent->_parent = cur;
		//判断parent节点是否为根节点
		if (parent == _root)
		{
			//cur成为根节点
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			//cur链接parent的父节点
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else if (ppnode->_right == parent)
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		parent->_bf = cur->_bf = 0;
	}
	void RotateR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		//curright链接成为parent的左子树
		parent->_left = curright;
		if (curright)
		{
			//curright不为空情况进行,改变其父指针指向
			curright->_parent = parent;
		}
		//parent链接成为cur的右子树
		cur->_right = parent;
		//记录parent父指针
		Node* ppnode = parent->_parent;
		//让parent父指针指向cur
		parent->_parent = cur;
		//cur与原parent指向父节点链接
		if (parent == _root)
		{
			//parent为根节点的情况
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			//parent为局部子树根节点
			//判断parent原先为局部左子树还是局部右子树
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		//更新parent\cur的平衡因子为0
		parent->_bf = cur->_bf = 0;
	}
	void RotateRL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		int bf = curleft->_bf;//旋转过程中,curleft的bf发生变化,需要提前记录

		//先以cur(parent->_right)为根节点右旋
		//再以parent为根节点左旋
		RotateR(cur);
		RotateL(parent);

		if (bf == 0)
		{
			curleft->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			curleft->_bf = 0;
			cur->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
			curleft->_bf = 0;
			cur->_bf = 1;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	void RotateLR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		int bf = curright->_bf;

		//先以cur(parent->_left)为根节点左旋
		//再以parent为根节点右旋
		RotateL(parent->_left);
		RotateR(parent);

		if (bf == 0)
		{
			curright->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			curright->_bf = 0;
			cur->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			curright->_bf = 0;
			cur->_bf = -1;
			parent->_bf = 0;
		}
	}
	int Height()
	{
		return Height(_root);
	}
	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 << "Exception:" << root->_kv.first << "->" << root->_bf  << endl;
			assert(false);
			return false;
		}

		return abs(leftHeight - rightHeight) < 2 
			&& IsBalance(root->_left) 
			&& IsBalance(root->_right);
	}
private:
	Node* _root = nullptr;
};

4.总结

①AVL树的删除(了解)

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

② AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值