上一小節咱們用三道題了解一下面試過程當中棧和隊列的常見面試題。本小節筆者將經過幾個 位運算 的題目來帶你們熟悉下經常使用的位運算知識。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 第二版》 《程序員代碼面試指南 - 左程雲》