「大数」六则运算计算器的简单实现

写在前面

花了近三天时间把这个简单的小计算器给写好了,可以实现大型数据上百位的加减乘除乘方求余六则运算,同时也支持浮点数

其实内部实现并不复杂,为什么想写这么个小程序 大概是因为我现在才刚大一,只接触了半年多的编程,哪怕是学校的C语言测验也都是考一些基本的算法,都是基于过程的,并不涉及到一个完整的"项目开发" (其实我感觉这个简单的小程序都不能被冠以"项目开发"的名号)。

我想看看目前掌握的这些浅显的C语言知识,再加上寒假刚开始三天看的C++网课,能不能写出来什么有用的东西

写完之后很满足,哪怕源代码看起来显得很臃肿。由于我的C++所有的知识只局限于看了三天的网课,学了class的一点皮毛,所以源代码很多很多还是用C语言写出来的,但是用了C++的类,感觉有点四不像hhh

这一篇博客也算是对这三天的一个总结吧,人生中第一个七百多行的程序还是想纪念一下的。

源代码比较长,所以单独放在附件里面了。我打算按照从第一天开始 写程序的过程中冒出的一个个问题 然后我是怎么解决的,这个顺序来记录,也就是“遇到问题 -> 解决问题 -> 又遇到问题 -> 再次解决" (类似于写日记) 以便将来我回顾我的思路 也方便阅读。

源代码都在附件里 所以这篇文章不会放很多源代码 大多都是记录解决问题的过程还有思路 我也会尝试给每一个实现方法配个图 以便回顾 资源地址是:https://download.csdn.net/download/X_P_X_P/77746690

本人才疏学浅学艺不精,如果有路过的大佬发现了什么问题请直接指出,万分感谢!

文章目录

  • 写在前面
  • 需求目标
      • 总体目标
      • 细分目标
      • 关于细分目标的补充
  • 编程思路
    • Day 1 大型整数的加减乘
      • 如何读取输入的整数算式
      • 如何内部存取以及创建单个整数变量
        • 单独存放数字位数的必要性
      • 整数加法的实现
        • Step1.数字颠倒
        • Step2.各位相加
        • Step3.处理进位
        • Step4.获取`digit`
          • 先获取`digit`再颠倒`res`的必要性
        • Step5.颠倒`res`
      • 整数减法的实现
        • Updated Step3.5 .处理负数位
        • 加减法完整流程图
      • 整数乘法的实现
      • 整数除法的实现
      • 整数四则运算完整流程图
    • Day2 浮点数的六则运算
      • 如何内部存取以及创建单个浮点数变量
        • ★ 取
        • ★ 创建变量
      • ★★ 如何读取输入的浮点数算式
        • 目标
        • ★★ 实现思路
      • 浮点数乘除法的实现
      • 浮点数乘方的实现
      • 浮点数加减法的实现
      • 浮点数求余的实现
      • 浮点数六则运算流程图
    • Day3 实现包括括号的运算优先级
      • ★ 括号配对
      • ★★★ 实现带括号的优先级计算
  • 写在后面 给自己

需求目标

总体目标

输入一串数学计算式 返回运算结果

​ e.g.输入(233.333+233)^2-0.066= 返回 217466.400889

细分目标

  1. 实现浮点数的运算 而且精确到小数点后很多很多位
  2. 实现六则运算 : 加+ 、 减- 、 乘* 、 除/ 、 乘方 ^ 、求余 %
  3. 实现优先级运算 也就是能看懂括号

关于细分目标的补充

  1. 看懂括号实现优先级运算的同时,顺带着把括号标的对不对给输出

    e.g. {[]}就是正确的括号配对顺序 {[(])}就是错误的括号配对顺序 ([])虽然配对没问题 但是格式不对 这个时候依然正常运算 但是要告诉用户括号标错了

  2. 输出时小数点后最后一个数不进行补0 也就是233.3333不会输出为233.33330000

  3. 对于除法运算,规定分母为0的时候结果为0

  4. 对于乘方运算,规定幂指数自动取它的整数部分 (带小数的乘方属实不知道咋办) 但是负指数幂是可以的 10^-2=0.01

  5. 小数之间的求余运算,例如A%B规定结果是A-int(A/B)*B

编程思路

为了还原写完整个程序过程中的思路,我不打算循规蹈矩的直接讲述源代码每个部分的作用。我打算按照我这三天从一开始到最后,遇到的一个个问题,就像写日记一样从零开始还原编写过程。

Day 1 大型整数的加减乘

对没错 第一天我还没想着把浮点数这个东西给纳入考虑范围…

至于为什么第一天没有实现除法 可以看[整数除法的实现](# 整数除法的实现)

如何读取输入的整数算式

由于Day1的xp还只是想着弄整数的运算 所以输入的式子只考虑到了整数 但是浮点数算式的读取是整数算式读取2.0版本 所以在下面Day2的时候再详细讲述如何读取输入的算式

如何内部存取以及创建单个整数变量

首先我们的这个计算器实现的是大数的运算,也就是哪怕long long还是double之类的原有的数据类型都没法存储的数据,所以我选择的存储方法就是**int*数组**,数组中的每一位存储数字中的一位数.例如:2333存储为 : {[2],[3],[3],[3]}

那么正负号呢 ? 我选择的方法是 变量sign来存储正负号 正号为1 负号为-1

再放一个东西来存储数字的位数吧 变量digit来存储数字的位数

一定需要存放位数吗 ? 一定需要。 下面会解释原因。

于是乎 我们的存放数据的东西就整好了:

class large_num{
public: 
	...
private:
    int* num;int sign;int digit;
}

针对符号<<的重载 就把num数组的前digit位给一个个cout<<就是整数的输出了 如果是负数那么前面加个符号就好

单独存放数字位数的必要性

为了后续运算的方便 用数组存储每一位的数字时,数组的大小并不是正好到该数字的位数,而是直接把上限撑满!(上限我设置的100,源代码中的define n 100就是上限 结合需求修改即可)

也就是说 2333存进去时,并非[2,3,3,3]就完事了,而是**[2,3,3,3,0,0,0,0,...,0]后面全是零**

看似这种做法很占用空间 但这个在后续的乘除运算中将非常方便 (不过我也确实对这个做法产生过怀疑 如果有大佬有更好的做法请务必告诉我 万分感谢!)

好的 话归正题 这种操作就导致一个问题:

233323330这两个不同的数 提取成数组形式都是[2,3,3,3,0,0,0,0,...,0] 所以必须使用digit存放位数加以区分

下面就该开始进行运算的实现了。

整数加法的实现

例子:2990+38

注意到 我们存储的形式是数组 所以这个加法实际上相当于

	[2,9,9,0,0,...0]
+	[3,8,0,0,0,...0]

这个是什么 这个是我们小学就学过的列竖式啊 !

但是 小学的竖式应该是这样子的:

	2 9 9 0
+	    3 8
=   2 8 2 8

他是右对齐的 ! 个位对应个位 十位对应十位

而对于数组来说 存储当然都是顶着左边开始存储 也就是左对齐

这个时候我们就需要用数组的左对齐来模仿右对齐 也就是将数字颠倒!

「大数」六则运算计算器的简单实现_第1张图片

Step1.数字颠倒

「大数」六则运算计算器的简单实现_第2张图片

颠倒之后 就和我们竖式一样 个位对应个位 十位对应十位了

接下来就是各位相加了

Step2.各位相加

「大数」六则运算计算器的简单实现_第3张图片

Step3.处理进位

很显然 我们经过上一步得到的res数组是这样的: [8,12,9,2,0,0...0] 这显然需要进行进位

我的进位方法是:

从头到尾遍历 对于第i位,我们进行如下两个操作:

  1. i+1位 加上 第i位除以10的结果
  2. i位对10取余

也就是:

for (int i=0;i<n;i++){
    res[i+1]+=res[i]/10;
    res[i]%=10;
}

「大数」六则运算计算器的简单实现_第4张图片

Step4.获取digit

此时的res还是颠倒的 但是千万千万不能急着颠倒res 一定一定要**先获取digit**再颠倒! 原因见这里: [先获取digit再颠倒res的必要性](# 先获取digit再颠倒res的必要性)

如何获取digit? 我的方法就是把digit先放在最后一位 然后不断往前跑 一直到碰到第一个不是0的数字为止

int digit=n;	//n为数组容量上限
while(res[digit-1]==0)digit--;  	//如果digit对应的那个是0那么digit就往左边挪一格
先获取digit再颠倒res的必要性

如果先颠倒res的话 为了知道哪两个数相互交换 你将面临两种情况:

  1. 直接取原来的digit作为resdigit 但是如果三位数加三位数结果为四位数 也就是digit变化的情况 这种方法就出大问题
  2. 如果碰巧digit没有产生变化 你成功颠倒了res 然后你想再把digit给找到 继续使用从后往前挪的这个方法 你将会没法识别诸如2330这种数字的末位0

所以必须先取得结果的digit再去颠倒res数组

Step5.颠倒res

别忘了一开始我们把两个数字都倒过来了 直接相加之后最后的结果也是倒过来的 所以最后一个Step就是把结果正过来即可
「大数」六则运算计算器的简单实现_第5张图片

好的 我们成功实现了整数的加法!

整数减法的实现

对于减法式 : 233-66来说 其实相当于 233 + (-66) 所以当成加法式来看待就好

如果是正常的66,我们存储的是这样: [6,6,0,...,0] 而对于-66 ,在运算中我们可以把它存储成这样: [-6,-6,0,...,0] 也就是每一位取负数 然后进行上面实现加法的过程中所使用的两数组各位相加就好

但是!我们在进行的过程中会遇到一个很严重的问题。让我们先按部就班的一步步来。

先进行Step1.两数组数字颠倒 然后是Step2.各位相加
「大数」六则运算计算器的简单实现_第6张图片

在进行Step3.处理进位的时候 我们拿个位的-3来举例

-3在C++里面对10求余 结果并不是7 而是-3 !!

而且-3除以10的结果是0 后面一位实际上大小没变 根本达不到我们所要的进位 (在减法竖式中叫做借位) 的效果

想到这里 我们意识到原来的Step3需要一个改进

Updated Step3.5 .处理负数位

对于每一个格子里面的数字 我们的目标是:

遇到23 留下3 后一位+2

遇到-2 留下8 后一位-1

遇到-10 留下0 后一位-1

遇到-12 留下8 后一位-2

我们随便拿来一个数组:

[13,-26,19,7,0,-10,123,0,0,...,0]

进行完原先Step3的操作后 原来的数组变成了这样:

[3,-5,7,8,0,0,2,2,1,0,0,...,0]

我们发现 如果存在负数 那么进行完Step3之后也都会变成-9到-1之间了

那我们只需要再进行一次遍历 只要小于0我们就加10 并且下一位-1就好了

for (int i=0;i<n;i++) 
    if(res[i]<0){res[i]+=10;res[i+1]--;}

但这真的结束了吗?

让我们想象一下 如果一个全是0的数组 只有第一位是-1 那会发生什么

判断-1<0 ; -1+10=9 ; 后一位0-1=-1 ; 判断下一位

判断-1<0 ; -1+10=9 ; 后一位0-1=-1 ; 判断下一位

判断-1<0 ; -1+10=9 ; 后一位0-1=-1 ; 判断下一位…

然后呢 原先的 [-1,0,0,...,0] 就变成了 [9,9,9,9,...,9]

这很显然是个极其错误的答案

所以在进行这一个处理负数位的step之前 我们还要先干一件事情:

找到最高位 判断是否小于0 如果小于0的话就先把各位取负数 然后再进行操作 !

那提出来的负号放在哪里呢? 我的做法是把它放在数组的最末尾 也就是res[n-1]的地方

于是乎 一个 -1 我们就可以存储为:

[1,0,0,...,0,-1] 其中最后一位-1是用来标识正负号的 如果是-1那么这个数就是个负数

加减法完整流程图

综上 我们可以得出如下的加减法流程图

「大数」六则运算计算器的简单实现_第7张图片

整数乘法的实现

乘法其实可以看做加法v2.0

我们小学列乘法竖式A*B的时候也是取B的一位跟A相乘 以此类推 全部乘完之后再一起相加

数组乘法也是这样 逐位相乘之后再相加 然后整理好即可

只是要注意不同位数的对齐

例如648*89

列竖式我们右对齐 那么写程序的时候由于数组的左对齐性质 我们不可避免的第一步是颠倒两个数组 所以:

Step1.两数颠倒

颠倒后得到[8,4,6,0,..,0][9,8,0,...,0]

下面就是一位一位相乘了。直接上gif:

「大数」六则运算计算器的简单实现_第8张图片

然后就是跟加法Step3一样的处理进位

最后进行颠倒res就行

也就是说 对于减法和乘法 这两种运算的根本还是在加法

BUT 整数的除法 就完全完全不一样了…

整数除法的实现

我们小学怎么算除法的?

是不是从左往右算?

所以 除法运算 应该是左对齐的 而不应该右对齐 也就是说我们并不需要进行两个数的颠倒

那为什么Day1我没能实现除法呢 ? 原因往后看就知道了

我实现除法的方法 和正常的列除法竖式没有什么两样 对于A/B 将B一位一位往后挪 然后让B和1~9的各数相乘

如果 B*i 而且 B*(i+1)>A 那么这一位的结果就是i 存储i作为答案 然后将B往后挪就好

如果A说明B还得往后挪 如果A>B那么就如上述的那样找i就好

gif如下:

「大数」六则运算计算器的简单实现_第9张图片

这个时候你就会发现一件事情

这个循环什么时候结束呢???

如果一直不结束其实就是计算小数部分 但是我这边进行的是整数的运算啊

两个解决方案:

  1. 还是按照整数来计算 在B怼到A的个位之后停下 然后输出答案
  2. 照样输出小数部分 对结果取整数部分 作为答案

很显然 方案二更加普适化 因为能得到小数部分

但我们这里只有整数啊

这个时候xp就打算把这个计算器扩展成能计算浮点数的计算器了…

而且他也这么干了 这之后的故事就要看Day2了

整数四则运算完整流程图

「大数」六则运算计算器的简单实现_第10张图片

Day1到此结束 下面就是第二天xp干了什么了

Day2 浮点数的六则运算

如何内部存取以及创建单个浮点数变量

要计算浮点数 跟计算整数第一个不同的地方就在于数据存储

我采取的方法跟计算机存取浮点数的IEEE 754标准相仿

将每个数表示为科学计数法 例如31415.926=3.1415926* 10^4

num数组还是存储数字 而power变量存储10的幂指数

对于原先用来存取位数的digit变量 我也不想丢弃 我想到的做法是:

​ 如果数据是整数 那么digit存取整数位数

​ 如果数据是真正的浮点数(小数点后面有非0数) 那么digit存取有效数字位数

这样可以在一些问题上简化算法 尤其涉及到1000这种后面带0的整数的一些运算

于是乎 这些数字我们就存成了如下形式:

1000 : num=[1] digit=4 power=3

0.0233:num=[2,3,3] digit=3 power=-2

3.1415:num=[3,1,4,1,5] digit=5 power=0

★ 取

依旧是对<<的重载 跟输出整数很相仿 但是最重要的一点就是小数点的输出

下面看三个实例:

num=[1] digit=4 power=3 实际数字1000,连续输出4个整数后 由于没有小数 不输出小数点

num=[2,3,3] digit=3 power=-2 实际数字0.0233 输出一个0后输出小数点 小数点后输出1个0然后将有效数字输出

num=[3,1,4,1,5] digit=5 power=0 实际数字3.1415 输出1个有效数字后输出小数点 然后把剩下(5-1)=4个有效数字输出

归纳总结后得出我们要输出浮点数的话进行的操作:

如果power<0那么数字就是在0~1之间 先输出一个0. 接着不断输出0并且power++ 如果power==0说明有效数字前的0已经输出完毕(直接输出-power个0也完全可以) 接下来按照digit中记录的有效数字位数挨个输出有效数字就好
如果power>=0 那么数字>=1, 我们每输出一位就让power-- 如果power==0就立刻输出一个小数点

整理下来就是这样:

ostream& operator << (ostream& os, const large_num& x) {
	int temppower = x.power;
	if (x.power < 0) {		//0.0xx 等待-power个0之后才开始第一位有效数字
		cout << "0."; temppower++;
		for (int i = 0; i < -temppower; i++) cout << "0";
		for (int i = 0; i < x.digit; i++)cout << x.num[i];
	}
	else {					//xx.xx 第一位即为有效数字
		for (int i = 0; i < x.digit; i++) {
			cout << x.num[i];
			temppower--;
			if (temppower == 0)cout << ".";
		}
	}
	return os;
}

这段程序显然还能继续简化 但这里就记录一下初始思路

★ 创建变量

首先带指针的class都要有的基本构造函数(ctor)拷贝构造函数(copy ctor)复制构造函数(copy op=)析构函数(dtor)四个该有的一个都不能少 但是在这里xp新加了一种构造方法:输入字符串形式的浮点数 将其转换为我们的large_num形式 也就是传进去一个"23.333" 构造一个large_num变量 num数组是[2,3,3,3,3,0,...,0] 幂指数power是1,有效数字digit是5

large_num(const char* str);

如何实现呢?

sign : 首先如果第一个符号是-号 那肯定就是负数了 所以sign=-1 如果是+或者啥都没有那显然就是正数,sign=1

numdigit : 这个也很简单 把字符串总头到尾遍历 碰到第一个且是1~9之间的数 那就开始把后面的所有数放进num数组就好 为什么碰到0没用?因为有效数字就是从第一个非0数开始的呀

最难的是如何找到幂指数power 。.
其实有一种方法 是记录从小数点开始到最后的数字个数 也就是记录小数位数 这个变量跟digit可以直接算出power的值 但这个关系在之后浮点数的加减法那里会提到 这里就先不拿出来说了

所以讲一下直接得到power的方法
如果第一个数字是1~9 说明有效数字从第一位就开始了 那么就不会是0.0xx这种情况 在这时候 小数点前面有多少数字 power+1就是多少
2.33 小数点前面一个数字 , power就是1-1=0 ; 423.1小数点前面3个数字 , power就是3-1=2
如果第一个数字是0 那么肯定就是0.00xx这种情况了 也就是power<0 这时候小数点后面有多少个0 -power+1就是多少
0.0233小数点后1个0 power就是-1-1= -2 ; 0.00000066小数点后6个0 power就是-6-1=-7

这一套寻找power的流程有个很大的好处 如果碰见233.33.333这种多个小数点的数据 将不会关后面的小数点 提高了输入的容错率

至于为什么要有这么个构造函数 原因等你看了下面的如何读取浮点数算式就明白了 这么做可以大大节省读取式子时候的难度

★★ 如何读取输入的浮点数算式

好了 知道我们内部怎么存取以及创建单个的浮点数变量了 接下来很重要的问题就是如何读取用户输入的一长串浮点数计算式并且转换成我们内部存储的一个个可供运算的变量了

目标

输入23412.5*(-324.1)/(-0.023)=
能够存取每个数字以及每个数字之间的运算

也就是number数组里存取 : [23412.5 , -324.1 , -0.023]
cal运算符数组存取 : [* , /]

进阶目标:提高输入的容错能力
如果输入了$-#^#233*-* *(6 6/6)=这种里面掺杂了很多无关符号的式子 能够提取出正确的内容
也就是-233 * -66 / 6

★★ 实现思路

可能下面的文字听着会比较绕 没事我还是整了形象直观的gif

Step1
创建一个ifstartnumber变量 他的目的是检测是不是开始要读入数字了 为啥要创建?因为+-号不但是运算符 而且还可能是表示正负号的 并不一定是运算关系

所以当你还没开始真正读入数字时 +-号应当默认为代表这个数字是正是负的一个标志 而不是运算关系
毕竟+233++233=这个算式看上去很画蛇添足 但是本质上是没有错误的

那也许你会问 如果是式子233 + -233= 第二个233前面的负号表示自身是负数 这没有问题
但是式子233 - 233=这里面第二个233前面的负号却是一个实打实的运算符啊 这怎么加以区分呢?

这个的解决方法是 : 跟着后面步骤走 根本不用加以区分。且往下看

Step2
接下来就是输入算式中各个有效的浮点数字符段的截取了
判断方法:
如果ifstartnumber==0 说明此时还没开始读入数字 对于除了+-号以外的任何符号都不用理会
要是遇上了0~9那显然是肯定要开始读取数字了 把ifstartnumber变成1然后开始读取数字就好
但如果 在ifstartnumber==0的时候遇上了+-号 那么你就知道接下来这个数字的正负号了 也就是告知了你sign 此时ifstartnumber变为1 意味着接下来的所有数字部分都是你需要的

如果ifstartnumber==1 说明此时往后所有的数字0~9还有小数点 (勿忘! ) 都是你需要的 那么遇到的这些东西都要塞进一个临时字符串数组里面tempstr 这个字符串数组就是上文讨论large_num的构造函数提到的large_num (const char* str)
这个时候最重要的东西来了
如果ifstartnumber==1并且你碰到了个运算符(+-*/^%)那么意味着这个数字输入结束了 你碰到的这个符号存入运算符数组cal中 你将tempstr中的字符串传入large_num(const char* str)中 那么一个浮点数变量就被你存储完了 接下来全部初始化开始重复上面的过程就好

什么时候跳出?碰见=就说明你式子输入完毕了 接下来运算就好

这整套流程的一大好处就是规避了里面的无用符号 无用符号将不会被读取进来也不会传递给构造函数ctor

如果无用符号是小数点呢???也就是输入了233.33.333这种贵物玩意儿呢?我们的构造函数考虑到了这种情况已经默认读取第一个小数点了第二个小数点自动忽略 上面的构造单个浮点数变量里面有提到这个问题

喜闻乐见的gif环节:

-54 / ( 0.23 * -34) =举例

哪怕把原式给魔改成:

-5(4)$/# (0.]2 3 * !-3 \4 )=

也依旧不影响它的存储

于是乎 这么个式子被我们存为了这样的形式:

「大数」六则运算计算器的简单实现_第11张图片
这种存储在Day3的时候将会大显身手

浮点数乘除法的实现

其实浮点数乘除法跟整数的乘除法没啥区别 唯一多出来的运算就是power幂指数的加减

所以这边直接略过

浮点数乘方的实现

众所周知 乘方就是不断的乘法 所以乘方也是写几行代码就出来了

large_num res("1");
for(int i=0;i<B;i++){
    res*=A;
}
return res;

浮点数加减法的实现

整数那里有提及 减法其实就是加法 , A+B=A+(-B) 所以接下来我们专注于加法

对于浮点数的加法 的确跟整数很像 但是他多了个很棘手的东西:小数点

小学我们学整数的加法竖式 要求右边对齐 其实对齐的并不是个位 实际上对齐的是小数点 !

所以在进行完常规的Step1两数颠倒之后 我们就必须进行两个数字的小数点对齐

对齐小数点也就是小数点后面小数的位数要相同 但是我们没有存取小数位数啊 !

所以要先进行小数位数的计算

这个时候我们存的有效数字位数digit就要派上大用场了

对于不同数字:

25000:digit=5 power=4 小数位数=0

23.333: digit=5 power=1 小数位数=3

0.066:digit=2 power=-2 小数位数=3

我们可以得出一个非常重要的结论:

小数位数point=有效数字位数digit-幂指数power+1

这也是为什么我们不需要存储小数位数的原因 因为他们三个变量之间有这么个恒等关系

好的 得到小数位数之后我们就可以进行小数点对齐了

四位小数+三位小数 那么三位小数后面要添个0

颠倒之后 也就是要在三位小数数组的前面添上1个0 (为啥在前面添?别忘了我们第一步把两数组倒过来了哦)

然后我们就可以进行整数加法运算辣

「大数」六则运算计算器的简单实现_第12张图片

浮点数求余的实现

当你有了除法之后 求余就变得很简单了

先算除法 得到结果后取整数 然后进行A-B*int即可

于是乎 浮点数的六则运算我们全部完成了

浮点数六则运算流程图

「大数」六则运算计算器的简单实现_第13张图片

好的 看来Day2的工作也完成了

但这个时候程序还有个很严重的问题 或者说有个很大的遗憾:

还记得我们前面对于式子-54 / ( 0.23 * -34) =的存储吗

我们把它存成了一个数列 专门用来放每个数 NUMBERS=[ -54 , 0.23 , -34 ]
还有个数列 专门用来放两两之间的运算符号 CAL=[ / , * ]
这其实就相当于用来储存

但在Day2 xp选择的得出答案的方法是 从头到尾按着顺序一个个算 一直算到栈顶

但这就会导致我们这个程序还有个很重要的东西没能实现 —— 运算优先级

这个我们在Day3的时候实现 因为这个时候已经凌晨三点了 而且xp早上八点钟还得起床去打球

Day3 实现包括括号的运算优先级

说是凌晨三点多了 而且早上八点还要起床去打球 但上了床的xp还是不断的在想着怎么实现括号优先级 逐渐有了思路之后越想越兴奋越想越睡不着 一直瞪着眼睛想到了凌晨五点 早上打球也不出意外的迟到了
不过那段睡不着的时光还是让xp成功想出来了解法

★ 括号配对

还是拿-54 / ( 0.23 * -34) = 这个式子来举例 首先的首先 我们要注意一点 :要实现带括号运算的优先级 不储存括号怎么行呢…
所以在读入输入的算式的时候 我们就要加一个bracket数组来储存括号

既然存了括号 那不如顺手判断一下括号的格式正确与否

对于这几种括号嵌套形式来说 : [] , []] , {[}] , ([]) , {[()]}
第一个显然没问题
第二个错误 , 有右括号没能配对
第三个错误 , 虽然两两括号都有自己的另一半 但是嵌套顺序显然不符合规则
第四个不够严谨 虽然嵌套的没问题 但一般我们从内到外是圆括号 -> 方括号 -> 花括号的顺序 所以要指出这个不规范
第五个完全正确 顺序也是完美嵌套

所以我们得到结论 :
正确与否的条件是第一个右括号跟最后一个左括号两两配对相消 按照这个条件继续判断相消之后的新数组
严谨与否的条件是顺序从内到外是圆括号 -> 方括号 -> 花括号是否正确 而且只有花括号能有很多个 其他两个只能有一个

其实这个问题也是个很经典的储存括号的问题了
遇到左括号就往栈里面放 遇到右括号就与栈顶的左括号比对 不匹配就return false 匹配就top-- 然后一直进行操作
如果最后栈顶top大于0(类似于(([]))或者中途出现了明明top==0但是检测到右括号的情况(类似于[]]) 那么还是return false
如果都没遇到 结束的时候栈顶top也是回到了0 那么就是个正确的括号配对

至于判断严谨 只要后面新来的左括号小于上一个左括号就好(花括号可以有很多个)

这一小章是实现了我们最最最一开始定下的目标中的返回用户输入的括号配对正确与否的问题 而这个思想对我们实现优先级其实也可以用的上

接下来就是重头戏

★★★ 实现带括号的优先级计算

我们先看一下(-25*(3+4)^2)/10-5%3=这个简单的式子的计算过程

「大数」六则运算计算器的简单实现_第14张图片人人都会 总的来说其实就是

  1. 先算最最里面的括号 然后一层层向外算
    也就是说 如果你是个运算符 你身上每多套一层括号 你就该更被优先计算

  2. 相同的括号里面 遵循乘除大于加减 由于这里还有乘方还有求余 所以我们扩展一下 并且用数字大小的形式来记录六个符号的优先级大小关系
    + - : 1
    * / : 2
    ^ % : 3
    数字越大优先级越高

有了这两个条件 我们其实可以把原式具象化成这么一张图

「大数」六则运算计算器的简单实现_第15张图片

  1. 从高到低计算每一层
  2. 每算完一层 用这一整层的结果来替代这一层 即从上而下消去每一层
  3. 每一层内按照刚刚六则运算内部的优先级来计算

也就是:
「大数」六则运算计算器的简单实现_第16张图片这就启发我们一件事:

在这个具象化的"金字塔"里 你越高 你就越被先算 那你怎么能去更高的地方呢?
答案是: 你左边有多少个未被配对左括号 你就能去多高

我们看中间左边的3+4 他的左边有两个未被配对的左括号 说明他是第二层括号嵌套 也就是他应该在第三层的高度(第一层平地)
再来看 ^2 他左边有两个左括号 但有个已经被配对 说明那一层已经结束 他左边只有一个真正的未被配对的左括号 也就是他应该在第二层

接着 每一层里面的顺序那就按照 乘方,求余 > 乘除 > 加减的常规顺序就好

下面最重要最重要的步骤来了 : 怎么把这个形象的图像干的事情转化成计算机能理解的数据形式呢?

我们可以给每个运算符打个分
如果是同一级别 那么 乘方求余是3分 乘除2分 加减1分
再看他们每个人左边有多少个没配对的左括号 有一个的话分数乘10 两个分数乘100 n个的话分数乘10^n
然后运算优先级 就是分数从高到低 谁分高谁先被算

于是乎 按照这套理论 (-25*(3+4)^2)/10-5%3= 这个式子里面的六个运算符的分数如下:
[ * , + , ^ , / , - , % ]
[20 ,100,30 , 2 , 1 , 3 ]
得知计算顺序应该是+ -> ^ -> * -> % -> / -> -
只能说完全一致

我们再来最后一个gif来看看写程序的时候应该怎么写

我们需要一个sequence数组来记录第一个应该计算哪个符号 第二个又应该算哪个

「大数」六则运算计算器的简单实现_第17张图片
另外 在计算中 我们算完一个式子 其实就是两个数据变成一个数据了 此时需要往下压入栈来把空缺的地方给补起来
而且cal数组中的符号也被使用过丢弃了 所以也会产生空缺 也会进行把上面的往下压的这个操作
这时一定一定要记得 sequence中 比这个符号位置大的数 的位置 给减一
否则上面的符号被压下来了 而sequence中存的位置没有相应的变化的话会溢出

正确操作就像这样:
「大数」六则运算计算器的简单实现_第18张图片

以此类推 就可以得到正确计算的样子:

「大数」六则运算计算器的简单实现_第19张图片于是乎 我们成功实现了优先级运算

到这里 这三天的工作就告一段落了

写在后面 给自己

程序写了三天 文章写了一天半 中途一天出去拔了牙 这么算下来一小个程序花费了我本就不长的寒假五六天的时间了…

写文章这两天我的心里一直有声音在问自己 : 你的C++网课才只看了三天 就火急火燎的写这么个四不像的程序 而且含金量也不高 耗这么多时间值得吗?

确实是个值得深思的问题 这个小计算器里面绝大部分的知识其实很简单 也许真的对我的编程水平没啥提高 。

但后来我还是决心把它好好的完成并且记录下来。总归是我自个儿完全独立编写的第一个小项目嘛 任何人都是有第一次的。
如果它完美无缺 就用它增添自信 ; 如果它漏洞百出 就用它警醒自己。
(而且照结果来看估计是要拿来警醒自己了)

这个小计算器就先写到这里 也许以后学到了新的知识会来丰富它修改它 (前几天跟同学出去玩的时候还讨论了用来算根号的牛顿近似 也许等数学水平上去了之后来补充求根号啊 求小数幂指数乘方啊 甚至求积分啊 留个幻想hhh)
寒假剩下的时间就好好学习真正的知识了 希望能把C++学个大概吧 给大一下学期减轻一点负担。

好了,祝我 —— 如果你能看到这里的话,当然也祝你 ——未来,万事顺意,前程似锦。

你可能感兴趣的:(Way,on,Coding,c语言,c++)