异或高效算法

聆听 沉淀 传播… 关注微信公众号【架构技术之美】,学习更多技术和学习资料

Ⅰ 异或运算

  • 1 ^ 0 = 1
  • 1 ^ 1 = 0
  • 0 ^ 0 = 0
  • 0 ^ 1 = 1
  • 0 ^ N = N
  • N ^ N = 0
  • 异或满足结合律和交换律:A ^ B = B ^ A;A ^ B ^ C = A ^ (B ^ C)

Ⅱ 异或实现两个数交换

正常实现2个数的交换,我们可以会借助第三个临时变量来实现:

private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

当然也可以通过异或来实现,不用借助第三个临时变量:

private static void swap(int[] arr, int i, int j) {
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

但是你知道为何通过3次异或运算就实现了2个数交换呢? 异或交换2个数有哪些坑呢?

原理:假设a的值为v1,b的值的为v2
a = a ^ b = v1 ^ v2
b = a ^ b = v1 ^ v2 ^ v2 = v1 ^ (v2 ^ v2) = v1 ^ 0 = v1
a = a ^ b = v1 ^ v2 ^ v1 = (v1 ^ v1) ^ v2 = 0 ^ v2 = v2

但是如何交换的2个数指向的是同一个数,会出现什么情况呢?假设a和b的值都为v。
a = a ^ b = v ^ v = 0,此时b也变为0了。
b = a ^ b = 0 ^ 0 = 0
a = a ^ b = 0 ^ 0 = 0
最终会导致a和b的值都变为0,出现错误。
所以用异或实现2个数交换的前提条件是2个数不能指向同一个数据(内存)。

Ⅲ 异或高效解决算法问题

题目1

一个数组中,有一种数只出现了奇数次,其他数都出现了偶数次,如何快速找到这个数?

思路:因为2个相同的数进行异或的值为0,则偶次数的数进行异或最终的值也为0,最后0与那一个多出来的奇数次的数进行异或,值即为这个出现了奇数次的数的值。

package com.nobody.eor;

import java.util.Arrays;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/9/6
 */
public class Code01_ {
    public static void main(String[] args) {
        // 只有4是出现了奇数次的数组
        int[] arr = {1, 2, 6, 4, 1, 2, 1, 1, 4, 4, 6};
        int eor = 0;
        for (int e : arr) {
            // 将所有数进行异或
            eor ^= e;
        }
        System.out.println(eor); // 输出结果为4
    }
}

题目2

对于一个整数,如何提取出二进制形式的最右侧的1?
例如整数20,二进制是00010100,提取最右侧的1,即为00000100。

思路:

  • 简单来说,就是除了最右侧1的位置是1保持不变,其他位置都变为0。
  • 而2个相反的数(0和1)进行与操作值为0,0和0进行与操作值为0,1和1进行与操作值为1。
  • 原数据最右侧的位肯定都为0,取反后都变为1,再加1因为进位原因都会重新变成0。
  • 所以采用算法:N & (~N + 1)
     N = 00010100
    ~N = 11101011
~N + 1 = 11101100
N & (~N + 1) : 00010100
             & 11101100
             = 00000100
package com.nobody.eor;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/9/6
 */
public class Code02 {
    public static void main(String[] args) {
        int N = 20;
        System.out.println(Integer.toBinaryString(N));
        int result = N & (~N + 1);
        System.out.println(Integer.toBinaryString(result));
    }
}
10100
100

题目3

一个数组中,有两种数只出现了奇数次,其他数都出现了偶数次,如何快速找到这个两个数?

思路:

  • 假设这俩个数分别为a和b,因为2个相同的数异或等于0,则将数组所有数进行异或操作后,结果result = a ^ b。
  • 因为是两种奇数,所以a!=b,所以result=a ^ b != 0,那就代表result这个数的二进制数肯定存在1,而且这个1又是a和b异或出来的。那a和b的二进制位对应位置上,它们分别为0和1,才能异或出1。
  • 我们就取最右侧位置(假设 Li )的1来说,那数组中所有数,它们的Li位置要么就是1,要么就是0。而且a和b的Li位置是不一样的(不妨假设a的Li位置是1,b的Li位置是0)。
  • 所以我们可以将数组中的所有数划分为2组,Li位置是1的一组,Li位置是0的一组。相同的数肯定在同一组,即其他偶次数的数在同一组(可以两两异或值为0),所以分别将组内的所有数进行异或,即可找出那2个奇数。
package com.nobody.eor;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/9/6
 */
public class Code03 {
    public static void main(String[] args) {
        // 只有4和11是出现了奇数次的数组
        int[] arr = {1, 2, 6, 4, 1, 2, 1, 1, 4, 4, 6, 11};

        // 假设这两种数为a和b
        // 首先将数组所有数进行异或,结果result = a ^ b;
        // a!=b,则result!=0,故result必有一个位置为1
        int result = 0;
        for (int e : arr) {
            result ^= e;
        }

        // 提取最右侧的1
        int rightOne = result & (~result + 1);

        // 将其中一组(Li位置是1)的数进行异或,
        int eor1 = 0;
        for (int e : arr) {
            if ((e & rightOne) != 0) {
                eor1 ^= e;
            }
        }
        System.out.println("这两个数分别为:" + eor1 + " " + (eor1 ^ result));
    }
}
这两个数分别为:11 4

题目4

如何计算出一个整数的二进制形式中,1的个数?

思路:

  • 只需要从最右侧的1开始往左数,数一个之后,将这个1置为0,再继续数
  • 其实就是每次都提取出最右侧的1,计算+1,然后将这个1置为0,再继续提取1,计数…
package com.nobody.eor;

/**
 * @author Mr.nobody
 * @Description
 * @date 2020/9/6
 */
public class Code04 {
    public static void main(String[] args) {
        int num = 45;
        System.out.println("二进制:" + Integer.toBinaryString(num));
        int count = 0;
        while (num != 0) {
            int rightOne = num & (~num + 1);
            count++;
            num ^= rightOne;
        }
        System.out.println("1的个数:" + count);
    }
}
二进制:101101
1的个数:4

猜你喜欢

转载自blog.csdn.net/chenlixiao007/article/details/108428828