算法-位运算

位运算符概念: 

位运算符有哪些:

基本位运算符包括:四个逻辑运算符按位与、或、非、异或)、两个移位运算符(左移、右移)

除了以上基本的位运算符,还有一些组合的位运算符:

&= 、|= 、^= 、>>== 、<<==

位运算操作数为整数:

注意:两个数字做与运算的时候结果仍然是一个长长的二进位,而不是0/1数值;

逻辑右移与算术右移的区别?

逻辑右移就是不考虑符号位,右移一位,左边补零即可。
算术右移需要考虑符号位,右移一位,若符号位为1,就在左边补1;否则,就补0。

例如,8位二进制数11001101分别右移一位。
逻辑右移就是[0]1100110
算术右移就是[1]1100110

>>进行的是逻辑右移还是算数右移依赖机器。

位运算符常用技巧:

移位>>运算符:

技巧:对二进制单个位的基本操作

从低位到高位,取n的第m位:

int getBit(int n, int m){
	return (n >> (m-1)) & 1;
}
//当然也可以按照这个方法遍历打印

技巧:乘除2的m次方的运算

乘以2运算:

int mulTwo(int n){//计算n*2 
	return n << 1;
}

除以2运算:

int divTwo(int n){//负奇数的运算不可用
	return n >> 1;//除以2
}

 乘以2的m次方

int mulTwoPower(int n,int m){//计算n*(2^m)
	return n << m;
}

 除以2的m次方

int divTwoPower(int n,int m){//计算n/(2^m)
	return n >> m;
}

CPU计算加减法的速度跟位运算(与、或、非、异或)相当,乘法的速度比加减法慢近10倍,除法的速度比加减法慢(近20倍——8位,近30倍——16位,40倍以上——32位)。

有种说法:

位运算只要1个CPU时钟

+-要2个
*要4个
/要40个

不过一般好的编译器中包含一个优化器,该优化器知道如何以目标处理器体系结构所能达到的速度快速进行乘法。

这种时候最好的选择是清楚地告诉编译器您的意图(即i * 2而不是i << 1),然后让它决定最快的汇编/机器码序列,即处理器本身甚至可能会将乘法指令实现为一系列移位和微码加法运算。

除了一些特别的算法题关注时间复杂度之外,最好如果您要转移,那就转移。如果要乘,就乘。做语义上最清晰的事情-您的同事以后会感谢您。或者,更可能的是,如果以后否则诅咒您。

 技巧:不能使用pow 函数求一个整数的n次方(剑指 Offer 16)

你会怎么做呢?这还不简单,连续让 n 个 3 相乘就行了,代码如下:

int pow(int n){
    int tmp = 1;
    for(int i = 1; i <= n; i++) {
        tmp = tmp * 3;
    }
    return tmp;
}

时间复杂度为 O(n) 了,怕是小学生都会!如果让你用位运算来做,你会怎么做呢?我们能不能不步进呢?翻着番的想目标次数前进呢?比如求2^8,能不能不要2*2*2*2*2*2*2*2,而是2*2*4*16,要想快速,那只能这么办。

我举个例子吧,例如 n = 13,则 n 的二进制表示为 1101, 那么 3 的 13 次方可以拆解为:

3^1101 = 3^0001 * 3^0100 * 3^1000。

我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:

int pow(int n){
    int sum = 1;
    int tmp = 3;
    while(n != 0){
        if(n & 1 == 1){
            sum *= tmp;
        }
        tmp *= tmp;
        n = n >> 1;
    }
    return sum;
}

时间复杂度近为 O(logn)

技巧:获得int、long型最大值最小值

首先要清楚的知道二者的补码

最大值:

int getMaxInt(){
        return (1 << 31) - 1;//2147483647, 由于优先级关系,括号不可省略
}

最大值:

int getMinInt(){
	return 1 << 31;//-2147483648
 }

位与&运算:

技巧:用于消去x的最后一位1

x & (x-1)

例如:

x = 1100
x-1 = 1011
x & (x-1) = 1000

应用一 :用O(1)时间检测整数n是否是2的幂次/(用O(1)时间检测是否整数n的二进制中只含一个1)。

思路解析:一位N的二进制表示中只有一个1,所以使用N&(N-1)将唯一的一个1消去。如果N是2的幂次,那么N&(N-1)得到结果为0,即可判断。

应用二:计算在一个 32 位的整数的二进制表示中有多少个 1.

思路解析:由 x & (x-1) 消去x最后一位知。循环使用x & (x-1)消去最后一位1,计算总共消去了多少次即可。

应用三:将整数A转换为B,需要改变多少个bit位

思路解析:这个应用是上面一个应用的拓展。思考将整数A转换为B,如果A和B在第i(0<=i<32)个位上相等,则不需要改变这个BIT位,如果在第i位上不相等,则需要改变这个BIT位。所以问题转化为了A和B有多少个BIT位不相同。联想到位运算有一个异或操作,相同为0,相异为1,所以问题转变成了计算A异或B之后这个数中1的个数。

技巧:判断奇偶数

判断一个数是基于还是偶数,相信很多人都做过,一般的做法的代码如下

if( (n % 2) == 1 )
    // n 是个奇数
}

如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 的话,代表是奇数,如果是 0 则代表是偶数,所以采用位运算的方式的话,代码如下:

if(n & 1 == 1){
    // n 是个奇数。
}

有人可能会说,我们写成 n % 2 的形式,编译器也会自动帮我们优化成位运算啊,这个确实,有些编译器确实会自动帮我们优化。但是,我们自己能够采用位运算的形式写出来,当然更好了。当然,(除法比位运算)时间效率也快很多,不信你去测试测试。

位 异或^ 运算:

技巧:不使用中间变量而交换两个数

交换两个数相信很多人天天写过,我也相信你每次都会使用一个额外来变量来辅助交换,例如,我们要交换 x 与 y 值,传统代码如下:

int tmp = x;
x = y;
y = tmp;

这样写有问题吗?没问题,通俗易懂。万一哪天有人要为难你,**不允许你使用额外的辅助变量来完成交换呢?

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

但是在没有时间复杂度要求的情况下,还是尽量写第一种,容易互相之间理解代码。知道这一种解法即可!

技巧:找出没有重复两(偶数)次的孤独数

136. 只出现一次的数字

给你一组整型数据:1, 2, 3, 4, 5, 1, 2, 3, 4,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。

 这道题可能很多人会用一个哈希表来做。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)了。

异或支持:

1、交换律和结合律

2、两个相同的数异或的结果是 0

3、一个数和 0 异或的结果是它本身

也就是说,那些出现了两次的数异或之后会变成0,那个出现一次的数,和 0 异或之后就等于它本身。代码如下:

int find(int[] arr){
    int tmp = arr[0];//注意,要么初始化为数组首元素,要么初始化为0,否则会导致异或得到的结果出错
    for(int i = 1;i < arr.length; i++){
        tmp = tmp ^ arr[i];
    }
    return tmp;
}
时间复杂度为 O(n),空间复杂度为 O(1)
技巧六-1:找出没有重复m(一般用于奇数,偶数也能用)次的孤独数

技巧六-2:一个整型数组里除两个孤独数之外,其他数字都出现了两次。

这种情况下,异或操作依然可以用;一次全异或操作之后,得到的两个孤独数的异或数,貌似没什么价值,而且往下好像不能往下做了。其实可以分成两组,这样就可以用技巧六做了

但是怎么分组,是个核心技巧问题。还是从得到的 “两个孤独数的异或数” 分析,毕竟这里面蕴含了两个孤独数的信息。“异或数”每一位的1表示两个孤独数在这一位是不同的。我们可以根据这一条,把两个孤独数分开。

其他:用一个整数映射一个集合的所有子集(leetcode78

用一个整数的部分二进制位映射一个集合的所有子集,然后把每一个这个整数按位解析出来。时间复杂度(2* 2^n);

我们知道一个包含n个元素的集合一共有2^n个子集,例如:

集合S包含三个元素{a, b, c},则它的所有子集为:{ }(空集), {a}, {b}, {c}, {a, b}, {a, c}, {b, c} 和{a, b, c}。

这里先用位操作的思路来求解,具体方法:用2进制Bit位来标记集合中的某个元素是否被选中,1代表选中,0代表未选中。例如集合{a, b, c}的所有子集可如下表示:

{}(空集)               0 0 0

{a}                       0 0 1

{b}                       0 1 0

{c}                       1 0 0

{a, b}                   0 1 1

{a, c}                   1 0 1

{b, c}                   1 1 0

{a, b, c}               1 1 1

分析中也可以看出一个包含N个元素的集合S有2^N个子集,非常容易想到的方法就是遍历0~2^N-1的所有整数,并转化为二进制,按以上思路输出所有子集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值