二分搜尋法有很多種不同的條件、例子,上面的範例,只是要求在一連串數列裡面回答有沒有找到,有的話在第幾個位置,但其實原理都很類似,一樣是用二分搜尋去排除最多的數字,但是在一些條件判斷上會有些微差異,如果條件沒寫好的話,很容易會變成無窮迴圈。
O(n²) 選擇排序(Selection Sort)
效能較差的演算法。像是選擇排序,大致上來說,包了兩層 for 迴圈的都是 n² 。
選擇排序只需要重複執行兩個步驟:
11.從數列中找出最小值
2.將最小值與數列最左邊的數交換,結束排序。回到 (1)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let input = [5, 4, 1, 3, 2]
function selectionSort(arr) {
let len = arr.length // 陣列長度有幾個,就要找幾輪
let minIndex
for (let i = 0 ; i < len ; i++) { // i 以前的元素都排序好了
let min = arr[i] // 預設第一個是最小的
minIndex = i
for (let j = i ; j < len ; j++) { // 找未排序的最小值
if (arr[j] < min) {
min = arr[j]
minIndex = j
}
}
[arr[minIndex], arr[i]] = [arr[i], arr[minIndex]] // 交換兩個數
}
return arr
}
selectionSort(input) // [1, 2, 3, 4, 5]
第一個回合要從 5 個數字中找到最小值,需要 5 個步驟。第二個回合則是從 4 個數字中找,需要 4 個步驟,如果總共要排序的數字有 n 個,則第一個回合需要 n 個步驟,第二回合需要 n-1 個,一直到最後一個回合需要 1 個步驟為止。
可以得知需要經過:
(n + (n - 1) + (n - 2) + … + 1)
= n * (n + 1) / 2
個步驟。
時間複雜度就是 O(n²),最好、最壞、平均都是一樣的,因為無論原本的陣列長怎樣,都要經過這麼多輪比較。 為什麼會是 O(n²) 後面會提到
O(2^n) 費波那契數列(Fibonacci numbers)
代表著執行步驟會是 2 的 n 次方。這樣的執行效率非常的慢,因此,這樣的時間複雜度是大部分工程師在設計演算法時想要避免的,最常見的例子是以遞迴計算費波那契數列。
規則:
1第 0 項 = 0
第 1 項 = 1
第 n 項 = 第 n-1 項 + 第 n-2 項
ex : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
2
3
4
// 遞迴不斷呼叫自己
let fib = n => n > 1 ? fib(n - 1) + fib(n - 2) : n
fib(2) // 1
fib(10) // 55
執行時間的計算方式
該如何測量執行時間隨輸入內容而變化的程度?最實際的方式是,編程並在電腦上運作,計算真正耗費的時間。不過就算是相同的演算法,也會因為使用的電腦不同,影響執行時間,這點會造成不便。
因此,使用「步驟次數」來表示執行時間。「1個步驟」是執行的基本單位,用「運作結束之前,共執行幾次基本單位」來測量執行時間。
用上面選擇排序的例子來看執行時間,選擇排序的演算法如下:
1.從數列中找出最小值
2.將最小值與數列最左邊的數交換,結束排序。回到 (1)
假設數列中有 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
1 個數,確認完 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
1 個數後,(1)「找出最小值」的過程即結束。將「確認 1 個數」的操作視為基本單位,耗費的時間是 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
3。,因此,(1) 是經過let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
4的時間後結束。
接著,將「交換 2 個數」也視為基本單位,設定為耗費 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
5 的時間。如此一來,(2) 的過程並不隨 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
1 變化,只進行 1 次數的交換,經過 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
5 的時間後結束。
反覆進行 (1) 和 (2) n 次,加上每回合要確認的數會減少 1 個,總計的執行時間如下:
(n Tc + Ts)+((n - 1) x Tc + Ts)+((n - 2) x Tc + Ts) + … + (2 Tc + Ts) + (1 * Tc + Ts)
= 1/2Tcn(n + 1) + Tsn
= 1/2Tcn² + (1/2Tc + Ts)n
上面已求出執行時間,接著來看如何稍微簡化結果。 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
3 和 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
5 是基本單位,與輸入無關。會隨輸入而變動的是數列長度,假設 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
1 極大的情況下,當 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
1 越大,上列數式中的 1
2
3
4
5
6
7
8
2 就會變得非常大,其他部分相對變小。因此,最容易受影響的是 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
1 ,所以可刪除其他部分,將式子變成:
1/2Tcn² + (1/2Tc + Ts)n = O(n²)
可以得知,選擇排序的執行時間,大致會依輸入的數列長度 let color = ['red', 'yellow', 'blue']
console.log(color[2]) // blue
1 的平方成比例變化,同樣地,比方說某個演算法的運作量分別如下:
5Tx * n³ + 20Tyn² + 3Tzn
此時,用 O(n³) 表示。
5n log n + 2Tyn
此時,用 O(n log n) 表示。
O 這個符號表示「忽略重要項目之外的部分」的意思。 O(n²) 是表示「執行時間在最糟的情況時,能控制在 n² 的常數倍以內」。透過這種表示方式,可以直覺地理解演算法的執行時間,舉例來說,當選擇排序的執行時間是 O(n²) ,快速排序的執行時間是 O(n log n)馬上就能知道快速排序的執行時間較短。此外,隨著輸入的 n 的大小,執行時間會有多大的變化也能一目瞭然。