本篇文章淺談位運算符、二進制、機器數的原碼、反碼、補碼以及位運算的應用,最後附上吳軍老師提到過的硅谷面試題,但願閱讀完本篇文章的同窗,熟練掌握二進制與位運算,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
問題來了,在原碼中(+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位被稱爲符號位。
計算機採用二進制表示數,那麼具體如何表示其中也是有學問的。原碼是人類最直觀想到的表現方式,但原碼有缺點,因而產生了反碼,反碼也有缺點,最後產生了相對合理補碼,因而計算機系統大多使用補碼來存儲二進制數。反碼的做用主要是方便原碼到補碼的過渡,方便理解。 若是隻有加法和正數,那麼使用原碼沒有什麼問題。之因此出現反碼、補碼,正是爲了解決負數和減法所帶來的問題。因此正數的原碼、反碼、補碼都是同樣的。
若是想對原碼、反碼、補碼有更深的瞭解,推薦讀《計算機原理》這本書,這裏就再也不贅述了。
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
複製代碼
下面講一下位運算符
當兩個操做數相對應的位都爲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 = 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種,正數、負數和零。
在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秒後公佈答案) .
.
.
.
.
.
.
答案:六隻小老鼠
咱們將這些藥從0~63按照二進制編號,得到64個六位數的二進制編號,也就是從000000(六個零)到111111(六個一),每一個二進制編號的最左邊是第一位,最右邊是第六位。
而後選六隻老鼠從左到右排開,和二進制的六位,從左到右地依次對應。文稿裏的二進制編號,你能夠試着一位一位豎着看,下面每隻老鼠負責一位。
從左邊數第一個老鼠吃對應的二進制是1的藥,0就不吃。那麼老鼠1依次吃第32,33,34,……,63號藥。第二隻吃16,17,……,31,48,49,……,63號藥,等等。最後一隻老鼠吃1,3,5,……,63號藥。你可能注意到了,6只老鼠都吃了63號,那是由於63對應的二進制編號是6個1,因此6只都要吃。
吃完藥以後三天,某些老鼠可能死了,咱們假定第1,2,6這三隻老鼠死了,剩下的活着。這說明什麼呢?說明編號110001號藥有問題,也就是在第1,第2,第6位上分別是3個1,由於這三隻老鼠都吃了它,而3,4,5這三隻沒死的老鼠沒有吃它(對應的位置爲0)。而110001對應十進制的49,也就是說第49瓶藥是毒藥。
對於其它的組合也是一樣的,你能夠本身隨便假定哪幾只老鼠死了,看看哪瓶是毒藥。固然,還有一種狀況,就是全部的老鼠都沒有死,那說明第0號藥是毒藥,由於其餘的藥都吃過了,就這一瓶沒有吃。
關於位運算的基本運算方式和麪試題就講到這裏,若是你有更經典、有趣的面試題,不妨分享出來。一塊兒討論,歡迎留言。
祝工做順利
鄧文斌
2019年6月6日