資料結構 &
STL Container

記得用Esc看看有什麼東西喔!

Special Thanks:
bookgin

資料結構是什麼?

如何去存一個資料

你想怎麼存OOXX?

  • 「啊,好像棋盤似的,用二維 a[3][3] 陣列」
  • 「我看倒有點像稿紙,用一維 a[9] 就夠了」
  • 「真像一塊塊綠豆糕,不如用整數的九個位數來存。」

如何存資料是你的事。

  •  每個題目你都可以用隨便一個資料結構來實做。
  •  但是可能時間複雜度(所花的時間)就不一樣了。

Array & Linked List

Photo Credit: 112CS 蔡欣穆教授的DSA

Queue

什麼是Queue?

像排隊一樣,先進先出。
後來的人塞屁股。

先進先出的專有名詞:FIFO (First In First Out)

std::queue用法

函式庫 #include <queue>
宣告 std::queue<T> Q;
enqueue(放屁股) Q.push(T);
dequeue(拿頭) Q.pop();
看前面是誰 Q.front();
現在裡面有幾個? Q.size();
現在是不是空的? Q.empty()

T可以帶入所有型態,包括你自己寫的struct。

以上全O(1)操作。

std::queue 例子

#include <iostream>
#include <queue>
using namespace std;
struct crood{
    int x,y;
    crood(int _x,int _y){
        x = _x, y = _y;        
    }
};
int main(){
    queue<crood> Q;
    Q.push(crood(1,1));
    Q.push(crood(3,3));
    cout << Q.front().x << endl;
    Q.pop();
    cout << Q.front().x << endl;
}
// 輸出 1 3

Queue 練習!

Stack

什麼是Stack?

像你的衣櫃,被早被塞進去的都會忘記(?)

先進先出的專有名詞:LIFO (Last In First Out)

std::stack用法

函式庫 #include <stack>
宣告 std::stack<T> S;
enqueue(放屁股) S.push(T);
dequeue(拿頭) S.pop();
看屁股是誰 S.top();
現在裡面有幾個? S.size();
現在是不是空的? S.empty()

T可以帶入所有型態,包括你自己寫的struct。

以上全O(1)操作。

std::stack 例子

#include <iostream>
#include <stack>
using namespace std;
struct crood{
    int x,y;
    crood(int _x,int _y){
        x = _x, y = _y;        
    }
};
int main(){
    stack<crood> S;
    S.push(crood(1,1));
    S.push(crood(3,3));
    cout << S.top().x << endl;
    S.pop();
    cout << S.top().x << endl;
}
// 輸出 3 1

Stack 練習!

Challenge - 1

Deque

有沒有一種天方夜譚...

有stack和queue的功能,
但是時間複雜度都是O(1)?

Stack & Queue...

什麼是Deque?

就是同時有stack和queue的資料結構!

它唸作Deck,不唸De-queue

std::deque用法

函式庫 #include <deque>
宣告 std::deque<T> D;
放頭 D.push_front(T);
放屁股 D.push_back(T);
拿頭 D.pop_front(T);
拿屁股 D.pop_back();
看頭是誰 D.front();
看屁股是誰 D.back();
現在是不是空的? D.empty();

std::deque 例子

#include <iostream>
#include <deque>
using namespace std;
struct crood{
    int x,y;
    crood(int _x,int _y){
        x = _x, y = _y;        
    }
};
int main(){
    deque<crood> D;
    D.push_back(crood(1,1));
    D.push_back(crood(2,2));
    D.push_front(crood(3,3));
    cout << D.front().x << endl;
    D.pop_front();
    cout << D.front().x << endl;
    D.pop_front();
    cout << D.front().x << endl;
}
// 輸出 3 1 2

Challenge - 2

Vector

會不會有一天...

題目請你存一個數列,但是卻步知道這個數列最多有幾個?

那該怎麼辦呢?

直接開到最大也不是太好?

Dynamic Array

當你用到2^k個的時候,就幫你擴充到2^(k+1),也就是兩倍。

-> 空間複雜度O(n),不會浪費辣麼多

STL的vector就是一種Dynamic Array!

std::vector用法

函式庫 #include <vector>
一般宣告
先宣告N個東西
先宣告N個c的東西
std::vector<T> V;
std::vector<T> V(N);
std::vector<T> V(N,c);
enqueue(放屁股) V.push_back(T);
dequeue(拿屁股) V.pop_back();
清空整個vector V.clear();
現在裡面有幾個? V.size();
現在是不是空的? V.empty();
在vector的iter插入x V.insert(iter,x);
在iter刪除東西 V.erase(iter);
存取第k個東西 V[k]
開頭的iterator V.begin()
結尾的iterator(指向最後一個元素+1) V.end()

std::vector 例子

#include <iostream>
#include <vector>
using namespace std;
int main(){
    vector<int> V(5,-1);
    for(int i=0;i<5;i++)
        V.push_back(i);
    V[2] = -10;
    V.pop_back();
    for(int i=0;i<V.size();i++)
        cout << V[i] << " ";
}
// 輸出-1 -1 -10 -1 -1 0 1 2 3

有沒有發現一件事情?

好像vector可以代替stack也...

沒錯,
所以你可以乾脆直接用vector。
通常會寫stack都是為了讓自己知道自己要用的是stack而已啦~

Array & Vector & Linked List

Photo Credit: 112CS 蔡欣穆教授的DSA

等等,
是不是漏講了什麼?

erase和insert呢?

Iterator

Iterator (迭代器)

  • 跟指標87%像
  • 基本上你可以當作是STL的專用指標。
  • 不要管怎麼實做的,你會豆頁痛。

iterator的各種操作

假設宣告了 ... vector<int> V int ary[N]
拿到一個container的開頭iter/ptr V.begin() ary
拿到一個container的結尾iter/ptr V.end() ary+N
這一個iterator指到的東西是什麼? *(iter) *ptr
下一個iterator是誰? iter + 1 ptr + 1
讓它變成下一個iterator iter++ ptr ++
iterator/ptr的型態? vector<int>::iterator ptr

一個簡單的圖例

  • 所以V.end()是不能夠 *的,因為它沒有東西
  • 跟int ary[N]的ary[N]沒有東西是一樣的!

所以那個insert和erase...

在vector的iter插入x V.insert(iter,x);
在iter刪除東西 V.erase(iter);
在vector的地k個插入x V.insert(V.begin()+k,x);
在vector的第k個刪除 V.erase(V.begin()+k);

配合iterator就可以寫成...

不能直接給數字喔!會CE

std::vector::iterator 例子

#include <iostream>
#include <vector>
using namespace std;
int main(){
    vector<int> V(5,-1);
    for(int i=0;i<5;i++)
        V.push_back(i);
    vector<int>::iterator iter;
    V.insert(V.begin()+1, -10);
    V.erase(V.begin());
    for(iter = V.begin(); iter != V.end() ; iter++)
        cout << *(iter) << " ";
    cout << endl;
}
// 輸出-10 -1 -1 -1 -1 0 1 2 3 4

一些iterator的警告

我們不知道
STL怎麼實做這些資料結構的。

所以一個vector被你push_back一個東西後,可能STL就會幫你換個位置!

例如

#include <iostream>
#include <vector>
using namespace std;
int main(){
    vector<int> V(1),V2(1024,77);
    vector<int>::iterator iter=V.begin()+2047;
    for(int i=0;i<=2048;i++)
       V.push_back(i);
    cout << *iter << endl;
}

你會發現答案有可能不是2046,而是一個奇怪的數字,甚至RE。

所以...

要是改到container的話,

最好要小心iterator的位置。

題外話: auto

std::vector<int>::iterator
也太長吧! 有什麼簡寫呢?

auto

自動偵測型態並且幫你填補。

假設宣告了 ... auto會幫你填....
auto i = 0; int
auto s = "aoa"; char *
auto d = 0.0; double
auto c = 'X'; char
auto iter = std::vector<int>.begin() std::vector<int>::iterator
auto iter = std::list<int>.begin() std::vector<list>::iterator

auto的各種預測

題外話: for-each

如果你哪一天需要這樣寫...

vector<int> V(5,-1);
std::vector<int>::iterator iter;
for(iter = V.begin(); iter != V.end() ; iter++){
    int i = *(iter);
    // 做一些事情
}
vector<int> V(5,-1);
for( int i : V){
    // 做一些事情
}

你可以簡寫成這樣

簡單來說

for-each就是幫你從container把一個一個東西丟到迴圈。

for( auto i : container ){
    // 做一些事情
}

所以對於所有container可以寫成這樣

List

List其實就是
Doubly Linked List!

std::list用法

函式庫 #include <list>
宣告 std::list<T> L;
放頭 L.push_front(T);
放屁股 L.push_back(T);
拿頭 L.pop_front(T);
拿屁股 L.pop_back();
頭的iterator L.begin();
結尾的iterator (最後一個+1) L.end();
iterator指向下一個 iter ++ ;  **list的iter不能+=1**
iterator指到上一個 iter -- ;
在iter插入x L.insert(iter,x);
在iter刪除x L.erase(iter);

你可以想成其實List的功能

就是vector + deque !

有vector的insert/erase 和
deque 的雙重push/pop

但注意list::iter不能夠iter+k。

std::list 例子

#include <iostream>
#include <list>
using namespace std;
int main(){
    list<int> L;
    L.push_back(1);             // front (1) - end
    L.push_front(0);            // front (0) - 1 - end
    L.push_back(3);             // front (0) - 1 - 3 - end
    list<int>::iterator iter;
    iter = L.begin();           // front/iter (0) - 1 - 3 - end
    cout << *iter << endl;
    L.insert(iter,5);           // front(5) - iter(0) - 1 - 3 - end
    iter++;                     // front(5) - 0 - iter(1) - 3 - end
    L.erase(iter);              // front(5) - 0 - 3 - end
    for(auto i: L)
       cout << i << " ";
    cout << endl;
}
// 輸出 5 0 3 

小小總結

整理表

名稱 拿front
拿end 放front 放end 看front 看end
stack S.pop() S.push() S.top()
queue Q.pop()
Q.push() Q.front()
deque Q.pop_front()
Q.pop_back() Q.push_front() Q.push_back() Q.front() Q.back()
vector
Q.pop_back() Q.push_back() Q.front() Q.back()
list Q.pop_front()
Q.pop_back() Q.push_front() Q.push_back()
名稱 empty() size() clear() 開頭iter: begin() 結尾iter: end()
stack O O X 沒有iterator 沒有iterator
queue O O X 沒有iterator 沒有iterator
deque O O O O O
vector O O O O O
list O O O O O

結語

- 等價交換 -

- 等價交換 -

雖然看起來list什麼事情都做的到,但是代價就是速度很慢。

相對的,vector就快很多。

 

因此對一個情境找到一個最適當的資料結構才是最好的。

去寫

Challenge / HW

囉:D

* Challenge 1難度 > Challenge 2難度 >> HW難度

Appendix

std::pair

可以一次裝兩個東西的container

宣告: std::pair<int,int>

做出一個pair:std::make_pair(a,b);

Challenge 1 - Solution

1. 對於每位勇者能夠"往前"溝通的勇者,

必是非嚴格遞減序列。 (否則就會被阻斷)

這個要用stack來做。

2. 假設一個勇者等級為k,
他可以往前溝通到所有等級<=k的勇者。

但是小於k的勇者們這次溝通完就用不到了。因為這個勇者會阻斷他根後面勇者的溝通

3. 假設一個勇者等級為k,
他最多只能夠溝通到一位等級大於k的勇者

這是一個特判。

Challenge 1 - Solution Code

#include <iostream>
#include <stack>
using namespace std;
int T,n,x;
int main(){
    cin >> T;
    while(T--){
        long long ans = 0;
        scanf("%d",&n);
        stack<pair<int,int> > S;
        while(n--){
            scanf("%d",&x);
            while(!S.empty() && S.top().first < x)
                ans+=S.top().second,S.pop();
            int tmp = 0;
            if(!S.empty() && S.top().first == x){
                ans += tmp = S.top().second;
                S.pop();
            }
            if(!S.empty())
                ans++;
            S.push(make_pair(x,tmp+1));
        }            
        cout << ans << endl;
    }
    return 0;
}

Challenge 2 - Solution

1. 預先處理好所有答案。不要問了再做。

2. 每算完一個隊伍,都會新增一個人,砍掉一個人。這個稱為Sliding Window

假如現在做[0,3],那麼下一個就是[1,4],再下一個就是[2,5]

3. 如果現在是[i,i+k-1],發現第i+k的B數比i+k-1的B數更低,我們可以把i+k-1拿掉。因為他始終不會是答案。

所以你的container會是一個非嚴格遞增數列。 -> 答案都會是deque.front()

4.  綜合2跟3,要pop_front pop_back & push_back,

所以要用deque。

Challenge 2 - Solution Code

#include <iostream>
#include <deque>
using namespace std;
int ans[10000000];
int main(){
    deque<pair<int,int> > D;
    int n,k,q,i,x;
    cin >> n >> k;

    cin >> x;
    D.push_back(make_pair(x,0));
    for(int i=1;i<n;i++){
        cin >> x;
        while(!D.empty() && D.back().first > x)
            D.pop_back();
        D.push_back(make_pair(x,i));
        if(D.front().second == i-k)
            D.pop_front();
        ans[i] = D.front().first;
    }
    scanf("%d",&q);
    while(q--){
        cin >> i;
        cout << ans[i+k-1] << endl;
    }
}

STL

By Arvin Liu

STL

Teaching Slide - stack/queue/list/vector & pass by ??

  • 1,002