zoukankan      html  css  js  c++  java
  • 中缀表达式求值问题

                               中缀表达式求值问题

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

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

      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)若前面一个字符为其他运算符,如*,/,则必定是负号;

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

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

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

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

      下面是具体实现:

    /*2014.5.6 测试环境: mingw*/ 
    #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]='';
    	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] = '';
    					token=temp;
    					tokens.push_back(token);
    					break;
    				}
    			case '-':
    				{
    					if(p[j-1]==')'||isdigit(p[j-1]))  //作为减号使用 
    					{
    						temp[0] =p[j];
    						temp[1] = '';
    						token=temp;
    						tokens.push_back(token);
    					}
    					else    //作为负号使用 
    					{
    						temp[0] ='#';
    						temp[1] = '';
    						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]='';
    					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.后缀表达式直接求值

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

      http://www.cnblogs.com/dolphin0520/p/3708587.html

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

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

    • 利用二叉树进行转换

      由于二叉树本身结构的特殊性,使得我们可以利用它来很轻松地将中缀表达式转变成后缀表达式,事实上,只要根据中缀表达式建立好相应的二叉树之后,直接对二叉树进行前序遍历和后序遍历便可得到前缀表达式和后缀表达式。在利用二叉树来表示表达式时,用叶子节点来存储操作数,用内部节点存储操作符,比如这样一个表达式3*5+5/2+(3+5)*2,表示成二叉树的形式(注意其有等同的其他形式)就是:

      

      其实讲中缀表达式的过程转变成二叉树的形式是一个递归的过程,比如有一个表达式,其对应的的二叉树的根节点必定是优先级最低的操作符(也就是说是整个表达式中最后进行的运算操作),然后再在操作符的左部分中找出最后进行的操作符作为根节点的左孩子,在操作符的右部分中找出最后进行的操作符作为根节点的右孩子,然后知道左部分或者右部分是单纯的操作数,则作为叶子节点,直到整个二叉树建立完毕。

      下面是具体实现:

       参考了一下这篇博文的实现,但是这篇博文没有考虑减号作为负号使用的情况。

      http://blog.csdn.net/ericming200409/article/details/5919883

       

    /*
    测试环境:VS2010
    */
    #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]='';
    	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]='';
    		p->left = NULL;
    		p->right = NULL;
    		cout<<"操作数:"<<p->data<<endl;
    	}
    	else
    	{
    		p->data = (char*)malloc(2);
    		p->data[0] = str[index];
    		p->data[1]='';
    		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;
    }
    

      上述代码在VS2010下运行是没有问题的,但是在gcc下编译运行会崩溃,调试了很久没发现原因(如果有哪位朋友知道原因的请麻烦告知)。测试结果:

    • 利用栈进行转换

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

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

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

      3)遇到左括号,入栈;

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

      具体实现:

     

    /*2014.5.6 测试环境: mingw*/ 
    #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]='';
    	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] = '';
    					token=temp;
    					tokens.push_back(token);
    					break;
    				}
    			case '-':
    				{
    					if(p[j-1]==')'||isdigit(p[j-1]))  //作为减号使用 
    					{
    						temp[0] =p[j];
    						temp[1] = '';
    						token=temp;
    						tokens.push_back(token);
    					}
    					else    //作为负号使用 
    					{
    						temp[0] ='#';
    						temp[1] = '';
    						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]='';
    					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*-

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

  • 相关阅读:
    lua编程之协程介绍
    lua编程之元表与元方法
    设计模式系列之单例模式
    设计模式系列之生成器模式
    设计模式系列之抽象工厂模式
    设计模式系列之原型模式
    设计模式系列之工厂模式
    stl源码分析之hash table
    2018/2019款 MacBookPro 接口失灵的原因及解决方案
    test
  • 原文地址:https://www.cnblogs.com/dolphin0520/p/3708602.html
Copyright © 2011-2022 走看看