1. 程式人生 > >位運算相關題解

位運算相關題解

> *今日得到: 位運算真的是 666, 計算機基礎還有數學知識都很重要.* ### [LeetCode-191 二進位制位1的個數](https://leetcode-cn.com/problems/number-of-1-bits/submissions/) > LeetCode上第 191 號問題:編寫一個函式,輸入是一個無符號整數,返回其二進位制表示式中數字位數為 ‘1’ 的個數。 觀察一下 n 與 n-1 這兩個數的二進位制表示:對於 n-1 這個數的二進位制來說,相對於 n 的二進位制,它的最末位的一個 1 會變成 0,最末位一個 1 之後的 0 會全部變成 1,其它位相同不變。 > 比如 n = 8888,其二進位制為 **10001010111000** > 則 n - 1 = 8887 ,其二進位制為 **10001010110111** > > 通過按位與操作後:n **&** (n-1) = **10001010110000** > > **也就是說:通過 n&(n-1)這個操作,可以起到消除最後一個1的作用。** 所以可以通過執行 n&(n-1) 操作來消除 n 末尾的 1 ,消除了多少次,就說明有多少個 1 。 ```python class Solution: def hammingWeight(self, n): res = 0 while n != 0: res += 1 n &= (n - 1) return res def hammingWeight2(self, n): res = 0 while n != 0: res += (n & 1) n = n >> 1 return res ``` [演算法-求二進位制數中1的個數 多種解法](https://www.cnblogs.com/graphics/archive/2010/06/21/1752421.html) ### [LeetCode-231 2的冪](https://leetcode-cn.com/problems/power-of-two/) > 給定一個整數,編寫一個函式來判斷它是否是 2 的冪次方。 ![](https://img2020.cnblogs.com/blog/778496/202007/778496-20200717234510752-1749252597.png) 仔細觀察,可以看出 2 的次方數都只有一個 1 ,剩下的都是 0 。根據這個特點,只需要每次判斷最低位是否為 1 ,然後向右移位,最後統計 1 的個數即可判斷是否是 2 的次方數, 可以使用上一個問題的解法 ```python def isPowerOfTwo(n): res = 0 while n != 0: res += (n & 1) n >>= 1 return res == 1 ``` **該題還有一種巧妙的解法。再觀察上面的表格,如果一個數是 2 的次方數的話,那麼它的二進數必然是最高位為1,其它都為 0 ,那麼如果此時我們減 1 的話,則最高位會降一位,其餘為 0 的位現在都為變為 1,那麼我們把兩數相與,就會得到 0**. ![](https://img2020.cnblogs.com/blog/778496/202007/778496-20200717235219652-1983918441.png) [圖片來源](https://leetcode-cn.com/problems/power-of-two/solution/power-of-two-er-jin-zhi-ji-jian-by-jyd/) 比如 2 的 3 次方為 8,二進位制位 1000 ,那麼 `8 - 1 = 7`,其中 7 的二進位制位 0111 ```python class Solution: def isPowerOfTwo(self, n: int) -> bool: return (n > 0) and ((n & (n - 1)) == 0) ``` ### [LeetCode-201. 閉區間範圍內數字按位與](https://leetcode-cn.com/problems/bitwise-and-of-numbers-range/) > 給定範圍 [m, n],其中 0 <= m <= n <= 2147483647,返回此範圍內所有數字的按位與(包含 m, n 兩端點) ![在這裡插入圖片描述](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9waWMubGVldGNvZGUtY24uY29tL0ZpZ3VyZXMvMjAxLzIwMV9wcmVmaXgucG5n?x-oss-process=image/format,png) 所有這些位字串的公共字首也是指定範圍的起始和結束編號的公共字首(即在上面的示例中分別為 9 和 12),因此,我們可以將問題重新表述為:給定兩個整數,要求我們找到她們**二進位制字串的公共字首** 1. 使用191的方法 `Brian Kernighan 演算法` `n & (n-1) ` ```python class Solution: def rangeBitwiseAnd(self, m: int, n: int) ->
int: while m < n: # turn off rightmost 1-bit n = n & (n - 1) return m & n ``` 2. 找m, n 的最高位1出現的位置 , 如果不相等,則返回0,如果相等,則找公共字首。[B站視訊-位運算練習【LeetCode】](https://www.bilibili.com/video/BV1wp4y1X768) ### [LeetCode-187.重複的DNA序列](https://leetcode-cn.com/problems/repeated-dna-sequences/) > 所有 DNA 都由一系列縮寫為 A,C,G 和 T 的核苷酸組成,例如:“ACGAATTCCG”。在研究 DNA 時,識別 DNA 中的重複序列有時會對研究非常有幫助。 > > 編寫一個函式來查詢目標子串,目標子串的長度為 10,且在 DNA 字串 s 中出現次數超過一次。差點沒看懂題 QAQ! ``` 輸入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT" 輸出:["AAAAACCCCC", "CCCCCAAAAA"] ``` ```python # 普通解法 class Solution: def findRepeatedDnaSequences(self, s: str) ->
List[str]: d = {} for i in range(len(s) - 9): k = s[i: i+10] if k in d: d[k] = True else: d[k] = False return [*filter(lambda x: d[x], d)] ``` 該的位運算解法暫時沒看懂,先記錄著,有點暈了,後面繼續看 [題解](https://leetcode-cn.com/problems/repeated-dna-sequences/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-4-7/) ### [LeetCode-36.只出現一次的數字](https://leetcode-cn.com/problems/single-number/) >
給定一個非空整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素。 > > 要求: 你的演算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎? > > ``` > 輸入: [2,2,1] > 輸出: 1 > 輸入: [4,1,2,1,2] > 輸出: 4 > ``` 這題比較簡單,**想到異或運算,相同為0,不同為1**的規則就可以很快求解了 | a | b | a⊕b | | ---- | ---- | ---- | | 1 | 0 | 1 | | 1 | 1 | 0 | | 0 | 0 | 0 | | 0 | 1 | 1 | ```python class Solution: def singleNumber(self, nums: List[int]) -> int: # 非空陣列暫時不用判斷 from functools import reduce return reduce(lambda a, b: a ^ b, nums) ``` ### [LeetCode-137.只出現一次的數字 II](https://leetcode-cn.com/problems/single-number-ii/) > 給定一個**非空**整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現了三次。找出那個只出現了一次的元素。 > > 要求: 你的演算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎? > > ``` > 輸入: [2,2,3,2] > 輸出: 3 > 輸入: [0,1,0,1,0,1,99] > 輸出: 99 > ``` ![Picture1.png](https://pic.leetcode-cn.com/28f2379be5beccb877c8f1586d8673a256594e0fc45422b03773b8d4c8418825-Picture1.png) > 3×(*a*+*b*+*c*)−(*a*+*a*+*a*+*b*+*b*+*b*+*c*)=2*c* 也可以應用在上一題 ```python ## 普通解法 class Solution: def singleNumber(self, nums): return (3 * sum(set(nums)) - sum(nums)) // 2 ``` 推廣到一般情況: > 如果其他數都出現了 k 次,一個數出現了一次。那麼如果 k 是偶數,還是把所有的數異或起來就行了。如果 k 是奇數,那麼統計每一位是 1 的個數,然後模 k 取餘數就能得到那個單獨的數了 。其中有`sum = kn + 1` 位運算的解法是有限狀態機+位運算,感覺有點難理解,自己推敲一遍勉強可以理解,自己畫一個狀態表,然後推匯出響應的公式就比較好了。 ![Picture4.png](https://pic.leetcode-cn.com/f75d89219ad93c69757b187c64784b4c7a57dce7911884fe82f14073d654d32f-Picture4.png) 我是先看題解1, 在看題解2,才搞明白了。 1. [【自動機+位運算】最詳細的推導過程](https://leetcode-cn.com/problems/single-number-ii/solution/zi-dong-ji-wei-yun-suan-zui-xiang-xi-de-tui-dao-gu/) 2. [圖片來源:有限狀態自動機 + 位運算,清晰圖解-此題解清晰](https://leetcode-cn.com/problems/single-number-ii/solution/single-number-ii-mo-ni-san-jin-zhi-fa-by-jin407891/) **幾乎每道題都能看到題解2的作者,佩服不已,時而習之,但求甚解。** ```python class Solution: def singleNumber(self, nums: List[int]) -> int: ones, twos = 0, 0 for num in nums: ones = ones ^ num & ~twos twos = twos ^ num & ~ones return ones ``` ### [LeetCode-260. 只出現一次的數字 III](https://leetcode-cn.com/problems/single-number-iii/) > 給定一個整數陣列 `nums`,其中恰好有兩個元素只出現一次,其餘所有元素均出現兩次。 找出只出現一次的那兩個元素。 > > ``` > 輸入: [1,2,1,3,2,5] > 輸出: [3,5] > ``` **注意:** 1. 結果輸出的順序並不重要,對於上面的例子, `[5, 3]` 也是正確答案。 2. 你的演算法應該具有線性時間複雜度。你能否僅使用常數空間複雜度來實現? 根據前面找一個不同數的思路演算法,在這裡把所有元素都異或,那麼得到的結果就是那兩個只出現一次的元素異或的結果。 > **如果我們把原陣列分成兩組,只出現過一次的兩個數字分別在兩組裡邊,那麼問題就轉換成之前的老問題了,只需要這兩組裡的數字各自異或,答案就出來了。** **那麼通過什麼把陣列分成兩組呢?** 放眼到二進位制,我們要找的這兩個數字是不同的,所以它倆至少有一位是不同的,所以我們可以根據這一位,把陣列分成這一位都是 1 的一類和這一位都是 0 的一類,這樣就把這兩個數分到兩組裡了。 **那麼怎麼知道那兩個數字哪一位不同呢?** 回到我們異或的結果,如果把陣列中的所有數字異或,最後異或的結果,其實就是我們要找的兩個數字的異或。而異或結果如果某一位是 1,也就意味著當前位兩個數字一個是 1 ,一個是 0,也就找到了不同的一位。 以上思路源於作者:[windliang](https://leetcode-cn.com/problems/single-number-iii/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-5-8/) ```python class Solution: def FindNumsAppearOnce(self, nums): length = len(nums) if length <= 0: return nums result = 0 # 先將所有數子異或得到一個值 for num in nums: result ^= num # 找到這個值最低位二進位制位1的位置,根據這個位置來區分兩個陣列,分別異或求出只出現一次的數字 firstBitIndex = self.FindFirstBit(result) n1, n2 = 0, 0 for num in nums: if self.IsSameBit(num, firstBitIndex): n1 ^= num else: n2 ^= num return n1, n2 def FindFirstBit(self, num): indexBit = 0 while num & 1 == 0: indexBit += 1 num = num >> 1 return indexBit def IsSameBit(self, num, indexBit): num = num >> indexBit return num & 1 ``` ```python # 解放2 class Solution2: def FindNumsAppearOnce(self, nums): length = len(nums) if length <= 0: return [] diff = 0 for i in nums: diff ^= i n1, n2 = 0, 0 minDiff = self.getMinDiff(diff) for num in nums: if minDiff & num == 0: n1 ^= num n2 = diff ^ n1 return n1, n2 def getMinDiff(self, num): # 保留一個低位是1的數字 # 取負號其實就是先取反,再加 1,需要 補碼 的知識。最後再和原數相與就會保留最低位的 1。比如 1010,先取反是 0101,再加 1,就是 0110,再和 1010 相與,就是 0010 了 return num & (-num) ``` ### 如何得到二進位制位只有一個1的數,幾種方法 > diff &= -diff ; [這裡](https://leetcode.com/problems/single-number-iii/discuss/68900/Accepted-C%2B%2BJava-O(n)-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的做法。 取負號其實就是先取反,再加 1,需要 補碼 的知識。最後再和原數相與就會保留最低位的 1。比如 `1010`,先取反是 `0101`,再加 1,就是 `0110`,再和 `1010` 相與,就是 `0010` 了。 > diff = (diff & (diff - 1)) ^ diff; [這裡](https://leetcode.com/problems/single-number-iii/discuss/68921/C%2B%2B-solution-O(n)-time-and-O(1)-space-easy-understaning-with-simple-explanation) 的做法 `n & (n - 1)` 的操作在 191 題 用過,它可以將最低位的 1 置為 0。比如 `1110`,先將最低位的 1 置為 0 就變成 `1100`,然後再和原數 `1110 `異或,就得到了` 0010` > diff = xor & ~(diff - 1) [這裡](https://leetcode.com/problems/single-number-iii/discuss/68923/Bit-manipulation-beats-99.62) 的做法 先減 `1`,再取反,再相與。比如 `1010` 減 `1` 就是 `1001`,然後取反 `0110`,然後和原數 `1010` 相與,就是 `0010` 了 ``` python mask=1 while((diff & mask)==0): mask <<= 1 # mask 就是我們要構造的了 ``` [這裡](https://leetcode.com/problems/single-number-iii/discuss/342714/Best-Explanation-C%2B%2B) 的做法 ### 參考資料 [補碼為什麼按位取反再加一](http://blog.sina.com.cn/s/blog_e68cc97e0102wx8i.html)認真看完會有收穫的。 [五分鐘學演算法-面試官,別問我 Bit Operation 了!](https://mp.weixin.qq.com/s?__biz=MzUyNjQxNjYyMg==&mid=2247484756&idx=1&sn=637e3a62b0be2d6ca10c44820e919a03&chksm=fa0e6ad5cd79e3c312fe2e8ec4577ac37cf5e8370cbdb5b237331ae68319bdf251fd07fd48ea&token=1508886278&lang=zh