【技巧總結】位運算裝逼指南

帥地發表於2020-01-15

位演算法的效率有多快我就不說,不信你可以去用 10 億個資料模擬一下,今天給大家講一講位運算的一些經典例子。不過,最重要的不是看懂了這些例子就好,而是要在以後多去運用位運算這些技巧,當然,採用位運算,也是可以裝逼的,不信,你往下看。我會從最簡單的講起,一道比一道難度遞增,不過居然是講技巧,那麼也不會太難,相信你分分鐘看懂。

判斷奇偶數

判斷一個數是基於還是偶數,相信很多人都做過,一般的做法的程式碼如下

if( n % 2) == 01
    // n 是個奇數
}

如果把 n 以二進位制的形式展示的話,其實我們只需要判斷最後一個二進位制位是 1 還是 0 就行了,如果是 1 的話,代表是奇數,如果是 0 則代表是偶數,所以採用位運算的方式的話,程式碼如下:

if(n & 1 == 1){
    // n 是個奇數。
}

有人可能會說,我們寫成 n % 2 的形式,編譯器也會自動幫我們優化成位運算啊,這個確實,有些編譯器確實會自動幫我們優化。但是,我們自己能夠採用位運算的形式寫出來,當然更好了。別人看到你的程式碼,我靠,牛逼啊。無形中還能裝下逼,是不是。當然,時間效率也快很多,不信你去測試測試。

2、交換兩個數

交換兩個數相信很多人天天寫過,我也相信你每次都會使用一個額外來變數來輔助交換,例如,我們要交換 x 與 y 值,傳統程式碼如下:

int tmp = x;
x = y;
y = tmp;

這樣寫有問題嗎?沒問題,通俗易懂,萬一哪天有人要為難你,**不允許你使用額外的輔助變數來完成交換呢?**你還別說,有人面試確實被問過,這個時候,位運算大法就來了。程式碼如下:

x = x ^ y   // (1)
y = x ^ y   // (2)
x = x ^ y   // (3)

我靠,牛逼!三個都是 x ^ y,就莫名交換成功了。在此我解釋下吧,我們知道,兩個相同的數異或之後結果會等於 0,即 n ^ n = 0。並且任何數與 0 異或等於它本身,即 n ^ 0 = n。所以,解釋如下:

把(1)中的 x 帶入 (2)中的 x,有

y = x^y = (xy)y = x(yy) = x^0 = x。 x 的值成功賦給了 y。

對於(3),推導如下:

x = x^y = (xy)x = (xx)y = 0^y = y。

這裡解釋一下,異或運算支援運算的交換律和結合律哦。

以後你要是別人看不懂你的程式碼,逼格裝高點,就可以在程式碼裡面採用這樣的公式來交換兩個變數的值了,被打了不要找我。

講這個呢,是想告訴你位運算的強大,讓你以後能夠更多著去利用位運算去解決一些問題,一時之間學不會也沒事,看多了就學會了,不信?繼續往下看,下面的這幾道題,也是非常常見的,可能你之前也都做過。

3、找出沒有重複的數

給你一組整型資料,這些資料中,其中有一個數只出現了一次,其他的數都出現了兩次,讓你來找出一個數 。

這道題可能很多人會用一個雜湊表來儲存,每次儲存的時候,記錄 某個數出現的次數,最後再遍歷雜湊表,看看哪個數只出現了一次。這種方法的時間複雜度為 O(n),空間複雜度也為 O(n)了。

然而我想告訴你的是,採用位運算來做,絕對高逼格!

我們剛才說過,兩個相同的數異或的結果是 0,一個數和 0 異或的結果是它本身,所以我們把這一組整型全部異或一下,例如這組資料是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出現了一次,其他都出現了兩次,把他們全部異或一下,結果如下:

由於異或支援交換律和結合律,所以:

123451234 = (11)(22)(33)(44)5= 00005 = 5。

也就是說,那些出現了兩次的數異或之後會變成0,那個出現一次的數,和 0 異或之後就等於它本身。就問這個解法牛不牛逼?所以程式碼如下

int find(int[] arr){
    int tmp = arr[0];
    for(int i = 1;i < arr.length; i++){
        tmp = tmp ^ arr[i];
    }
    return tmp;
}

時間複雜度為 O(n),空間複雜度為 O(1),而且看起來很牛逼。

4、m的n次方

如果讓你求解 m 的 n 次方,並且不能使用系統自帶的 pow 函式,你會怎麼做呢?這還不簡單,連續讓 n 個 m 相乘就行了,程式碼如下:

int pow(int n){
    int tmp = 1;
    for(int i = 1; i <= n; i++) {
        tmp = tmp * m;
    }
    return tmp;
}

不過你要是這樣做的話,我只能呵呵,時間複雜度為 O(n) 了,怕是小學生都會!如果讓你用位運算來做,你會怎麼做呢?

我舉個例子吧,例如 n = 13,則 n 的二進位制表示為 1101, 那麼 m 的 13 次方可以拆解為:

m^1101 = m^0001 * m^0100 * m^1000。

我們可以通過 & 1和 >>1 來逐位讀取 1101,為1時將該位代表的乘數累乘到最終結果。直接看程式碼吧,反而容易理解:

int pow(int n){
    int sum = 1;
    int tmp = m;
    while(n != 0){
        if(n & 1 == 1){
            sum *= tmp;
        }
        tmp *= tmp;
        n = n >> 1;
    }
    
    return sum;
}

時間複雜度近為 O(logn),而且看起來很牛逼。

這裡說一下,位運算很多情況下都是很二進位制扯上關係的,所以我們要判斷是否是否位運算,很多情況下都會把他們拆分成二進位制,然後觀察特性,或者就是利用與,或,異或的特性來觀察,總之,我覺得多看一些例子,加上自己多動手,就比較容易上手了。所以呢,繼續往下看,注意,先別看答案,先看看自己會不會做。

5、找出不大於N的最大的2的冪指數

傳統的做法就是讓 1 不斷著乘以 2,程式碼如下:

int findN(int N){
    int sum = 1;
   while(true){
        if(sum * 2 > N){
            return sum;
        }
        sum = sum * 2;
   }
}

這樣做的話,時間複雜度是 O(logn),那如果改成位運算,該怎麼做呢?我剛才說了,如果要弄成位運算的方式,很多時候我們把某個數拆成二進位制,然後看看有哪些發現。這裡我舉個例子吧。

例如 N = 19,那麼轉換成二進位制就是 00010011(這裡為了方便,我採用8位的二進位制來表示)。那麼我們要找的數就是,把二進位制中最左邊的 1 保留,後面的 1 全部變為 0。即我們的目標數是 00010000。那麼如何獲得這個數呢?相應解法如下:

1、找到最左邊的 1,然後把它右邊的所有 0 變成 1

2、把得到的數值加 1,可以得到 00100000即 00011111 + 1 = 00100000。

3、把 得到的 00100000 向右移動一位,即可得到 00010000,即 00100000 >> 1 = 00010000。

那麼問題來了,第一步中把最左邊 1 中後面的 0 轉化為 1 該怎麼弄呢?我先給出程式碼再解釋吧。下面這段程式碼就可以把最左邊 1 中後面的 0 全部轉化為 1,

n |= n >> 1;
n |= n >> 2;
n |= n >> 4;

就是通過把 n 右移並且做運算即可得到。我解釋下吧,我們假設最左邊的 1 處於二進位制位中的第 k 位(從左往右數),那麼把 n 右移一位之後,那麼得到的結果中第 k+1 位也必定為 1,然後把 n 與右移後的結果做或運算,那麼得到的結果中第 k 和 第 k + 1 位必定是 1;同樣的道理,再次把 n 右移兩位,那麼得到的結果中第 k+2和第 k+3 位必定是 1,然後再次做或運算,那麼就能得到第 k, k+1, k+2, k+3 都是 1,如此往復下去…

最終的程式碼如下

int findN(int n){
    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8 // 整型一般是 32 位,上面我是假設 8 位。
    return (n + 1) >> 1;
}

這種做法的時間複雜度近似 O(1),重點是,高逼格。

總結

上面講了 5 道題,本來想寫十道的,發現五道就已經寫了好久了,,,,十道的話,怕你們也沒耐心寫完,而且一道比一道難的那種,,,,。

不過呢,我給出的這些例子中,並不是讓你們學會了這些題就 Ok,而且讓你們有一個意識:很多時候,位運算是個不錯的選擇,至少時間效率會快很多,而且高逼格,裝逼必備。所以呢,以後可以多嘗試去使用位運算哦,以後我會再給大家找些題來講講,遇到高逼格的,感覺很不錯的,就會拿來供大家學習了。

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

1、給俺點個讚唄,可以讓更多的人看到這篇文章,順便激勵下我,嘻嘻。

2、老鐵們,關注我的原創微信公眾號「苦逼的碼農」,專注於寫演算法 + 計算機基礎知識(計算機網路+ 作業系統+資料庫+Linux)。

儲存讓你看完有所收穫,不信你打我。後臺回覆『電子書』送你一份精選電子書大禮包,包含各類技能的優質電子書。如果關注的人多了,我就要把公眾號改為 牛逼的碼農了。

文末封面圖

相關文章