全面瞭解位運算與硅谷面試題

前言

本篇文章淺談位運算符、二進制、機器數的原碼、反碼、補碼以及位運算的應用,最後附上吳軍老師提到過的硅谷面試題,但願閱讀完本篇文章的同窗,熟練掌握二進制與位運算,leetcode刷到爆。git

位運算符

按位運算符將其操做數視爲32位(0和1)的序列,而不是十進制,十六進制或八進制數。例如,十進制數字9 具備1001的二進制表示。按位操做符操做數字的二進制形式,但返回值依然是標準的JavaScript數值。github

七種運算方式

下面的表格總結了JavaScript中的按位操做符面試

運算符 用法 描述
與(&) a & b 當兩個操做數相對應的位都爲1時,結果才爲1,不然爲0。
或(|) a | b 當兩個操做數相對應的位至少有一個爲1時,結果爲1,不然爲0。
異或(^) a ^ b 當兩個操做數相對應的位只有一個爲1時,結果爲1,不然爲0。
取反(~) ~ a 反轉操做數的位,即0變成1,1變成0。
左移(<<) a << b 將 a 的二進制形式向左移 b (< 32) 位,右邊用0填充。
右移(>>) a >> b 將 a 的二進制表示向右移 b (< 32) 位,丟棄被移出的位。
零填充右移(>>>) a >>> b 將 a 的二進制表示向右移 b (< 32) 位,丟棄被移出的位,並使用 0 在左側填充。

在講位運算以前,先簡要講述一下機器數的形式原碼、反碼、補碼數組

什麼是機器數?

機器數是將符號"數字化"的數,是數字在計算機中的二進制表示形式。在一般的運算中,用「+」、「-」表示正數和負數,而數字電路不識別「+」,「-」。所以,在數字電路中把一個數的最高位做爲符號位,並用0表示「+」,用1表示「-」。bash

原碼

首位爲符號位,0表示整數,1表示負數,其他位表示數值。函數

例如0000 0011表示+3,而1000 0011表示-3。post

  • 原碼中0有兩種表現形式,中+0爲 0000 0000,-0爲 1 000 0000

問題來了,在原碼中(+3)+(-3) = 0 嗎?0000 0011 + 1000 0011 = 1000 0110 爲-6的錯誤結果,顯然這個結果並非咱們想要的。若是解決原碼中正負相加的問題呢?反碼優化

反碼

若是是正數,則表示方法和原碼同樣;若是是負數,符號位不變,其他各位取反。ui

例如 -3原碼爲 1000 0011,反碼爲1111 1100編碼

0000 0011 + 1111 1100 = 1111 1111 爲-0,解決上面原碼遇到的問題,可是新問題來了,反碼中0依然有兩種表現方式,0000 0000(+0)和 1111 1111(-0)。這時出現了補碼

補碼

若是是正數,則表示方法和原碼同樣;若是是負數,則將數字的反碼未位加1(至關於將原碼數值位取反後未位加1)

例如 -3反碼爲1111 1100,其補碼爲1111 1101

0000 0011 + 1111 1101 = 0000 0000(+0)
複製代碼

重述一遍,補碼保證了當一個數是正數時,其最左的bit位是0,當一個數是負數時,其最左的bit位是1。所以,最左邊的bit位被稱爲符號位。

爲何會有原碼,反碼,補碼?

計算機採用二進制表示數,那麼具體如何表示其中也是有學問的。原碼是人類最直觀想到的表現方式,但原碼有缺點,因而產生了反碼,反碼也有缺點,最後產生了相對合理補碼,因而計算機系統大多使用補碼來存儲二進制數。反碼的做用主要是方便原碼到補碼的過渡,方便理解。 若是隻有加法和正數,那麼使用原碼沒有什麼問題。之因此出現反碼、補碼,正是爲了解決負數和減法所帶來的問題。因此正數的原碼、反碼、補碼都是同樣的。

若是想對原碼、反碼、補碼有更深的瞭解,推薦讀《計算機原理》這本書,這裏就再也不贅述了。

有符號32位整數

0 是全部bit數字0組成的整數。

// 十進制 = 二進制

0 = 0000 0000 0000 0000 0000 0000 0000 0000
複製代碼

-1 是全部bit數字1組成的整數。

-1 = 1111 1111 1111 1111 1111 1111 1111 1111
複製代碼

-2147483648(十六進制形式:-0x80000000)是除了最左邊爲1之外,其餘bit位都爲0的整數。

-2147483648 = 1000 0000 0000 0000 0000 0000 0000 0000
複製代碼

2147483647(十六進制形式:0x7fffffff)是除了最左邊爲0之外,其餘bit位都爲1的整數。

2147483647 = 0111 1111 1111 1111 1111 1111 1111 1111
複製代碼
  • 因此 **數字-2147483648 和 2147483647 是32位有符號數字所能表示的最小和最大整數。**這裏可能會成爲考點。

下面講一下位運算符

& (按位與)

當兩個操做數相對應的位都爲1時,結果才爲1,不然爲0。

舉個例子,7 & 4 (默認例子按照8bit展現)

7 = 0000 0111 
4 = 0000 0100

-------------

7 & 4 = 0000 0100  => 4
複製代碼

再舉一個例子,7 & -4(關於負數在上面機器數中有介紹)

7 = 0000 0111 
-4 = 1111 1100

-------------

7 & -4 = 0000 0100 => 4
複製代碼

| (按位或)

當兩個操做數相對應的位至少有一個爲1時,結果爲1,不然爲0。

舉個例子,7 | 4

7 = 0000 0111 
4 = 0000 0100

-------------

7 | 4 = 0000 0111  => 7
複製代碼

^ (按位異或)

當兩個操做數相對應的位只有一個爲1時,結果爲1,不然爲0。

舉個例子,7 ^ 4

7 = 0000 0111 
4 = 0000 0100

-------------

7 ^ 4 = 0000 0011  => 3
複製代碼

~ (按位非)

反轉操做數的位,即0變成1,1變成0。

舉個例子,~ 3

3 = 0000 0011 

-------------

~ 3 = 1111 1100  => -4

複製代碼
  • ~ 小技巧

根據~運算符,~ -1等於0咱們能夠把如下代碼進行優化

const arr = ['a', 'b', 'c']

// if (arr.indexOf('d') > -1) { 這裏也能夠用includes代替
if (~arr.indexOf('d')) 
   // 存在
} else {
   // 不存在
}
複製代碼

~~取整

const num = 3.14

~~num // 3 原理根據位運算基於整數進行計算
複製代碼

<< (左移)

將 a 的二進制形式向左移 b 位,右邊末尾用0填充。

舉個例子,7 << 2

7 = 0000 0111

-------------

7 << 2 = 0001 1100  => 28
複製代碼

這裏說一下數字溢出,當二進制數的位數超過了系統所指定的位數。 上面咱們說到32位有符號數字最小值爲-2147483648,二進制爲1後面31個0,當咱們將它進行左移後結果是什麼呢?你們能夠試試。

>> (右移)

將 a 的二進制表示向右移 b 位,丟棄被移出的位。

舉個例子,7 >> 2

7 = 0000 0111 

-------------

7 >> 2 = 0000 0001  => 1
複製代碼

-7 >> 2(32bit)

-7 = 1111 1111 1111 1111 1111 1111 1111 1001

-------------

-7 >> 2 = 1111 1111 1111 1111 1111 1111 1111 1110 => -2

複製代碼

>>>(零填充右移)

將 a 的二進制表示向右移 b 位,丟棄被移出的位,並使用 0 在左側填充。

舉個例子,7 >>> 2

7 = 0000 0111 

-------------

7 >>> 2 = 0000 0001  => 1
複製代碼
  • -7 >>> 2 (與 >> 的區別)
-7 = 1111 1111 1111 1111 1111 1111 1111 1001

-------------

-7 >>> 2 = 0011 1111 1111 1111 1111 1111 1111 1110  => 1073741822
複製代碼

幾種運算方式到這裏就簡單的講完了,咱們結合原理來作幾道面試題。

常見進制以及位運算面試題

第一題

二進制中1的個數

問題:請實現一個函數,輸入一個整數,輸出該二進制表示中1的個數。例如把9表示爲二進制是1001,有2位是1。所以輸入9時,該函數返回2。

(5秒後公佈答案) .

.

.

.

.

.

.

解法1:右移

function method (n) {
  var count = 0
  while (n) {
     if (n & 1) count++

     n = n >> 1
  }
  return count
}
複製代碼

該方法有一個重大的缺陷,當n爲負數時,將會陷入無限循環。緣由是位移前爲負數,位移後仍然要保持爲負數,所以位移後最高位會是1。假如一直向右位移,最終數字永遠爲-1。如何修復這個缺陷呢?這裏我給出一個非官方解法2

解法2:零填充右移

function method (n) {
  var count = 0
  while (n) {
     if (n & 1) count++

     n = n >>> 1
  }
  return count
}
複製代碼

乍一看沒啥區別,只是把右移換成了零填充右移。細心的同窗已經猜到了,上面我有說位移要保持正負值不變,所以負數的最高位會是1,這一點致使了無限循環。那麼咱們把最高位1換成0不就能夠了麼!零填充右移(>>>)偏偏知足了需求。

解法3:左移

function method (n) {
  var count = 0
  var flag = 1
  while (flag) {
    if (n & flag) count++
    flag = flag << 1
  }
  return count
}
複製代碼

flag值不斷地左移與n值作與操做,大於0表示爲1,反之爲0。這個解法存在一個缺陷,就是循環的次數等於整數二進制的位數,32位整數須要循環32次。

解法4:減1與

function method (n) {
  var count = 0
  while (n) {
    ++count
    n = (n - 1) & n
  }
  return count
}
複製代碼

該方法循環次數與二進制1的個數相等。這個解法我詳細分解一下過程。n分爲3種,正數、負數和零。

  1. 當n爲0時,count爲0
  2. 當n爲正數時,打個比方n爲9,9的二進制爲1001,進入循環 count + 1,(n - 1) & n這裏咱們分解爲二進制 1000 & 1001,上面有說過與操做符含義是當兩個操做數相對應的位都爲1時,結果才爲1,不然爲0。因此這裏n等於1000,十進制爲8。n不等於0,繼續進入循環,count + 1,而後 n = 0111 & 1000,n等於0,結束循環。count等於2。
  3. 當n爲負數時,打個比方n爲-1,二進制就是32個1,-2的二進制是31個1,末尾一個0,以此類推。接下來咱們分析過程,n爲-1時,n不爲零,count + 1,n = -2 & -1,n爲-2,從新循環,...(中間過程省略),循環到第32次,-2147483649 & -2147483648 爲 0,上面提到32位最小值爲-2147483648,-2147483649屬於數字溢出,在32位二進制表示是32個0。因此最終結果爲0。

第二題

在Excel中,用A表示第1列,B表示第2列...Z表示第26列,AA表示第27列,AB表示第28列以此類推。請寫一個函數,輸入用字母表示的列號碼錶,輸出它是第幾列。

(5秒後公佈答案) .

.

.

.

.

.

.

function method (str) {
  let num = 0
  const arr = str.split('')
  for (let len = str.length - 1, i = len, j = 1; i >= 0; i--, j *= 26) {
    let s = str[i].toUpperCase()
    if (!/[A-Z]/.test(s)) {
      return 0
    }
    num += (s.charCodeAt(0) - 64) * j
  }
  return num
}
複製代碼

分析:將字符串轉換爲數組,方便遍歷。將A到Z的字符轉換爲Unicode編碼,已知A的Unicode編碼爲65,如題A爲1,因此減64。設定一個值j,基數爲26的n次方。每循環一次n + 1,這裏用 j *= 26 方式代替,也可使用Math.pow方法。

第三題

如上題請寫一個函數,當輸入第幾列,返回字母表示的列號碼錶。

(5秒後公佈答案) .

.

.

.

.

.

.

function method (num) {
  let str = ''
  while (num > 0) {
    let r = num % 26
    if (!r) {
      r = 26
    }
    str += String.fromCharCode(r + 64)
    num = (num - r) / 26
  }
  return str
}
複製代碼

分析:判斷值是否大於0,取餘數、指定Unicode值獲得字符串。

附加:硅谷面試題

有64瓶藥,其中63瓶是無毒的,一瓶是有毒的。若是作實驗的小白鼠喝了有毒的藥,3天后會死掉,固然喝了其它的藥,包括同時喝幾種就沒事。如今只剩下3天時間,請問最少須要多少隻小白鼠才能試出那瓶藥有毒?

(5秒後公佈答案) .

.

.

.

.

.

.

答案:六隻小老鼠

  1. 咱們將這些藥從0~63按照二進制編號,得到64個六位數的二進制編號,也就是從000000(六個零)到111111(六個一),每一個二進制編號的最左邊是第一位,最右邊是第六位。

  2. 而後選六隻老鼠從左到右排開,和二進制的六位,從左到右地依次對應。文稿裏的二進制編號,你能夠試着一位一位豎着看,下面每隻老鼠負責一位。

  3. 從左邊數第一個老鼠吃對應的二進制是1的藥,0就不吃。那麼老鼠1依次吃第32,33,34,……,63號藥。第二隻吃16,17,……,31,48,49,……,63號藥,等等。最後一隻老鼠吃1,3,5,……,63號藥。你可能注意到了,6只老鼠都吃了63號,那是由於63對應的二進制編號是6個1,因此6只都要吃。

  4. 吃完藥以後三天,某些老鼠可能死了,咱們假定第1,2,6這三隻老鼠死了,剩下的活着。這說明什麼呢?說明編號110001號藥有問題,也就是在第1,第2,第6位上分別是3個1,由於這三隻老鼠都吃了它,而3,4,5這三隻沒死的老鼠沒有吃它(對應的位置爲0)。而110001對應十進制的49,也就是說第49瓶藥是毒藥。

對於其它的組合也是一樣的,你能夠本身隨便假定哪幾只老鼠死了,看看哪瓶是毒藥。固然,還有一種狀況,就是全部的老鼠都沒有死,那說明第0號藥是毒藥,由於其餘的藥都吃過了,就這一瓶沒有吃。

結語

關於位運算的基本運算方式和麪試題就講到這裏,若是你有更經典、有趣的面試題,不妨分享出來。一塊兒討論,歡迎留言。

其餘連接

Vue UI組件庫從1到N開發心得-組件篇

Vue UI組件庫從0到1開發心得

Github

祝工做順利

鄧文斌

2019年6月6日

相關文章
相關標籤/搜索