使用异或(XOR)交换两个变量

大多数人会使用临时变量交换两个变量 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指令,用于交换两个寄存器。

延伸阅读:

本系列其他文章

  1. 数制(Number Systems)与进制(Bases)
  2. GUID 快速指南
  3. 理解 Quake 的快速平方根倒数(Fast Inverse Square Root)算法
  4. 计算机网络简明入门
  5. 使用异或(XOR)交换两个变量
  6. 理解大端(Big Endian)与小端(Little Endian)字节序
  7. Unicode 与你
  8. 关于二进制文件格式的一点小曲
  9. 排序算法(Sorting Algorithms)

加入 45 万月度读者

喜欢这篇文章?还有更多内容能帮你建立持久、直观的数学理解。加入通讯以获取额外内容和最新更新。