[C/C++] 复杂计算器——四则运算表达式求值(中缀转后缀表达式)

一、问题背景

【复杂计算器】:使用C/C++编程,从字符串中获取表达式,比如9+(3-1)×3+10÷2,并完成计算。

在之前,由于笔者才疏学浅,掌握知识甚少,完成某次任务【复杂计算器】时,利用了三个栈,而且还是使用的STL中的栈的实现,极大的增大了内存的开销。
于是,这次算是一次补票:利用栈和队列重新实现复杂计算器。

设想是:

  • 首先,我们输入中缀表达式的四则运算表达式
  • 将中缀表达式转换为后缀表达式,并存入队列之中
  • 将队列中的后缀表达式取出,并计算结果

二、原理分析

此处参考为程杰作者的《大话数据结构[溢彩加强版]》

1 后缀表达式计算结果

为了解释后缀表达式的好处,我们先来看看,计算机如何应用后缀表达式计算出最终的结果 20 的。
后缀表达式:9 3 1-3 *+10 2 /+

规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。

  1. 初始化一个空栈。此栈用来对要运算的数字进出使用。


    2. 后缀表达式中前三个都是数字,所以 931 进栈。


    3. 接下来是 -,所以将栈中的 1 出栈作为减数,3 出栈作为被减数,并运算 3-1 得到 2,再将 2 进栈。


    4. 接着是数字 3 进栈



以此类推,直到表达式遍历完成,得到结果:


最后出栈:

2 中缀表达式转后缀表达式

那么,我们是如何由中缀表达式得到后缀表达式的呢?

我们把平时所用的标准四则运算表达式,即9+(3-1)×3+10÷2叫做中缀表达式。

我们现在将中缀表达式转换为后缀表达式。
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终得到后缀表达式为止。

  1. 初始化一空栈,用来对符号进出栈使用。


2. 第一个字符是数字 9,输出 9,后面是符号 +,进栈。

  1. 第三个字符是 (,依然是符号,因其只是左括号,还未配对,故进栈。

  1. 第四个字符是数字 3,输出,总表达式为 9 3,接着是 -,进栈。


5. 接下来是数字 1,输出,总表达式为 9 3 1,后面是符号 ,此时,我们需要去匹配此前的 ( ,所以栈顶依次出栈,并输出,直到 ( 出栈为止。此时左括号上方只有 - ,因此输出 - 。总的输出表达式为 9 3 1-

  1. 紧接着是符号 ×,因为此时的栈顶符号为 +,优先级低于 ×,因此不输出, *进栈。接着是数字 3,输出,总的表达式为 9 3 1 – 3

  1. 之后是符号 + ,此时当前栈顶元素 * 比这个 + 的优先级高,因此栈中元素出栈并输出 (没有比 + 更低的优先级,所以全部出栈),总输出表达式为 9 3 1-3 * +。然后将当前这个符号 + 进栈。也就是说,前 6 张图的栈底的 + 是指中缀表达式中开头的 9 后面那个 + ,而左图中的栈底 (也是栈顶)的 + 是指 9+ (3-1)×3+ 中的最后一个 +


8. 紧接着数字 10,输出,总表达式变为 9 3 1-3 *+10。后是符号 ÷,所以 /进栈。如右图所示。

  1. 最后一个数字 2,输出,总的表达式为 9 3 1 – 3 *+10 2

  1. 因为已经到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为 9 3 1 – 3 *+10 2 /+

三、准备数据结构

我们为计算器编写一个栈和队列,这里笔者使用的C++,仅仅只使用了部分C++的类的特性,方便封装和整合,核心代码部分,C语言也可以食用。

1 栈

定义两个类型,分别表示数字和符号:

	typedef double ElemType;
    typedef char OpType;

我们使用链式结构进行设计我们的结构:
先定义好节点,这里我们为每个节点的数据域声明两个类型,再使用is_data进行判断是数字还是符号:

struct Node {
        ElemType data = 0.0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };

其他部分和一般链栈无异,仅仅用了一些C++的构造函数,重载函数错误检查等功能,C语言修改多增加不重名函数也可以实现。
这里直接上源码:

LinkStack.h

//
// Created by Whisky on 2023/2/9.
//
#ifndef DATA_STRUCTURE_LINKSTACK_H
#define DATA_STRUCTURE_LINKSTACK_H
#include <iostream>

struct LinkStack {
public:
    typedef double ElemType;
    typedef char OpType;
private:
    struct Node {
        ElemType data = 0.0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };
    void check();
public:
    void push(const ElemType &);
    void push(const OpType &);
    void pop();
    void clear();
    Node *top = nullptr;
    unsigned int size = 0;
};

#endif //DATA_STRUCTURE_LINKSTACK_H

LinkStack.cpp

//
// Created by Whisky on 2023/2/9.
//

#include "LinkStack.h"
#include <stdexcept>
#include <cassert>

void LinkStack::push(const LinkStack::ElemType &e) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    p->is_data = true;
    p->data = e;
    p->next = top;
    top = p;
    ++size;
}

void LinkStack::push(const LinkStack::OpType &e) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    p->is_data = false;
    p->op = e;
    p->next = top;
    top = p;
    ++size;
}

void LinkStack::pop() {
    check();
    Node *p = top;
    top = top->next;
    free(p);
    --size;
}

void LinkStack::check() {
    try {
        if (top == nullptr)
            throw std::underflow_error("the stack is empty");
    }
    catch (std::exception &err)
    {
        std::cerr << err.what() << "\n";
        assert(0);
    }
}

void LinkStack::clear() {
    while (top != nullptr) {
        Node *p =top;
        top = top->next;
        free(p);
    }
}

2 队列

我们的队列的节点的数据域也设计为两种类型:

struct Node {
        ElemType data = 0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };

其他与一般链式队列无异。
这里也直接上源码:

LinkQueue.h

//
// Created by Whisky on 2023/2/9.
//

#ifndef DATA_STRUCTURE_LINKQUEUE_H
#define DATA_STRUCTURE_LINKQUEUE_H
#include <iostream>

struct LinkQueue {
public:
    typedef double ElemType;
    typedef char OpType;
    unsigned int length();
    LinkQueue() { front = rear = &node; };
    void push(const ElemType &);
    void push(const OpType &);
    void pop();
private:
    struct Node {
        ElemType data = 0;
        OpType op = 0;
        bool is_data = true;
        Node *next = nullptr;
    };
    Node node;
public:
    Node *front = nullptr;
    Node *rear = nullptr;

};
#endif //DATA_STRUCTURE_LINKQUEUE_H

LinkQueue.cpp

//
// Created by Whisky on 2023/2/9.
//

#include "LinkQueue.h"
#include <cassert>

void LinkQueue::push(const LinkQueue::ElemType &e) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    assert(!(p == nullptr));//申请失败
    p->data = e;
    p->is_data = true;
    p->next = nullptr;
    rear->next = p;
    rear = p;
    ++node.data;
}

void LinkQueue::push(const LinkQueue::OpType &o) {
    Node *p = static_cast<Node *>(malloc(sizeof (Node)));
    assert(!(p == nullptr));//申请失败
    p->op = o;
    p->is_data = false;
    p->next = nullptr;
    rear->next = p;
    rear = p;
    ++node.data;
}

void LinkQueue::pop() {
    assert(!(front == rear));//空了
    Node *p = front->next;
    ElemType tmp = p->data;
    front->next = p->next;
    if (rear == p)
        rear = front;
    free(p);
    --node.data;
}

unsigned int LinkQueue::length() {
    return node.data;
}

四、设计计算器

完成了这基本数据结构的搭建,我们也可以开始设计计算器了。

我们首先定义大致结构,后续功能根据实际编程添加:

class Calculator {
public:
    Calculator(std::istream &);
    Calculator() = default;
    void input(std::istream &);
    void print();
private:
    LinkStack cal_stack;
    LinkQueue cal_queue;
};

计算器利用input成员函数得到表达式,并使用print成员函数输出结果。
为了方便使用我们类的程序员使用,我们添加了重载构造函数,用来直接input。而且input的参数为std::istream &基本IO类,这让用户可以选择多种输入方式(文件、命令框、string字符串流等等)。

Calculator::Calculator(std::istream &is) {
    input(is);
}

1 提取字符串的数字

从IO获取一行数据并存入字符串:

	std::string line;
    getline(is, line);

我们通过遍历字符串,对每个字符进行判断,连续数字就进行移位加合,遇到小数点就利用向右移位加合(这里的移位都是针对10进制的),遇到我们需要的字符就进行处理:

for (auto it = line.begin(); it != line.end() && running; ++it)
{
      double sum = 0.0;
       bool float_flag = false;
       float sum2 = 0.0;
       double power = 0.1;
       while (isdigit(*it) || (*it == '.'))
       {
           if (isdigit(*it)) {
               if (float_flag) {
                   sum2 += power * ((*it) - '0');
                   power *= 0.1;
               }
               else sum = sum * 10 + (*it) - '0';
           }
           else {
               float_flag = true;
           }
           is_dight = true;
           ++it;
           if  (it == line.end())
               running = false;
       }
       sum += sum2;//整数加小数部分
       if (is_dight)
       {
           //sum为数字,等待处理
           is_dight = false;
       }
       if (*it == '+'|| *it == '-'|| *it == '*'||
           *it == '/'|| *it == '('|| *it == ')')
       {
          //*it为操作符,等待处理
       }
}

2 进栈处理

规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终得到后缀表达式为止。

按照之前原理中的方法,我们把得到的数和符号利用栈的特性处理,把结果放进队列。

判断优先级:

#define PRI_HIGH 2
#define PRI_LOW 1

int priority(char ch) {
    if (ch == '*' || ch == '/')
        return PRI_HIGH;
    else
        return PRI_LOW;
}

进栈处理并输出到队列:

void Calculator::input(std::istream &is) {
    std::string line;
    getline(is, line);
    bool is_dight = false;
    bool running = true;
    bool begin_flag = true;
    for (auto it = line.begin(); it != line.end() && running; ++it)
    {
        double sum = 0.0;
        bool float_flag = false;
        float sum2 = 0.0;
        double power = 0.1;
        while (isdigit(*it) || (*it == '.'))
        {
            if (isdigit(*it)) {
                if (float_flag) {
                    sum2 += power * ((*it) - '0');
                    power *= 0.1;
                }
                else sum = sum * 10 + (*it) - '0';
            }
            else {
                float_flag = true;
            }
            is_dight = true;
            ++it;
            if  (it == line.end())
                running = false;
        }
        sum += sum2;//整数加小数部分
        if (is_dight)
        {
            cal_queue.push(sum);//数字直接输出进队列
            is_dight = false;
        }
        if (*it == '+'|| *it == '-'|| *it == '*'||
            *it == '/'|| *it == '('|| *it == ')')//符号借用栈的特性解决
        {
            if (cal_stack.top == nullptr || begin_flag)
            {
                begin_flag = false;
                cal_stack.push(*it);//第一个直接放入栈
            }
            else
            {
                if (*it == ')' || *it == '(')
                {
                    if (*it == '(')
                        begin_flag = true;//左括号之后的符号直接放入栈,避免与左括号比较
                    cal_stack.push(*it);
                }
                else if (priority(*it) > priority(cal_stack.top->op))//符号优先级高于栈顶符号
                    cal_stack.push(*it);
                else {
                	//判断符号与栈顶符号的优先级,优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出(以括号为一个层级),并将当前符号进栈
                    while (cal_stack.top != nullptr)
                    {
                    	if (cal_stack.top->op == '(')
                            break;
                        cal_queue.push(cal_stack.top->op);
                        cal_stack.pop();
                    }
                    cal_stack.push(*it);
                }
            }
            if (*it == ')')
            {
            //遇到右括号,去匹配左括号,同时把栈里面的符号都输出
                while (true)
                {
                    char tmp = cal_stack.top->op;
                    if (tmp != '(' && tmp != ')')
                        cal_queue.push(tmp);
                    cal_stack.pop();
                    if (tmp == '(')
                        break;
                }
            }
        }
    }
    //剩下的依次出栈
    while (cal_stack.size){
        cal_queue.push(cal_stack.top->op);
        cal_stack.pop();
    }
}

3 后缀计算

规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。

用于计算的函数(这里没有处理除数为0的情况,大家可以自行补充):

double cal_operator(double a, char c, double b)
{
    switch (c) {
        case '+':
            return a + b;
        case '-':
            return a - b;
        case '*':
            return a * b;
        case '/':
            return a / b;
        default:
            return 0;
    }
}

后缀计算则非常简单了,遇到数字进栈,遇到符号就运算(注意谁减谁或者谁除谁):

void Calculator::cal() {
    while (cal_queue.length() != 0)
    {
        if (cal_queue.front->next->is_data)//数字进栈
        {
            cal_stack.push(cal_queue.front->next->data);
        }
        else {//符号运算
            char &op = cal_queue.front->next->op;
            double tmp1, tmp2;
            tmp1 = cal_stack.top->data;cal_stack.pop();
            tmp2 = cal_stack.top->data;cal_stack.pop();
            tmp2 =  cal_operator(tmp2, op, tmp1);
            cal_stack.push(tmp2);//运算完再进栈
        }
        cal_queue.pop();
    }
}

4 输出结果

输出结果就是出栈就行了:

void Calculator::print_stack() {
    while (cal_stack.top)
    {
        if (cal_stack.top->is_data)
            std::cout << cal_stack.top->data << " ";
        else
            std::cout << cal_stack.top->op << " ";
        cal_stack.pop();
    }
    std::cout << std::endl;
}

虽然其实写一个cal_stack.top->data就行(因为最后栈里只有一个元素)。这些是笔者用于测试时,用于输出整个栈的代码。




结尾附上完整代码:

Calculator.h

//
// Created by Whisky on 2023/2/9.
//

#ifndef DATA_STRUCTURE_CALCULATOR_H
#define DATA_STRUCTURE_CALCULATOR_H
#include <iostream>
#include "LinkStack.h"
#include "LinkQueue.h"

class Calculator {
public:
    Calculator(std::istream &);
    Calculator() = default;
    void input(std::istream &);
    void print();
    void print_queue();
private:
    void cal();
    void print_stack();

#define PRI_HIGH 2
#define PRI_LOW 1
    LinkStack cal_stack;
    LinkQueue cal_queue;
    friend int priority(char);
};
int priority(char ch);

#endif //DATA_STRUCTURE_CALCULATOR_H

Calculator.cpp

//
// Created by Whisky on 2023/2/9.
//

#include "Calculator.h"

int priority(char ch) {
    if (ch == '*' || ch == '/')
        return PRI_HIGH;
    else
        return PRI_LOW;
}

double cal_operator(double a, char c, double b)
{
    switch (c) {
        case '+':
            return a + b;
        case '-':
            return a - b;
        case '*':
            return a * b;
        case '/':
            return a / b;
        default:
            return 0;
    }
}

void Calculator::cal() {
    while (cal_queue.length() != 0)
    {
        if (cal_queue.front->next->is_data)
        {
            cal_stack.push(cal_queue.front->next->data);
        }
        else {
            char &op = cal_queue.front->next->op;
            double tmp1, tmp2;
            tmp1 = cal_stack.top->data;cal_stack.pop();
            tmp2 = cal_stack.top->data;cal_stack.pop();
            tmp2 =  cal_operator(tmp2, op, tmp1);
            cal_stack.push(tmp2);
        }
        cal_queue.pop();
    }
}

void Calculator::print_queue() {
    while (cal_queue.front->next != nullptr) {
        if (cal_queue.front->next->is_data)
            std::cout << cal_queue.front->next->data << " ";
        else
            std::cout << cal_queue.front->next->op << " ";
        cal_queue.pop();
    }
    std::cout << std::endl;
}

void Calculator::print_stack() {
    while (cal_stack.top)
    {
        if (cal_stack.top->is_data)
            std::cout << cal_stack.top->data << " ";
        else
            std::cout << cal_stack.top->op << " ";
        cal_stack.pop();
    }
    std::cout << std::endl;
}

void Calculator::input(std::istream &is) {
    std::string line;
    getline(is, line);
    bool is_dight = false;
    bool running = true;
    bool begin_flag = true;
    for (auto it = line.begin(); it != line.end() && running; ++it)
    {
        double sum = 0.0;
        bool float_flag = false;
        float sum2 = 0.0;
        double power = 0.1;
        while (isdigit(*it) || (*it == '.'))
        {
            if (isdigit(*it)) {
                if (float_flag) {
                    sum2 += power * ((*it) - '0');
                    power *= 0.1;
                }
                else sum = sum * 10 + (*it) - '0';
            }
            else {
                float_flag = true;
            }
            is_dight = true;
            ++it;
            if  (it == line.end())
                running = false;
        }
        sum += sum2;
        if (is_dight)
        {
            cal_queue.push(sum);
            is_dight = false;
        }
        if (*it == '+'|| *it == '-'|| *it == '*'||
            *it == '/'|| *it == '('|| *it == ')')
        {
            if (cal_stack.top == nullptr || begin_flag)
            {
                begin_flag = false;
                cal_stack.push(*it);
            }
            else
            {
                if (*it == ')' || *it == '(')
                {
                    if (*it == '(')
                        begin_flag = true;
                    cal_stack.push(*it);
                }
                else if (priority(*it) > priority(cal_stack.top->op))
                    cal_stack.push(*it);
                else {
                    while (cal_stack.top != nullptr)
                    {
                        if (cal_stack.top->op == '(')
                            break;
                        cal_queue.push(cal_stack.top->op);
                        cal_stack.pop();
                    }
                    cal_stack.push(*it);
                }
            }
            if (*it == ')')
            {
                while (true)
                {
                    char tmp = cal_stack.top->op;
                    if (tmp != '(' && tmp != ')')
                        cal_queue.push(tmp);
                    cal_stack.pop();
                    if (tmp == '(')
                        break;
                }
            }
        }
    }
    while (cal_stack.size){
        cal_queue.push(cal_stack.top->op);
        cal_stack.pop();
    }

}

void Calculator::print() {
    cal();
    print_stack();
}

Calculator::Calculator(std::istream &is) {
    input(is);
}

.

  • 10
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,下面是关于栈应用(表达式求值)的学习笔记。 ## 1. 表达式求值的基本概念 ### 1.1 中缀表达式 中缀表达式是我们平时写的表达式,如:2 + 3 * 4 - 6 / 2。 ### 1.2 后缀表达式 后缀表达式也叫逆波兰表达式,它是一种不包含括号的表达式。在后缀表达式中,运算符在操作数的后面,因此也叫后缀表示法。例如,上面的中缀表达式的后缀表达式为:2 3 4 * + 6 2 / -。 ### 1.3 前缀表达式 前缀表达式也叫波兰式,它与后缀表达式类似,只是运算符在操作数的前面。例如,上面的中缀表达式的前缀表达式为:- + * 3 4 2 / 6 2。 ### 1.4 运算符优先级 在中缀表达式中,运算符有不同的优先级。通常,乘法和除法的优先级高于加法和减法。如果有括号,则括号内的表达式优先计算。 ### 1.5 中缀表达式后缀表达式中缀表达式换成后缀表达式的过程,也叫中缀表达式的后缀表达式化。具体的换规则如下: - 遍历中缀表达式的每个元素。 - 如果当前元素是操作数,则将其加入后缀表达式中。 - 如果当前元素是运算符,则判断其与栈顶运算符的优先级,如果栈顶运算符优先级高于或等于当前运算符,则弹出栈顶运算符加入后缀表达式中,并继续比较下一个栈顶运算符,直到当前运算符的优先级高于栈顶运算符或栈为空时,将当前运算符入栈。 - 如果当前元素是左括号“(”,则直接入栈。 - 如果当前元素是右括号“)”,则依次弹出栈顶运算符加入后缀表达式中,直到遇到左括号为止,此时将左括号弹出,但不加入后缀表达式中。 ### 1.6 后缀表达式求值 将后缀表达式求值的过程,也叫后缀表达式的求值。具体的求值规则如下: - 遍历后缀表达式的每个元素。 - 如果当前元素是操作数,则将其入栈。 - 如果当前元素是运算符,则弹出栈顶的两个操作数,进行运算,并将运算结果入栈。 - 遍历完后缀表达式后,栈中只剩下一个元素,即为表达式的值。 ## 2. 表达式求值的实现 ### 2.1 中缀表达式后缀表达式的实现 中缀表达式后缀表达式可以使用栈来实现。具体的代码实现如下: ```cpp #include <iostream> #include <stack> #include <string> using namespace std; // 判断一个字符是否为操作符 bool isOperator(char c) { return c == '+' || c == '-' || c == '*' || c == '/'; } // 判断两个操作符的优先级 int getPriority(char op1, char op2) { if ((op1 == '*' || op1 == '/') && (op2 == '+' || op2 == '-')) { return -1; } else if ((op1 == '+' || op1 == '-') && (op2 == '*' || op2 == '/')) { return 1; } else { return 0; } } // 将中缀表达式换成后缀表达式 string infixToPostfix(string infix) { stack<char> opStack; // 运算符栈 string postfix; // 后缀表达式 for (char c : infix) { if (isdigit(c)) { // 如果是数字,直接加入后缀表达式 postfix += c; } else if (isOperator(c)) { // 如果是操作符 while (!opStack.empty() && opStack.top() != '(' && getPriority(opStack.top(), c) >= 0) { postfix += opStack.top(); // 弹出栈顶操作符加入后缀表达式 opStack.pop(); } opStack.push(c); } else if (c == '(') { // 如果是左括号,直接入栈 opStack.push(c); } else if (c == ')') { // 如果是右括号 while (!opStack.empty() && opStack.top() != '(') { postfix += opStack.top(); // 弹出栈顶操作符加入后缀表达式 opStack.pop(); } opStack.pop(); // 弹出左括号 } } while (!opStack.empty()) { // 将剩余的操作符加入后缀表达式 postfix += opStack.top(); opStack.pop(); } return postfix; } int main() { string infix = "2+3*4-6/2"; string postfix = infixToPostfix(infix); cout << postfix << endl; // 输出后缀表达式:234*+62/- return 0; } ``` ### 2.2 后缀表达式求值的实现 后缀表达式求值也可以使用栈来实现。具体的代码实现如下: ```cpp #include <iostream> #include <stack> #include <string> using namespace std; // 判断一个字符是否为操作符 bool isOperator(char c) { return c == '+' || c == '-' || c == '*' || c == '/'; } // 计算两个操作数的运算结果 int calculate(int a, int b, char op) { if (op == '+') { return a + b; } else if (op == '-') { return a - b; } else if (op == '*') { return a * b; } else { return a / b; } } // 计算后缀表达式的值 int evaluate(string postfix) { stack<int> numStack; // 操作数栈 for (char c : postfix) { if (isdigit(c)) { // 如果是数字,将其换成整数并入栈 int num = c - '0'; numStack.push(num); } else if (isOperator(c)) { // 如果是操作符 int b = numStack.top(); numStack.pop(); int a = numStack.top(); numStack.pop(); int result = calculate(a, b, c); numStack.push(result); } } return numStack.top(); } int main() { string postfix = "234*+62/-"; int result = evaluate(postfix); cout << result << endl; // 输出计算结果:8 return 0; } ``` ## 3. 总结 栈在表达式求值中的应用是很常见的,掌握了这个知识点,对于编写计算器等应用程序会有很大的帮助。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值