查看: 1161| 回复: 7
跳转到指定楼层
上一主题 下一主题
收起左侧

[高频题] 讨论一道表达式问题

全局:

注册一亩三分地论坛,查看更多干货!

您需要 登录 才可以下载或查看附件。没有帐号?注册账号

x
本帖最后由 玛玛哈哈 于 2021-1-29 05:29 编辑

想问一下大家这道题的思路或对我的代码指出哪里可以优化就更感激不尽了。回答的都加米。

给定一个只包含数字、加减乘除的算术表达式,要求按照运算顺序添加括号。所有数字大小都是 0~9,也就是只有个位。
例子:
输入: 1+4*3-2/1
输出: ((1+(4*3))-(2/1))

我尝试了两种解法, 都是有一部分数据超时了。 一种是用双栈直接对中缀表达式操作, 另外一种是先转成后缀表达式再在后缀表达式上操作。

以下是我的代码: 这道题应该是线性复杂度才能全部过, 我觉得我的代码瓶颈应该是在每次string都需要拼接, 这个要是string很长的情况下开销很大(比如第一种解法中的第30行和第40行)。 想问一下大家有没有办法解决这个问题或者说是有更好的解法。谢谢

这个是用双栈直接对中缀表达式操作
  1. # include <iostream>
  2. # include <stack>
  3. # include <string>
  4. using namespace std;

  5. // 不转后缀表达式, 直接对中缀表达式求解
  6. int getPrecedence(string &oprator) {
  7.     if (oprator == "+" || oprator == "-") return 1;
  8.     return 2;
  9. }

  10. bool priorityCompare(string &oprator1, string &oprator2) {
  11.     return getPrecedence(oprator1) >= getPrecedence(oprator2);
  12. }

  13. // expr:算术表达式
  14. // 返回值:加上括号后的表达式
  15. string solve(string &expr) {
  16.     stack<string> expStack;
  17.     stack<string> opStack;
  18.     for (char c: expr) {
  19.         string s = string(1, c);
  20.         if (isdigit(c)) {
  21.             expStack.push(s);
  22.         } else {
  23.             while (!opStack.empty() && priorityCompare(opStack.top(), s)) {
  24.                 string op = opStack.top(); opStack.pop();
  25.                 string right = expStack.top(); expStack.pop();
  26.                 string left = expStack.top(); expStack.pop();
  27.                 string newExp = "(" + left + op + right + string(")");
  28.                 expStack.push(newExp);
  29.             }
  30.             opStack.push(s);
  31.         }
  32.     }
  33.     while (expStack.size() > 1) {
  34.         string op = opStack.top(); opStack.pop();
  35.         string right = expStack.top(); expStack.pop();
  36.         string left = expStack.top(); expStack.pop();
  37.         string newExp = "(" + left + op + right + string(")");
  38.         expStack.push(newExp);
  39.     }
  40.     return expStack.top();
  41. }


  42. int main() {
  43.     ios::sync_with_stdio(false);
  44.     string s;
  45.     cin >> s;
  46.     cout << solve(s) << '\n';
  47.     return 0;
  48. }
复制代码



这个是先转成后缀再操作的代码:
  1. # include <iostream>
  2. # include <stack>
  3. # include <vector>
  4. # include <string>
  5. using namespace std;

  6. // 1. use shunting-yard algorithm to convert to RPN
  7. // 2. use RPN evaluation algorithm to add brackets

  8. class Op {
  9. public:
  10.     char oprator;
  11.     int oprand;
  12.     bool isOprand;

  13.     Op() {}

  14.     Op(int oprand) {
  15.         this->isOprand = true;
  16.         this->oprand = oprand;
  17.     }

  18.     Op(char oprator) { // 运算符
  19.         this->isOprand = false;
  20.         this->oprator = oprator;
  21.     }

  22. };

  23. int getPrecedence(char &oprator) {
  24.     if (oprator == '+' || oprator == '-') return 1;
  25.     return 2;
  26. }

  27. bool priorityCompare(char &oprator1, char &oprator2) {
  28.     return getPrecedence(oprator1) >= getPrecedence(oprator2);
  29. }

  30. vector<Op> convertToRPN(string &expr) { // use shunting-yard algorithm
  31.     vector<Op> res;
  32.     stack<Op> opStack;
  33.     for (char c: expr) {
  34.         // check if is digit or not
  35.         Op curOp;
  36.         if (isdigit(c)) { // yes: push to back of res
  37.             curOp = Op(c - '0');
  38.             res.push_back(curOp);
  39.         } else { // no: while top of stack's precedence is no smaller than current operator => push to back of res
  40.             curOp = Op(c);
  41.             while (!opStack.empty() && priorityCompare(opStack.top().oprator, curOp.oprator)) {
  42.                 res.push_back(opStack.top());
  43.                 opStack.pop();
  44.             }
  45.             opStack.push(curOp);
  46.         }
  47.     }
  48.     while (!opStack.empty()) {
  49.         res.push_back(opStack.top());
  50.         opStack.pop();
  51.     }
  52.     return res;
  53. }

  54. // expr:算术表达式
  55. // 返回值:加上括号后的表达式
  56. string solve(string expr) {
  57.     stack<string> expStack;
  58.     vector<Op> rpn = convertToRPN(expr);
  59.     for (Op op: rpn) {
  60.         if (op.isOprand) {
  61.             expStack.push(to_string(op.oprand));
  62.         } else {
  63.             string right = expStack.top(); expStack.pop();
  64.             string left = expStack.top(); expStack.pop();
  65.             string newExp = "(" + left + string(1, op.oprator) + right + string(")");
  66.             expStack.push(newExp);
  67.         }
  68.     }
  69.     return expStack.top();
  70. }


  71. int main() {
  72.     ios::sync_with_stdio(false);
  73.     string s;
  74.     cin >> s;
  75.     cout << solve(s) << '\n';
  76.     return 0;
  77. }
复制代码


评分

参与人数 1大米 +1 收起 理由
我是一条大咸鱼 + 1 赞一个

查看全部评分


上一篇:讨论一道最短路径问题
下一篇:推荐刷题做笔记的方法
推荐
usr_opta 2021-1-29 14:29:03 | 只看该作者
全局:
本帖最后由 usr_opta 于 2021-1-29 14:30 编辑
玛玛哈哈 发表于 2021-1-29 13:44
你能给我讲解一下term_size和terms_size具体是做什么的吗

简单起见只考虑+和*, - 和 / 一样。考虑  1*2+3+4*5*6。这里一共有3个term, "1*2", "3", "4*5*6", 分别有2, 1, 3个乘数,所以 terms_size 就是 [2,1,3]。

append_term 负责把一个形如 "((4*5)*6)" 的字符串添加到 ret. 由于我们知道是左结合,所以字符顺序肯定是
1.一串左括号,可能为空;2.一个数字;3a.一个符号;3b.一个数字;3c.一个右括号;3d.回到3a循环。数字和符号可以直接从输入里提取(append_ch).

+/-层级的处理一模一样,把步骤2和3b的输出数字改成输出term就是了。

肯定有更好理解的写法,但是时间复杂度是线性应该不能更低了。

评分

参与人数 1大米 +1 收起 理由
玛玛哈哈 + 1 赞一个

查看全部评分

回复

使用道具 举报

推荐
usr_opta 2021-1-29 11:05:29 | 只看该作者
全局:
因为输入不含括号,所以整个表达式肯定是 "(...(项+项)+项)+项)....+项)" 的形式. 每个项都只包含乘除,所以是纯粹的左结合。知道了总体结构以后可以预先扫描一遍,可以提前知道在每个地方需要加多少个连续的左括号,然后就可以顺序构造输出了。据说用 string.append() 会比用 operator+() 快:

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. using namespace std;

  5. string add_parenthesis(string s) {
  6.     vector<int> terms_size;
  7.     int term_size = 1;
  8.     for (char ch : s) {
  9.         switch (ch) {
  10.             case '*':
  11.             case '/':
  12.                 term_size++;
  13.                 break;
  14.             case '+':
  15.             case '-':
  16.                 terms_size.push_back(term_size);
  17.                 term_size = 1;
  18.                 break;
  19.         }
  20.     }
  21.     terms_size.push_back(term_size);

  22.     string ret;
  23.     int ch_idx = 0;
  24.     int term_idx = 0;
  25.     auto append_ch = [&]{ ret.append({s[ch_idx++]}); };
  26.     auto append_term = [&]{
  27.         int term_size = terms_size[term_idx++];
  28.         if (term_size == 1) {
  29.             append_ch();
  30.         } else {
  31.             ret.append(string(term_size-1, '('));
  32.             append_ch();
  33.             for (int i = 0; i < term_size-1; i++) {
  34.                 append_ch();
  35.                 append_ch();
  36.                 ret.append(")");
  37.             }
  38.         }
  39.     };
  40.    
  41.     if (terms_size.size() == 1) {
  42.         append_term();
  43.     } else {
  44.         ret.append(string(terms_size.size()-1, '('));
  45.         append_term();
  46.         for (int i = 0; i < terms_size.size()-1; i++) {
  47.             append_ch();
  48.             append_term();
  49.             ret.append(")");
  50.         }
  51.     }
  52.     return ret;
  53. }

  54. int main(int argc, char*argv[]) {
  55.     string s;
  56.     cin >> s;
  57.     cout << add_parenthesis(s) << endl;
  58.     return 0;
  59. }
复制代码

评分

参与人数 1大米 +1 收起 理由
玛玛哈哈 + 1 赞一个

查看全部评分

回复

使用道具 举报

🔗
 楼主| 玛玛哈哈 2021-1-29 13:16:44 来自APP | 只看该作者
全局:
usr_opta 发表于 2021-01-28 19:05:29
因为输入不含括号,所以整个表达式肯定是 "(...(项+项)+项)+项)....+项)" 的形式. 每个项都只包含乘除,所以是纯粹的左结合。知道了总体结构以后可以预先扫描一遍,可以提前知道在每个地方需
谢谢你的回答和代码!我消化一下
回复

使用道具 举报

🔗
 楼主| 玛玛哈哈 2021-1-29 13:44:06 来自APP | 只看该作者
全局:
usr_opta 发表于 2021-01-28 19:05:29
因为输入不含括号,所以整个表达式肯定是 "(...(项+项)+项)+项)....+项)" 的形式. 每个项都只包含乘除,所以是纯粹的左结合。知道了总体结构以后可以预先扫描一遍,可以提前知道在每个地方需
你能给我讲解一下term_size和terms_size具体是做什么的吗
回复

使用道具 举报

🔗
 楼主| 玛玛哈哈 2021-1-29 14:21:43 | 只看该作者
全局:
usr_opta 发表于 2021-1-29 11:05
因为输入不含括号,所以整个表达式肯定是 "(...(项+项)+项)+项)....+项)" 的形式. 每个项都只包含乘除,所 ...

你好我理解啦, terms_size是在计算每一项里数字的个数。这个解法可以通过所有的数据, 学习到了, 再次感谢。想问一下层主是怎么想到或者分析的呢, 是否有类似的一类题型或者知识点可以让我加以练习或者是巩固呢?
回复

使用道具 举报

🔗
usr_opta 2021-1-29 15:06:29 | 只看该作者
全局:
玛玛哈哈 发表于 2021-1-29 14:21
你好我理解啦, terms_size是在计算每一项里数字的个数。这个解法可以通过所有的数据, 学习到了, 再次感谢 ...

我觉得其实基本功还是很重要的,比方说如果学过逻辑学或者compiler相关的课程,可以立马看出输入是一个 sum of product 格式的表达式,进而联想到从加号切分。或者如果你尝试手动计算 1+1+1+1+1+...+1, 应该能比较容易观察到这个左结合的结构。

很多这种表达式的题和 compiler/AST/状态机 有密切的关系,可以尝试了解下,不一定能帮你解题,但希望能给你一个不同的角度去理解题解。

评分

参与人数 1大米 +1 收起 理由
玛玛哈哈 + 1 赞一个

查看全部评分

回复

使用道具 举报

🔗
 楼主| 玛玛哈哈 2021-1-29 15:08:18 来自APP | 只看该作者
全局:
usr_opta 发表于 2021-01-28 23:06:29
我觉得其实基本功还是很重要的,比方说如果学过逻辑学或者compiler相关的课程,可以立马看出输入是一个 sum of product 格式的表达式,进而联想到从加号切分。或者如果你尝试手动计算 1+
感谢!有什么推荐的compiler课程吗?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册账号
隐私提醒:
  • ☑ 禁止发布广告,拉群,贴个人联系方式:找人请去🔗同学同事飞友,拉群请去🔗拉群结伴,广告请去🔗跳蚤市场,和 🔗租房广告|找室友
  • ☑ 论坛内容在发帖 30 分钟内可以编辑,过后则不能删帖。为防止被骚扰甚至人肉,不要公开留微信等联系方式,如有需求请以论坛私信方式发送。
  • ☑ 干货版块可免费使用 🔗超级匿名:面经(美国面经、中国面经、数科面经、PM面经),抖包袱(美国、中国)和录取汇报、定位选校版
  • ☑ 查阅全站 🔗各种匿名方法

本版积分规则

>
快速回复 返回顶部 返回列表