1.知识储备-异或
英文:“exclusive OR”,缩写:“xor”,数学符号:“⊕”,C语言符号:“^”
运算规则:如果a、b的两个值不相同,则结果为1。如果a、b两个值相同,结果为0。
记忆小技巧:异或的名字是“异或”,注意名字开头是“异”,既然要相异,那么它的规则就是相异才输出1,否则为0。“同或”可以类比,相同才会输出1,否则为0。
性质:
(1)交换律: a ^ b= b ^ a
(2)结合律: ( a ^ b) ^ c = a ^ ( b ^ c )
(3)自反性:a ^ b ^ b = a 【由结合律: a^ b ^ b = a^ ( b ^ b ) = a ^ 0 = a】
从自反性可以看出,一个数和它本身进行异或,结果为零,如 a ^ a = 0;任何数和零进行异或,结果为这个任何数,如 a ^ 0 = a。下边的分析中将会频繁使用。
2.解析
计算机中,所有数据以二进制补码的形式进行存储,为了便于显示,将二进制数转化为十六进制数在内存中进行显示。所以在编译程序时,查看内存中的数据有十六进制的形式,但实际存储的数据还是原始的二进制补码形式。异或其实是在二进制数据(补码)之间进行的。如在C语言中,假设有:
int a = 3;
int b = 5;
本例的int a和int b类型分别占四个字节,即32个比特位。在计算机中,
将二进制的 a 和 b 按照从低位到高位的顺序进行按位异或。首先计算 a ^ b 的结果的最低位,a中是1,b中是1,1和1相同,那么异或结果 a ^ b 的最低位就是0,如上图所示。依次向高位进行按位异或,得到结果。
注意C语言中有符号位和无符号位的区别,在计算机中进行存储时,如本例中的int 3和int 5,都是正数,默认有符号位,最高位的0表示这个数字是正数,如果看到1,表示该数为负数。计算机中都是存储数据的二进制补码形式,负数写成补码形式进行运算分析后,记得恢复成原码查看数值。关于原码、反码和补码的知识请自行查找。
3.代码解释
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("before: a = %d, b = %d\n", a, b);
a = a ^ b;//A -- a1 = a ^ b = 6
b = a ^ b;//B -- b1 = a1 ^ b = (a ^ b) ^ b = a ^ b ^ b = a = 3
a = a ^ b;//A -- a2 = a1 ^ b1 = (a ^ b) ^ ( (a ^ b) ^ b ) = a ^ a ^ b ^ b ^ b = b = 5
printf("after : a = %d, b = %d\n", a, b);
return 0;
}
这里解释会偏向小白,没耐心的可以直接再找一篇文章了解。
核心代码为如下三行。其中最为重要的是【存储了a和b原始数据的异或结果的第一行代码: a = a ^ b】。后续所有对 a 和 b 数据的恢复都少不了这个结果,这句代码也是整个代码的灵魂所在。
a = a ^ b;//a1 = a ^ b = 6
b = a ^ b;//b1 = a1 ^ b = (a ^ b) ^ b = a ^ b ^ b = a = 3
a = a ^ b;//a2 = a1 ^ b1 = (a ^ b) ^ ( (a ^ b) ^ b ) = a ^ a ^ b ^ b ^ b = b = 5
C语言中,如本次代码示例中的 int a 和 int b 只是在内存中开辟出的两个整形存储空间,他们的名字分别记作 a 和 b ,但其中存放的数据内容不一定是原来初始化时定义 3 和 5 ,要结合后边的代码才能看到详细的数值变化和变化的过程。
为了便于理解,粗略类比,接下来将空间 a 和 b 分别取个新名字 A 和 B,初始化时给的数据分别记为 a(=3) 和 b(=5),便于后续的分析和理解。空间可以理解为酒店的房间,数据可以理解为住房的客人,一旦酒店开始营业,房间位置不会发生变化,但居住的客人一直在流动。铁打的存储空间,流水的数据。
题目转化:现有 A 和 B 两个房间,其中居住的客人分别为 a 和 b ,现要求客人 a 和 b 互换房间,即 A 房间居住客人 b ,B 房间居住客人 a 。
开始有: A -- a -- 3 B -- b -- 5
接下来逐行分析这三行代码。
a = a ^ b;//A -- a1 = a ^ b = 6
b = a ^ b;//B -- b1 = a1 ^ b = (a ^ b) ^ b = a ^ b ^ b = a = 3
a = a ^ b;//A -- a2 = a1 ^ b1 = (a ^ b) ^ ( (a ^ b) ^ b ) = a ^ a ^ b ^ b ^ b = b = 5
第一行:其实是为了存储原始数据 a 和 b 的异或结果 6 ,这个结果非常重要!!!结果放在a1 中,a1 相当于空间 A 的另一个名字,a1 等价空间 A 。原代码等价于:
(A)a1 = a ^ b = 6
计算结束后,A空间内的数据产生变化,从 a 变为了 a1 ,从数值上看,从 3 变为 6 ;B空间内的数据 b 未变化,还是 5 。
A (a1) = 6 B -- b = 5
第二行:得到原始数据 a ,将数据 a 的结果放在 b1 ,b1相当于空间 B 的另一个名字,b1等价空间 B 。这里如何得到原始数据 a 就很重要了,还记得之前我已经两次强调过的原始数据异或结果 6 的保存吗?此时这个结果就是 a1 的值(6),a1 和 b 进行异或,结果放在 b1 中,原代码等价于:
(B)b1 = a1 ^ b = (a ^ b) ^ b = a ^ b ^ b = a
根据异或的结合律和交换律,先让后边两个 b 进行异或,结果为零,零再与前边的 a 进行异或,结果还是 a 。
我们来看下数值上的变化:
(B)b1 = 6 ^ 5 = 3
到这里已经可以看出,我们通过原始数据的异或结果 a1 ,还原出了原始数据 a ,从而实现了将 B(b1) 空间内的数据更换为 a 。计算完成后,A 空间内的数据 a1 未变化,B(b1) 空间内的数据产生变化,从 b 变为 a 。
A (a1) = 6 B (b1) -- a = 3
第三行:得到原始数据 b ,将数据 b 的结果放在 a2,a2 相当于空间A的又一个新名字,a2 等价空间 A 。这里还是要使用原始数据的异或结果 a1 ,如果之前没有保存这个结果,就无法继续还原得到原始数据 b 。使用 a1 和 b1 进行异或,原代码等价于:
(A)a2 = a1 ^ b1 = (a ^ b) ^ ( (a ^ b) ^ b ) = a ^ a ^ b ^ b ^ b = b
我们来看下数值上的变化:
(A)a2 = 6 ^ 3 = 5
到这里已经可以看出,我们通过原始数据的异或结果 a1,还原出了原始数据 b ,从而实现了将A 空间内数据更换为 b,a2 等价于空间 A ,a2 只是空间 A 的新名字,空间的地址未发生变化。计算完成后,A 空间内的数据产生变化,从 a1 变为 b ;B 空间内的数据未变化,仍为 a 。
A(a2/a1) -- b = 5 B(b1) -- a = 3
代码运行结果:
before: a = 3, b = 5
after : a = 5, b = 3
4.总结
本次介绍过程中,将空间 A 又取了两个新名字 a1 和 a2 ,将空间 B 取了一个新名字 b1 ,这里取新名字的原因主要是为了便于使用数学代换的方法理解连续异或的具体变化过程。后续只要看的多了,写的多了,也就能一眼看出来了。感谢每位读者的耐心观看。
最后再说下我了解到的交换变量内容的三种方法的优劣。
中间变量:使用中间变量很便捷,但最大的问题就是使用了中间变量,其它没啥问题,考官说了,不让用这个方法......
异或:计算过程不适合新手理解,有点绕,第一次看的时候可能没啥概念。优点就是完全不存在栈溢出的问题,因为在计算过程中只涉及到按位异或,即0和1的异或运算,除了优点外也没啥缺点。
加减法:这个稍微提下,(a + b) - a 就得到了原来的b,大致就是这样子,因为涉及到加减运算,其实还是加法运算,当数值非常大时,很容易造成栈溢出的问题,简单理解,你一直给我钱(进行加法运算),给我的钱太多,还没花完(计算结束)就发现这些钱在家里(空间)都堆不下了!!!留下了不争气的眼泪┭┮﹏┭┮