力扣高頻|算法面試題彙總(一):開始之前
力扣高頻|算法面試題彙總(二):字符串
力扣高頻|算法面試題彙總(三):數組
力扣高頻|算法面試題彙總(四):堆、棧與隊列
力扣高頻|算法面試題彙總(五):鏈表
力扣高頻|算法面試題彙總(六):哈希與映射
力扣高頻|算法面試題彙總(七):樹
力扣高頻|算法面試題彙總(八):排序與檢索
力扣高頻|算法面試題彙總(九):動態規劃
力扣高頻|算法面試題彙總(十):圖論
力扣高頻|算法面試題彙總(十一):數學&位運算
力扣高頻|算法面試題彙總(十一):數學&位運算
力扣鏈接
目錄:
- 1.只出現一次的數字
- 2.直線上最多的點數
- 3.分數到小數
- 4.階乘後的零
- 5.顛倒二進制位
- 6.位1的個數
- 7.計數質數
- 8.缺失數字
- 9.3的冪
1.只出現一次的數字
給定一個非空整數數組,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素。
說明:
你的算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎?
示例 1:
輸入: [2,2,1]
輸出: 1
示例 2:
輸入: [4,1,2,1,2]
輸出: 4
思路:
構建一個哈希表統計每個數字出現的次數,統計完後遍歷哈希表,獲得只出現一次的數字。
該方法時間複雜度:,空間複雜度:,需要一個額外的哈希表存儲每個數字出現的次數。
思路2:
使用異或操作。
一個數字異或其本身等於0。由於本題除了一個數字之外,其餘數字均出現了兩次,所以挨個異或,最後的那個數字就是隻出現一次的數字。
時間複雜度:, 空間複雜度:
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
思路:
參考思路:暴力法
兩點確定一條直線,直線方程可以表示成下邊的樣子:
所以當來了一個點 的時候,理論上,我們只需要代入到上邊的方程進行判斷即可。
第一個想法是,等式兩邊分子乘分母,轉換爲乘法的形式:
如果用int
存可能會溢出,所以需要long
。
此外,還有一個方案:
還可以理解成判斷兩個分數相等,回到數學上,我們只需要將兩個分數約分到最簡,然後分別判斷分子和分母是否相等即可。
所以,需要求分子和分母的最大公約數,直接用輾轉相除法即可。
int gcd(int a, int b) {
while (b != 0) {
int temp = a % b;
a = b;
b = temp;
}
return a;
}
然後 test
函數就可以寫成下邊的樣子。需要注意的是,求了y - y2
和 x - x2
最大公約數,所以要保證他倆都不是 ,防止除零錯誤。
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();
}
這個算法的時間複雜:,需要3此遍歷所有節點。沒有使用額外輔助數組,所以空間複雜度。
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:
參考思路:點斜式方程表示直線
直線方程的另一種表示方式,「點斜式」:,換句話,一個點加一個斜率即可唯一的確定一條直線。
所以可以對「點」進行分類然後去求,問題轉換成,經過某個點的直線,哪條直線上的點最多。
當確定一個點後,平面上的其他點都和這個點可以求出一個斜率,斜率相同的點就意味着在同一條直線上。
所以可以用 HashMap
去計數,斜率作爲 key
,然後遍歷平面上的其他點,相同的 key 意味着在同一條直線上。
上邊的思想解決了「經過某個點的直線,哪條直線上的點最多」的問題。接下來只需要換一個點,然後用同樣的方法考慮完所有的點即可。
當然還有一個問題就是斜率是小數,怎麼辦。
之前提到過了,用分數去表示,求分子分母的最大公約數,然後約分,最後將 「分子 + “@” + “分母”」作爲 key 即可。
最後還有一個細節就是,當確定某個點的時候,平面內如果有和這個重疊的點,如果按照正常的算法約分的話,會出現除 0 的情況,所以我們需要單獨用一個變量記錄重複點的個數,而重複點一定是過當前點的直線的。
複雜度分析:
時間複雜度:,兩個for循環,比思路1減少一個數量級。
空間複雜度: ,每次遍歷時,需要大小的輔助空間來存儲當前點與剩下點組合的斜率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)”
思路:
參考官方解法:長除法
本題有諸多細節。
要點:
- 不需要複雜的數學知識,只需要數學的基本知識。瞭解長除法的運算規則。
- 使用長除法計算,循環節很顯然就會找到。那麼計算卻不容易。
- 注意邊界情況!
本題核心思想:當餘數出現循環的時候,對應的商也會循環。
算法步驟:
- 1.使用一個哈希表記錄餘數出現在小數部分的位置,當發現已經出現的餘數,就可以將重複出現的小數部分用括號括起來。
- 2.過程中餘數可能爲 0,意味着不會出現循環小數,立刻停止程序。
- 3.就像 兩數相除 問題一樣,要考慮負分數以及極端情況,比如說
一些測試樣例:
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) 。
思路:
最直觀的解法:計算,每次計算它的末尾數 0 個數,如果末尾有0,就除以10,可以通過反覆檢查數字是否可以被 1010 整除來計算末尾 0 的個數。
時間複雜度: 低於,這個詳細的推導可看官方解析。
空間複雜度:,爲了存儲 ,我們需要 位,而它等於 。
官方例程如下:
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
在一個階乘中,我們把所有 和 之間的數相乘,這和把所有 和 之間所有數字的因子相乘是一樣的。
例如,如果 ,我們需要查看 到 之間所有數字的因子。我們只對 22 和 55 有興趣。包含 55 因子的數字是 ,包含因子 的數字是 。因爲只三個完整的對,因此 後有三個零。
這可以解決大部分情況,但是有的數字存在一個以上的因子。例如,若 i = 25
,那麼我們只做了 fives += 1
。但是我們應該 fives += 2
,因爲 有兩個因子 。
因此,我們需要計算每個數字中的因子。我們可以使用一個循環而不是 if
語句,我們若有因子 將數字除以 。如果還有剩餘的因子 ,則將重複步驟(加一個while
循環)。
這樣就得到了正確答案,但是仍然可以做一些改進。
首先,我們可以注意到因子 數總是比因子 大。爲什麼?因爲每四個數字算作額外的因子 ,但是隻有每 個數字算作額外的因子 。下圖可以清晰的看見:
因此可以刪除計算因子 2
的過程,留下計算因子5
的過程。
可以做最後一個優化。在上面的算法中,我們分析了從 到 的每個數字。但是隻有 等等,至少有一個因子 。所以,不必一步一步的往上迭代,可以五步的往上迭代:因此可以修改爲:
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
時間複雜度:,(仍然超時了)
空間複雜度:,只是用了一個整數變量
官方例程:
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仍然太慢。爲了得出一個足夠快的算法,需要做進一步改進,這個改進能使在對數時間內計算出答案
思考之前簡化的算法(但不正確),它不正確是因爲對於有多個因子 時會計算出錯,例如 。
會發現這是執行 的低效方法。我們只對 的倍數感興趣,不是 的倍數可以忽略,因此可以簡化成:
fives = n / 5
tens = fives
關於解決多重因子的數字:所有包含兩個及以上的因子 的數字都是 的倍數。所以我們可以簡單的除以 來計算 的倍數是多少。另外,在 已經計算了25一次,所以只需要額外因子 (而不是),所以結合起來得到:
fives = n / 5 + n / 25
tens = fives
但是存在有三個因子的情況,爲了得到最終的結果,需要所有的等相加。得到:
fives
這樣看起來會一直計算下去,但是並非如此!使用整數除法,最終,分母將大於,因此當項等於 0 時,就可以停止計算。
例如當時,得到:
fives
等於:
在代碼中,可以通過循環 的冪來計算:
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
編寫此算法的另一種方法是,不必每次嘗試 的冪,而是每次將 本身除以 。這也是一樣的,因爲最終得到的序列是
注意,在第二步中,有,這是因爲前一步本身除以。
如果熟悉分數規則,會發現和是一樣的,意味着序列與。這種編寫算法的替代方法是等價的。
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
時,終止迭代。
複雜度分析:
時間複雜度:,有一個循環來迭代輸入的最高非零位,即
空間複雜度:,因爲不管輸入什麼,內存的消耗是固定的。
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’。
進階:
如果多次調用這個函數,你將如何優化你的算法?
思路:
循環位移:不斷對數字進行與操作,如果爲1,則計數加1。每次與操作完之後,進行移位操作。
複雜度分析
時間複雜度:。運行時間依賴於數字 的位數。由於這題中 是一個 32 位數,所以運行時間是 的。
空間複雜度:。沒有使用額外空間。
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$ 反轉,並把答案加一。當數字變成 的時候,就知道它沒有 的位了,此時返回答案。
這裏關鍵的想法是對於任意數字 ,將 和 做與運算,會把最後一個 的位變成 。爲什麼?考慮 和 的二進制表示。
在二進制表示中,數字 中最低位的 總是對應 中的 。因此,將 和 與運算總是能把 中最低位的 變成 ,並保持其他位不變。
複雜度分析
時間複雜度: 。運行時間與 中位爲 的有關。在最壞情況下, 中所有位都是 。對於 32 位整數,運行時間是 的。
空間複雜度: 。沒有使用額外空間。
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循環遍歷所有數字,然後判斷該數字是否是質數。
時間複雜度:
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)
:
比如 , 時算法會標記 , 等等數字,但是這兩個數字已經被 和 的 和 標記了。
可以稍微優化一下,讓 j
從 i
的平方開始遍歷,而不是從 2 * i
開始:
for (int i = 2; i * i < n; i++)
if (isPrim[i])
...
該算法的時間複雜度比較難算,顯然時間跟這兩個嵌套的 for
循環有關,其操作數應該是:
括號中是素數的倒數。其最終結果是 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。
時間複雜度:,空間複雜度
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
中缺失的那個數字,可以假設沒有缺失,那麼原數組就是可以排列成一個等差數列,等差數列求和:之後,挨個減去數組中的每個數字,剩下的那個數字便是缺失的數字。
時間複雜度:,一次循環即可。
空間複雜度,只需要用一個變量計算等差數列的和。
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 |
可以將結果的初始值設爲 ,再對數組中的每一個數以及它的下標進行一個異或運算,即:
時間複雜度:
空間複雜度:
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的冪,否則不是。
複雜度分析
時間複雜度:,在例子中是 。除數是用對數表示的。
空間複雜度:,沒有使用額外的空間。
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
通常有四個字節。它的最大值爲 。
知道了 的限制,我們現在可以推斷出 的最大值,也就是 的冪,是 。計算如下:
因爲 3 是質數,所以的除數只有,因此我們只需要將 除以 。若餘數爲 意味着 是 的除數,因此是 的冪。
複雜度分析
時間複雜度:。我們只做了一次操作。
空間複雜度: ,沒有使用額外空間。
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