位運算 | (二)位運算常見技巧及講解


前言

上一節里我們介紹了位運算的幾個基本運算符以及運算規則,在本節將會結合具體案例來講解位運算的一些常見使用技巧及應用場景。為了讓介紹更加有條理,本文將按照與(&)、或(|)、異或(^)、取反(~)以及位移運算操作的順序,來分別介紹對應運算的常見使用技巧。對於某些技巧,如果需要使用多個運算符結合,則會靠后講解,此外本文中針對某個數的位數均從0開始。

技巧總結

  1. &的常見技巧

    我們知道,&運算只有在運算的兩個數位都為1時,才為1。所以這個特點也正是&的一些技巧核心(多通過一個魔法數用於將數字的某些位置0)。例如將一個數x的第2位置0,只需要將x和0xffff fffb進行&運算即可(b對應1011)。下面就來具體介紹幾個與&有關的技巧:

    • 一般來說,我們判斷一個數的奇偶,都是通過x % 2的結果進行判斷,其實我們也可以通過x & 1的結果來判斷,由上文,我們知道x & 1會將除了第0位,其它位全部置0。而奇數的二進制表示末位為1,偶數為0。故可以通過這種方式用於判斷奇偶。

      特別注意,由於運算符優先級的原因,對於x & 1 == 1,會先運算1 == 1,所以對於x & 1 == 1,必須改為對於(x & 1) == 1。由於Java中不像C等語言可以使用if (x & 1) {}這樣的語句。因此,其實在Java中還是直接使用x % 2 == 0來的方便。

    • 在第一點里我們講了一個看起來不怎么實用的技巧,這次我們將一個稍微有用點的。我們都知道在ASCII表里,A - Z對應編碼為65(0100 0001) - 90(0101 1010),而a - z對應編碼為97(0110 0001) - 122(0111 1010)。仔細比較,我們可以發現,對於小寫字母和大寫字母的二進制表示,比如A和a,一個為0100 0001(A),一個為0110 1010(a),只有第5位有差別,因此我們只需要將第5位利用&置0,其它位保留,便可以很快將小寫字母轉為大寫。而這個魔法數便是-33(0xffff ffdf),因此在Java中我們可以通過(char) (c & -33)這個語句,快速將一個字母c轉為其對應的大寫字母。既然有小寫轉大寫,那么有大寫轉小寫的方法嗎?當然有啦,不過並不是使用&,而是使用|運算,下面很快就會講到。

    上面我們講了兩個比較基礎的技巧,下面就講一個稍微復雜一些的技巧,同時也准備了相應的例題,以加深理解。

    • 首先,先說一個結論n & (n - 1)用於將n對應二進制位最右邊的1去掉(對於二進制數位中不包含1的0,后面將不做討論)。然后我們再來解釋具體原因(為了方便,接下來的數以二進制形式表示):對於任意一個數,由於我們考慮的是將一個數的二進制數位的最后一個1置為0,因此我們我可將一個數n表示為xxx100這種形式(x代表任意0或者1,代表最有右邊的1前面的數位,由於是數位中的最后的一個1,那邊1后面也就一定跟着若干個0了(對於1而言,即1后面跟着0個0,但最終依然滿足這個運算,這里我們就以2個0代表若干個0))。

      那么對於n(xxx100),n-1(xxx011),其運算結果,不用我說,大家也都知道是xxx 000,實現了將n(xxx 100)的最后一個1置為0的目標。這里只是做了簡單證明,大家可以自己進行驗證運算試試。下面我們就來根據這個技巧來講一道例題(並不一定說是這道題的最好結果,但可以用於加深對這個技巧的理解):

      統計一個數中二進制位中1的個數,試着不使用Java等語言自帶的bitCount方法試試看!

      當看到這題,我們很容易想到的方法就是類似下面這種代碼:

      count = 0
      while n != 0:
          '''
          對二進制位的每一位進行判斷
          為1則count加1,否則加0,
          總運算次數取決於n個最高位1所在的位置
          '''
          count += n & 1
          n >>= 1
      print(count)
      

      不過,試着想一想,怎么能夠利用我們剛才所講的方法呢?仔細考慮可以想到,剛才的技巧可以用於將一個數的最后一個數位置為0,那么每去掉最后一個1,接下來便可以去掉倒數第二個1......仔細考慮,這種方法的運算次數只需要數位中1的個數的次數,下面是代碼實現:

      count = 0
      while n != 0:
          # 每次去掉n的二進制表示中的一個1
          n &= n - 1
          count += 1
      print(count)
      

    這個技巧的本質依然是將指定為置為0(即將一個數二進制數位中最后一個1置為0),所以和&有關的技巧的核心都是為了將某些數位置為0。下面還會講到一個和~組合來實現置0操作的技巧,我們放到講解~的時候再說。

  2. |的常見技巧

    由上文討論&的技巧核心(將數位置0),相信聰明的各位一定知道,|的核心技巧便是利用|運算的特點,將某些數位置為1。還是舉個例子,比如將一個數x的第3位置1,只需要將x和0x0000 0008進行|運算即可(8對應1000)。下面就介紹幾個與這一核心有關的技巧:

    • 在第一點我們就先說上文提到的將大寫字母轉為小寫字母的方法。由上面討論大小寫字母的二進制時提到的,每個小寫字母與其大寫字母只有第5位不同,小寫字母第5位均為1,而大寫字母均為0。因此,我們便可以通過|運算,將第5位置為1,即可將一個大寫字母轉為小寫字母,這個魔法數便是32(0x0000 0040),因此在Java中我們可以通過(char) (c | 32)這個語句,快速將一個字母c轉為其對應的大寫字母。

      |運算當然不止這一個簡單的常用技巧,不過為了平(qiang)衡(po)性(zheng),加上那個技巧也使用到了位移運算,雖然其核心是|,我還是將其放到位運算部分就行講解了。

  3. ^的常見技巧

    ^運算符不像&|可以有一些常用的運算式,使用^的時候,大多都是利用其(兩數相同則結果為0,否則非0)這個特性,下面就來講兩個小技巧,第一個還是有一定運用場景的:

    • 在講第一個技巧之前,我們先看一道題:

      給定兩個整數,判斷兩個數是否同號(即同正或者同負)。

      當我們第一眼看到題,很容易就想到下面兩種代碼:

      def f(a:int, b:int):
          return (a >= 0 and b >= 0) or (a < 0 and b < 0)
      
      def f(a:int, b:int):
       # 可能會溢出,並不正確
          return a * b >= 0
      

      而這道題,如果我們使用^,則不僅會讓代碼看起來特別簡潔,還不需要擔心乘法的溢出問題,對應的代碼如下:

      def f(a:int, b:int):
          return (a ^ b) >= 0
      

      而原理也很簡單,由於兩個如果同號,那么最高符號位一定相同,經過異或后會變為0,那么異或結果就為非負數,故可以根據兩數異或結果的正負來判斷兩數是否同號。

    • 第二個技巧則並不是那么實用,更多的是讓人感覺有意思,相信很多人也都見到過,代碼和解釋如下:

      a ^= b // 原始的a變為了a ^ b
      b ^= a // 此時的b = (a ^ b) ^ b = a
      a ^= b // 當前a的值為a ^ b,b的值變為了a,故a = (a ^ b) ^ a = b
      

      第一次看上述代碼和解釋可能會覺得有些迷惑,但稍加思考就會發現,這個操作只是為交換兩個數的值而已。這中方法在實際編碼中並不建議使用,更多的拓展自己的思維方式。

  4. ~的常見技巧

    其實~的常用技巧可以說沒有,唯一一個還不是作為主角,為了平衡一下,就放到這里講了,相信看了上文已經知道,主角是誰了,對,就是&!先來回顧一下~的用處:用於將二進制數位中1轉化為0,0轉化為1,一個整數得到其對應的負數可以使用(~x + 1)。由於這個技巧其實也不是那么常用吧,我只是因為強迫症哈哈,所以先說結論:n & (~n + 1)可以用於將一個數的二進制位中除了最后一個1之外,其它數位均置為0,聽起來是不是覺得有點懵,但是又感覺有點熟悉?這就對了,這個其實我們在&里最后一個例子,反過來的效果,那里是把一個數的最后一個1置為0,而這里這是保護這最后一個1,其它位都變為0!下面來解釋原因(又使用到了之前的表示方法,對於0和1的情況,大家可以自己驗證喲,為了效果名言,我們就把1后面的若干個0表述為2個0了):考慮n(xxx100),為了計算~n + 1,我們先計算~n,結果為zzz011這里的z代表和x相反),然后進行加1可以得到zzz100,因此n和~n + 1的計算結果就如下:

    x x x 1 0 0

    &

    z z z 1 0 0

    0 0 0 1 0 0 (由於x和z相反故為0)

    這里有一道例題,可以用到這個技巧,當然這道題的核心是使用^的特點,不過大家也可以試試。

  5. 位移運算(>><<>>>)的常見技巧

    通過上一節的學習我們了解到了位移運算的規則以及一些運算細節,一般來說位移操作都可以代替乘法進行乘2或者除2之類的操作,就像在Java中的ArrayList源碼中,在進行容量擴充時,通過類似x + (x >> 1)實現了將原始容量擴充為1.5倍,下面我就來講一些與位移運算的技巧:

    • 第一點先講一個基礎的,也是我們經常容易忽略的,相信很多人也有所了解,就是在二分查找中,(left + right) / 2這個操作會因為left和right都很大時導致加法溢出而無法得到想要的結果,一般的解決方法,都是通過left + (right - left) / 2來實現,不過在Java中,由於我們有>>>這個無符號右移,所以我只需要使用(left + right) >>> 1即可避免溢出,當然由於不是所有語言都有支持,所以>>>這個技巧也顯得有些局限。

    • 最后我們來講一個比較實用的操作(雖然只是在刷題中實用,在LeetCode上有好幾道題可以利用這一思路)。由於比較復雜,我們直接來講結論,然后再仔細分析和證明:

      1. a |= a >> 16;	// 整個操作用於使得a最高
      2. a |= a >> 8;		// 位1及以下均變為1,例如
      3. a |= a >> 4;		// 1010 0000,經過這幾步操作
      4. a |= a >> 2;		// 會變為1111 1111.
      5. a |= a >> 1;
      

      下面就開始進行具體證明,整個解釋基於8位整數而言(為了講解方便,因此我們只需要3-5步的操作),數位從0開始,由於數字0不包含1,所以經過操作得到結果依然是0,即0 - 7,假設有一個數其最高位的1在第x位(0 <= x <= 7),那么經過第一次操作,第x位和第x - 4位均會變為1(利用了|運算置1的特點),然后再執行第4步操作,由於我們現在第x位和第x - 4位均為1,故經過第5步,我們現在就可以讓第x - 2位和第x - 6位也變為1,同理經過第5步,我們就可以讓第x-7 ~ x位均為1(假如x為3,那么對應-4 ~ 3,負數位拋棄,即0 ~ 3位均會變為1),最終實現了x位及x以下數位均置為1的操作。最后再留一道題,正是利用了上面的思路,大家可以試一下,數字的補數

小結

以上便是關於幾個常見位運算的使用技巧和具體案例說明,當然還有很多常用的技巧,由於通用性等原因,可能會放到后面再講,不過興趣是最好的老師,還是希望大家能自己多發掘一些,然后一起交流,此外后續還會總結與位運算有關的數據結構,可能的話還會有實際應用場景的總結,這次就講到這里,如有錯誤之處,也請各位幫忙指出。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM