算法之异或运算及其应用

算法之异或运算及其应用

基本介绍

异或算法又可称为无进位加法

1 ^ 1 = 0 ( 1 + 1 = 10 ,如果不进位的话,那结果就是0 )

1 ^ 0 = 1 ( 1 + 0 = 1 )

0 ^ 1 = 1 ( 0 + 1 = 1 )

0 ^ 0 = 0 ( 0 + 0 = 0 )

特性

满足交换律和结合律,表明计算结果和异或顺序无关

N ^ 0 = N N ^ N = 0

应用 - 快速交换值

1. 代码实现

交换 a 与 b 的值

  private void swap(int a, int b) {
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;
    }

2. 好处

按照上述方式进行值的交换,就无需开辟一个新的空间(不用创建一个变量来辅助进行值的交换)

3. 原理说明

需要使用到的知识为:

异或运算结果和顺序无关

N ^ 0 = N N ^ N = 0

说明如下:

譬如 a = 甲,b = 乙

① a = a ^ b => 此时 a = 甲 ^ 乙 , b = 乙

② b = a ^ b => 此时 a = 甲 ^ 乙 , b = 甲 ^ 乙 ^ 乙 = 甲 ^ 0 = 甲

③ a = a ^ b => 此时 a = 甲 ^ 乙 ^ 甲 = 甲 ^ 甲 ^ 乙 = 0 ^ 乙 = 乙 , b = 甲

通过分析,我们可以得到,经过这三步后, a 和 b 的值确实交换了

4. 使用前提

使用这个看似很高逼格的交换前提是 a 和 b 不能指向同一空间 ( 它们的值可以相等,但不能指向同一空间 ), 不然 a 和 b 就都会变成 0 ,因为此时 a ^ b = a ^ a = 0

你没办法确保传进来的两个值一定是不同的( 在排序算法及其应用2 - 冒泡排序中我们曾用过这个交换方法,是因为我们确定传进来的两个数肯定不是指向同一空间的 ),所以这种抖机灵的写法是不推荐的,日常最好还是按照我们一般的交换方式来写

算法题

1. 问题

有一个数组[]

① 数组中只有一种数字出现奇数次,其他数字都出现偶数次

② 数组中有两种数字出现奇数次,其他数字都出现偶数次

找到出现奇数次的数字

要求: 时间复杂度为 O(n), 空间复杂度为 O(1)

2. 思路

对于第 ① 种情况,所求的奇数次的数字即为数组中每个元素进行异或后的结果

证明如下:

由异或运算的特性:运算顺序不影响异或结果,所以出现偶数次的数字经过异或后就会变成0,所以结果自然只剩下出现奇数次的数字

举个栗子: 比如 arr = {1, 3, 9, 6, 6, 3, 3, 1, 3} [ 其中唯一出现奇数次的数字为 9 ]

将所有元素异或后得到 1 ^ 3 ^ 9 ^ 6 ^ 6 ^ 3 ^ 3 ^ 1 ^ 3 = 1 ^ 1 ^ 3 ^ 3 ^ 3 ^ 3 ^ 6 ^ 6 ^ 9 = 0 ^ 9 = 9

对于第 ② 种情况,会比第一种难一些,实现思路如下:

首先将数组中每个元素进行异或,得到的结果就是两个奇数次的数字的异或,将这个结果赋值给 eor

由于这两个数字不一样,所以 eor != 0,这两个数字的二进制肯定有一位是不同的

假设它们的第 8 位不同,即得到的异或结果的第 8 位为 1,我们让 eor' = “第 8 位为 1,其他都为 0”,而数组中所有元素可以分为两组, 一组为 ‘第 8 位为 0’,另一组为 ‘第 8 位为 1’,而我们所要求的这两个数分别在这两组中( 不可能为同一个组,因为上面已经假设他们第 8 位是不同的,所以必然一个为 1,一个为 0 ) ,并且每一组中除了要求的两个数,其他数都是偶数次的

我们将数组元素分别与 eor' 进行 & 运算,将第 8 位上等于 1 (或 0)的数字筛选出来,进行异或运算,这样就可以得到其中一个出现奇数次的数 a

再将 eor 与 a 进行异或,即可得到另外一个出现奇数次的数 b

听上去是不是有些抽象,那我们来举个栗子~

比如 arr = {1, 3, 9, 6, 6, 3, 3, 1, 3, 7} [ 其中唯一出现奇数次的数字为 9 和 7 ]

第一步,先将所有元素进行异或: eor = 1 ^ 3 ^ 9 ^ 6 ^ 6 ^ 3 ^ 3 ^ 1 ^ 3 ^ 7 = 1 ^ 1 ^ 3 ^ 3 ^ 3 ^ 3 ^ 6 ^ 6 ^ 9 ^ 7 = 0 ^ 9 ^ 7= 9 ^ 7 = 14

9 的二进制 1 0 0 1

7 的二进制 ^ 1 1 1

​ -----------------

​ 1 1 1 0 ( 化为十进制就是14 )

第二步,我们看到 eor 的二进制结果中至少有一位不为 0,我们选出不为 0 的一位,譬如第 2 位, 那么 eor' = "第 2 位为1,其他都为 0" = 2

第三步,将 eor' 分别与数组元素进行 & 运算,就可以将元素分为了两组

1, 1 | 3, 3, 3, 3

9 | 6, 6

​ | 7

第 2 位为 0 第 2 位为 1

我们取与 eor' 异或结果为 0 的那一组(你想取结果为 0 或结果为 1 的都可以),即 第 2 位为 0 的那一组,将这组的元素进行异或运算,得到结果 a = 1 ^ 1 ^ 9 = 0 ^ 9 = 9

第四步,再将 a 与 eor 进行异或 ,即可得到结果 b = 9 ^ 14 = 7

a 和 b 即是我们要求的值

3. 代码

问题 ① 的代码

  public int getOneNum(int[] arr) {
        int eor = 0;
        for (int i : arr) {
            eor ^= i;
        }
        return eor;
    }

问题 ② 的代码

    public void getTwoNum(int[] arr) {
        int eor = 0;
        for (int i : arr) {
            eor ^= i;
        }
        // eor = 所求两个数的异或结果
        // eor 一定不为0, eor 必然有一位上是 1
        int rightOne = eor & (~eor + 1);    // 提取出最右位的1
        int onlyOne = 0;    // eor'
        for (int cur: arr){
            if ((cur & rightOne) == rightOne) { // 这里目的是为了分组, == 0 也是可以的
                onlyOne ^= cur;
            }
        }
        System.out.println(onlyOne + " " + (eor ^ onlyOne));
    }

说明:其中 int rightOne = eor & (~eor + 1); // 提取出最右位的1 用于得到某个数最右边位置上的1,是一种常规操作,要学会使用

欢迎大家来我博客逛逛 mmimo技术小栈

posted @ 2021-10-09 22:28  工程小白菜  阅读(466)  评论(0编辑  收藏  举报