红黑树,位图的理解以及实现

深入理解数据结构:红黑树与图的探索 本章深入探讨了高级数据结构中的红黑树及其特性、操作方法,同时对图的定义、分类和存储结构进行了详细解析。涵盖了二叉排序树的搜索方法、红黑树的平衡操作、图的类型与表示,以及位图在大数据处理中的应用。 阅读详情

一:红黑树

为什么要引入红黑树:

因为我们前面讲到的AVL树对于平衡有着绝对的追求,它要求树高度差的绝对值要小于等于一,所以需要经常进行旋转,但是旋转会很消耗时间,所以即使AVL树的查找时间更快,我们依然引入了更加均衡的红黑树

红黑树的性质:

1,每个节点只能是红色或者黑色

2,根节点一定是黑色的

3,所有叶子节点(NIL节点)都是黑色的

4,红色节点不能连续出现

5,根节点到叶子节点的路径上黑色节点的个数一样

根据这五条性质,可以总结出来:最长路径是最短路径的两倍(最短就是全部是黑色节点,最长路径就是红黑交替)

思考:如果一棵红黑树中黑色节点的个数是X,并且总共有N个节点,那么N的范围是什么

答:N的范围是【X,2X】,那么根据这个我们也可以推断出来:最短路径的时间复杂度是logX,最长路径的时间复杂度是log2X,log2X约等于logX+1,根据时间复杂度的性质,我们也可以认定它的时间复杂度也是logX,所以不管N是X还是2X,我们的时间复杂度都是logN

红黑树的插入与检验:

前提:我们认定新插入的节点是红的,因为根据性质5,我们需要黑色个数一样,所以可能会插入一些我们不需要的黑色节点来满足性质5,所以我们默认新插入的是红的,然后进行颜色调整

//红黑树
public class rbtree {
    static class TreeNode{
        public TreeNode left;
        public TreeNode right;
        public TreeNode parent;
        public color color;
        public int val;
        public TreeNode(int val){
            this.val=val;
            //默认是红色
            this.color=color.RED;
        }
    }
    public TreeNode root;
    public boolean insert(int val){
        TreeNode treeNode=new TreeNode(val);
        //先正常插入
        if(root==null){
            root=treeNode;
            root.color=color.BLACK;
            return true;
        }
        //正常插入的时候定义了两个新变量,一个是p(parent),一个是cur
        TreeNode p=null;
        TreeNode cur=root;
        while(cur!=null){
            if (val>cur.val){
                p=cur;
                cur=cur.right;
            } else if (val==cur.val) {
                return false;
            } else {
                p=cur;
                cur=cur.left;
            }
        }
        //走完这里之后p走到了我们要放的地方的父亲节点,cur走到了空
        if (val>p.val){
            p.right=treeNode;
        }else {
            p.left=treeNode;
        }
        treeNode.parent=p;
        cur=treeNode;
        //插入完之后,要判断是否满足红黑树的性质,不满足的话进行旋转和变色
        while(p!=null && p.color==color.RED){
            //先把grandpa节点定义出来
            TreeNode grandpa=p.parent;
            if (p==grandpa.left){
                //再定义一个uncle
                TreeNode uncle=grandpa.right;
                //uncle存在并且是红色
                if(uncle!=null && uncle.color==color.RED){
                    p.color=color.BLACK;
                    uncle.color=color.BLACK;
                    grandpa.color=color.RED;
                    //并且继续向上调整
                    cur=grandpa;
                    p=cur.parent;
                }else {
                    //uncle不存在或者是黑色
                    //情况三先写
                    if(cur==p.right){
                        //先左旋p
                        RotateL(p);
                        //再改变指向
                        TreeNode tmp=p;
                        p=cur;
                        cur=tmp;
                    }
                    //情况二,因为没有else,所以怎么都会走到这里
                    RotateR(grandpa);
                    //变色
                    p.color=color.BLACK;
                    grandpa.color=color.RED;
                }
            }else {
                //cur.parent==grandpa.right)
                //再定义一个uncle
                TreeNode uncle=grandpa.left;
                //uncle存在并且是红色
                if(uncle!=null && uncle.color==color.RED){
                    p.color=color.BLACK;
                    uncle.color=color.BLACK;
                    grandpa.color=color.RED;
                    //并且继续向上调整
                    cur=grandpa;
                    p=cur.parent;
                }else {
                    //uncle不存在或者是黑色
                    //uncle不存在或者是黑色
                    //情况三先写
                    if(cur==p.left){
                        //先左旋p
                        RotateR(p);
                        //再改变指向
                        TreeNode tmp=p;
                        p=cur;
                        cur=tmp;
                    }
                    //情况二,因为没有else,所以怎么都会走到这里
                    RotateL(grandpa);
                    //变色
                    p.color=color.BLACK;
                    grandpa.color=color.RED;
                }
            }
        }
        root.color=color.BLACK;
        return true;
    }
    private void RotateL(TreeNode p) {
        TreeNode subR=p.right;
        TreeNode subRL=subR.left;
        subR.left=p;
        p.right=subRL;
        //开始指向父亲节点
        if (subRL!=null){
            subRL.parent=p;
        }
        p.parent=subR;
        TreeNode Pp=p.parent;
        if(p==root) {
            root = subR;
            subR.parent = null;
        }else {
            if(Pp.left==p){
                Pp.left=subR;
            }else {
                Pp.right=subR;
            }
            subR.parent=Pp;
        }
    }
    //右旋
    private void RotateR(TreeNode p) {
        TreeNode subL=p.left;
        TreeNode subLR=subL.right;
        //subLR可能是空的
        //开始旋转
        //先指向子结点
        subL.right=p;
        p.left=subLR;
        //再指向父结点
        if(subLR!=null){
            subLR.parent=p;
        }
        TreeNode Pp=p.parent;
        p.parent=subL;
        //可能原来的p节点并不是根结点
        if(p==root){
            root=subL;
            subL.parent=null;
        }else {
            if (Pp.left==p){
                Pp.left=subL;
            }else {
                Pp.right=subL;
            }
            subL.parent=Pp;
        }
    }
    //检查是否是红黑树
    public boolean check(TreeNode root){
        if (root==null){
            return true;
        }
        //先看根节点是不是黑色的
        if (root.color==color.RED){
            System.out.println("违反了根节点是黑色的这个性质!!");
            return false;
        }
        //再看是否有两个连续的红色节点以及黑色节点的个数
        int pathNum=0;
        TreeNode cur = root;
        while (cur!=null){
            if (cur.color==color.BLACK){
                pathNum++;
            }
            cur=cur.left;

        }

        return CheckRedColor(root) && CheckBlackNum(root,pathNum,0);
    }
    public boolean CheckRedColor(TreeNode root){
        if (root==null) {
            return true;
        }
        if(root.color == color.RED) {
            TreeNode parent = root.parent;
            if(parent.color == color.RED) {
                System.out.println("违反了性质:连续出现了两个红色的节点");
                return false;
            }
        }
        return CheckRedColor(root.left) && CheckRedColor(root.right);
    }
    //pathNum是事先记录好的黑色的个数
    public boolean CheckBlackNum(TreeNode root,int pathNum,int blackNum){
        if (root==null) {
            return true;
        }
        if (root.color==color.BLACK){
            blackNum++;
        }
        //在root达到叶子节点的时候进行了判断
        if (root.left==null && root.right==null){
            if (blackNum!=pathNum){
                System.out.println("违反了性质:黑色节点个数不一致");
                return false;
            }
        }
        return CheckBlackNum(root.left,pathNum,blackNum) &&
                CheckBlackNum(root.right,pathNum,blackNum);
    }
    //中序遍历也可以证明是否是红黑树
    public void inorder(TreeNode root) {
        if(root == null) {
            return;
        }
        inorder(root.left);
        System.out.print(root.val+" ");
        inorder(root.right);
    }
}
public enum color {
    RED,BLACK;
}
public class Test {
    public static void main(String[] args) {
        int[] array = {4, 2, 6, 1, 3, 5, 15, 7, 16,14};
        rbtree rbTree = new rbtree();
        for (int i = 0; i < array.length; i++) {
            rbTree.insert(array[i]);
        }
        System.out.println(rbTree.check(rbTree.root));
        rbTree.inorder(rbTree.root);
    }
}

二:位图

为什么要引入位图:

当数据量很大的时候,比如我们给40亿个整数,判断某一个数字是否存在,40亿个整数大概是16个G,如果我们用数组来存储遍历会空间消耗很大,甚至运行不起来,所以我们引入了位图,将一个字节分成8个位,通过数字0或者1来表示当前数字是否存在,这样我们所需要的空间大概为500左右MB,并且查询速度很快,为O(1)

位图的相关方法实现:

我们放数据的时候只需要让原本对应elem[ArrayIndex]里的八位或上一个1左移对应的index位就好了,因为按位或会有1则1,就会将原来0的位置改为1,原来1的不变

我们查找数据的时候就要按位与,最后得出来的不是0就可以说明有这个数

我们重置的时候需要1左移index然后取反,其他1的位置还是1,想改的那个位置会改为0

import java.util.Arrays;

public class MyBitset {
    public byte[] elem;
    public int size;
    public MyBitset(int size){
        this.size = size;
        elem = new byte[size/8 + 1];
    }
    //放元素
    public void set(int val){
        if (val<0){
            throw new IllegalArgumentException("val<0");

        }
        //看在哪一个字节下
        int ArrayIndex=val/8;
        if (ArrayIndex>=elem.length){
            Arrays.copyOf(elem,ArrayIndex*2);
        }
        //看在字节的哪一个位置
        int index=val%8;
        elem[ArrayIndex]|=(1<<index);
        size++;
    }
    //查元素
    public boolean get(int val){
        if (val<0){
            throw new IllegalArgumentException("val<0");

        }
        //看在哪一个字节下
        int ArrayIndex=val/8;
        //看在字节的哪一个位置
        int index=val%8;
        if ((elem[ArrayIndex]&(1<<index))!=0){
            return true;
        }
        return false;
    }
    //重置某位
    public void reset(int val){
        if (val<0){
            throw new IllegalArgumentException("val<0");

        }
        //看在哪一个字节下
        int ArrayIndex=val/8;
        //看在字节的哪一个位置
        int index=val%8;
        elem[ArrayIndex]&=~(1<<index);
        size--;
    }
}
public class Test {
    public static void main(String[] args) {
        int[] array = {1,3,2,13,10,3,14,18,3};
        MyBitset myBitSet = new MyBitset(18);
        for (int i = 0; i < array.length; i++) {
            myBitSet.set(array[i]);
        }

        for (int i = 0; i < myBitSet.elem.length; i++) {
            for (int j = 0; j < 8; j++) {
                if((myBitSet.elem[i] & (1 << j) ) != 0 ) {
                    System.out.print(i*8+j+" ");
                }
            }
        }
    }
}

Linux运维踩坑实录:手把手教你修复LVM中PV显示[unknown]的诡异问题 本文详细解析了Linux运维中LVM物理卷(PV)显示[unknown]的故障现象及修复方法。通过分析问题根源、提供标准修复流程和特殊场景处理方案,帮助运维人员快速解决VG容量异常、PV状态丢失等问题,并分享预防措施与最佳实践,确保LVM存储系统的稳定运行。 阅读详情

相关推荐

逆向实战:手把手教你分析Google DroidGuard虚拟机的加密与反调试(附Python脚本)

本文深入解析Google DroidGuard虚拟机的加密机制与反调试策略,提供从环境配置到逆向分析的完整实战指南。通过IDA Pro、Frida等工具链,详细剖析DroidGuard的信号拦截、内存校验等保护机制,并附Python脚本实现自动化分析,助力Android安全研究人员突破GMS核心组件的安全防护。

weixin_32428571的博客 313

吐血整理 | 肝翻linux内核常用数据结构汇总

在计算机科学的璀璨星空中,数据结构是一颗耀眼的明星,它是计算机存储、组织数据的巧妙方式,如同建筑中的蓝图,规划着数据的布局与管理。简单来说,数据结构就是相互之间存在一种或多种特定关系的数据元素的集合,它涵盖了数据的逻辑结构、物理结构以及相关的操作运算。数据结构主要分为线性结构和非线性结构。线性结构中的数据元素呈现出一对一的线性关系,如同一条整齐排列的队伍,数组、链表、栈和队列等都属于这一范畴;非线性结构则更为复杂,元素之间存在一对多或多对多的关系,像是一张错综复杂的关系网,树和图就是典型的非线性结构。

深度Linux 894

linux 4.19在龙芯平台上编译出错解决方法

OS: Loongnix server 1.7 工具链为:系统自带的gcc-4.8.5 和 gcc-7.3 平台:3A3000/3A4000 源码:kernel: 4.19 编译步骤 ··· make loongson3_defconfig make -j4 ··· 出错信息: HOSTCC arch/mips/boot/compressed/calc_vmlinuz_load_addr arch/mips/boot/compressed/head.S: Assembler messages: arch/m

sense的专栏 1125

吐血整理 | 肝翻linux内核常用数据结构汇总(转)

在计算机科学的璀璨星空中,数据结构是一颗耀眼的明星,它是计算机存储、组织数据的巧妙方式,如同建筑中的蓝图,规划着数据的布局与管理。简单来说,数据结构就是相互之间存在一种或多种特定关系的数据元素的集合,它涵盖了数据的逻辑结构、物理结构以及相关的操作运算。数据结构主要分为线性结构和非线性结构。线性结构中的数据元素呈现出一对一的线性关系,如同一条整齐排列的队伍,数组、链表、栈和队列等都属于这一范畴;非线性结构则更为复杂,元素之间存在一对多或多对多的关系,像是一张错综复杂的关系网,树和图就是典型的非线性结构。

oushaojun2的专栏 1131

保姆级教程:手把手教你为STM32F407和W25Q128制作Keil MDK的SPI Flash下载算法(FLM文件)

本文提供了一份详细的教程,指导开发者如何为STM32F407和W25Q128制作Keil MDK的SPI Flash下载算法(FLM文件)。从理解下载算法的核心机制到实现FlashPrg.c的核心操作,再到链接脚本与地址映射实战,手把手教你完成定制化SPI Flash下载算法的开发与优化。

weixin_28716769的博客 408

国产麒麟系统下用Rider开发C#桌面应用:从环境配置到Avalonia UI实战

本文详细介绍了在国产麒麟系统上使用JetBrains Rider开发C#桌面应用的完整流程,重点讲解了Avalonia UI框架的实战应用。从环境配置、Rider安装到Avalonia项目创建和UI开发技巧,全面覆盖了在非Windows环境下进行C#开发的关键步骤和优化建议,助力开发者高效构建跨平台桌面应用。

weixin_27791839的博客 387

Java面试核心考点解析:从排序算法到系统设计

排序算法与数据结构是计算机科学的基础核心,通过时间复杂度与空间复杂度的权衡实现性能优化。Java作为主流编程语言,其面向对象特性如多态通过虚方法表实现运行时绑定,设计模式则提供了工程实践的最佳范例。在数据库领域,B+树索引因其磁盘友好的分层结构成为标准选择,而位图(Bitmap)凭借极低的内存消耗在海量数据处理中表现优异。异步编程通过线程池与消息队列实现系统解耦,安全沙箱则保障了代码执行的可控性。这些技术广泛应用于高并发系统设计、分布式架构等领域,也是技术面试的重点考察方向。

weixin_29522547的博客 215

英飞凌TC3xx之一起认识GTM系列(六)如何实现GTM与VADC关联的配置

本文给出了GTM与EVADC之间关联的细节,并通过两个实例分别说明了如何对寄存器进行配置。

无所适从的我的博客 2933

Java面试核心:排序算法与OOP设计深度解析

排序算法是计算机科学基础,通过比较不同算法的时间复杂度与空间复杂度,可以理解快速排序为何成为工业级首选。面向对象编程(OOP)的封装、继承、多态特性构建了软件设计的基石,结合设计模式能实现高扩展性系统架构。在Java技术面试中,算法实现与OOP设计能力是核心考察点,快速排序的优化实现和抽象工厂模式的应用尤其值得关注。本文通过算法对比和代码实例,展示如何在实际工程中平衡性能与设计优雅性。

weixin_31236101的博客 342

【路径规划】基于matlab蚁群算法机器人栅格地图最短路径规划【含Matlab源码 4590期】

基于matlab蚁群算法机器人栅格地图最短路径规划 完整代码,包运行;可提供运行操作视频!适合小白!

订阅付费专栏Matlab(奶茶价版),可赠送奶茶价版付费专栏指定代码1份; 1278

手把手教你从入门到精通C# Modbus通信

本文是《Modbus通信从入门到精通》的导读,本专栏将会详细介绍ModusRTU 、ModbusASCII、ModbusTCP这三种协议的C#实现,由一个高级工程师多年实战经验总结而成,绝对干货!

qq_34059233的博客 1万+

Learning C++ No.26 【深入学习位图】

使用位图结构进行海量数据处理,深入理解什么是位图

狂小伍的博客 1763

使用Gitlab 搭建私有镜像仓库(外置Nginx)

为了让团队尽快使用Gitlab的CI进行工作,必须做好Gitlab的初始化工作,大家都知道拉取镜像由于某些原因,会比较慢,因此构建自己的私有镜像仓库就成为了关键的一步。当然私有镜像仓库的搭建有很多种方式,这里直接使用Gitlab提供的镜像仓库功能。一揽子解决方案比较香。gitlab真的越来越好用了,有了这个镜像仓,也没必要再使用三方的镜像仓库了。Devops 真好用!

网尘的技术专栏 1829

一篇文章理解map、set、哈希、位图、布隆过滤器

map、set、哈希、位图、布隆过滤器底层原理的代码实现

qq_42392701的博客 1088

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Java 数据结构详解:从栈、队列、链表到红黑树、B-Tree 与位图

本文摘要:数据结构解决的是如何高效组织数据以优化常见操作的问题。文章以Java为例,讲解了8种核心数据结构(栈、队列、链表、散列表、二叉搜索树、红黑树、B树、位图)的实现与应用。重点分析了复杂度概念(O(1)到O(n²))和数据结构选择标准(查询/插入效率、有序性等)。通过括号匹配和BFS等典型示例,展示了栈和队列的实际应用场景。文中推荐优先使用ArrayDeque实现栈/队列,并对比了不同数据结构的适用场景和Java标准库实现(如HashMap、TreeMap等)。全文基于Java 8+,强调理解数据结构

qq_29029209的博客 962

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

初步学习位图的小总结

位图的学习与总结~~~

xxwl585的博客 593

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

C++——位图与布隆过滤器

先看一个面试题:给40亿个不重复的未排序的无符号整数,然后如何快速判断一个数是否在这40亿个数中以我们前面的知识,最先想到的有两种方法:1,排序+二分查找 2,存到哈希表和红黑树中再查但是题目中给的是40亿个整数,我们先算下需要的内存:1G就是1024MB,是1024*1024KB,是1024*1024*1024Byte,相当于10亿Byte,所以,40亿个数需要4*40Byte大概就是16G的空间,所以由于空间的限制,上面两种方法就都可以去掉了。

aaqq800520的博客 1221

红黑树与AVL树

一、在理解红黑树之前,我们先看一下二叉查找树 由于红黑树本质上就是一棵二叉查找树,所以在了解红黑树之前,咱们先来看下二叉查找树。 二叉查找树特性: 1、左子树上所有的节点的值都小于或等于他的根节点上的值 2、右子树上所有节点的值均大于或等于他的根节点的值 3、左、右子树也分别为二叉查找树 如图: 可以看到如果要查询10的话,10>9 因此到他的右子树,右子树根节点为13,10<1...

qq_38769551的博客 739

Java数据结构-红黑树

红黑树 R-B Tree,全称是 Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每 个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。 红黑树的特性 (1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。 左旋 对 x

Fanx繁星 283

位图bitset ,布隆过滤器详解及模拟实现[C++]

可以实现一个打印函数,便于检查我们上述代码的正确性,打印位图时遍历位图所包含的比特位进行打印即可,在打印位图的过程中可以顺便统计位图中位的个数count,将count与我们传入的非类型模板参数N进行比较,可以判断位图大小是否是符合我们的预期。我们这里可以大概估算一下,如果使用3个哈希函数,即k的值为3,l n 2 ln2ln2的值我们取0.7,那么 m mm 和 n nn 的关系大概是m = 4.3× n ,也就是布隆过滤器的长度应该是插入元素个数的5倍。类型开辟数组, 没有听说过开多少bit位的数组。

EQUINOX1的博客 223

【C++】位图和布隆过滤器

本篇文章围绕位图和布隆过滤器展开讲解,包含二者的概念、思想、实现和应用场景等内容

Eristic0618的博客 1953

哈希思想的应用 - 位图,布隆过滤器

哈希思想的应用:处理海量数据,位图,布隆过滤器,哈希切割

TPON的博客 649

pick_next_task与红黑树浅析

一、schedule()中关键步骤pick_next_task的分析在整个函数的流程中,pick_next_task是一个十分重要的步骤,它负责找到当前cpu的运行队列中最应该运行的那个进程,那么它是如何实现的呢? 首先它的实现是依靠钩子函数,每个调度类的具体实现都是靠钩子函数来完成的,这里说下对钩子函数的理解: 钩子函数就是提前声明一个指向函数的指针,其中也声明好了关于此函数的参数以及其的返

taocr的博客 1218

高阶数据结构(AVL树、红黑树、位图、布隆过滤器、并查集、LRUCache、B-树、图等)

红黑树的结点的定义相较于AVL树来说,只是多了关于结点颜色的定义,而一个结点的初始颜色默认为红色(由上述的性质5可知,对于每个节点到其后代的简单路径上的黑色节点的数目是相同的,如果在红黑树结点的定义中默认结点的颜色为黑色的话,当一个新的结点插入时必然会破环性质5从而导致要对该红黑树进行调整,效率极低)具体的定义如下代码所示//创建的新节点默认为红色@Override//先将头节点和尾巴节点连接起来。

S122321的博客 396

c++ unordered_set/unordered_map的底层理解及其实现,位图实现及其应用,布隆过滤器的实现及其应用,哈希切分的应用

unordered_map在线文档说明unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。

qq_54626677的博客 1336

带你从了解到掌握位图

概念:位图是一种利用二进制位来表示特定元素状态的数据结构,每一位对应一个元素,可以表示两种状态。优点:从概念我们可以得知,这个数据结构是操作二进制位来设置状态的,所以和它相关的操作都是位运算。计算机底层就是使用二进制进行运算的,这也是他效率会比较高的原因之一,但是它最大的优势在于节省空间,int所占的字节数在我们常用的开发环境中一般是4个字节,也就是32个比特位,使用位图的话,那么一个int就可以表示32个数据的状态。这对比传统的存储方式是不是相当高效和节省空间了?

qwer456tyu123的博客 86
上一篇: AVL树的实现(Java版)
下一篇: 文件操作和IO
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值