通过异或运算优雅的交换两个变量

在这里插入图片描述

一、常见的交换两个变量的方法

  • 使用临时变量的方法
    这个方法应该是大家喜闻乐见的方式,也是最常用最容易想到的。刚踏入编程世界的小伙伴肯定一来就最先是接触这种方法,先看代码吧:

    #include<iostream>
    using namespace std;
    
    int main(){
          
          
        int a=5;
        int b=15;
        int temp=0;
    
        temp = a;
        a = b;
        b = temp;
        
        cout << "a:" << a << endl;
        cout << "b:" << b << endl; 
    }
    

    这样做的优点就是:简单,思路清晰,最容易理解;
    缺点就是:会使用额外的变量,会占用额外的内存空间;

  • 使用算数的方法
    直接上代码:

    #include<iostream>
    using namespace std;
    
    int main(){
          
          
        int a=5;
        int b=15;
        
        a = a+b;
        b = a - b;
        a = a - b;
    
        cout << "a:" << a << endl;
        cout << "b:" << b << endl; 
    }
    

    你可以看到这种方式就是利用纯数学的算数运算来交换两个变量的值。
    这种方式的优点就是:不会利用额外的变量;
    缺陷就是:逻辑性比较强,理解起来要稍微难一些,会额外增加CPU的运算量,两个值相加有溢出的风险;
    这种法式还有变形:
    例如使用乘除法来实现:

    #include<iostream>
    using namespace std;
    
    int main(){
          
          
        int a=5;
        int b=15;
    
        a = a*b;
        b = a / b;
        a = a / b;
    
        cout << "a:" << a << endl;
        cout << "b:" << b << endl; 
    }
    

    其实这种变形是得不偿失的,因为在计算机当中乘法除法最后都是转换为加法来计算的,所以会多绕一圈,造成不必要的开销;

  • 使用指针的方式

    #include<iostream>
    using namespace std;
    
    void num_swap(int *n1,int *n2){
          
          
        int t = *n1;
        *n1 = *n2; 
        *n2 = t;
    }
    
    int main(){
          
          
        int a = 5;
        int b = 15;
        num_swap(&a,&b);
        cout << "a:" << a << endl;
        cout << "b:" << b << endl; 
    }
    

    这种方法其实和前面第一种的方式差不多,只是我们有时候需要将这个功能封装成函数,所以就需要利用到指针才能通过形参改变实参的效果。
    这种方式的优点就是:提高了代码的复用性,一次定义可以多次利用;
    缺点就是:有额外的内存开销,函数调用也会有开销,对初学者并不友好很容易模糊概念;
    这种方式和C++98 中STL标准库的 swap() 函数类似,只不过后者是采用的引用的方式实现的;

二、什么是异或?

  • 百度百科的解释:
    异或,英文为exclusive OR,缩写成xor
    异或(xor)是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。其运算法则为:
    a⊕b = (¬a ∧ b) ∨ (a ∧¬b)
    如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
    异或也叫半加运算,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假,则异或的运算法则为:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。

  • 通俗的理解
    如果两个比特位上对应的值不相同则结果位1,否则就为0。没错描述就是这么简单粗暴

  • 异或的真值表

    1 0
    1 0 1
    0 1 0
  • 异或的交换律
    a ⊕ b = b ⊕ a a \oplus b=b \oplus a ab=ba
    推导过程中符号的含义:
    a ˉ \bar{a} aˉ 表示 非 a
    ¬ \lnot ¬ 也表示 逻辑非
    ∨ \lor 表示逻辑或
    ∧ \land 表示逻辑与
    交换律可以通过下面的方式来进行推导:
    根据异或的含义,可以将异或转换成下面的等价形式
    (其实不加小括号也是一样的,因为逻辑运算是由运算符优先级的,这里加上括号方便大家理解)
    a ⊕ b = ( a ˉ ∧ b ) ∨ ( a ∧ b ˉ ) a \oplus b=(\bar{a} \land b) \lor (a \land \bar{b}) ab=(aˉb)(abˉ)
    b ⊕ a = ( b ˉ ∧ a ) ∨ ( b ∧ a ˉ ) b \oplus a=(\bar{b} \land a )\lor (b \land \bar{a}) ba=(bˉa)(baˉ)
    根据 逻辑与 和 逻辑或 的交换律可以将 b ⊕ a b \oplus a ba 变成:
    b ⊕ a = ( a ˉ ∧ b ) ∨ ( a ∧ b ˉ ) b \oplus a=(\bar{a} \land b) \lor (a \land \bar{b}) ba=(aˉb)(abˉ)
    所以就有:
    a ⊕ b = b ⊕ a a \oplus b=b \oplus a ab=ba

  • 异或的结合律
    ( a ⊕ b ) ⊕ c = a ⊕ ( b ⊕ c ) (a \oplus b) \oplus c=a \oplus (b \oplus c) (ab)c=a(bc)
    推导过程:
    ( a ⊕ b ) ⊕ c = ( ( a ˉ ∧ b ) ∨ ( a ∧ b ˉ ) ) ⊕ c (a \oplus b) \oplus c=((\bar{a} \land b) \lor (a \land \bar{b})) \oplus c (ab)c=((aˉb)(abˉ))c
    = ( ¬ ( ( a ˉ ∧ b ) ∨ ( a ∧ b ˉ ) ) ) ∧ c ∨ ( ( a ˉ ∧ b ) ∨ ( a ∧ b ˉ ) ) ∧ c ˉ =(\lnot ((\bar{a} \land b) \lor (a \land \bar{b}))) \land c \lor ((\bar{a} \land b) \lor (a \land \bar{b})) \land \bar{c} =(¬((aˉb)(abˉ)))c((aˉb)(abˉ))cˉ
    根据摩尔定理:
    ¬ ( a ∧ b ) = ( ¬ a ) ∨ ( ¬ b ) ¬ ( a ∨ b ) = ( ¬ a ) ∧ ( ¬ b ) \lnot (a \land b)=(\lnot a) \lor (\lnot b)\\ \lnot(a \lor b)=(\lnot a) \land (\lnot b) ¬(ab)=(¬a)(¬b)¬(ab)=(¬a)(¬b)
    就可以得到:
    = ( ( ( ¬ ( a ˉ ∧ b ) ) ∧ ( ¬ ( a ∧ b ˉ ) ) ) ∧ c ∨ ( ( a ˉ ∧ b ) ∨ ( a ∧ b ˉ ) ) ∧ c ˉ =(((\lnot (\bar{a} \land b)) \land (\lnot(a \land \bar{b}))) \land c \lor ((\bar{a} \land b) \lor (a \land \bar{b})) \land \bar{c} =(((¬(aˉb))(¬(abˉ)))c((aˉb)(abˉ))cˉ
    再继续使用摩尔定理:
    = ( ( a ∨ b ˉ ) ∧ ( a ˉ ∨ b ) ) ∧ c ∨ ( ( a ˉ ∧ b ) ∨ ( a ∧ b ˉ ) ) ∧ c ˉ =((a \lor \bar{b}) \land (\bar{a} \lor b)) \land c \lor ((\bar{a} \land b) \lor (a \land \bar{b})) \land \bar{c} =((abˉ)(aˉb))c((aˉb)(abˉ))cˉ
    根据 逻辑与 和 逻辑或 的分配律结合律可得:
    = ( ( a ∨ b ˉ ) ∧ ( a ˉ ∨ b ) ) ∧ c ∨ ( ( a ˉ ∧ b ∧ c ˉ ) ∨ ( a ∧ b ˉ ∧ c ˉ ) ) =((a \lor \bar{b}) \land (\bar{a} \lor b)) \land c \lor ((\bar{a} \land b \land \bar{c}) \lor (a \land \bar{b} \land \bar{c})) =((abˉ)(aˉb))c((aˉbcˉ)(abˉcˉ))
    同理也可以得:
    = ( ( a ∧ a ˉ ) ∨ ( a ∧ b ) ∨ ( b ˉ ∧ a ˉ ) ∨ ( b ˉ ∧ b ) ) ∧ c ∨ ( ( a ˉ ∧ b ∧ c ˉ ) ∨ ( a ∧ b ˉ ∧ c ˉ ) ) =((a \land \bar{a}) \lor (a \land b) \lor (\bar{b} \land \bar{a}) \lor (\bar{b} \land b)) \land c \lor ((\bar{a} \land b \land \bar{c}) \lor (a \land \bar{b} \land \bar{c})) =((aaˉ)(ab)(bˉaˉ)(bˉb))c((aˉbcˉ)(abˉcˉ))
    显然 a ∧ a ˉ = 0 ,   b ˉ ∧ b = 0 a \land \bar{a}=0,\ \bar{b} \land b=0 aaˉ=0, bˉb=0
    而且逻辑运算中这样一个性质: a ∨ 0 = a a \lor 0=a a0=a
    所以上面的式子可以化简为:
    = ( ( a ∧ b ) ∨ ( b ˉ ∧ a ˉ ) ) ∧ c ∨ ( ( a ˉ ∧ b ∧ c ˉ ) ∨ ( a ∧ b ˉ ∧ c ˉ ) ) =((a \land b) \lor (\bar{b} \land \bar{a})) \land c \lor ((\bar{a} \land b \land \bar{c}) \lor (a \land \bar{b} \land \bar{c})) =((ab)(bˉaˉ))c((aˉbcˉ)(abˉcˉ))
    根据逻辑与的分配律则有:
    = ( a ∧ b ∧ c ) ∨ ( b ˉ ∧ a ˉ ∧ c ) ∨ ( ( a ˉ ∧ b ∧ c ˉ ) ∨ ( a ∧ b ˉ ∧ c ˉ ) ) =(a \land b \land c) \lor (\bar{b} \land \bar{a} \land c) \lor ((\bar{a} \land b \land \bar{c}) \lor (a \land \bar{b} \land \bar{c})) =(abc)(bˉaˉc)((aˉbcˉ)(abˉcˉ))
    和上面的过程一样我们可以来变换 a ⊕ ( b ⊕ c ) a \oplus (b \oplus c) a(bc)
    也可以得到(由于是重复的操作所以就不板书了,有兴趣的可以自行再推导一遍):
    a ⊕ ( b ⊕ c ) = ( a ∧ b ∧ c ) ∨ ( b ˉ ∧ a ˉ ∧ c ) ∨ ( ( a ˉ ∧ b ∧ c ˉ ) ∨ ( a ∧ b ˉ ∧ c ˉ ) ) a \oplus (b \oplus c)=(a \land b \land c) \lor (\bar{b} \land \bar{a} \land c) \lor ((\bar{a} \land b \land \bar{c}) \lor (a \land \bar{b} \land \bar{c})) a(bc)=(abc)(bˉaˉc)((aˉbcˉ)(abˉcˉ))
    所以就有:
    ( a ⊕ b ) ⊕ c = a ⊕ ( b ⊕ c ) (a \oplus b) \oplus c=a \oplus (b \oplus c) (ab)c=a(bc)

  • 任何数异或 0 还是其本身
    a ⊕ 0 = a a\oplus0=a a0=a

  • 任何数异或其本身都等于 0
    a ⊕ a = 0 a \oplus a=0 aa=0
    后面两个个性质很简单,直接根据异或的定义就可以知道。

  • 我们再来到程序中看看

    #include<iostream>
    #include<bitset>
    
    using namespace std;
    
    void show_XOR(int a,int b){
          
          
        // bitset<n> 表示显示n位二进制数,不足补 0
        cout << to_string(a) + ":\t" << bitset<4>(5) << endl;
        cout << to_string(b) + ":\t" << bitset<4>(7) << endl;
        cout << to_string(a) + "^" + to_string(b)+ ":\t" << bitset<4>(5^7) << endl;
    }
    
    int main(){
          
          
        show_XOR(5,13);
    }
    

    输出结果:

    5:      0101
    13:     0111
    5^13:   0010
    

三、使用异或的方式交换变量

  • 既然已经了解的异或的相关概念了,那我们该如何使用异或交换变量呢?

  • 先别急,我们先来看看下的例子:
    5    ( 0101 ) 2 5 \ \ (0101)_2 5  (0101)2
    13 ( 0111 ) 2 13(0111)_2 13(0111)2
    我们可以轻易的算出:
    ( 0101 ) 2 ⊕ ( 0111 ) 2 = ( 0010 ) 2 (0101)_2 \oplus (0111)_2=(0010)_2 (0101)2(0111)2=(0010)2
    然后我们再来看看这样又会如何:
    ( 0101 ) 2 ⊕ ( 0010 ) 2 = ( 0111 ) 2 = 13 (0101)_2\oplus(0010)_2=(0111)_2=13 (0101)2(0010)2=(0111)2=13
    ( 0111 ) 2 ⊕ ( 0010 ) 2 = ( 0101 ) 2 = 5 (0111)_2\oplus(0010)_2=(0101)_2=5 (0111)2(0010)2=(0101)2=5
    不妨我们用字母来代替上面的所有式子:
    c = a ⊕ b c=a \oplus b c=ab
    a ⊕ c = b a \oplus c =b ac=b
    b ⊕ c = a b \oplus c=a bc=a
    你可使用任意数做测试,上面的式子都是成立的,这样的结果是不是很惊讶,但这是为什么呢?
    其实原理很简单,两个数异或之后的结果存放了两个数字每一个对应二进制位上(相同还是不同 1-> 不同 0->相同)的信息,
    所以它能够通过一方计算出另外一方的值;
    有了这个概念那我们实现交换变量就简单了;

    #include<iostream>
    using namespace std;
    
    int main(){
          
          
        int a = 10;
        int b = 20;
        int temp = a ^ b;
        a = temp ^ a;
        b = temp ^ b;
        cout << a << '\t' << b;
    }
    

    上面最直接的 一种方式,但是这显然不是我们想要的,因为代码中同样用到了临时变量,那就和和先前将的方法无异了,

  • 如何摆脱这样的束缚呢?分析代码可以得知,在运行过程当中,我们的临时变量temp是从初始化以来,后面就没有被再被赋值过,后面都是读,值是没有改变的。既然如此那我们何不就用原来a,b中的某个变量来存储 a^b 的结果呢。不要担心信息会丢失,因为我们知道如果想要保存 a 和 b的值,我们只需从 (a , b , a ^ b) 它们三个中任选两个出来就可以保存全部的信息了(因为可以相互转换),所以我们就可以用下面的方式进行交换:

    #include<iostream>
    using namespace std;
    
    int main(){
          
          
        int a = 10;
        int b = 20;
        a = a ^ b;
        // 通过异或计算出原先 a 的值
        b = a ^ b;
        // 现在的 b 已经变成以前的 a 了
        b = a ^ b; 
        cout << a << '\t' << b;
    }
    

    上面已经够简洁的了,当然还可以有更简洁的表达方式:

    #include<iostream>
    using namespace std;
    
    int main(){
          
          
        int a = 10;
        int b = 20;
        a^=b^=a^=b;
        cout << a << '\t' << b;
    }
    

    这样做虽然代码上看起来非常简洁,但是通用性并不高,因为在不同的编译器上等式的运算顺序是不同的所以会存在不兼容的风险,而且代码易读性也不强,但是执行效率还是很高。

猜你喜欢

转载自blog.csdn.net/qq_42359956/article/details/106093527