您的位置:首页 > 其它

二进制问题解题思路

2012-07-22 10:50 369 查看

注:部分内容直接来自:MoreWindows博客:http://blog.csdn.net/MoreWindows,另外还有根据《剑指offer》添加的内容

一:基础知识

基本的位操作符有与、或、异或、取反、左移、右移这6种,它们的运算规则如下所示:

符号

描述

运算规则 by MoreWindows

&



两个位都为1时,结果才为1

|



两个位都为0时,结果才为0

^

异或

两个位相同为0,相异为1

~

取反

0变1,1变0

<<

左移

各二进位全部左移若干位,高位丢弃,低位补0

>>

右移

各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)

注意以下几点:
1. 位操作只能用于整形数据,对float和double类型进行位操作会被编译器报错。
2. 对于移位操作,在微软的VC6.0和VS2008编译器都是采取算术称位即算术移位操作,算术移位是相对于逻辑移位,它们在左移操作中都一样,低位补0即可,但在右移中逻辑移位的高位补0而算术移位的高位是补符号位。
举例:

int a = -15, b = 15;

printf("%d %d\n", a >> 2, b >> 2);

会得到a = -4 , b = 3 。
原因是:
假设int类型占4个字节,32位,则
15 = 00000000 00000000 00000000 00001111 取低8位,简记为:00001111
-15 =11111111
11111111 11111111 11110001 取低8位 简记为:11110001
15=0000 1111(二进制),右移二位,最高位由符号位填充将得到0000
0011即3
-15
= 1111 0001(二进制),右移二位,最高位由符号位填充将得到1111 1100即-4

注:负数的二进制表示为负数对应正数取反加1,转化为正数也是取反加1,比如
-15 想要表示成二进制先写出15的二进制表示形式00001111,然后对其取反并加1,得到11110001。 想要对-15的二进制表示转换成10进制,就对11110001取反并加1 得到00001111 即15 然后在加个负号就可以

3.一定要注意:位操作符的运算优先级比较低,因为尽量使用括号来确保运算顺序,否则很可能会得到莫明其妙的结果。比如要得到像1,3,5,9这些2^i+1的数字。写成int
a = 1 << i + 1;是不对的,程序会先执行i + 1,再执行左移操作。应该写成int a = (1 << i) + 1;

二:常用位操作小技巧



1.判断奇偶

不管是正数还是负数只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if (a & 1 == 0)代替if (a % 2 == 0)来判断a是不是偶数。

2.不使用新变量,交换两个变量的值

方法一:利用异或运算的规律:
1. 异或运算^满足交换律。即b^(a^b)=b^b^a
2. 一个数和自己异或的结果为0
3.任何数与0异或都不变
4. 任何数与-1异或都是相当于取反(在下面求绝对值时用的到)
得到:

void Swap(int &a, int &b)

{

if (a != b)

{

a ^= b;

b ^= a;

a ^= b;

}

}

可以这样理解:

第一步 a^=b 即a=(a^b);

第二步 b^=a 即b=b^(a^b),由于^运算满足交换律,b^(a^b)=b^b^a。由于一个数和自己异或的结果为0并且任何数与0异或都会不变的,所以此时b被赋上了a的值。
第三步 a^=b 就是a=a^b,由于前面二步可知a=(a^b),b=a,所以a=a^b即a=(a^b)^a。故a会被赋上b的值。当然还有一种解法类似,如下:(缺点,如果a和b很大的话会容易导致越界)

void Swap(int &a, int &b)

{

if (a != b)

{

a = a + b;

b = b - a;

a = b - a;

}

}


3.变换符号

根据上面15与-15转换的例子,我们知道变换符号只需要取反后加1即可

int SignReversal(int a)

{

return ~a + 1;

}


4.求绝对值

正数的绝对值为本身,负数的绝对值为自身取反加1
因此可得如下代码:

因此先移位来取符号位,int i = a >> 31;要注意如果a为正数,i等于0,为负数,i等于-1。然后对i进行判断——如果i等于0,直接返回。否之,返回~a+1。完整代码如下:

[cpp] view
plaincopy

//by MoreWindows( http://blog.csdn.net/MoreWindows )

int my_abs(int a)

{

int i = a >> 31;

return i == 0 ? a : (~a + 1);

}

还有一种解法是让1左移31位,与最高位相与,得到符号位

int my_abs(int a)

{

return a&(1<<31) == 0
? a : (~a + 1);

}

现在再分析下。对于任何数,与0异或都会保持不变,与-1即0xFFFFFFFF异或就相当于取反。因此,a与i异或后再减i(因为i为0或-1,所以减i即是要么加0要么加1)也可以得到绝对值。所以可以对上面代码优化下:

[cpp] view
plaincopy

//by MoreWindows( http://blog.csdn.net/MoreWindows )

int my_abs(int a)

{

int i = a >> 31;

return ((a ^ i) - i);

}

而且有些笔面试题就要求这样做,因此建议读者记住该方法



三:位操作与空间压缩


先不介绍


四:位操作的趣味应用


1. 高低位交换

给出一个16位的无符号整数。称这个二进制数的前8位为“高位”,后8位为“低位”。现在写一程序将它的高低位交换。例如,数34520用二进制表示为:
10000110 11011000
将它的高低位进行交换,我们得到了一个新的二进制数:
11011000 10000110
思路:假设所要高地位交换的数位X,由于X为无符号数,左移和右移都是进行补零操作。所以只需将X<<8 与 X>>8 得到的值想或就可以得到结果

//高低位交换 by MoreWindows( http://blog.csdn.net/MoreWindows )

#include <stdio.h>

template <class T>

void PrintfBinary(T a)

{

int i;

for (i = sizeof(a) * 8 - 1; i >= 0; --i)

{

if ((a >> i) & 1)

putchar('1');

else

putchar('0');

if (i == 8)

putchar(' ');

}

putchar('\n');

}

int main()

{

printf("高低位交换 --- by MoreWindows( http://blog.csdn.net/MoreWindows ) ---\n\n");

printf("交换前: ");

unsigned short a = 3344520;

PrintfBinary(a);

printf("交换后: ");

a = (a >> 8) | (a << 8);

PrintfBinary(a);

return 0;

}

2. 二进制中1的个数
常规的解法:把n和1做与运算,判断n的最低位是不是1。接着把1左移一位得到2,再和n做与运算,就能判断n的次低位是否为1....这样反复左移,每次都能判断n的其中一位是不是1.代码如下:
int NumberOf1(int n)
{
int count = 0;
unsigned int flag = 1;
while(flag)
{
if(n&flag)
{
count++;
}
flag = flag <<1;  // 到32位在左移会截断为0,循环结束
}
return count;
}


或者如下代码:
int NumberOf1(int n)
{
int count = 0;
int i = 0;
while(i != 8*sizeof(a))  // 方法一
{
if((a&(1<<i)) != 0)
count++;
i++ ;
}
return count;
}
上面算法循环的次数为整数二进制的位数。32位的数需要循环32次

更加优化的代码:
另外一种思路是如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减去1,那么原来处在整数最右边的1就会变成0,原来在1后面的所有的0都会变成1。其余的所有位将不受到影响。举个例子:一个二进制数1100,从右边数起的第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成1,而前面的1保持不变,因此得到结果是1011。

我们发现减1的结果是把从最右边一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000。也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0。那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
总结起来就是:把一个整数减去1,再和原来整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制中有多少个1,就可以进行多少次这样操作。很多二进制问题都可以用它来解决。
代码如下:

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


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

一个整数如果是2的整数次方,那么它的二进制表示中只有一位是1,而其他所有位都是0。根据前面的分析,把这个数减去1之后再和它自己做与运算,这个整数中的唯一的1就会变成0
扩展2: 输入两个整数m和n,计算需要改变m的二进制表示中的多少个位才能得到n:
比如10的二进制表示为1010, 13的二进制表示为1101 。需要改变1010的3位才能得到1101.
解法,分两步,首先求这两个数的异或。然后再统计异或结果中1的位数,便得到需要改变的位数。
3. 缺失的数字:

很多成对出现数字保存在磁盘文件中,注意成对的数字不一定是相邻的,如2, 3, 4,
3, 4, 2……,由于意外有一个数字消失了,如何尽快的找到是哪个数字消失了?

用异或运算的两个特性——1.自己与自己异或结果为0,2.异或满足交换律。因此我们将这些数字全异或一遍,结果就一定是那个仅出现一个的那个数。

代码如下:

//缺失的数字 by MoreWindows( http://blog.csdn.net/MoreWindows )

#include <stdio.h>

int main()

{

printf("缺失的数字 --- by MoreWindows( http://blog.csdn.net/MoreWindows ) ---\n\n");

const int MAXN = 15;

int a[MAXN] = {1, 347, 6, 9, 13, 65, 889, 712, 889, 347, 1, 9, 65, 13, 712};

int lostNum = 0;

for (int i = 0; i < MAXN; i++)

lostNum ^= a[i];

printf("缺失的数字为: %d\n", lostNum);

return 0;

}


4. 缺失的数字扩展:

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

解析:根据问题3我们知道如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现依次的数字,因为那些出现两次的数字全部在异或中抵消掉了。
那么本题中如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其他数字都出现两次。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。
我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其他数字都出现了两次,在异或中全部抵消掉了。由于这两个数字肯定不一样,那么这个异或结果肯定不为0,也就是说在这个结果数字的二进制表示中至少就有一位为1。我们在结果数字中找到第一个为1的位的位置,记为第N位。现在我们以第N位是不是1为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第N位都为1,而第二个子数组的每个数字的第N位都为0。

现在我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其他数字都出现了两次。因此到此为止,所有的问题我们都已经解决。

部分代码如下:
unsigned int FindFirstBitIs1(int num);
bool IsBit1(int num, unsigned int indexBit);

void FindNumsAppearOnce(int data[], int length, int* num1, int* num2)
{
if (data == NULL || length < 2)
return;

int resultExclusiveOR = 0;
for (int i = 0; i < length; ++ i)
resultExclusiveOR ^= data[i];

unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);

*num1 = *num2 = 0;
for (int j = 0; j < length; ++ j)
{
if(IsBit1(data[j], indexOf1))
*num1 ^= data[j];
else
*num2 ^= data[j];
}
}

// 找到num从右边数起第一个是1的位
unsigned int FindFirstBitIs1(int num)
{
int indexBit = 0;
while (((num & 1) == 0) && (indexBit < 8 * sizeof(int)))
{
num = num >> 1;
++ indexBit;
}

return indexBit;
}

// 判断数字num的第indexBit位是不是1
bool IsBit1(int num, unsigned int indexBit)
{
num = num >> indexBit;
return (num & 1);
}


5. 不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*
运算

解法一:取巧的方法
int Add(int a, int b)
{
char *c = (char*)a;
return (int)&c;
}
解析:
本来 char *c = (char*)a 这种转换方式是不正确的,假设a = 2 则得到的结果是c = 0x00000002 由于下标的操作其实就是地址“+”的操作。假设b = 3, 则相当于地址 00000002 + 3*1 = 00000005。转化为int 类型结果是5 。 同理,如果 a = -1 则 c = 0xffffffff 。然后再偏移.........
注意:
c只能是char* 类型的,不能改为int * 类型。如果改为int* 类型的话 c[b] 操作 其实就是c + b*4 了........

[b]解法二:
用位运算


首先我们可以分析人们是如何做十进制的加法的,比如是如何得出5+17=22这个结果的。实际上,我们可以分成三步的:第一步只做各位相加不进位,此时相加的结果是12(个位数5和7相加不要进位是2,十位数0和1相加结果是1);第二步做进位,5+7中有进位,进位的值是10;第三步把前面两个结果加起来,12+10的结果是22,刚好5+17=22。
只能是c
前面我们就在想,求两数之和四则运算都不能用,那还能用什么啊?对呀,还能用什么呢?对数字做运算,除了四则运算之外,也就只剩下位运算了。位运算是针对二进制的,我们也就以二进制再来分析一下前面的三步走策略对二进制是不是也管用。

5的二进制是101,17的二进制10001。还是试着把计算分成三步:第一步各位相加但不计进位,得到的结果是10100(最后一位两个数都是1,相加的结果是二进制的10。这一步不计进位,因此结果仍然是0);第二步记下进位。在这个例子中只在最后一位相加时产生一个进位,结果是二进制的10;第三步把前两步的结果相加,得到的结果是10110,正好是22。由此可见三步走的策略对二进制也是管用的。

接下来我们试着把二进制上的加法用位运算来替代。第一步不考虑进位,对每一位相加。0加0与 1加1的结果都0,0加1与1加0的结果都是1。我们可以注意到,这和异或的结果是一样的。对异或而言,0和0、1和1异或的结果是0,而0和1、1和0的异或结果是1。接着考虑第二步进位,对0加0、0加1、1加0而言,都不会产生进位,只有1加1时,会向前产生一个进位。此时我们可以想象成是两个数先做位与运算,然后再向左移动一位。只有两个数都是1的时候,位与得到的结果是1,其余都是0。第三步把前两个步骤的结果相加。

代码如下:

int Add(int num1, int num2)
{
int sum, carry;
do
{
sum = num1 ^ num2;
carry = (num1 & num2) << 1;
num1 = sum;
num2 = carry;
}
while(num2 != 0);

return num1;
}



X. 二进制逆序

我们知道如何对字符串求逆序,现在要求计算二进制的逆序,如数34520用二进制表示为:
10000110 11011000
将它逆序,我们得到了一个新的二进制数:
00011011 01100001
它即是十进制的7009。
回顾下字符串的逆序,可以从字符串的首尾开始,依次交换两端的数据。在二进制逆序我们也可以用这种方法,但运用位操作的高低位交换来处理二进制逆序将会得到更简洁的方法。类似于归并排序的分组处理,可以通过下面4步得到16位数据的二进制逆序:

第一步:每2位为一组,组内高低位交换
10 00 01 10 11 01 10 00
-->01 00 10 01 11 10 01 00

第二步:每4位为一组,组内高低位交换
0100 1001 1110 0100
-->0001 0110 1011 0001

第三步:每8位为一组,组内高低位交换
00010110 10110001
-->01100001 00011011

第四步:每16位为一组,组内高低位交换
01100001 00011011
-->00011011 01100001

对第一步,可以依次取出每2位作一组,再组内高低位交换,这样有点麻烦,下面介绍一种非常有技巧的方法。先分别取10000110 11011000的奇数位和偶数位,空位以下划线表示。

原 数 10000110 11011000
奇数位 1_0_0_1_ 1_0_1_0_
偶数位 _0_0_1_0 _1_1_0_0
将下划线用0填充,可得

原 数 10000110 11011000

奇数位 10000010 10001000

偶数位 00000100 01010000

再将奇数位右移一位,偶数位左移一位,此时将这两个数据相与即可以达到奇偶位上数据交换的效果了。

原 数 10000110 11011000

奇数位右移 01000011 01101100

偶数位左移 0000100 010100000
相或得到 01001000 11100100

可以看出,结果完全达到了奇偶位的数据交换,再来考虑代码的实现——

取x的奇数位并将偶数位用0填充用代码实现就是x & 0xAAAA

取x的偶数位并将奇数位用0填充用代码实现就是x & 0x5555

因此,第一步就用代码实现就是:

x = ((x & 0xAAAA) >> 1) | ((x & 0x5555) << 1);

类似可以得到后三步的代码。完整程序如下:

[cpp] view
plaincopy

//二进制逆序 by MoreWindows( http://blog.csdn.net/MoreWindows )

#include <stdio.h>

template <class T>

void PrintfBinary(T a)

{

int i;

for (i = sizeof(a) * 8 - 1; i >= 0; --i)

{

if ((a >> i) & 1)

putchar('1');

else

putchar('0');

if (i == 8)

putchar(' ');

}

putchar('\n');

}

int main()

{

printf("二进制逆序 --- by MoreWindows( http://blog.csdn.net/MoreWindows ) ---\n\n");

printf("逆序前: ");

unsigned short a = 34520;

PrintfBinary(a);

printf("逆序后: ");

a = ((a & 0xAAAA) >> 1) | ((a & 0x5555) << 1);

a = ((a & 0xCCCC) >> 2) | ((a & 0x3333) << 2);

a = ((a & 0xF0F0) >> 4) | ((a & 0x0F0F) << 4);

a = ((a & 0xFF00) >> 8) | ((a & 0x00FF) << 8);

PrintfBinary(a);

}

bitset类型使用:

1.包含头文件
#include<bitset>
using std::bitset;
2. 定义和初始化
bitset<32> bitvec; // 32bits, all zero
3. 用unsigned值初始化bitset对象
当用unsigned long值作为bitset对象的初始值时,该值将转化为二进制的位模式。而bitset对象中的位集作为这种模式的副本。
如果bitset类型长度大于unsigned long值得二进制位数,则其余的高阶位将置为0;如果bitset类型长度小于unsigned long值的二进制位数,则只是用unsigned long的低阶位,超过bitset类型长度的高阶位将被丢弃。
如下:
bitset<16> bitvec1(0xffff); // bits 0...15 are set to 1
bitset<32> bitvec2(oxffff); // bits 0... 15 are set to 1 ; 16...31 are 0
4. 用string对象初始化bitset对象
当用string对象初始化bitset对象时,string对象直接表示为位模式。从string对象读入位集的顺序是从右向左
string strval("1100");
bitset<32>bitvec(strval); // bitvec中第2和第3位置为1,其余位置都为0.如果string对象的字符个数小于bitset类型长度,则高阶位将置为0
5. 访问bitset对象中的位
可以用下标操作符来读或者写某个索引位置的二进制位。
for(int index = 0; index != 32; index +=2)
bitvec[index] = 1; // 或者 bitset.set(index)

6. 对整个bitset对象进行设置
set 和reset操作分别用来对整个bitset对象的所有二进制位全部置1和全部置0
bitvec.reset(); // set all the bits to 0
bitvec.set(); //set all the bites to 1
7.获取bitset对象的值
to_ulong 操作返回一个unsigned long 值,该值与bitset对象为模式存储相同。仅当bitset类型的长度小于或者等于unsigned long 的长度时,才可以使用to_ulong操作
unsigned long ulong = bitvec.to_ulong();
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: