概述
與、或、異或、取反或者移位運算這幾種基本的位操作想必諸位讀者並不陌生,如果我們能在某些合適場景下使用位運算,有些時候可以大大提高算法的效率。但由於本身位運算太過靈活,甚至某些技巧比較苦澀難懂,因而,本篇文章主要介紹幾種常見的或者有趣的位操作,並且給出一些用到這些技巧的算法題目,便於讀者練習。
有趣的操作
1. 大小寫字母轉換
- 利用
或操作
和空格將英文字母轉成小寫
('a' | ' ') = 'a';
('A' | ' ') = 'a';
- 利用與運算
&
和下划線將英文字符轉換成大寫
('b' & '_') = 'B';
('B' & '_') = 'B';
- 利用異或運算
^
和空格進行英文字符大小寫互換
('d' ^ ' ') = 'D';
('D' ^ ' ') = 'd'
常用指數: 🔯🔯
容易指數:🔯🔯🔯
PS:上述技巧能夠產生奇特效果的原因在於字符類型的數據都是通過ASCII進行編碼的,字符本身其實就是數字,而剛好這些字符對應的數字通過位運算符就可以得到正確結果,此處就不展開來說了。
2. 判斷兩個數是否異號
int x = -1, y = 2;
boolean f = ((x ^ y) < 0); // true 兩個int類型數據進行異或運算小於零證明異號
int x = 1, y = 2;
boolean f = ((x ^ y) < 0); // false 兩個int類型數據進行疑惑運算大於零證明同好
常用指數:🔯🔯🔯
困難指數:🔯
PS:這個操作在我們判斷兩個數異號的時候非常有用,一方面運算效率較高,另一方面可以減少if else 分支的使用。其背后的原理主要是一個正數補碼的符號位和一個負數補碼的符號位肯定想法,經過疑惑運算后,最后符號位結果肯定是1
(代表負數)。
3. 移除最后一位"1"
byte n = 10;
// n的二進制表示為: 0 0 0 0 1 0 1 0
// 異或運算 ^
//n-1的二進制表示為:0 0 0 0 1 0 0 1
n & (n-1); //結果為:0 0 0 0 1 0 0 0
根據上邊的注釋以及示意圖,整個操作過程應該不難理解
簡單來說n & (n -1 )
主要作用:就是消除數字n的二進制表示中的最后一個1.
常用指數:🔯🔯🔯🔯🔯
困難指數:🔯🔯
PS:這個操作特別常用,在好多leetcode題目中都有涉及,比如用來判斷一個數的二進制數中1的個數。
4. 獲取最后一個1
byte n = 10;
//結果為:0 0 0 0 0 0 1 0
(n & (n-1)) ^ n;
常用指數:🔯🔯🔯🔯
困難指數:🔯🔯🔯
PS:該操作剛好和[操作3](# 3. 移除最后一位"1")相反,主要是為了獲取數字n的二進制表示中的最后一個1.該操作通常會用在位標記的時候使用(可參考[漢明距離](#2. 漢明距離))。
5. 異或運算的簡單性質
a=0^a=a^0
0=a^a
常用指數:🔯🔯🔯🔯
困難指數:🔯
PS:這個性質在我們判斷兩個數是否相同的時候非常常用。
應用
前邊總結了那么多常用的位操作,下邊來做幾道題消化吸收一下剛剛所學的知識。
1. 只出現一次的數字
這道題比較簡單,說實話即使不用位運算我們也可以有好多種方法求解,比如可以用一個Map來對元素進行boolean標記來求解。但如果我們此處能想到用異或運算的性質,那這道題目我們便可以簡單優雅的解出來。
思路:用一個初值為0的變量不斷和數組中的元素進行異或運算,最終得到的變量值便是最終結果。
原因:因為0和任何一個元素進行異或運算都是0,而任何兩個相同元素進行異或之后的結果都是0,而題目中只有一個數是單獨存在的,其他數都是2個,因而不斷進行異或運算最后的結果必然是那個獨特的數值,也就是最終的結果。
代碼如下:
public int singleNumber(int[] nums) {
int result = 0;
for (int i =0;i<nums.length;i++){
result ^=nums[i];
}
return result;
}
2. 漢明距離
這個問題,我們常規思路可能是通過一次for循環
並且在循環過程中判斷不同位置的二進制位是否相同,同時做計數。
但同樣的我們通過位運算可以比較快速的解決該問題
思路:通過一次異或運算,獲取一個不同位置二進制數構成的一個整數,然后計算該整數中1的個數即為最終結果。在計算1個數的時候一個小技巧,可以通過n & (n-1)不斷做移位運算來計算。
實現代碼如下:
public int hammingDistance(int x, int y) {
x = x ^ y;
int count = 0;
while (x != 0) {
x = x & (x - 1);
count++;
}
return count;
}
1. 只出現一次的數字 III
這個問題跟[只出現一次的數字](#1. 只出現一次的數字)很像,主要差別可能在這個唯一的數是兩個,而不是一個。同樣的這個問題有很多常規的結果,比如街主要map來統計和標注每個數字出現的次數。但我們使用位運算的時候我們會發現其速度是極快的。
思路:
前面分析了此題目和原來題目的最大區別在於只出現一次的數字不再是唯一的了,而變成了兩個。因而我們考慮能否對着集合元素按照某種標准做一個分類,經過分類之后,每一個類別都分別包含一個特殊的元素以及若干相同的元素。此時該問題就轉換成了[問題1](#1. 只出現一次的數字),那個簡單的問題。
其實分類標准應該不難找,由於這兩個元素是不相等的,因此這兩個元素的某個二進制位必然是不相等。因此我們可能考慮根據該二進制位為0或者1將數組分成A組合B組。而這個數組中其它數字要么就屬於A組,要么就屬於B組。再對A組和B組分別執行“異或”解法就可以得到A,B了。而要判斷A,B在哪一位上不相同,只要根據A異或B的結果就可以知道了,這個結果在二進制上為1的位就說明A,B在這一位上是不相同的。
比如:
int a[] = {1, 1, 3, 5, 2, 2}
整個數組異或的結果為3^5,即 0x0011 ^ 0x0101 = 0x0110
對0x0110
,第1位(由低向高,從0開始)就是1。因此整個數組根據第1位是0還是1分成兩組。
a[0] =1 0x0001 第一組
a[1] =1 0x0001 第一組
a[2] =3 0x0011 第二組
a[3] =5 0x0101 第一組
a[4] =2 0x0010 第二組
a[5] =2 0x0010 第二組
第一組有{1,1,5},第二組有{3,2,2},然后對這二組分別執行“異或”解法就可以得到5和3了。\
實現代碼如下:
public int[] singleNumber(int[] nums) {
int xorVal = 0;
for (int num : nums) {
xorVal ^= num;
}
// 獲取最后一個 1
xorVal = (xorVal & (xorVal - 1)) ^ xorVal;
int res[] = new int[2];
//根據和xorVal的與運算結果不同進行分類
for (int num : nums) {
if ((num & xorVal) == 0) {
res[0] ^= num;
} else {
res[1] ^= num;
}
}
return res;
}
總結
以上便是一些常用的位操作,以及對應的一些簡單應用。其實關於微操作的技巧很多,有很多也非常有趣,其中有一個叫做 BitTwiddling Hacks 的外國網站收集了幾乎所有位操作的黑科技玩法,感興趣的話,可以看一看哈!!