本文主要参考了这篇博文[位操作基础篇之位操作全面总结],并且复习了深入理解计算系统第二章的内容。
基础原理
位操作实现
- n&(n-1)的妙用:消除最低位1
计算位向量中1的个数
/*
n&n-1的妙用。
n与n-1的区别在于,对于n,最低位1开始一直到右,和n-1,完全相反
n = 10-100
n-1 = 10-011
因此,n&n-1可以把n的最低位1变成0
下面这个例子计算位向量中1的个数。
*/
class Solution {
public:
int hammingWeight(uint32_t n) {
int cnt = 0;
while( n )
{
++cnt;
n &= n-1;
}
return cnt;
}
};
/*
判断一个数是否是2的幂,如果是2的幂。位向量当中,只有1个1。所以,用n&(n-1)干掉之后,就没了。
*/
class Solution {
public:
bool isPowerOfTwo(int n) {
return ( n > 0 && !(n&(n-1)) );
}
};
寻找只出现1次的一个数
问题扩展:(小米2013校招笔试题)一个数组里,除了三个数是唯一出现的,其余的都出现偶数个,找出这三个数中的任一个。比如数组元素为【1, 2,4,5,6,4,2】,只有1,5,6这三个数字是唯一出现的,我们只需要输出1,5,6中的一个就行。思路:之前做的是两个数分开,思路一样。现在三个数分开,只让你输出其中的一个数。对于出现2次的,xor都没了。对于出现一次的三个数,因为三个数不同,所以有一位三个数的位向量不同。一定是两个0,一个1。此时,xor之后,肯定会把一个数字分出来。然后用这位当mask,把这个数分到一组。然后xor就得到了。
int find( std::vector<int>& nums )
{
// get the a^b^c
int sz = nums.size();
int ret = 0;
for( int i = 0; i < sz; ++i )
{
ret ^= nums[i];
}
// get the mask
int mask = 1;
while( !(ret&mask) )
{
mask = mask << 1;
}
// separate
std::vector<int> ret_vec;
for( int i = 0; i < sz; ++i )
{
if( nums[i] & mask )
ret_vec.push_back( nums[i] );
}
// get the ans
sz = ret_vec.size();
int ans = 0;
for( int i = 0; i < sz; ++i )
{
ans ^= ret_vec[i];
}
return ans;
}
/*
【1,2,4,5,6,4,2】得到6
*/
- 二进制中1的个数
思路:当然这个题目的实现方法非常多啊。说两种我觉得挺简单的。
- 方法1:移位。每次看最低位是否为1.若是,则加1。从那边移动都可以,对于正负数也都没有问题。前提是参数必须是无符号数。即用无符号数的视角审视这段位向量。无符号数是逻辑移位,不影响。
- 方法2:用掩码。看看每一位是否为1,和mask相与后不为0即改位为1.
// 移位
int number_of_1bits( uint32_t n )
{
int ans = 0;
do
{
ans += (n&1);
n >>= 1;
}while(n);
return ans;
}
//掩码
int number_of_1bits1( uint32_t n )
{
uint32_t mask = 0x80000000;
int ans = 0;
do
{
ans += (( mask & n )?1:0);
mask >>= 1;
}
while(mask);
return ans;
}
加法实现
思路:- 计算不带进位加法( a ^ b )
- 计算进位( a & b )
- 累加这两部分的和(???)
思路大致如上面的步骤,问题是本省需要实现加法,但是第三步又要用到加法,所以第三步的实现还是通过前两部分。直到没有进位,那么此时的加法退化成不带进位加法,用xor实现即可。
代码
int getSum(int a, int b) {
int carry = (a&b)<<1; // 进位
int ret = a^b; // 不带进位加法
while( carry )
{
int new_carry = (carry & ret)<<1;
int new_ret = carry ^ ret;
carry = new_carry;
ret = new_ret;
}
return ret;
}
二进制求逆序
描述:我们知道如何对字符串求逆序,现在要求计算二进制的逆序,如数34520用二进制表示为:
10000110 11011000
将它逆序,我们得到了一个新的二进制数:
00011011 01100001
它即是十进制的7009。思路:
总体的思路是分治,和归并排序的思路一样。
对于一个字符串 s = “abcdefgh”;给出一种分治的求逆方法。
分治:先把abcd置逆 -> dcba
分治:再把edgh置逆 -> hgfe
归并:hgfe dcba
剩下递归就好了。递归分治到最底层是两两交换。然后依次向上即可。
但是问题是,怎么实现?具体的思路:首先分别取出需要交换的位,高位右移,低位左移,然后做或运算。完成交换。一下是一个具体步骤。
原数:10000110 11011000
第一步:10 00 01 10 11 01 10 00
-> 01 00 10 01 11 10 01 00
四个四个
第二步:0100 1001 1110 0100
-> 0010 1001 0111 0010
八个八个
第三步:00101001 01110010
-> 10010100 01001110
十六个
第四步:10010100 01001110
-> 0111001000101001代码:
uint16_t reverse_bits( uint16_t val )
{
val = ( ( val & 0xAAAA ) >> 1 )|( ( val & 0x5555) << 1 );
val = ( ( val & 0xCCCC ) >> 2 )|( ( val & 0X3333) << 2 );
val = ( ( val & 0xF0F0 ) >> 4 )|( ( val & 0x0F0F ) << 4 );
val = ( ( val & 0XFF00 ) >> 8 )|( ( val & 0X00FF ) << 8 );
return val;
}
高低位交换
描述:给出一个16位的无符号整数。称这个二进制数的前8位为“高位”,后8位为“低位”。现在写一程序将它的高低位交换。例如,数34520用二进制表示为:
10000110 11011000
将它的高低位进行交换,我们得到了一个新的二进制数:
11011000 10000110
它即是十进制的55430。思路:
思路也非常简单啊,高八位左移,第八位右移,然后求或即可。
代码:
uint16_t change_high_low( uint16_t val )
{
return (val >> 8)^(val << 8);
}
判断奇偶
思路:只需看最后一位即可代码
bool is_odd(int val)
{
return val&1;
}
- 交换两数
思路:主要是利用xor的性质。a^a = 0, 所以a^a^b = b.
代码
void swap( int& a, int& b )
{
a ^= b;
b ^= a; // 注意这个顺序不能变,一定要先求b
a ^= b;
}
取反
思路:在原操作数补码的基础上,直接取反 + 1即可(包括符号位)。至于我们课本里面学的那一套,那是在负数原码的基础上,符号位不变,取反 + 1。本质都一样。代码
int sign_reverse(int val)
{
return (~val+1);
}
取绝对值
思路:这个题的思路秒。主要利用了一个性质 a ^ 0xffffffff = ~a , a^0 = a。对于负数而言,算术右移,所以sign = 0xffffffff(-1)代码
int sign_abs1(int val)
{
int sign = val >> 31; // 获得符号位
return ( (val ^ sign) - sign);
}