刷题汇总(六)leetcode 多线程 / Shell

题目来源

leetcode 多线程 / Shell

相关:
刷题汇总(一)leetcode 精选50题 JavaScript答案总结
刷题汇总(二)剑指Offer 66题 C++答案总结
刷题汇总(三)leetcode 精选50题 C++答案总结
刷题汇总(四)技术类编程题汇总 C++
刷题汇总(五)leetcode 热题 HOT 100 C++ 答案总结

这篇文章使用 POSIX 编写多线程 C++ 程序。POSIX Threads 或 Pthreads 提供的 API 可在多种类 Unix POSIX 系统上可用,比如 FreeBSD、NetBSD、GNU/Linux、Mac OS X 和 Solaris。

知识点1:创建线程/终止线程
#include <pthread.h>
pthread_create (thread, attr, start_routine, arg)
pthread_exit (status)

使用 -lpthread 库编译:
$ g++ test.cpp -lpthread -o test.o

1、按序打印

我们提供了一个类:

public class Foo {
  public void one() { print("one"); }
  public void two() { print("two"); }
  public void three() { print("three"); }
}

三个不同的线程将会共用一个 Foo 实例。

线程 A 将会调用 one() 方法
线程 B 将会调用 two() 方法
线程 C 将会调用 three() 方法
请设计修改程序,以确保 two() 方法在 one() 方法之后被执行,three() 方法在 two() 方法之后被执行。

示例 1:

输入: [1,2,3]
输出: “onetwothree”
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。
正确的输出是 “onetwothree”。
示例 2:

输入: [1,3,2]
输出: “onetwothree”
解释:
输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。
正确的输出是 “onetwothree”。

注意:

尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
你看到的输入格式主要是为了确保测试的全面性。

#include <semaphore.h>
class Foo {
public:
    sem_t t1, t2, t3;

    Foo() {
        sem_init(&t1,0,1);
        sem_init(&t2,0,0);
        sem_init(&t3,0,0);
    }

    void first(function<void()> printFirst) {
        sem_wait(&t1);
        // printFirst() outputs "first". Do not change or remove this line.
        printFirst();
        sem_post(&t2);
    }

    void second(function<void()> printSecond) {
        sem_wait(&t2);
        // printSecond() outputs "second". Do not change or remove this line.
        printSecond();
        sem_post(&t3);
    }

    void third(function<void()> printThird) {
        sem_wait(&t3);
        // printThird() outputs "third". Do not change or remove this line.
        printThird();
    }
};

知识点2:信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int val); // 初始化信号量
int sem_wait(sem_t *sem); // 信号量减1
int sem_post(sem_t *sem); // 信号量加1
int sem_destory(sem_t *sem); // 销毁信号量

2、交替打印FooBar

我们提供一个类:

class FooBar {
  public void foo() {
    for (int i = 0; i < n; i++) {
      print("foo");
    }
  }

  public void bar() {
    for (int i = 0; i < n; i++) {
      print("bar");
    }
  }
}

两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。

请设计修改程序,以确保 “foobar” 被输出 n 次。

示例 1:

输入: n = 1
输出: “foobar”
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,“foobar” 将被输出一次。
示例 2:

输入: n = 2
输出: “foobarfoobar”

# include<semaphore.h>
class FooBar {
private:
    int n;
    sem_t t1, t2;
public:
    FooBar(int n) {
        this->n = n;
        sem_init(&t1,0,1);
        sem_init(&t2,0,0);
    }

    void foo(function<void()> printFoo) {
        for (int i = 0; i < n; i++) {
            sem_wait(&t1);
            // printFoo() outputs "foo". Do not change or remove this line.
        	printFoo();
            sem_post(&t2);
        }
    }

    void bar(function<void()> printBar) {
        for (int i = 0; i < n; i++) {
            sem_wait(&t2);
            // printBar() outputs "bar". Do not change or remove this line.
        	printBar();
            sem_post(&t1);
        }
    }
};
class FooBar {
private:
    int n;
    pthread_mutex_t t1, t2;
public:
    FooBar(int n) {
        this->n = n;
        pthread_mutex_init(&t1,NULL);
        pthread_mutex_init(&t2,NULL);
        pthread_mutex_lock(&t2);
    }

    void foo(function<void()> printFoo) {
        for (int i = 0; i < n; i++) {
            pthread_mutex_lock(&t1);
            // printFoo() outputs "foo". Do not change or remove this line.
        	printFoo();
            pthread_mutex_unlock(&t2);
        }
    }

    void bar(function<void()> printBar) {
        for (int i = 0; i < n; i++) {
            pthread_mutex_lock(&t2);
            // printBar() outputs "bar". Do not change or remove this line.
        	printBar();
            pthread_mutex_unlock(&t1);
        }
    }
};

知识点3:互斥量
pthread_mutex_t sum_mutex; //互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER //静态初始化
pthread_mutex_init( &sum_mutex, NULL ); //动态初始化 初始化成功返回0
pthread_mutex_lock( &sum_mutex ); //加锁
pthread_mutex_unlock( &sum_mutex ); //释放锁,供其他线程使用
pthread_mutex_destroy( &sum_mutex ); //注销锁

3、打印零与奇偶数

假设有这么一个类:

class ZeroEvenOdd {
  public ZeroEvenOdd(int n) { ... }      // 构造函数
  public void zero(printNumber) { ... }  // 仅打印出 0
  public void even(printNumber) { ... }  // 仅打印出 偶数
  public void odd(printNumber) { ... }   // 仅打印出 奇数
}

相同的一个 ZeroEvenOdd 类实例将会传递给三个不同的线程:

线程 A 将调用 zero(),它只输出 0 。
线程 B 将调用 even(),它只输出偶数。
线程 C 将调用 odd(),它只输出奇数。
每个线程都有一个 printNumber 方法来输出一个整数。请修改给出的代码以输出整数序列 010203040506… ,其中序列的长度必须为 2n。

示例 1:

输入:n = 2
输出:“0102”
说明:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 “0102”。
示例 2:

输入:n = 5
输出:“0102030405”

# include<semaphore.h>
class ZeroEvenOdd {
private:
    int n;
    sem_t t0, t1, t2;
public:
    ZeroEvenOdd(int n) {
        this->n = n;
        sem_init(&t0,0,1);
        sem_init(&t1,0,0);
        sem_init(&t2,0,0);
    }

    // printNumber(x) outputs "x", where x is an integer.
    void zero(function<void(int)> printNumber) {
        for(int i=1;i<=n;i++){
            sem_wait(&t0);
            printNumber(0);
            if(i%2==1) sem_post(&t1);
            else sem_post(&t2);
        }
    }

    void even(function<void(int)> printNumber) {
        for(int i=2;i<=n;i+=2){
            sem_wait(&t2);
            printNumber(i);
            sem_post(&t0);
        }
    }

    void odd(function<void(int)> printNumber) {
        for(int i=1;i<=n;i+=2){
            sem_wait(&t1);
            printNumber(i);
            sem_post(&t0);
        }
    }
};

4、H2O 生成

现在有两种线程,氢 oxygen 和氧 hydrogen,你的目标是组织这两种线程来产生水分子。

存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。

氢和氧线程会被分别给予 releaseHydrogen 和 releaseOxygen 方法来允许它们突破屏障。

这些线程应该三三成组突破屏障并能立即组合产生一个水分子。

你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。

换句话说:

如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。
书写满足这些限制条件的氢、氧线程同步代码。

示例 1:

输入: “HOH”
输出: “HHO”
解释: “HOH” 和 “OHH” 依然都是有效解。
示例 2:

输入: “OOHHHH”
输出: “HHOHHO”
解释: “HOHHHO”, “OHHHHO”, “HHOHOH”, “HOHHOH”, “OHHHOH”, “HHOOHH”, “HOHOHH” 和 “OHHOHH” 依然都是有效解。

限制条件:

输入字符串的总长将会是 3n, 1 ≤ n ≤ 50;
输入字符串中的 “H” 总数将会是 2n;
输入字符串中的 “O” 总数将会是 n。

# include<semaphore.h>
class H2O {
public:
    sem_t h_limit, o_limit, h, o;

    H2O() {
        sem_init(&h_limit,0,2);
        sem_init(&o_limit,0,1);
        sem_init(&h,0,0);
        sem_init(&o,0,0);
    }

    void hydrogen(function<void()> releaseHydrogen) {
        sem_wait(&h_limit);
        sem_post(&h);
        sem_wait(&o);
        // releaseHydrogen() outputs "H". Do not change or remove this line.
        releaseHydrogen();
        sem_post(&h_limit);
    }

    void oxygen(function<void()> releaseOxygen) {
        sem_wait(&o_limit);
        sem_post(&o);
        sem_post(&o);
        sem_wait(&h);
        sem_wait(&h);
        // releaseOxygen() outputs "O". Do not change or remove this line.
        releaseOxygen();
        sem_post(&o_limit);
    }
};

5、交替打印字符串

编写一个可以从 1 到 n 输出代表这个数字的字符串的程序,但是:

如果这个数字可以被 3 整除,输出 “fizz”。
如果这个数字可以被 5 整除,输出 “buzz”。
如果这个数字可以同时被 3 和 5 整除,输出 “fizzbuzz”。
例如,当 n = 15,输出: 1, 2, fizz, 4, buzz, fizz, 7, 8, fizz, buzz, 11, fizz, 13, 14, fizzbuzz。

假设有这么一个类:

class FizzBuzz {
  public FizzBuzz(int n) { ... }               // constructor
  public void fizz(printFizz) { ... }          // only output "fizz"
  public void buzz(printBuzz) { ... }          // only output "buzz"
  public void fizzbuzz(printFizzBuzz) { ... }  // only output "fizzbuzz"
  public void number(printNumber) { ... }      // only output the numbers
}

请你实现一个有四个线程的多线程版 FizzBuzz, 同一个 FizzBuzz 实例会被如下四个线程使用:

线程A将调用 fizz() 来判断是否能被 3 整除,如果可以,则输出 fizz。
线程B将调用 buzz() 来判断是否能被 5 整除,如果可以,则输出 buzz。
线程C将调用 fizzbuzz() 来判断是否同时能被 3 和 5 整除,如果可以,则输出 fizzbuzz。
线程D将调用 number() 来实现输出既不能被 3 整除也不能被 5 整除的数字。

# include<semaphore.h>
class FizzBuzz {
private:
    int n;
    sem_t t0, t3, t5, t35;

public:
    FizzBuzz(int n) {
        this->n = n;
        sem_init(&t0,0,1);
        sem_init(&t3,0,0);
        sem_init(&t5,0,0);
        sem_init(&t35,0,0);
    }

    // printFizz() outputs "fizz".
    void fizz(function<void()> printFizz) {
        for(int i=1;i<=n;i++){
            if(i%3==0 && i%5!=0){
                sem_wait(&t3);
                printFizz();
                sem_post(&t0);
            }
        }
    }

    // printBuzz() outputs "buzz".
    void buzz(function<void()> printBuzz) {
        for(int i=1;i<=n;i++){
            if(i%3!=0 && i%5==0){
                sem_wait(&t5);
                printBuzz();
                sem_post(&t0);
            }
        }
    }

    // printFizzBuzz() outputs "fizzbuzz".
	void fizzbuzz(function<void()> printFizzBuzz) {
        for(int i=1;i<=n;i++){
            if(i%3==0 && i%5==0){
                sem_wait(&t35);
                printFizzBuzz();
                sem_post(&t0);
            }
        }
    }

    // printNumber(x) outputs "x", where x is an integer.
    void number(function<void(int)> printNumber) {
        for(int i=1;i<=n;i++){
            sem_wait(&t0);
            if(i%3!=0 && i%5!=0){
                printNumber(i);
                sem_post(&t0);
            }
            else if(i%3==0 && i%5!=0){
                sem_post(&t3);
            }
            else if(i%3!=0 && i%5==0){
                sem_post(&t5);
            }
            else{
                sem_post(&t35);
            }
        }
    }
};

6、哲学家进餐

5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。
在这里插入图片描述

问题描述和图片来自维基百科 wikipedia.org

哲学家从 0 到 4 按 顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):

philosopher 哲学家的编号。
pickLeftFork 和 pickRightFork 表示拿起左边或右边的叉子。
eat 表示吃面。
putLeftFork 和 pickRightFork 表示放下左边或右边的叉子。
由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。
给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。

示例:

输入:n = 1
输出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
解释:
n 表示每个哲学家需要进餐的次数。
输出数组描述了叉子的控制和进餐的调用,它的格式如下:
output[i] = [a, b, c] (3个整数)

  • a 哲学家编号。
  • b 指定叉子:{1 : 左边, 2 : 右边}.
  • c 指定行为:{1 : 拿起, 2 : 放下, 3 : 吃面}。
    如 [4,2,1] 表示 4 号哲学家拿起了右边的叉子。

提示:
1 <= n <= 60

# include<semaphore.h>
class DiningPhilosophers {
private:
    sem_t sem;
    pthread_mutex_t mutex[5];
public:
    DiningPhilosophers() {
        sem_init(&sem,0,4);
        for(int i=0;i<5;i++){
            pthread_mutex_init(&mutex[i],NULL);
        }
    }
    
    // 限定哲学家就餐数量
    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {

                    int left = philosopher;
                    int right = (philosopher+1)%5;

                    sem_wait(&sem);
                    pthread_mutex_lock(&mutex[left]);
                    pthread_mutex_lock(&mutex[right]);

                    pickLeftFork();
                    pickRightFork();

                    eat();

                    putLeftFork();
                    putRightFork();

                    pthread_mutex_unlock(&mutex[left]);
                    pthread_mutex_unlock(&mutex[right]);

                    sem_post(&sem);
    }
};
# include<semaphore.h>
class DiningPhilosophers {
private:
    pthread_mutex_t mutex;
    pthread_mutex_t forks_mutex[5];
public:
    DiningPhilosophers() {
        pthread_mutex_init(&mutex,NULL);
        for(int i=0;i<5;i++){
            pthread_mutex_init(&forks_mutex[i],NULL);
        }
    }

    // 同时拿起左右2把叉子
    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {

                    int left = philosopher;
                    int right = (philosopher+1)%5;

                    pthread_mutex_lock(&mutex);

                    pthread_mutex_lock(&forks_mutex[left]);
                    pthread_mutex_lock(&forks_mutex[right]);

                    pickLeftFork();
                    pickRightFork();

                    pthread_mutex_unlock(&mutex);

                    eat();

                    putLeftFork();
                    putRightFork();

                    pthread_mutex_unlock(&forks_mutex[left]);
                    pthread_mutex_unlock(&forks_mutex[right]);
    }
};
# include<semaphore.h>
class DiningPhilosophers {
private:
    pthread_mutex_t forks_mutex[5];
public:
    DiningPhilosophers() {
        for(int i=0;i<5;i++){
            pthread_mutex_init(&forks_mutex[i],NULL);
        }
    }

    // 一部分哲学家优先去获取其左边的叉子,再去获取其右边的叉子;
    // 再让剩余哲学家优先去获取其右边的叉子,再去获取其左边的叉子
    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {

                    int left = philosopher;
                    int right = (philosopher+1)%5;

                    if(philosopher%2 == 1){
                        pthread_mutex_lock(&forks_mutex[left]);
                        pthread_mutex_lock(&forks_mutex[right]);
                    }
                    else{
                        pthread_mutex_lock(&forks_mutex[right]);
                        pthread_mutex_lock(&forks_mutex[left]);

                    }

                    pickLeftFork();
                    pickRightFork();

                    eat();

                    putLeftFork();
                    putRightFork();

                    pthread_mutex_unlock(&forks_mutex[left]);
                    pthread_mutex_unlock(&forks_mutex[right]);
    }
};

7、统计词频

写一个 bash 脚本以统计一个文本文件 words.txt 中每个单词出现的频率。

为了简单起见,你可以假设:

words.txt只包括小写字母和 ’ ’ 。
每个单词只由小写字母组成。
单词间由一个或多个空格字符分隔。
示例:

假设 words.txt 内容如下:

the day is sunny the the
the sunny is is
你的脚本应当输出(以词频降序排列):

the 4
is 3
sunny 2
day 1
说明:

不要担心词频相同的单词的排序问题,每个单词出现的频率都是唯一的。
你可以使用一行 Unix pipes 实现吗?

# Read from the file words.txt and output the word frequency list to stdout.
cat words.txt | xargs -n1 | awk '{data[$1] += 1} END {for(str in data) print data[str],str}' | sort -nr | awk '{print $2, $1}'

xargs 用作替换工具,读取输入数据重新格式化后输出
# cat test.txt
a b c d e
f g h
$ cat test.txt | xargs -n3
a b c
d e f
g h
按照数值大小 反向排序
$ sort -nr

cat words.txt | xargs -n1 | awk '{data[$1] += 1} END {for(str in data) print str,data[str]}' | sort -nr -k2

将第二列按照数值大小 反向排序
$ sort -nr -k2

cat words.txt | awk '{for(i=1;i<=NF;i++) data[$i] += 1} END {for(str in data) print str,data[str]}' | sort -nr -k2
awk '{for(i=1;i<=NF;i++) data[$i] += 1} END {for(str in data) print str,data[str]}' words.txt | sort -nr -k2

NF 一条记录的字段的数目
NR 已经读出的记录数,就是行号,从1开始

cat words.txt | xargs -n1 | sort | uniq -c | sort -nr | awk '{print $2,$1}'

统计各行在文件中出现的次数:
$ sort testfile | uniq -c
当重复的行并不相邻时,uniq 命令是不起作用的,因此常与sort一起使用

cat words.txt | tr -s ' ' '\n' | sort | uniq -c | sort -nr | awk '{print $2,$1}'

空格改为换行符
$ tr -s ’ ’ ‘\n’
缩减连续重复的字符成指定的单个字符

8、有效电话号码

给定一个包含电话号码列表(一行一个电话号码)的文本文件 file.txt,写一个 bash 脚本输出所有有效的电话号码。

你可以假设一个有效的电话号码必须满足以下两种格式: (xxx) xxx-xxxx 或 xxx-xxx-xxxx。(x 表示一个数字)

你也可以假设每行前后没有多余的空格字符。

示例:

假设 file.txt 内容如下:

987-123-4567
123 456 7890
(123) 456-7890
你的脚本应当输出下列有效的电话号码:

987-123-4567
(123) 456-7890

# Read from the file file.txt and output all valid phone numbers to stdout.
grep -E "^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$|^[0-9]{3}-[0-9]{3}-[0-9]{4}$" file.txt

^ 为匹配输入字符串的开始位置,$ 为匹配输入字符串的结束位置

awk "/^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$|^[0-9]{3}-[0-9]{3}-[0-9]{4}$/" file.txt
awk "/^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$/ || /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/" file.txt
awk "/^(\([0-9]{3}\) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$/" file.txt
grep -P "^(\(\d{3}\) |\d{3}-)\d{3}-\d{4}$" file.txt

grep -E主要是用来支持扩展正则表达式 加上-P(使用Perl的正则引擎)在MAC OS下面man grep是没有-P参数的,新的主流正则引擎已经默认加上了-P参数

9、转置文件

给定一个文件 file.txt,转置它的内容。

你可以假设每行列数相同,并且每个字段由 ’ ’ 分隔.

示例:

假设 file.txt 文件内容如下:

name age
alice 21
ryan 30
应当输出:

name alice ryan
age 21 30

# Read from the file file.txt and print its transposed content to stdout.
awk '{for(i=1;i<=NF;i++)if(arr[i]){arr[i]=arr[i]" "$i}else{arr[i]=$i}}END{for(key in arr)print arr[key]}' file.txt

10、第十行

给定一个文本文件 file.txt,请只打印这个文件中的第十行。

示例:

假设 file.txt 有如下内容:

Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
你的脚本应当显示第十行:

Line 10
说明:

  1. 如果文件少于十行,你应当输出什么?
  2. 至少有三种不同的解法,请尝试尽可能多的方法来解题。
awk 'NR==10' file.txt

NR在awk中指行号

tail -n +10 file.txt | head -1

tail -n +10 从第 10 行至文件末尾

sed -n '10p' file.txt

-n表示只输出匹配行,p表示Print

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值