時間複雜度 & 基礎資料結構

Lecturer:Lemon

Slides:yeedrag

複雜度? 能吃嗎

複雜度是我們估計一個程式的執行花費的工具

也就是代表程式的運算次數 or 記憶體用量

複雜度越高,當然電腦的負擔就越大

而你們就會吃 TLE、MLE

所以在我們開始寫程式碼前

我們通常會先估計程式的複雜度

避免我們陷入明明刻完程式碼

測資卻全部都錯光光的窘境

O - notation

如果你有看過演算法的題目、講義

你應該會常常看到\(O(1) , O(n), O(n!) \)之類的

前面的\(O\)就代表O - notation

O - notation

在O - notation下

我們只取跟\(n\)有關且增長最快的項

嚴謹的證明當然不簡單

但可以說這樣的表示法是因為

在\(n\)很大的時候 i.e. \(n = 10^6\) 

其他的數字對複雜度的影響其實就不大ㄌ

 

e.g.

\(n^2 + n + 2 = O(n^2) \)

\(2^n + n^2 = O(2^n)\) 

\(100000 = O(1)\)

複雜度估算

把複雜度以O - notation表示

將\(n\)的值帶進去,就大約是我們的複雜度ㄌ

 

時間複雜度 一秒內複雜度大約可以到\(10^8  \sim 10^9\)

空間複雜度 一般的題目複雜度大約可以到\(10^7\)左右

懂ㄇ(?

#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)

What are STLs?

 

 

標準模板庫英文Standard Template Library縮寫STL),是一個C++軟體庫,大量影響了C++標準程式庫但並非是其的一部分。其中包含4個組件,分別為演算法容器函式迭代器[1]

模板是C++程式設計語言中的一個重要特徵,而標準模板庫正是基於此特徵。標準模板庫使得C++程式語言在有了同Java一樣強大的類別館的同時,保有了更大的可延伸性

反正就是容器函式迭代器的酷東西.w.

今天會教的STL們

  • Queue
  • Stack
  • Vector
  • Pair/Tuple

我有Array,幹嘛學他們?

A:你開心也可以,但很多題目使用STL會使其方便非常多,但不同資料結構的複雜度可能有差異喔!

迭代器(?

簡單來說,假設今天我們有一個容器C

但我們的 C 長的並不像陣列

原本我們可以透過for迴圈對一個Array做遍歷

但 C 長得比較畸形

 

所以STL中便提供了迭代器來解決這樣的問題

雖然迭代器稍微有點毒瘤(?

但如果打校內賽編譯器很爛就不得不用ㄌ

迭代器種類

  1. 隨機存取(Random Access)迭代器
  2. 雙向(Bidirectional)
  3. 單向(Forward)迭代器

這其實完全不重要

但感覺還是放一下比較好

btw 我也沒打算要講啦

可以參考2016年校培講義(如果你真的有興趣的話

宣告

資料結構名<型別>::iterator 名字

寫出來大概長這樣

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int>::iterator it; 
	//vector後面會教喔喔喔
}

.begin()

回傳資料結構的第一個元素的迭代器

#include <iostream>
#include <vector>
using namespace std;

int main() {
	//建立一個vector
	vector<int> yeedragorz;
	//讓it等於vector的第一項的迭代器
	vector<int>::iterator it = yeedragorz.begin(); 
}

.end()

回傳資料結構的最後一項元素的後一項的迭代器

(對 就是這樣 不要懷疑

#include <iostream>
#include <vector>
using namespace std;

int main() {
	//建立一個vector
	vector<int> yungyaorz;
	//讓it等於vector的第最後一項的後面一項的迭代器
	vector<int>::iterator it = yungyaorz.end(); 
}

迭代器到底可以幹嘛

#include <iostream>
#include <vector>
using namespace std;

int main() {
	//vector裡面放{1, 2}
	vector<int> lemonweak = {1, 2}; 
	vector<int>::iterator it = lemonweak.begin();
	//*it取值
	cout << *it << '\n';
}

透過*的操作(跟指標一樣 雖然我們沒教)

可以取得迭代器所對應的值

OUTPUT:
1

迭代器到底可以幹嘛

#include <iostream>
#include <vector>
using namespace std;

int main() {
	//vector裡面放{1, 2}
	vector<int> lemonweak = {1, 2}; 
	vector<int>::iterator it = lemonweak.begin();
	//it移到下一項
	it++;
	cout << *it << '\n';
}

透過++的操作

可以得到迭代器對應的後一項

OUTPUT:
2

迭代器到底可以幹嘛

#include <iostream>
#include <vector>
using namespace std;

int main() {
	//vector裡面放{1, 2}
	vector<int> lemonweak = {1, 2}; 
	//記得.end()是最後的後一項
	vector<int>::iterator it = lemonweak.end();
	//it移到前一項
	it--;
	cout << *it << '\n';
}

根據迭代器種類的不同

有些運算是被允許的

例如 --的操作可以找到迭代器的前一項

OUTPUT:
2

所以...

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v = {1, 3, 2};
	for(vector<int>::iterator it = v.begin(), it != v.end(); ++it) {
		cout << *it << '\n';
	}
}
OUTPUT:
1
3
2

我們可以用迴圈迭代ㄌ

好難寫QAQ

迭代器雖然寫起來並不直覺

但如果使用的好的話會是非常有用的工具ㄛ

( btw迭代器運算的時間複雜度都是\(O(1) \) )

尤其在編譯器版本不夠新的比賽

更幾乎是一定得使用迭代器呢!

 

那接下來我們就可以正式進入資料結構的世界囉

蕾絲狗

容器之王 Vector (我也不知道中文

需要#include<vector>

Vector是甚麼?

其實就是陣列

可以伸縮自如的那種

如果是Array

宣告時就要有大小了

例如:arr[10]

但vector沒在跟你管這些的啦

直接伸縮

宣告

vector<int> v;
//宣告名字為v,型態為int的vector

vector<型態> 名字

插入元素(後面)

(不能前面ㄛ)

vector<int> v;
v.push_back(5);
v.push_back(7);

.push_back()

時間複雜度: \(O(1)\)

訪問最前面元素

vector<int> v;
v.push_back(5); 
v.push_back(7); 
cout<<v.front();//5

.front()

時間複雜度: \(O(1)\)

訪問最後面元素

vector<int> v;
v.push_back(5); 
v.push_back(7); 
cout<<v.back();//7

.back()

時間複雜度: \(O(1)\)

將最後面元素移除

(不能前面ㄛ)

vector<int> v;
v.push_back(5); 
v.push_back(7); 
v.pop_back();//7被移出去
cout<<v.back();//5

.pop_back()

時間複雜度: \(O(1)\)

檢測Vector大小

.size()

時間複雜度: \(O(1)\)

vector<int> v;
v.push_back(5); 
v.push_back(7); 
v.pop_back();//7被移出去
cout<<v.size();//1
v.pop_back();//5被移出去
cout<<v.size();//0

檢測Vector是否為空

vector<int> v;
v.push_back(5);
if(v.empty()){
  cout<<"empty!\n";
} else {
  cout<<"not empty!\n"
}
//輸出 "not empty!"

.empty()//為空回傳true

時間複雜度: \(O(1)\)

其實接下來才是最重要的

vector<int> v;
v.push_back(5);
v.push_back(7);
v.push_back(8);
cout << v[1]; //7
for(int i = 0; i < v.size(); ++i) {
	cout << v[i] << ' ';
}
輸出 "5 7 8"

vector可以用[],像陣列一樣取值

時間複雜度: \(O(1)\)

容器適配器

Container adapter

我們會利用某些容器的特殊性質

取其精隨,然後改造它

就變成了一個新的容器ㄛ

484 很神奇

1. Queue 隊列

需要#include<queue>

Queue是甚麼?

可以想成像排隊一樣,先進來的,就會先出去,秉持FIFO(先進先出)的特性。

Queue 只能訪問隊首/隊尾元素喔!

Queue的宣告和操作:

 

宣告

queue<int> q;
//宣告名字為q,型態為int的queue

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)\)

檢測隊列大小

queue<int> q;
q.push(5); {5}
int s = q.size();//s = 1
q.push(7); {5,7}
s = q.size()// s = 2

.size()

時間複雜度: \(O(1)\)

檢測隊列是否為空

queue<int> q;
q.push(5); {5}
if(q.empty()){
  cout<<"empty!\n";
} else {
  cout<<"not empty!\n"
}
//輸出 "not empty!"

.empty()//為空回傳true

時間複雜度: \(O(1)\)

練習:

 

2. Stack 堆疊

需要#include<stack>

Stack是甚麼?

可以想成像一疊盤子一樣,後來放上去的,就要先拿走才能拿下面的,秉持FILO(先進後出)

的特性。

Stack 只能訪問最上方元素喔!

宣告

stack<int> stk;
//宣告名字為stk,型態為int的stack

stack<型態> 名字

插入元素

stack<int> stk;
stk.push(5);
stk.push(7);

.push(x)

時間複雜度: \(O(1)\)

訪問最上層元素

stack<int> stk;
stk.push(5); 
stk.push(7); 
cout<<stk.top();//7

.top()

時間複雜度: \(O(1)\)

將最上層元素移除

stack<int> stk;
stk.push(5); 
stk.push(7); 
stk.pop();//7被移出去
cout<<stk.top();//5

.pop()

時間複雜度: \(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

時間複雜度: \(O(1)\)

練習:

 

3. Pair/Tuple

需要#include<utility> for pair

#include<tuple> for tuple

宣告

//宣告名字為p,有兩個int的pair
pair<int, int> p;
//宣告名字為t,有三個int的pair
tuple<int, int, int> t;

pair<型態, 型態> 名字

tuple<型態, 型態, 型態> 名字

賦值

pair<int, int> p1 = {1, 2};
pair<int, int> p2 = {2, 5};
p1 = p2; // p1 = {2, 5}
//tuple以此類推

pair = {a, b} 或另外一個型別相同的 pair

tuple = {a, b, c} 或另外一個型別相同的 tuple

取值

pair<int, int> p = {1, 2};
cout << p.first << ' ' << p.second; // 1 2
//tuple以此類推

.first 第一個 .second第二個 (.third 第三個)

記得讀英文ㄛ

酷酷的東東

pair<int, int> p = {1, 2};
p = make_pair(4, 10); // p = {4, 10}
//tuple以此類推

make_pair(a, b) 生成一個pair型別還幫你用好

同理我們有make_tuple(a, b, c)

STL

By lemonilemon

STL

  • 606