前言
很多人在初学C语言时,遇到的第一个难题往往就就是课设。常见的课设有两种,其一是学生管理系统,这种其实相对比较简单,只要肯花时间,还是能够写出来的;其二则是四则运算计算器,那很多同学一看就说:“雾草,计算器,那还不简单,用一个switch不就好了吗?”
然鹅,当他们开始写的时候,才突然发现,上了个
大逼当!光是乘除先于加减就已经难倒一片同学了,更丧心病狂的是有的学校还加上了括号!不过没关系,今天就让濡白来给大家梳理一下,如何利用栈的思想以及逆波兰表达式快速解决这道题!
目录
这是我的gitee地址,欢迎大家参观指正:濡白记录一些有趣题目的小仓库
本次的代码也存入了其中,文件名为 The calculator
栈的思想
在此我们将先介绍栈的思想,这样才能更好的完成后缀表达式。作为一种基本的数据结构,栈其实挺好理解。首先,我们先假设有一个一端封闭圆筒,直径为1,现在将一个直径为1的红球放入其中,然后依次放入黄球,蓝球,绿球。现在假如想要拿出最开始的红球,应该如何做?我们只能从顶部先拿出绿球,在依次拿出蓝球,黄球,最后才能拿出红球。现在懂了吗?栈用一句话解释就是“先入后出,后入先出”。
如图就是进栈,出栈的简单视图。
逆波兰表达式
在开始之前,我们首先需要了解一种适合计算机的计算机表达式,逆波兰表达式。
对于带括号的四则表达式,其实最困难的部分就是如何解决括号优先级最大的问题,而一位波兰的逻辑学家,就像牛顿被苹果砸到一样,突然来了灵感,想出了一种可以去掉括号的后缀表达式。而为了纪念这位波兰逻辑学家,后人将这种表达式命名为“逆波兰表达式”。
什么?你问为啥不以那位逻辑学家的名字命名,就像拉格朗日公式一样。emmmm,这也许,,大概,,是这位逻辑学家的名字不好读?
---------------------------------------------这是一条奇妙的分割线----------------------------------------------
让我们先来看一个简单的例子,对于“ 9+(3 - 1)× 3+ 10 ÷ 2 ”,它的后缀表达式应该是怎么样的呢?答案是“ 9 3 1 - 3 * + 10 2 / + ",在计算机上 × 为 * ,÷ 为 / 。让我们来看一下这个式子会如何运算:
首先,遇到数字就存入一个数组之中,遇到符号就对最后存入的两个数字进行运算。
9 3 1 - 结果为 9 (3 - 1),运算之后,数组中余下 9 2 ,接下来 3 * ,结果为 9(2*3)
,然后是 + ,那么 9 + 6 ,运算之后数组中只剩下 15, 然后 10 2 /,运算为 10 / 2 = 5,此时数组中剩下15 5,最后一个 + ,运算结果为 20 ,显然和我们正常计算得到的结果是一样的。让我们来用图像帮助大家理解的更深刻一些:1.初始化一个空栈。此栈用来对要运算的数字进出使用。
2.后缀表达式中前三个都是数字,所以9、3、1进栈。
3,接下来是“-”,所以将栈中的1出栈作为减数,3出栈作为被减数,并运算3-1 得到2,再将2进栈。
4.接着是数 3 进栈。
5.后面是 “*”,也就意味着栈中3和2出栈,2与3相乘,得到6,并将6进栈。
6.下面是“ + ”,所以栈中6和9出栈,9与6相加,得到15,将15进栈。
7.接着是10与2两数字进栈。
8.接下来是符号“/” ,因此栈顶的 10 和 2 出栈,10 与 2 相除得到5,将5入栈。
9.最后一个符号是“+”,所以 5 和 15 相加得到 20,20入栈。
10.运算完毕,结果是20,出栈。
后缀表达式可以轻松解决问题,欸!先别急!不要以为了解后缀表达式就可以轻松写出来了,“ 9 3 1 - 3 * + 10 2 / + ”后缀式看着这么丑,也只有计算机还挺稀罕他了,怎么能让用户进行输入呢?,我们平时正常输入的式子叫做中缀式,我们现在应该考虑的是如何将中缀式改为后缀式。
中缀式改后缀式
在此我们先初始一个栈区来保存符号部分。
其规则如下:
从左到右遍历中缀表达式的每一个数字和符号,若是数字则输出为后缀表达式的一部分,若是符号,则先判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号,则依次出栈直到栈底或者遇到左括号,然后将当前符号进栈,一直到最后最终输出后缀表达式。下面对栈区做一个浅浅的分析。
-------------------------------------------这又是一个优美的分割线---------------------------------------------
1.初始化一个空栈。
2.第一个字符是数字9,输出。后面是符号“ + ”,入栈。
3.接下来的字符是“(”,入栈。
4.下一个字符是数字3,输出,接着是“ - ”入栈。然后是数字1输出, 当前总表达式为9 3 1。
5.下一个字符是“)”,因此我们需要出栈,直到遇到可以匹配的“(”,即输出“ - ”,总表达式为9 3 1 -。
6.接下来是数字3输出,然后是“ * ”进栈。
7. 下一个符号是“ + ”,而此时栈顶元素为 “ * ”,优先级更高,因此我们出栈,直到栈空,然后“ + ”入栈。
8.接下来是数字10输出,最后“ / ”入栈。
9.然后是最后一个数字2输出。
10.此时判断已经读取完毕,因此所有符号出栈,成为最后的表达式。
最后也就输出了我们的最后表达式。好了现在你已经掌握了如何将中缀式改为后缀式,现在你需要做的就是,再读取一遍后缀式子然后输出就可.........
.
.
.
.
.
.
哈哈哈哈哈哈,那当然是不可能的,我们要做的是利用两个栈,来进行存储,遇到符号需要输出的时候,我们直接将数字栈顶的两个数字进行运算然后继续保存到数组之中就可以了,下面是具体的代码实现。
具体代码实现
#define my_MAX 100 // 调整栈的大小
// 模拟实现四则运算的计算器,实现包含括号的输入,括号的嵌套等
#include<stdio.h>
#include<math.h>
struct stack_of_nums // 用于存放数据的栈区
{
float nums[my_MAX];
int top; // 栈顶的下标
}bolannums;
struct stack_of_symbols // 用于存放不同运算符号的栈区
{
char symbols[my_MAX];
int top; // 栈顶下标
}bolansymbols;
void outpush(struct stack_of_nums*, struct stack_of_symbols*); // 使两个栈区出栈的函数
int main()
{
float num = 0; // 用于记录一行中读取到的一个数字
int key = 2, index = 1; // index为计算小数部分时,代表10的指数
// key表示状态
//2:num此时为0,未读取数据,此时为两个符号连续 如: (1 + 2) * 2
//1:num此时已经读取过数据,且还在整数部分
//0:num接下来要开始读取小数部分
char c = 0; // 用于从输入中读取一个字符
bolannums.top = bolansymbols.top = -1; // 先将栈顶初始化为-1,表示栈内无元素
while ((c = getchar()) != '\n') // scanf的正则表达式
{
if (('0' <= c && c <= '9') && key) // 如果是数字则可以存入整数部分,key = 2 或者 1 的时候都可以进入整数的存储
{
key = 1; // 一旦进入之后,赋值表示num已经读取到了数字
num = num * 10 + c - '0'; // 将数字加到num中,连续读取数字的时候先 *10
}
else if (c == '.') // 遇到小数点的时候标志开始记录小数部分
key = 0;
else if ((c >= '0' && c <= '9') && key == 0) // 小数部分的运算
{
num += (c - '0') * (float)pow(10, -(index++)); // 当前数字 * 0.1得到小数部分,并加入num
}
else if (c != ' ') // 不读取空格,只读取 + - * /
{
if (key != 2) // 在num被赋值过的时候才会加入栈nums, 同时表示读取数字直到遇到一个符号, 不包括换行
{
bolannums.nums[++bolannums.top] = num; // 移动栈顶指针并且加入栈区
num = 0;
index = 1;
key = 2; // 将 num, index, key 初始化,方便下一次数据的读取
}
if (bolansymbols.top != -1) // 栈顶只有一个符号不需要outpush
{
if (c == '+' || c == '-') // 只要当栈顶元素优先级大于等于 C 的时候才会outpush
outpush(&bolannums, &bolansymbols);
else if (c == ')') // 当 c 为反括号的时候只需要outpush,但是 此时的 c 不需要入栈
{
outpush(&bolannums, &bolansymbols);
if (bolansymbols.symbols[bolansymbols.top] == '(')
(bolansymbols.top)--;
continue; // 直接开始下次循环
}
}
bolansymbols.symbols[++bolansymbols.top] = c; // c 入栈
}
else // 遇到空格直接开始下次循环 (可以不加此分支)
continue;
}
if (key != 2)
bolannums.nums[++bolannums.top] = num; // 如果式子最后以数字结尾则再次入栈
if (bolansymbols.top != -1) // 判断符号栈区内是否为空,最后将所有栈区清空,只留下bolannums的第一个元素
outpush(&bolannums, &bolansymbols);
printf("%.2f", bolannums.nums[0]);
return 0;
}
void outpush(struct stack_of_nums* nums, struct stack_of_symbols* symbols)
{
while (symbols->top != -1 && symbols->symbols[symbols->top] != '(') // 出栈,直到是遇见左括号或者栈内无元素
{
switch (symbols->symbols[symbols->top]) // 四则运算,每次运算会消耗掉栈顶元素,而每次出栈会,栈顶向下移动,相当于消耗掉了不会再次被使用
{
case '*':
nums->nums[nums->top - 1] *= nums->nums[nums->top];
break;
case '/':
nums->nums[nums->top - 1] /= nums->nums[nums->top];
break;
case '+':
nums->nums[nums->top - 1] += nums->nums[nums->top];
break;
case '-':
nums->nums[nums->top - 1] -= nums->nums[nums->top];
break;
}
(nums->top)--, (symbols->top)--;
}
}
只需要简简单单一百行你就可以拥有一个可以带括号的四则运算计算器了。当然,上述代码为了全面,使用的浮点型数组记录数据,同学们可以先从整形练起哦~什么?你还不满足?还想要摩多摩多?
呵,贪婪的女人,这就作为大家的小练习吧!请尝试在此基础上加上“ ^ ”表示指数,如2^3为2*2*2 =8,以及“$” 表示开根,如$4 = 2。