直面位运算

        在这篇文章之前,笔者只是简单了解过位运算相关概念,但是每次刷LeetCode碰到位运算相关题目,都会敬而远之。一方面是觉得看起来晦涩难懂,另一方面觉得日常开发用处不大。

        近期本着学习的目的,静下心来研究了一下,感觉位运算确实巧妙,而且涉及知识点还不少,这里来总结一下初步接触位运算的收获。本文是基于JavaScript来进行讲解。


什么是位运算

        所谓位运算,就是对二进制位进行操作的运算方式。

        例如我们需要对「15」这个10进制整数进行位运算。位运算符首先会将15转换成32位的二进制串,然后再对二进制进行运算。运算后得出结果,会再次转换成标准的十进制数字,然后返回。

   例:     15 => '0000 0000 0000 0000 0000 0000 0000 1111'

首先,这里有几个知识点需要记住:

        1、位运算会将数值会转成32位的二进制。对任何数进行位运算都会转换成一个固定长度为32位的由0和1组成的二进制串。如果有一个整数很大,转换成二进制之后,位数超过32位了,那么超出的部分将会被丢弃,这样在做位运算的时候结果肯定是不准确的。因此在使用位运算的时候,确保要处理的数值转换成二进制之后不会超过32位的范围。安全范围也就是[-2147483648, 2147483647],也可以说(-2)^31 至 (2^31) -1。

        2、第一位是符号位。在JavaScript中,位运算的32位二进制是包括符号位的。第一位通常被用作符号位,用来确定数据的正负,0表示正数,1表示负数。

        3、负数用补码表示。计算机内部是用补码来表示负数的。简单来说补码就是原码取反后+1。二进制原码补码相关规则忘记的小伙伴自行百度,这里不再详细介绍。

示例: -1 的二进制是什么呢。

首先-1用原码表示为”10000000 00000000 00000000 00000001“

然后按位取反(除符号位):"11111111 11111111 11111111 11111110"

最后再加1。

因此-1用二进制表示为:”11111111 11111111 11111111 11111111“

        至于位运算为什么必须是32位,只能说是出于规范的一致性,以及简化底层数据处理。

扩展知识点:

        1、在js中,将一个正整数转换成二进制可以通过toString方法

let a = 15;
a.toString(2);    // '1111'

        2、js中并没有直接返回一个负数二进制的方法,如果想要返回一个负数的二进制,则需要根据补码的规则编写一个简单函数来返回。

常见的位运算

        常见的位运算包括:按位与(AND)、按位或(OR)、按位异或(XOR)、按位取反(NOT)、左移(Shift Left)和右移(Shift Right)

        1、按位与( & ):两个二进制位都为1时,结果才为1。

        2、按位或( | ):两个二进制位只要有一个为1,结果就为1。

        3、按位异或( ^ ):两个二进制位必须一个为1,一个为0,结果才为1。( 任何数跟0异或都是本身,跟本身异或都为0)

        4、按位取反( ~ ):如果一个二进制位为1,结果为0;如果为0,结果为1。

        5、左移( << ):将二进制整体向左移动。右侧用0填充

        6、有符号右移( >> ):将二进制整体向右移动,左侧用符号位填充(符号位是0填充0,符号位是1填充1)。

        7、无符号右移( >>> ):将二进制整体向右移动(符号位看做普通二进制位),左侧用0填充。

实战讲解

        掌握了上面这些基础的概念之后,我们来做几道算法题。

题目1:LeetCode上第136题《只出现一次的数字》

        给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

        你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

示例 1 :

        输入:nums = [2,2,1]
        输出:1
示例 2 :

        输入:nums = [4,1,2,1,2]
        输出:4
示例 3 :

        输入:nums = [1]
        输出:1

提示:

        1 <= nums.length <= 3 * 10^4
        -3 * 10^4 <= nums[i] <= 3 * 10^4
        除了某个元素只出现一次以外,其余每个元素均出现两次。

思路分析:

        如果没有空间复杂度限制的话,我们可以定义一个set。只需遍历一遍数组。判断set是否存在,不存在则添加;存在则删除。最终set中剩下的肯定是只出现一次的数字。

        但是题目要求我们只能用常量额外空间来处理。同时根据提示中的范围,我们知道数组中的整数,没超过位运算的安全范围,所以我们可以考虑使用位运算来做。

        考虑到位运算,我们根据上文中异或运算的特性——任何数和0异或等于本身,和本身异或等于0。因此如果我们对数组中的每个数都进行一次异或。重复的肯定消失,最后剩下的则是那唯一的值。

        代码如下:

var singleNumber = function (nums) {let result = 0;for (const item of nums) {result = result ^ item;}return result;
};
题目2:LeetCode上第137题《只出现一次的数字 II》

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。

示例 1:

        输入:nums = [2,2,3,2]
        输出:3
示例 2:

        输入:nums = [0,1,0,1,0,1,99]
        输出:99

提示:

        1 <= nums.length <= 3 * 10^4
        -2^31 <= nums[i] <= 2^31 - 1
        nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次

思路分析:

        跟第一题同理,如果没有空间复杂度限制的话,这题目也比较简单。首先遍历一遍数组,定义一个map记录一下出现次数,然后再遍历一遍,输出次数为1的元素即可。

        根据提示给出的范围,正好满足位运算的安全范围。不过这题相对第一题来说复杂一点,我们来提供一种思路。

        首先根据位运算的规则我们知道,在进行位运算的时候,会将整数转换成2进制后进行运算,因此我们将示例1的输入值 [ 2,2,3,2 ] 转换成二进制如下所示。 

        根据上图我们能够发现,最后一列有1个1,倒数第2列有4个1。

        假如有一个数出现了3次,那么在相对应的2进制位上,肯定也会出现3次。

        接着我们统计一下每一个二进制位1的个数,然后跟3取余,最后结果就是要找的那个数。

        如上图所示,每一位和3取余之后,结果为 "0011" 。而"0011"的二进制则为3。

        有了这个思路之后我们接下来要做的就是想办法统计每一个二进制位上1的个数。首先如何判断一个二进制位是1还是0呢,根据位运算中按位与的概念,「1 & 1 === 1」,「1 & 0 === 0」

        我们拿一个代表性的数字10举例,10 的二进制是"1010"。10 & 1结果是什么呢?

        如图所示,结果是0。然后我们让10右移一位(右移一位的结果我们这里并不需要关注),再跟1做与运算

        如图所示,结果是1。我们发现一个数跟1进行与运算,能判断出最后一位是否是1。然后每次都让数字右移一位,就能判断出每一位是否是1。

        知道这些之后我们回归题目来整体梳理一下逻辑。我们让数组中每一个数都跟1进行与运算,如果为1则累加。最后能够得到最后一列中有几个1。然后我们让每个数都右移一位,重新计算,以此类推就能够得到每一个位置有几个1。我们还知道二进制位有32位,所以我们只需要移动31次即可。代码如下

var singleNumber = function (nums) {for (let i = 0; i < 32; i++) {let sum = 0;for (const item of nums) {sum += (item >> i) & 1;}console.log('倒数第i列1的个数为:', sum);}};

        核心代码是第六行,我们将每个数移动i位,然后跟1进行与运算并累加。就得到能从后往前第i列有几个1。

        到这里还没结束呢,我们还需要将每一位跟3取余,并得到最终结果。

        这里想先问一下大家日常根据一个二进制怎么计算10进制呢。相信很多人跟笔者一样都是使用一二四八大法吧。随便定义一个二进制,"1010101" 结果是什么呢?

        从后往前第一位是1,第二位是2,第三位是4,第四位是8,前一位是后一位的两倍。能够得到下图。

        然后我们用1+4+16+64,能得到答案—— 85。

        之后我们再假设一下让数字1左移的话会有什么效果。能发现左移1位变为2,左移2位变为4,左移3位变为8。

        知道这个之后,我们来看完整代码

var singleNumber = function (nums) {let res = 0;for (let i = 0; i < 32; i++) {let sum = 0;for (const item of nums) {sum += (item >> i) & 1;}if (sum % 3 !== 0) {res += (1 << i);}}return res;
};

        这段代码只需关注11-13行。当我们统计到1的个数之后跟3取余,如果不等于0,说明这个二进制位就是要找的数据的某一个二进制位,然后让1左移i位,累加起来。最后的结果就是结果数据。

题目3:LeetCode上第260题《只出现一次的数字 III》

给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

你必须设计并实现线性时间复杂度的算法且仅使用常量额外空间来解决此问题。

示例 1:

        输入:nums = [1,2,1,3,2,5]
        输出:[3,5]
        解释:[5, 3] 也是有效的答案。
示例 2:

        输入:nums = [-1,0]
        输出:[-1,0]
示例 3:

        输入:nums = [0,1]
        输出:[1,0]

提示:

        2 <= nums.length <= 3 * 10^4
        -2^31 <= nums[i] <= 2^31 - 1
        除两个只出现一次的整数外,nums 中的其他数字都出现两次

     这道题是在第一题的基础上,增加了一个只出现一次的元素。因此我们无法直接通过异或得到结果。因为异或后的最终结果是那两个数异或后的值。

        所以我们需要考虑的是有没有办法将这两个异或后的值拆分出来,是解题关键。

        这道题解法太巧妙了,笔者想破脑袋也想不到啊,仅针对LeetCode官方给出的解法,给大家解释明白。

        首先我们根据异或的概念知道,两个数不一样,结果才为1。这里我们假设最终结果A和B。如果A^B的结果二进制中有一位是1,那么肯定要不就是A中的,要不就是B中的,

        我们如果将一个数转换成2进制,并且需要找出其中一个二进制位是1的位置,相信大家有很多办法。官方给出找这个位置的巧妙办法是这样的。

        假设最终求出的两个结果为A和B。将A和B的异或结果(A ^ B)设为X。那么X 和负X进行与运算(x & -x)得出的结果,就是X中最低位的那个1。举几个例子:

       (妙,妙到家了,这谁能想到啊。当然,我们也可以用其他办法随便找个二进制位为1的位置。剩下的处理逻辑是一样的)

        得到这个数之后,我们拿每个数都跟这个结果进行与运算。结果只有两种情况,要不就是1,要不就是0。上面讲过了,这个数非A即B,如果这个数是跟A与运算结果是1,那么B肯定是0。反之也一样。这样我们就可以将所有的数分为两部分,其中A和B肯定没有在一起。

        至于其他数,相同的数肯定在同一部分,再对这两部分分别进行异或运算,则两部分就只会剩下A和B了。代码如下:

var singleNumber = function(nums) {let answer = 0;// 首先对数组中所有数进行异或运算,得到结果A和结果B的异或结果 answerfor(const item of nums) {answer ^= item;}// 然后通过异或结果跟相反数进行与运算,得到一个表示异或结果最低位为1的值const index =  answer & (-answer);let left = 0;let right = 0;for(const item of nums) {// 遍历每一个数,跟index与运算为1的放左边,为0的放右边if (item & index) {//  对左边的所有数进行异或运算left ^= item;} else {//  对右边的所有数进行异或运算right ^= item;}}// 最终left只剩下一个,right也只剩下一个,为最后答案return [left, right]
};

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

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

相关文章

单链表的插入和删除

一、插入操作 按位序插入&#xff08;带头结点&#xff09;&#xff1a; ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。 typedef struct LNode{ElemType data;struct LNode *next; }LNode,*LinkList;//在第i 个位置插插入元素e (带头结点) bool Li…

国内IP切换软件:解锁网络世界的新钥匙

在数字化快速发展的今天&#xff0c;互联网已成为我们生活中不可或缺的一部分。然而&#xff0c;伴随着网络使用的深入&#xff0c;许多用户逐渐意识到&#xff0c;不同的IP地址可能会带来截然不同的网络体验。为了应对这一问题&#xff0c;国内IP切换软件应运而生&#xff0c;…

iOS客户端自动化UI自动化airtest+appium从0到1搭建macos+脚本设计demo演示+全网最全最详细保姆级有步骤有图

Android客户端自动化UI自动化airtest从0到1搭建macos脚本设计demo演示全网最全最详细保姆级有步骤有图-CSDN博客 避坑系列-必读&#xff1a; 不要安装iOS-Tagent &#xff0c;安装appium -这2个性质其实是差不多的都是为了安装wda。注意安装appium最新版本&#xff0c;安装完…

结构体,联合体,枚举( 1 )

目录 前言 1.结构体 1.1结构体的声明 1.2结构体变量的创建和初始化 1.3结构体成员的访问字符 1.4结构体的内存大小 1.4.1对齐规则 1.5结构体传参 前言 在编程的世界里&#xff0c;数据结构的选择对于程序的效率和可读性有着至关重要的影响。不同的数据结构适用于不同的…

19. 变量

文章目录 一、变量二、变量的定义格式 一、变量 变量&#xff1a;程序中临时存储数据的容器&#xff0c;在程序执行过程中&#xff0c;其值有可能发生改变的量&#xff08;数据&#xff09;。但是这个容器中只能存一个值。 应用场景&#xff1a;在我们登录页面的时候&#xf…

C++多重继承与虚继承

多重继承的原理 多重继承(multiple inheritance)是指从多个直接基类中产生派生类的能力。 多重继承的派生类继承了所有父类的属性。 在面向对象的编程中&#xff0c;多重继承意味着一个类可以从多个父类继承属性和方法。 就像你有一杯混合果汁&#xff0c;它是由多种水果榨取…

46.continue语句

目录 一.continue语句 二.视频教程 一.continue语句 continue语句的作用和break语句很像&#xff0c;break语句会跳出当前循环&#xff0c;而continue语句则是跳出本次循环&#xff0c;继续执行下一次循环。 举个例子&#xff1a; #include <stdio.h>void main(void)…

蓝桥杯练习题总结(三)线性dp题(摆花、数字三角形加强版)

目录 一、摆花 思路一&#xff1a; 确定状态&#xff1a; 初始化&#xff1a; 思路二&#xff1a; 确定状态&#xff1a; 初始化&#xff1a; 循环遍历&#xff1a; 状态转移方程&#xff1a; 二、数字三角形加强版 一、摆花 题目描述 小明的花店新开张&#xff0c;为了吸…

计算机组成原理 — 指令系统

指令系统 指令系统指令的概述指令的格式指令的字长取决于 操作数类型和操作种类操作数的类型数据在存储器中的存放方式操作类型 寻址方式指令寻址数据寻址立即寻址直接寻址隐含寻址间接寻址寄存器寻址寄存器间接寻址基址寻址变址寻址堆栈寻址 RISC 和 CISC 技术RISC 即精简指令…

「AI作曲家」Suno 使用 v3 在几秒钟内创作完整的两分钟歌曲

Suno 被誉为“音乐界的 ChatGPT”,它的独特之处在于能够根据简单的提示,自主创作包括歌词、人声和配器在内的完整音乐作品。更令人惊叹的是,你可以引导它精确地选择任何想要的音乐风格,从古老的三角洲蓝调到现代的电子寒潮,它还能灵活运用各种方言。 Suno 正在开启一个全…

【学习笔记】java项目—苍穹外卖day01

文章目录 苍穹外卖-day01课程内容1. 软件开发整体介绍1.1 软件开发流程1.2 角色分工1.3 软件环境 2. 苍穹外卖项目介绍2.1 项目介绍2.2 产品原型2.3 技术选型 3. 开发环境搭建3.1 前端环境搭建3.2 后端环境搭建3.2.1 熟悉项目结构3.2.2 Git版本控制3.2.3 数据库环境搭建3.2.4 前…

使用certbot为网站启用https

1. 安装certbot客户端 cd /usr/local/bin wget https://dl.eff.org/certbot-auto chmod ax ./certbot-auto 2. 创建目录和配置nginx用于验证域名 mkdir -p /data/www/letsencryptserver {listen 80;server_name ~^(?<subdomain>.).ninvfeng.com;location /.well-known…

【Linux】 centos7安装卸载SQL server(2017、2019)

一、安装配置 准备一个基础Linux配置&#xff1a; 内存为20GB 运行内存为2GB的系统&#xff08;数据库小于2GB安装不了&#xff09; 1、网络配置 我们需要进行网络的连接 进入 cd /ect/sysconfig/network-script/ 编辑文件ifcfg-ens33 vi ifcfg-ens33 Insert键进行编辑 把ONBOO…

2024最新Win系统下VSCode下载安装与配置C/C++教程

2024最新Win系统下VSCode下载安装与配置C/C教程 文章目录 2024最新Win系统下VSCode下载安装与配置C/C教程1、下载安装VSCode2、安装运行时环境GCGC的环境配置 3、安装VSCode插件4、配置程序调试环境4.1确定文件存储路径4.2新建文件夹【.vscode】4.3在.vscode文件夹里新建四个配…

学透Spring Boot — [二] Spring 和 Spring Boot的比较

欢迎关注我们的专栏 学透 Spring Boot 一、创建一个简单Web应用 本篇文章&#xff0c;我们将会比较 Spring 框架和 Spring Boot 的区别。 什么是 Spring? 也许你在项目中已经可以很熟练的使用 Spring 了&#xff0c;但是当被问到这个问题时&#xff0c;会不会犹豫一下&#…

轻松赚钱,精彩生活:上班族副业赚钱新攻略大揭秘!

薪水总是捉襟见肘&#xff0c;每月账单总让人倍感压力。你是否曾在静谧的夜晚&#xff0c;躺在床上&#xff0c;思索如何为家庭多赚一分钱&#xff1f;其实&#xff0c;你并不孤单。在这个充满机遇与挑战的时代&#xff0c;越来越多的人开始寻找副业&#xff0c;以期望让生活更…

QT控件之显示控件

Qt Designer显示窗口部件提供的面板中&#xff0c;提供了10种显示小部件 &#xff08;1&#xff09; Label标签 &#xff08;2&#xff09; Text Browser文本浏览器 &#xff08;3&#xff09; Graphics View图形视图 &#xff08;4&#xff09; Calendar Widget日历 &…

【Bug-ModuleNotFoundError: No module named ‘models‘】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;Python &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 出现这个错误&#xff1a; 出现了ModuleNotFoundError: No module named models’的问题。 文件在Model…

【操作系统复习之路】操作系统概述(复习的同学有福啦)

长话短说&#xff0c;就记下笔记&#xff0c;期待期末90&#xff0c;随便希望能帮助到有需要的同学。 目录 一、操作系统的目标和作用 二、操作系统的发展过程 2.1 无OS 2.2 有OS 【1】批处理系统 【2】分时系统 【3】实时操作系统 【4】三种基本操作系统的比较&#…

域环境共享文件夹,容量配额管理

首先&#xff0c;我们先创建一个新的磁盘&#xff0c;必须在服务器关机的状态下创建&#xff0c;只有在关机状态下才能创建NVMe类型的磁盘。 打开此电脑&#xff0c;右击创建的磁盘&#xff0c;点击属性。 点击共享&#xff0c;点击高级共享。 将共享此文件夹勾选上&#xff0c…