# 排序 & 初探STL `第6週社課` 關於排序的部分內容可以參考新竹實驗中學的講義Ch13: https://hackmd.io/@CLKO/ry9twDVpW?type=view 其他關於STL沒教到的內容可以參考維基百科: https://zh.wikipedia.org/zh-tw/%E6%A0%87%E5%87%86%E6%A8%A1%E6%9D%BF%E5%BA%93 <!-- 顏色字體: --> <!-- <span style="color:#0000FF"> str <span> --> ## 目錄 * <a href="##STL介紹" style="color: black; ">STL介紹</a> * <a href="##vector" style="color: black; ">vector</a> * <a href="##pair" style="color: black; ">pair</a> * <a href="##Counting-Sort(補充)" style="color: black; ">Counting Sort</a> * <a href="##sort函數" style="color: black; ">sort函數</a> * <a href="##多欄位排序" style="color: black; ">多欄位排序</a> * <a href="##stable_sort" style="color: black; ">stable_sort</a> ## STL介紹 **STL**(標準樣板函式庫 Standard Template Library),是一個C++軟體庫,裡面包含了常用到的基本資料結構與演算法(函數),正確的使用可以有效**簡化程式設計**。 ### 資料結構? 資料結構就是儲存資料的容器,不同的資料結構會以不同的方法儲存這些資料, 只要使用正確的資料結構就可以提升程式的效率(**壓低複雜度**) ### 內容 STL主要包含以下內容:(引用自維基百科) * 容器(container):包含、放置資料的地方。 * 指位器(iterator):用來指定操作所涉及到的記憶體位置(範圍)。 * 演算法(algorithm):要執行的操作(Ex:排序演算法)。 * (其他不在此處介紹) 其實這些名詞對於競程都不是很重要,只要會用就好了。 以下的內容都只會教怎麼用,沒有講原理。 ## vector ### 功能 可以改變大小的陣列,它和傳統陣列最大的差異就是:使用時可以不用預先知道需要多少記憶體, 直接動態分配。 因為它最常用,所以放在第一個講 ### 宣告 ``` vector<type> v(size, init); ``` type : 內容物種類 v : vector名稱 size : vector初始大小,可以省略(預設為0) init : 每一項的初始值,可以省略(預設為0) 不可以省略size但保留init,因為不知道初始大小就不可能填入初始值 Ex: ```cpp vector<int> v1; // 空陣列 vector<int> v2(5); // 5個0 vector<int> v3(10,1); // 10個1 ``` 二維陣列就是把int改成vector&#60;int&#62;,依此類推。 ```cpp vector<vector<int>> v1; vector<vector<int>> v2(3, vector<int>(2,0)); // {{0,0}, {0,0}, {0,0}} ``` vector結合傳統陣列: ```cpp vector<int> v[1000]; ``` 這是一個二維陣列,由1000個vector組成 ### 使用 ```cpp // 取值,如同一般陣列(i必須小於v.size()) v[i] // 從後方加入元素 v.push_back(x); // 從後方刪除元素 v.pop_back(); // 手動調整大小 v.resize(sz); // 取得大小 v.size() // 清除全部 v.clear(); // 回傳是否為空 v.empty() ``` ### 迴圈遍歷 #### 先備知識 * v.begin() &nbsp;指向v的第一項的指標 * v.end() &nbsp;&nbsp;&nbsp; 指向v的最後一項的下一個位置的指標 * v.rbegin() 指向v的最後一項的反向指標 * v.rend() &nbsp;&nbsp; 指向v的第一項的前一個位置的反向指標 #### 正向(指標) STL ```cpp vector<int> v(5,1); for (auto m = v.begin(); m != v.end(); m++) { cout << *m << " "; } ``` 或是 ```cpp for_each(v.begin(), v.end(), func); ``` 傳統陣列 ```cpp for (int i = 0; i < n; ++i) { //do something with *(a + i) } ``` #### 反向(指標) STL ```cpp vector<int> v(5,1); for (auto m = v.rbegin(); m != v.rend(); m++) { cout << *m << " "; } ``` auto表示讓編譯器來判斷變數的型態,因為型態的名稱太長了 傳統陣列 ```cpp for (int i = n - 1; i >= 0; --i) { //do something with *(a + i) } ``` #### auto ```cpp vector<int> v(5,1); for (auto &i : v) { cout << i << " "; } ``` ## pair ### 功能 把兩個不同的變數型別綁在一起,廣泛使用在不同實作中 ### 宣告 ``` pair<type1,type2> p; ``` 如果今天要把變數塞進去的話,有兩種方法 第一種是簡潔的 ```cpp pair<int, char> p = {10, 'c'}; ``` 第二種是在第一種被編譯器判定為語意太模糊情況下使用 ```cpp pair<int, char> p = make_pair(10, 'c'); ``` ### 取值 ```cpp p.first p.second ``` ### 省略 ```cpp #include <bits/stdc++.h> using namespace std; #define ff first #define ss second int main{ pair<int, int> p; cin >> p.ff >> p.ss; } ``` ## Counting Sort(補充) ### 功能 計數排序,一種$O(n)$的排序法,只有在題目的數字範圍不大時用了才比較好 例如今天想排序 ``` 1 4 2 7 10 8 7 7 1 9 1 1 10 ``` 那麼 紀錄每個數字出現次數(假設數字最大到12) ``` 1 2 3 4 5 6 7 8 9 10 11 12 4 1 0 1 0 0 3 1 1 2 0 0 ``` 再輸出到一個陣列就結束了,或whatever你要怎麼做 ``` 1 1 1 1 2 4 7 7 7 8 9 10 10 ``` ### 範例程式碼 ```cpp #include <bits/stdc++.h> using namespace std; const int MAX_N = 1000; vector<int> chart(MAX_N); int main() { int i, n, num; cin >> n; for (i = 0; i < n; ++i) { cin >> num; ++chart[num]; } for (i = 0; i < MAX_N; ++i) { while (chart[i]--) { cout << i << ' '; } } } ``` ## sort函數 ### 語法 ``` sort(iterator_begin, iterator_end, compare_function) compare_function default = less<type> ``` ### 時間 c++ 內建 $\Theta(n \log n)$ 的排序函數 ### 實際 不用你手刻merge sort ```cpp vector<int> a = {1, 2, 4, 1, 4, 2, 3, 1}; sort(a.begin(), a.end()); ``` ## 排序傳統陣列 ```cpp sort(a+0, a+n); ``` 排序$a_0$到$a_{n-1}$ ## 多欄位排序 less&#60;int&#62;(),greater&#60;long long&#62;()不夠用了嗎? 沒關係,自訂義compare函數搞定一切 第一種作法:在定義該資料型別時直接宣告入其中 ```cpp struct point { int x, y; bool operator< (const point other) const { if (x == other.x) return y < other.y; return x < other.x; } }; vector<point> v; for(int i = 0; i < n; i++){ cin >> v[i].x >> v[i].y; } sort(v.begin(), v.end()); ``` 第二種作法:直接寫好cmp函數放到sort裡面用 ```cpp struct point { int x, y, z; }; bool cmp(const point A, const point B) { if (A.z == B.z) { if (A.y == B.y) return A.x < B.x; return A.y < B.y; } return A.z < B.z; } vector<point> v; for(int i = 0; i < n; i++){ cin >> v[i].x >> v[i].y >> v[i].z; } sort(v.begin(), v.end()); ``` 如果你想要first正著排,second倒著排: ```cpp #include <bits/stdc++.h> #define pii pair<int,int> #define ff first #define ss second using namespace std; bool cmp(pii a, pii b){ if(a.ff == b.ff){ return a.ss > b.ss; } return a.ff < b.ff; } int main(){ int n; cin >> n; vector<pii> v(n); for(int i = 0; i < n; i++){ cin >> v[i].ff >> v[i].ss; } sort(v.begin(), v.end(), cmp); // cmp後面不用加括號 for(int i = 0; i < n; i++){ cout << v[i].ff << " " << v[i].ss << "\n"; } return 0; } ``` 輸入: ``` 7 1 1 1 2 1 3 1 4 1 5 2 1 2 2 ``` 輸出: ``` 1 5 1 4 1 3 1 2 1 1 2 2 2 1 ``` ### 範例:CSES Movie Festival 現場實作 ## stable_sort ### 語法 ``` stable_sort(iterator_begin, iterator_end, compare_function) compare_function default = less<type> ``` ### 時間 支持"保序",就是說相對位置不會變 對於無須交換的兩項,它保證排序後這兩項不會被交換 Ex:對pair排序,但只排序第一項。 那麼在排序{0,1}和{0,2}時,stable_sort保證這兩項會維持其原本的順序, 但一般的sort函數不保證,也就是說這兩項有可能在排序的過程中被交換 c++ 內建 ,$O(n \log n)$,$\Theta(n\log n)$ 的排序函數 對於給定額外空間$O(n)$的情況下有$O(n\log n)$ 原地排序變成$O(n\log^2n)$ ($O$:最壞時間複雜度,$\Theta$:平均時間複雜度) ### 實際 不用你手刻merge sort ```cpp vector<int> a = {1, 2, 4, 1, 4, 2, 3, 1}; stable_sort(a.begin(), a.end()); ```