面試官,你再問我 Bit Operation 試試?

程式設計師吳師兄發表於2019-02-21

在面試環節中,面試官很喜歡問一些特別的題目,這些題目有著特殊的解法,如果回答的巧妙往往能在面試中加分。

在這些題目中,位操作(Bit Operation)就是極具魅力的一種。今天,吳師兄就來分享 LeetCode 上幾道跟 Bit Operation 有關的題目。

題目一: 位 1 的個數

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 。

程式碼如下:

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int cnt = 0;
        while(n > 0){
            cnt++;
            n = n & (n - 1);
        }
        return cnt;
    }
};
複製程式碼

題目二:2 的冪

LeetCode上第 231 號問題:給定一個整數,編寫一個函式來判斷它是否是 2 的冪次方。

首先,先來分析一下 2 的次方數的二進位制寫法:

表格

仔細觀察,可以看出 2 的次方數都只有一個 1 ,剩下的都是 0 。根據這個特點,只需要每次判斷最低位是否為 1 ,然後向右移位,最後統計 1 的個數即可判斷是否是 2 的次方數。

程式碼很簡單:

class Solution {
public:
    bool isPowerOfTwo(int n) {
        int cnt = 0;
        while (n > 0) {
            cnt += (n & 1);
            n >>= 1;
        }
        return cnt == 1;
    } 
};
複製程式碼

該題還有一種巧妙的解法。再觀察上面的表格,如果一個數是 2 的次方數的話,那麼它的二進數必然是最高位為1,其它都為 0 ,那麼如果此時我們減 1 的話,則最高位會降一位,其餘為 0 的位現在都為變為 1,那麼我們把兩數相與,就會得到 0。

比如 2 的 3 次方為 8,二進位制位 1000 ,那麼 8 - 1 = 7,其中 7 的二進位制位 0111。

圖 2

利用這個性質,只需一行程式碼就可以搞定。

class Solution {
public:
    bool isPowerOfTwo(int n) {
        return (n > 0) && (!(n & (n - 1)));
    } 
};
複製程式碼

題目三:數字範圍按位與

LeetCode上第 201 號問題:給定範圍 [m, n],其中 0 <= m <= n <= 2147483647,返回此範圍內所有數字的按位與(包含 m, n 兩端點)。

示例 :

輸入: [26,30]
輸出: 24
複製程式碼

首先,將 [ 26 , 30 ] 的範圍數字用二進位制表示出來:

11010  11011  11100  11101  11110

而輸出 24 的二進位制是 11000 。

可以發現,只要找到二進位制的 左邊公共部分 即可。

所以,可以先建立一個 32 位都是 1 的 mask,然後每次向左移一位,比較 m 和 n 是否相同,不同再繼續左移一位,直至相同,然後把 m 和 mask 相與就是最終結果。

class Solution {
public:
    int rangeBitwiseAnd(int m, int n) {
        int d = INT_MAX;
        while ((m & d) != (n & d)) {
            d <<= 1;
        }
        return m & d;
    }
};
複製程式碼

題目四:重複的 DNA 序列

LeetCode上第 187 號問題:所有 DNA 由一系列縮寫為 A,C,G 和 T 的核苷酸組成,例如:“ACGAATTCCG”。在研究 DNA 時,識別 DNA 中的重複序列有時會對研究非常有幫助。

編寫一個函式來查詢 DNA 分子中所有出現超過一次的 10 個字母長的序列(子串)。

示例:

輸入: s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"

輸出: ["AAAAACCCCC", "CCCCCAAAAA"]
複製程式碼

首先,依舊先將 A , C , G , T 的 ASCII 碼用二進位制來表示:

A: 0100 0001  C: 0100 0011  G: 0100 0111  T: 0101 0100

通過觀察發現每個字元的後三位都不相同,因此可以用末尾的三位來區分這四個字元。

題目要求是查詢 10 個字母長的序列,這裡我們將每個字元用三位來區分的話,10 個字元就需要 30 位 ,在32位機上也 OK 。

為了提取出後 30 位,需要使用 mask ,取值為 0x7ffffff(二進位制表示含有 27 個 1) ,先用此 mask 可取出整個序列的後 27 位,然後再向左平移三位可取出 10 個字母長的序列 ( 30 位)。

為了儲存子串的頻率,這裡使用雜湊表

首先當取出第十個字元時,將其存在雜湊表裡,和該字串出現頻率對映,之後每向左移三位替換一個字元,查詢新字串在雜湊表裡出現次數,如果之前剛好出現過一次,則將當前字串存入返回值的陣列並將其出現次數加一,如果從未出現過,則將其對映到 1。

舉個?:

根據題意,第一個操作:首先取出前九個字元 AAAAACCCC ,根據上面的分析,用三位來表示一個字元,所以這九個字元可以用二進位制表示為 001001001001001011011011011,

第二個操作:開始遍歷字串,下一個進來的是 C ,則當前字元為 AAAAACCCCC ,二進位制表示為001001001001001011011011011011,然後將其存入雜湊表中。然後再讀入下一個字元 A,則此時字串為AAAACCCCCA,依舊使用二進位制進行表示。

以此類推,當某個序列之前已經出現過了,只需要將其存入結果 res 中即可,參見程式碼如下:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> res;
        if (s.size() <= 10) return res;
        int mask = 0x7ffffff, cur = 0;
        unordered_map<int, int> m;
        for (int i = 0; i < 9; ++i) {
            cur = (cur << 3) | (s[i] & 7);
        }
        for (int i = 9; i < s.size(); ++i) {
            cur = ((cur & mask) << 3) | (s[i] & 7);
            if (m.count(cur)) {
                if (m[cur] == 1) res.push_back(s.substr(i - 9, 10));
                ++m[cur]; 
            } else {
                m[cur] = 1;
            }
        }
        return res;
    }
};
複製程式碼

如果你看過我前文的 演算法科普:有趣的霍夫曼編碼,肯定會思考能不能用更簡單的字元進行表示。

答案是可以的!

上面的方法都是用三位來表示一個字元,由於這裡只有四個不同的字母,用兩位來表示一個字元也是可以滿足需要的。

00 表示 A ,01 表示 C ,10 表示G ,11 表示T ,這樣的話總共需要20位就可以表示十個字元流,其餘的思路跟上面的方法完全相同,只需要將 mask 修改為 0x3ffff (二進位制表示含有 18 個 1)即可。

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        unordered_set<string> res;
        unordered_set<int> st;
        unordered_map<int, int> m{{'A', 0}, {'C', 1}, {'G', 2}, {'T', 3}};
        int cur = 0;
        for (int i = 0; i < 9; ++i) cur = cur << 2 | m[s[i]];
        for (int i = 9; i < s.size(); ++i) {
            cur = ((cur & 0x3ffff) << 2) | (m[s[i]]);
            if (st.count(cur)) res.insert(s.substr(i - 9, 10));
            else st.insert(cur);
        }
        return vector<string>(res.begin(), res.end());
    }
};
複製程式碼

End

除了上面這四道跟 Bit Operation 有關的題目外,LeetCode 上的還有很多題目也和位操作有關,比如 格雷碼翻轉位兩數相除等等。

當然,之前寫過的那篇 一道讓你拍案叫絕的演算法題 也是 Bit Operation 的經典操作。

面試官,你再問我 Bit Operation 試試?

相關文章