資料結構

講師 王政祺

課程大綱(一)

  • 什麼是資料結構?

  • Queue

  • Stack

  • Deque

  • Linked-list

課程大綱(二)

  • Standard Template Library

    • Vector, Pair, Stack, Queue, Deque

  • Heap (priority_queue)

  • Set

  • Map

  • Disjoint Set

是的 其實這是語法課

課程目的

  • 理解常見資料結構的運作原理

  • 能夠實作基礎的資料結構

  • 獲得好用的工具 - STL

  • 擔任這次營隊裡最簡單的課程

自我介紹

  • 王政祺,casperwang

  • CKMSC36th => NTU CSIE B10

  • 2020, 2021 資奧二階選訓營

  • 2017 APCS 5/4 + 2020 APCS 4/5

什麼是資料結構?

資料結構

Data Structure

什麼是資料結構?

  • 是電腦中儲存、組織資料的方式

  • 好的資料處理方式,能讓程式節省時間與空間

  • 例如:「陣列」就是一種資料結構

這很重要嗎?

Algorithms + Data Structures = Programs

Niklaus Wirth,

the designer of Pascal

台大有一門課叫  資料結構與演算法

常見的資料結構

  • Stack、Queue、Deque

  • Linked-list

  • Heap、Set、Map

  • disjoint set

  • ......

Stack

考慮一個很簡單的問題

怎麼拿到藍色那本書?

依序把紅、黃、綠色的書拿起來

得到藍色的書

再依序把綠、黃、紅色的書放回去

Stack

堆疊

Empty

Stack

堆疊

加入新資料

1

Stack

堆疊

加入新資料

1

2

Stack

堆疊

刪除最頂端資料

1

Stack

堆疊

加入新資料

1

3

Stack

堆疊

3

4

1

加入新資料

Stack

堆疊

刪除最頂端資料

3

1

Stack

堆疊

刪除最頂端資料

1

Stack

堆疊

刪除最頂端資料

stack 的功能

  • 存取排在 stack 最頂端的資料

  • 刪除排在 stack 最頂端的資料

  • 新增資料到 stack 的最頂端

用陣列來實作吧!

程式要支援以下功能

  • top() 回傳 stack 最頂端的值

  • pop() 刪除 stack 最頂端的資料

  • push() 將一個新的值加入 stack

  • size() 回傳 stack 的大小

#16

用一個陣列代表 stack

  • 假設任意時刻 stack 裡的資料筆數不會超過陣列大小

用一個變數 now 紀錄頂端位置

  • 如果 now = 0, 代表 stack 是空的

  • push 時 now++

  • pop 時 now--

9 9 8 2 4 4 3

now

用一個變數 now 紀錄頂端位置

  • 如果 now = 0, 代表 stack 是空的

  • push 時 now++

  • pop 時 now--

9 9 8 2 4 4 3 5

now

用一個變數 now 紀錄頂端位置

  • 如果 now = 0, 代表 stack 是空的

  • push 時 now++

  • pop 時 now--

9 9 8 2 4 4 3

now

struct Stack{
    int arr[MAXN], now;
    Stack() : now(0) {}
    int top() { //回傳stack最頂端的值
        return arr[now-1];
    }
    void pop() { //刪除stack最頂端的資料
        now--;
    }
    void push(int val) { //將一個新的值加入stack的最頂端
        arr[now++] = val;
    }
    int size() { //回傳stack的大小
        return now;
    }
};

Stack 的特性

  • 只能從最頂端存取、刪除、新增資料

  • 後進先出(Last In First Out, LIFO)

Queue

先到的先拿!

Queue

佇列

queue 的功能

  • 存取排在queue最前端的資料

  • 刪除排在queue最前端的資料

  • 新增資料到queue的最後端

實作

程式要支援以下功能

  • front() 回傳 queue 最前端的值

  • pop() 刪除 queue 最前端的資料

  • push() 將一個新的值加入 queue 的最後端

  • size() 回傳 queue 的大小

#12

跟 stack 類似的方法

  • 用變數 head, tail 紀錄目前 queue 的開頭、結尾位置

  • push 時 tail++

  • pop 時 head++

跟 stack 類似的方法

  • 用變數 head, tail 紀錄目前 queue 的開頭、結尾位置

  • push 時 tail++

  • pop 時 head++

9 7 9 3 2 3 8

Head

Tail

跟 stack 類似的方法

  • 用變數 head, tail 紀錄目前 queue 的開頭、結尾位置

  • push 時 tail++

  • pop 時 head++

7 9 3 2 3 8

Head

Tail

跟 stack 類似的方法

  • 用變數 head, tail 紀錄目前 queue 的開頭、結尾位置

  • push 時 tail++

  • pop 時 head++

7 9 3 2 3 8 4

Head

Tail

有沒有發現什麼問題?

如果 push, pop 超過一定數量後就會壞掉

怎麼解決?

碰到尾巴的話,就循環從頭再來一次!

  • 如果 tail 碰到末端,就從頭開始循環

  • head 也是比照辦理

7 9 3 2 3 8 4

Head

Tail

碰到尾巴的話,就循環從頭再來一次!

  • 如果 tail 碰到末端,就從頭開始循環

  • head 也是比照辦理

7 9 3 2 3 8 4 6

Head

Tail

碰到尾巴的話,就循環從頭再來一次!

  • 如果 tail 碰到末端,就從頭開始循環

  • head 也是比照辦理

9 3 2 3 8 4 6

Head

Tail

碰到尾巴的話,就循環從頭再來一次!

  • 如果 tail 碰到末端,就從頭開始循環

  • head 也是比照辦理

2 9 3 2 3 8 4 6

Head

Tail

struct Queue{
    int arr[MAXN], head, tail;
    Queue() : head(0), tail(0) {}
    int front() { //回傳queue最前端的值
        return arr[head];
    }
    void pop() { //刪除queue最前端的資料
        head++;
        if (head == MAXN) head = 0;
    }
    void push(int val) { //將一個新的值加入queue的最後端
        arr[tail++] = val;
        if (tail == MAXN) tail = 0;
    }
    int size() { //回傳queue的大小
        return (tail + MAXN - head) % MAXN;
    }
};

Queue 的特性

  • 只能從最前端存取、刪除資料

  • 只能從最後端新增資料

  • 先進先出(First In First Out, FIFO)

Deque

如果 想同時支援從前後加東西跟刪東西呢?

到底要用 stack 還是 queue?

Deque

 (Double-ended queue)

雙端佇列

關於念法

  • 有些人喜歡念成「de-queue」

關於念法

  • 有些人喜歡念成「de-queue」

  • usually pronounced like "deck" —— by CPP reference

deque 的功能

  • 存取、刪除排在deque最前端的資料

  • 存取、刪除排在deque最後端的資料

  • 新增資料到deque的最前端、最後端

實作

struct Deque{
    int arr[MAXN], head, tail;
    Deque() : head(0), tail(0) {}
    int front() { //回傳deque最前端的值
        return arr[head];
    }
    int back() { //回傳deque最後端的值
        if (tail == 0) tail = MAXN
        return arr[tail-1];
    }
    void pop_front() { //刪除deque最前端的資料
        head++;
        if (head == MAXN) head = 0;
    }
    void pop_back() { //刪除deque最後端的資料
        if (tail == 0) tail = MAXN;
        tail--;
    }
    void push_front(int val) { //將一個新的值加入deque的最前端
        if (head == 0) head = MAXN-1;
        arr[head--] = val;
    }
    void push_back(int val) { //將一個新的值加入deque的最後端
        arr[tail++] = val;
        if (tail == MAXN) tail = 0;
    }
    int size() { //回傳deque的大小
        return (tail + MAXN - head) % MAXN;
    }
};

程式要支援以下功能

  • front(), back() 詢問

  • pop_front(), pop_back() 刪除

  • push_front(), push_back() 加入

  • size()

#23

例題們

例題(一)

給定一個僅包含 '('、')' 的字串,問其是否為合法括弧字串。

 

範例:

  • " ( ) ( ( ) ( ) ) " 是一個合法括弧字串

  • " ( ) ( ( ( ) ( ) " 不是一個合法括弧字串

#17

Hint

什麼樣的字串是合法括弧字串?

例題(一)合法括弧字串

「每個左括弧都能夠找到右括弧與其互相配對,且不會有多餘的右括弧沒有配對到」

例題(一)作法

  • 由左到右把字元加到stack看看

  • 遇到 '(' 就 push

  • 遇到 ')' 就 pop

例題(一)作法

  • 由左到右把字元加到stack看看

  • 遇到 '(' 就 push

  • 遇到 ')' 就 pop

  • 如果 pop 的時候發現 stack 空了 => 非法字串

  • 如果 stack 最後不是空的 => 非法字串

例題(一)作法

  • 由左到右把字元加到stack看看

  • 遇到 '(' 就 push

  • 遇到 ')' 就 pop

  • 如果 pop 的時候發現 stack 空了 => 非法字串

  • 如果 stack 最後不是空的 => 非法字串

  • 其實甚至不需要 stack !?

例題(二)

給定一個包含'('、')'、'+'、'-'的運算式,計算該運算式的答案。

(保證該運算式合法)

 

範例:

  • " ( 5 + 4 ) - 3 " 的答案是 6
  • " ( 1 + 2 ) - ( 7 + 3 ) " 的答案是 -7

#18

Hint

  1. 可以從後面做回來
  2. 用兩個 stack 比較好實作

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

+
5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

) +
5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

) ) +
5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

) ) +
2 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

+ ) ) +
2 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 兩個 stack,一個紀錄符號、一個紀錄數值

  • 碰到 '(' 或結尾再做事!

+ ) ) +
3 2 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 遇到 '(' 了,開始一路計算到直到碰到 ')'

  • 每次把一個運算符號跟兩個數字取出,算完的結果丟回去

+ ) ) +
3 2 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 遇到 '(' 了,開始一路計算到直到碰到 ')'

  • 每次把一個運算符號跟兩個數字取出,算完的結果丟回去

) +
5 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 繼續做一樣的事情

- ) +
5 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 繼續做一樣的事情

- ) +
2 5 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 又碰到 '(' 了

- ) +
2 5 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 又碰到 '(' 了

+
-3 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 繼續做一樣的事情

- +
-3 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 繼續做一樣的事情

- +
11 -3 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 碰到結尾

- +
11 -3 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 碰到結尾

+
14 5

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)作法

  • 得到答案

19

11 - ( 2  - ( 3 + 2 ) ) + 5

例題(二)

還有一個小問題...

數字不是個位數怎麼辦?

例題(二)實作細節

  • 開一個變數紀錄目前的 digit 是10的幾次方,先處理好數字再丟進 stack 中

  • 遇到 '(' 往前計算的時候不是只算一次,是一路到 ')' 並將其 pop 出來為止!

for (int i = str.size()-1; i >= 0; i--) { // 由後往前!
    if (str[i] >= '0' && str[i] <= '9') {
        num = num + k * (str[i] - '0');
        if (i-1 >= 0 && str[i-1] >= '0' && str[i-1] <= '9') {
            k = k * 10; // 如果下一位還是數字的話
        } else {
            stk_num.push(num);
            num = 0, k = 1;
        }
    } else if (str[i] == '+' || str[i] == '-' || str[i] == ')') {
        stk_sym.push(str[i]); // 遇到正常運算符號就 push 進 stack
    } else if (str[i] == '(') {
        calc(); // 遇到 '(' 就回推
    }
}
calc(); // 結尾回推
cout << stk_num.top() << '\n';
void calc() {
    int a = stk_num.top(), b;
    stk_num.pop();
    while (stk_sym.size() && stk_sym.top() != ')') {
        b = stk_num.top();
        stk_num.pop();
        if (stk_sym.top() == '+') {
            a = a + b;
        } else if (stk_sym.top() == '-') {
            a = a - b;
        }
        stk_sym.pop();
    }
    if (stk_sym.size()) stk_sym.pop();
    // 若並非結尾,則將 ')' pop 出來
    stk_num.push(a); // 將運算結果放回 stack 中
}

例題(三)

給你一張長條圖每個位置的高度,問你能畫出的最大矩形面積。

(\(N \leq 10^5\),高度 \(\leq 10^9\) )

 

範例:

  • 2 3 4 3 1

8

5

#19

例題(三)

給你一張長條圖每個位置的高度,問你能畫出的最大矩形面積。

(\(N \leq 10^5\),高度 \(\leq 10^9\) )

 

範例:

  • 2 3 4 3 1:最大面積為 9

9

例題(三)

給你一張長條圖每個位置的高度,問你能畫出的最大矩形面積。

(\(N \leq 10^5\),高度 \(\leq 10^9\) )

 

範例:

  • 2 3 4 3 1:最大面積為 9

9

Hint

不知道從何下手的時候,可以先從複雜度較差的解開始想!

例題(三)直覺的作法

枚舉每段區間,然後看高度最高可以是多少

  • 正確性?

  • 複雜度?

例題(三)直覺的作法

枚舉每段區間,然後看高度最高可以是多少

  • 正確性?應該不用懷疑吧

  • 複雜度?\(O(N^3)\)

    • 區間總共有 \(\frac{N(N+1)}{2}\) 個(why?

    • 高度至多只能到最矮的那個 \(\Rightarrow\) 掃一遍區間找最小值

例題(三)再想多一點...

「一塊區間的高度至多只能到最矮的那個」

\(\Rightarrow\) 重點不是區間,是「最矮的那個」

例題(三)再想多一點...

「一塊區間的高度至多只能到最矮的那個」

\(\Rightarrow\) 重點不是區間,是「最矮的那個」

例題(三)再想多一點...

「一塊區間的高度至多只能到最矮的那個」

\(\Rightarrow\) 重點不是區間,是「最矮的那個」

例題(三)再想多一點...

「一塊區間的高度至多只能到最矮的那個」

\(\Rightarrow\) 重點不是區間,是「最矮的那個」

\(\Rightarrow\) 從枚舉區間,變成枚舉每個 bar 的高度

例題(三)再想多一點...

「一塊區間的高度至多只能到最矮的那個」

\(\Rightarrow\) 重點不是區間,是「最矮的那個」

\(\Rightarrow\) 從枚舉區間,變成枚舉每個 bar 的高度

\(\Rightarrow\) 如果我是最低的,那往左往右最多可以延伸多少?

例題(三)再想多一點...

「一塊區間的高度至多只能到最矮的那個」

\(\Rightarrow\) 重點不是區間,是「最矮的那個」

\(\Rightarrow\) 從枚舉區間,變成枚舉每個 bar 的高度

\(\Rightarrow\) 如果我是最低的,那往左往右最多可以延伸多少?

\(\Rightarrow\) 只要分別找到左右兩邊第一個比我小的!

例題(三)問題轉換

給定序列,對每一項分別找到左右離他最近且比他小的值。

(\(N \leq 10^5\),值域 \(\leq 10^9\) )

例題(三)問題轉換

給定序列,對每一項分別找到左右離他最近且比他小的值。

(\(N \leq 10^5\),值域 \(\leq 10^9\) )

  • 其實等價於對每一項找到左邊離他最近且比他小的值,然後再把序列反轉過來做一次

  • 複雜度?

例題(三)問題轉換

給定序列,對每一項分別找到左右離他最近且比他小的值。

(\(N \leq 10^5\),值域 \(\leq 10^9\) )

  • 其實等價於對每一項找到左邊離他最近且比他小的值,然後再把序列反轉過來做一次

  • 複雜度?\(O(N^2)\)

    • ​但有沒有可能再做得更好一點?

例題(三)觀察

來畫張圖自己玩玩看

例題(三)觀察

來畫張圖自己玩玩看

例題(三)觀察

例題(三)觀察

假設我們還不知道答案

例題(三)觀察

但答案絕對不會是它

Why?

例題(三)觀察

例題(三)觀察

例題(三)觀察

例題(三)觀察

例題(三)觀察

例題(三)觀察

Hint

考慮每一項在什麼時間點以後注定不可能成為答案

例題(三)作法

 「如果右邊有東西不比我大,那我就不可能是答案」

 

例題(三)作法

 「如果右邊有東西不比我大,那我就不可能是答案」

 \(\Rightarrow\) 我們要用 stack 維護這樣的「單調性」

 

例題(三)作法

 「如果右邊有東西不比我大,那我就不可能是答案」

 \(\Rightarrow\) 我們要用 stack 維護這樣的「單調性」

 \(\Rightarrow\) stack 裡頭的每一項一定比前一項大

 

例題(三)作法

 「如果右邊有東西不比我大,那我就不可能是答案」

 \(\Rightarrow\) 我們要用 stack 維護這樣的「單調性」

 \(\Rightarrow\) stack 裡頭的每一項一定比前一項大

 

while (size() > 0 && top() >= value[idx]) { // top比我還大
    pop(); // 把top丟掉
}
if (size() > 0) ans[idx] = top();
// 目前的top會是比我小且離我最近的那個
push(value[idx]); // 記得把值丟進stack

例題(三)思路、步驟整理

  1. 要找最大矩形,可以「枚舉每個值作為最小值」向外延伸

  2. 將向左、向右拆開成兩個問題

  3. 題目轉化為「找到左邊第一個比我小的值」

  4. 一個值不可能成為最小值的條件(右邊出現比它小的值)

  5. 利用 stack 維護這樣的「單調遞增」

例題(四)

有一群各自屬於不同小圈圈的人要排隊,當有人想加入隊伍時,若隊伍前方有同個小圈圈的人,則他會直接插進去,否則就要從最後面開始排隊。

 

操作包含:

  • 編號 \(id\) 、屬於小圈圈 \(c_i\) 的人想加入隊伍

  • 詢問隊伍最前方人的 \(id\),並讓他離開隊伍

#13

例題(四)

範例輸入:

    "add"  3  1

    "add"  2  2

    "add"  1  1

    "pop"

    "pop"

範例輸出:

    3

    1

例題  作法

  • 開一個紀錄小圈圈順序的 queue

  • 對每個小圈圈各別再開一個 queue

  • 加入跟刪除的時候要怎麼處理就交給各位了!

Linked List

如果想把陣列中的某一項刪掉?

你會怎麼做?複雜度?

Linked List

連結串列

linked list 的概念

  • 對於每個資料紀錄前後資料的位置

  • 可以 \(\mathcal{O}(1)\) 加入、刪除特定資料

  • 不支援 random-access

    • 不能 \(\mathcal{O}(1)\) 存取指定 index 的資料

例題

維護一個可變動序列中,每個值所在位置的前後數字。

(值域 \(\leq 10^5\),數字不重複)

 

操作包含:

  • 將一個數字 \(A\) 加入序列中的某個位置(ex. 數字 \(B\) 和 \(C\) 之間)

  • 從序列中刪除數值 \(A\),其餘順序不變

  • 詢問數字 \(A\) 所在位置的前後數字

作法

  • 對於每個數字紀錄前面是誰、後面是誰

  • 加入數字 \(A\) 到兩個數字 \(B\)、\(C\) 中間時
    • \(A\) 的前一個設為 \(B\)、後一個設為 \(C\)
    • \(B\) 的後一個改為 \(A\)
    • \(C\) 的前一個改為 \(A\)

作法

  • 刪除數字 \(A\) 時,假設 \(A\) 的前一個為 \(B\)、後一個為 \(C\)

    • 把 \(B\) 的後一個改為 \(C\)

    • 把 \(C\) 的前一個改為 \(B\)

  • 詢問時即可直接得到答案

資結習題整理(一)

STL

Standard Template Library

  • C++ 標準模板庫

  • 提供了各種容器的實作(包含 stack, queue, deque)

  • 其實就是有人幫你把一些感覺比較複雜但又會常常用到的東西寫好,讓你寫程式可以輕鬆一點(x

先插播一下

#include <bits/stdc++.h>

  • 如果有人沒用過可以參考(?)

  • 非必要,不過用了會變得比較輕鬆

  • 使用mac或其他電腦裡沒有內建這個東東的人:連結

以下正式進入語法時間

對大家而言可能是新的內容,回去練習的時候有不熟悉的語法要回來察看是很正常的!

vector

#include <vector>

  • 向量,不過可能要再更廣義一點

  • 可變長度的陣列

宣告

vector <int> vec;

也可以 vector 包 vector

vector<vector<int>> vec2;

加入數值到最後面

vec.push_back(123);

詢問當前 vector 大小

cout << vec.size() << endl;

也可以在一開始就決定好大小

vector <int> vec(50); // 方法一
vec.resize(50); // 方法二

將 vector 排序

sort(vec.begin(), vec.end());

將 vector 反轉

reverse(vec.begin(), vec.end());

遍歷 vector 中的元素

// 方法一
for (int i = 0; i < vec.size(); i++)
    cout << vec[i] << endl;
// 方法二
for (int val : vec)
    cout << val << endl;

pair

#include <utility>

  • 裡頭有很多小工具,不過今天只討論 pair

  • 相當於只有兩個參數的 struct

宣告

pair<int,int> p;

取值

p = make_pair(5, 3);
cout << p.first << ' ' << p.second << endl;

:怎麼感覺寫起來沒有比較簡單

你可以考慮這樣做

  • 適合很懶惰的人(ex. 講師本人)

  • 非必要,僅供參考

#include <bits/stdc++.h>
#define pb push_back
#define pii pair<int,int>
#define ff first
#define ss second
#define 2qbx soft // 這個是亂打的,你想定義什麼都可以
using namespace std;

stack

#include <stack>

  • 其實就跟昨天的內容一模一樣

stack <int> stk;
for (int i = 0; i < 10; i++) stk.push(i);
while (stk.size()) {
    cout << stk.size() << " : ";
    cout << stk.top() << '\n';
    stk.pop();
}

queue

#include <queue>

queue <int> que;
for (int i = 0; i < 10; i++) que.push(i);
while (que.size()) {
    cout << que.size() << " : ";
    cout << que.front() << '\n';
    que.pop();
}

deque

#include <deque>

deque <int> deq;
for (int i = 0; i < 10; i++) {
    if (i % 2) deq.push_back(i);
    else deq.push_front(i);
}
cout << deq.size() << '\n';
cout << deq.front() << ' ' << deq.back() << '\n';

如果昨天還有作業沒寫的話

就用 STL 來試試看吧!

Heap

想像有一個神奇的黑盒子,可以一直丟東西進去,然後隨時把最大的拿出來

Heap

堆積

9

7

5

3

6

4

1

heap 堆積

  • 一個能維護極值的資料結構

  • 功能

    • 存取、刪除 heap 最頂端的資料
      (最頂端的資料為按照定義的比較函數所得到的極值)

    • 新增資料到 heap 中

  • 各項操作複雜度皆為 \(\mathcal{O}(\log N)\)(\(N\) 為 heap 大小)

實作

實作

#include <queue>

  • STL 提供的 heap 叫做 priority_queue

  • 注意預設的極值是最大值

#29

宣告

priority_queue <int> pq;

新增

pq.push(103);

詢問極值

cout << pq.top() << endl;

刪除極值

pq.pop();

:想要不同的極值怎麼辦

如果是想要最小值的話

priority_queue <int, vector<int>, greater<int>> pq;

宣告時加入比較函數

struct cmp {
    bool operator()(int a, int b) {
        return a%100 < b%100;
    }
};
priority_queue <int, vector<int>, cmp> pq;

例題

例題

請維護以下操作:

  • 將一個值加入目前的集合中

  • 輸出最大值,並把他從集合中刪除

  • 輸出最小值,並把他從集合中刪除

(值域 \(\leq 10^5\),數字可重複)

#30

作法

直接開兩個 priority_queue 會出現什麼問題?

作法

直接開兩個 priority_queue 會出現什麼問題?

\(\Rightarrow\) 沒有辦法在最小值的 priority_queue 中刪除最大值

作法

沒有辦法在最小值的 priority_queue 中刪除最大值

\(\Rightarrow\) 多開一個陣列維護每個值的出現次數

作法

沒有辦法在最小值的 priority_queue 中刪除最大值

\(\Rightarrow\) 多開一個陣列維護每個值的出現次數

\(\Rightarrow\) 每次一直刪,直到刪掉一個目前存在於集合中的數值

    (也就是還沒被刪過的數值)

Heap 的原理(概要)

Tree 樹

  • 將 \(N\) 個點用 \(N-1\) 條邊相連

  • 節點?子節點?

  • 有根 or 無根?

9

7

5

3

6

1

4

8

2

Binary Tree 二元樹

  • 有根樹

  • 每個節點有至多兩個子節點

9

7

5

3

6

1

4

8

Complete Binary Tree 完全二元樹

  • 每一層在填滿節點之前不會進入下一層

  • \(N\) 個節點的樹至多只有 \(\log_2 N\) 層

9

7

5

3

6

4

1

Binary Heap 堆積

  • 用 Complete Binary Tree 來維護 Heap

  • 設法讓每個節點的值大於其子節點

9

7

5

3

6

4

1

Binary Heap - 存取極值

\(\mathcal{O}(1)\) 直接拿最頂端那個就好了

 

Binary Heap - 加入

  1. 加入一個點時,先把他排在下一個空位置

  2. 不管怎樣直接把他換到最上面(要把原本在最上面的往下擠

  3. while (任意左右子節點比他小) 就往下換

交換次數大約是樹的深度!

 

Binary Heap - 刪除

  1. 把最上面的點刪掉

  2. 不管怎樣直接把最下面的某個點換到最上面

  3. while (任意左右子節點比他小) 就往下換

交換次數大約也是樹的深度!

 

Binary Heap

  • 我們發現在這樣的結構裡頭加入、刪除的複雜度是由樹的深度決定的

  • 樹的深度是 \(\log_2 N\),因此這些操作的    複雜度也是 \(\log_2 N\)

9

7

5

3

6

4

1

Set

Set

集合(元素不重複)

9

7

5

3

6

4

1

set 的功能

  • 插入資料

  • 刪除資料

  • 尋找資料

  • 各項操作複雜度皆為 \(\mathcal{O}(\log N)\)(常數有點大)

#include <set>

  • 和數學上的集合不一樣的地方 - set 是有序的!

#33

宣告

set <int> S;

新增

S.insert(51);

刪除

S.erase(51); // 會順便回傳下一個元素的iterator

尋找

S.find(51) // 回傳該元素的iterator
S.count(51) // 回傳該元素是否存在

什麼是 iterator?

  • 迭代器

  • 一種資料形態,其意義為記憶空間位址,使用方法如指標

  • 在 set 中可以利用 prev(), next() 來得到前後的 iterator

遍歷 set 中的元素

// 方法一
for (auto it = S.begin(); it != S.end(); it++)
    cout << *it << endl;
// 方法二
for (int val : S)
    cout << val << endl;

尋找值大於等於自己的第一個 iterator

auto it = S.lower_bound(40);

尋找值大於自己的第一個 iterator

auto it = S.upper_bound(40);

尋找值小於等於自己的第一個 iterator

auto it = prev(S.upper_bound(40));

尋找值小於自己的第一個 iterator

auto it = prev(S.lower_bound(40));

Map

Map

映射(key不重複)

9

7

3

4

4

6

map 的功能

  • 用 key 去索引 value

  • 插入、修改、刪除、尋找

  • 各項操作複雜度皆為 \(\mathcal{O}(\log N)\)(常數有點大)

#include <map>

  • map 也是有序的(按照 key 排序)

#36

宣告

map <string,int> mp; // string打到int

新增 or 修改

mp["confirmed_case"] = 13240;

刪除, 同 set

mp.erase("abc");

尋找, 同 set

mp.find("abc");
mp.count("abc");

遍歷 map 中的元素

// 方法一
for (auto it = mp.begin(); it != mp.end(); it++)
    cout << it->first << ' ' << it->second << endl;
// 方法二
for (auto [a, b] : mp)
    cout << a << ' ' << b << endl;

例題

給定一個長度為 \(N\) 的黑白格子帶,每次操作會將一個目前是白色的位置塗為黑色,請在每次操作後輸出當前最長及最短連續黑色格子的長度。

(\(N \leq 10^5\)、操作數量 \(\leq 10^5\))

#26

例題

範例:

Ans:3, 1

例題

範例:

Ans:3, 1

Ans:3, 2

例題

範例:

Ans:3, 1

Ans:3, 2

Ans:6, 2

作法

  • 首先,我們要用一個map來記錄每個連續黑色長度的數量

  • 對於每塊連續黑色的格子,我們在最左端及最右端紀錄這段連續黑色的長度。

  • 每次將一個格子塗黑時,我們檢查它的左右是否已經是連續黑色的格子

作法

  • 每次將一個格子塗黑時,我們檢查它的左右是否已經是連續黑色的格子,若是,則將它們合併:

    • 維持讓新連續黑色塊的最左端及最右端能夠紀錄這段連續黑色的長度

    • 在 map 上更新這些資訊

作法

  • 每次將一個格子塗黑時,我們檢查它的左右是否已經是連續黑色的格子,若是,則將它們合併:

    • 維持讓新連續黑色塊的最左端及最右端能夠紀錄這段連續黑色的長度

    • 在 map 上更新這些資訊

  • 每次操作完後,因為 map 是有序的,我們可以從中得到最大及最小的 key,也就是最長及最短的連續黑色格子。

實作細節

  • 當連續黑色格子長度改變在 map 上更新時,要記得把原本的長度扣回去,如果扣完後發現已經不存在該種長度的連續黑色格子,就要將它從 map 中刪掉

DSU

註:額外補充,之後圖論課才會用到

Disjoint Set

併查集

9

7

5

3

6

4

1

Disjoint Set 併查集

  • 一種用來處理不交集(disjoint set)的資料結構

  • 功能

    • 詢問兩個元素是否在同一個集合

    • 合併兩個元素所在的集合

  • 在有優化的情況下各項操作複雜度皆為 \(O(\alpha(N))\)  (\(\alpha \)為反阿克曼函數)

Disjoint Set 併查集

  • 我們將每個集合以一棵樹來表示

  • 對於每個節點只記錄他父節點的參照

  • 我們之後將使用每個集合的根節點來代表那個集合

Disjoint Set 併查集 - 初始化

  • 開一個陣列,每個 index 代表一個節點

  • 每個節點紀錄其父節點,初始值設為自己的 index

Disjoint Set 併查集 - 查詢

  • 想尋找一個節點所在的集合,我們就將這個節點一直往上指,直到碰到代表這個集合的根節點

  • 因為 \(a\)、\(b\) 最終都指向 \(r\),因此他們在同一個集合!

r

b

a

Disjoint Set 併查集 - 查詢

  • 想尋找一個節點所在的集合,我們就將這個節點一直往上指,直到碰到代表這個集合的根節點

int fnd(int now) {
    if (dsu[now] != now)
        return fnd(dsu[now]);
    else // 代表遇到根了!
        return now;
}

Disjoint Set 併查集 - 查詢的複雜度分析

  • 如果這棵樹長這樣

Disjoint Set 併查集 - 查詢的複雜度分析

  • 如果這棵樹長這樣

  • 每次查詢最差是 \(O(N)\),TLE

Disjoint Set 併查集 - 路徑壓縮

  • 在往上尋找根節點時,同時改變樹的結構,直接讓過程中經過的節點都指向根

Disjoint Set 併查集 - 路徑壓縮

  • 在往上尋找根節點時,同時改變樹的結構,直接讓過程中經過的節點都指向根

  • 想像原本這種最差的 \(O(N)\) 情形會怎麼樣?

Disjoint Set 併查集 - 路徑壓縮

  • 程式碼剛好多十個字元而已xd

int fnd(int now) {
    if (dsu[now] != now)
        return dsu[now] = fnd(dsu[now]);
    else
        return now;
}

Disjoint Set 併查集 - 合併

  • 想合併兩個節點所在的集合,我們只要找到各自集合的根節點,再將其中一個指向對方就好!

r

b

a

t

c

d

Disjoint Set 併查集 - 合併

  • 想合併兩個節點所在的集合,我們只要找到各自集合的根節點,再將其中一個指向對方就好!

void Union(int a, int b) {
    dsu[fnd(a)] = fnd(b);
    // 將「a的根節點的父節點」設為「b的根節點」
}

Disjoint Set 併查集 - 一點優化

  • 如果多維護一下每個集合的 size,合併時讓小的集合往大的合併複雜度會比較好

void Union(int a, int b) {
    a = fnd(a), b = fnd(b);
    if (a == b) return;
    if (sze[a] > sze[b]) {
        sze[a] += sze[b];
        dsu[b] = a;
    } else {
        sze[b] += sze[a];
        dsu[a] = b;
    }
}

資結習題整理(二)

The End

APCS Camp 資料結構

By CasperWang

APCS Camp 資料結構

APCS Camp 資料結構

  • 648