Basic Data Structure

&

STL[0]

algorithm[3] 22504 鹽亞倫

索引(可以點)

參考

reference

上週來不及講的語法

既然都說是參考了那就...

但他好爛他上週上課前30分鐘才被我才更正他的錯誤觀念

看這個比較好啦

複雜度

complexity

複雜度是啥麼?

能吃嗎?

不行吃,想吃東西來秋遊烤肉

複雜度  ===> 程式花費東西的程度

花費什麼勒?  ===> 時間空間

也就是說,一個程式的時間複雜度空間複雜度越高

電腦的負擔就越大

然後就會吃TLEMLE

所以在寫程式之前,我們要先估計我們作法的複雜度

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

測資卻全部都錯光光,然後跟講師一樣一直拿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的運算量

複雜度

n^2 + n + 10
3n \log n + 5n + 87
2^n + n^2 + \log n + n + 20
1000
\mathcal O(1)
\mathcal O(2^n)
\mathcal O(n \log n)
\mathcal O(n^2)

怎麼知道會不會 TLE / MLE ?

把題目給的 \(n\) 的最大值放進去估算

看看會不會超過:

TLE : 一般限制到 10^7\(10^7\) 或 10^7\(10^8\)

MLE : 一般大約可以到10^7\(10^7\)左右

舉例:

TIOJ 1030

(隨便亂抓的題目,還沒教的範圍)

\( 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++

迭代器種類?

  1. 隨機存取(Random Access)迭代器
  2. 雙向(Bidirectional)
  3. 單向(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常數較大(較慢)

在處理 單調隊列DP優化 時會用到,現在只是稍微提一下

有興趣自己查資料

接下來這兩個東西屬於 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,可以想想有沒有不用的做法)

類似題:

ZJ c123(一樣題目but輸入麻煩)

TIOJ 1012 (多了一個調度軌)

NEOJ 23(多了一個車站)

更多題目:

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)

下課啦~

要繼續來聽喔