当前位置: 首页 > news >正文

【iOS】简单的四则运算

【iOS】简单的四则运算

  • 前言
  • 表达式
  • 进行简单四则运算
    • 直接使用中缀表达式计算
    • 中缀表达式转后缀表达式再计算
      • 中缀表达式转后缀表达式
      • 后缀表达式的计算
  • 总结

前言

四则运算的本质是使用运算符号优先级来判断是否入栈出栈,其思路有两种:一种是中缀表达式转后缀表达式,对后缀表达式进行计算得到结果;另一种是直接使用中缀表达式计算结果。

表达式

  • 中缀表达式:操作符以中缀形式位于运算数中间,是我们日常通用的算术逻辑公式,如 3 + 2。
  • 后缀表达式:又称逆波兰式,操作符以后缀形式位于两个运算数后,如 3 2 +。
  • 前缀表达式:又称波兰式,操作符以前缀形式位于两个运算数前,如 + 3 2。

其中中缀表达式适合于人类思维结构和运算习惯,但不适用于计算机。适用于计算机的表达式是后缀表达式。与中缀表达式不同,后缀表达式不需要使用括号来标识操作符的优先级,而是按操作符从左到右出现的顺序依次执行进行计算。

进行简单四则运算

无论哪种方法计算,都需要使用栈,因此我们要使用OC数组等模拟实现栈:

#import <Foundation/Foundation.h>@interface Stack : NSObject@property(nonatomic, strong) NSMutableArray *stackArray;
@property(nonatomic, assign) NSInteger stackSize;-(void)push:(double)num;
-(void)pop:(double*)num;
-(double)getTop;
-(BOOL)isEmpty;@end
#import "Stack.h"@implementation Stack-(instancetype)init {self = [super init];if (self) {//+arrayWithCapacity:(NSUInteger)numItems:创建可变数组self.stackArray = [NSMutableArray arrayWithCapacity:100];self.stackSize = 100;}return self;
}-(void)push:(double)num {[self.stackArray addObject:@(num)];
}-(void)pop:(double *)num {if (self.stackArray.count > 0) {NSNumber *last = [self.stackArray lastObject];*num = [last doubleValue];[self.stackArray removeLastObject];}
}-(double)getTop {return [[self.stackArray lastObject] doubleValue];
}-(BOOL)isEmpty {return self.stackArray.count == 0;
}@end

直接使用中缀表达式计算

直接使用中缀表达式计算不需要生成一个新的表达式序列,而是使用两个栈一边读取一边计算,动态判断是否直接计算或者先压入栈。

这里我们提前写好运算符优先级表,行对应 theta1(栈顶运算符),列对应 theta2(当前读入的运算符)。

其返回值:

  • ‘<’:栈顶运算符优先级低,当前读入运算符入栈
  • ‘>’:栈顶运算符优先级高,栈顶符号出栈并计算
  • ‘=’:括号匹配或表达式结束
  • ‘0’:非法情况
i\j+-*****/()=
+>><<<>>
->><<<>>
*****>>>><>>
/>>>><>>
(<<<<<=0
)>>>>0>>
=<<<<<0=

具体实现:

char Precede(char theta1, char theta2) {//theta1:栈顶运算符//theta2:当前正在处理运算符int i = 0, j = 0;char pre[7][7] = {{'>', '>', '<', '<', '<', '>', '>'},{'>', '>', '<', '<', '<', '>', '>'},{'>', '>', '>', '>', '<', '>', '>'},{'>', '>', '>', '>', '<', '>', '>'},{'<', '<', '<', '<', '<', '=', '0'},{'>', '>', '>', '>', '0', '>', '>'},{'<', '<', '<', '<', '<', '0', '='}};switch (theta1) {case '+':i = 0;break;case '-':i = 1;break;case '*':i = 2;break;case '/':i = 3;break;case '(':i = 4;break;case ')':i = 5;break;case '=':i = 6;break;}switch (theta2) {case '+':j = 0;break;case '-':j = 1;break;case '*':j = 2;break;case '/':j = 3;break;case '(':j = 4;break;case ')':j = 5;break;case '=':j = 6;break;}return pre[i][j];
}

后续的主要逻辑是,初始化两个栈,一个存储数字,一个存储运算符号,然后将符号与数字不断地入栈出栈知道"="。其中对小数、负数再单独进行处理。

这里展示主要运算操作部分:

-(NSString *)evaluateResult:(NSString *)input {int index = 0;int isNegative = 0;int isParentheses = 0;self.StackNum = [[Model alloc] init];self.StackSign = [[Model alloc] init];[self.StackSign push:'='];char ch = [input characterAtIndex:index++];if (ch == '-') {ch = [input characterAtIndex:index++];isNegative = 1;}double a, b, theta, x1, x2;while (ch != '=' || [self.StackSign getTop] != '=') {if (istTheta(ch)) {if (ch == '(') {isParentheses = 1;}if (ch == '-' && [input characterAtIndex:index - 2] == '(') {//检查符号前是否有括号判断是否为负数//检查后重置isParentheses,防止让程序以为自己还在括号开头isNegative = 1;isParentheses = 0;ch = [input characterAtIndex:index++];continue;//判断为符号而不是运算符减,跳出while循环剩余部分,避免继续往下执行将-当作减号运算符压入符号栈}switch (Precede([self.StackSign getTop], ch)) {case '<':[self.StackSign push:ch];ch = [input characterAtIndex:index++];break;case '>':[self.StackSign pop:&theta];[self.StackNum pop:&b];[self.StackNum pop:&a];[self.StackNum push:Operate(a, theta, b)];break;case '=':[self.StackSign pop:&theta];ch = [input characterAtIndex:index++];break;}} else if (isdigit(ch)) {x1 = ch - '0';[self.StackNum push:x1];x2 = x1;ch = [input characterAtIndex:index++];while (isdigit(ch)) {x1 = ch - '0';x2 = 10 * x2 + x1;ch = [input characterAtIndex:index++];}if (ch == '.') {ch = [input characterAtIndex:index++];double decimal = 0.0;double count = 0;while (isdigit(ch)) {double f = (double)(ch - '0');decimal += f / pow(10, count++);ch = [input characterAtIndex:index++];x2 += decimal;}}double tempX1;[self.StackNum pop:&tempX1];if (isNegative) {[self.StackNum push:-x2];} else {[self.StackNum push:x2];}} else {return @"错误";}}double result = [self.StackNum getTop];if (isnan(result)) {return @"错误";} else {NSString *resultString = [NSString stringWithFormat:@"%f", result];resultString = [self removeZero:resultString];return resultString;}
}

输入几个算式验证下运算结果:
在这里插入图片描述

中缀表达式转后缀表达式再计算

中缀表达式转后缀表达式

主要操作为:准备一个字符栈存储尚未处理的操作符和括号,从左至右依次遍历中缀表达式各个字符

  • 字符为运算数:直接送入后缀表达式。
  • 字符为左括号:直接入栈。
  • 字符为右括号:直接出栈,并将出栈字符依次送入后缀表达式,直到栈顶字符为左括号,只要满足栈顶为左括号,即可进行最后一次出栈
    (左右括号只出栈,不送入后缀表达式)
  • 字符为操作符:
    • 若栈空:直接入栈。
    • 若栈非空:判断栈顶操作符。若栈顶操作符低于该操作符,该操作符入栈;否则出栈,并将出栈字符依次送入后缀表达式,直到栈空或栈顶操作符优先级高于该操作符,停止出栈。
  • 重复上述步骤直至完成中缀表达式的遍历,接着判断字符栈是否为空,非空直接出栈,并将出栈字符依次送入后缀表达式。
-(NSArray*)infixToPostfix:(NSArray*)tokens {NSMutableArray *output = [NSMutableArray array];Model *model = [[Model alloc] init];for (NSString *token in tokens) {if (token.length == 0) {continue;}if (![self isTheta:token]) {[output addObject:token];} else {if ([token isEqualToString:@"("]) {[model push:token];} else if ([token isEqualToString:@")"]) {while (![[model top] isEqualToString:@"("]) {[output addObject:[model pop]];}[model pop];} else {while (![model isEmpty] && [self priorityOfOperator:[model top]] >= [self priorityOfOperator:token]) {[output addObject:[model pop]];}[model push:token];}}}while (![model isEmpty]) {[output addObject:[model pop]];}return output;
}

这里可以参考《数据结构》:中缀表达式转后缀表达式 + 后缀表达式的计算博客中的示例图来更形象地理解。

后缀表达式的计算

主要操作为:准备一个运算数栈存储运算数和操作结果,从左至右依次遍历后缀表达式各个字符

  • 字符为运算数:直接入栈。
  • 字符为操作符:连续出栈两次,使用出栈的两个数据进行相应计算,并将计算结果入栈。

注意:第一个出栈的运算数为 a ,第二个为 b ,此时的运算符为 - ,计算为 b - a ,a 和 b 顺序不能反!!

  • 重复上述步骤直完成后缀表达式的遍历,最后栈中的数据就是计算结果。
-(double)evaluatePostfix:(NSArray*)tokens {Model *model = [[Model alloc] init];for (NSString *token in tokens) {if (![self isTheta:token]) {[model push:@(token.doubleValue)];} else {double a = [[model pop] doubleValue];double b = [[model pop] doubleValue];double result = 0;if ([token isEqualToString:@"+"]) {result = b + a;} else if ([token isEqualToString:@"-"]) {result = b - a;} else if ([token isEqualToString:@"*"]) {result = b * a;} else if ([token isEqualToString:@"/"]) {if (a == 0) {return NAN;}result = b / a;}[model push:@(result)];}}return [[model pop] doubleValue];
}

这里值得注意的有两点:

  1. 分词

我们需要将每个运算符和数字分隔开各自作为一个字符存入数组中(尤其注意代码中分隔负数的方法

-(NSArray*)tokenize:(NSString*)input {NSMutableArray *tokens = [NSMutableArray array];NSMutableString *numberBuffer = [NSMutableString string];for (int i = 0; i < input.length; i++) {unichar ch = [input characterAtIndex:i];//当前字符NSString *chStr = [NSString stringWithFormat:@"%c", ch];//字符改写成字符串形式BOOL isNegative = NO;if (ch == '-') {if (i == 0) {isNegative = YES;} else {unichar preChar = [input characterAtIndex:i - 1];if ([self isTheta:[NSString stringWithFormat:@"%c", preChar]] && preChar != ')') {isNegative = YES;}}}if (isdigit(ch) || ch == '.' || isNegative) {//如果是数字、小数、负数,追加到字符后面形成完整数字[numberBuffer appendString:chStr];} else {if (numberBuffer.length > 0) {//将处理好的完整数字加到数组中并置空,准备处理下一个字符//[tokens addObject:numberBuffer];[tokens addObject:[numberBuffer copy]];[numberBuffer setString:@""];}if (ch != ' ' && ch != '=') {[tokens addObject:chStr];}}}if (numberBuffer.length > 0) {//[tokens addObject:numberBuffer];[tokens addObject:[numberBuffer copy]];}return tokens;
}
  1. NSMutableString的可变性和对象引用

我们在分词时,有一个缓存字符串numberBuffer,当我们得到完整数字或是符号,并要将其加到数组tokens中时,使用了copy。

[tokens addObject:[numberBuffer copy]];

那么为什么不是直接把numberBuffer添加到tokens数组中呢?

回想一下copy的内容,非容器类可变对象的copy和mutableCopy都是深拷贝,与原对象不共用同一内存地址,也就是说[numberBuffer copy]会创建一个NSString副本,这样数组中每个元素不会随numberBuffer改变而改变。

这样的输出是正确的:

在这里插入图片描述

然而,[tokens addObject:numberBuffer]存的是同一个NSMutableString 的引用,当numberBuffer的值改变时,数组中对应内容也会随之改变,这样tokens中的数组最终都会变成最后一次numberBuffer的内容。

这样会违背我们的预期,输出结果就是错误的:

在这里插入图片描述

这里我们再逐步输出一下tokens和numberBuffer直观地看一下不使用copy的问题:
在这里插入图片描述

总结

简单的四则运算是我在仿写计算器时的核心,同时也对栈的学习很有帮助,后面将会多复习这里。

http://www.dtcms.com/a/442191.html

相关文章:

  • Tomcat的CATALINA_BASE
  • 嵌入式 Tomcat 与 Spring MVC 集成
  • MyBatis 进阶
  • 软件设计师-软件工程-软件过程模型
  • 论坛网站方案手机网站建设的趋势
  • LeetCode每日一题——单调数列
  • LeetCode 100题(10题)
  • 后端开发网站做一些什么建设部网站官网 造价鉴定
  • day52-Zabbix(第一部分)
  • 依托Java和百度地图实现长沙市热门道路与景点实时路况检索的实践探索
  • 7-1-查询练习
  • Numpy 手搓线性回归
  • 昆明网站服务器湖北seo推广
  • 医院网站建设怎么样盐城网站建设效果
  • dockerfile理解
  • SpringBoot集成Druid连接池_配置优化与监控实践指南
  • 12380网站建设打算公众号小程序开发公司
  • 高并发场景下的前后端数据同步策略:长轮询、SSE与WebSocket对比分析
  • 网站推广对接北京手机网站搭建费用
  • Raydium
  • 动态Vault
  • 量化交易策略中ATR与波动率的配合
  • 便宜的网站设计企业永久免费自动建站系统
  • 深入解析JS事件循环机制 (Event Loop)
  • 亭湖区建设局网站小红书推广计划
  • 吃透大数据算法-时间轮(TimingWheel)
  • 从输入URL到展示出页面的这个过程~
  • WebDAV 与 SMB 在钓鱼攻击中的区别
  • 8. Pandas 日期与时间序列数据处理
  • 免费网站模板做零食的网站有哪些