前置技能

栈 (stack)

栈是一种限制访问端口的线性表,栈的所有操作都先定在线性表的一端进行。表首被称为“ 栈底 ”,表尾被称为“ 栈顶 ”(这里书上第36页大概说反了)。每次取出的总是最后压进的元素,即“ 后进先出 ”。与之相对的是队列 (queue),在表的一端插入,另一端取出,即“ 先进先出 ”。

中缀表达式 (InfixExp) 与后缀表达式 (PostfixExp)

中缀表达式即我们常用的23 + (34*45) / (5+6+7)这样的表达式(没错就是65页的例子),对于人类的我们来说,经过了小学的各种计算训练,基本到了五年级统考的时候我们都能扫一眼以后就轻松计算出上面的例子,但是对于计算机来说,括号的处理是一个大难题。反正我没有想到什么比书上讲到的后缀表达式更好的解决方法。

此时我们就需要把中缀表达式转变为后缀表达式,后缀表达式又称逆波兰表达式,不含括号,运算符放在两个参与运算的语法成分后面,求值严格从左向右(对计算机很友好)。

顺便吐个槽:此处的infix、postfix都是算术里中缀和后缀的意思。书上概要设计里面的suffix是英文单词的后缀的意思……

中缀:23 + (34*45) / (5+6+7)

后缀:23 34 45 * 5 6 + 7 + / +

需求描述

  • 可以使别加减乘除以及括号的中缀表达式并计算
  • 如果表达式有误,应给出相应的提示

概要设计

书上的类设计图可以债见了。

如果有人按着后面的表达式流程图码代码,我可以保证你能码到怀疑人生。(然而前提是能码出来(?))

下面是头文件calculator.h

    #ifndef CALCULATOR_H
#define CALCULATOR_H
#include<iostream>
#include<string> using namespace std; class C {
public:
//C(); 默认构造
//~C(); 默认析构
double calculate(string InfixExp) ; //计算函数(炒鸡安全2333
private:
string infix_to_postfix(string InfixExp); //中缀转后缀
double cal_postfix(string PostfixExp); //计算后缀
int priority(char op); //返回优先级
double stringToDouble(string num); //字符串转浮点数
double cal(double num1, double num2, char op); //计算两个数
}; #endif // CALCULATOR_H

在这里我们可以清晰地看明白这个计算器类的运行方法:在我们读入了一段中缀表达式以后,直接调用calculate(string InfixExp)函数,在calculate函数内先调用infix_to_postfix(string InfixExp)函数,将中缀表达式转为后缀表达式,然后调用cal_postfix(string PostfixExp)函数,计算后缀表达式。

函数详细设计

这里直接简单摘取书上66、67页的内容对infix_to_postfix(string InfixExp)函数和cal_postfix(string PostfixExp)函数进行说明(然而书上对压栈等操作的说明写的十分笼统,且对栈的类型定义等一律没有……)

中缀转后缀 infix_to_postfix(string InfixExp)

  1. 定义后缀表达式 string PostfixExp = " ";,定义存储操作符 stack<char> calculate;
  2. 当读入的是数字时,直接输出到后缀表达式 PostfixExp += InfixExp[i];

    当读入的不是数字时,需要插入空格将数字隔开 PostfixExp += " ";
  3. 当读入的是左括号时,直接将其压栈 calculate.push(InfixExp[i]);
  4. 当读入的是右括号时,在栈非空的情况下弹出左括号前所有操作符 PostfixExp += calculate.top(); calculate.pop();。此时如果括号不匹配(缺少左括号),则栈会被清空,检测栈是否为空 if(calculate.empty()),为空则报错 “ 括号不匹配 ” ,返回 “ error ” 。括号匹配则在之后弹出左括号。
  5. 当读入的是运算符 + - * / 时,当栈非空且栈顶非左括号且当前运算符优先级不高于栈顶运算符优先级时,反复将栈顶元素弹出至后缀表达式。之后将输入的运算符压栈 <书上原话 > while (!calculate.empty() && calculate.top() != '(' && priority(InfixExp[i]) <= priority(calculate.top())) 通俗地讲就是* / 先存进后缀表达式,再存入 + -
  6. 读到了非法字符,报错返回 “ error ” 。
  7. while (!calculate.empty())清栈,如果碰到左括号,说明括号不匹配(缺少右括号),报错返回 “ error ” 。

计算后缀 cal_postfix(string PostfixExp)

  1. 处理中缀转后缀的报错 if (PostfixExp == "error") return 0; 。定义字符串用来转成浮点型 string str; 定义存储数字 stack<double> calculate;

    ~ ) 我在这个函数中读取数字并转成浮点型存储,助教小哥哥的意思好像是读取的时候就分开存好(?)不过没差啦……我这个方法除了读取的部分十分诡异以外,更大限度地保留了书上的原汁原味(大雾)至于哪里诡异呢……大概就是为了防止把123读出1,12,123三个数字,我在循环嵌套的内循环里顺便把外循环的计数君也自增了,那么外循环会多1,所以后面又要减掉1……嗯,常规操作常规操作……
  2. 读取数字并压栈 calculate.push(stringToDouble(str));
  3. 遇到运算符则弹出两个数字并计算,将结果压栈 calculate.push(cal(num1, num2, PostfixExp[i]));

其他函数

  1. 计算两个数 cal(double num1, double num2, char op)

    这个函数没什么好说的,num1 和 num2 的顺序一开始有点懵但实际上跑一下看看就好了。唯一要注意的是除法要判断被除数是否为 0 ,为 0 则报错(返回什么随意) 。

  2. 优先级 priority(char op)

    这个优先级的大小就自己看着办吧,反正可以测试一下 3 + 6 / 2 这样的东西。正确与否取决于中缀转后缀的第四步的 while 语句写法和这里的大小赋值。原理上只要赋值 + - * / 但我连括号都赋上了 2333 完全不理解当时在想什么啊!

  3. 字符串转浮点型 stringToDouble(string num)

    这个代码我在网上copy的,很显然我也忘了是copy哪里的所以就不放出处了。这个代码还能转小数点……我就顺手加了两个判断,把几个int型改成double型,就可以计算浮点数了哦好神奇。

具体实现

我觉得直接copy不能提高个人coding能力,所以我觉得如果你真的想学好coding,还是应该对照我上面十分详尽的讲解看完下面的代码,然后自己写一个,或者至少尝试自己写,不会了再跟大神们讨论讨论,刷一下自己的魅力值,对吧。

calculator.cpp

    #include"calculator.h"
#include<iostream>
#include<string>
#include<stack> //计算函数
double C::calculate(string InfixExp) {
string PostfixExp = infix_to_postfix(InfixExp); //调用中缀转后缀
return cal_postfix(PostfixExp); //返回后缀计算值
}
//中缀转后缀
string C::infix_to_postfix(string InfixExp) {
string PostfixExp = "";
stack<char> calculate;
for (int i = 0; i < InfixExp.length(); i++) {
if ((InfixExp[i] >= '0' && InfixExp[i] <= '9')|| InfixExp[i] == '.') {
PostfixExp += InfixExp[i]; //数字直接压栈
}
else {
PostfixExp += " "; //操作符
if (InfixExp[i] == '(') { //左括号压栈
calculate.push(InfixExp[i]);
}
else if (InfixExp[i] == ')') { //右括号
while (!calculate.empty() && calculate.top() != '(') {
PostfixExp += calculate.top(); //将所有操作符弹出
calculate.pop();
}
if (calculate.empty()) {
std::cout << "括号不匹配" << std::endl;
return "error"; //确认括号匹配
}
calculate.pop(); //删除栈内左括号
}
else if (InfixExp[i] == '+' || InfixExp[i] == '-' ||
InfixExp[i] == '*' || InfixExp[i] == '/') {
while (!calculate.empty() && calculate.top() != '('
&& priority(InfixExp[i]) <= priority(calculate.top())) {
PostfixExp += calculate.top(); //比较优先级
calculate.pop();
}
calculate.push(InfixExp[i]);
}
else {
std::cout << "非法字符" << std::endl;
return "error";
}
}
}
while (!calculate.empty()){ //清栈
if(calculate.top() == '('){
std::cout << "括号不匹配" << std::endl;
return "error";
}
PostfixExp += calculate.top();
calculate.pop();
}
return PostfixExp;
}
//计算后缀
double C::cal_postfix(string PostfixExp) {
if (PostfixExp == "error")
return 0;
double num1,num2;
string str;
stack<double> calculate;
for (int i = 0; i < PostfixExp.length(); i++) { //遇到数字,读取并入栈
if (PostfixExp[i] == ' ') continue; //无视空格
else if ((PostfixExp[i] >= '0'&&PostfixExp[i] <= '9')
||PostfixExp[i]=='.') {
str = "";
for (int j = i;(PostfixExp[j] >= '0'&&PostfixExp[j] <= '9')
|| PostfixExp[i] == '.'; i++, j++) {
str += PostfixExp[j];
}
i--;
calculate.push(stringToDouble(str));
}
else { //遇到操作符,提取并计算
num1 = calculate.top();
calculate.pop();
num2 = calculate.top();
calculate.pop();
calculate.push(cal(num1, num2, PostfixExp[i]));
}
}
return calculate.top();
}
//计算两个数
double C::cal(double num1, double num2, char op) {
if (op == '+')
return (num2 + num1);
else if (op == '-')
return (num2 - num1);
else if (op == '*')
return (num2 * num1);
else if (op == '/') {
if(num1 == 0){
std::cout<<"除数不能为0"<<std::endl;
return 0;
}
return (num2 / num1);
}
}
//返回优先值
int C::priority(char op) {
switch (op) {
case '(': //留着这群可笑的赋值,我当时大概是脑子不清醒哈哈哈
case ')': return 1;
case '+':
case '-': return 2;
case '*':
case '/': return 3;
default:return 0;
}
}
//字符串转浮点数
double C::stringToDouble(string num)
{
bool isDec = false; //标记是否有小数
string real = num; //real表示num的绝对值
char c;
int i = 0;
double result = 0.0, dec = 10.0;
unsigned long size = real.size();
while (i < size)
{
c = real.at(i);
if (c == '.')
{ //包含小数
isDec = true;
i++;
continue;
}
if (!isDec)
{
result = result * 10 + c - '0';
}
else
{ //识别小数点
result = result + (c - '0') / dec;
dec *= 10;
}
i++;
}
return result;
}

main.cpp

    #include"calculator.h"
#include<iostream>
#include<string> int main() {
string temp;
cout << "请输入表达式:" << endl;
cin >> temp;
C cal;
cout << "计算结果为:" << cal.calculate(temp) << endl;
return 0;
}

封装好的类通常可以让调用很简单……但码这个代码让我明白,像我这样的凡人想封装好这么一个类还是很难的。

谨以此文祭奠我逝去的头发。

[C++] 例题 2.7.1 用栈实现简易计算器的更多相关文章

  1. python 利用栈实现复杂计算器

    #第五周的作业--多功能计算器#1.实现加减乘除及括号的优先级的解析,不能使用eval功能,print(eval(equation))#2.解析复杂的计算,与真实的计算器结果一致#用户输入 1 - 2 ...

  2. 深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器

    在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #de ...

  3. 数据结构应用实例#栈&单链表#简易计算器

    修改BUG的时候一不小心BUG越修越多,鉴于维护程序并不是学习数据结构的初衷,我已经果断的弃坑了!! 以下内容再不更新,Github上的代码直接无法正常编译运行.... 参考参考就好,学习到栈的作用就 ...

  4. C#数据结构与算法系列(九):栈实现综合计算器(中缀表达式)

    1.问题介绍 2.实现思路 3.代码实现 第一个版本(采用这个) public class ArrayStack { private int _maxSize; private int[] _arr; ...

  5. leetcode c++做题思路和题解(4)——队列的例题和总结

    队列的例题和总结 0. 目录 栈实现队列 队列实现栈 滑动窗口最大值 1. 栈实现队列 FIFO和FILO,相当于+-号,互转都是利用"负负得正"的原理. 官方解答中第二种思路很6 ...

  6. Python全栈开发之目录

    基础篇 Python全栈开发之1.输入输出与流程控制 Python全栈开发之2.运算符与基本数据结构 Python全栈开发之3.数据类型set补充.深浅拷贝与函数 Python全栈开发之4.内置函数. ...

  7. Python全栈

    Python基础 Python基础01 Hello World! Python基础02 基本数据类型 Python基础03 序列 Python基础04 运算 Python基础05 缩进和选择 Pyth ...

  8. 手写实现java栈结构,并实现简易的计算器(基于后缀算法)

    一.定义 栈是一种线性表结构,栈结构中有两端,对栈的操作都是对栈的一端进行操作的,那么被操作的一端称为栈顶,另一端则为栈底.对栈的操作其实就是只有两种,分别是入栈(也称为压栈)和出栈(也称为弹栈).入 ...

  9. OI中组合数学公式和定理90%歼灭

    组合数学 基础概念 加法和乘法原理 加法原理 同一步下的不同选择,可以通过累加得到方案数. 乘法原理 整个流程的方案数可以由每一步的方案数相乘得到. 有了加法原理和乘法原理,就可以解决一些没有选择导致 ...

随机推荐

  1. [WEB安全]PHP伪协议总结

    0x01 简介 首先来看一下有哪些文件包含函数: include.require.include_once.require_once.highlight_file show_source .readf ...

  2. SpringMVC+Spring+Mybatis简单总结

    SpringMVC+Spring+Mybatis总结 第一部分:分析 web.xml中的配置 SSM框架的整合其实是Spring和SpringMVC的整合以及Spring和Mybatis进行整合. 当 ...

  3. ICEM-闪闪的党徽

    ​原视频下载地址:http://yunpan.cn/cusb64DXrammF  访问密码 3d0f

  4. 美团Android自动化之旅—适配渠道包

    http://tech.meituan.com/mt-apk-adaptation.html 概述 前一篇文章(美团Android自动化之旅-生成渠道包)介绍了Android中几种生成渠道包的方式,基 ...

  5. JVM 自定义类加载器

    一.创建自定义类加载器 package com.example.jvm.classloader; import java.io.ByteArrayOutputStream; import java.i ...

  6. RecyclerView 实现快速滚动 (转)

    RecyclerView 实现快速滚动 极小光  简书作者   简评:Android Support Library 26 中终于实现了一个等待已久的功能:RecyclerView 的快速滚动. An ...

  7. 网络流中 InputStream.available() = 0 问题探究

    在处理文件输入流时,通过调用available()方法来获取还有多少字节可以读取,根据该数值创建固定大小的byte数组,从而读取输入流的信息. FileInputStream fi = new Fil ...

  8. Flutter -------- 网络请求之HttpClient

    今天来说说Flutter中的网络请求,HttpClient网络请求,包含get,post get var data; _get() async { Map newTitle; var response ...

  9. Navicat Premium 12 mysql show error: connection is being used

    错误原因:连接数满了. 解决方案:杀掉无用连接,释放资源.

  10. ISO/IEC 9899:2011 条款6.7.9——初始化

    6.7.9 初始化 语法 1.initializer: assignment-expression {    initializer-list    } {    initializer-list   ...