作者 | bigsai  责编 | 张文

头图 | CSDN 下载自视觉中国

位运算隐藏在编程语言的角落中,其神秘而又强大,暗藏内力,有些人光听位运算的大名的心中忐忑,还有些人更是一看到位运算就远远离去,我之前也是。但狡猾的面试官往往喜欢搞偷袭,抓住我们的弱点搞我们,为了防患于未然,特记此篇!

本篇的内容为位运算的介绍和一些比较经典的位运算问题进行介绍分析,当然,位运算这么牛,后面肯定还是要归纳总结的。

认识位运算

什么是位运算?

程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算就是直接对整数在内存中的二进制位进行操作。

位运算就是直接操作二进制数,那么有哪些种类的位运算呢?

常见的运算符有与(&)、或(|)、异或(^)、取反(~)、左移(<<)、右移(>>是带符号右移 >>>无符号右移动)。下面来细看看每一种位运算的规则。

位运算 & (与)

规则:二进制对应位两两进行逻辑AND运算(只有对应位的值都是 1 时结果才为 1, 否则即为 0),即 0&0=0,0&1=0,1&1=1

例如:2 & -2

位运算 | (或)

规则:二进制对应位两两进行逻辑或运算(对应位中有一 个为1则为1) ,即0|0=0,0|1=1,1|1=1

例如:2 | -2

位运算 ^ (异或)

规则:二进制对应位两两进行逻辑 XOR (异或) 的运算(当对应位的值不同时为 1, 否则为 0),即0^0=0, 0^1=1, 1^1=0

例如:2 ^ -2

按位取反~

规则:二进制的 0 变成 1,1 变成 0。

移位运算符

左移运算<<:左移后右边位补 0

右移运算>>:右移后左边位补原最左位值(可能是 0,可能是 1)

右移运算>>>:右移后左边位补 0

  • 对于左移运算符<<没有悬念右侧填个零无论正负数相当于整个数乘以 2。

  • 而右移运算符就有分歧了,分别是左侧补 0>>>和左侧补原始位>>,如果是正数没争议左侧都是补 0,达到除以 2 的效果;如果是负数的话左侧补 0>>>那么数值的正负会发生改变,会从一个负数变成一个相对较大的正数。而如果是左侧补原始位(负数补1)>>那么整个数还是负数,也就是相当于除以 2 的效果。

下面这张图可以很好的帮助你理解负数的移位运算符:

到这里,我想你应该对位运算有了初步的认识,在这里把上面提到的部分案例执行对比一下让你看一下可能会理解的更清晰:

位运算小技巧

在这里有些常用的位运算小技巧。

判断奇偶数

正常判断奇数偶数的时候我们会这样写:

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

使用位运算可以这么写:

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

其核心就是判断二进制的最后一位是否为 1,如果为 1 那么结果加上 2^0=1一定是个奇数,否则就是个偶数。

交换两个数

对于传统的交换两个数,我们需要使用一个变量来辅助完成操作,可能会是这样:

int team = a;a = b;b = team;

但是使用位运算可以不需要借助额外空间完成数值交换:

a=a^b;//a=a^bb=a^b;//b=(a^b)^b=a^0=aa=a^b;//a=(a^b)^(a^b^b)=0^b=0

原理已经写在注释里面了,是不是感觉非常强呢?

二进制枚举

在遇到子集问题的处理时候,我们有时候会借助二进制枚举来遍历各种状态(效率大于 dfs回溯)。这种就属于排列组合的问题了,对于每个物品(位置)来说,就是使用和不使用的两个状态,而在二进制中刚好可以用 1 和 0 来表示。而在实现上,通过枚举数字范围分析每个二进制数字各符号位上的特征进行计算求解操作即可。

二进制枚举的代码实现为:

for(int i = 0; i < (1<<n); i++) //从0~2^n-1个状态{for(int j = 0; j < n; j++) //遍历二进制的每一位 共n位  {if(i & (1 << j))//判断二进制数字i的第j位是否存在    {//操作或者输出    }  }}

位运算经典问题

有了上面的位运算基础,我们怎么用位运算处理实际问题呢?或者有哪些经典的问题可以用位运算来解决呢。

不用加减乘除做加法

题目描述:写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

分析:这道题咋一听可能没啥思路,简单研究一下位运算还是能独立推出来和理解的。

当然,解决这题前,需要了解上面的四种位运算。还要知道二进制的运算:0+0=0,0+1=1,1+1=0(进位)

对于加法的一个二进制运算。如果不进位那么就是非常容易的。这时候相同位都为 0 则为 0,0 和 1 则为 1。满足这种运算的异或(不相同取 1,相同取 0)和或(有一个 1 则为 1)都能满足.

但事实肯定有进位的运算啊!看到上面操作的不足之后,我们肯定还需要解决进位的问题对于进位的两数相加,这种核心思想为:

  • 用两个数,一个正常 m 相加(不考虑进位的)。用异或 a^b 就是满足这种要求,先不考虑进位(如果没进位那么就是最终结果)。另一个专门考虑进位的 n。两个 1 需要进位。所以我们用 a&b 与记录需要进位的。但是还有个问题,进位的要往上面进位,所以就变成这个需要进位的数左移一位。

  • 然后就变成 m+n 重新迭代开始上面直到不需要进位的(即 n=0 时候)。

实现代码为:

public class Solution {public int Add(int num1,int num2) {/*   *  5+3   5^3(0110)   5&3(0001)    *  0101       *  0011    */int a=num1^num2;int b=num1&num2;  b=b<<1;if(b==0)return a;else {return Add(a, b);  }          }}

当然,这里也可以科普一下二进制求加法:average = (a&b) + ((a^b)>>1) ;

二进制中 1 的个数

这是一道经典题,其具体题目要求输入一个整数,输出该数二进制表示中 1 的个数(其中负数用补码表示)。

对于这个问题,不用位运算将它转成二进制字符串直接枚举字符'1'的个数也可以直接求出来,但是这样做是没有灵魂的并且效率比较差。这里提供两种解决思路

法一:大家知道每个类型的数据它的背后实际都是二进制操作。大家知道 int 的数据类型的范围是(-2^31,2^31 -1)。并且 int 有 32 位。但是并非 32 位全部用来表示数据。真正用来表示数据大小的也是 31 位。最高位用来表示正负。

首先要知道:

其次还要知道位运算&与。两个十进制与运算.每一位同 1 为 1。所以我们用 2 的正数次幂与知道的数分别进行与运算操作。如果结果不为 0,那么就说明这位为 1。(前面 31 个都是大于 0 的,最后一个与结果是负数但是如果该位为 1,那么结果肯定不为 0)

具体代码实现为:

public int NumberOf1(int n) {int va=0;for(int i=0;i<32;i++)  {if((n&(1<<i))!=0)    {                 va++;    }  }return va;       }

法二是运用 n&(n-1)。n 如果不为 0,那么 n-1 就是二进制,第一个为 1 的变为0,后面全为 1。

这样的 n&(n-1)一次运算就相当于把最后一个 1 变成 0。这样一直到运算的数为 0 停止计算次数就好了,如下图共进行三次运算那么 n 的二进制中就有三个 1。

实现代码为:

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

只出现一次的(一个)数字①

问题描述:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:你的算法应该具有线性时间复杂度。你可以不使用额外空间来实现吗?

分析:这是一道简单的面试题,面试官常问怎么样用不太复杂的方法找出数组中仅出现一次的数字(其他均出现两次),暴力枚举或者使用其他的存储结构都不够优化,而这个问题最高效的答案就是使用位运算。

首先你要注意两点:

  • 0 和任意数字进行异或操作结果为数字本身.

  • 两个相同的数字进行异或的结果为 0.

具体的操作就是用 0 开始和数组中每个数进行异或,得到的值和下个数进行异或,最终获得的值就是出现一次(奇数次)的值。

class Solution {public int singleNumber(int[] nums) {int value=0;for(int i=0;i<nums.length;i++)        {value^=nums[i];        }return value;    }}

只出现一次的(一个)数字②

问题描述:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

说明:你的算法应该具有线性时间复杂度。你可以不使用额外空间来实现吗?

分析:这题和上一题的思路略有不同,这题其他数字出现了 3 次。那么我们如果直接使用位运算异或操作的话无法直接找到结果,就需要巧妙的运用二进制的其他特性:判断整除求余操作。即判断所有数字二进制 1 的总个数和 0 的总个数,一定有一个不是三的整数倍,如果 0 不是三的整数倍那么就说明结果的该位二进制数字为 0,同理否则为 1。

在具体的操作实现上,问题中给出数组中的数据在 int 范围之内,那么我们就可以在实现上可以对 int 的 32 个位每个位进行依次判断,该位 1 的个数求余 3 后是否为 1,如果为 1 说明结果该位二进制为 1,可以将结果加上去。最终得到的值即为答案。

具体代码为:

class Solution {public int singleNumber(int[] nums) {int value=0;for(int i=0;i<32;i++)        {int sum=0;for(int num:nums)            {if(((num>>i)&1)==1)                {                    sum++;                }            }if(sum%3==1)value+=(1<<i);        }return value;    }}

只出现一次的(两个)数字③

题目描述:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

思路:上面的问题处理和理解起来可能比较容易,但是这个问题可能稍微复杂一点,但是这题可以通过特殊的手段转化为上面只出现一次的一个数字问题来解决,当然核心的位运算也是异或^。

具体思路就是想办法将数组逻辑上一分为二!

先异或一遍到最后得到一个数,得到的肯定是 a^b (假设两个数值分别为a和b)的值。在看异或^的属性:不同为 1,相同为 0。 也就是说最终这个结果的二进制为 1 的位置上 a 和 b 是不相同的。而我们可以找到这个第一个不同的位,然后将数组中的数分成两份,该位为 0 的进行异或求解得到其中一个结果 a,该位为 1 的进行异或求解得到另一个结果 b。

具体可以参考下图流程:

实现代码为:

public int[] singleNumbers(int[] nums) {int value[]=new int[2];if(nums.length==2)return  nums;int val=0;//异或求的值for(int i=0;i<nums.length;i++)    {        val^=nums[i];    }int index=getFirst1(val);int num1=0,num2=0;for(int i=0;i<nums.length;i++)    {if(((nums[i]>>index)&1)==0)//如果这个数第index为0 和num1异或            num1^=nums[i];else//否则和 num2 异或            num2^=nums[i];    }value[0]=num1;value[1]=num2;return  value;}
private int getFirst1(int val) {int index=0;while (((val&1)==0&&index<32))    {        val>>=1;// val=val/2        index++;    }return index;}

结语

当然,上面的问题可能有更好的解法,也有更多经典位运算问题将在后面归纳总结,希望本篇的位运算介绍能够让你有所收获,对位运算能有更深一点的认识。

☞小米回应造车传闻;饿了么就春节“套路”骑手活动致歉;Go 1.16 正式发布 | 极客头条☞破圈!不止于浏览器,WebAssembly 2020 大事记☞25 万行逆向源码遭下架!
☞Linux 登陆火星,占有率再超 Windows,NASA 还在 GitHub 发布源码教程

你可能不知道的位运算技巧相关推荐

  1. 漫画:位运算技巧整理汇总+一道被嫌弃的题目

    (这首歌挺喜欢的...分享出来) 今天是小浩算法"365刷题计划"第65天.这两天总有人来问我,做公众号赚了多少钱,或者就是怎么能和你一样,2个月就做到7000粉丝.说实话,至少到 ...

  2. 那些你所不知道的arXiv使用技巧

    作者:Tom Hardy Date:2020-12-23 来源:那些你所不知道的arXiv使用技巧

  3. python写整数逆位运算_简单了解python的一些位运算技巧

    前言 位运算的性能大家想必是清楚的,效率绝对高.相信爱好源码的同学,在学习阅读源码的过程中会发现不少源码使用了位运算.但是为啥在实际编程过程中应用少呢?想必最大的原因,是较为难懂.不过,在面试的过程中 ...

  4. 你所不知道的模块调试技巧 - npm link #17

    你所不知道的模块调试技巧 - npm link #17 1. 背景 node 应用开发中,我们不可避免的需要使用或拆分为 npm 模块,经常遇到的一个问题是: 新开发或修改的 npm 模块,如何在项目 ...

  5. 你可能不知道的 CSS 阴影技巧与细节

    关于 CSS 阴影,之前已经有写过一篇,box-shadow 与 filter:drop-shadow 详解及奇技淫巧[1],介绍了一些关于 box-shadow 的用法. 最近一个新的项目,CSS- ...

  6. 使用位运算技巧比较两个数中较大的数

    使用位运算技巧比较两个数中较大的数 作者:Grey 原文地址: 博客园:使用位运算技巧比较两个数中较大的数 CSDN:使用位运算技巧比较两个数中较大的数 题目要求 如何不要用任何比较判断符(>, ...

  7. 被低估的css滤镜,你所不知道的 CSS 滤镜技巧与细节

    本文主要介绍 CSS 滤镜的不常用用法,希望能给读者带来一些干货! 系列 CSS 文章汇总在我的 Github ,持续更新,欢迎点个 star 订阅收藏. OK,下面直接进入正文.本文所描述的滤镜,指 ...

  8. 14个你可能不知道的JavaScript调试技巧

    以更快的速度和更高的效率来调试JavaScript 熟悉工具可以让工具在工作中发挥出更大的作用.尽管江湖传言 JavaScript 很难调试,但如果你掌握了几个技巧,就能用很少的时间来解决错误和bug ...

  9. 七大你可能不知道的 Chrome 使用技巧

    随着Chromium/Chrome以及其衍生的浏览器用户越来越多,掌握一些技巧可以让你更加快速熟练的使用Chrome,让事情变得更简单. 微 软 IE 一手遮天的时代已经一去不复返了,而在当下浏览器的 ...

最新文章

  1. text段,data段,bss段,堆和栈
  2. Windows内核实验003 再次回到中断
  3. 权限控制相关数据表分析和创建
  4. 《Python Cookbook 3rd》笔记(1.11):命名切片
  5. 通过阿里OSS文件服务返回的URL获取文件流下载
  6. easyui-combobox之setText与setValue先后之分
  7. 怎么彻底粉碎文件夹?文件还能恢复吗?操作过程在这里
  8. SSSP整合分页CRUD
  9. c语言文件压缩怎么求压缩比,文件压缩C语言程序设计报告.doc
  10. passive-interface 总结整理
  11. 计算机网络安全(三)
  12. Access根据出生日期计算年龄_小技巧 | 在SPSS中根据出生日期计算年龄
  13. 大白话Vue之publicPath
  14. Win10删除|修改鼠标右键快捷键快捷菜单的操作
  15. 【Javascript】函数中的arguments
  16. 为什么使用多级页表?
  17. BlogBus居然没有人谈及‘魔方’
  18. Linux锁定账号,禁止登录系统设置
  19. JSJ-3/AC220V时间继电器
  20. 历年诺贝尔1901—2021

热门文章

  1. Java工程师必学知识点【吊打面试官系列】
  2. Kaggle 注册手机号以获取GPU资源
  3. 敏捷开发一千零一问系列之九:总体架构什么时机进行?(上)
  4. python中自定义超时异常的几种方法
  5. ZBrush中如何实现智能对称
  6. 搭配和谐的色彩的秘密
  7. 深入浅出SharePoint2010——请假系统无代码篇之表单设计
  8. Ubuntu13.04配置优化(四)转贴
  9. 《TensorFlow 2.0深度学习算法实战教材》学习笔记(九、卷积神经网络)
  10. Openstack Fuel 9.0安装后网卡设置