中缀表达式求值问题

中缀表达式的求值问题是一个比较常见的问题之一,我们通常在编写程序时,直接写出表达式让编译器去处理,很少去关心编译器是怎么对表达式进行求值的,今天我们来一起了解一下其中具体的原理和过程。

表达式一般来说有三种:前缀表达式、中缀表达式、后缀表达式,其中后缀表达式又叫做逆波兰表达式。中缀表达式是最符合人们思维方式的一种表达式,顾名思义,就是操作符在操作数的中间。而前缀表达式和后缀表达式中操作符分别在操作数的前面和操作数的后面。举个例子:

3+2

这个是最简单的一个中缀表达式。而其等同的前缀表达式形式为+32,后缀表达式形式为32+

那么一些朋友可能会问既然中缀表达式最符合人类的思维习惯,为什么还需要前缀表达式和后缀表达式?先看一个例子,假如在前面的表达式基础上加一点东西:

3+2*5

此时的表达式很显然,如果进行计算,则先计算2*5,最后计算加法。但是如果需要先计算加法运算呢?则必须加上括号,(3+2)*5

而如果用后缀表达式来表示,则为32+5*,那么该表达式的计算顺序为3+2 --> (3+2)*5

区别就在这里,后缀表达式不需要用括号就能表示出 整个表达式哪部分运算先进行。同理,前缀表达式也是如此。这种表达式正好最符合计算机的处理方式,因为后缀表达式和前缀表达式求值不需要考虑优先级的问题,计算机处理起来便简单很多。

今天我们这里主要讲解中缀表达式和后缀表达式(前缀表达式和后缀表达式很类似,就不做过多赘述),下面是讲解大纲:

中缀表达式如何直接求值?
后缀表达式如何直接求值?
中缀表达式如何转换为后缀表达式?

1.中缀表达式直接求值

对于中缀表达式求值来说,一般最常见的直接解决办法就是利用栈,一个栈用来保存操作数,一个栈用来保存操作符。

为了简便起见,暂时表达式中只考虑简单的+,-,*,/运算,只有圆括号,并且都是整数。

假如有这样一个表达式:((3+5*2)+3)/5+6/4*2+3

对于这样一个表达式,如果让你来设计操作数和操作符进栈的出栈的规则,你会怎么设计?

先不看这么复杂的表达式,考虑一下简单点的,还是前面的3+2*5,那么很显然先进行乘法运算,后进行加法运算,但是由于操作符在操作数中间,所以当一个操作符进操作符栈时,该操作符的两个操作数并没有都进入到操作数栈中,那么如何解决呢?只有在后面一个操作符进操作符栈时,前面的一个操作符所作用的两个操作数才会全部进栈。比如3+2*5,栈的变化过程为:

操作数栈:3 操作数栈:3 操作数栈:3 2

操作符栈:空 操作符栈:+ 操作符栈:+

注意此时遇到操作符“*”,是不是需要弹出操作数栈中的两个操作数进行运算呢,很显然不是,因为乘法运算法比操作符栈的栈顶运算符优先级高,也就是说当前的操作符在“+”前进行运算,那么还需要将当前操作符压栈,则变成:

操作数栈:3 2 操作数栈:3 2 5

操作符栈:+ * 操作符栈:+ *

此时到了表达式的结尾,既然栈顶的操作符的优先级比栈底的操作符的优先级高,那么可以取操作符栈的栈顶操作符和操作数栈的栈顶两个元素进行计算,则得到2*5=10,(注意从操作数栈先弹出的操作数为右操作数)。此时得到10 ,则应该把10继续压到操作数栈中,继续取操作符栈的栈顶操作符,依次进行下去,则当操作符栈为空时表示计算过程完毕,此时操作数栈中剩下的唯一元素便是整个表达式的值。

再换个例子:2*5+3,这个表达式跟前面表达式的结果虽然相同,但是操作数和操作符入栈和出栈的顺序发生了很大变化:

操作数栈:2 操作数栈:2 操作数栈:2 5

操作符栈:空 操作符栈:* 操作符栈:*

此时遇到"+",而操作符栈的栈顶操作符为“*”,栈顶操作符优先级更高,表示此时可以取操作符栈顶操作符进行运算,那么栈变成:

操作数栈:10 操作数栈:10 3

操作符栈:空 操作符栈:+

后面的过程跟前面一个例子类似。

如果复杂一点,比如包含有括号,连续的乘除法这些怎么处理呢?道理是一样的,对于左括号直接入栈,碰到右括号,则一直将操作符退栈,直到碰到左括号,则括号中的表达式计算完毕。对于连续的乘除法,跟前面例子中处理过程类似。只需要记住一点:只有当前操作符的优先级高于操作符栈栈顶的操作符的优先级,才入栈,否则弹出操作符以及操作数进行计算直至栈顶操作符的优先级低于当前操作符,然后将当前操作符压栈。当所有的操作符处理完毕(即操作符栈为空时),操作数栈中剩下的唯一一个元素便是最终的表达式的值。而操作符的优先级为:+-优先级是一样的,*/优先级是一样的,+-的优先级低于*/的优先级。

不过需要注意的是在求值之前需要对表达式进行预处理,去掉空格、识别负号(区分“-”是作为减号还是负号),提取操作数等。

对于“-”的区分,主要判别方法为:

  • 1)若前一个字符为‘(’,则必定为负号;

  • 2)若前一个字符为')'或者数字,则必定为减号;

  • 3)若前面一个字符为其他运算符,如*/,则必定是负号;

  • 4)若前面没有字符,即该字符为表达式的第一个字符,则必定是负号。

也就是说只有一种情况下,”-“是作为减号使用的,就是前一个字符为')'或者数字的时候。

如果判断出“-”是作为负号使用的,这里我采用“#”来代替“-”,并将其作为一种运算(优先级最高)。比如:-3*2

我采取的做法是将"#"入栈,然后当遇到“*”时,由于栈顶操作符为"#",因此取#,然后取操作数栈的栈顶元素(只取一个)进行运算,然后再把结果压栈。

#include <iostream>
#include <vector>
#include <stack>
#include <string>
#include <cstdlib>
#include <cctype>
#include <cstring>
using namespace std;
 
 
vector<string> preParse(char *str)   //对中缀表达式进行预处理,分离出每个token
{
    vector<string> tokens;
    int len = strlen(str);
    char *p = (char *)malloc((len+1)*sizeof(char));  //注意不要用 char *p = (char *)malloc(sizeof(str))来申请空间
    int i=0,j=0;
    while(i<len)          //去除表达式中的空格
    {
        if(str[i]==' ')
        {
            i++;
            continue;
        }
        p[j++] = str[i++];
    }
    p[j]='\0';
    j=0;
    len = strlen(p);
    while(j<len)
    {
        char temp[2];
        string token;
        switch(p[j])
        {
            case '+':
            case '*':
            case '/':
            case '(':
            case ')':
                {
                    temp[0] =p[j];
                    temp[1] = '\0';
                    token=temp;
                    tokens.push_back(token);
                    break;
                }
            case '-':
                {
                    if(p[j-1]==')'||isdigit(p[j-1]))  //作为减号使用
                    {
                        temp[0] =p[j];
                        temp[1] = '\0';
                        token=temp;
                        tokens.push_back(token);
                    }
                    else    //作为负号使用
                    {
                        temp[0] ='#';
                        temp[1] = '\0';
                        token=temp;
                        tokens.push_back(token);
                    }  
                    break;
                }
            default:     //是数字
                {
                    i = j;
                    while(isdigit(p[i])&&i<len)
                    {
                        i++;
                    }
                    char *opd = (char *)malloc(i-j+1);
                    strncpy(opd,p+j,i-j);
                    opd[i-j]='\0';
                    token=opd;
                    tokens.push_back(token);
                    j=i-1;
                    free(opd);
                    break;
                }  
        }
        j++;
    }
    free(p);
    return tokens;
}
 
int getPriority(string opt)
{
    int priority;
    if(opt=="#")
        priority = 3;  
    else if(opt=="*"||opt=="/")
        priority = 2;
    else if(opt=="+"||opt=="-")
        priority = 1;
    else if(opt=="(")
        priority = 0;
    return priority;
}
 
 
void calculate(stack<int> &opdStack,string opt)
{
    if(opt=="#")  //进行负号运算
    {
        int opd = opdStack.top();
        int result = 0-opd;
        opdStack.pop();
        opdStack.push(result);
        cout<<"操作符:"<<opt<<" "<<"操作数:"<<opd<<endl; 
    }
    else if(opt=="+")
    {
        int rOpd = opdStack.top();
        opdStack.pop();
        int lOpd = opdStack.top();
        opdStack.pop();
        int result = lOpd + rOpd;
        opdStack.push(result); 
         
        cout<<"操作符:"<<opt<<" "<<"操作数:"<<lOpd<<" "<<rOpd<<endl; 
    }
    else if(opt=="-")
    {
        int rOpd = opdStack.top();
        opdStack.pop();
        int lOpd = opdStack.top();
        opdStack.pop();
        int result = lOpd - rOpd;
        opdStack.push(result); 
        cout<<"操作符:"<<opt<<" "<<"操作数:"<<lOpd<<" "<<rOpd<<endl; 
    }
    else if(opt=="*")
    {
        int rOpd = opdStack.top();
        opdStack.pop();
        int lOpd = opdStack.top();
        opdStack.pop();
        int result = lOpd * rOpd;
        opdStack.push(result); 
        cout<<"操作符:"<<opt<<" "<<"操作数:"<<lOpd<<" "<<rOpd<<endl; 
    }
    else if(opt=="/")
    {
        int rOpd = opdStack.top();
        opdStack.pop();
        int lOpd = opdStack.top();
        opdStack.pop();
        int result = lOpd / rOpd;
        opdStack.push(result); 
        cout<<"操作符:"<<opt<<" "<<"操作数:"<<lOpd<<" "<<rOpd<<endl; 
    }
}
 
int evaMidExpression(char *str)   //中缀表达式直接求值
{
    vector<string> tokens = preParse(str);
    int i=0;
    int size = tokens.size();
     
    stack<int> opdStack;     //存储操作数
    stack<string> optStack;   //存储操作符
    for(i = 0; i < size; i++)
    {
        string token = tokens[i];
        if(token=="#" || token=="+" || token=="-" || token=="*" || token=="/")
        {
            if(optStack.size() == 0)   //如果操作符栈为空
            {
                optStack.push(token);
            }
            else {
                int tokenPriority = getPriority(token);
                string topOpt = optStack.top();
                int topOptPriority = getPriority(topOpt);
                if(tokenPriority > topOptPriority) {
                    optStack.push(token);
                }else{
                    while(tokenPriority <= topOptPriority)
                    {
                        optStack.pop();
                        calculate(opdStack, topOpt);
                        if(optStack.size() > 0){
                            topOpt = optStack.top();
                            topOptPriority = getPriority(topOpt);
                        }else
                            break;
                    }
                    optStack.push(token);
                }
            }  
        }
        else if(token=="("){
            optStack.push(token);
        }
        else if(token==")"){
            while(optStack.top()!="("){
                string topOpt = optStack.top();
                calculate(opdStack, topOpt);
                optStack.pop();
            }
            optStack.pop();
        }else{   //如果是操作数,直接入操作数栈
            opdStack.push(atoi(token.c_str()));
        }
    }
    while(optStack.size() != 0)
    {
        string topOpt = optStack.top();
        calculate(opdStack,topOpt);
        optStack.pop();
    }
    return opdStack.top();
}
 
 
int main(int argc, char *argv[])
{
    char *str = "((3+5*2)+3)/5+(-6)/4*2+3";
    cout << evaMidExpression(str) << endl;
    return 0;
}

2.后缀表达式直接求值

由于后缀表达式不需要用括号来表示计算顺序,因此处理起来比较简单,具体的可以参照:
Evaluate Reverse Polish Notation

3.中缀表达式如何转为后缀

大部分数据结构教材在讲述 栈的时候都会涉及到中缀表达式转为后缀表达式的问题,因为这个是栈的典型应用之一。因此很多教材上都会利用栈来进行转换,这里我们来讨论一下最常见的两种转换思路和一种简便的验证方法。

利用二叉树进行转换
  由于二叉树本身结构的特殊性,使得我们可以利用它来很轻松地将中缀表达式转变成后缀表达式,事实上,只要根据中缀表达式建立好相应的二叉树之后,直接对二叉树进行前序遍历和后序遍历便可得到前缀表达式和后缀表达式。在利用二叉树来表示表达式时,用叶子节点来存储操作数,用内部节点存储操作符,比如这样一个表达式3*5+5/2+(3+5)*2,表示成二叉树的形式(注意其有等同的其他形式)就是:
  image-20210430154641870
其实讲中缀表达式的过程转变成二叉树的形式是一个递归的过程,比如有一个表达式,其对应的的二叉树的根节点必定是优先级最低的操作符(也就是说是整个表达式中最后进行的运算操作),然后再在操作符的左部分中找出最后进行的操作符作为根节点的左孩子,在操作符的右部分中找出最后进行的操作符作为根节点的右孩子,然后知道左部分或者右部分是单纯的操作数,则作为叶子节点,直到整个二叉树建立完毕。
下面是具体实现:

参考了一下这篇博文的实现,但是这篇博文没有考虑减号作为负号使用的情况
中缀表达式 转换成 前缀表达式(二叉树实现)

#include <iostream>
#include <string>
#include <cctype>
#include <cstring>
#include <cstdlib>
#include <queue>
using namespace std;

typedef struct node
{
  struct node *left;
  struct node *right;
  char *data;
}BinTree;

char * preProcess(char *str)   //预处理,除去空格,将负号替代为#
{
  int len = strlen(str);
  char *p = (char *)malloc(sizeof(char)*len);
  int i=0,j=0;
  while(i<len)          //去除表达式中的空格
  {
      if(str[i]==' ')
      {
          i++;
          continue;
      }
      p[j++] = str[i++];
  }
  p[j]='\0';
  j=0;
  len = strlen(p);
  while(j<len)
  {
      if(p[j]=='-')
      {
          if(!(p[j-1]==')'||isdigit(p[j-1])))  //作为减号使用
          {
              p[j]='#';  
          }
      }
      j++;
  }
  return p;
}


/*
最后执行的操作符一定是在括号外面,也就是说brackets一定是等于0的,
*/
int indexOfOpt(char *str,int begin ,int end)   //寻找最后执行的操作符的下标
{
  int i;
  int brackets=0;                    //所在括号层次
  int index = -1;
  int existAddOrMinus = 0;
  int existMulOrDevide = 0;
  while(str[begin]=='('&&str[end]==')')   //去除最外层的括号
  {
      begin++;
      end--;
  }
  for(i=begin;i<=end;i++)
  {
      if(str[i]=='(')
          brackets++;
      else if(str[i]==')')
          brackets--;
      else if((str[i]=='+'||str[i]=='-')&&brackets==0)
      {
          index = i;
          existAddOrMinus = 1;    //存在加减号
      }
      else if((str[i]=='*'||str[i]=='/')&&brackets==0&&existAddOrMinus==0)
      {
          index = i;
          existMulOrDevide = 1;  //存在乘除号
      }
      else if(str[i]=='#'&&brackets==0&&existAddOrMinus==0&&existMulOrDevide==0)  //用'#'代表负号
      {
          index = i;
      }
  }
  return index;
}

BinTree * createBinTree(char *str,int begin,int end)
{
  BinTree *p =(BinTree *)malloc(sizeof(BinTree));;
  int index = indexOfOpt(str,begin,end);
  cout<<"index:"<<index<<endl; 
  if(index==-1)   //表示只有操作数了
  {
      while(str[begin]=='('&&str[end]==')')
      {
          begin++;
          end--;
      }
      p->data = (char *)malloc(sizeof(end-begin+2));
      int i,j=0;
      for(i=begin;i<=end;i++) 
          p->data[j++] = str[i];
      p->data[j]='\0';
      p->left = NULL;
      p->right = NULL;
      cout<<"操作数:"<<p->data<<endl;
  }
  else
  {
      p->data = (char*)malloc(2);
      p->data[0] = str[index];
      p->data[1]='\0';
      cout<<"操作符:"<<p->data<<endl;
           
      while(str[begin]=='('&&str[end]==')')
      {
          begin++;
          end--;
      }
      if(str[index]=='#')  //是负号
      {
          p->left = NULL;
      }
      else
      {
          p->left = createBinTree(str,begin,index-1);
      }
      p->right = createBinTree(str,index+1,end);
  }
  return p;
}

void preOrder(BinTree *root)
{
  if(root!=NULL)
  {
      cout<<root->data<<" ";
      preOrder(root->left);
      preOrder(root->right);
  }
}

void inOrder(BinTree *root)
{
  if(root!=NULL)
  {
      inOrder(root->left);
      cout<<root->data<<" ";
      inOrder(root->right);
  }
}

void postOrder(BinTree *root)
{
  if(root!=NULL)
  {
      postOrder(root->left);
      postOrder(root->right);
      cout<<root->data<<" ";
  }
}



int main(void)
{
  char *str = "((3+5*2)+3)/5+(-6)/4*2+3";
  char *newStr = preProcess(str);
  cout<<newStr<<endl;
  BinTree *root=createBinTree(newStr,0,strlen(newStr)-1);
  inOrder(root);
  cout<<endl;
  postOrder(root);
  cout<<endl;
  system("pause");
  return 0;
}
  • 利用栈进行转换

利用栈进行转换的思路其实跟前面直接对中缀表达式求值的过程类似,在这过程中需要一个栈用来保存操作符optStack,需要一个数组用来保存后缀表达式suffix[],然后从头到尾扫描表达式

1)如果遇到操作符,则跟optStack的栈顶操作符比较优先级,如果大于栈顶操作符的优先级,则入栈,否则不断取栈顶操作符加到suffix的末尾,直到栈顶操作符优先级低于该操作符,然后将该操作符入栈;

2)遇到操作数,直接加到suffix的末尾

3)遇到左括号,入栈;

4)遇到右括号,则依次弹出栈顶操作符加到suffix的末尾,直到遇到左括号,然后将左括号出栈
具体实现:

#include <iostream>
#include <vector>
#include <stack>
#include <string>
#include <cstdlib>
#include <cctype>
#include <cstring>
using namespace std;
 
 
vector<string> preParse(char *str)   //对中缀表达式进行预处理,分离出每个token
{
    vector<string> tokens;
    int len = strlen(str);
    char *p = (char *)malloc((len+1)*sizeof(char));  //注意不要用 char *p = (char *)malloc(sizeof(str))来申请空间
    int i=0,j=0;
    while(i<len)          //去除表达式中的空格
    {
        if(str[i]==' ')
        {
            i++;
            continue;
        }
        p[j++] = str[i++];
    }
    p[j]='\0';
    j=0;
    len = strlen(p);
    while(j<len)
    {
        char temp[2];
        string token;
        switch(p[j])
        {
            case '+':
            case '*':
            case '/':
            case '(':
            case ')':
                {
                    temp[0] =p[j];
                    temp[1] = '\0';
                    token=temp;
                    tokens.push_back(token);
                    break;
                }
            case '-':
                {
                    if(p[j-1]==')'||isdigit(p[j-1]))  //作为减号使用
                    {
                        temp[0] =p[j];
                        temp[1] = '\0';
                        token=temp;
                        tokens.push_back(token);
                    }
                    else    //作为负号使用
                    {
                        temp[0] ='#';
                        temp[1] = '\0';
                        token=temp;
                        tokens.push_back(token);
                    }  
                    break;
                }
            default:     //是数字
                {
                    i = j;
                    while(isdigit(p[i])&&i<len)
                    {
                        i++;
                    }
                    char *opd = (char *)malloc(i-j+1);
                    strncpy(opd,p+j,i-j);
                    opd[i-j]='\0';
                    token=opd;
                    tokens.push_back(token);
                    j=i-1;
                    free(opd);
                    break;
                }  
        }
        j++;
    }
    free(p);
    return tokens;
}
 
int getPriority(string opt)
{
    int priority;
    if(opt=="#")
        priority = 3;  
    else if(opt=="*"||opt=="/")
        priority = 2;
    else if(opt=="+"||opt=="-")
        priority = 1;
    else if(opt=="(")
        priority = 0;
    return priority;
}
 
vector<string> toSuffix(char *str)  //转变为后缀形式
{
    vector<string> tokens = preParse(str);
    int i=0;
    int size = tokens.size();
     
    vector<string> suffix;     //存储后缀表达式
    stack<string> optStack;   //存储操作符
     
    for(i=0;i<size;i++)
    {
        string token = tokens[i];
        if(token=="#"||token=="+"||token=="-"||token=="*"||token=="/")
        {
            if(optStack.size()==0)   //如果操作符栈为空
            {
                optStack.push(token);
            }
            else
            {
                int tokenPriority = getPriority(token);
                string topOpt = optStack.top();
                int topOptPriority = getPriority(topOpt);
                if(tokenPriority>topOptPriority)
                {
                    optStack.push(token);
                }
                else
                {
                    while(tokenPriority<=topOptPriority)
                    {
                        optStack.pop();
                        suffix.push_back(topOpt);
                        if(optStack.size()>0)
                        {
                            topOpt = optStack.top();
                            topOptPriority = getPriority(topOpt);
                        }
                        else
                            break;
                         
                    }
                    optStack.push(token);
                }
            }  
        }
        else if(token=="(")
        {
            optStack.push(token);
        }
        else if(token==")")
        {
            while(optStack.top()!="(")
            {
                string topOpt = optStack.top();
                suffix.push_back(topOpt);
                optStack.pop();
            }
            optStack.pop();
        }
        else   //如果是操作数,直接入操作数栈
        {
            suffix.push_back(token);
        }
    }
    while(optStack.size()!=0)
    {
        string topOpt = optStack.top();
        suffix.push_back(topOpt);
        optStack.pop();
    }
    return suffix;
}
 
 
 
int main(int argc, char *argv[])
{
    char *str = "((3+5*2)+3)/5+(-6)/4*2+3";
    vector<string> suffix = toSuffix(str);
    int size = suffix.size();
    for(int i=0;i<size;i++)
        cout<<suffix[i]<<" ";
    cout<<endl;
    return 0;
}
  • 简便验证方法

最后一种办法可以很快速地求出中缀表达式对应的前缀表达式和后缀表达式,就是添括号去括号法。

比如有表达式: (3+5*2)-2*3

先对每一个小部分添加括号: ((3+(5*2))-(2*3))

然后将每个操作符放到括号后面:((3(52)*)+(23)*)-

然后去括号:352*+23*-

便得到了后缀表达式,前缀表达式类似(只需把操作符放到括号前面即可)。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于中缀表达式求值,一般需要借助栈来完成。具体的步骤如下: 1. 创建一个运算符栈和一个操作数栈。 2. 从左到右遍历中缀表达式的每一个元素。 - 如果当前元素是操作数,直接将其压入操作数栈。 - 如果当前元素是运算符,分为以下几种情况处理: - 如果运算符栈为空或者栈顶运算符是"(",则直接将该运算符压入运算符栈。 - 如果当前运算符的优先级大于栈顶运算符的优先级,将当前运算符压入运算符栈。 - 如果当前运算符的优先级小于或等于栈顶运算符的优先级,将栈顶运算符弹出,并将其与操作数栈栈顶的两个操作数进行计算,将计算结果压入操作数栈,并将当前运算符压入运算符栈。重复这一步骤直到当前运算符的优先级大于栈顶运算符的优先级。 - 如果当前运算符为")",则依次弹出运算符栈的栈顶元素,直到栈顶元素为"("。将弹出的运算符与操作数栈栈顶的两个操作数进行计算,将计算结果压入操作数栈。 3. 遍历完中缀表达式后,将运算符栈中的剩余运算符依次弹出,并对操作数栈栈顶的两个操作数进行计算,将计算结果压入操作数栈。 4. 遍历完后,将操作数栈中的唯一元素即为中缀表达式的求值结果。 需要注意的是,对于乘法和除法这样的高优先级运算符,要先进行计算再进行压栈操作,而对于加法和减法这样的低优先级运算符,只有在栈顶运算符优先级小于当前运算符时才进行计算。 这就是中缀表达式求值的基本步骤。通过按照以上步骤运算,可以得到中缀表达式c的求值结果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值