在学习算法过程中,第一次接触到了位运算异或及其一些用法,感觉非常妙,写一篇文章来整理一下。

异或:

性质:a^a=0;
     a^0=a;
     满足交换律和结合律

下面我们来看一道妙用异或的题:

  • 给定一个数组,这个数组中有一个数字出现过奇数次,而其他数字出现过偶数次,找出这个数字。
  • 思路:根据异或的结合律,可以将数组中所有数字从头到尾进行异或,偶数次的数字两两异或为0,最后只剩下出现奇数次的数字。

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

下面我们看一下这道题的升级版

  • 给定一个数组,其中有两个出现奇数次的数字,其余均出现偶数次,找出这两个数字。
  • 思路:同样根据异或的结合律,把数组从头到尾异或一遍,这里假设两个奇数次数字为a和b,则异或结果eor=a^b,a和b一定不相等,则eor一定不为零,则eor至少有一位为1,假设是第n位(选择eor中最靠右的那个1,原因后面会说明),下面,根据第n位,对整个数组进行分类。
第n位为0第n位为1
部分偶数次数部分偶数次数
ab

于是,我们可以对某一类进行全部异或,比如在第n位为1类上进行异或,最后结果为eor1=b,另一个数b自然可通过eor^eor1得到。

public static int[] xor2(int[] arr) {
        int eor = 0;
        for (int i : arr) {
            eor ^= arr[i];        //->  eor = a^b
        }
        int rightOne = eor & (~eor + 1);
        int eor1 = 0;
        for (int i = 0; i < arr.length; i++) {
            if ((arr[i] & rightOne) == rightOne) {    //在该位上是1的
                eor1 ^= arr[i];
            }
        }
        return new int[]{eor1, eor1 ^ eor};
    }

别小看了这段代码,在这段代码中,还有两个位运算的妙用:

  • int rightOne = eor & (~eor + 1);
    这行代码意为将eor二进制最靠右的1取出来赋给rightOne,目前可先记忆,实际上与补码的性质有关。
  • (arr[i] & rightOne) == rightOne
    这行代码意为筛选出数组中第n位为1的数字。

下面我们来看另一道题:
给定一个数,判断其是否是2的幂次方。

  • 思路:我们可以想到,一个数如果是2的幂次方,例如4,8,16,其二进制必然只有一个1,即100,1000,10000,所以我们只需利用这点去判断即可,如果该数n二进制位上只有一个1,则n&(n-1)==0

    public static boolean if2(int n){
       if((n&(n-1))==0){
           return true;
       }
           return false;
    }
    

位运算的另一个用途,是进行运算优化加速:
接下来介绍我们经典的n皇后问题:

  • 题目:给定一个整数N,要求在N*N的棋盘上摆放N个皇后,且这N个皇后任意两个都不在同一行、同一列、同一斜线上

常规解法:暴力递归尝试,按行尝试,逐个尝试所有位置。
现可使用位运算优化,虽然时间复杂度不变,但其中常数大大降低,可有效提高效率。

假设现在n=8,则可使用8位二进制数代替当前皇后所在位置
我们假设在第一行这样放:00001000(1代表皇后所在位置)
则,对于第二行,我们即可给定一串数字去限定皇后可放的位置

行数二进制数字限制
100001000
200011100
300111110

上表中2、3行1位置即皇后不可放的位置(共列、共斜线),选取1作为限制位置的原因是可以通过上一行的简单移位得到。以这种策略进行下去可有效提升效率。

此外,位运算还有许多妙用,比如位图、布隆过滤器等,在以后的文章中会重点介绍。


Echo
2 声望1 粉丝