【算法学习】位运算专题 奇妙位操作总结1

位操作有太多的神奇写法,这里总结一部分。


1. 无算术运算符的加法 Add two numbers without using arithmetic operators

(1) 题意

如果有一个题目,要求我们实现一个 add 函数,不使用包括 +,++,-,--,.. 等算术运算符,完成两个整型数的相加。

两个比特的相加,可以用 ^ 来代替(可能存在进位);进位一个比特,可以用 & 来实现。类似的,数字逻辑中,完成两个单独的比特相加的是半加器(half adder) ,通过半加器的串联,可以实现全加器。

一个朴素的思维是,从最高位开始,xy 的每一位相异或 ^ ,如果有进位,则对结果的高位异或 1 ,有进位就继续。或者从最低位开始?这样想太麻烦了。

(2) 思路

我们直接对 xy 进行操作,相当于并行进位。

(1) 如果 xy 在每一个二进制位都不同,x^y 就是 x+y 。如:0101 ^ 1010 = 1111

(2) 如果存在相同的二进制位(都是 1),那么需要用 & 来将其吸纳进去。都是 0 的情况无关紧要。

x = 7 = 0000 0111
y = 5 = 0000 0101
x+y   = 0000 1100

x=x^y = 0000 0010  //x^y为0的二进制位代表的x,y对应位可能都是0, 或都是1
c=x&y = 0000 0101  //x&y为1的二进制位标记出x,y对应位都是0的情况, 这代表这些位都需要进位
//进位: y = c << 1 = 0000 1010, 即准备对这些位的高一位进行“加法”
//加法的^, 递归进行
x=x^y = 0000 1000
c=x&y = 0000 0010
//进位: y = c << 1 = 0000 0100
//加法的^
x=x^y = 0000 1100  //此时x和y在每一个二进制位都不同
c=x&y = 0000 0000 //c=x&y=0, 不用再进位

(3) 模板代码

下面的写法可以作为模板。

// C++ Program to add two numbers without using arithmetic operator  
#include <bits/stdc++.h> 
using namespace std; 
	
int add(int x, int y) {
	//iterate utill there is no carry
	while (y) {
		//the carry contains all the common bits of x and y 
		int carry = x & y; 
		//now, x is the SUM of bits of x and y
		//where at least one of the bits is not set 
		x = x ^ y;
		//carry is right-shifted by 1 so that adding it to x
		y = carry << 1;
	}
	return x;
}

int main() {
	cout << add(15, 32);
	return 0;
}
递归版: ```cpp int add(int x, int y) { if (y == 0) return x; return add(x ^ y, (x & y) << 1); } ``` ## (4) 习题 [POJ 1001 A + B Problem](https://blog.csdn.net/myRealization/article/details/106750883) .

2. 无算术运算符的加1操作 Add 1 to a given number without using arithmetic operators

(1) 题意

写一个函数 addOne 给一个整数加1,不使用任何算术运算符。

Examples:

Input:  12
Output: 13

Input:  6
Output: 7

(2) 思路1

其一:使用 add 函数,用 ^&

x = 12 = 0000 1100
y =  1 = 0000 0001
x^y    = 0000 1101 = 13  //结果

x =  7 = 0000 0111
y =  1 = 0000 0001
x=x^y  = 0000 0110  
c=x&y  = 0000 0001       //左移为y = 0000 0010, 进行进位加法
-----
x=x^y  = 0000 0100
c=x&y  = 0000 0010       //左移为y = 0000 0100, 进行进位加法 
-----
x=x^y  = 0000 0000        
c=x&y  = 0000 0100       //左移为y = 0000 1000, 进行进位加法 
-----
x=x^y  = 0000 1000     
c=x&y  = 0000 0000       //退出

感觉太麻烦了,有没有更简单的写法呢?其实可以从上面的第二个例子看出,如果我们要对 7 加1,可以将其二进制表示形式中的最右边的那个 0 之后的所有 1 全部翻转为 0 ,然后将之前的 rightmost 0 翻转为 1

比如说,为了给 x = 1100 0111 加1,我们可以先找到 rightmost 0 ,1100 0111;翻转 flip 所有在红字 0 后的比特,可以得到 1100 0000;然后翻转 flip 这个红字 0 ,得到 1100 1000 ,就是结果。

(3) 模板代码1

#include <bits/stdc++.h> 
using namespace std; 
 
//无算术运算符加1
int addOne(int x) {
	int m = 1;
	//flip all the set bits until we find rightmost 0 
	while (x & m) { 
		x = x ^ m;
		m <<= 1;
	}
	//flip the rightmost 0 
	x = x ^ m;
	return x;
}
 
int main()  
{   
	cout << addOne(13);  //0000 1011->0000 1100
    return 0;  
}
## (4) 思路2 由于补码 `2's Complement` 的广泛使用,我们知道 `[-x]补 = ~[x]补 + 1` 。因此,有 `~([-x]补 - 1) = [x]补` ,即 `~[-(x+1)]补 = [x]补` => `[x+1]补 = -(~[x]补)` 。

所以,为了得到 x+1 ,我们写成 -(~x) 即可。

(5) 模板代码2

#include <bits/stdc++.h> 
using namespace std; 
 
//无算术运算符加1
int addOne(int x) {
	return (-(~x));
}
 
int main()  
{   
	cout << addOne(13);  //0000 1011->0000 1100
    return 0;  
}

3. 检查相反符号 Detect if two integers have opposite signs

(1) 题意

给出两个有符号整数,写一个函数判断两个整数的符号是否不同,不同则返回 true0 视作正数。实质上相当于判断两个整数补码的符号位是否不同。

Examples:

Input: -1 100
Output: true

Input: -1 -200
Output: false

(2) 思路和代码一 使用比较运算符

使用两次比较操作,可以实现题意:

bool oppositeSigns(int x, int y) {
	return (x < 0) ? (y >= 0) : (y < 0);

(3) 思路和代码二 位运算

由于使用比较运算符,可能涉及到分支预测和指令预取,一旦预取错误,可能清空多个时间周期的指令。因此会比较慢。为此,我们代之以位运算符。

可以注意到,两个有符号数 x,y ,如果符号相异,则符号位一个为 1 ,另一个为 0 ,所以 x ^ y 的符号位为 1 ,小于零。代码如下:

//C++ Program to Detect if two integers have opposite signs
#include <bits/stdc++.h>
using namespace std;

bool oppositeSigns(int x, int y) {
	return ((x ^ y) < 0);
}

int main() {
	int x = 123, y = -100;
	if (oppositeSigns(x, y)) cout << "Signs are opposite" << endl;
	else cout << "Signs are not opposite" << endl;
	return 0;
}
一次位运算和一次比较比起两次比较效率更高。针对某些 `int` 是32位的系统,我们可以采用如下的写法,检查 `x ^ y` 的 $31^{th}$ 位是否是 `1` ,从而完全排除比较运算。 ```cpp bool oppositeSigns(int x, int y) { return ((x ^ y) >> 31); } ```

4. 无比较运算符比较两个整数 Compare two integers without using comparison operators

(1) 题意

给出两个整数,检查 AB 是否相等,不使用任何比较运算符。

Examples:

Input: A = 5, B = 6
Output: 0

Input: A = 5, B = 5
Output: 1

Note: 1 = "Yes" and 0 = "No"

(3) 思路和代码 异或

思路很简单,两个整数异或 A ^ B ,结果为零则相等。用减法也可以,不过这是位运算专题,就不这么写了。

//C++ Programs to compare two integers without any comparison operator
#include <bits/stdc++.h>
using namespace std;

//return true if (A ^ B) == 0 else return false
bool equalNumber(int A, int B) {
	return !(A ^ B);
}

int main() {
	int A = 5, B = 6;
	cout << equalNumber(A, B) << endl;
	return 0;
}

5. 置最右二进制位1为0 Turn off the rightmost set bit

(1) 题意

写一个程序,将一个整数的二进制形式最右边的 1 置为 0

Examples :

Input:  12 (00...01100)
Output: 8 (00...01000)

Input:  7 (00...00111)
Output: 6 (00...00110)

(2) 思路

这很简单,当初写子集生成的迭代版时,就用到了这个 bits magic 。为了给 n 最右边的 1 置零,我们可以用 (n-1) & n 得到这个结果。

(3) 代码

#include <bits/stdc++.h> 
using namespace std; 
  
// unsets the rightmost set bit of n and returns the result  
int fun(unsigned int n) {  
    return n & (n - 1);  
}  
int main() {  
    int n = 7;  
    cout << "7 after unsetting the  rightmost set bit " << unsetRight1(7) << endl;
   	cout << "-1 after unsetting the  rightmost set bit " << unsetRight1(-1) << endl;
	return 0;   
}  

6. 数二进制形式中的1 Count set bits in an integer

(1) 题意

写一个高效率的程序,数出数的二进制形式中的 1 的数量。

Examples :

Input : n = 6
Output : 2
Binary representation of 6 is 110 and has 2 set bits

Input : n = 13
Output : 3
Binary representation of 13 is 1101 and has 3 set bits

(2) 思路和代码1(简单方法)

搜索所有位,计数即可。

#include <bits/stdc++.h> 
using namespace std; 

//Count set bits in an integer
unsigned int countSetBits(unsigned int n) {
	unsigned int count = 0;
	while (n) {
		count += n & 1;
		n >>= 1;
	}
	return count;
}

int main() {
	int i = 9; 
	cout << countSetBits(i);
	return 0;
}

在这里插入图片描述

(3) 思路和代码2 位运算

将5中的做法 n & (n - 1) 一直使用,直到原来的数为 0 ,即可知道其二进制形式的中 1 有多少个。

unsigned int countSetBits(unsigned int n) {
	unsigned int count = 0;
	while (n) {
		n &= (n - 1);
		++count;
	}
	return count;
}

(4) 思路和代码3 查表 ★★★

使用 lookup table 搜索表,能够在 O(1) \text{O(1)} O(1) 的时间内完成计数。

  • 首先,我们假设 int32 位的,然后将 32 位分成 48 位数(0-255);
  • 对每个 8 位数,用 & 0xff 将其截出,得到一个 0-255 的数字;
  • 然后用 4 个数字,到 lookuptable 中查询它们的二进制形式中的 1 有多少个;

自然,从上面的步骤可以发现,我们要提前准备好 lookuptable 这张表,然后查表即可:

  • 因为不可能创建一个规模为 2^31 - 1 的表,所以我们无法直接用原数值来查表;而是分成了 4 个八位数;
  • 从下面的几个数字,我们可以得到规律:
    number	binary         countSetBits
    0		0000 0000      0 = 0 
    1		0000 0001      1  
    2		0000 0010 	   1
    3		0000 0011	   2
    4		0000 0100      1
    5		0000 0101 	   2
    6       0000 0110      2
    ....
    255     1111 1111      8
    ① number是0时,countSetBits=0; (基准点)
    ② number是偶数时, number相当于(number / 2)(可能是奇数或偶数)左移一位, countSetBits不变
    ③ number是奇数时, number相当于(number / 2)左移一位, 再加1
    
  • 由此,我们可以递推,写出 lookuptable[256] 的初始化程序。

举一个例子,如果 n = 354 ,则二进制形式为 0000000000000000000000101100010

分割为8个bits块:
In Binary  :  00000000 | 00000000 | 00000001 | 01100010
In decimal :     0          0          1         98

现在对setBits使用lookuptable进行计数:
LookupTable[0] = 0
LookupTable[1] = 1
LookupTable[98] = 3
所以总共的setBits count = 4. 

代码如下:

//C++ program to count number of set bits using lookup table in O(1) time
#include <iostream> 
using namespace std; 

unsigned int lookuptable[256];
//generate lookup table
void initialize() {
	lookuptable[0] = 0;
	for (int i = 0; i < 256; ++i)
		lookuptable[i] = (i & 1) + lookuptable[i >> 1];
}
//count set bits using lookup table and return set bits count
unsigned int countSetBits(int N) {  //4
	unsigned int count = lookuptable[N & 0xff] + 
						 lookuptable[(N >> 8) & 0xff] + 
						 lookuptable[(N >> 16) & 0xff] + 
						 lookuptable[(N >> 24) & 0xff];
	return count;
}

int main() {
	initialize();
	unsigned int n = 354;
	cout << countSetBits(n) << endl;
	return 0;
}

(5) 思路和代码4 查表 ★★★

做法是 mapping numbers with the bit 。简单的维持一个数值到 setBits countmap/array 。这里 array 长为 16 ,保持 4 个比特。

int num_to_bits[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};

然后我们只需要递归查表即可。 O(1) \text{O(1)} O(1) 的复杂度。代码如下,这个函数能够适应 char/short/int/long... 等:

#include <bits/stdc++.h> 
using namespace std; 
  
int num_to_bits[16] = {0, 1, 1, 2, 1, 2, 2, 3, 
              		   1, 2, 2, 3, 2, 3, 3, 4}; 
                        
unsigned int countSetBitsRec(unsigned int num) {
	int nibble = 0;
	if (num == 0) return num_to_bits[0];
	nibble = num & 0xf; 
	//use map to find count in last nibble
	//then plus recursively the counts of remaining nibbles
	return num_to_bits[nibble] + countSetBitsRec(num >> 4);
}
int main() { 
    int num = 31; 
    cout << countSetBitsRec(num); 
    return 0; 
} 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

memcpy0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值