--- tags: 進階班 --- # 排序和搜尋法 --- ## Binary Search (二分搜尋法) 試玩一次範例 `code` 應該就懂了 :poop: `int` 範圍的二分搜如下(左閉右開): 1. 先設 `l, r`,代表猜數字的範圍 2. 猜的數字比答案大,`r = guess` 3. 猜的數字比答案小,`l = guess + 1` (你必須回答電腦,電腦猜得太小輸入 1,太大輸入 -1,0 代表正確) `Time Complexity` : $O(lgN)$ :::spoiler `code` ```cpp= #include<bits/stdc++.h> #define LL long long #define f first #define s second using namespace std; int main() { int T; cin >> T; while (T--) { int ans; long long l = 0, r = 4294967296LL; bool flag = false; for (int x = 0; x < 33; x++) { long long m = (l + r) >> 1; cout << m << '\n'; cin >> ans; if (ans == 0){flag = true; break;} else if (ans == 1) l = m + 1; else if (ans == -1) r = m; } if(!flag) break; } return 0; } ``` ::: 好用函數介紹: `lower_bound(v.begin(), v.end(), n)` 找出 `vector` 中,第一個大於等於 `n` 的數字。 `upper_bound(v.begin(), v.end(), n)` 找出 `vector` 中,第一個大於 `n` 的數字。 ## Merge Sort (合併排序法) 假設今天要處理的數列是 |5|2|8|6|3|7|1|4| |-|-|-|-|-|-|-|-| 接下來進行 split (分割) 的動作: ```graphviz digraph main{ node[color="black" fontcolor="black" fontname="Arial"] edge[color="black" fontcolor="black"] fontname="" {"5, 2, 8, 6, 3, 7, 1, 4"}->{"5, 2, 8, 6","3, 7, 1, 4"} {"5, 2, 8, 6"}->{"5, 2","8, 6"} {"3, 7, 1, 4"}->{"3, 7","1, 4"} {"5, 2"}->{5,2}{"8, 6"}->{8,6}{"3, 7"}->{3,7}{"1, 4"}->{1,4} } ``` 這邊沒什麼好說的,就用遞迴把數列二分成兩組,直到每組分到剩 $1$ 個數字 接下來,開始 merge (合併) ~ 先上完整 merge 動作: ```graphviz digraph main{ node[color="black" fontcolor="black" fontname="Arial"] edge[color="black" fontcolor="black"] {5,2}->{"2, 5"}{8,6}->{"6, 8"}{3,7}->{"3, 7"}{1,4}->{"1, 4"} {"2, 5, 6, 8","1, 3, 4, 7"}->{"1, 2, 3, 4, 5, 6, 7, 8"} {"2, 5","6, 8"}->{"2, 5, 6, 8"} {"3, 7","1, 4"}->{"1, 3, 4, 7"} } ``` 可以發現,每合併一層,都要經過排序 (其實就是優化後的 `Selection Sort` ) `Time Complexity` : $O(NlgN)$ :::spoiler `code` ```cpp= #include<bits/stdc++.h> using namespace std; #define LL long long void merge_sort(vector<int>& v) { if (v.size() != 1) { //start to split size_t mid = int(v.size() / 2); vector<int> left(v.begin(), v.begin() + mid); vector<int> right(v.begin() + mid, v.end()); merge_sort(left), merge_sort(right); //start to merge size_t l = 0, r = 0, x = 0; while (l != left.size() && r != right.size()) { if (left[l] <= right[r]) v[x] = left[l], l++; else v[x] = right[r], r++; x++; } while (l != left.size()) v[x] = left[l], l++, x++; while (r != right.size()) v[x] = right[r], r++, x++; } else return; } int main() { cin.tie(nullptr); ios_base::sync_with_stdio(false); int n; cin >> n; vector<int> v(n); for (auto& i : v) cin >> i; merge_sort(v); for (auto& i : v) cout << i << ' '; return 0; } ``` ::: ## Quick Sort (快速排序法) 和 `Merge sort` 有點像, 只是把數列二分的標準是: 隨便找數列中的一個數字,比該數小就擺左邊,比該數大就擺右邊, 當 `left.size()` 和 `right.size()` $\leq 1$ 時,就排序結束了。 放個例子 (挑的基準數字都是當前數列第一項) 處理數列是: |5|2|8|6|3|7|1|4| |-|-|-|-|-|-|-|-| ```graphviz digraph main{ node[color="black" fontcolor="black" fontname="Arial"] edge[color="black" fontcolor="black"] fontname="" {"5, 2, 8, 6, 3, 7, 1, 4"}->{"2, 3, 1, 4","5","8, 6, 7"} {"2, 3, 1, 4"}->{"1","2","3, 4"} {"8, 6, 7"}->{"6, 7","8"} {"3, 4"}->{3,4}{"6, 7"}->{6,7} } ``` 在實作時,`Quick sort` 還有一個特點是:**它不需要多餘的記憶體來儲存左/右數列** 可以靠數學方法解決: 1. 將「比對基準數」放在數列第一位 (也可以直接選數列第一位當基準) 2. 記錄當前右陣列的第一數的位置 (初始值設為數列的第二位) 3. 每當出現應擺在左陣列的數,和右陣列的第一位交換位置並更新右陣列的第一數位置 4. 處理完整個數列後,將左數列最後一項和第一項位置交換 :::spoiler `code` ```cpp= void quick_sort(vector<LL>& v, int i, int j) { if (j - i >= 1) { swap(v[i], v[int(i + j) / 2]);//取數列中間項為基準 int left = i; for (int x = i + 1; x < j; x++) { if (v[x] <= v[i]) swap(v[left + 1], v[x]),left++; } swap(v[i], v[left]); quick_sort(v, i, left), quick_sort(v, left + 1, j); } else return; //其實這行可以不用打 } ``` ::: 但要注意,**`Quick sort` 的複雜度有時候會暴增** 看看以下範例: ```graphviz digraph main{ node[color="black" fontcolor="black" fontname="Arial"] edge[color="black" fontcolor="black"] fontname="" {"5, 2, 8, 6, 3, 7, 1, 4"}->{"1","5, 2, 8, 6, 3, 7, 4"} {"5, 2, 8, 6, 3, 7, 4"}->{"5, 2, 6, 3, 7, 4", "8"} {"5, 2, 6, 3, 7, 4"}->{"5, 2, 6, 3, 4", "7"} {"5, 2, 6, 3, 4"}->{"2", "5, 6, 3, 4"}{"5, 6, 3, 4"}->{"3","5, 6, 4"}{"5, 6, 4"}->{"4", "5, 6"}{"5, 6"}->{5, 6} } ``` 假設今天就是這麼衰,每次都挑到數列中最大或最小的數,那麼 `Quick sort` 就會變得非常慢。 `Time Complexity` : $O(NlgN)$ ~ $O(N^2)$ ## 小結 所以 `Merge sort` 雖然比正常情況下的 `Quick sort` 慢,但是**它很穩定** 所以解題時一般情況都建議使用 `Merge sort` 當然,如果可以使用內建 `sort` 優先用內建的! --- ## 補充:Radix Sort (基數排序法) 假設今天處理的數列是: |274|369|501|487|63|446|871|228| |-|-|-|-|-|-|-|-| 我們先對「個位數」做排序 $\Rightarrow$ |501|871|63|274|446|487|228|369| |-|-|-|-|-|-|-|-| 然後對「十位數」做排序 $\Rightarrow$ |501|228|446|63|369|871|274|487| |-|-|-|-|-|-|-|-| 接著是「百位數」 (沒有百位數的就補零) $\Rightarrow$ |63|228|274|369|446|487|501|871| |-|-|-|-|-|-|-|-| 因為本數列最大就到百位數,排序完成! 當然,你要從百位數做到個位也沒問題 ### 實作 可以建 `vector<vector<type>>`, 第一維度的大小是 「數字要分幾組」, 例如向範例以十進位分就是分十組,實作時通常是取 16 進位 而第二維度的長度就取決於每一組有多少數字啦~ :::spoiler `code` ```cpp= void radix_sort(vector<LL>& v) { vector<vector<LL>> bkt(16, vector<LL>()); LL k = 0; unsigned LL filter = 15; int n = 0; while (bkt[0].size() < v.size()) { n = 0; for (size_t x = 0; x < v.size(); x++) { bkt[(v[x] & filter) >> (k << 2)].push_back(v[x]); } for (int x = 0; x < 16; x++) { for (size_t y = 0; y < bkt[x].size(); y++) v[n] = bkt[x][y], n++; if (bkt[x].size() != v.size()) bkt[x].clear(); } filter <<= 4; k++; } } ``` ::: `Time Complexity`:$O(N\log_m N)$,$m =$ 分組組數