【C++】競程筆記(資料結構:Stack、Queue)
===
[TOC]
程式碼範例參考:NTUCPC Guide,此筆記僅為個人學習用途。
資料結構
---
這啥?就是:
> 在電腦科學中,資料結構(data structure)是電腦中儲存、組織資料的方式。
> From:Wikipedia
資料結構有兩種操作:
1. 修改操作
2. 查詢操作
修改就是可以在一個資料結構,像是陣列裡面新增或移除值。
查詢就是可以存取一個資料結構的元素,如 `a[0]`。
常見的資料結構有 stack(堆疊)、queue(佇列)、array(陣列)、vector(C++ STL 的動態陣列)、linked-list(鏈結串列)等等。
vector 的部分可以參照我先前寫的:https://hackmd.io/@LukeTseng/H1ZqX9RHkl
### 迭代器(iterator)
---
> 迭代器就是一個能枚舉資料結構的資料的物件。
然後迭代器跟指標很像,但實際上不太一樣,他是一種設計模式,用來逐個存取容器(如 array、set等)內的元素,而不需要顯示容器的內部結構。
用一句話概括就是:迭代器是一種提供通用介面來遍歷容器中元素的物件。
以下程式碼會印出 a 的每一項:
```cpp=
// from NTUCPC Guide
vector<int>::iterator p = a.begin();
while (p != a.end()) {
cout << *p << endl;
p = next(p);
}
```
`a.begin()` 是指向 a 這個 vector 的第一項,`a.end()` 為指向 a 這個 vector 的最後一項的下一項物件。
* 指向迭代器的下一項用:`next()`
* 上一項:`prev()`
以下程式碼也會印出 a 的每一項:
```cpp=
// from NTUCPC Guide
auto p = a.begin();
while (p != a.end()) {
cout << *p << endl;
p = p + 1;
}
```
有時候不太知道要定義成什麼樣的資料型態,所以會用到 auto 讓編譯器自動去判斷他的資料型態。
而這支程式碼將 `p = next(p)` 更換成 `p = p + 1`,原因為 vector 是跟 array 差不多的東西,都是在記憶體中的一個連續空間。同理,`p = prev(p)` 也可換成 `p = p - 1`。
堆疊(Stack)
---
Stack 是一種先進後出(FILO:First In Last Out)的資料結構。
可以想像他是一個垂直地板而立的桶子,然後已經滿了,我們要搜尋最底下的東西,勢必要將其上層的物品一個一個拿走,最終才能取得最底下的物品。
stack 練習:https://neoj.sprout.tw/problem/36/
可以直接使用 C++ 的 STL 庫:
```cpp=
#include <iostream>
#include <stack>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
stack <int> mystack;
for (int i = 0;i < T;i++){
int state = 0;
int num = 0;
cin >> state;
if (state == 1){
cin >> num;
mystack.push(num);
}
else if (!mystack.empty()){
cout << mystack.top() << "\n";
mystack.pop();
}
else{
cout << "empty!" << "\n";
}
}
return 0;
}
```
使用前需要引入 `<stack>` 標頭檔。
`push()`、`pop()` 分別用於「將元素堆入堆疊內」、「移除堆疊頂端的元素」。
由於 `pop()` 是沒有回傳值的,所以可以先用 `top()` 取得頂端的值,再用 `pop()` 把它移除掉。
這邊有個細節,必須注意使用 `pop()` 時,要避免 stack 裡面是空的,否則會產生 undefined behavior。
筆者提供了 STL 的寫法,以下是 NTUCPC Guide 用基本的 array 實作 stack:
```cpp=
// from NTU CPC Guide
int Stack[maxn], tot = 0;
void PUSH(int x) {
Stack[tot++] = x;
}
void POP(){
if(tot == 0)
cout << "stack is empty!\n";
else
cout << Stack[--tot] << endl;
}
```
以下是 stack STL 中基本操作:
* push() 可以將資料放入堆疊的頂部;
* pop() 將頂端元素刪除;
* top() 查詢堆疊頂端元素;
* size() 查詢目前還位於堆疊中的資料數;
* empty() 回傳堆疊是否為空。
實際上 stack 在競賽中不會直接使用到其 STL,通常使用 vector 替代。
### stack 習題
---
1. Uva673:https://zerojudge.tw/ShowProblem?problemid=b304
原文 pdf:https://onlinejudge.org/external/6/673.pdf
```cpp=
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int main() {
int n;
cin >> n;
cin.ignore(); // 因為 cin 會讀到 "\n", 後面又有 getline 所以必須先清除 "\n"
for (int i = 0; i < n; i++) {
string s;
getline(cin, s);
stack<char> stk;
bool isValid = true; // 判斷是否為有效的運算式
for (char c : s) {
if (c == '(' || c == '[') {
stk.push(c);
} else if (c == ')') {
if (stk.empty() || stk.top() != '(') {
isValid = false;
break;
} else {
stk.pop();
}
} else if (c == ']') {
if (stk.empty() || stk.top() != '[') {
isValid = false;
break;
} else {
stk.pop();
}
}
}
if (isValid && stk.empty()) {
cout << "Yes" << "\n";
} else {
cout << "No" << "\n";
}
}
return 0;
}
```
2. Rails:https://tioj.ck.tp.edu.tw/problems/1012
這題我真的看得不是很懂,就交給各位解題了XD。
佇列(Queue)
---
佇列又可稱為隊列,就像是一群人在排隊一樣,是一種先進先出(FIFO)的資料結構。
queue 的 push 跟 pop 操作如下:
* push:從最後面加入某元素。
* pop:從最前面刪除某元素。
queue 練習:https://neoj.sprout.tw/problem/37/
以下是筆者使用 C++ STL 寫出的佇列程式碼:
```cpp=
#include <iostream>
#include <queue>
using namespace std;
int main(){
int T;
cin >> T;
queue <int> q;
for (int i = 0;i < T;i++){
int state, num;
cin >> state;
if (state == 1){
cin >> num;
q.push(num);
}
else if (!q.empty()){
cout << q.front() << "\n";
q.pop();
}
else{
cout << "empty!";
}
}
return 0;
}
```
* q.push() - 堆入某元素於佇列尾端
* q.pop() - 移除某元素於佇列頂端
* q.front() - 回傳佇列頂端的值
* q.back() - 回傳佇列尾端的值
* q.size() - 回傳佇列長度
* q.empty() - 檢查佇列是否為空
以下是 NTUCPC Guide 依照實作原理製作的 queue 程式碼:
```cpp=
// from NTUCPC Guide
int Queue[maxn], Front = 0, Back = 0;
void PUSH(int x) {
Queue[Back++] = x;
}
void POP() {
if (Front == Back)
cout << "empty!\n";
else {
cout << Queue[Front++] << endl;
}
}
```
環狀佇列:
```cpp=
// from NTUCPC Guide
int Queue[maxn], Front = 0, Back = 0;
void PUSH(int x) {
Queue[Back] = x;
if (++Back >= maxn) Back -= maxn;
}
void POP() {
if (Front == Back)
cout << "empty!\n";
else {
cout << Queue[Front] << endl;
if (++Front >= maxn) Front -= maxn;
}
}
```
### queue 習題
---
1. Uva 10935:https://onlinejudge.org/index.php?option=onlinejudge&Itemid=8&page=show_problem&problem=1876
zerojudge:https://zerojudge.tw/ShowProblem?problemid=e155
題目翻譯:
給定一副有序的牌組,共有 $n$ 張牌,牌面編號從 $1$ 到 $n$ ,其中編號 $1$ 的牌位於最上方,編號 $n$ 的牌位於最下方。
只要牌組中至少還有兩張牌,就執行以下操作:
將最上方的牌丟棄,然後把現在位於最上方的牌移到牌組的最下方。
你的任務是找出被丟棄的牌的順序,以及最後剩下的那張牌。
:::info
順便說一下英文文法XD,原文:
Your task is to find the sequence of discarded cards and the last, remaining card.
and the last 後面的逗號 , 代表同位語當作 and the last 名詞片語的補充說明。
:::
輸入說明:
每一行輸入(最後一行除外)包含一個數字 $n$ ≤ $50$ 。最後一行為 '0' 的話,則此行不應進行處理。
輸出說明:
對於輸入中的每個數字,請輸出兩行結果。第一行顯示被丟棄的牌的序列,第二行則顯示最後剩下的牌。每一行的開頭與結尾都不應有空格。參見範例以了解所受預期的格式。
範例輸入:
```
7
19
10
6
0
```
範例輸出:
```
Discarded cards: 1, 3, 5, 7, 4, 2
Remaining card: 6
Discarded cards: 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 4, 8, 12, 16, 2, 10, 18, 14
Remaining card: 6
Discarded cards: 1, 3, 5, 7, 9, 2, 6, 10, 8
Remaining card: 4
Discarded cards: 1, 3, 5, 2, 6
Remaining card: 4
```
程式碼:
```cpp=
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (true) {
int n;
cin >> n;
if (n == 0) break;
queue<int> q;
for (int i = 1; i <= n; ++i) {
q.push(i);
}
vector<int> discarded;
if (n > 1) {
while (q.size() > 1) {
// 把最上面的牌丟掉:
discarded.push_back(q.front());
q.pop();
// 再把目前最上面的牌移到最後面
int top = q.front();
q.pop();
q.push(top);
}
}
cout << "Discarded cards:";
if (!discarded.empty()) {
cout << ' ';
for (size_t i = 0; i < discarded.size(); ++i) {
cout << discarded[i];
if (i + 1 < discarded.size()) {
cout << ", ";
}
}
}
cout << "\n";
cout << "Remaining card: " << q.front() << "\n";
}
return 0;
}
```
2. Uva 540:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=481
zerojudge:https://zerojudge.tw/ShowProblem?problemid=e564
```cpp=
#include <iostream>
#include <queue>
#include <unordered_map>
#include <string>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int scenario = 1;
while (true) {
int t;
cin >> t;
if (t == 0) break;
unordered_map<int, int> elementToTeam;
for (int i = 1; i <= t; ++i) {
int n;
cin >> n;
for (int j = 0; j < n; ++j) {
int x;
cin >> x;
elementToTeam[x] = i;
}
}
cout << "Scenario #" << scenario << "\n";
scenario++;
unordered_map<int, queue<int>> teamQueues;
queue<int> teamOrder;
string command;
while (cin >> command) {
if (command == "STOP") break;
if (command == "ENQUEUE") {
int x;
cin >> x;
int team = elementToTeam[x];
if (teamQueues[team].empty()) {
teamOrder.push(team);
}
teamQueues[team].push(x);
} else if (command == "DEQUEUE") {
int frontTeam = teamOrder.front();
cout << teamQueues[frontTeam].front() << "\n";
teamQueues[frontTeam].pop();
if (teamQueues[frontTeam].empty()) {
teamOrder.pop();
}
}
}
cout << "\n";
}
return 0;
}
```
雙端佇列(Deque)
---
deque(double-ended queue) 唸作 [dɛk],並不唸作 [di:kju:],因為會很容易跟 dequeue 搞混,dequeue 是指將元素從 queue 中移除,與雙端佇列的 deque 差多了。
deque 有點像是 stack 和 queue 的結合體,因為它可以從頭尾放入跟移除元素。
:::info
Deque 練習
來實作 Deque 吧!這一次,需要支援四個操作:
1. POP_FRONT 和 POP_BACK,分別代表要從前面和後面拿出東西出來並輸出拿出來的東西的值,如果沒有東西可以拿的話,那就輸出 empty!。
2. PUSH_FRONT $x$ 和 PUSH_BACK $x$ ,分別代表要從前面和後面插入一個值為 $x$ 的物體。並且保證總共不會超過 $N$ 個操作。
:::spoiler 條件限制
* $N \leq 10^{5}$
:::
```cpp=
// from NTU CPC Guide
int deq[maxn], Front = 0, Back = 0;
void PUSH_FRONT(int x) {
if (--Front < 0) Front += maxn;
deq[Front] = x;
}
void PUSH_BACK(int x) {
if (++Back >= maxn) Back -= maxn;
deq[Back] = x;
}
void POP_FRONT(){
if (Front == Back)
cout << "empty!\n";
else {
if (++Front >= maxn) Front -= maxn;
cout << deq[Front] << endl;
}
}
void POP_BACK(){
if (Front == Back)
cout << "empty!\n";
else {
if (--Back < 0) Back += maxn;
cout << deq[Back] << endl;
}
}
```
上述這些函數,在 C++ 中 STL 也可做到。
使用前需要引入標頭檔 `#include <deque>`,建立 deque 方式與前面建立 queue 相同。
基本操作如下:
* push_back():將資料堆入雙端佇列尾端。
* push_front():將資料堆入雙端佇列前端。
* pop_back():移除雙端佇列最後一個元素。
* pop_front():移除雙端佇列最前面的元素。
* insert():插入元素於雙端佇列中。
* erase():移除某個位置的元素,也可移除某一段範圍的元素。
* clear():清空容器裡所有元素。
* empty():回傳雙端佇列是否為空。
* size():回傳雙端佇列目前的長度。
* front():存取並回傳雙端佇列最前面的元素。
* back():存取並回傳雙端佇列尾端的元素。