Basic Data Structure
&
STL[0]
algorithm[3] 22504 鹽亞倫
索引(可以點)
參考
reference
上週來不及講的語法
既然都說是參考了那就...
但他好爛他上週上課前30分鐘才被我才更正他的錯誤觀念
看這個比較好啦
複雜度
complexity
複雜度是啥麼?
能吃嗎?
不行吃,想吃東西來秋遊烤肉
複雜度 ===> 程式花費東西的程度
花費什麼勒? ===> 時間和空間
也就是說,一個程式的時間複雜度或空間複雜度越高
電腦的負擔就越大
然後就會吃TLE和MLE
所以在寫程式之前,我們要先估計我們作法的複雜度
避免我們陷入明明寫完程式碼
測資卻全部都錯光光,然後跟講師一樣一直拿0分
運算次數
記憶體用量
big - O notation
複雜度ㄟ估計方法
如果有看過演算法的資料
就會看到類似 \( \mathcal O(n) \; \mathcal O( \log n)\) 之類的東東
這是什麼勒?
假設有一個演算法的時間複雜度是 \( \mathcal O (n^2)\)
代表當此演算法輸入值的大小為\(n\)時,
程式的運算量會接近\( n^2 \)的尺度!
常見的幾個複雜度
如果你們大概知道一點數學,
可以把\(O ( f(n) )\)曲線視為 \(f(n) \)的函數圖形
順帶一提,\(\log\)一般是以2為底的,不過底數其實沒差(why? math!)
Big-O Notation 的估計方式
在O - notation下
我們只取跟nn有關且增長最快的項
且不看方程式的係數
嚴謹定義與證明要用極限來解釋
但可以說這樣的表示法是因為,在nn很大的時候 i.e. \(n = 10^6\)
其他的數字對複雜度的影響其實不大
舉例:
演算法隨輸入值n的運算量
複雜度
怎麼知道會不會 TLE / MLE ?
把題目給的 \(n\) 的最大值放進去估算
看看會不會超過:
TLE : 一般限制到 10^7\(10^7\) 或 10^7\(10^8\)
MLE : 一般大約可以到10^7\(10^7\)左右
舉例:
(隨便亂抓的題目,還沒教的範圍)
\( O (n^2) \) 演算法: \( (10^5 )^2 = 10^{10}\)
\( O (n\ log\ n) \) :
TLE
AC
\( (10^5 )\times \log_2 (10^5) \approx 16 \times 10^{5} \approx 10^6\)
\( O (n) \) :\(10^5 \)
AC
#include <iostream>
using namespace std;
int main() {
// 這是一個把數字倒著輸出的程式
int n;
cin >> n;
int seq[n];
for(int i = 0; i < n; ++i) {
cin >> seq[i];
}
for(int i = n-1; i >= 0; --i) {
cout << seq[i] << ' ';
}
cout << '\n';
return 0;
}
小測驗
複雜度?
\( O (n) \)
STL
什麼是STL?
Standard Template Library
- 一堆好用的內建容器
- 和其他酷東西
- 大部分的東西屬於資料結構
今天會講的STL
下週會講的STL
- vector
- deque
- queue
- stack
- pair / tuple
- set
- map
- priority_queue
- sort
- algorithm
很多STL算是一種資料結構
資料結構 ==> 可以儲存資料的東西
ex. 陣列、堆(heap)、線段樹
阿為什麼不用陣列就好?
你開心也可以,但很多題目使用STL會使其方便非常多。
另外,如同前面所說,不同資料結構的複雜度可能有差異喔!
- 不同資料結構會有不同的特性,這些特性可以讓我們方便的使用存取、修改資料
- 又或著說某些操作只有特定資料結構可以在較好的複雜度當中達成
- 因此,理解他們以及熟悉他們之間的差異十分重要!
Vector
名字是向量,但其實是進化版陣列的神奇好用東東
Vector 是什麼?
- 屬於STL的進化版的陣列
- 特色:長度可以變!!!
如果是Array
宣告時就要有大小了
例如:arr[10]
但vector沒在跟你管這些的啦
直接伸縮
你可以從他的結尾塞東西進去
宣告
vector<型態> 名字;
或
vector<型態> 名字(初始長度, 初始值);
記得要#include <vector>
vector<int> v;
//宣告名字為v,存的東西型態為int的vector,初始長度為0
vector<bool> aawsoweak;
//宣告名字為aawsoweak,存的東西型態為bool的vector,長度為0
vector<int> vec2(10);
//宣告名字為vec2,初始長度為10的vector,沒指定初始值因此自動填0
vector<int> vec3(5, 2);
//vec3 裡面為 {2, 2, 2, 2, 2}
vector<int> vec4 = {2, 12, 23, 222}; // 直接給予初始長相
插入元素(後面)
(不能插入前面ㄛ)
.push_back(值)
刪除元素(後面)
(不能刪除前面ㄛ)
.pop_back()
vector<int> aaw(3, 0); // {0, 0, 0};
aaw.push_back(22); // 變成{0, 0, 0, 22};
aaw.push_back(59); // 變成{0, 0, 0, 22, 59};
aaw.pop_back(); // 變成{0, 0, 0, 22};
aaw.pop_back(); // 變成{0, 0, 0};
時間複雜度 : \( O(1) \)
超棒der
訪問最前面元素
vector<int> v;
v.push_back(5);
v.push_back(7); // {5, 7}
cout << v.front() << '\n'; // 5
.front()
時間複雜度: \(O(1)\)
訪問最後面元素
vector<int> v;
v.push_back(5);
v.push_back(7); // {5, 7}
cout << v.back() << '\n'; // 7
.back()
時間複雜度: \(O(1)\)
取得vector大小
.size()
vector<int> aaw(3, 0); // {0, 0, 0};
aaw.push_back(22); // 變成{0, 0, 0, 22};
cout << aaw.size(); // 4
aaw.push_back(59); // 變成{0, 0, 0, 22, 59};
cout << aaw.size(); // 5
aaw.pop_back(); // 變成{0, 0, 0, 22}
aaw.pop_back(); // 變成{0, 0, 0}
cout << aaw.size(); // 3
時間複雜度: \(O(1)\)
檢測Vector是否為空
.empty()
// 為空回傳true
vector<int> v;
v.push_back(5);
if ( v.empty() ) {
cout << "empty!\n";
} else {
cout << "not empty!\n";
}
// result : 輸出 "not empty!"
時間複雜度: \(O(1)\)
所以Vector到底可以幹嘛?
最重要的用法:
和陣列一樣直接用 vec[i]
取第i
項的值!
vector <int> vec(3, 0); // {0, 0, 0}
vec[0] = 10; // {10, 0, 0}
cout << "vec[2] : " << vec[2] << endl;
vec.push_back(4); // {10, 0, 0, 4}
for (int i = 0; i < n; ++i) {
cout << vec[i] << " \n"[i == (n-1)];
}
/*
result:
vec[2] : 0
10 0 0 4
*/
時間複雜度:\( O (1)\)
erase() 刪除任意項
insert() 再任意位置插入
這兩個操作時間複雜度皆為 \(O(n)\)
能不要用就不要用
vector 其他用法:
如果想要頻繁地插入刪除,建議用list(等等會提到的另一個資料結構)
實作時間:
題目大意:
給你一個數列 \(A_1, A_2, A_3 ... A_n\)
並且定義一個前綴和陣列B,
其中 \(B_1 = A_1\)、 \(B_2 = A_1+A_2\)、 \(B_3 = A_1+A_2+A_3\)......
請輸出陣列B
試試看能不能用vector做?
HINT:
注意\(B_i\)中某些東西前面算過摟
其實這題用一般陣列也做得出來,但我想要你們用vector做做看
Ans :
\( B_i = B_{i-1} + A_i\)
如果有發現這件事,接下來照著做就好
#include <iostream>
#include <vector>
#define ll long long
using namespace std;
int main() {
int n;
cin >> n;
vector<ll> A(n);
vector<ll> B(n);
for (int i = 0; i < n; ++i) {
cin >> A[i];
}
B[0] = A[0]; // 首項要特別處理!
for (int i = 1; i < n; ++i) {
B[i] = B[i-1] + A[i];
}
for (ll &i : B) { // 神奇for 用法!
cout << i << " \n"[&i == &B.back()];
// 利用i的記憶體位置有沒有和B最後一項一樣來決定要不要換行
}
}
練習
iterator
迭代器
Iterator是什麼
今天當我們的資料結構是一個vector或陣列時,我們可以用for迴圈輕鬆的遍歷
例如:
vector<int> arr = {12, 23, 10, 35};
for (int i = 0; i < 4; ++i) {
cout << arr[i] << endl;
}
但是假如今天東西長得很畸形呢?
index | 0 | 1 | 2 | 3 | |
---|---|---|---|---|---|
value | 12 | 23 | 10 | 35 |
所以這時候我們就需要迭代器了
迭代器是STL的資料結構中內建的東西
有點像是進化版的指標
可以幫助我們遍歷一個資料結構
以set為例
orz.begin()
orz.end()
orz.end()-1
因為記憶體不連續,所以需要用iterator這種特別的東東
it++
迭代器種類?
- 隨機存取(Random Access)迭代器
- 雙向(Bidirectional)
- 單向(Forward)迭代器
現在還不用知道他們分別是什麼
有興趣自己去看 2016年校培講義
How To Use an Iterator ?
1. 宣告
資料結構名<型別>::iterator 名字
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int>::iterator it;
}
大概長這樣
但其實迭代器很少直接宣告使用
通常會用接下來兩個方法
begin()
end()
回傳「指向資料結構的第一個元素」的迭代器
回傳「指向資料結構的結尾元素」的迭代器
結尾是空元素!
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 建立一個vector
vector<int> brineOrz;
// 讓it等於vector的第一項的迭代器
vector<int>::iterator it = brineOrz.begin();
// 或是
auto it2 = brineOrz.begin();
}
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 建立一個vector
vector<int> brineOrz;
// 讓it等於vector的最後一項的下一項的迭代器
vector<int>::iterator it = brineOrz.end();
// 或是
auto it2 = brineOrz.end();
}
要注意的是
begin(), end() 會回傳一個迭代器
front(), back() 會直接回傳值!
阿我拿到了iterator
要怎麼取得他指著的值?
一樣用*符號
#include <iostream>
#include <vector>
using namespace std;
int main() {
//vector裡面放{1, 2}
vector<int> lemondian = {1, 2};
vector<int>::iterator it = lemondian.begin();
//*it取值
cout << *it << '\n';
}
OUTPUT:
1
迭代器還可以幹嘛?
可以迭代
it++ ==> 將迭代器指向下一個位置
it-- ==> 將迭代器指向上一個位置
#include <iostream>
#include <vector>
using namespace std;
int main() {
//vector裡面放{1, 2}
vector<int> lemondian = {1, 2};
vector<int>::iterator it = lemondian.begin();
//it移到下一項
it++;
cout << *it << '\n';
}
OUTPUT:
2
最強用法
用迭代器+for迴圈走過整個vector或其他容器
vector<int> cont = {1, 24, 434, 10};
for (auto it = cont.begin(); it != cont.end(); ++it) { // 正
cout << *it << " ";
}
cout << endl;
for (auto it = cont.rbegin(); it != cont.rend(); ++it) { // 反
cout << *it << " ";
}
cout << endl;
OUTPUT:
1 24 434 10
10 434 24 1
迭代器雖然寫起來並不直覺
但如果使用的好的話會是非常有用的工具ㄛ
( btw迭代器運算的時間複雜度都是\(O(1) \) )
尤其在編譯器版本不夠新的比賽
更幾乎是一定得使用迭代器呢!
而且,在很多STL的內建函式當中,
都要用iterator作為傳入值或回傳值,
例如:sort( )
因此這個一定要學的啦!
那接下來我們就可以正式進入資料結構的世界囉
蕾絲狗
DEQUE
What is DEQUE?
double ended queue
Queue是什麼等等會講到
基本上和vector差不多,也可以用[]取值
但是前後皆可以\(O(1)\)插入、刪除
常用的方法有:
push_front(), pop_front()
push_back(), pop_back()
除非你需要從前面插入、刪除,不然平常用vector就好,deque常數較大(較慢)
接下來這兩個東西屬於 STL 中的
container adapter 適配器
我們會利用某些容器的特殊性質
取其精隨,然後改造它
就變成了一個新的容器ㄛ
484 很神奇
這個名字聽過就好
STACK
什麼是Stack
- 堆疊
- 可以想像為一疊疊起來的盤子,以只能在最上面放東西&拿最上面的東西
- 有著 FILO ( First In Last Out ) 的性質
- ===> 最先被放進Stack的東西會最後拿出來
- ===> 上面的東西被拿走之前自己無法被拿出
內建Stack的使用
宣告
記得 #include <stack>
stack <型態> 名字;
stack<int> aawsoweak;
放入東西
名字.push(要放入的東西)
stack<int> stk;
stk.push(5);
stk.push(7);
5
7
時間複雜度: \(O(1)\)
訪問最上層元素
名字.top()
stack<int> stk;
stk.push(5);
cout << stk.top() << endl; // 5
stk.push(7);
cout << stk.top() << endl; // 7
5
top()
時間複雜度: \(O(1)\)
訪問最上層元素
名字.top()
stack<int> stk;
stk.push(5);
cout << stk.top() << endl; // 5
stk.push(7);
cout << stk.top() << endl; // 7
5
top()
7
時間複雜度: \(O(1)\)
刪除最上方元素
名字.pop()
stack<int> stk;
stk.push(5);
stk.push(7);
cout << stk.top() << endl; // 7
stk.pop();
cout << stk.top() << endl; // 5
5
top()
7
時間複雜度: \(O(1)\)
刪除最上方元素
名字.pop()
stack<int> stk;
stk.push(5);
stk.push(7);
cout << stk.top() << endl; // 7
stk.pop();
cout << stk.top() << endl; // 5
5
top()
時間複雜度: \(O(1)\)
檢測堆疊大小
stack<int> stk;
stk.push(5);
stk.push(7);
cout<<stk.size();//2
stk.pop();//7被移出去
cout<<stk.size()//1
.size()
時間複雜度: \(O\,(1)\)
檢測堆疊是否為空
stack<int> stk;
stk.push(5);
if(stk.empty()){
cout<<"empty!\n";
} else {
cout<<"not empty!\n"
}
//輸出 "not empty!"
.empty()
//為空回傳true,否則false
時間複雜度:\(O(1)\)
Stack 題目練習
NEOJ 36
裸題,練習看看
AC code點我
#include <iostream>
#include <vector>
#include <stack>
#define ll long long int
#define debug(x) cerr<<#x<<" = "<<(x)<<endl
using namespace std;
int main() {
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int t;
cin >> t;
stack<int>a;
while (t--) {
int type;
cin >> type;
if (type == 1) {
int temp;
cin >> temp;
a.push(temp);
}else{
if (a.empty()) {
cout << "empty!\n";
}else{
cout << a.top() << endl;
a.pop();
}
}
}
return 0;
}
好誒~是我最愛的火車
大意:給你一列按照數字排列在右邊的火車,問能否用一個車站使這些火車的車廂以給定方式在左邊排列?
有沒有覺得這個車站長得像什麼東西?
一個stack!!!
- 也就是說,我要把一些東西放入stack中,看看能不能以一定方式取出來
- 阿要怎麼做勒?直接做!
- 假設左側希望下一輛車是A
- 如果 A 在右側最前面 ?
- 直接開去左邊!
-
如果 A 在右側,但有其他車擋住?
- 把其他車塞入stack中
- 直接開去左邊!
-
如果 A 在 stack?
- 在最上面 -> 開去左邊!
- 不在最上面 -> 開不出來!
#include <iostream>
#include <vector>
#include <stack>
#include <queue>
#define ll long long int
#define debug(x) cerr<<#x<<" = "<<(x)<<endl
#define _ ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
int main() {_
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
bool tf = 1;
int cur = 1;
queue<int>left;
stack<int>mid;
for (int i = 0; i < n; i++) {
int temp;
cin >> temp;
left.push(temp);
}
while (cur <= n+1 && !left.empty()) {
if (mid.empty() || mid.top()!=left.front()) {
mid.push(cur);
cur++;
}
if (mid.top()==left.front()) {
mid.pop();
left.pop();
}
}
if (mid.empty()) {
cout << "Yes\n";
}else{
cout << "No\n";
}
}
return 0;
}
AC code點我
(我偷偷用了等等會講的queue,可以想想有沒有不用的做法)
更多題目:
Queue
什麼是Queue(佇列)
可以想成像排隊一樣,先進來的,就會先出去,秉持FIFO (First In First Out / 先進先出) 的特性。
Queue 只能訪問隊首/隊尾元素
5 | 8 | 7 | 4 |
---|
10
3
10
3
1 2 3 4
.front()
.pop()
.push()
.back()
長相
宣告
queue<型態> 名字
queue<int> q;
//宣告名字為q,型態為int的queue
插入元素
queue<int> q;
q.push(5); {5}
q.push(7); {5,7}
.push(x)
時間複雜度: \(O(1)\)
訪問隊首/尾元素
queue<int> q;
q.push(5); {5}
q.push(7); {5,7}
cout<<q.front();//5
cout<<q.back();//7
.front() / .back()
時間複雜度: \(O(1)\)
將隊首元素出隊
queue<int> q;
q.push(5); {5}
q.push(7); {5,7}
q.pop();//移除5
q.front();//回傳7
.pop()
時間複雜度: \(O(1)\)
.size() / .empty()
和stack、vector用法一樣
queue<int> q;
q.push(5); {5}
cout << q.size() << endl; // 1
q.push(7); {5,7}
cout << q.size() << endl; // 2
q.pop(); //移除5
cout << q.size() << endl; // 1
q.pop(); //移除7
cout << q.size() << endl; // 0
if (q.empty()) {
cout << "empty\n";
} else {
cout << "not empty\n";
}
// OUTPUT:
1
2
1
0
empty
時間複雜度: \(O(1)\)
來練習ㄅㄚ
pair / tuple
你有很多盤子,每個盤子有一個價格
今天鹽亞倫要你練習堆盤子,如果你不會堆,你就要當盤子請他吃拉麵。
每一次鹽亞倫可能會叫你把一個盤子疊到盤子堆、把最上面的盤子丟掉、或是問你疊疊樂最上面的那個盤子的價格是多少?
怎麼做?
就只是stack!!!
你有很多盤子,每個盤子有一個價格&顏色
今天鹽亞倫要你練習堆盤子,如果你不會堆,他就要當盤子請他吃拉麵。
每一次鹽亞倫可能會叫你把一個盤子疊到盤子堆、把最上面的盤子丟掉、或是問你疊疊樂最上面的那個盤子的價格&顏色是多少?
哇現在有兩個東西要記錄誒,怎麼做?
開兩個stack?
聰明一點的做法 --- 把兩個變數合成一個!
How? Pair!!!
pair<型態, 型態> 名字
就是一個雙格儲存空間的變數啦~
宣告
//宣告名字為p,有兩個int的pair
pair<int, int> p;
//宣告名字為p2,有一個int一個string的pair
pair<int, string> p2;
// 神奇用法:
//宣告名字為p3,有一個stack<int>一個queue<int>的pair
pair<stack<int>, queue<int>> AaWsoWEAK;
怎麼賦值?
pair = {a, b} 或另外一個型別相同的 pair
pair<int, int> p1 = {1, 2};
pair<int, int> p2 = {2, 5};
p1 = p2; // p1 = {2, 5}
怎麼使用?
- 第一個叫做.first
- 第二個叫做.second
- 剩下和一般變數一樣用
- 夠簡單吧
- 注意沒有括號喔
pair<int, int> p = {1, 2};
cout << p.first << ' ' << p.second; // 1 2
所以剛剛的問題
可以用一個 stack< pair<顏色,價錢> >來處理!
懂了嗎~
呵呵建北OJ還是爛的我沒辦法出題啦之後再補題號
啊可是,pair只能兩個東西誒
如果我想要三個四個五個勒?
tuple!!!
使用方式:
tuple <int, int, int> AaW = {1, 2, 44}; // 宣告
cout << get<0>(AaW) << " " << get<1>(AaW) << " "<< get<2>(AaW) << endl;
// 要用get<位置>(名字)來取值
// OUTPUT: 1 2 44
酷酷的東東
pair<int, int> p = {1, 2};
int a = 222;
p = make_pair(a, 10); // p = {222, 10}
// tuple以此類推
make_pair(a, b) 生成一個pair型別還幫你用好
同理我們有make_tuple(a, b, c)
下課啦~