算法:位运算

算法:位运算

    • 常见位运算操作
    • 基本题型
    • 模拟加法
    • 数字查找
    • 总结


常见位运算操作

在C/C++中,有比较丰富的位运算操作符,常见的有:

&:按位与
|:按位或
~:按位取反
^:按位异或
<<:左移操作符
>>:右移操作符

这些操作符的用法就不详细讲解了,本博客主要围绕算法来讲解。

先概述一下位运算中常会用到的操作与算法。

给一个数n,求它二进制的第x位是0还是1

只需要将n右移x位,然后与1按位与即可,也就是:

(n >> x) & 1 

如果结果是1,那么第x位就是1,反之为0

给一个数n,将它二进制的第x位修改为1

如果要把第x位修改为1

  • 对于x以外的位,不能被影响,它们要与0进行按位或。

  • 而对于第x位,想要变成1,就与1按位或

也就是说我们现在要得到x位为1,其他位为0的数据,也就是1 << x

公式如下:

n |= 1 << x;

给一个数n,将它二进制的第x位修改为0

如果要把第x位修改为1

  • 对于x以外的位,不能被影响,它们要与1进行按位与。

  • 而对于第x位,想要变成0,就与0按位与

也就是说我们现在要得到x位为0,其他位为1的数据,我们可以对1 << x这个整体进行取反,得到想要的值,也就是~(1 << x)

公式如下:

n &= ~(1 << x);

给一个数n,提取它最右侧的1

想得到n最右侧的1,公式为:

n & -n;

此时整个表达式的结果,就只保留了n最右侧的1

原理如下:

复数在内存的存储机制为:源码取反,然后再+1,如果n的二进制为:00110000那么反码的推演过程为:

00110000
11001111   取反
11010000   +1

取反的时候,所有位变成了相反的位,再进行+1,此时右侧的所有1都变回了0,并且发送进位,此时最右侧的一位1被复原。

最后在和自己进行按位与,最后一位1左侧的所有数都是取反得到的,按位与结果都为0。最后一位1右侧的所有值都是0,按位与结果也为0。这样我们就得到了只保留最右侧的一位1,其余位都变成0的值了。

给一个数n,把它最右侧的1变为0

想要消除一个数最右侧的一位1,公式为:

n & (n - 1);

在执行n - 1的时候,对于二进制而言,右侧的0就会向高位借位,那么直到借到第一个1为止。最后最右侧的1就会变成0,右侧的所有0都会变成1

00110000   n
00101111   n - 1

异或运算率

按位异或的常见的运算率如下:

a ^ a = 0;
a ^ 0 = a;
a ^ b ^ c = (a ^ b) ^ c;

也就是自己与自己异或,结果为0;与0异或,值不变;以及异或的交换律。


基本题型

LeetCode 191. 位1的个数

在这里插入图片描述

本题要求我们求出一个整型中二进制的1的数目,我们可以直接遍历整数的所有位,然后只要这个位是1就计数一次:

class Solution
{
public:int hammingWeight(int n){int count = 0;for (int i = 0; i < 32; i++){if ((n >> i) & 1)count++;}return count;}
};

该方法通过整型只有32位,来进行每一个位的遍历,但是存在两个缺点:

  1. 该方法遍历方式过于暴力,把所有的位都检查了一遍
  2. 该方法只能作用于int类型数据,不具有普适性

在开头我讲解了如何删除一个整数最右侧的1,那么我们只需要每次都删除一位1,最后n 就会变成0。删除了几次·,那么就有几位1

代码如下:

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

这种写法,每一次都固定消除一位1,提高了效率。另外地,任何长度的整型都可以用这个方法来计算。

LeetCode 338. 比特位计数

在这里插入图片描述

本题要求出一个[0, n]所有值的1的个数,并返回一个数组。

对于这道题,可以用暴力的解法,直接遍历数组,每一个数字都单独求一次1的个数。

但是由于数字是连续的,其实我们可以通过前面的值来简化求后面值的操作。

比如说00010010,由于最后一位是0,其+1后,下一位数字00010011刚好比其多一位1

也就是说:一个奇数的二进制的1的个数,比前面那个数(一定是偶数)的二进制的1的个数多一个

由于奇数的最后一位一定是1,那么奇数+1一定会发生进位,进位的时候会有1会被消除,而被消除的1的数目是不确定的,因此一个偶数的二进制1的个数,与前面那个数的二进制1的个数,没有必然联系

但是一个数字×2,其二进制的表现为:所有位整体左移一位。比如00110011乘以二,得到01100110,这个过程只发生移位,1的个数不会改变。而一个偶数一定是2的倍数,所以一个偶数n的二进制的1的个数,等于n/2的二进制的1的个数

代码逻辑如下:

  1. 遍历[0, n]
  • 如果当前值x是偶数,位1的个数等于2/x的位1的个数
  • 如果当前值x是奇数,位1的个数比x - 1的位1的个数多一个

代码如下:

class Solution 
{
public:vector<int> countBits(int n){vector<int> ret(n + 1);ret[0] = 0;for(int i = 1; i < n + 1; i++){if (i % 2 == 0) // 偶数ret[i] = ret[i / 2];else // 奇数ret[i] = ret[i - 1] + 1;}return ret;}
};

LeetCode 461. 汉明距离

在这里插入图片描述

本题要求求出两个整数的不同的位的个数,提到不同的位,这就很明显要使用按位异或了:相同为0,相异为1

因此我们只需要先让两个数按位异或,然后求结果的1的个数,就是两个数中不同位的个数。

代码如下:

class Solution 
{
public:int hammingDistance(int x, int y){int n = x ^ y;int ret = 0;while (n){n &= n - 1;ret++;}return ret;}
};

模拟加法

LeetCode 371. 两整数之和

在这里插入图片描述

本题要求不用+-完成两数的加法。想要解决这道题,那就要再理解一下按位异或的其他含义了。

按位异或基本规则为:相同为0,相异为1,从数学角度也可以理解为不进位加法

比如0011001101010101进行异或:

00110011
01010101
---------
01100110

两个数的最低位都是1,如果执行加法,那么1 + 1 = 2会导致进位,本位余0,向上进1 。但是按位异或 只余0,不进1

两数的第二位,分别是01,如果执行加法,0 + 1 = 1本位余1,不进位。按位异或也可以理解为只余1,不进位

因此,按位异或可以理解为一个不进位的加法

那么现在给我们两个数ab,我们要用位运算来模拟加法,现在我们可以通过按位异或执行一个不进位的加法。那么进位应该怎么办呢?

如果在二进制计算中发生了进位,那么两个位一定都是1,进位也就是把多出来的1加到高位去。因此我们可以通过按位与a & b,得到两个位都为1的位,也就是需要进位的位。然后将其左移一位(a & b) << 1,来模拟进位

代码:

class Solution 
{
public:int getSum(int a, int b) {while (b){int tmp = a ^ b; // 不进位加法int carry = (a & b) << 1; // 得到进位a = tmp;b = carry;}return a;}
};

我来解析一下以上代码:

在每一轮循环中,先通过a ^ b得到不进位的加法,然后通过(a & b) << 1得到进位。那么现在的任务就是把进位carry加到tmp中,但是我们不能用+,来进行carry + tmp的操作。

这样我们的问题从a + b转化为了carry + tmp。于是把tmp交给acarry交给b,利用循环继续以上的模拟进位操作,来完成carry + tmp

carry0,也就是没有进位的时候,carry + tmp = tmp,此时就可以退出循环,得到最终结果了。

b = 0,那么就是carry = 0,此时最终结果就是tmp,由于最后我们会把tmp赋值给a,所以return a


数字查找

LeetCode 136. 只出现一次的数字

在这里插入图片描述

本题要求我们在一堆数字中,找到只出现一次的数字,对于这种在一堆数字中查找出现次数比较特殊的题型,都可以优先考虑用位运算

这种题型,都要用到异或的运算率:

a ^ a = 0;
a ^ 0 = a;
a ^ b ^ c = (a ^ b) ^ c;

以上运算率中,可以提取出一个重要信息:将一堆数字异或在一起,出现偶数次的数字,会变成0。最后变成所有出现次数为奇数的数字互相异或。

比如:a ^ b ^ b = a ,因为a出现一次,为奇数次,b出现两次,为偶数次,最后两个b抵消,变成a ^ 0,也就是a

再比如:c ^ a ^ b ^ b ^ c ^ c = a ^ c,因为ac都是出现了奇数次,b出现了偶数次,最后就只剩下ac进行异或。

而对于本题来说,我们要求的数字只出现一次,为奇数次;其他数字都出现两次,为偶数次。我们只需要遍历数组,把所有值异或起来,除了目标值以外的值都被抵消掉了,最后结果就是目标值

代码:

class Solution 
{
public:int singleNumber(vector<int>& nums) {int ret = 0;for (auto& e : nums) // 遍历数组ret ^= e;return ret;}
};

LeetCode 260. 只出现一次的数字 III

在这里插入图片描述

本题中有两个数字只出现了一次,其他数字都出现了两次。

与之前思路一样,假设我们要求的目标值为ab,把所有值都异或起来,结果就是a ^ b

那么问题就来了,我们要如何从a ^ b中拆分出ab呢?

按位异或的规则为:相同为0,相异为1,也就是说a ^ b中为1的地方,就是ab不同的位,我们可以通过这个位来区分两者。

假设我们求出了,a ^ b的第x位为1,那么整个数组的数据就可以拆分为两份:第x位为1,第x位为0。而ab就分别在这两份中。假设a在第一份元素中,b在第二份元素中。

第x位为1a + 其它第x位为1
第x位为0b + 其它第x位为0

因为除了ab,其他数字都出现两次,相同的数字第x位也相同,会被分到同一份中。

因此第一份数据中,除了a以外,其他数字都出现两次;第二份数据中,除了b以外,其他数字也出现两次。

我们将第一份数据中所有元素异或起来,得到的就是a,第二份数据中的所有元素异或起来,得到的就是b

逻辑:

  1. 先把整个数组按位异或,得到a ^ b
  2. 找出a ^ b中,任意一个为1的位
  3. 根据这个位把数组划分为两份
  4. 每一份中分别按位异或,得到的结果就分别是ab

代码:

class Solution 
{
public:vector<int> singleNumber(vector<int>& nums){int tmp = 0;for (auto& e : nums) // 按位异或整个数组tmp ^= e;vector<int> ret = { tmp, tmp };int x = (tmp == INT_MIN) ? tmp : tmp & (-tmp); // 得到a ^ b 中的一位1for (auto& e : nums) // 遍历数组{if (e & x) // 第一份数据,第x位为1ret[0] ^= e;else // 第二份数据,第x位为0ret[1] ^= e;}return ret;}
};

这里我额外说明一句代码:

int x = (tmp == INT_MIN) ? tmp : tmp & (-tmp); // 得到a ^ b 中的一位1

其目的在于,得到a ^ b中的最右侧的一位1,也就是tmp & -tmp,并赋值给x。但是如果tmp的值是int类型的最小值,其二进制就是:

10000000 00000000 00000000 00000000 

也就是,除了第一位,其它位都是0,此时-tmp是超过了int的存储范围的,会发生进位

其取反,+1后为:

11111111 11111111 11111111 11111111  //取反
1 00000000 00000000 00000000 00000000   //+1

可以看出来发生了越界。

而我们的目的只是为了提取tmp最右侧的一个1,如果tmpINT_MIN,本身就只有一个1,因此不用额外提取,直接x = tmp即可。

LeetCode 137. 只出现一次的数字 II

在这里插入图片描述

本题中,目标元素出现一次,其它元素出现三次,都是奇数次,那么我们就不能用异或来区分它们了。

这种情况下,就要用位图的思想,来单独统计每一个位的情况

因为输入的数据是int类型,那么就固定只有32个位,对于每个位,我们都单独统计。

创建一个32个元素的整型数组bitmap,每个元素代表一个比特位。

遍历数组,对于每个元素,将其所有位的值加进bitmap中。

比如某个元素第0位是1,那么bitmap[0] += 1,第1位为0,那么bitmapp[1] += 0,以此类推,直到第31位。每个元素都执行一次该操作。

最后bitmap数组中,就统计到了对应位上出现的1个数。由于其它元素都出现了三次,对于每一位数据,都是3的倍数 + 目标值在该位的值0/1

那么bitmap[i] % 3就可以还原出目标元素第i位是0还是1

代码逻辑:

  1. 创建一个bitmap数组,存储每一位上的1的个数
  2. 遍历数字,把每一个元素的比特位加到bitmap
  3. bitmap中每个元素都%3得到原始数据,此时整个数组都由10组成,也就是目标元素的二进制值

代码:

class Solution
{
public:int singleNumber(vector<int>& nums){vector<int> bitmap(32);int ret = 0;for (auto& e : nums) // 遍历数组{for (int i = 0; i < 32; i++) // 遍历每个元素的32个比特位{bitmap[i] += (e >> i) & 1; // 将第i位数据,加到bitmap的第i个元素中}}for (int i = 0; i < 32; i++) // 遍历bitmap{ret |= (bitmap[i] % 3) << i; // %3还原出目标元素的二进制}return ret;}
};

总结

常见运算

&:按位与
|:按位或
~:按位取反
^:按位异或
<<:左移操作符
>>:右移操作符

给一个数n,求它二进制的第x位是0还是1

(n >> x) & 1 

给一个数n,将它二进制的第x位修改为1

n |= 1 << x;

给一个数n,将它二进制的第x位修改为0

n &= ~(1 << x);

给一个数n,提取它最右侧的1

n & -n;

给一个数n,把它最右侧的1变为0

n & (n - 1);

异或运算率

a ^ a = 0;
a ^ 0 = a;
a ^ b ^ c = (a ^ b) ^ c;

模拟加法

按位异或^可以视为无进位加法,而(a & b) << 1可以求出进位,两者配合可以模拟加法操作。

查找数字

  • 如果目标元素与其它元素奇偶性不同,通过异或的运算律求解;如果目标值有两个,利用按位异或的特性找出两个目标元素不同的位,然后利用这个位把数组划分为两份,再进行分别查找

  • 如果目标元素与其它元素奇偶性相同,利用位图思想,把每个比特位的1的个数求出来,然后想办法利用%来消除其它元素的影响,最后位图中只留下目标元素的二进制值


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/815219.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQ数据库: MySQL数据库的安装配置 ,图文步骤详细,一篇即可完成安装完成! MySQL数据库如何与客户端连接

LiuJinTao&#xff1a; 2024年4月14日 文章目录 MySQL的安装配置1. 下载2. 安装 三、 MySQL 启动与停止1. 第一种 方式&#xff1a;2. 第二种方式&#xff1a; 四、MySQL 客户端连接2. 方式二&#xff1a; MySQL的安装配置 1. 下载 官方下载网址&#xff1a;https://www.mysq…

代码随想录刷题随记21-回溯1

代码随想录刷题随记21-回溯1 回溯法解决的问题 回溯法&#xff0c;一般可以解决如下几种问题&#xff1a; 组合问题&#xff1a;N个数里面按一定规则找出k个数的集合 切割问题&#xff1a;一个字符串按一定规则有几种切割方式 子集问题&#xff1a;一个N个数的集合里有多少符…

可视化大屏C位图:​地理信息—地球焦点图

Hello&#xff0c;我是大千UI工场&#xff0c;本期可视化大屏的焦点图&#xff08;C位&#xff09;分享将地球作为焦点图的情形&#xff0c;欢迎友友们关注、评论&#xff0c;如果有订单可私信。 将地球作为可视化大屏焦点图可以有以下几个作用&#xff1a; 全球数据展示&…

蓝桥杯嵌入式(G431)备赛笔记——DMA+ADC(单通道+多通道)

单通道&#xff1a; 开启循环模式&#xff0c;两个参数设为word u32 adc_tick0; u32 r37_value0; u32 r38_value0; float r37_volt0; float r38_volt0;//DMAADCvoid DMA_ADC() {if(uwTick-adc_tick<100) return;adc_tick uwTick;HAL_ADC_Start_DMA(&hadc2, &r37_v…

Python学习笔记19 - 类与对象

类的创建 对象的创建 类属性、类方法、静态方法 动态绑定属性和方法 面向对象的三大特征 封装&#xff1a;提高程序的安全性 继承&#xff1a;提高代码的复用性 多态&#xff1a;提高程序的可扩展性和可维护性 类的常用的特殊属性 类的常用的特殊方法 –add–() –len–() –…

Java中创建多线程的方法

继承Thread类&#xff0c;对该类进行new一个实例&#xff0c;对实例调用start方法&#xff0c;重写run方法。 缺点&#xff1a;单继承&#xff0c;无法继承 public class myThread extends Thread {public static void main(String[] args) {myThread myThread new myThread()…

Netty学习——实战篇1 BIO、NIO入门demo 备注

1 BIO 实战代码 Slf4j public class BIOServer {public static void main(String[] args) throws IOException {//1 创建线程池ExecutorService threadPool Executors.newCachedThreadPool();//2 创建ServerSocketServerSocket serverSocket new ServerSocket(8000);log.in…

【嵌入式基础知识学习】AD/DA—数模/模数转换

AD/DA—数模/模数转换概念 数字电路只能处理二进制数字信号&#xff0c;而声音、温度、速度和光线等都是模拟量&#xff0c;利用相应的传感器&#xff08;如声音用话筒&#xff09;可以将它们转换成模拟信号&#xff0c;然后由A/D转换器将它们转换成二进制数字信号&#xff0c…

音视频学习—音视频理论基础(1)

音视频学习—音视频理论基础&#xff08;1&#xff09; 一、音视频处理流程1.1 音频处理流程1.2 视频处理流程1.3 音视频数据流转1.4 为什么音视频采集完之后&#xff0c;不能直接传输&#xff0c;要进行编码&#xff1f;1.5 模数转换1.6 PCM1.7 WAV 总结 一、音视频处理流程 音…

漫途水产养殖水质智能监测方案,科技助力养殖业高效生产!

随着水产养殖业的蓬勃发展&#xff0c;水质和饲料等多重因素逐渐成为影响其持续健康发展的关键因素。由于传统养殖模式因监控和调节手段不足&#xff0c;往往造成养殖环境的恶化。需要通过智能化养殖&#xff0c;调控养殖环境&#xff0c;实现养殖的精细化管理模式&#xff0c;…

Bug的定义生命周期

1、bug的定义 你们觉得bug是什么? 软件的Bug狭义概含是指软件程序的漏洞或缺陷&#xff0c; 广义概念除此之外还包括测试工程师或用户所发现和提出的软件可改进的细节(增强性&#xff0c;建议性)、或 与需求文档存在差异的功能实现等。 我们的职责就是&#xff0c;发现这些B…

Orangepi Zero2 wiringPi外设库SDK安装

文章目录 1. sdk 下载2. sdk 使用 1. sdk 下载 1、使用git 下载 # apt-get update # apt-get install -y git # git clone https://github.com/orangepi-xunlong/wiringOP.git2、手动下载并上传 下载连接 https://github.com/orangepi-xunlong/wiringOP 选master分支 上传到…

【vue】跨组件通信--依赖注入

import { provide,inject } from vue provide&#xff1a;将父组件的数据传递给所有子组件&#xff08;子孙都有&#xff09;inject&#xff1a;接收provide 项目文件结构 App.vue是Header.vue的父组件&#xff0c;Header.vue是Nav.vue的父组件 传值过程 App.vue <tem…

Python学习笔记20 - 模块

什么叫模块 自定义模块 Python中的包 Python中常用的内置模块 第三方模块的安装与使用

计算机网络——DHCP协议

前言 本博客是博主用于复习计算机网络的博客&#xff0c;如果疏忽出现错误&#xff0c;还望各位指正。 这篇博客是在B站掌芝士zzs这个UP主的视频的总结&#xff0c;讲的非常好。 可以先去看一篇视频&#xff0c;再来参考这篇笔记&#xff08;或者说直接偷走&#xff09;。 …

funasr 麦克风实时流语音识别;模拟vad检测单独输出完整每句话

参考: https://github.com/alibaba-damo-academy/FunASR chunk_size 是用于流式传输延迟的配置。[0,10,5] 表示实时显示的粒度为 1060=600 毫秒,并且预测的向前信息为 560=300 毫秒。每个推理输入为 600 毫秒(采样点为 16000*0.6=960),输出为相应的文本。对于最后一个语音…

【学习】软件测试需求分析要从哪些方面入手

软件测试需求分析是软件测试过程中非常重要的一个环节&#xff0c;它是为了明确软件测试的目标、范围、资源和时间等要素&#xff0c;以确保软件测试的有效性和全面性。本文将从以下几个方面对软件测试需求分析进行详细的阐述&#xff1a; 一、软件测试目标 软件测试目标是指…

机器学习周记(第三十四周:文献阅读[GNet-LS])2024.4.8~2024.4.14

目录 摘要 ABSTRACT 1 论文信息 1.1 论文标题 1.2 论文摘要 1.3 论文模型 1.3.1 数据处理 1.3.2 GNet-LS 2 相关代码 摘要 本周阅读了一篇时间序列预测论文。论文模型为GNet-LS&#xff0c;主要包含四个模块&#xff1a;粒度划分模块&#xff08;GD&#xff09;&…

RabbitMQ消息模型之Work消息模型

Work消息模型 * work模型&#xff1a; * 多个消费者消费同一个队列中的消息&#xff0c;每个消费者获取到的消息唯一&#xff0c;且只能消费一次 * 作用&#xff1a;提高消息的消费速度&#xff0c;避免消息的堆积 * 默认采用轮询的方式分发消息 * 如果某…

无人机/飞控--ArduPilot、PX4学习记录(5)

这几天看dronekit&#xff0c;做无人机失控保护。 PX4官网上的经典案例&#xff0c;我做了很多注解&#xff0c;把代码过了一遍。 无人机具体执行了&#xff1a; 先起飞&#xff0c;飞至正上空10m->向北移动10m->向东移动10m->向南移动10m->向西移动10m->回到初…