演算法日記 - Big-O 表示法
作者 WaitZ
FundamentalsbeginnerBig-OO(1), O(log n), O(n), O(n log n), O(n^2)algorithmsbig-obeginnercomplexity-analysis
今日主題
主題: Big-O 表示法
難度: 初級
分類: 演算法基礎
概念學習
核心概念
Big-O 表示法是一種數學符號,用於描述演算法時間複雜度或空間複雜度的上界。它表示最壞情況,幫助我們理解當輸入規模增長時演算法如何擴展。
核心原則:Big-O 關注增長率,而非精確執行時間。
- 忽略常數:
O(2n)→O(n) - 忽略低階項:
O(n² + n)→O(n²) - 關注當 n 趨近無限大時的行為
常見時間複雜度(由快到慢)
O(1) - 常數時間
- 透過索引存取陣列元素
- 雜湊表查找
- 範例:
array[5]、map.get(key)
O(log n) - 對數時間
- 已排序陣列的二元搜尋
- 平衡樹的操作
- 範例:透過將搜尋空間減半來尋找元素
O(n) - 線性時間
- 遍歷陣列一次
- 線性搜尋
- 範例:
for (int i = 0; i < n; i++)
O(n log n) - 線性對數時間
- 高效排序演算法(合併排序、快速排序、堆積排序)
- 範例:對陣列進行排序
O(n²) - 平方時間
- 巢狀迴圈遍歷陣列
- 氣泡排序、選擇排序、插入排序
- 範例:比較每個元素與其他所有元素
O(2^n) - 指數時間
- 解決子問題的遞迴演算法
- 範例:樸素的費波那契遞迴解法
O(n!) - 階乘時間
- 產生所有排列組合
- 旅行推銷員問題的暴力解法
關鍵重點
- 去除常數:O(2n) = O(n)、O(1000) = O(1)
- 去除非主導項:O(n² + n) = O(n²)、O(n + log n) = O(n)
- 不同輸入使用不同變數:處理兩個陣列時使用 O(a + b) 而非 O(n)
- Big-O 描述最壞情況(除非另有說明,最佳情況為 Big-Ω,平均情況為 Big-Θ)
- 空間複雜度遵循相同規則但衡量記憶體使用量
實際比較
當 n = 1,000,000 時:
- O(1):1 次操作
- O(log n):約 20 次操作
- O(n):1,000,000 次操作
- O(n log n):約 20,000,000 次操作
- O(n²):1,000,000,000,000 次操作(太慢了!)
程式實作範例
O(1) - 常數時間
fun getFirstElement(array: Array<Int>): Int {
return array[0] // 無論陣列大小如何,總是花費相同時間
}
O(n) - 線性時間
fun findMax(array: Array<Int>): Int {
var max = array[0]
for (element in array) { // 迴圈 n 次
if (element > max) {
max = element
}
}
return max
}
O(n²) - 平方時間
fun printPairs(array: Array<Int>) {
for (i in array.indices) { // n 次
for (j in array.indices) { // 每個 i 執行 n 次
println("${array[i]}, ${array[j]}")
}
}
// 總計:n * n = n² 次操作
}
O(log n) - 對數時間
fun binarySearch(array: Array<Int>, target: Int): Int {
var left = 0
var right = array.size - 1
while (left <= right) {
val mid = left + (right - left) / 2
when {
array[mid] == target -> return mid
array[mid] < target -> left = mid + 1
else -> right = mid - 1
}
}
return -1
// 每次迭代將搜尋空間減半
}
重要細節
常見誤解
- O(n) 不一定總是優於 O(n²):對於小輸入,常數很重要
- 更好的複雜度 ≠ 實際更快:常數為 1000 的 O(n) 可能比常數為 1 的 O(n²) 在小 n 時慢
- 空間時間權衡:有時我們使用更多空間(記憶體)來達到更好的時間複雜度
何時需要最佳化
- 大型資料集(n > 10,000)
- 頻繁執行的程式碼
- 即時系統
- 對於小 n,可讀性 > 最佳化
分析技巧
- 識別迴圈及其迭代次數
- 檢查巢狀操作
- 考慮遞迴呼叫深度和分支
- 根據輸入大小計算操作次數
- 專注於 n 增大時的主導項
今日反思
心得:
- 選擇演算法時始終考慮輸入大小
- O(n) 迴圈內的 O(1) 操作會產生 O(n) 的總複雜度
- 巢狀迴圈通常導致 O(n²) 或更差 - 盡量避免
相關概念:
- 空間複雜度分析
- 最佳/平均/最壞情況分析(Big-Ω、Big-Θ)
- 攤銷時間複雜度
- 分治演算法的主定理