javascript
巧用JS位运算
位運算的方法在其它語言也是一樣的,不局限于JS,所以本文提到的位運算也適用于其它語言。
位運算是低級的運算操作,所以速度往往也是最快的(相對其它運算如加減乘除來說),并且借助位運算的特性還能實現一些算法。恰當地使用運算有很多好處。下面舉幾個例子。
1. 使用按位非~判斷索引存在
這是一個很常用的技巧,如判斷一個數是否在數組里面:
// 如果url含有?號,則后面拼上&符號,否則加上?號 url += ~url.indexOf("?") ? "&" : "?";因為:
~-1 === 0
-1在內存的表示的二進制符號全為1,按位非之后就變成了0. 進一步說明——1在內存的表示為: 0000...0001,第一位0表示符號位為正,如果是-1的話符號位為負用1表示1000...0001,這個是-1的原碼,然后符號位不動,其余位取反變成1111...1110,這個就是-1的反碼表示,反碼再加1就變成了1111...1111,這個就是-1的補碼,負數在內存里面(機器數)使用補碼表示,正數是用原碼。所以全部都是1的機器數按位非之后就變成了全為0。剩下的其它所有數按位非都不為0,所以利用這個特性可以用來做indexOf的判斷,這樣代碼看起來更簡潔一點。
2. 使用異或交換兩個數
交換兩個整數的值,最直觀的做法是借助一個臨時變量:
let a = 5,b = 6; // 交換a, b的值 let c = a; a = b; b = c;現在要求不能使用額外的變量或內容空間來交換兩個整數的值。這個時候就得借助位運算,使用異或可以達到這個目的:
let a = 5,b = 6;a = a ^ b; b = a ^ b; // b 等于 5 a = a ^ b; // a 等于 6這個是為什么呢?很簡單,把1、2式:
a = a ^ b; b = a ^ b;連起來就等價于:
b = (a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a;同理連同第3式可得:
a = (a ^ b) ^ a // 在執行第3式的時候b已經變成a了,而a是第1式的a ^ b= a ^ a ^ b = 0 ^ b = b;為什么a ^ a = 0, b ^ b = 0呢?因為異或的運算就是這樣的。如下示例:
01011010 ^ 10010110 -----------11001100異或的運算過程可以當作把兩個數加起來,然后進位去掉,0 + 0 = 0,1 + 0 = 1,1 + 1 = 0。這樣就很好記。所以a ^ a在所有二進制位上,要么同為0,要么同為1,相加結果都為0,最后就為0.
異或還經常被用于加密。
3. 使用按位與&去掉高位
按位與有很多作用,其中一個就是去操作數的高位,只保留低位,例如有a, b兩個數:
let a = 0b01000110; // 十進制為70 let b = 0b10000101; // 十進制為133現在認為他們的高位是沒用的,只有低4位是有用的,即最后面4位,為了比較a,b后4位的大小,可以這樣比較:
a & 0b00001111 < b & 0b00001111 // truea, b的前4位和0000與一下之后就都變成0了,而后四位和1111與一下之后還是原來的數。這個實際的作用是有一個數字它的前幾位被當作A用途,而后幾位被用當B用途,為了去掉前幾位對B用途的影響,就可以這樣與一下。
另外一個例子是子網掩碼,假設現在我是網絡管理員,我能夠管理的IP地址是從192.168.1.0到192.168.1.255,即只能調配最后面8位。現在把這些IP地址分成6個子網,通過IP地址進行區分,由于6等于二進制的110,所以最后面8位的前3位用來表示子網,而后5位用來表示主機(即總的主機數的范圍為00001 ~ 11111, 共30個)。當前網絡的子網掩碼取為255.255.255.111?00000即255.255.255.224,假設某臺主機的IP地址為192.168.1.120,現在要知道它處于哪個子網,可以用它IP地址與子網掩碼與一下:120 & 224 = 96 = 0b?011?00000,就知道它所在的子網為011即3號子網。
這個是保留高位去掉低位的例子,剛好與上面的例子相反。
4. 使用按位與&進行標志位判斷
現在有個后臺管理系統,操作權限分為一級、二級、三級管理員,其中一級管理員擁有最高的權限,二、三級較低,有些操作只允許一、二級管理員操作,有些操作只允許一、三級管理員操作。現在已經登陸的某權限的用戶要進行某個操作,要用怎樣的數據結構能很方便地判斷他能不能進行這個操作呢?
我們用位來表示管理權限,一級用第3位,二級用第2位,三級用第1位,即一級的權限表示為0b100 = 4,二級權限表示為0b010 = 2,三級權限表示為0b001 = 1。如果A操作只能由一級和二級操作,那么這個權限值表示為6 = 0b110,它和一級權限與一下:6 & 4 = 0b110 & 0b100 = 4,得到的值不為0,所以認為有權限,同理和二級權限與一下6 & 2 = 2也不為0,而與三級權限與一下6 & 1 = 0,所以三級沒有權限。這里標志位的1表示打開,0表示關閉。
這樣的好處在于,我們可以用一個數字,而不是一個數組來表示某個操作的權限集,同時在進行權限判斷的時候也很方便。
5. 使用按位|構造屬性集
上面構造了一個權限的屬性集,屬性集的例子還有很多,例如我在《Google地圖開發總結》里面就提到一個邊界判斷的例子——要在當前鼠標的位置往上彈一個懸浮框,如下圖左所示,但是當鼠標比較靠邊的時候就會導致懸浮框超出邊界了,如下圖右所示:
為此,需要做邊界判斷,總共有3種超出情況:右、上、左,并且可能會疊加,如鼠標在左上角的時候會導致左邊和上面同時超出。需要記錄超出的情況進行調整,用001表示右邊超出,010表示上方超出,100表示左邊超出,如下代碼計算:
let postFlag = 0; //右邊超出 if(pos.right < maxLen) posFlag |= 1; //上面超出 if(pos.top < maxLen) posFlag |= 2; //左邊超出 if(pos.left < maxLeftLen) posFlag |= 4; //對超出的情況進行處理,代碼略 switch(posFlag){case 1: //右case 2: //上case 3: //右上case 4: //左case 6: //左上 }如果左邊和上面同時超出,那么通過或運算2 | 4 = 6,得到6 = 0b110. 就知道了超出的情況,這樣的代碼相對于在if里面寫兩個判斷要好一些。
6. 位運算的綜合應用
這里有個例子——不使用加減乘除來做加法,經常用來考察對位運算的掌握情況。讀者可以先自行嘗試分析和實現。
不能用加減乘除,意思就是要你用位運算進行計算。以實際例子說明,如a = 81 = 0b1010001,b = 53 = 0b0110101。通過異或運算,我們發現異或把兩個數相加但是不能進位,而通過與運算能夠知道哪些位需要進位,如下所示:
1010001 ^ 0110101---------11001001010001 & 0110101---------0010001把通過與運算得到的值向左移一位,再和通過異或得到的值相加,就相當于實現了進位,這個應該不難理解。為了實現這兩個數的相加可以再重復這個過程:先異或,再與,然后進位,直到不需要再進位了就加完了。所以不難寫出以下代碼:
function addByBit(a, b) {if (b === 0) {return a;}// 不用進位的相加let c = a ^ b;// 記錄需要進位的let d = a & b;d = d << 1;// 繼續相加,直到d進位為0return addByBit(c, d); }let ans = addByBit(5, 8); console.log(ans);位運算還經常用于生成隨機數、哈希,例如Chrome對字符串進行哈希的算法是這樣的:
uint32_t StringHasher::AddCharacterCore(uint32_t running_hash, uint16_t c) {running_hash += c;running_hash += (running_hash << 10);running_hash ^= (running_hash >> 6);return running_hash; }不斷對當前字符串的ASCII值進行累加運算,里面用到了異或,左移和右移。
本篇介紹了使用位運算的幾個實際的例子,希望能加深位運算的理解、對開發有所幫助。
總結
- 上一篇: KepOPC全新DA2UA中间件实现OP
- 下一篇: 编译原理生成中间代码(flex和biso