# 演算法和複雜度 Sprout 2020 --- ### 演算法的定義 「任何良定義的具體計算步驟的一個序列」-- 維基 --- ### 演算法的特徵 1. 具有零個或以上的輸入量 2. 一個以上的輸出量 3. 算法的描述不可有歧義 4. 必須在有限步驟內完成任務 5. 可行(能用既有的基本運算重複有限次實現) -- Donald Knuth --- ### 哪個演算法比較好? 當兩個**程式**對於所有可能的輸入都能輸出正確的結果時,通常我們會考慮兩個要素: * 哪一個程式花的空間(記憶體)較少? * 哪一個程式可以跑得比較快? ---- 有沒有可能程式 A 在某些情況下跑得比 B 快,但其他情況不是? --- ### 哪個程式跑得比較快? * 算法所需的基本運算步驟越少,程式就越快? ---- ![](https://i.stack.imgur.com/rbPmq.png) ---- 實際上,需要考慮不同的基本運算在處理器之間和之內的差異、計算機組織的排程行為、快取等...。 --- ### 複雜度 一樣的算法就連在同一種實作下,都有可能有不同的執行速度,討論不同算法的優劣就變得更困難。 ---- 因此衍生了複雜度的概念,例如: 如果我們說一個算法當 $N$ 為輸入大小時的複雜度是 $O(N)$,那麼我們可以找到一個常數 $c$ 和 $n$ 使得對於任意 $n \leq N$,$執行步數 \leq cN$。 ---- ### 複雜度(白話文) 一個算法的複雜度是 $O(N)$,則我們可以找到(決定)一個常數 $c$,並且當 $N$ 夠大時,執行基本運算的次數永遠不會超過 $cN$。 --- ### 用複雜度比較算法的好處 * 可以不用考慮環境差異(因為單位基本運算消耗的時間理論上與 $N$ 無關) * 可以用漸進的角度良好定義優劣 ---- 較差的算法在 $N$ 小時,可能有較小的執行時間。 ![](https://social.msdn.microsoft.com/Forums/getfile/329411) --- ### 複雜度分析 ```cpp // Selection Sort for (int i = 0; i < N - 1; ++i) { // N int min_index = i; for (int j = i + 1; j < N; ++j) { // N * N if (arr[j] < arr[min_index]) { min_index = j; } } std::swap(arr[i], arr[min_index]); } ``` -> $O(N^2)$ ---- ```cpp for (int i = 0; i < N; ++i) { // N int j = i; while (j < N && arr[i] == arr[j]) ++j; // ? i = j; } ``` -> $O(N)$ --- # 二分搜 ---- ### 單調遞增函數 ![](https://i.stack.imgur.com/9y426.png) 對於任意 $x_0 < x_1$,有 $f(x_0) \leq f(x_1)$。 ---- ### 問題 對於任意 $x$,你知道怎麼求 $f(x)$。 現在給你 $y$,請你求出 $x$ 使得 $f(x) = y$。 ---- [0, 3, 7, 11, 19, 27] find 11 ---- 好簡單??? ---- ![](https://i.imgur.com/6Afu1Bo.jpg) ---- ![](https://i.imgur.com/kQhUOf0.jpg =500x500) --- ### 例子 給你數列 $a = [0, 1, 2, 3, 6, 8, 9, 10]$,求 $2$ 在哪。 * 從左邊往右邊掃? -> 這次問你 $10$ 在哪 -> $O(N)$ 跑好跑滿 * 從右邊往左邊掃? -> ... * 從中間往外面掃? -> ... 怎麼做都是 $O(N)$ ? ---- ### 觀察 $a = [0, 1, 2, 3, 6, 8, 9, 10]$ * 數字是升序的 ---- $a = [?, ?, ?, [3], ?, ?, ?, ?]$ * 先問中間的數字 * 2 一定在 3 的左邊! ---- $a = [?, ?, ?, 3, X, X, X, X]$ ---- $a = [?, [1], ?, 3, X, X, X, X]$ * 再問中間的數字 * 2 一定在 1 的右邊! ---- $a = [X, 1, ?, 3, X, X, X, X]$ ---- $a = [X, 1, [2], 3, X, X, X, X]$ * 再問中間的數字... ---- ```cpp int find_index(int *a, int L, int R, int Q) { if (R < L) return -1; // 解空間是空集合 -> 無解 int M = (L + R) / 2; if (a[M] == Q) return M; // 找到了 if (a[M] < Q) return find_index(a, M+1, R, Q); // 在右邊 return find_index(a, L, M-1, Q); // 在左邊 } ``` 注意以上的算法是針對單調遞增; 如果題目是遞減,縮減方向會不一樣。 ---- ### 複雜度分析 ```cpp int find_index(int *a, int L, int R, int Q) { if (R < L) return -1; // O(1) int M = (L + R) / 2; // O(1) if (a[M] == Q) return M; // O(1) // 以下 T((R-L+1) / 2) if (a[M] < Q) return find_index(a, M+1, R, Q); // 在右邊 return find_index(a, L, M-1, Q); // 在左邊 } ``` 每次搜尋範圍都會減少一半,直到剩空集合 ---- ### 迴圈版 ```cpp int ans = -1; while (lb <= rb) { int mb = (lb + rb) / 2; if (arr[mb] == please_find) { ans = mb; break; } if (arr[mb] < please_find) { lb = mb + 1; } else { assert(please_find < arr[mb]); rb = mb - 1; } } ``` --- ### 練習 NEOJ#148 Guess Number --- ## 小結 * Big-O 複雜度分析利用漸進函數描述算法最差的運算次數 * 當搜尋問題具有單調性時可以用二分搜做到 $O(lgN)$ 的查詢次數
{"metaMigratedAt":"2023-06-15T07:52:24.692Z","metaMigratedFrom":"YAML","title":"csie-sprout-complexity-and-binsearch","breaks":true,"contributors":"[{\"id\":\"dec33987-0cd9-4214-9d3e-825262921019\",\"add\":4378,\"del\":1156}]"}
    289 views