力扣高頻|算法面試題彙總(十一):數學&位運算

力扣高頻|算法面試題彙總(一):開始之前
力扣高頻|算法面試題彙總(二):字符串
力扣高頻|算法面試題彙總(三):數組
力扣高頻|算法面試題彙總(四):堆、棧與隊列
力扣高頻|算法面試題彙總(五):鏈表
力扣高頻|算法面試題彙總(六):哈希與映射
力扣高頻|算法面試題彙總(七):樹
力扣高頻|算法面試題彙總(八):排序與檢索
力扣高頻|算法面試題彙總(九):動態規劃
力扣高頻|算法面試題彙總(十):圖論
力扣高頻|算法面試題彙總(十一):數學&位運算

力扣高頻|算法面試題彙總(十一):數學&位運算

力扣鏈接
目錄:

  • 1.只出現一次的數字
  • 2.直線上最多的點數
  • 3.分數到小數
  • 4.階乘後的零
  • 5.顛倒二進制位
  • 6.位1的個數
  • 7.計數質數
  • 8.缺失數字
  • 9.3的冪

1.只出現一次的數字

給定一個非空整數數組,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素。
說明:
你的算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎?
示例 1:
輸入: [2,2,1]
輸出: 1
示例 2:
輸入: [4,1,2,1,2]
輸出: 4

思路
構建一個哈希表統計每個數字出現的次數,統計完後遍歷哈希表,獲得只出現一次的數字。
該方法時間複雜度:O(n)O(n),空間複雜度:O(n)O(n),需要一個額外的哈希表存儲每個數字出現的次數。

思路2:
使用異或操作
一個數字異或其本身等於0。由於本題除了一個數字之外,其餘數字均出現了兩次,所以挨個異或,最後的那個數字就是隻出現一次的數字。
時間複雜度:O(n)O(n), 空間複雜度:O(1)O(1)
C++

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = nums[0];
        for(int i = 1; i < nums.size(); ++i){
            res ^= nums[i];
        }
        return res;
    }
};

Python

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        res = nums[0]
        for i in range(1, len(nums)):
            res ^= nums[i]
        return res

2.直線上最多的點數

給定一個二維平面,平面上有 n 個點,求最多有多少個點在同一條直線上。
示例 1:
輸入: [[1,1],[2,2],[3,3]]
輸出: 3
解釋:
^
|
| o
| o
| o
±------------>
0 1 2 3 4
示例 2:
輸入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
輸出: 4
解釋:
^
|
| o
| o o
| o
| o o
±------------------>
0 1 2 3 4 5 6

思路
參考思路:暴力法
兩點確定一條直線,直線方程可以表示成下邊的樣子:y2y1x2x1=yy2xx2\frac{y 2-y 1}{x 2-x 1}=\frac{y-y 2}{x-x 2}
所以當來了一個點 (x,y)(x,y) 的時候,理論上,我們只需要代入到上邊的方程進行判斷即可。
第一個想法是,等式兩邊分子乘分母,轉換爲乘法的形式:(y2y1)(xx2)=(yy2)(x2x1)\left(y_{2}-y_{1}\right) *\left(x-x_{2}\right)=\left(y-y_{2}\right) *\left(x_{2}-x_{1}\right)
如果用int存可能會溢出,所以需要long
此外,還有一個方案:y2y1x2x1=yy2xx2\frac{y 2-y 1}{x 2-x 1}=\frac{y-y 2}{x-x 2}
還可以理解成判斷兩個分數相等,回到數學上,我們只需要將兩個分數約分到最簡,然後分別判斷分子和分母是否相等即可
所以,需要求分子和分母的最大公約數,直接用輾轉相除法即可

int gcd(int a, int b) {
    while (b != 0) {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

然後 test 函數就可以寫成下邊的樣子。需要注意的是,求了y - y2x - x2 最大公約數,所以要保證他倆都不是 00 ,防止除零錯誤。

bool test(int x1, int y1, int x2, int y2, int x, int y) {
    int g1 = gcd(y2 - y1, x2 - x1);// 求出最大公約數
    if(y == y2 && x == x2){ // 避免分母爲0的情況
        return true;
    }
    int g2 = gcd(y - y2, x - x2);// 求出最大公約數
    // 判斷是否在一個直線上
    return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2;
}

需要注意的是,因爲我們兩點組成一條直線,必須保證這兩個點不重合。所以我們進入第三層循環之前,如果兩個點相等就可以直接跳過:

if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) {
    continue;
}

此外,還需要考慮所有點都相等的情況,這樣就可以看做所有點都在一條直線上:

int i = 0;
for (; i < points.size() - 1; i++) {
    if (points[i][0] != points[i + 1][0] || points[i][1] != points[i + 1][1]) {
        break;
    }
}
if (i == points.size() - 1) {
    return points.size();
}

這個算法的時間複雜:O(n3)O(n^3),需要3此遍歷所有節點。沒有使用額外輔助數組,所以空間複雜度O(1)O(1)
C++

class Solution {
private:
    int gcd(int a, int b) {
        while (b != 0) {
            int temp = a % b;
            a = b;
            b = temp;
        }
        return a;
    }
    bool test(int x1, int y1, int x2, int y2, int x, int y) {
        int g1 = gcd(y2 - y1, x2 - x1);// 求出最大公約數
        if(y == y2 && x == x2){ // 避免點重合,導致分母爲0的情況
            return true;
        }
        int g2 = gcd(y - y2, x - x2);// 求出最大公約數
        // 判斷是否在一個直線上
        return (y2 - y1) / g1 == (y - y2) / g2 && (x2 - x1) / g1 == (x - x2) / g2;
    }
public:
    int maxPoints(vector<vector<int>>& points) {
        if(points.size() < 3) return points.size();// 點數小於3
        int i = 0;
        // 防止所有點重合
        for(;i< points.size() -1; ++i){
            if(points[i][0] != points[i+1][0] || points[i][1] != points[i+1][1]) break;
        }
        if(i == points.size() -1) return points.size();
        int max = 0;
        // 循環暴力查找
        for(int i = 0; i < points.size(); ++i){
            for(int j = i + 1; j < points.size(); ++j){
                if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) {
                    continue;// 跳過相等的點
                }
                int tempMax = 0;
                // 第三層循環
                for(int k = 0; k < points.size(); ++k){
                    if(k != i && k != j){
                        // 進行判斷
                        if(test(points[i][0], points[i][1],
                                points[j][0], points[j][1],
                                points[k][0], points[k][1]))
                            ++tempMax;
                    }
                }
                if(tempMax > max) max = tempMax;
            }
        }
        //加上直線本身的兩個點
        return max + 2;
    }
};

Python:

class Solution:
    def gcd(self, a, b):
        while b != 0:
            temp = a % b
            a = b
            b = temp
        return a
    def test(self, x1, y1, x2, y2, x, y):
        g1 = self.gcd(y2 - y1, x2 -x1)
        if y == y2 and x == x2: return True
        g2 = self.gcd(y - y2, x - x2)
        return (y2 - y1)/ g1 == (y - y2)/ g2 and \
                (x2 - x1)/ g1 == (x - x2)/ g2;
                
    def maxPoints(self, points):
        if len(points) < 3 : return len(points)
        count = 0
        for i in range(len(points)-1):
            count = i
            # print("i", i)
            if(points[i][0] != points[i+1][0] or
               points[i][1] != points[i+1][1]):
                break
        if count + 1 == len(points) - 1: return len(points)
        max = 0
        for i in range(len(points)):
            for j in range(i+1, len(points)):
                if points[i][0] == points[j][0] and \
                    points[i][1] == points[j][1]:continue
                        
                tempMax = 0
                for k in range(len(points)):
                    if k != i and k != j:
                        if self.test(points[i][0], points[i][1],
                                points[j][0], points[j][1],
                                points[k][0], points[k][1]):
                            tempMax += 1
                if tempMax > max:
                    max = tempMax
        return max + 2

思路2
參考思路:點斜式方程表示直線
直線方程的另一種表示方式,「點斜式」:yy0=k(xx0)y-y_0=k(x-x_0),換句話,一個點加一個斜率即可唯一的確定一條直線。
所以可以對「點」進行分類然後去求,問題轉換成,經過某個點的直線,哪條直線上的點最多。
在這裏插入圖片描述
當確定一個點後,平面上的其他點都和這個點可以求出一個斜率,斜率相同的點就意味着在同一條直線上。
所以可以用 HashMap 去計數,斜率作爲 key,然後遍歷平面上的其他點,相同的 key 意味着在同一條直線上。
上邊的思想解決了「經過某個點的直線,哪條直線上的點最多」的問題。接下來只需要換一個點,然後用同樣的方法考慮完所有的點即可。
當然還有一個問題就是斜率是小數,怎麼辦。
之前提到過了,用分數去表示,求分子分母的最大公約數,然後約分,最後將 「分子 + “@” + “分母”」作爲 key 即可。
最後還有一個細節就是,當確定某個點的時候,平面內如果有和這個重疊的點,如果按照正常的算法約分的話,會出現除 0 的情況,所以我們需要單獨用一個變量記錄重複點的個數,而重複點一定是過當前點的直線的。
複雜度分析
時間複雜度:O(n2)O(n^2),兩個for循環,比思路1減少一個數量級。
空間複雜度: O(n)O(n),每次遍歷時,需要O(n)O(n)大小的輔助空間來存儲當前點剩下點組合的斜率k,每次循環時都會重新初始化。
C++

class Solution {
private:
    int gcd(int a, int b) {
        while (b != 0) {
            int temp = a % b;
            a = b;
            b = temp;
        }
        return a;
    }
public:
    int maxPoints(vector<vector<int>>& points) {
        if(points.size() < 3) return points.size();// 點數小於3
        int res = 0;
        // 遍歷每個點
        for(int i = 0; i < points.size(); ++i){
            int duplicate = 0;
            int max_value =  0; // 保存經過當前點的直線中,最多的點
            unordered_map<string, int> hashMap;
            for(int j = i + 1; j < points.size(); ++j){
                // 求出分子分母
                int x = points[j][0] - points[i][0];
                int y = points[j][1] - points[i][1];
                if(x == 0 && y == 0){// 如果點重合,跳過
                    ++duplicate;
                    continue;
                }
                // 進行約分
                int gcd_value = gcd(x, y);
                x /= gcd_value;
                y /= gcd_value;
                int n=100;
                string str=to_string(n);
                // 構建key 來表示斜率
                string key = to_string(x)+"@"+to_string(y);
                // 該斜率下的直線個數加1
                ++hashMap[key];
                // 獲取較大值
                max_value = max(max_value, hashMap[key]);
            }
            //1 代表當前考慮的點,duplicate 代表和當前的點重複的點
            res = max(res, max_value + duplicate + 1);
        }
        return res;
    }
};

Python

class Solution:
    def gcd(self, a, b):
        while b != 0:
            temp = a % b
            a = b
            b = temp
        return a
              
    def maxPoints(self, points):
        if len(points) < 3 : return len(points)
        res = 0
        
        # 遍歷每個點
        for i in range(len(points)):
            duplicate = 0
            max_value = 0 
            hashMap = {} # 這個字典必須在這裏 防止重複計算
            for j in range(i+1, len(points)):
                x = points[j][0] - points[i][0]
                y = points[j][1] - points[i][1]
                if x == 0 and y == 0:
                    duplicate += 1
                    continue
                gcd_value = self.gcd(x, y)
                x /= gcd_value
                y /= gcd_value
                
                key = str(x) + "@" + str(y)
                if not key in hashMap:
                    hashMap[key] = 1
                else:
                    hashMap[key] += 1
                max_value = max(max_value, hashMap[key])
            res = max(res, max_value + duplicate + 1)
        return res
                

3.分數到小數

給定兩個整數,分別表示分數的分子 numerator 和分母 denominator,以字符串形式返回小數。
如果小數部分爲循環小數,則將循環的部分括在括號內。
示例 1:
輸入: numerator = 1, denominator = 2
輸出: “0.5”
示例 2:
輸入: numerator = 2, denominator = 1
輸出: “2”
示例 3:
輸入: numerator = 2, denominator = 3
輸出: “0.(6)”

思路:
參考官方解法:長除法
本題有諸多細節。
要點:

  • 不需要複雜的數學知識,只需要數學的基本知識。瞭解長除法的運算規則。
  • 使用長除法計算4/94/9,循環節很顯然就會找到。那麼計算4/3334/333卻不容易。
  • 注意邊界情況!

本題核心思想:當餘數出現循環的時候,對應的商也會循環
在這裏插入圖片描述
算法步驟:

  • 1.使用一個哈希表記錄餘數出現在小數部分的位置,當發現已經出現的餘數,就可以將重複出現的小數部分用括號括起來。
  • 2.過程中餘數可能爲 0,意味着不會出現循環小數,立刻停止程序。
  • 3.就像 兩數相除 問題一樣,要考慮負分數以及極端情況,比如說2147483648/1-2147483648/-1

一些測試樣例:
在這裏插入圖片描述
C++

class Solution {
public:
    string fractionToDecimal(int numerator, int denominator) {
        if(numerator == 0) return "0"; // 被除數是0
        string res = "";
        // 如果其中一個數字是負數
        // 異或對布爾進行運算
        if(numerator < 0 ^ denominator < 0) res += "-";
        // 轉換成long long 防止溢出
        long long dividend = static_cast<long long>(numerator);
        long long divisor  = static_cast<long long>(denominator);
        // 再取絕對值
        dividend = llabs(dividend);
        divisor = llabs(divisor);
        // 獲取商
        res += to_string(dividend/divisor);
        // 獲取餘數
        long long remainder  = dividend % divisor;
        if(remainder == 0) return res;
        // 餘數不爲0,說明有小數
        res += ".";
        int index = res.size() - 1; // 獲得小數點的下標
        // 構建哈希表 用來記錄出現重複數的下標,然後將'('插入到重複數前面就好了
        unordered_map<int, int> hashMap;
        // 長除法
        // 循環條件:餘數不爲0且餘數還沒有出現重複數字
        while(remainder != 0 && hashMap.count(remainder) == 0){
            ++index; // 位置加1
            hashMap[remainder] = index;
            //餘數擴大10倍,然後求商,和草稿本上運算方法是一樣的
            remainder *= 10;
            res += to_string(remainder/divisor);
            remainder %= divisor;
        }
        // 如果出現餘數,則在重複的數字前面加'(' ,末尾加')'
        if(hashMap.count(remainder)){
            res.insert(hashMap[remainder], "(");
            res += ")";
        }
        return res;
    }
};

Python

class Solution:
    def fractionToDecimal(self, numerator: int, denominator: int) -> str:
        if numerator == 0: return "0"
        res = ""
        if (numerator < 0) ^ (denominator < 0): res += "-"
        numerator = numerator if numerator >=0 else -numerator
        denominator = denominator if denominator >=0 else -denominator
        
        res += str(int(numerator/denominator))
        
        num = numerator % denominator
        if num == 0: return res
        
        res += "."
        hashMap = {}
        index = len(res) - 1
       
        while num != 0 and not num in hashMap:
            index += 1
            hashMap[num] = index
            num *= 10 
            res += str(int(num/denominator))
            num = num % denominator         
        if num in hashMap:
            res = res[:hashMap[num]] + "(" + res[hashMap[num]:] + ")"
        return res

4.階乘後的零

給定一個整數 n,返回 n! 結果尾數中零的數量。
示例 1:
輸入: 3
輸出: 0
解釋: 3! = 6, 尾數中沒有零。
示例 2:
輸入: 5
輸出: 1
解釋: 5! = 120, 尾數中有 1 個零.
說明: 你算法的時間複雜度應爲 O(log n) 。

思路:
最直觀的解法:計算n!=1234...nn!=1*2*3*4*...*n,每次計算它的末尾數 0 個數,如果末尾有0,就除以10,可以通過反覆檢查數字是否可以被 1010 整除來計算末尾 0 的個數
時間複雜度: 低於O(n)O(n),這個詳細的推導可看官方解析
空間複雜度:O(logn!)=O(nlogn)O(\log n!)=O(nlogn),爲了存儲 n!n!,我們需要 O(logn!)O(logn!) 位,而它等於 O(nlogn)O(nlogn)
官方例程如下:

def trailingZeroes(self, n: int) -> int:
        
    # Calculate n!
    n_factorial = 1
    for i in range(2, n + 1):
        n_factorial *= i
    
    # Count how many 0's are on the end.
    zero_count = 0
    while n_factorial % 10 == 0:
        zero_count += 1
        n_factorial //= 10
        
    return zero_count

思路2:
參考官方思路:計算因子 5
在一個階乘中,我們把所有 11nn 之間的數相乘,這和把所有 11nn 之間所有數字的因子相乘是一樣的。
例如,如果 n=16n=16,我們需要查看 111616 之間所有數字的因子。我們只對 22 和 55 有興趣。包含 55 因子的數字是 510155,10,15,包含因子 22 的數字是 2468101214162、4、6、8、10、12、14、16。因爲只三個完整的對,因此 16!16! 後有三個零。
這可以解決大部分情況,但是有的數字存在一個以上的因子。例如,若 i = 25,那麼我們只做了 fives += 1。但是我們應該 fives += 2,因爲 2525 有兩個因子 55
因此,我們需要計算每個數字中的因子55。我們可以使用一個循環而不是 if 語句,我們若有因子 55將數字除以 55。如果還有剩餘的因子 55,則將重複步驟(加一個while循環)。
這樣就得到了正確答案,但是仍然可以做一些改進。
首先,我們可以注意到因子 22 數總是比因子 55 大。爲什麼?因爲每四個數字算作額外的因子 22,但是隻有每 2525 個數字算作額外的因子 55。下圖可以清晰的看見:
在這裏插入圖片描述
因此可以刪除計算因子 2 的過程,留下計算因子5的過程。
可以做最後一個優化。在上面的算法中,我們分析了從 11nn 的每個數字。但是隻有 5,10,15,20,25,30,...5, 10, 15, 20, 25, 30,...等等,至少有一個因子 55。所以,不必一步一步的往上迭代,可以五步的往上迭代:因此可以修改爲:

fives = 0
for i from 5 to n inclusive in steps of 5:
    remaining_i = i
    while remaining_i is divisible by 5:
        fives += 1
        remaining_i = remaining_i / 5

tens = fives

時間複雜度:O(n)O(n),(仍然超時了)
空間複雜度:O(1)O(1),只是用了一個整數變量

官方例程

class Solution:
    def trailingZeroes(self, n: int) -> int:
        zero_count = 0
        for i in range(5, n + 1, 5):
            current = i
            while current % 5 == 0:
                zero_count += 1
                current //= 5

        return zero_count

思路3:
參考官方思路:高效的計算因子 5
思路2仍然太慢。爲了得出一個足夠快的算法,需要做進一步改進,這個改進能使在對數時間內計算出答案
思考之前簡化的算法(但不正確),它不正確是因爲對於有多個因子55 時會計算出錯,例如 2525
會發現這是執行 n5\frac{n}{5} 的低效方法。我們只對 55 的倍數感興趣,不是 55 的倍數可以忽略,因此可以簡化成:

fives = n / 5
tens = fives

關於解決多重因子的數字:所有包含兩個及以上的因子55 的數字都是 2525的倍數。所以我們可以簡單的除以 2525 來計算 2525 的倍數是多少。另外,在 n5\frac{n}{5} 已經計算了25一次,所以只需要額外因子n25\frac{n}{25} (而不是2n52*\frac{n}{5}),所以結合起來得到:

fives = n / 5 + n / 25
tens = fives

但是存在有三個因子55的情況,爲了得到最終的結果,需要所有的n5n25n125n625\frac{n}{5}、\frac{n}{25}、\frac{n}{125}、\frac{n}{625}等相加。得到:

fives=n5+n25+n125+n625+n3125+=\frac{n}{5}+\frac{n}{25}+\frac{n}{125}+\frac{n}{625}+\frac{n}{3125}+\cdots
這樣看起來會一直計算下去,但是並非如此!使用整數除法,最終,分母將大於nn,因此當項等於 0 時,就可以停止計算。
例如當n=12345n=12345時,得到:
fives=123455+1234525+12345125+12345625+123453125+1234516075+1234580375+=\frac{12345}{5}+\frac{12345}{25}+\frac{12345}{125}+\frac{12345}{625}+\frac{12345}{3125}+\frac{12345}{16075}+\frac{12345}{80375}+\dots
等於:
fives=2469+493+98+3+0+0+...=3082fives=2469+493+98+3+0+0+...=3082
在代碼中,可以通過循環 55 的冪來計算:

fives = 0
power_of_5 = 5
while n >= power_of_5:
    fives += n / power_of_5
    power_of_5 *= 5
tens = fives

C++

class Solution {
public:
    int trailingZeroes(int n) {
        int zero_count = 0;
        long long current_multiple  = 5;
        while(n >= current_multiple){
            zero_count += n/current_multiple;
            current_multiple *= 5;
        }
        return zero_count;
    }
};

Python:

class Solution:
    def trailingZeroes(self, n: int) -> int:
        zero_count = 0
        current_multiple = 5
        while n >= current_multiple:
            zero_count += n // current_multiple
            current_multiple *= 5
        return zero_count

編寫此算法的另一種方法是,不必每次嘗試55 的冪,而是每次將 nn 本身除以 55。這也是一樣的,因爲最終得到的序列是
fives=n5+(n5)5+((n)5)5+fives=\frac{n}{5}+\frac{\left(\frac{n}{5}\right)}{5}+\frac{\left(\frac{(n)}{5}\right)}{5}+\cdots
注意,在第二步中,有n55\frac{\frac{n}{5}}{5},這是因爲前一步nn本身除以55
如果熟悉分數規則,會發現n55\frac{\frac{n}{5}}{5}n55=n25\frac{n}{5*5}=\frac{n}{25}是一樣的,意味着序列與n5+n25+n125+\frac{n}{5}+\frac{n}{25}+\frac{n}{125}+\cdots。這種編寫算法的替代方法是等價的。
C++

class Solution {
public:
    int trailingZeroes(int n) {
        int zero_count = 0;
        while(n > 0){
            n /= 5;
            zero_count += n;
        }
        return zero_count;
    }
};

Python

class Solution:
    def trailingZeroes(self, n: int) -> int:
        zero_count = 0
        while n > 0:
            n //= 5
            zero_count += n
        return zero_count

5.顛倒二進制位

顛倒給定的 32 位無符號整數的二進制位。
示例 1:
輸入: 00000010100101000001111010011100
輸出: 00111001011110000010100101000000
解釋: 輸入的二進制串 00000010100101000001111010011100 表示無符號整數 43261596,
因此返回 964176192,其二進制表示形式爲 00111001011110000010100101000000。
示例 2:
輸入:11111111111111111111111111111101
輸出:10111111111111111111111111111111
解釋:輸入的二進制串 11111111111111111111111111111101 表示無符號整數 4294967293,
因此返回 3221225471 其二進制表示形式爲 10111111111111111111111111111111 。
進階:
如果多次調用這個函數,你將如何優化你的算法?

思路:
參考官方思路:逐位顛倒
在這裏插入圖片描述
在這裏插入圖片描述
關鍵思想是,對於位於索引 i 處的位,在反轉之後,其位置應爲 31-i(注:索引從零開始)

  • 從右到左遍歷輸入整數的位字符串(即 n=n>>1)。要檢索整數的最右邊的位,應用與運算(n&1)。
  • 對於每個位,我們將其反轉到正確的位置(即(n&1)<<power)。然後添加到最終結果。
  • n==0 時,終止迭代。

複雜度分析:
時間複雜度:O(log2N)O(log_2N),有一個循環來迭代輸入的最高非零位,即log2Nlog_2N
空間複雜度:O(1)O(1),因爲不管輸入什麼,內存的消耗是固定的。
C++

class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        int ret = 0;
        int power = 31;
        while(n){
            ret += (n & 1)<<power;
            n = n >> 1;
            power -= 1;
        }
        return ret;
    }
};

Python

class Solution:
    # @param n, an integer
    # @return an integer
    def reverseBits(self, n):
        ret, power = 0, 31
        while n:
            ret += (n & 1) << power
            n = n >> 1
            power -= 1
        return ret

思路2
參考官方思路大佬的解析
這種思想可以看作是一種分治的策略,通過掩碼將 32 位整數劃分成具有較少位的塊,然後通過將每個塊反轉,最後將每個塊的結果合併得到最終結果。
在下圖中,演示如何使用上述思想反轉兩個位。同樣的,這個想法可以應用到比特塊上。
在這裏插入圖片描述
算法步驟:

  • 首先,將原來的 32 位分爲 2 個 16 位的塊。
  • 然後將 16 位塊分成 2 個 8 位的塊。
  • 然後繼續將這些塊分成更小的塊,直到達到 1 位的塊。
  • 在上述每個步驟中,將中間結果合併爲一個整數,作爲下一步的輸入

大佬的解析:
既然知道 int 值一共32位,那麼可以採用分治思想,反轉左右16位,然後反轉每個16位中的左右8位,依次類推,最後反轉2位,反轉後合併即可,同時可以利用位運算在原地反轉。
通過debug查看:

  • 首先找一個數 (爲了看的清楚用_作分隔,可以忽略):十進制43261596; // 0000 ‭0010 1001 0100 _ 0001 1110 1001 1100‬
  • 左邊16位移到右邊,右邊16位移到左邊,然後使用|符號合併起來。 >>:帶符號右移。正數右移高位補0,負數右移高位補1。|:按位或邏輯,該位只要有一位爲1,結果就爲1,這裏用來合併。
  • 使用一些有規律的數,將16位,再分成左右8位進行反轉後合併,起始數字變爲‭0001 1110 1001 1100 _ 0000 0010 1001 0100‬
    0xff00ff00 表示16進制數1111 1111 0000 0000 _ 1111 1111 0000 0000
    0x00ff00ff 表示16進制數0000 0000 1111 1111 _ 0000 0000 1111 1111
  • 重複以上步驟,分組、合併,最後得到反轉後的結果。
  • 總結來說就是利用位運算進行反轉,同時存儲反轉後的數,繼續分治進行反轉,直到全部反轉完成,變化過程爲
// 原數字43261596
 0000 ‭0010 1001 0100 _ 0001 1110 1001 1100‬ 
// 反轉左右16位:
‭ 0001 1110 1001 1100 _ 0000 0010 1001 0100‬ 
// 繼續分爲8位一組反轉:
 1001 1100 0001 1110 _ 1001 0100 0000 0010
// 4位一組反轉:
 1100 1001 1110 0001 _ 0100 1001 0010 0000‬
// 2位一組反轉:
‭ 0011 1001 0111 1000 _ 0010 1001 0100 0000‬‬
// 這就是43261596反轉後的結果:‭964176192‬

C++

class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        n = (n >> 16) | (n << 16);
        n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8);
        n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4);
        n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2);
        n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1);
        return n;
    }
};

Python

class Solution:
    # @param n, an integer
    # @return an integer
    def reverseBits(self, n):
        n = (n >> 16) | (n << 16)
        n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8)
        n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4)
        n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2)
        n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1)
        return n

6.位1的個數

編寫一個函數,輸入是一個無符號整數,返回其二進制表達式中數字位數爲 ‘1’ 的個數(也被稱爲漢明重量)。
示例 1:
輸入:00000000000000000000000000001011
輸出:3
解釋:輸入的二進制串 00000000000000000000000000001011 中,共有三位爲 ‘1’。
示例 2:
輸入:00000000000000000000000010000000
輸出:1
解釋:輸入的二進制串 00000000000000000000000010000000 中,共有一位爲 ‘1’。
進階:
如果多次調用這個函數,你將如何優化你的算法?

思路:
循環位移:不斷對數字11進行與操作,如果爲1,則計數加1。每次與操作完之後,進行移位操作。
複雜度分析
時間複雜度:O(1)O(1)。運行時間依賴於數字 nn 的位數。由於這題中 nn 是一個 32 位數,所以運行時間是 O(1)O(1)的。
空間複雜度:O(1)O(1)。沒有使用額外空間。

C++

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0;
        while(n){
            if(n & 1) ++count;
            n >>= 1;
        }
        //cout<<"count:"<<count;
        return count;
    }
};

Python

class Solution:
    def hammingWeight(self, n: int) -> int:
        count = 0
        while n:
            if n & 1 : count += 1
            n >>= 1
        return count

思路2
參考官方思路:位操作的小技巧
不再檢查數字的每一個位,而是不斷把數字最後一個 ¥1$ 反轉,並把答案加一。當數字變成 00 的時候,就知道它沒有 11 的位了,此時返回答案。
這裏關鍵的想法是對於任意數字 nn ,將 nnn1n−1 做與運算,會把最後一個 11 的位變成 00 。爲什麼?考慮 nnn1n - 1的二進制表示。
在這裏插入圖片描述
在二進制表示中,數字 nn中最低位的 11 總是對應 n1n−1 中的 00 。因此,將 nnn1n−1 與運算總是能把 nn最低位的 11 變成 00 ,並保持其他位不變
複雜度分析
時間複雜度:O(1)O(1) 。運行時間與 nn 中位爲 11 的有關。在最壞情況下, nn 中所有位都是 11 。對於 32 位整數,運行時間是 O(1)O(1) 的。
空間複雜度:O(1)O(1) 。沒有使用額外空間。

C++

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0;
        while(n){
            ++count;
            n &= (n-1);
        }
        return count;
    }
};

Python

class Solution:
    def hammingWeight(self, n: int) -> int:
        count = 0
        while n:
            count += 1
            n &= (n-1)
        return count

7.計數質數

統計所有小於非負整數 n 的質數的數量。
示例:
輸入: 10
輸出: 4
解釋: 小於 10 的質數一共有 4 個, 它們是 2, 3, 5, 7 。

思路
暴力(最後一個超時)
第一個for循環遍歷所有數字,然後判斷該數字是否是質數。
時間複雜度:O(n(sqrt(n)))O(n(sqrt(n)))
C++

class Solution {
private:
    bool isPrimes(int num){
        for(int i =2; i <int(sqrt(num)) + 1; ++i){
            if(num % i == 0) return false; // 有額外的因數
        }
        return true;
    }
public:
    int countPrimes(int n) {
        int count = 0;
        for(int i = 2; i < n; ++i){
            if(isPrimes(i)) ++count;                      
        }
        return count;
    }
};

思路2
參考大佬解析: 高效計算isPrimes
首先從 2 開始,知道 2 是一個素數,那麼 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8… 都不可能是素數了。
然後發現 3 也是素數,那麼 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12… 也都不可能是素數了
也就是以素數爲因子的數字不可能是素數了。
參考圖示:

在這裏插入圖片描述
可以優化的地方:
回想剛纔判斷一個數是否是素數的 isPrime 函數,由於因子的對稱性,其中的 for 循環只需要遍歷 [2,sqrt(n)] 就夠了。這裏也是類似的,我們外層的 for 循環也只需要遍歷到 sqrt(n)
比如 n=25n = 25i=4i = 4 時算法會標記 4×2=84 × 2 = 84×3=124 × 3 = 12 等等數字,但是這兩個數字已經被 i=2i = 2i=3i = 32×42 × 43×43 × 4 標記了。
可以稍微優化一下,讓 ji 的平方開始遍歷,而不是從 2 * i 開始:

for (int i = 2; i * i < n; i++) 
    if (isPrim[i]) 
        ...

該算法的時間複雜度比較難算,顯然時間跟這兩個嵌套的 for 循環有關,其操作數應該是:
n/2+n/3+n/5+n/7+...=n×(1/2+1/3+1/5+1/7...)n/2 + n/3 + n/5 + n/7 + ... = n × (1/2 + 1/3 + 1/5 + 1/7...)
括號中是素數的倒數。其最終結果是 O(N * loglogN)

C++

class Solution {
public:
    int countPrimes(int n) {
        vector<bool> isPrimes(n, true); // 初始化
        // 判斷數字因子,因子小於sqrt(n)
        for(int i = 2; i * i < n; ++i){
            if(isPrimes[i]){// 如果是質數
                for(int j = i * i; j < n; j += i)
                    isPrimes[j] = false;
            }
        }
        int count = 0;
        for(int i = 2; i < n; ++i)
            if(isPrimes[i]) ++count;
        return count;
    }
};

Python

class Solution:
    def countPrimes(self, n: int) -> int:
        isPrimes = [True] * n
        for i in range(2, int(sqrt(n)) + 1):
            if isPrimes[i]:
                for j in range(i*i, n, i):
                    isPrimes[j] = False
        count = 0
        for i in range(2, n):
            if isPrimes[i]:
                count += 1
        return count

8.缺失數字

給定一個包含 0, 1, 2, …, n 中 n 個數的序列,找出 0 … n 中沒有出現在序列中的那個數。
示例 1:
輸入: [3,0,1]
輸出: 2
示例 2:
輸入: [9,6,4,2,3,5,7,0,1]
輸出: 8
說明:
你的算法應具有線性時間複雜度。你能否僅使用額外常數空間來實現?

思路:
第一個循環,使用一個哈希表來記錄每個數字是否出現,第二個循環,找到哈希表中沒有出現數字的id。
時間複雜度:O(n)O(n),空間複雜度O(n)O(n)
C++

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        vector<bool> flag(nums.size()+1);
        for(auto num : nums){
            flag[num] = true;
        }
        for(int i = 0; i < flag.size(); ++i){
            if(!flag[i]) return i;
        }
        return -1;
    }
};

Python

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        idHash = [False] * (len(nums) + 1)
        for num in nums:
            idHash[num] = True
        for i in range(len(idHash)):
            if not idHash[i]: return i
        return -1

思路2
等差數列
題目給的是找到0,1,2,...,n中缺失的那個數字,可以假設沒有缺失,那麼原數組就是可以排列成一個等差數列,等差數列求和:(0+n)(n+1)/2(0+n)*(n+1)/2之後,挨個減去數組中的每個數字,剩下的那個數字便是缺失的數字。
時間複雜度:O(n)O(n),一次循環即可。
空間複雜度O(1)O(1),只需要用一個變量計算等差數列的和。
C++

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int sum = (0 + nums.size()) * (nums.size() + 1) / 2;
        for(auto num : nums){
            sum -= num;
        }
        return sum;
    }
};

Python:

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        sum = (0 + len(nums))*(len(nums) + 1)//2
        for num in nums: 
            sum -= num
        return sum

思路3
參考官方解析:位運算
由於異或運算(XOR)滿足結合律,並且對一個數進行兩次完全相同的異或運算會得到原來的數,因此可以通過異或運算找到缺失的數字。

下標 0 1 2 3
數字 0 1 3 4

可以將結果的初始值設爲 nn,再對數組中的每一個數以及它的下標進行一個異或運算,即:
在這裏插入圖片描述
時間複雜度:O(n)O(n)
空間複雜度:O(1)O(1)
C++

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int length = nums.size();
        for(int i = 0; i < nums.size(); ++i)
            length ^= i^nums[i];
        return length;
    }
};

Python

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        length = len(nums)
        for i in range(len(nums)):
            length ^= i ^ nums[i]
        return length

9.3的冪

給定一個整數,寫一個函數來判斷它是否是 3 的冪次方。
示例 1:
輸入: 27
輸出: true
示例 2:
輸入: 0
輸出: false
示例 3:
輸入: 9
輸出: true
示例 4:
輸入: 45
輸出: false
進階:
你能不使用循環或者遞歸來完成本題嗎?

思路:
使用一個循環,不斷除3,當被除數小於3時或無法被3整除時停止。判斷被除數的大小,如果等於1,則是3的冪,否則不是。
複雜度分析

時間複雜度:O(logb(n))O(log_b(n)),在例子中是 O(logn)O(logn)。除數是用對數表示的。
空間複雜度:O(1)O(1),沒有使用額外的空間。

C++

class Solution {
public:
    bool isPowerOfThree(int n) {
        while(n >= 3 and n %3 == 0){
            n /= 3;
        }
        if(n == 1) return true;
        else return false;      
    }
};

Python

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        while n >=3 and n % 3 == 0:
            n = n//3
        return n == 1

思路2:
參考官方思路:整數限制
可以看出 n 的類型是 int。在C++中,int通常有四個字節。它的最大值爲 21474836472147483647
知道了 nn 的限制,我們現在可以推斷出 nn 的最大值,也就是 33 的冪,是 11622614671162261467。計算如下:

3log3MaxInt=319.56=319=11622614673^{⌊log 3MaxInt⌋}=3^{⌊19.56⌋}=3^{19}=1162261467
因爲 3 是質數,所以3193^{19}的除數只有30,31,...,3193^0,3^1,...,3^{19},因此我們只需要將 3193^{19} 除以 nn。若餘數爲 00 意味着 nn3193^{19}的除數,因此是 33 的冪。
複雜度分析
時間複雜度:O(1)O(1)。我們只做了一次操作。
空間複雜度: O(1)O(1),沒有使用額外空間。
C++

class Solution {
public:
    bool isPowerOfThree(int n) {
        return n > 0 && 1162261467 % n == 0;    
    }
};

Python:

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        return n > 0 and 1162261467 % n == 0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章