为什么后缀(rpn)表示法比前缀更常用?

Why postfix (rpn) notation is more used than prefix?

我所说的使用是指它在许多计算器中的使用,例如 HP35-

我的猜测(和困惑)是 -

  1. postfix 实际上是更有效的内存 -(SO post 评论 here)。 (混淆 - 两者的评估算法与堆栈相似)
  2. 当时计算器中的键盘输入类型(混淆 - 这应该无关紧要,因为它只取决于首先或最后给出的运算符的顺序)

可以问这个问题的另一种方式是 postfix notation 相对于 prefix 有什么优势?
谁能赐教一下?

一方面,更容易实施评估。

有了前缀,如果你推送一个操作符,然后是它的操作数,你需要知道操作符何时拥有它的所有操作数。基本上,您需要跟踪您推送的运算符何时拥有所有操作数,以便您可以展开堆栈并进行评估。

由于一个复杂的表达式最终可能会在堆栈上有许多运算符,因此您需要一个可以处理此问题的数据结构。

例如,这个表达式:- + 10 20 + 30 40 将同时在堆栈上有一个 - 和一个 +,对于每个,你需要知道你是否有可用的操作数。

带后缀,当你压入一个运算符时,操作数(应该)已经在堆栈上,只需弹出操作数并计算。你只需要一个可以处理操作数的栈,不需要其他数据结构。

基本上,因为如果您在 postfix 中编写表达式,您可以仅使用 Stack:

  1. 读取表达式的下一个元素,

  2. 如果是操作数,则入栈,

  3. 否则从Operation需要的Stack中读取操作数,并将结果压入Stack。

  4. 如果不是表达式结束,转到1。

例子

expression = 1 2 + 3 4 + *
stack = [  ]

Read 1, 1 is Operand, Push 1
[ 1 ]

Read 2, 2 is Operand, Push 2
[ 1 2 ]

Read +, + is Operation, Pop two Operands 1 2
Evaluate 1 + 2 = 3, Push 3
[ 3 ]

Read 3, 3 is Operand, Push 3
[ 3 3 ]

Read 4, 4 is Operand, Push 4
[ 3 3 4 ]

Read +, + is Operation, Pop two Operands 3 4
Evaluate 3 + 4 = 7, Push 7
[ 3 7 ]

Read *, * is Operation, Pop two Operands 3 7
Evaluate 3 * 7 = 21, Push 21
[ 21 ]

如果您希望您的人类阅读顺序与机器基于堆栈的评估顺序相匹配,那么 postfix 是一个不错的选择。

也就是说,假设您是从左到右阅读的,并不是每个人都这样做(例如希伯来语、阿拉伯语……)。并假设您的机器使用堆栈进行评估,但并非所有人都这样做(例如术语重写 - 参见 Joy)。

另一方面,当机器评估 "back to front/bottom-to-top" 时,人类偏好的前缀没有任何问题。如果关注的是评估 as 令牌到达,序列化也可以逆转。工具帮助在前缀表示法中可能会更好(首先了解 functions/words 可能有助于确定有效参数的范围),但您始终可以从右到左 type

我认为这只是一个约定...

前缀表示法可能更常用...在数学中,在 F(x,y) 等表达式中。这是一个非常古老的约定,但与许多旧系统(英尺和英寸、信纸)一样,与我们使用更精心设计的系统所能做的相比,它有缺点。

几乎每一年的大学数学教科书都至少要浪费一页来解释f(g(x))意味着我们先申请g然后f。按阅读顺序这样做更有意义:x.f.g 意味着我们首先应用 f。那么如果我们想申请 h "after" 我们就说 x.f.g.h.

例如,考虑我最近不得不处理的 3d 旋转问题。我们想根据 XYZ 约定旋转矢量。在 postfix 中,操作是 vec.rotx(phi).roty(theta).rotz(psi)。使用前缀,我们必须重载 *() 然后反转操作顺序,例如 rotz*roty*rotx*vec。当您想考虑更大的问题时,这很容易出错并且不得不一直考虑它。

例如,我在别人的代码中看到类似rotx*roty*rotz*vec的东西,我不知道这是一个错误还是不寻常的ZYX轮换约定。我还是不知道。该程序有效,因此它在内部是自洽的,但在这种情况下,前缀表示法使其难以维护。

前缀表示法的另一个问题是,当我们(或计算机)解析表达式 f(g(h(x))) 时,我们必须将 f 保存在内存中(或堆栈中),然后 g,然后 h,然后 ok 我们可以将 h 应用于 x,然后我们可以将 g 应用于结果,然后 f 应用于结果。与 x.f.g.h 相比,内存中的东西太多了。在某些时候(对于人类来说比计算机要快得多)我们会 运行 内存不足。那样的失败并不常见,但为什么在 x.f.g.h 不需要短期记忆的情况下还要打开那扇门。就好比递归和循环的区别

还有一件事:f(g(h(x))) 有很多括号,它开始看起来像 Lisp。就运算符优先级而言,后缀表示法是明确的。

一些数学家(尤其是 Nathan Jacobson)曾尝试改变约定,因为后缀在顺序非常重要的非交换代数中更容易使用,但收效甚微。但是既然我们有机会在计算方面做得更好,我们应该抓住这个机会。

两种符号的离线评估在理论机器中是相同的

(Eager evaluation strategy)仅用一个堆栈进行评估(不将运算符放入堆栈)

可以通过评估前缀符号 right-to-left.

来完成
- 7 + 2 3
# evaluate + 2 3
- 7 5
# evaluate - 7 5
2

与计算后缀符号相同 left-to-right.

7 2 3 + -
# put 7 on stack
7 2 3 + -
# evaluate 2 3 +
7 5 -
# evaluate 7 5 -
2

(优化的短路策略)使用两个堆栈(一个用于运算符,一个用于操作数)进行评估

可以通过评估前缀符号 left-to-right.

来完成
|| 1 < 2 3
# put || in instruction stack, 1 in operand stack or keep the pair in stack
instruction-stack: or
operand-stack: 1
< 2 3
# push < 2 3 in stack
instruction-stack: or, less_than
operand-stack: 1, 2, 3
# evaluate < 2 3 as 1
instruction-stack: or
operand-stack: 1, 1
# evaluate || 1 1 as 1
operand-stack:1

请注意,我们可以在这里轻松地对 boolean 表达式进行 短路优化(与之前的评估序列相比)。

|| 1 < 2 3
# put || in instruction stack, 1 in operand stack or keep the pair in stack
instruction-stack: or
operand-stack: 1
< 2 3
# Is it possible to evaluate `|| 1` without evaluating the rest ? Yes !!
# skip < 2 3 and put place-holder 0
instruction-stack: or
operand-stack: 1 0
# evaluate || 1 0 as 1
operand-stack: 1

与计算后缀符号相同 right-to-left.

(优化的短路策略)用一个带元组的堆栈进行评估(同上)

可以通过评估前缀符号 left-to-right.

来完成
|| 1 < 2 3
# put || 1 in tuple-stack
stack tuple[or,1,unknown]
< 2 3
# We do not need to compute < 2 3
stack tuple[or,1,unknown]
# evaluate || 1 unknown as 1
1

与计算后缀符号相同 right-to-left.

人工从左到右输入数据时在计算器中进行在线评估

在计算器中输入数字时,后缀记法 2 3 + 可以立即求值,而人类将要输入的符号一无所知。它与前缀表示法相反,因为当我们有 - 7 + 时我们无事可做,直到我们得到像 - 7 + 2 3.

这样的东西

人工从右到左输入数据时在计算器中进行在线评估

现在 Prefix-notation 可以立即评估 + 2 3,而 Postfix-notation 等待进一步输入时有 3 + - .

请参考@AshleyF,注意阿拉伯语是从右到左书写的,而英语是从左到写的!

我猜 little-endianbig-endian 与此 prefix/postfix 符号相关.

最后说一句,Reverse-Polish notation得到Dijkstra的大力支持(他是短路优化的强烈反对者,被认为是Reverse-Polish notation的发明者)。支持不支持是你的选择(我不支持)。