算法之美 : 位运算

上一小节我们用三道题了解一下面试过程中栈和队列的常见面试题。本小节笔者将通过几个 位运算 的题目来带大家熟悉下常用的位运算知识。

相比于栈和队列来讲,笔者自身认为位运算需要掌握的知识就要多一些,包括对于数字的二进制表示,二进制的反码,补码。以及二进制的常见运算都需要了解。当然如果系统的去学,可能没有经历,也可能即使学完了,仍旧不会做题。所以笔者认为通过直接去刷一些相应的题目,则是一个比较便捷的途径。

给定一个整数,请写一个函数判断该整数的奇偶性(✭✩✩✩✩)

该题目作为后续题目的铺垫,看上去还是没有任何难度的。主要考察了面试能否想到用二进制的位运算方法去解决。

首先整数可以分为正数,负数,0。也可以分为奇数和偶数。偶数的定义是:如果一个数是2的整数倍数,那么这个数便是偶数。如果不使用位运算的方法,我们完全可以使用下面的方式解决:

public boolean isOdd(int num){//odd 奇数return num % 2 != 0;
}
复制代码

可是面试题不可能去简单就考察这么简单的解法,进而我们想到了二进制中如果 一个数是偶数那么最后一个一定是 0 如果一个数是奇数那么最后一位一定是 1;而十进制 1 在 8 位二进制中表示为 0000 0001,我们只需将一个数个 1相与(&) 得到的结果如果是 1 则表示该数为奇数,否知为偶数。所以这道题的最佳解法如下:

public boolean isOdd(int num){return num & 1 != 0;
}
复制代码
#include "iostream"  
using namespace std;  
//声明
bool IsOdd(int num);bool IsOdd(int num)
{int res = (num & 1);return res != 0;
}
复制代码

测试:

int main(int argc, const char * argv[]) {std::cout << "是否是奇数 : " << IsOdd(1) <<endl;std::cout << "是否是奇数 : " << IsOdd(4) <<endl;return 0;
}//结果
是否是奇数 : 1//是 true
是否是奇数 : 0//不是 false
复制代码

同样给定一个整数,请写一个函数判断该整数是不是2的整数次幂(✭✩✩✩✩)

这道题仍旧考察面试者对于一个数的二进制的表示特点,一个整数如果是2的整数次幂,那么他用二进制表示完肯定有唯一一位为1其余各位都为 0,形如 0..0100...0。比如 8 是 2的3次幂,那么这个数表示为二进制位 0000 1000 。

除此之外我们还应该想到,一个二进制如果表示为 0..0100...0,那么它减去1得到的数二进制表示肯定是 0..0011..1 的形式。那么这个数与自自己减一后的数相与得到结果肯定为0。

如:

所以该题最佳解法为:

public boolean log2(int num){return (num & (num - 1)) == 0;
}
复制代码
#include "iostream"  
using namespace std;  
//声明
bool IsLog2(int num);
//定义
bool IsLog2(int num)
{return (num & (num -1)) == 0;
}
复制代码

测试:

int main(int argc, const char * argv[]) {std::cout << "是否是2的整数次幂 : " << IsLog2(1) <<endl;std::cout << "是否是2的整数次幂 : " << IsLog2(3) <<endl;return 0;
}//结果
是否是2的整数次幂 : 1 //是 true
是否是2的整数次幂 : 0 //不是 false
复制代码

给定一个整数,请写一个函数判断该整数的二进制表示中1的个数(✭✭✩✩✩)

此题较之上一题又再进一步,判断一个整数二进制表示中1的个数,假设这个整数用32位表示,可正可负可0,那么这个数中有多少个1,就需要考虑到符号位的问题了。

相信读者应该都能想到最近基本的解法即通过右移运算后与 1 相与得到的结果来计算结果,如果采用这种解法,那么这个题的陷阱就在于存在负数的情况,如果负数的话标志位应该算一个1。所以右移的时候一定要采用无符号右移才能得到正确的解法。

ps 对于正数右移和无符号右移得到结果一样,如果是负数,右移操作将在二进制补码左边添加追加1,而无符号右移则是补 0 。

所以此题一种解法如下:

public int count1(int n) {int res = 0;while (n != 0) {res += n & 1;n >>>= 1;}return res;
}
复制代码
#include "iostream"  
using namespace std;//注意C++中没有无符号右移操作,所以这里传入一个 unsigned 数作为 params
int count1(unsigned int n){int res = 0;while(n != 0){res += n & 1;n >>= 1;}return res;
}
复制代码

测试结果:

int main(int argc, const char * argv[]) {std::cout << "二进制中1的个数 : " <<  count1(-1) <<endl;std::cout << "二进制中1的个数 : " <<  count1(1) <<endl;return 0;
}//结果
二进制中1的个数 : 32
二进制中1的个数 : 1
复制代码

能回答出上边的答案你的面试肯定是及格了,但是作为练习来说,是否有额外的解法呢?首先上述结果最坏的情况可能需要循环32次。上面我们算过一道如何判断一个数是否是2的整数倍,我们用过了 n&(n-1)==0 的方法。其实该题的第二个解法也可以用这个方法。为什么呢?我们开看一次上边的图:

我们是否能发现,每次与比自己小1的数与那么该数的二进制表示最后一个为1位上的1将将会被抹去。其实这是一个知道有这种原理才能想到的方法,所以大家也不用哀叹说我怎么想不到,通过这次记住有这个规律下次就多一个思路也不是很么坏事。

下面我们来看下判断一个数中有多少个1的完整图解:

所以我们可以通过如下方法来得到题解,这样我们可以减少移动次数

public int countA(int n){int res = 0;while(n != 0){n &= (n - 1);res++;}return res;
}
复制代码
#include "iostream"  
using namespace std;  
// 同上传入无符号整数 
int countA(unsigned int n){int res = 0;while(n != 0){n &= (n - 1);res++;}return res;
}
复制代码

测试结果:

int main(int argc, const char * argv[]) {std::cout << "二进制中1的个数 : " <<  countA(-1) <<endl;std::cout << "二进制中1的个数 : " <<  countA(1) <<endl;return 0;
}//结果
二进制中1的个数 : 32
二进制中1的个数 : 1
复制代码

在其他数都出现两次的数组中找到只出现一次的那个数(✭✭✩✩✩)

这道题同样是考察为位运算的一道题,但是如果对于不熟悉位运算的朋友可能压根都不会往这方面想,也许当场直接就下边写下了遍历数组记每个数出现次数的代码了。其实这道题要求在时间复杂度在O(n) 空间复杂度为O(1)的条件下,那种解法是不符合要求的。我们来看下为位运算的解题思路。

首先我们应该知道二进制异或操作,异或结果是二进制中两个位相同为0,相异为1。因此可以有个规律:

任何整数 n 与 0 异或总等于其本身 n,一个数与其本身异或那么结果肯定是 0。

还需要知道一个规律:

多个数异或操作,遵循交换律和结合律。

对于第一条朋友们肯定都很好理解,然而第二条规律才是这道题的解题关键。如果我们有一个变量 eO = 0 那么在遍历数组过程中,使每个数与 eO 异或得到的值在赋值给额 eO 即 eO=eO ^ num 那么遍历结束后eO的值一定是那个出现一次的数的值。这是为什么呢?我们可以举个例子:

假设有这么一个序列: C B D A A B C 其中只有 D 出现一次,那么因为异或满足交换律和结合律,所以我们遍历异或此序列的过程等价于

eO ^ (A ^ A ^ B ^ B ^ C ^ C ) ^ D = eO ^ 0 ^ D = D
复制代码

所以对于任何排列的数组,如果只有一个数只出现了奇数次,其他的数都出现了欧数次,那么最终异或的结果肯定为出现奇数次的那个数。

所以此题可以有下面的这种解法:

java 解法

public int oddTimesNum(int[] arr) {int eO = 0;for (int cur : arr) {eO = eO ^ cur;}return eO;
}
复制代码

C++ 解法

int oddTimesNum(vector<int> arr) {int eO = 0;for (int cur : arr) {eO = eO ^ cur;}return eO;
}
复制代码

测试:

int main(int argc, const char * argv[]) {vector<int>  arr = {2,1,3,3,2,1,4,5,4};std::cout << "出现奇数次的那个数: " << oddTimesNum(arr) <<endl;return 0;
}//结果
出现奇数次的那个数: 5
复制代码

关于这道题还有个延伸版本,就是如果数组中出现1次的数有两个,那么该如何得到这两个数。

在其他数都出现两次的数组中找到只出现一次的那两个数(✭✭✭✩✩)

我们顺着上题的思路来思考,如果有两个数获得的结果 eO 肯定是 eO = a^b,此题的关键就在于如何分别得到 a,b 这两个数。我们应该想到,任何不相同的两个除了跟自己异或外,不可能每一个位都相同,也就是说不相同的两个数 a b 异或得到结果二进制表示上肯定有一位为 1。 这是关键。

我们可以假设第 k 位不为 0 ,那么就说明 a 与 b 在这位上数值不相同。我们要做只是设置一个数第 k 位 为 1,其余位为 0 记为 rightOne

这时需要拿 eOhasOne = 0 再异或遍历一次数组,但是需要忽略与 rightOne 相与等于 0 的数。因为相与等于 0 则代表了这个数肯定是两个数中第 k 位不为 1的那个。最终得到的 eOhasOne 就是 a b 中第 k 为为 1 的那个。

那么接下来就剩下一个问题要解决了,如何找到 rightOne ,这里采用与本身补码相与的方法得到即 int rightOne = eO & (~eO + 1)

可以参照下图来理解下整个过程:

我们来看下最终的代码:

java 写法

public void printOddTimesNum(int[] arr) {int eO = 0;int eOhasOne = 0;for (int cur : arr) {eO = eO ^ cur;}int rightOne = eO & (~eO + 1);for (int cur : arr) {if ((rightOne & cur) != 0) {eOhasOne = eOhasOne ^ cur;}}System.out.println("eOhasOne = " + eOhasOne + "  " + (eOhasOne ^ eO));
}
复制代码

C++ 写法

void printOddTimesNum(vector<int> arr) {int eO = 0;int eOhasOne = 0;for (int cur : arr) {eO = eO ^ cur;}int rightOne = eO & (~eO + 1);for (int cur : arr) {if ((cur & rightOne) != 0) {eOhasOne = eOhasOne ^ cur;}}std::cout<<"一个出现1次的数 " << eOhasOne << endl;std::cout<<"二个出现1次的数 " << (eO ^ eOhasOne) <<endl;
}
复制代码

测试:

int main(int argc, const char * argv[]) {vector<int>  arr1 = {2,1,3,3,2,1,4,5};printOddTimesNum(arr1);return 0;
} //结果:
一个出现1次的数 5
二个出现1次的数 4
复制代码

参考

《剑指 offer 第二版》 《程序员代码面试指南 - 左程云》

欢迎关注我的微信公众号,接收第一手技术干货

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

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

相关文章

447. 回旋镖的数量

447. 回旋镖的数量 给定平面上 n 对 互不相同 的点 points &#xff0c;其中 points[i] [xi, yi] 。回旋镖 是由点 (i, j, k) 表示的元组 &#xff0c;其中 i 和 j 之间的距离和 i 和 k 之间的距离相等&#xff08;需要考虑元组的顺序&#xff09;。 返回平面上所有回旋镖的…

一名3年工作经验的程序员应该具备的技能

本文转自:https://m.imooc.com/article/details?article_id7557 前言 因为和同事有约定再加上LZ自己也喜欢做完一件事之后进行总结&#xff0c;因此有了这篇文章。这篇文章大部分内容都是面向整个程序员群体的&#xff0c;当然因为LZ本身是做Java开发的&#xff0c;因此有一部…

js 排序算法总结

1.冒泡排序 平均时间复杂度O(N2) 最好情况O(N)最坏情况O(N2) 空间复杂度O(1) function bubbleSort(arr){if(arr.length < 1)return arr;var flag 1; // 标识是否进行交换for(var i0; i < arr.length; i){if(i !0 && flag) break;for(var j0; j <…

524. 通过删除字母匹配到字典里最长单词

524. 通过删除字母匹配到字典里最长单词 给你一个字符串 s 和一个字符串数组 dictionary 作为字典&#xff0c;找出并返回字典中最长的字符串&#xff0c;该字符串可以通过删除 s 中的某些字符得到。 如果答案不止一个&#xff0c;返回长度最长且字典序最小的字符串。如果答案…

django开发商城(提供初始数据,商城首页及购物车)

1.爬取数据 2.json数据转化为sql语句 3.新建轮播图模型(模型名与sql语句对应表名相同) class Wheel(models.Model):imgmodels.CharField(max_length150)namemodels.CharField(max_length20)trackidmodels.CharField(max_length20) 4.终端打开mysql,执行插入语句 5.在首页进行展…

多语言版希尔排序

2019独角兽企业重金招聘Python工程师标准>>> 简介 希尔排序(Shells Sort)是插入排序的一种又称“缩小增量排序”&#xff08;Diminishing Increment Sort&#xff09;&#xff0c;是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L…

UML 中extend和include的区别

在UML用例图中有两种关系——包含和扩展&#xff0c;容易混淆&#xff0c;下面通过一张表来区别一下这两种关系。 转载于:https://www.cnblogs.com/yonyong/p/8555547.html

hdu 6301 Distinct Values(贪心)题解

题意&#xff1a;长为n的串&#xff0c;给你m个区间&#xff0c;这些区间内元素不重复&#xff0c;问这样的串字典序最小为&#xff1f; 思路&#xff1a;用set保存当前能插入的元素&#xff0c;这样就能直接插入最小元素了。对操作按l排序&#xff0c;因为排过的不用排&#x…

浏览器兼容CSS渐进增强 VS 优雅降级如何选择

由于低级浏览器不支持 CSS3&#xff0c;但是 CSS3 特效太优秀不忍放弃&#xff0c;所以在高级浏览器中使用CSS3&#xff0c;而在低级浏览器只保证最基本的功能。二者的目的都是关注不同浏览器下的不同体验&#xff0c;但是它们侧重点不同&#xff0c;所以导致了工作流程上的不同…

细数sass安装中遇到的坑

前言&#xff1a; 前两天打算清理电脑的时候&#xff0c;遇到了一点特殊的问题&#xff0c;打算重装一些东西&#xff0c;其中就有我一直用的顺手的SASS预编译工具。 但是在重装的时候&#xff0c;我发现我居然不会用了&#xff1f;&#xff1f;&#xff1f; 靠&#xff0c;要不…

442. 数组中重复的数据

442. 数组中重复的数据 给定一个整数数组 a&#xff0c;其中1 ≤ a[i] ≤ n &#xff08;n为数组长度&#xff09;, 其中有些元素出现两次而其他元素出现一次。 找到所有出现两次的元素。 你可以不用到任何额外空间并在O(n)时间复杂度内解决这个问题吗&#xff1f; 示例&am…

[bzoj 2726] 任务安排 (斜率优化 线性dp)

3月14日第三题&#xff01;&#xff01;&#xff01;&#xff08;虽然是15号发的qwq&#xff09; Description 机器上有N个需要处理的任务&#xff0c;它们构成了一个序列。这些任务被标号为1到N&#xff0c;因此序列的排列为1,2,3…N。这N个任务被分成若干批&#xff0c;每批…

2018年,牛客网小白月赛5

第一次啊&#xff0c;补题&#xff0c;希望大佬批评。 题目按我补题顺序来的。 https://www.nowcoder.com/acm/contest/135#question H 题 最大公倍数 题意:给出两个数&#xff0c;求最大公倍数 欧几里德算法算出最大公约数k; 然后算出。最大公倍数即可 代码如下&#xff1a; …

292. Nim 游戏

292. Nim 游戏 你和你的朋友&#xff0c;两个人一起玩 Nim 游戏&#xff1a; 桌子上有一堆石头。你们轮流进行自己的回合&#xff0c;你作为先手。每一回合&#xff0c;轮到的人拿掉 1 - 3 块石头。拿掉最后一块石头的人就是获胜者。 假设你们每一步都是最优解。请编写一个函…

0710 mux协议的作用(ppp拨号时如何和gprs进行at指令交互)

ppp拨号使gprs上网的同时如何和gprs模块进行at指令的交互&#xff0c;这是一个问题。 在linux中&#xff0c;ppp拨号上网是内核中支持的&#xff0c;只需要在内核配置中选上。 ppp拨号的方式使gprs进行上网与at指令使gprs上网&#xff0c;两者之间有不同。ppp是一个将用at指令使…

爬虫笔记(十二)——浏览器伪装技术

为什么要进行浏览器伪装技术&#xff1f; 有一些网站为了避免爬虫的恶意访问&#xff0c;会设置一些反爬虫机制&#xff0c;对方服务器会对爬虫进行屏蔽。常见的饭爬虫机制主要有下面几个&#xff1a; 1. 通过分析用户请求的Headers信息进行反爬虫 2. 通过检测用户行为进行反…

650. 只有两个键的键盘

650. 只有两个键的键盘 最初记事本上只有一个字符 ‘A’ 。你每次可以对这个记事本进行两种操作&#xff1a; Copy All&#xff08;复制全部&#xff09;&#xff1a;复制这个记事本中的所有字符&#xff08;不允许仅复制部分字符&#xff09;。Paste&#xff08;粘贴&#x…

Codeforces 626F Group Projects (DP)

题目链接 8VC Venture Cup 2016 - Elimination Round 题意 把$n$个物品分成若干组&#xff0c;每个组的代价为组内价值的极差&#xff0c;求所有组的代价之和不超过$k$的方案数。 考虑DP&#xff0c;$f[i][j][k]$表示考虑到第$i$个物品的时候&#xff0c;还有$j$组尚未分配完…

《活出生命的意义》:人生有何意义?

在你一生的阅读体验中&#xff0c;如果能够有一本书&#xff0c;它的某个章节、某种思想、或者某句话能够触动你的内心&#xff0c;解决你的困惑&#xff0c;甚至能改变你的命运&#xff0c;那这样的一本书你一定要视如珍宝&#xff0c;经常翻阅&#xff0c;维克多弗兰克尔的《…

右键添加git-bash

主要&#xff1a; 右键如果没有git-bash&#xff0c;如何给右键手动添加 前面对右键存在git-bash但使用出现问题的解决&#xff0c;也想到如果右键都没有&#xff0c;该如何给右键添加了&#xff0c;于是接着记录下如何添加的过程&#xff1a; 情形&#xff1a; 手动给右键添加…