您的位置:首页 > 其它

算法学习--位运算

2016-02-17 17:41 375 查看


检测一个数能否被3整除

第一个解决方案就是小学就学过的,如果一个数的每个位相加之和能被3整除,则这个数就可以被3整除。例如612各位之和为9,则可以被3整除。但是这个解决方法并不高效,我们需要取得每一位,然后再一个个相加。

观察二进制,我们可以找到一个模式来判断一个数能否被3整除。如果所有的偶数位出现1的次数为 even_count, 奇数位出现1的次数为 odd_count,两者只差如果是3的倍数,那么这个数就是3倍数。

例如:23 (00…..0010111)  even_count=3, odd_count = 1.    3-1 不能被2整除,所以不是3的倍数。
#include<stdio.h>

/* 返回n是否是3的倍数*/
int isMultipleOf3(int n)
{
int odd_count = 0;
int even_count = 0;

/* 使n为正数,防止递归不退出*/
if(n < 0)   n = -n;
if(n == 0) return 1;
if(n <= 2) return 0;

while(n)
{
/* 统计偶数位有1的个数 */
if(n & 1)
odd_count++;
n = n>>1;

/* 统计奇数位 */
if(n & 1)
even_count++;
n = n>>1;
}

return isMultipleOf3(odd_count - even_coun));
}

/* 测试 */
int main()
{
int num = 23;
if (isMultipleOf3(num))
printf("num is multiple of 3");
else
printf("num is not a multiple of 3");
getchar();
return 0;
}



Single Number

大意:给定一个数组,除了一个元素,其它每个元素都出现了两次,找出这个出现一次的元素。时间复杂度O(n), 空间复杂度O(1).

由于时间复杂度和空间复杂度的限制,应该考虑用位运算来解决,也就是运用异或操作。出现一个和出现两次,其实可以推广到出现奇数次和出现偶数次,可以利用异或运算,考虑每一位: 如果出现了两次偶数次 1 或0 ,异或后都为 0.  奇数次的就为1. 因此
public class Solution {
public int singleNumber(int[] nums) {
int re = 0;
for(int i = 0;i < nums.length;i++){
re ^= nums[i];
}
return re;
}
}


Single Number II

问题:给一个数组,里面只有一个数字一次,其它数字都出现3次,找出这个出现一次的数字,要求时间复杂度为O(n),空间复杂度为O(1)。

可以通过排序在O(nlogn)的时间内解决,也可以用hash,但是最坏的情况下复杂度可能会超过O(n),hash需要的空间复杂度也比较大。

前面的Single Number[位运算] 是一个很简单的位运算题目。

这里的思想是还是位运算的方法解决。并不是简单的异或等操作,因为所有的数字都是出现奇数次。大家可以先参考careercup上面的这个面试题

这里我们需要重新思考,计算机是怎么存储数字的。考虑全部用二进制表示,如果我们把 第 ith  个位置上所有数字的和对3取余,那么只会有两个结果 0 或 1 (根据题意,3个0或3个1相加余数都为0).  因此取余的结果就是那个 “Single Number”.

一个直接的实现就是用大小为 32的数组来记录所有 位上的和。
int singleNumber(int A[], int n) {
int count[32] = {0};
int result = 0;
for (int i = 0; i < 32; i++) {
for (int j = 0; j < n; j++) {
if ((A[j] >> i) & 1) {
count[i]++;
}
}
result |= ((count[i] % 3) << i);
}
return result;
}



判断两个数是否符号相反

写一个函数,判断给定的两个数字是否是符号相反的,不可以使用比较运算符。

例如 fun(-1, 100) == true;  fun(5,6)=false;  fun(-1,-2)=false; 同时,规定0属于正数。

如果使用比较运算符的话:

1
bool
 
oppositeSigns(
int
 
x, 
int
 
y)
2
{
3
    
return
 
(x
< 0)? (y >= 0): (y < 0);
4
}
在二进制表示中,最高位是1的话,就是负数。最高位为0则为正数。因此我可以想办法通过位运算来判断。1 ^0 = 1。所以 负数^正数=负数。其实就是类似于乘法了。
bool oppositeSigns(int x, int y)
{
return ((x ^ y) >> 31);
}


Divide Two Integers

Divide two integers without using multiplication, division and mod operator.

对于这道题目,因为不能用乘除法和取余运算,我们只能使用位运算和加减法。比较直接的方法是用被除数一直减去除数,直到为0,记录下被减的次数就是结果。这种方法的迭代次数是结果的大小,即比如结果为n,算法复杂度是O(n)。

那么有没有办法优化呢? 这个我们就得使用位运算。我们知道任何一个整数可以表示成以2的幂为底的一组基的线性组合,即num=a_0*2^0+a_1*2^1+a_2*2^2+…+a_n*2^n。基于以上这个公式以及左移一位相当于乘以2,我们先让除数左移直到大于被除数之前得到一个最大的基。然后接下来我们每次尝试减去这个基,如果可以则结果增加加2^k,然后基继续右移迭代,直到基为0为止。因为这个方法的迭代次数是按2的幂知道超过结果,所以时间复杂度为O(logn)。

需要注意的是,整数的溢出问题,例如 divide(-2147483648, 2)。我这里问了方便,直接使用了long类型,java代码如下:

public class Solution {
public static int divide(int dividend, int divisor) {
return (int)divideL(dividend,divisor);
}

public static long divideL(long dividend,long divisor){
if(divisor == 1) return dividend;
if(divisor < 0) return -divideL(dividend, -divi
ea91
sor);
if(dividend < 0) return -divideL(-dividend, divisor);
if(divisor > dividend) return 0;
if(divisor == 0) return Integer.MAX_VALUE;
long d = divisor;
long bitcnt = 1;
long ans = 0;
while(d < dividend ){
d <<= 1;
bitcnt <<= 1;
}
while(d >= divisor){
while(dividend >= d){
dividend -= d;
ans += bitcnt;
}
d >>= 1;
bitcnt >>= 1;
}
return ans;
}
}


寻找缺失的数字

给一个长度为n-1的数组,数字的范围在 1到n(无重复),其中有一个缺失的数字,找出该数字。要求时间复杂度为O(n),空间复杂度为O(1).

1
Example:
2
I/P   
[1, 2, 4, ,6, 3, 7, 8]
3
O/P   
5
 方法1 使用公式

1 + 2 + … + n 的公式为 n*(n+1)/2,知道了总和,再前去数组的总和,即为缺失的数字。
int getMissingNo (int a[], int n)
{
int i, total;
total  = (n+1)*(n+2)/2;
for ( i = 0; i< n; i++)
total -= a[i];
return total;
}

int main()
{
int a[] = {1,2,4,5,6};
int miss = getMissingNo(a,5);
printf("%d", miss);
getchar();
}


方法2 使用位运算

此方法类似 Single Number[位运算] ,先对 1 到n的所有数字做异或运算,在多数组内的元素做异或运算,缺失的数字就是那个 single number。
#include<stdio.h>

int getMissingNo(int a[], int n)
{
int i;
int x1 = a[0];
int x2 = 1;

for (i = 1; i< n; i++)
x1 = x1^a[i];
for ( i = 2; i <= n+1; i++)
x2 = x2^i;
return (x1^x2);
}

int main()
{
int a[] = {1, 2, 4, 5, 6};
int miss = getMissingNo(a, 5);
printf("%d", miss);
getchar();
}


不用加减乘除做加法

题目:写一个函数,求两个整数的之和,要求在函数体内不得使用+、-、×、÷。

分析:这又是一道考察发散思维的很有意思的题目。当我们习以为常的东西被限制使用的时候,如何突破常规去思考,就是解决这个问题的关键所在。

看到的这个题目,我的第一反应是傻眼了,四则运算都不能用,那还能用什么啊?可是问题总是要解决的,只能打开思路去思考各种可能性。首先我们可以分析人们是如何做十进制的加法的,比如是如何得出5+17=22这个结果的。实际上,我们可以分成三步的:第一步只做各位相加不进位,此时相加的结果是12(个位数5和7相加不要进位是2,十位数0和1相加结果是1);第二步做进位,5+7中有进位,进位的值是10;第三步把前面两个结果加起来,12+10的结果是22,刚好5+17=22。

前面我们就在想,求两数之和四则运算都不能用,那还能用什么啊?对呀,还能用什么呢?对数字做运算,除了四则运算之外,也就只剩下位运算了。位运算是针对二进制的,我们也就以二进制再来分析一下前面的三步走策略对二进制是不是也管用。

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。第三步把前两个步骤的结果相加。如果我们定义一个函数AddWithoutArithmetic,第三步就相当于输入前两步骤的结果来递归调用自己。

有了这些分析之后,就不难写出如下的代码了:
int AddWithoutArithmetic(int num1, int num2)
{
if(num2 == 0)
return num1;

int sum = num1 ^ num2;
int carry = (num1 & num2) << 1;

return AddWithoutArithmetic(sum, carry);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: