C# 异或运算原理及加密、交换、备份等应用
异或,英文为exclusive OR,缩写成xor。异或(xor)是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。其运算法则为:
a⊕b = (¬a ∧ b) ∨ (a ∧¬b)(¬为非)
异或也叫半加运算,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假,则异或的运算法则为:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。
运算法则:
1. a ^ a = 0
2. a ^0 = a
3. a^ b = b ^a
4. a ^b ^ c = a ^ (b ^ c) = (a^ b) ^ c;
5. d = a ^b ^ c 可以推出 a = d ^ b ^ c.
6. a^ b ^ a = b.(可由3推出)
按位异或的几个常见用途:
c语言中使用^来表示异或运算(很多语言^表示乘方)
我们先来计算一下这个问题,5^3=?
5和3转为二进制分别为:0101 、0011,所以0101^0011=0110,换为十进制即为6。
6^3=0110^0011=0101,化为10进制即为5,也验证了上面第4条运算法则
常见应用
(1)判断两个数是否相等
一个数与自身异或,总是为 0,我们可以使用这一点来判断两个变量是否相等
( a ^ b ) == 0
当 a
和 b
相等时,表达式为真,否则为假
(2)不使用临时变量交换两个数的值
要交换两个数的值,通常做法是借助一个临时变量,然后再进行交换,比如:tmp
是临时变量,现需要交换 a
和 b
两个变量值,过程如下
tmp = a a = b b = tmp;
利用异或的一些性质,不用临时变量也能实现交换两个数的值,具体过程如下
a = a ^ b b = a ^ b a = a ^ b
(3) 使某些特定的位翻转
例如对数10100001的第2位和第3位翻转,则可以将该数与00000110进行按位异或运算。
10100001^00000110 = 10100111
(4) 实现两个值的交换,而不必使用临时变量。
例如交换两个整数a=10100001,b=00000110的值,可通过下列语句实现:
a = a^b; //a=10100111
b = b^a; //b=10100001
a = a^b; //a=00000110
(5)简化表达式
根据交换性,可以优化表达式中重复变量的异或运算,比如:表达式 a ^ b ^ c ^ a ^ b
可以做如下简化
a ^ b ^ c ^ a ^ b # 根据 x ^ y = y ^ x = ( a ^ a ) ^ ( b ^ b ) ^ c # 根据 x ^ x = 0 = 0 ^ 0 ^ c # 根据 x ^ 0 = x = c
(6)加密
利用异或运算加密是很常见的加密手段,它涉及到三个变量:明文、密钥、密文,假如它们分别记为 plain_text、 encrypt_key、 cipher_text
明文和密钥进行异或运算,可以得到密文
plain_text ^ encrypt_key = cipher_text
密文和密钥进行异或运算,可以得到明文
cipher_text ^ encrypt_key = ( plain_text ^ encrypt_key ) ^ encrypt_key = plain_text ^ ( encrypt_key ^ encrypt_key ) # 根据 x ^ ( y ^ z ) = ( x ^ z ) ^ y = plain_text ^ 0 # 根据 x ^ x = 0 = plain_text
异或(XOR)运算加密/解密算法原理:
从加密的主要方法看,换位法过于简单,特别是对于数据量少的情况很容易由密文猜出明文,而替换法不失为一种行之有效的简易算法。
从各种替换法运算的特点看,异或运算最适合用于简易加解密运算,这种方法的原理是:当一个数A和另一个数B进行异或运算会生成另一个数C,如果再将C和B进行异或运算则C又会还原为A。
相对于其他的简易加密算法,XOR算法的优点如下。
(1)算法简单,对于高级语言很容易能实现。
(2)速度快,可以在任何时候、任何地方使用。
(3)对任何字符都是有效的,不像有些简易加密算法,只对西文字符有效,对中文加密后再解密无法还原为原来的字符。
如果密钥是随机的(不重复),而且与消息长度相同,异或密码就会更为安全。当密钥流由伪随机数发生器生成时,结果就是流密码。若密钥是真正随机的,结果就是一次性密码本,这种密码在理论上是不可破解的。
在这些密码的任何部分中,密钥运算符在已知明文攻击下都是脆弱的,这是因为明文
(7)备份
根据异或的性质,异或运算还可以用于文件的备份
现有两个文件 filea
和 fileb
,它们进行异或运算,会产生一个新的备份文件 bakfile
bakfile = filea ^ fileb
根据异或的性质,可以通过 bakfile和 filea得到 fileb,或者通过 bakfile和 fileb得到 filea
后面无论是 filea或 fileb文件损坏了,只要不是两个文件同时损坏,都可以通过两者中未损坏的文件 和 bakfile文件,还原得到另一个文件
当 filea文件损坏了,可以通过 fileb和 bakfile进行异或运算得到完好的 filea文件
fileb ^ bakfile = fileb ^ ( filea ^ fileb ) = fileb ^ filea ^ fileb # 根据 x ^ ( y ^ z ) = ( x ^ z ) ^ y = fileb ^ fileb ^ filea # 根据 x ^ x = 0 = 0 ^ filea # 根据 x ^ 0 = x = filea
同样,当 fileb
文件损坏了,可以通过 filea
和 bakfile
进行异或运算得到完好的 fileb
文件
应用举例
(1)最后举一个小例子,leetcode上的习题:
给定一个包含 0, 1, 2, ..., n 中 n 个数的序列,找出 0 .. n 中没有出现在序列中的那个数。
例如:输入: [3,0,1] 输出: 2
输入: [9,6,4,2,3,5,7,0,1] 输出: 8
这道题方法较多,比如们可以求出 [0..n]的和,减去数组中所有数的和,就得到了缺失的数字。
int missingNumber(vector<int>& nums) {
int n=nums.size();
int sum=n*(n+1)/2;
for(int i=0;i<n;i++)sum-=nums[i];
return sum;
}
我们还可以使用位运算的方法,由于异或运算(XOR)满足结合律,并且对一个数进行两次完全相同的异或运算会得到原来的数,因此我们可以通过异或运算找到缺失的数字。
我们知道数组中有 n 个数,并且缺失的数在 [0..n]中。因此我们可以先得到 [0..n] 的异或值,再将结果对数组中的每一个数进行一次异或运算。未缺失的数在 [0..n] 和数组中各出现一次,因此异或后得到 0。而缺失的数字只在 [0..n]中出现了一次,在数组中没有出现,因此最终的异或结果即为这个缺失的数字。代码如下:
最快捷的解决方法是把数组的所有元素以及 0到 n-1之间的整数 全部进行异或运算
arry[0] ^ arry[1] ^ arry[2] ... ^ arry[n-2] ^ 0 ^ 1 ^ 2 .... ^ (n-1)
由于数组元素值范围在 0到 n-1,且所有元素值都没有重复
所以,上述的计算式子中,0到 n-1每个数会出现两次,只有缺少的那个数仅出现一次,根据异或的性质 x ^ x = 0可知,相同值之间的异或运算等于 0,因此算式的结果其实就是缺少的那个数
(2)
1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现
一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空
间,能否设计一个算法实现?
解法二(异或)
异或就没有这个问题,并且性能更好。
将所有的数全部异或,得到的结果 与 1 ^ 2 ^ 3 ^ … ^ 1000的结果进行异或,得到的结果就是重复数。
但是这个算法虽然很简单,但证明起来并不是一件容易的事情。这与异或运算的几个特性有关系。
证明
首先是异或运算满足交换律、结合律。
所以,1^2^...^n^...^n^...^1000,无论这两个n出现在什么位置,都可以转换成为1^2^...^1000^(n^n)的形式。
其次,对于任何数x,都有x^x=0,x^0=x。
所以1^2^...^n^...^n^...^1000 = 1^2^...^1000^(n^n)= 1^2^...^1000^0 = 1^2^...^1000(即序列中除了n的所有数的异或)。
令,1^2^...^1000(序列中不包含n)的结果为T
则1^2^...^1000(序列中包含n)的结果就是T^n。
T^(T^n)=n。
所以,将所有的数全部异或,得到的结果与1^2^3^...^1000的结果进行异或,得到的结果就是重复数。
当然有人会说,1+2+…+1000的结果有高斯定律可以快速计算,但实际上1 ^ 2 ^ … ^ 1000的结果也是有规律的,算法比高斯定律还该简单的多。