效率高的位運算子如何使用?

位運算子

我們知道,在計算機中資料其實都是以二進位制的形式所儲存的,而位運算子則可以對二進位制資料進行操作。舉個簡單的例子,給定兩個二進位制資料(其中 0b 是二進位制資料的字首)

const A = 0b1010const B = 0b1111複製程式碼

效率高的位運算子如何使用?

按位非運算子 ~

對每一位執行**非(NOT)**操作,也可以理解為取反碼。

效率高的位運算子如何使用?

按位與運算子 &

對每一位執行**與(AND)**操作,只要對應位置均為 1 時,結果才為 1,否則為 0。

效率高的位運算子如何使用?

按位或運算子 |

對每一位執行**或(OR)**操作,只要對應位置有一個 1 時,結果就為 1。

效率高的位運算子如何使用?

按位異或運算子 ^

對每一位執行**異或(XOR)**操作,當對應位置有且只有一個 1 時,結果就為 1,否則為 0。

效率高的位運算子如何使用?

左移運算子 <<

將資料向左移動一定的位(<32),右邊用 0 填充。

效率高的位運算子如何使用?

右移運算子 >>

將資料向右移動一定的位(<32),遺棄被丟出的位。

效率高的位運算子如何使用?

在學習完了位運算子以後,肯定有人會說,道理都明白了,那麼這些位運算子有什麼用呢?應該在什麼場合使用呢?平時的業務開發中也沒見過,是不是其實學了也沒什麼用?

(JDK原始碼中很多計算都用到了位運算,比如:Map集合的容量等操作。)

例子

假設我們有一個許可權系統,它透過 JSON 的方式記錄了某個使用者的許可權開通情況(姑且假設許可權集是 CURD)

const permission = { create: false, update: false, read: true, delete: false,}複製程式碼

如果我們把 false 寫成 0,true 寫成 1,那麼這個 permisson 物件可以簡寫為 0b0010。

const permission = { create: false, update: false, read: true, delete: false,}// 從左往右,依次為 create, update, read, delete 所對應的值const permissionBinary = 0b0010複製程式碼

對於 JSON 物件的許可權集,如果我們要檢視或者修改該使用者的某些許可權,只需要透過形如 permission。craete 的普通物件操作即可。那麼如果對於二進位制形式的許可權集,我們又應該如何進行檢視或者修改的操作呢?接下來我們就開始使用奇怪的知識——位掩碼來進行了。

位掩碼

首先進行名詞解釋,什麼是”位掩碼“。

位掩碼(BitMask),是”位(Bit)“和”掩碼(Mask)“的組合詞。”位“指代著二進位制資料當中的二進位制位,而”掩碼“指的是一串用於與目標資料進行按位操作的二進位制數字。組合起來,就是”用一串二進位制數字(掩碼)去操作另一串二進位制數字“的意思。

明白了位掩碼的作用以後,我們就可以透過它來對許可權集二進位制數進行操作了。

查詢使用者是否擁有某個許可權

已知使用者許可權集二進位制數為 permissionBinary = 0b0010。如果我想知道該使用者是否存在 update 這個許可權,可以先給定一個位掩碼 mask = 0b1。

效率高的位運算子如何使用?

由於 update 位於右數第三項,所以只需要把位掩碼向左移動兩位,剩餘位置補0。最後和許可權集二進位制數進行按位與運算即可得到結果。

效率高的位運算子如何使用?

最後算出來的 result 為 0b0000,使用 Boolean() 函式處理之即可得到 false 的結果,也就是說該使用者的 update 許可權為 false。

// 從左往右,依次為 create, update, read, delete 所對應的值const permissionBinary = 0b0010// 由於 update 位於右數第三位,因此只需要讓掩碼向左移動2位即可const mask = 0b1 << 2const result = permissionBinary & maskBoolean(result) // false複製程式碼

修改使用者的某個許可權

當我們明白瞭如何用位掩碼來查詢許可權後,要修改對應的許可權也就手到擒來了,無非就是換一種位運算。假設還是 update 許可權,如果我想把它修改成 true,我們可以這麼幹:

效率高的位運算子如何使用?

只需要把按位與改為按位異或即可,程式碼如下:

// 從左往右,依次為 create, update, read, delete 所對應的值const permissionBinary = 0b0010// 由於 update 位於右數第三位,因此只需要讓掩碼向左移動2位即可const mask = 0b1 << 2const result = permissionBinary ^ maskparseInt(result)。toString(2) // 0b0110複製程式碼

經過上面的內容,相信你已經基本掌握了位掩碼的知識,同時你肯定還有很多問號,比如說這麼複雜又不好閱讀的程式碼,真的有意義嗎?

髒資料記錄

前文例子中的許可權系統僅有區區4個數據的處理,位掩碼技術顯得複雜又小題大做。那麼有沒有什麼場景是真的適合使用位掩碼的呢?髒資料記錄就是其中一個。

假設我們存在著一份原始資料,其值如下:

let A = ‘a’let B = ‘b’let C = ‘c’let D = ‘d’複製程式碼

給定一個二進位制數,從左往右分別對應著 A/B/C/D 的狀態:

let O = 0b0000 // 十進位制 0複製程式碼

則資料一旦發生了修改,都可以用對應的位元位來表示

// 當且僅當 A 發生了修改O = 0b1000 // 十進位制 8// 當且僅當 B 發生了修改O = 0b0100 // 十進位制 4// 當且僅當 C 發生了修改O = 0b0010 // 十進位制 2// 當且僅當 D 發生了修改O = 0b0001 // 十進位制 1複製程式碼

同理,當多個數據發生了修改時,則可以同時表示

// 當 A 和 B 發生了修改O = 0b1100 // 十進位制 12// 當 A/B/C 都發生了修改O = 0b1110 // 十進位制 14複製程式碼

透過這個思路,應用排列組合的思想,可以很快知道只需要僅僅 4 個位元位,就可以表達 16 種資料變化的情況。由於二進位制和十進位制可以相互轉化,因此只需要區區 16 個十進位制數,就可以完整地表達 A/B/C/D 這四個資料的變化情況,也就是髒資料追蹤。舉個例子,給定一個髒資料記錄 14,二進位制轉換為 0b1110,因此表示 A/B/C 的資料被修改了。

Svelte 這個框架,就是透過這個思路來實現響應式的:

if ( A 資料變了 ) { 更新A對應的DOM節點}if ( B 資料變了 ) { 更新B對應的DOM節點}/** 轉化成虛擬碼 **/if ( dirty & 8 ) { // 8 === 0b1000 更新A對應的DOM節點}if ( dirty & 4 ) { // 4 === 0b0100 更新B對應的DOM節點}複製程式碼

更多具體的介紹可以檢視《新興前端框架 Svelte 從入門到原理》。

老鼠喝毒藥

除了用來做髒資料記錄以外,位掩碼也能夠用來處理經典的”老鼠喝毒藥“的問題。

有 1000 瓶水,其中有一瓶有毒,小白鼠只要嘗一點帶毒的水24小時後就會死亡,問至少要多少隻小白鼠才能在24小時內鑑別出哪瓶水有毒?

我們簡化一下問題,假設只有 8 瓶水,其編號用二進位制表示:

效率高的位運算子如何使用?

接著按照圖示的方式對水瓶的水進行混合,得到樣品 A/B/C/D,取4只老鼠編號為 a/b/c/d 分別喝下對應的水,得到如下的表格:

效率高的位運算子如何使用?

在 24 小時候,統計老鼠的死亡情況,彙總後可以得到表格和結果:

效率高的位運算子如何使用?

答案呼之欲出,由於 8 瓶水可以兌出 4 份樣品,因此只需要 4 只老鼠即可在 24 小時後確定到底哪一瓶水是有毒的。回到題目,如果是 1000 瓶水,只需要知道第 1000 號的二進位制數 0b1111101000即可。該二進位制數一共有 10 個位元位,意味著 1000 瓶水可以兌出 10 份樣品,也就是說只需要 10 只老鼠,就可以完成測試任務。

效率高的位運算子如何使用?

作者:code瀧

連結:https://juejin。cn/post/6947288074095165471