【数据结构与算法】位运算

位运算符

& :与
| :或
^ :异或
~ :非(取反)
>> << :右移(补符号位),左移(补0)
>>> :右移(0补充高位)
对于int型,1<<35与1<<3是相同的,而左边的操作数是long型时需要对右侧操作数模64

image

异或:
可以理解为不进位加法:1+1=0,0+0=0,1+0=1
性质:
1、交换律:a ^ b = b ^ a
2、结合律:(a ^ b) ^ c = a ^ (b ^ c)
3、对于任何数x,都有x ^ x = 0,x ^ 0 = x
4、自反性:a ^ b ^ b = a ^ 0 = a,连续和同一个因子做异或运算,最终结果为自己

应用

判断奇偶数

x & 1 = 1 x是奇数
x & 1 = 0 x是偶数

核心是判断二进制数最后一位是1还是0

交换两个整数变量的值

使用前提:a,b不相等

a = a^b;
b = a^b;
a = a^b;

不用判断语句求整数绝对值

(num^(num>>31))+(num>>>31)

11111111 11111111 11111111 11110111:num
11111111 11111111 11111111 11111111:num>>31
00000000 00000000 00000000 00001000:num^(num>>31)
00000000 00000000 00000000 00000001:num>>>31
00000000 00000000 00000000 00001001:num^(num>>31))+(num>>>31

正数显然满足
负数的相反数是原码含符号位取反加一

消除最低位的1

x&(x-1) :可以使二进制数x的最低位的1变成0;

例题

找出唯一成对的数

1-1000这1000个数放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间,能否设计一个算法实现?

核心:x^x=0

可以强行配凑消去其他元素:这1001个元素连续异或,再异或上1-1000这1000个数。
其他元素都出现2次,异或后变成0.而唯一的一个元素值出现3次,连续异或后值不变。

public static void main(String[] args) {
        int []arr = new int[]{1,2,3,4,4,5};
        int ans = 0;
        for(int i=1;i<=5;i++){
            ans^=i;
        }
        for(int i=0;i<6;i++){
            ans^=arr[i];
        }
        System.out.println(ans);
}

找出落单的数

一个数组里除了某一个数字之外,其他的数字都出现了两次。请写程序找出这个只出现一次的数字。

public static void main(String[] args) {
        int []arr = new int[]{1,1,3,4,4};
        int ans = 0;
        for(int i=0;i<arr.length;i++){
            ans^=arr[i];
        }
        System.out.println(ans);
}

二进制中1的个数

请实现一个函数,输入一个整数,输出该整数二进制表示中1的个数。
例:9的二进制表示为1001,有2位是1

法一:
该二进制数不断无符号右移并与1做与运算,结果为1则count++,直到该数变为0

public static void main(String[] args) {
        int n=-1;
        int count = 0;
        while (n!=0){
            int tmp = n&1;
            if(tmp == 1) count++;
            n = n >>> 1 ;
        }
        System.out.println(count);
    }   //32

法二:
每次使该二进制数的一个为1的二进制位变成0,count++,直到该数变成0;
x&(x-1) :可以使二进制数的最低位的1变成0;

public static void main(String[] args) {
        int n=-1;
        int count = 0;
        while (n!=0){
            n = n&(n-1);
            count++;
        }
        System.out.println(count);
    }

是不是2的整数次方

用一条语句判断一个整数是不是2的整数次方

也就是一个整数的二进制形式中为1的二进制位是不是只有一个。联系上一题,也就是count是不是为1

public static void main(String[] args) {
        int n=65536;
        System.out.println((n&(n-1))==0);
}

整数的二进制奇偶位交换

先取出偶数位和奇数位,偶数结果右移一位,奇数结果左移一位,最后异或
abababab abababab abababab abababab
a0a0a0a0 a0a0a0a0 a0a0a0a0 a0a0a0a0(ou)
0b0b0b0b 0b0b0b0b 0b0b0b0b 0b0b0b0b(ji)
0a0a0a0a 0a0a0a0a 0a0a0a0a 0a0a0a0a(ou>>>1)
b0b0b0b0 b0b0b0b0 b0b0b0b0 b0b0b0b0(ji<<1)
babababa babababa babababa babababa

public static void main(String[] args) {
    int a = 0b01000000_00000000_00000000_00000000;
    System.out.println(a);
    int b = m(a);
    System.out.println(b);  //0b10000000_00000000_00000000_00000000
  }

  private static int m(int i) {
    int ou = i & 0xaaaaaaaa;//和1010 1010 1010 。。。。做与运算取出偶数位
    int ji = i & 0x55555555;//和0101 0101 0101 。。。。做与运算取出奇数位
    return (ou >>> 1) ^ (ji << 1); // 连起来
  }

0~1直接的浮点实数的二进制表示

给定一个介于0和1之间的实数,(如0.625),类型为double,打印它的二进制表示(0.101,因为小数点后的二进制分别表示0.5,0.25.0.125......) 。
如果该数字无法精确地用32位以内的二进制表示,则打印“ERROR”

public static void main(String[] args) {
    double num = 0.625;
    StringBuilder sb = new StringBuilder("0.");
    while (num > 0) {
      //乘2:挪整
      double r = num * 2;
      //判断整数部分
      if (r >= 1) {
        sb.append("1");
        //消掉整数部分
        num = r - 1;
      } else {
        sb.append("0");
        num = r;
      }

      if (sb.length() > 34) {
        System.out.println("ERROR");
        return;
      }

    }
    System.out.println(sb.toString());
  }

出现k次与出现1次

数组中只有一个数出现了1次,其他的数都出现了k次,请输出只出现了1次的数。

2个相同的2进制数做不进位加法,结果为0
10个相同的10进制数做不进位加法,结果为O
k个相同的k进制数做不进位加法,结果为0

public static void main(String[] args) {
    int[] arr = {2, 2, 2, 9, 7, 7, 7, 3, 3, 3, 6, 6, 6, 0, 0, 0};
    int len = arr.length;
    char[][] kRadix = new char[len][];
    int k = 3;

    int maxLen = 0;
    //转成k进制字符数组
    //对于每个数字
    for (int i = 0; i < len; i++) {
      //求每个数字的k进制字符串并翻转,然后转为字符数组
      kRadix[i] = new StringBuilder(Integer.toString(arr[i], k)).reverse().toString().toCharArray();
      if (kRadix[i].length > maxLen)
        maxLen = kRadix[i].length;
    }
    //不进位加法
    int[] resArr = new int[maxLen];
    for (int i = 0; i < len; i++) {
      //  不进位加法
      for (int j = 0; j < maxLen; j++) {
        if (j >= kRadix[i].length)
          resArr[j] += 0;
        else
          resArr[j] += (kRadix[i][j] - '0');
      }
    }

    int res = 0;
    for (int i = 0; i < maxLen; i++) {
      res += (resArr[i] % k) * (int) (Math.pow(k, i));// 8%3=2,
    }
    System.out.println(res);
  }

出现奇数次的两种数

在一组数中,有两种数出现了奇数次,其他类型的数出现了偶数次,问这两个出现奇数次的数是什么,时间复杂度小于O(N)

解析:设这两个数是a和b,所以所有数字第一次连续异或可以得到a^b。下面我们只需要想办法搞出a或者b。
举个例子,我们成功搞出a来,那么异或运算aba就可以得到b。注意到a和b前提是肯定不相同,那么在二进制的层面看这两个数字,它们肯定至少有一位数字是不一样的,一个是0,一个是1
a:…………0001…………
b:…………0000…………
下面要做的就是把所有出现了两次的数字根据ab这一位不同的数字进行分组。
这一位数字为0的数字为一组,这一位数字为1的数字为一组
image
让其中一组进行连续异或,比如这一位为1的这一组数字连续异或,出现偶数次的数字异或为0,最后只剩下a,我们就成功得到了a。
最后进行a ^ b ^ a就可以得到b
本题默认寻找a,b从右到左第一次出现的不同二进制位的位置,这也是本题的关键,等价于求ab异或后从右向左第一次出现1的位置。
记住一个公式:原码和补码做与运算可以得到二进制数从右到左第一次出现1的位置为1,其他位置为0的数

1010111100
& 0101000100
= 0000000100
    public static void printOddTimesNum2(int[] arr) {
		int eO = 0, eOhasOne = 0;
		for (int curNum : arr) {
			eO ^= curNum;
		}
		int rightOne = eO & (~eO + 1);  原码和补码做与运算
		for (int cur : arr) {
			if ((cur & rightOne) == 1) { 该位是1的为一组
				eOhasOne ^= cur;
			}
		}
		System.out.println(eOhasOne + " " + (eO ^ eOhasOne));
	}
posted @ 2021-08-02 16:31  gonghr  阅读(402)  评论(0编辑  收藏  举报