介绍
作为一个编码器,你可能习惯告诉计算机要做什么,输入一些代码,运行它,计算机就会执行任何你给出的命令。
尽管我们拥有这种强大的计算机统治权,但我们的代码中仍然存在着大量的魔力,我们倾向于忽略它,特别是如果您正在使用具有预先构建功能的高级语言,就像我们大多数人一样。
在本文中,您将仔细研究这些概念之一,您可能在某个时候都使用过:操作顺序。
假设您想要评估此样本表达式:
15 + 10 * 3
按照操作的数学顺序,你会先将10
倍增至3个,然后将5
添加到结果中,但你会如何准确地告诉计算机这样做?
有不同的方法可以解析这种方程式,但有些需要比其他人更多的背景。
本教程将将方程式转换为正确的格式. 一旦它处于更易于机器读的形式,那么你可以通过你的解析算法来输送它,该算法将计算它。
了解Infix到Postfix
虽然您可能还没有意识到,但您可能已经熟悉 infix 标注。
15 + 10 * 3
这意味着操作员落在他们正在运作的操作符之间。
什么是Postfix Notation?
如前所述,您需要将方程式转换为计算机可以理解的格式. 此格式称为 postfix notation。
在 postfix 标注中写的表达式将有所有运算符跟随其运算符。
这很重要,因为当机器在读取此格式的表达式时,它将永远不会在操作符之前遇到一个操作符,这意味着它不必向前移动。
然后是样本表达式:
15 + 10 * 3
变成:
15 10 3 * +
这可能看起来不寻常,但有方法来达到这一点。
人类友好的方式从Infix转换到Postfix
在序列中以优先级列入:
1(5 + (10 * 3))
将每个运算器移动到右侧,直接在其关闭方针之前:
1(5 (10 3 *) +)
现在完全放下关节,这会让你在postfix标注中的表达式:
15 10 3 * +
以下是另一个示例,表明操作员不一定总是在最后:
18 * 4 + 2
2 ((8 * 4) + 2)
3 ((8 4 *) 2 +)
4 8 4 * 2 +
再次,这对计算机来说并不理想,它仍然不知道在哪里放置栏,幸运的是,有一个算法可以产生相同的结果。
使用 Shunting Yard 算法
Shunting Yard 算法由 Dijkstra 开发,用来将 infix 标注转换为 postfix 标注。
在你更进一步之前,让我们快速审查你将在这里使用的两个数据结构:堆栈和队列. 你可以使用数组来保持这两个数据集. 主要的区别来自 the order 你正在添加和删除数据。
队列:当你将数据添加到队列中时,你会将其推到后面。想象一下,你正在排队参加一个事件,而队列中的每个人都是队列中的一个元素。当你走到队列中时,你会自动插入队列的后面。当事件开始让人们进入(从队列中删除元素),他们会从队列的前面拉动,因为这些人已经在队列中了很长时间。
Stack:每次你添加一个新的元素到堆栈,它将放在顶部(或在前面)而不是在后面。当你想从堆栈中删除一个元素时,你会跳出顶部的元素。
<$>[注] **注:**本教程的其余部分将使用 push 和 pop 术语用于堆栈。
对于这个算法,假设你有一个临时堆栈来持有操作员(operator stack)和一个队列,将持有最终结果。
如何运作
Shunting Yard 算法遵循四个基本步骤:
如果你看到一个操作员 3. 如果你看到一个操作员:
- 如果操作员堆是空的,把入口的操作员推到操作员堆
- 如果入口的操作员比目前在操作员堆的顶部具有更高的优先级,那么把入口的操作员推到堆的顶部
- 如果入口的操作员有相同的优先级,从堆中跳出顶部的操作员,输出到排队,把入口的操作员推到堆
- 如果入口的操作员从堆顶部有较低的优先级,输出到排队,并测试入口的新的操作员从 stack_M_KBR1(
使用Shunting Yard算法评估一个Infix表达式
没有在行动中看到这些步骤是很难理解的,所以让我们通过上一个例子去尝试用算法来格式化它!
将该方程式从 infix 标注转换为 postfix 标注:
15 + 10 * 3
让我们设置两个数组:一个用于结果输出和一个用于临时操作员堆栈:
1expression = 5 + 10 * 3
2 output = []
3 operator stack = []
首先,你开始从左到右读出表达式,所以首先你有‘5’。
1expression = + 10 * 3
2 output = [5]
3 operator stack = []
接下来,你会看到+
。操作员堆栈是空的,所以你可以把它推到那里:
1expression = 10 * 3
2 output = [5]
3 operator stack = [+]
接下来是10
,所以你会立即输出:
1expression = * 3
2 output = [5, 10]
3 operator stack = [+]
现在您击中另一个操作员, *
. 由于操作员堆栈不是空的,您必须将其与操作员堆栈的当前顶部进行比较,以查看哪一个具有更高的优先级。
堆栈的当前顶部是+
。所以比较两者,你知道增量比增量具有更高的优先级。
这意味着你可以将其推到堆栈的顶部,这会给你:
1expression = 3
2 output = [5, 10]
3 operator stack = [*, +]
现在你击中你的最终值, 3
. 因为这不是一个运算器,你可以立即输出它:
1expression is now empty
2 output = [5, 10, 3]
3 operator stack = [*, +]
由于表达式现在是空的,剩下的只是从操作员堆栈中跳出所有代币并立即输出它们. 当你从堆栈中跳出时,你正在从顶部抓取,所以首先,你会拿到*
来推到队列的尽头,然后你会拿到+
。
1output = [5, 10, 3, *, +]
正如你所看到的,它匹配了你添加注释的上一个方法,但这种方式对于计算机来说更容易。
优先规则
你可能已经注意到有一点,而不是使用算法来决定,你依靠自己的知识来选择下一步要做的事情:确定哪个操作器具有更高的优先级。
当你了解算法背后的概念时,这并不重要,但当你写下实际代码来解决这个问题时,你将不得不建立在某些优先规则中。
您必须创建一个对象,该对象将基本上排名每个操作员. 您将为倍增和分割操作员提供2级,而添加和减去操作员为1级。
当你编码它时,你会通过比较他们的数字排名来比较两个操作员. 这里的实际数字1和2是任意的,所以不要太陷入其中。
1const precedence = {
2 "*": 2,
3 "/": 2,
4 "+": 1,
5 "-": 1
6 };
评估 postfix 表达式
您终于在 postfix 标注中获得了表达式,现在,您可以使用此格式来评估它。
算法评估表达式
以下是你将如何做到这一点:
- 开始用一个空堆
- 解析表达式中的第一个代币
- 如果它是操作员,请将其推到堆
- 如果它是操作员,请将适当数量的操作员从堆放到临时变量中(例如,倍数是二进制操作员,所以如果您正在解析并击中一个倍数操作员,那么您可以解除两个操作员)。
- 使用当前操作员和两种操作员来评估这个表达式( _6)。
在示例中,你只处理二进制操作员,所以当你看到一个操作员时,你总是可以打开两个操作员。
通过算法行走
让我们走过一些伪代码,在那里我们使用算法来评估样本修复后标注表达式:
15 10 3 * +
首先,您可以通过将每个操作员推到堆栈,直到您击中一个操作员:
1expression = [5, 10, 3, *, +]
2
3 - push 5
4 - push 10
5 - push 3
6
7 stack = [3, 10, 5]
所以现在你到达你的第一个操作员, *
,这意味着是时候开始出现了. 你会出现,直到你有两个值:
1- pop 3
2 - pop 10
现在你有你的两个操作符,3
和10
,所以你会将这与你的操作符*
相结合,留给你10 * 3
:
1expression = [+]
2 stack = [5]
3
4 tempOperand1 = 3
5 tempOperand2 = 10
6
7 tempOperator = *
8
9 eval(tempOperand1 + tempOperator + tempOperand2) // 3 * 10
你评估了这一点,得到30
,然后把它推回堆栈。
1expression = [+]
2 stack = [30, 5]
所以你再次开始分析表达式,你立即击中一个操作员. 再次,你必须从堆栈中跳出,直到你有两个操作员:
1expression = []
2 stack = []
3
4 tempOperand1 = 30
5 tempOperand2 = 5
6
7 tempOperator = +
8
9 eval(tempOperand1 + tempOperator + tempOperand2) // 30 + 5
你打开30
和5
,你已经准备好再次评估。5 + 30
给你35
,你现在可以把它推回堆栈。
回到原始表达式来分析下一个代币,你会发现它是空的!
1expression = []
2 stack = [35]
这要么意味着你已经完成了,要么原来的表达方式被扭曲了。
它只有一个值,所以这意味着你完成了,‘35’是原式表达式的最终输出,‘5 + 10 * 3’。
识别前缀笔记
在 prefix notation 中评估一个表达式的算法基本相同,但这一次你将从右到左读取。
如果您回到原始方法中添加和移动操作员,您可以将其转换为前缀符号,就像您 postfix 的方式一样。而不是将操作员移动到其操作员的末尾,您会将其移动到开始。
15 + 10 * 3
2 (5 + (10 * 3))
3 (+ 5 (* 10 3))
4 + 5 * 10 3
如果你想测试你的知识,试着找出你将如何在算法上做到这一点,通过对Shunting Yard算法进行小的修改。
结论
在本教程中,您创建了一种将表达式转换为后修复符号的算法,并通过评估一个表达式来测试它。