theme: orange
文章目錄
- 什么是算法的時間復(fù)雜度 ?什么是算法的空間復(fù)雜度?
- 給定一個只包括'(',')','{','}','[',']'的字符串s,判斷字符串是否有效。
- 燒繩子計時,燒一根粗細(xì)不均勻的繩子,從頭燒到尾總共需要60秒.現(xiàn)有若干條材質(zhì)相同的繩子,問如何用燒繩的方法來計時15秒?
- 打家劫舍|,你是一個專業(yè)的小偷,計劃偷竊沿街的房屋,每間房內(nèi)都藏有一定的現(xiàn)金.影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統(tǒng)...
- 打家劫舍||,你是一個專業(yè)的小偷,計劃偷竊沿街的房屋,每間房內(nèi)都藏有一定的現(xiàn)金.這個地方所有的房屋都圍成一圈...
什么是算法的時間復(fù)雜度 ?什么是算法的空間復(fù)雜度?
一句話總結(jié)
這兩個概念是用來衡量算法 "好壞" 的指標(biāo),幫助我們判斷一個算法效率高不高、占不占內(nèi)存。時間復(fù)雜度(Time Complexity)簡單說:算法執(zhí)行時,花費的時間隨輸入數(shù)據(jù)量增長的趨勢。空間復(fù)雜度(Space Complexity)簡單說:算法執(zhí)行時,占用的內(nèi)存空間隨輸入數(shù)據(jù)量增長的趨勢。
核心概念
1. 時間復(fù)雜度(Time Complexity)
算法執(zhí)行時,花費的時間隨輸入數(shù)據(jù)量增長的趨勢。
- 用
O(...)表示,比如O(n)、O(1)、O(n2)。 - 這里的
n通常代表輸入數(shù)據(jù)的規(guī)模(比如字符串長度、數(shù)組元素個數(shù))。
舉幾個例子:
- O(1) :無論輸入數(shù)據(jù)多大,執(zhí)行時間都不變。比如 "從數(shù)組中取第 1 個元素",不管數(shù)組有 10 個還是 10 萬個元素,一步就能完成。
- O(n) :執(zhí)行時間隨數(shù)據(jù)量增長成正比。比如 "遍歷字符串的每個字符",字符串長度是 100,就要做 100 次操作;長度是 1000,就要做 1000 次操作(我們的括號匹配算法就是 O (n),因為每個字符只處理一次)。
- O(n2) :執(zhí)行時間隨數(shù)據(jù)量增長成平方關(guān)系。比如 "嵌套循環(huán)遍歷數(shù)組",如果數(shù)組長度是 100,就要做 100×100=10000 次操作,數(shù)據(jù)量增大時,時間會急劇增加。
時間復(fù)雜度越小,算法效率越高。
2. 空間復(fù)雜度(Space Complexity)
算法執(zhí)行時,占用的內(nèi)存空間隨輸入數(shù)據(jù)量增長的趨勢。
- 也用
O(...)表示,衡量的是額外消耗的空間(不算輸入數(shù)據(jù)本身占用的空間)。
舉幾個例子:
- O(1) :不管輸入數(shù)據(jù)多大,額外占用的空間不變。比如 "計算兩個數(shù)的和",只需要幾個變量存儲中間結(jié)果,和輸入規(guī)模無關(guān)。
-
O(n) :額外占用的空間隨數(shù)據(jù)量增長成正比。比如我們的括號匹配算法:最壞情況下(比如字符串全是左括號 "((((...)))"),棧會存儲所有左括號,棧的大小和字符串長度
n相等,所以空間復(fù)雜度是O(n)。 -
O(n2) :比如 "創(chuàng)建一個 n×n 的二維數(shù)組",數(shù)組占用的空間和
n2成正比。
空間復(fù)雜度越小,算法越節(jié)省內(nèi)存。
給定一個只包括'(',')','{','}','[',']'的字符串s,判斷字符串是否有效。
題目內(nèi)容
給定一個只包括'(',')','{','}','[',']'的字符串s,判斷字符串是否有效。
有效字符串需滿足:
- 左括號必須用相同類型的右括號閉合。
- 左括號必須以正確的順序閉合。
- 每個右括號都有一個對應(yīng)的相同類型的左括號。
解答
這是一個經(jīng)典的棧應(yīng)用問題,通過棧可以高效判斷括號是否匹配?;舅悸肥牵?/p>
- 遍歷字符串中的每個字符
- 遇到左括號時,將其入棧
- 遇到右括號時,檢查棧頂元素是否為對應(yīng)的左括號
- 如果匹配,彈出棧頂元素
- 如果不匹配或棧為空,則字符串無效
- 遍歷結(jié)束后,棧必須為空才表示所有括號都正確匹配
代碼演示:
bool isValid(String s) {
// 創(chuàng)建一個棧來存儲左括號
final stack = <String>[];
// 創(chuàng)建括號映射關(guān)系,右括號對應(yīng)左括號
final map = {
')': '(',
'}': '{',
']': '['
};
for (var char in s.split('')) {
// 如果是右括號
if (map.containsKey(char)) {
// stack.removeLast() 移除并返回棧頂元素(列表的最后一個元素)
// 棧為空或棧頂元素不匹配,返回false
if (stack.isEmpty || stack.removeLast() != map[char]) {
return false;
}
} else {
// 如果是左括號,入棧
stack.add(char);
}
}
// 棧為空表示所有括號都匹配
return stack.isEmpty;
}
void main() {
print(isValid("()")); // true
print(isValid("()[]{}")); // true
print(isValid("(]")); // false
print(isValid("([)]")); // false
print(isValid("{[]}")); // true
}
算法分析
- 時間復(fù)雜度:O (n),其中 n 是字符串的長度。我們只需要遍歷字符串一次。
- 空間復(fù)雜度:O (n),在最壞情況下(如全是左括號),棧需要存儲所有字符。
燒繩子計時,燒一根粗細(xì)不均勻的繩子,從頭燒到尾總共需要60秒.現(xiàn)有若干條材質(zhì)相同的繩子,問如何用燒繩的方法來計時15秒?
題目內(nèi)容:
燒繩子計時,燒一根粗細(xì)不均勻的繩子,從頭燒到尾總共需要60秒.現(xiàn)有若干條材質(zhì)相同的繩子,問如何用燒繩的方法來計時15秒?
解答:
這是一道經(jīng)典的邏輯思維題,核心在于利用繩子燃燒的特性和時間的疊加 / 分割來實現(xiàn)目標(biāo)計時。
解決步驟:
- 準(zhǔn)備兩根材質(zhì)相同的繩子,記為 A 和 B
-
同時點燃繩子 A 的兩端和繩子 B 的一端
- 繩子 A 兩端同時燃燒,由于總?cè)紵龝r間是 60 秒,兩端同時燃燒會讓它在 30 秒后燒完(60 秒的一半)
-
當(dāng)繩子 A 完全燒完時(此時剛好過去 30 秒),立即點燃繩子 B 的另一端
- 此時繩子 B 已經(jīng)燃燒了 30 秒,還剩下 30 秒的燃燒時間
- 從兩端同時燃燒剩下的部分,會讓剩余時間減半
-
從點燃繩子 B 另一端開始,到繩子 B 完全燒完的這段時間,就是 15 秒
- 因為剩下的 30 秒燃燒時間,從兩端同時燒就變成了 15 秒
代碼演示:
import 'dart:async';
// 繩子類,表示一根可燃燒的繩子
class Rope {
final int totalBurningTime; // 總?cè)紵龝r間(秒)
int _remainingTime; // 剩余燃燒時間
bool _isBurningFromStart; // 是否從起點燃燒
bool _isBurningFromEnd; // 是否從終點燃燒
Rope(this.totalBurningTime)
: _remainingTime = totalBurningTime,
_isBurningFromStart = false,
_isBurningFromEnd = false;
// 從起點開始燃燒
void burnFromStart() {
_isBurningFromStart = true;
}
// 從終點開始燃燒
void burnFromEnd() {
_isBurningFromEnd = true;
}
// 停止燃燒
void stopBurning() {
_isBurningFromStart = false;
_isBurningFromEnd = false;
}
// 檢查是否正在燃燒
bool get isBurning => _isBurningFromStart || _isBurningFromEnd;
// 檢查是否已燒完
bool get isBurnedOut => _remainingTime <= 0;
// 模擬1秒的燃燒過程
void simulateOneSecond() {
if (isBurnedOut) return;
// 計算每秒燃燒的量
int burnAmount = 0;
if (_isBurningFromStart) burnAmount++;
if (_isBurningFromEnd) burnAmount++;
// 減少剩余燃燒時間
_remainingTime -= burnAmount;
if (_remainingTime < 0) _remainingTime = 0;
}
// 獲取剩余燃燒時間
int get remainingTime => _remainingTime;
}
// 模擬燒繩子計時15秒的過程
void simulate15SecondsTimer() {
print("開始模擬燒繩子計時15秒...\n");
// 創(chuàng)建兩根繩子,每根燃燒時間60秒
Rope ropeA = Rope(60);
Rope ropeB = Rope(60);
int elapsedTime = 0; // 已過去的時間
bool is15SecondStarted = false;
int fifteenSecondCount = 0;
// 每秒執(zhí)行一次的計時器
Timer.periodic(Duration(seconds: 1), (timer) {
// 第一階段:同時點燃A的兩端和B的一端
if (elapsedTime == 0) {
ropeA.burnFromStart();
ropeA.burnFromEnd();
ropeB.burnFromStart();
print("時間 $elapsedTime 秒: 點燃繩子A兩端和繩子B一端");
}
// 模擬燃燒1秒
ropeA.simulateOneSecond();
ropeB.simulateOneSecond();
elapsedTime++;
// 當(dāng)A燒完時(30秒),點燃B的另一端
if (ropeA.isBurnedOut && !is15SecondStarted) {
ropeB.burnFromEnd();
is15SecondStarted = true;
print("\n時間 $elapsedTime 秒: 繩子A已燒完,點燃繩子B另一端,開始計時15秒");
}
// 顯示15秒計時過程
if (is15SecondStarted && !ropeB.isBurnedOut) {
fifteenSecondCount++;
print("15秒計時中: $fifteenSecondCount 秒");
}
// 當(dāng)B燒完時(又過了15秒),計時完成
if (ropeB.isBurnedOut) {
print("\n時間 $elapsedTime 秒: 繩子B已燒完,15秒計時結(jié)束");
timer.cancel();
}
});
}
void main() {
simulate15SecondsTimer();
}
代碼解析:
這個模擬程序主要包含兩個部分:
-
Rope 類:模擬一根可燃燒的繩子
- 可以從兩端分別點燃或同時點燃
- 每秒鐘根據(jù)燃燒點數(shù)量(1 個或 2 個)減少相應(yīng)的剩余燃燒時間
- 提供檢查是否正在燃燒和是否已燒完的方法
-
模擬過程:
- 創(chuàng)建兩根 60 秒燃燒時間的繩子 A 和 B
- 0 秒時:點燃 A 的兩端和 B 的一端
- 當(dāng) A 燒完(30 秒時):點燃 B 的另一端,開始 15 秒計時
- 當(dāng) B 燒完(又過 15 秒,總 45 秒時):計時結(jié)束,完成 15 秒的測量
算法分析:
-
時間復(fù)雜度:O(1)
- 無論輸入如何(繩子燃燒時間固定為 60 秒),整個模擬過程都是固定的 45 秒
- 算法的執(zhí)行步驟和時間不受輸入規(guī)模影響,因此是常數(shù)時間復(fù)雜度
-
空間復(fù)雜度:O(1)
- 只需要固定數(shù)量的變量(兩根繩子和幾個計時變量)
- 空間使用量不隨問題規(guī)模變化,因此是常數(shù)空間復(fù)雜度
-
核心思路:
- 利用 "從兩端燃燒一根繩子會使燃燒時間減半" 的特性
- 通過兩次減半操作(60→30→15)實現(xiàn) 15 秒計時
- 這種方法不需要精確切割繩子,只需要控制點燃和熄滅的時機
打家劫舍|,你是一個專業(yè)的小偷,計劃偷竊沿街的房屋,每間房內(nèi)都藏有一定的現(xiàn)金.影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統(tǒng)...
題目內(nèi)容:
你是一個專業(yè)的小偷,計劃偷竊沿街的房屋。每間房內(nèi)都藏有一定的現(xiàn)金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統(tǒng),如果兩間相鄰的房屋在同一晚上被小偷闖入,系統(tǒng)會自動報警。
給定一個代表每個房屋存放金額的非負(fù)整數(shù)數(shù)組,計算你 不觸動警報裝置的情況下 ,一夜之內(nèi)能夠偷竊到的最高金額。
示例 1:
輸入: [1,2,3,1]
輸出: 4
解釋: 偷竊 1 號房屋 (金額 = 1) ,然后偷竊 3 號房屋 (金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。
示例 2:
輸入: [2,7,9,3,1]
輸出: 12
解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)。
偷竊到的最高金額 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 1000 <= nums[i] <= 400
解答:
這是一道經(jīng)典的動態(tài)規(guī)劃問題,核心是在不觸發(fā)警報(不偷竊相鄰房屋)的情況下,計算能偷竊到的最高金額。
問題分析
- 核心約束:不能偷竊相鄰的房屋(否則觸發(fā)警報)
- 目標(biāo):找到一條不包含相鄰元素的子序列,使其元素之和最大
- 動態(tài)規(guī)劃思路:對于每個房屋,都有兩種選擇(偷或不偷),通過比較兩種選擇的收益來做決策
動態(tài)規(guī)劃解法
-
狀態(tài)定義:
設(shè)dp[i]表示偷竊前i間房屋(即索引0到i)能獲得的最大金額。 -
狀態(tài)轉(zhuǎn)移方程:
對于第i間房屋:- 如果偷第
i間房屋,則不能偷第i-1間,收益為dp[i-2] + nums[i] - 如果不偷第
i間房屋,則收益等于前i-1間的最大收益dp[i-1] - 因此:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
- 如果偷第
-
邊界條件:
- 只有 1 間房時:
dp[0] = nums[0] - 有 2 間房時:
dp[1] = max(nums[0], nums[1])
- 只有 1 間房時:
代碼演示:
int rob(List<int> nums) {
int n = nums.length;
if (n == 0) return 0;
if (n == 1) return nums[0];
// 優(yōu)化空間:用兩個變量代替dp數(shù)組
int prevPrev = nums[0]; // 表示dp[i-2]
int prev = max(nums[0], nums[1]); // 表示dp[i-1]
// 從第3間房開始計算(索引2)
for (int i = 2; i < n; i++) {
int current = max(prev, prevPrev + nums[i]);
prevPrev = prev;
prev = current;
}
return prev;
}
// 輔助函數(shù):取兩個數(shù)的最大值
int max(int a, int b) => a > b ? a : b;
void main() {
print(rob([1,2,3,1])); // 輸出:4(1+3)
print(rob([2,7,9,3,1])); // 輸出:12(2+9+1)
print(rob([5])); // 輸出:5(只有一間房)
print(rob([3, 1])); // 輸出:3(選擇金額較大的)
print(rob([2,1,1,2])); // 輸出:4(2+2)
}
算法分析:
-
時間復(fù)雜度:O(n)
只需遍歷一次數(shù)組,其中n是房屋數(shù)量,因此時間復(fù)雜度為線性級別。 -
空間復(fù)雜度:O(1)
優(yōu)化后僅使用了 3 個變量(prevPrev、prev、current)來存儲中間結(jié)果,無需額外數(shù)組,空間復(fù)雜度為常數(shù)級。 -
優(yōu)化思路:
觀察狀態(tài)轉(zhuǎn)移方程可知,計算dp[i]只需要dp[i-1]和dp[i-2]的值,因此可以用兩個變量替代整個dp數(shù)組,將空間復(fù)雜度從 O (n) 優(yōu)化到 O (1)。
打家劫舍||,你是一個專業(yè)的小偷,計劃偷竊沿街的房屋,每間房內(nèi)都藏有一定的現(xiàn)金.這個地方所有的房屋都圍成一圈...
題目內(nèi)容:
你是一個專業(yè)的小偷,計劃偷竊沿街的房屋,每間房內(nèi)都藏有一定的現(xiàn)金。這個地方所有的房屋都 圍成一圈 ,這意味著第一個房屋和最后一個房屋是緊挨著的。同時,相鄰的房屋裝有相互連通的防盜系統(tǒng),如果兩間相鄰的房屋在同一晚上被小偷闖入,系統(tǒng)會自動報警 。
給定一個代表每個房屋存放金額的非負(fù)整數(shù)數(shù)組,計算你 在不觸動警報裝置的情況下 ,今晚能夠偷竊到的最高金額。
示例 1:
輸入: nums = [2,3,2]
輸出: 3
解釋: 你不能先偷竊 1 號房屋(金額 = 2),然后偷竊 3 號房屋(金額 = 2), 因為他們是相鄰的。
示例 2:
輸入: nums = [1,2,3,1]
輸出: 4
解釋: 你可以先偷竊 1 號房屋(金額 = 1),然后偷竊 3 號房屋(金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。
示例 3:
輸入: nums = [1,2,3]
輸出: 3
提示:
1 <= nums.length <= 1000 <= nums[i] <= 1000
解答:
這道題是經(jīng)典動態(tài)規(guī)劃問題 "打家劫舍" 的變種,核心差異在于房屋圍成一圈,導(dǎo)致第一個和最后一個房屋不能同時偷竊。
問題分析
- 約束條件:房屋環(huán)形排列(首末相鄰),不能偷竊相鄰房屋,否則觸發(fā)警報
- 核心難點:首末房屋的互斥性(不能同時偷)
-
轉(zhuǎn)化思路:將環(huán)形問題拆解為兩個線性問題
- 不偷第一個房屋,考慮偷竊范圍為
[1, n-1](從第 2 個到最后一個) - 不偷最后一個房屋,考慮偷竊范圍為
[0, n-2](從第一個到倒數(shù)第二個)
- 不偷第一個房屋,考慮偷竊范圍為
- 最終結(jié)果為兩個范圍的最大偷竊金額的最大值
動態(tài)規(guī)劃解法
普通線性排列的打家劫舍問題可以用動態(tài)規(guī)劃解決:
- 狀態(tài)定義:
dp[i]表示前i個房屋能偷竊的最大金額 - 狀態(tài)轉(zhuǎn)移:對于第
i個房屋,有兩種選擇- 不偷:
dp[i] = dp[i-1] - 偷:
dp[i] = dp[i-2] + nums[i](因為不能偷第i-1個) - 綜上:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
- 不偷:
代碼演示:
int rob(List<int> nums) {
int n = nums.length;
// 邊界情況:只有一間房時直接返回該房屋金額
if (n == 1) return nums[0];
// 計算兩個范圍的最大金額并取最大值
return max(
_robLinear(nums, 0, n - 2), // 不偷最后一個房屋
_robLinear(nums, 1, n - 1) // 不偷第一個房屋
);
}
// 輔助函數(shù):計算線性排列的房屋(start到end)的最大偷竊金額
int _robLinear(List<int> nums, int start, int end) {
if (start > end) return 0;
if (start == end) return nums[start];
int prevPrev = nums[start]; // 前兩個位置的最大金額(i-2)
int prev = max(nums[start], nums[start + 1]); // 前一個位置的最大金額(i-1)
// 從start+2開始遍歷
for (int i = start + 2; i <= end; i++) {
int current = max(prev, prevPrev + nums[i]);
prevPrev = prev;
prev = current;
}
return prev;
}
// 輔助函數(shù):取兩個數(shù)的最大值
int max(int a, int b) => a > b ? a : b;
void main() {
print(rob([2,3,2])); // 輸出:3(正確,偷中間的3)
print(rob([1,2,3,1])); // 輸出:4(正確,1+3)
print(rob([1,2,3])); // 輸出:3(正確,偷最后一個3)
print(rob([5])); // 輸出:5(邊界情況)
print(rob([1,9,1,1,9]));// 輸出:10(9+1或1+9)
}
算法分析
-
時間復(fù)雜度:O(n)
- 兩個線性子問題各遍歷一次數(shù)組,總遍歷次數(shù)為 2n,屬于線性時間
-
空間復(fù)雜度:O(1)
- 未使用額外數(shù)組,僅用常數(shù)個變量保存中間狀態(tài),空間復(fù)雜度為常數(shù)級
-
優(yōu)化點:
- 用兩個變量替代動態(tài)規(guī)劃數(shù)組,將空間復(fù)雜度從 O (n) 優(yōu)化到 O (1)
- 通過問題拆解,將環(huán)形約束轉(zhuǎn)化為兩個線性問題,簡化了邏輯