上一小节咱们用三道题了解一下面试过程当中栈和队列的常见面试题。本小节笔者将经过几个 位运算 的题目来带你们熟悉下经常使用的位运算知识。java
相比于栈和队列来说,笔者自身认为位运算须要掌握的知识就要多一些,包括对于数字的二进制表示,二进制的反码,补码。以及二进制的常见运算都须要了解。固然若是系统的去学,可能没有经历,也可能即便学完了,仍旧不会作题。因此笔者认为经过直接去刷一些相应的题目,则是一个比较便捷的途径。ios
该题目做为后续题目的铺垫,看上去仍是没有任何难度的。主要考察了面试可否想到用二进制的位运算方法去解决。程序员
首先整数能够分为正数,负数,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;
}
复制代码
测试:bash
int main(int argc, const char * argv[]) {
std::cout << "是不是奇数 : " << IsOdd(1) <<endl;
std::cout << "是不是奇数 : " << IsOdd(4) <<endl;
return 0;
}
//结果
是不是奇数 : 1//是 true
是不是奇数 : 0//不是 false
复制代码
这道题仍旧考察面试者对于一个数的二进制的表示特色,一个整数若是是2的整数次幂,那么他用二进制表示完确定有惟一一位为1其他各位都为 0,形如 0..0100...0。好比 8 是 2的3次幂,那么这个数表示为二进制位 0000 1000 。微信
除此以外咱们还应该想到,一个二进制若是表示为 0..0100...0,那么它减去1获得的数二进制表示确定是 0..0011..1 的形式。那么这个数与自本身减一后的数相与获得结果确定为0。函数
如:测试
因此该题最佳解法为:ui
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的个数,假设这个整数用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 第二版》 《程序员代码面试指南 - 左程云》