C++ 程序 随机生成中缀表达式 四则运算 带括号(可作为后缀表达式转中缀表达式程序
对拍一些用到中缀表达式的程序的时候一个生成器会很有用,写起来个人感觉并不是很容易,我在网上可以查到的中缀表达式生成程序很少,所以自己写了一个,浅讲一下原理,希望可以被更多人看见并用的开心。
原理:随机生成后缀表达式,然后后缀转中缀。
Part 1. 随机生成后缀表达式
随机生成的后缀表达式,需要满足一些条件:
- 符号数量为数字数量 \(- 1\);
- 数字数量最小为 \(2\)。
于是写随机生成的时候,将 \(2 + \operatorname{rand()}\bmod k - 1\) 作为数字的个数,其中 \(k\) 为生成数字的个数上限。
在生成数字的时候,若符号数量不足已生成数字的 \(\frac{2}{3}\),就进行一次判定是否生成一个符号。C++ 整数除法的向零取整决定了不会在只有一个数字的时候进行符号生成判定。生成完所有数字后,将符号补齐至 \(k - 1\) 个。
生成数字和符号时,将每一个生成的数字和符号转换成 string
格式,然后放入队列,最后返回此队列。过程见 randsuffix ()
函数。
Part 2. 后缀表达式转中缀表达式
转换成中缀表达式的流程如下:
- 将返回队列中的队头字符串记录,并出队;
- 判断此字符串是数字还是符号,若是数字前往步骤 3,否则前往步骤 4;
- 将数字的运算优先级设置为 \(9999999\),因为不包含任何运算符,然后将数字和优先级以
pair <string, int>
的格式一同入栈,返回步骤 1。 - 从优先级数组
pr[]
中读此符号的运算优先级; - 连续从栈中记录并弹出两个字符串。
- 判断前面(后弹出的)的字符串,若前面的字符串的运算优先级比此运算符的运算优先级低,则在两边加上括号;优先级的详细意义在下面会进行讲解。
- 判断后面(先弹出的)的字符串,若后面的字符串的运算优先级比此运算符的运算优先级低或运算符为减号、除号时运算优先级相同,则在两边加上括号;
特殊说明:减号除号相同也要加括号的原因是不加括号会改变原意,8 4 2 / /
应当转换成8/(4/2)
(结果为 \(4\))而非8/4/2
(结果为 \(1\))。 - 将这些东西拼接起来,运算优先级更新为运算符的优先级,入栈,返回步骤 1。
有关「运算优先级」的详细说明:
运算优先级指的是这一个公式最外层的最低的运算优先级。数字的运算优先级个人默认设为 \(9999999\),即一个极大的值。套有括号的公式看做数字。
一些奇怪的性质:运算优先级为 \(1\) 时这一串是多项式。
举一些例子:
1+2+3+4-5-6+7
这个公式的运算优先级为 \(1\),因为预处理时 +
和 -
符号的运算优先级均为 \(1\),为此公式中最小值。
7*6*5/4/3/2/1
这个公式的运算优先级为 \(2\),因为预处理时 *
和 /
符号的运算优先级均为 \(2\),为此公式中最小值。
7*6*5/4+3+2-1
这个公式的运算优先级为 \(1\),为此公式中运算优先级的最小值。
(1+2+3)*4*5/6*7
这个公式的运算优先级为 \(2\),因为套有括号的公式看做数字,最外层可以看做 R*4*5/6*7
。
(1+2+3)*4*5/6-7
这个公式的运算优先级为 \(1\),因为最外层可以看做 R*4*5/6-7
,最小的运算优先级是 +
符号的 \(1\)。
运算优先级更新方法(以 1 2 3 / 4 5 + 6 7 * / - *
为例):
- 入栈 \(1\),优先级为 \(9999999\),栈中有
1, 9999999
; - 入栈 \(2\),优先级为 \(9999999\),栈中有
1, 9999999
,2, 9999999
; - 入栈 \(3\),优先级为 \(9999999\),栈中有
1, 9999999
,2, 9999999
,3, 9999999
; - 发现符号
/
,优先级为 \(2\),弹出需要运算的两个数字,优先级均为 \(9999999\),均无需加括号,将整个算式的优先级更新为符号的优先级 \(2\),入栈2/3, 2
,栈中有1, 9999999
,2/3, 2
; - 入栈 \(4\)……
- 入栈 \(5\)……
- 对 \(4\) 和 \(5\) 进行
+
运算,运算优先级更新为 \(1\),入栈,栈中有1, 9999999
,2/3, 2
,4+5, 1
; - 入栈 \(6\)……
- 入栈 \(7\)……
- 对 \(6\) 和 \(7\) 进行
*
运算,运算优先级更新为 \(2\),入栈,栈中有1, 9999999
,2/3, 2
,4+5, 1
,6*7, 1
; - 对
4+5
和6*7
进行/
运算,前者运算优先级为 \(1\),小于除号运算优先级 \(2\),加上括号;后者运算优先级为 \(2\),但是进行的是除法运算,也加上括号。最终变成(4+5)/(6*7), 2
,入栈; - 对
2/3
和(4+5)/(6*7)
进行-
运算,前者运算优先级大于 \(1\),后者运算优先级也大于 \(1\),故都不加括号,变成2/3-(4+5)/(6*7), 1
,入栈; - 对 \(1\) 和
2/3-(4+5)/(6*7), 1
进行*
运算,后者运算优先级为 \(1\),加上括号。最后变成了1*(2/3-(4+5)/(6*7))
。 - 输出最终结果
1*(2/3-(4+5)/(6*7))
。
代码
Click to show code
#include <bits/stdc++.h>
using namespace std;
bool randomNumCount = 0;
// 可以接受的值:0 或 1,0 表示在 2 到 numMaxCount 范围内随机一个,1 表示使用 numMaxCount
int numMaxCount = 1000;
// 可接受范围:2 ~ 300000000,表示生成的最大数字个数
int numMaxLength = 2;
// 可接受范围:1 ~ 9,但是大于 9 也行。表示一个数字最大位数,填进去 2147483647 恶心做题人也不是不可以
int pr[129]; // 运算优先级表
void init () {
// operators priority
// + == - <= * == / <= ^ <= ...... < ()
// pr[(char)] -> pr[(ASCII)]
pr['+'] = 1;
pr['-'] = 1;
pr['*'] = 2;
pr['/'] = 2;
}
queue <string> randsuffix () { // 随机生成后缀表达式
queue <string> ret;
string s;
int r;
if (randomNumCount) {
numMaxCount -= 1;
if (numMaxCount <= 0) numMaxCount = 1;
r = 2 + (((rand () * 10000) + rand ()) % numMaxCount);
// rand 范围为 0 ~ 32767
// 所以理论上随机最大可以生成 327702768
} else {
if (numMaxCount <= 1) numMaxCount = 2;
r = numMaxCount;
}
numMaxLength = max (numMaxLength, 1);
int ops = 0;
for (int i = 1; i <= r; i++) {
// 生成数字
int dg = 1 + (rand () % numMaxLength);
bool tit = 1;
s = "";
while (dg--) {
if (tit == 1) { // 避免前导 0
s += char (49 + (rand () % 9));
tit = 0;
} else {
s += char (48 + (rand () % 10));
}
}
ret.push (s);
s = "";
// 随机生成符号
if (ops < 2 * i / 3 && (rand () % 3) == 0) {
int op = (rand () % 4);
if (op == 0) s += "+";
if (op == 1) s += "-";
if (op == 2) s += "*";
if (op == 3) s += "/";
ret.push (s);
s = "";
ops++;
}
}
// 最后补齐符号
for (; ops < r - 1; ops++) {
int op = (rand () % 4);
if (op == 0) s += "+";
if (op == 1) s += "-";
if (op == 2) s += "*";
if (op == 3) s += "/";
ret.push (s);
s = "";
}
return ret;
}
#define psi pair <string, int>
#define str first
#define pri second
#define mp make_pair
signed main () {
init ();
srand ((unsigned)time(NULL));
queue <string> q = randsuffix ();
stack <psi> s;
// int sz = q.size ();
// cout << "There are " << sz;
// cout << " elements in the queue:\n";
/* // 输出后缀表达式
for (int i = 1; i <= sz; i++) {
string s = q.front ();
q.pop ();
cout << s << " ";
q.push (s);
}
cout << endl;*/
while (!q.empty ()) {
// cout << q.size () << endl;
psi tmp;
tmp.str = q.front ();
q.pop ();
if (!isdigit (tmp.str[0])) {
tmp.pri = pr[tmp.str[0]];
psi t2 = s.top (); s.pop ();
psi t1 = s.top (); s.pop ();
psi ret;
if (t1.pri < tmp.pri) { // 判断前面需不需要加括号
ret.str += "(" + t1.str + ")";
} else {
ret.str += t1.str;
}
ret.str += tmp.str;
if (t2.pri < tmp.pri || // 判断后面需不需要加括号
(tmp.str == "/" && t2.pri <= tmp.pri) || // 特判除法
(tmp.str == "-" && t2.pri <= tmp.pri)) { // 特判减法
ret.str += "(" + t2.str + ")";
} else {
ret.str += t2.str;
}
ret.pri = tmp.pri; // 更新运算优先级
s.push (ret);
} else {
tmp.pri = 9999999; // 数字的运算优先级是最大的
s.push (tmp);
}
}
psi ans = s.top ();
cout << ans.str;
}
已知问题:除数可能为 \(0\)。尚未找到合适的解决方案。个人感觉可能需要套进去一个计算的程序,每次有了除数都计算一下,如果是 \(0\) 就换符号。但是这样可能不太好。如果有更好的解决方案的话,欢迎留言 QAQ
一个可以抑制但是不能完全解决的方案:减少除号生成个数
可能会去学习学习表达式树,然后重写一个 OwO