大多数人会使用临时变量交换两个变量 x 和 y,像这样:
tmp = x
x = y
y = tmp
这里有一个巧妙的编程技巧,无需临时变量即可交换两个值:
x = x xor y
y = x xor y
x = x xor y
不相信?试试看 – 为 x 和 y 写入任意初始值:
这几乎看起来像魔术 – 同样的语句重复了3次,然后 瞧 – 这些值神奇地交换了?让我们仔细看看。
工作原理
要理解这个技巧,把语句分解成唯一的值:
x1 = x xor y
y1 = x1 xor y
x2 = x1 xor y1
根据我们的代码,x2 应该具有 y 的原始值。我们来详细计算最后一步:
x2 = x1 xor y1
x2 = x1 xor (x1 xor y) // replace y1
x2 = (x1 xor x1) xor y // regroup parenthesis - order does not matter for XOR
x2 = 0 xor y // a xor a => 0
x2 = y // 0 xor a => a; x2 now has y's original value
哇——x2 真的等于 y!交换成功了。现在让我们试试 y1:
y1 = x1 xor y
y1 = (x xor y) xor y
y1 = x xor (y xor y)
y1 = x xor 0
y1 = x // y1 == x's original value
然后,这个技巧再次奏效。x2 和 y1 得到了交换后的值。
直观理解
好吧,当然,布尔代数运算得很好——但这不是令人满意的解释。我想 理解 深入理解它,让它变得合理,而不是 XOR 属性的某种产物。让我们再看一看:
1: x = x xor y
2: y = x xor y
3: x = x xor y
在第 1 行,我们将 x 和 y(使用 XOR)组合起来,得到这个“混合体”,并将其存回 x。XOR 是保存信息的好方法,因为你可以再次进行 XOR 来移除它。
所以,这正是我们在第 2 行所做的。我们将混合体与 y 进行 XOR,这会消除所有 y 的信息,只留下 x。我们将这个结果存回 y,现在它们交换了。
在最后一行,x 仍然有混合体的值。我们再次将其与 y(现在有 x 的原始值)进行 XOR,以从混合体中消除 x 的所有痕迹。这留下 y,交换完成!
你真的会用它吗?
不行。这是一个很酷的技巧,但不要把它写成一个实际的交换函数。如果你在 6 个月后需要调试它,你会遇到一些麻烦。让我告诉你为什么:
假设 x 和 y 是指向对象的指针或引用,并且两者指向同一位置。我们期望交换函数只是交换值,没有净变化,对吧?
好吧,看看如果我们展开第 1 行会发生什么:
x = x xor y
x = x xor x // x and y are equal
x = 0
哇!所以 x 一开始就变成了 0。这本身没问题,但因为 x 和 y 在同一位置,我们也让 y 变成了 0!我们丢失了原始值,这个问题被称为 别名副作用:改变一个变量会对另一个变量产生间接影响。
那么,你能发现这个 bug 吗?我本来不会,而且找出为什么一个无害的交换函数会导致数据丢失将是一场噩梦。像这样的小技巧可能相当危险。正如 Brian Kernighan 所说:
调试代码比编写代码要难一倍。因此,如果你尽可能巧妙地编写代码,那么按定义,你就不够聪明去调试它。
这在这里非常适用——我们尽可能巧妙地编写了代码 :)。所以,把它当作一次智力练习,提出几点:
- 异或是一种组合信息并随后提取信息的绝佳方式。基于异或的加密就使用了这项技术。此外,异或可以组合N个元素,而不仅仅是2个。
- 即使是最简单的操作,也有新的实现方式。
更多棘手的细节
现在,这在 CPU 层面是如何工作的?
计算机实际上有一个隐式的“临时”变量,在将中间结果写回寄存器之前存储它们。例如,如果你向寄存器加 3(用机器语言伪代码):
ADD 3 A // add 3 to register A
这个 ALU (算术逻辑单元)实际上执行指令3+A。它接收输入(3,A)并产生结果(3 + A),然后CPU将结果存回A的原始寄存器。因此,在得到最终答案之前,我们将ALU用作临时暂存空间。
我们理所当然地认为ALU有隐式的临时数据,但它始终存在。类似地,在 x = x xor y的情况下,ALU可以返回XOR的中间结果,此时CPU将其存储到x的原始寄存器中。
因为我们不习惯考虑那个被忽视的、可怜的ALU,所以XOR交换看起来神奇,因为它没有显式的临时变量。有些机器有一条一步交换的XCHG指令,用于交换两个寄存器。
延伸阅读: