# Linked list (鏈結串列) ## B4 start 因為好像沒人在乎他,我又剛好在 [$\text{OI wiki}$](https://oi-wiki.org/) 上看到類似的東西,所以來做一份講義 ## Intro 先備知識: 指標、$\text{struct}$ ![alt image](https://i.imgur.com/kZhq9QH.png) 差不多長這樣,每個人都有一個指標指向另一塊 跟人體蜈蚣一樣 優點是因為只要改變各元素之間指標的連接關係 所以只看插入和刪除的話複雜度是 $\mathcal{O}(1)$ 還有把兩個串接起來也是 $\mathcal{O}(1)$ 缺點也很明顯: 沒辦法 $\text{Random access}$ 查詢需要 $\mathcal{O}(n)$ 的複雜度 可以拿來實作 $\text{Stack}$ 或 $\text{Queue}$ 稍微爆改一下也能弄出一棵二元樹之類的 (詳見還沒做出來的講義)([喔做出來了](https://hackmd.io/@Viecon-342524/S1ZA5GM6s)) ## Smart pointer 剛好要用到指標所以順便介紹一下這個 為了要解決 $\text{memory leak}$ 的問題,$\text{C++}$ 就在 $\text{STL}$ 裡面引進了「$\text{smart pointer}$」的概念 分成以下三種: * `unique_ptr` :確保一份資源(被配置出來的記憶體空間)只會被一個 `unique_ptr` 指;當 `unique_ptr` 物件消失時,就會自動釋放資源。 * `shared_ptr` :有 $\text{reference count}$ 機制,當沒人指向一塊空間時他就會自動被釋放 * `weak_ptr` :跟 `shared_ptr` 很像,差別在不會計入 $\text{reference count}$ ### 宣告 原本 ```cpp= int *ptr = new int(10); cout << *ptr << endl; delete ptr; ``` 變成 ```cpp= unique_ptr<int> ptr(new int(10)); cout << *ptr << endl; ``` ## Singly linked list 簡單好寫,也挺暴力的 要做持久化也方便,缺點就是不知道能幹嘛 ```cpp= struct Linked_list { struct node { LL value; shared_ptr<node> next; node(LL _value) : value(_value) {} }; shared_ptr<node> head; LL sz = 0; void insert(LL p, LL v) { p = min(p, sz); if (p == 0) { auto tmp = head; head = shared_ptr<node>(new node(v)); head->next = tmp; return; } auto cur = head; for (int i = 0; i < p - 1; i++) cur = cur->next; auto tmp = cur->next; cur->next = shared_ptr<node>(new node(v)); cur->next->next = tmp; } bool erase(LL p) { if (sz == 0) return 0; if (p >= sz) return 0; if (p == 0) { head = head->next; return 1; } shared_ptr<node> cur = head; for (int i = 0; i < p - 1; i++) cur = cur->next; cur->next = cur->next->next; return 1; } LL at(LL p) { if (p >= sz) { return -1; } shared_ptr<node> cur = head; for (int i = 0; i < p; i++) cur = cur->next; return cur->value; } }; ``` 沒測過,也不知道對不對 ## Doubly linked list ![alt image](https://i.imgur.com/S8aM7pI.png) 就這樣,似乎也沒什麼 總之不是今天的重點 ### 補充 : XOR linked list 可以在降低空間複雜度的情況下達到和 $\text{Doubly linked list}$ 一樣的目的 在每個節點儲存 **前一個節點的位址** $\oplus$ **後一個節點的位址** 這樣就可以省下一個指標大小的空間 ## Circular linked list 也是有單向跟雙向 ![alt](https://i.imgur.com/YDWvYIJ.png) ![alt](https://i.imgur.com/tAdu2NY.png) 首尾相連 也不知道能幹嘛 但好像滿多地方都有介紹就順便講一下 ## std::list 好用的東西 不用再為指標煩惱 (變成為迭代器煩惱) [**cppreference**](https://en.cppreference.com/w/cpp/container/list) 成員函式之類的 除了 $\text{splice}$ 以外基本上都差不多 ### 應用 [**d027: 習題 Q-3-3. 加減乘除**](https://judge.tcirc.tw/ShowProblem?problemid=d027) $5\times 3+2-4\times 5$ 先乘除後加減 想要把「$5\times 3$」換成 $15$,把「$4\times 5$」換成 $20$ 所以把每個字都塞到 $\text{list}$ 裡面,找到「\*、/」就把它前後的數字收集起來。算完答案之後把前後都刪掉,把自己這格改成答案。 $15+2-20$ 再做一次 $-3$ ```cpp= struct qu { LL k; // number char t; // operator }; void sol() { string s; cin >> s; LL n = s.size(); list<qu> lis; for (int i = 0; i < n; i++) { if (isdigit(s[i])) { lis.pb({s[i] - '0', 'k'}); } else { lis.pb({-1, s[i]}); } } for (auto i = lis.begin(); i != lis.end(); i++) { if (i->t == '*' || i->t == '/') { auto a = i, b = i; a--, b++; if (i->t == '*') i->k = a->k * b->k; else i->k = a->k / b->k; i->t = 'O'; lis.erase(a); lis.erase(b); } } for (auto i = lis.begin(); i != lis.end(); i++) { if (i->t == '+' || i->t == '-') { auto a = i, b = i; a--, b++; if (i->t == '+') i->k = a->k + b->k; else i->k = a->k - b->k; i->t = 'O'; lis.erase(a); lis.erase(b); } } cout << lis.begin()->k << endl; } ``` 類題: [**d040: 普通分數的四則運算**](https://judge.tcirc.tw/ShowProblem?problemid=d040) --- [**NEOJ - 一天遊戲只能一小時**](https://neoj.sprout.tw/problem/25/) ```cpp= #include <bits/stdc++.h> using namespace std; int main() { ios_base::sync_with_stdio(false); cin.tie(0); int n, m; cin >> n >> m; vector<list<int>> v(n); while (m--) { string s; cin >> s; if (s[0] == 'A') { int a, b; cin >> a >> b; v[a - 1].push_back(b); } if (s[0] == 'L') { int a; cin >> a; a--; if (v[a].empty()) { cout << "queue " << a + 1 << " is empty!" << endl; } else { v[a].pop_front(); } } if (s[0] == 'J') { int a, b; cin >> a >> b; a--; b--; auto it = v[b].end(); v[b].splice(it, v[a]); } } for (int i = 0; i < n; i++) { cout << "queue " << i + 1 << ":"; if (v[i].empty()) { cout << " empty" << endl; } else { for (auto j : v[i]) { cout << " " << j; } cout << endl; } } return 0; } ``` ## 蛤 其實接下來才是重點 ## 塊狀鏈表 ![alt image](<https://i.imgur.com/ThnmPQG.png> =400x) (找不到台灣的譯名) 如上圖,其實就是 $\text{linked list}$,只是每一格都存了一個大小為 $\sqrt n$ 的陣列 可以用 $\text{STL}$ 來偷懶 寫起來輕鬆,又有 $\mathcal{O}(\sqrt n)$ 插入/刪除/詢問的複雜度 實作時會將每塊大小設為一個定值,畢竟不可能每插入一個數字就重算一次 $\sqrt n$ ### 插入 插入有兩種方法 1. 正常的插入,直到有一塊大小 $\geq 2\sqrt n$ 時就把那塊分裂成兩塊 2. 每插入一個就把超過 $\sqrt n$ 的部分往後移 兩種應該都滿好寫的 這裡只寫了第一種方法 ```cpp= struct Unrolled_linked_list { LL C; LL sz = 0; list<vector<LL>> l; // list<list<LL>> 也可以 void insert(LL p, LL v) { auto it = l.begin(); while (1) { if (p - it->size() < 0) { it->insert(it->begin() + p, v); if (it->size() >= 2 * C) { auto tmp = it; tmp++; l.insert(tmp, vector<LL>(it->begin() + C, it->end())); it->rz(C); } break; } p -= it->size(); it++; } } }; ``` (一樣沒測過,不保證正確) ### 刪除 刪除的操作跟插入差不多,這裡就不再贅述 ||(其實是想偷懶)|| ## 有序鏈表 就是 $\text{linked list}$ ,但裡面的元素有排序好 (不就是退化的 $\text{BST}$ 嗎) 插入的時候稍微維護一下下就好,不難 顯然插入 刪除 詢問都是 $\mathcal{O}(n)$ 當然也可以配合塊狀鏈表把複雜度壓到 $\mathcal{O}(\sqrt n)$ 但這不是今天的重點 ## 跳表 (skip list) 這份講義的起源 我補習前亂翻 $\text{OI wiki}$ 看到的,因為覺得滿酷的所以做了這整份講義 我個人覺得他的概念有點像 $\text{treap}$ ,都是利用 $\text{Random}$ 來保證有好的複雜度 可以把它當作 $\text{set}$ 或 $\text{map}$ 來用 ### 結構 一個跳表由多層有序鏈表組成 最底層是原始的有序鏈表 對於第 $i$ 層的元素,有 $p$ 的機率會出現在第 $i+1$ 層 ( $p$ 是自己訂的一個常數,通常用 $0.25$) 查詢時從最頂層開始往右走,如果不能往右或右邊的比目標大就往下 一直到找到為止 ![alt image](https://i.imgur.com/hsvTwEW.png) ![alt image](https://i.imgur.com/ZRuNgCh.png) ### 時間複雜度 第 $i$ 層期望會有 $np^{i-1}$ 個元素 每一層的大小是底下那一層的 $p$ 倍 故整個跳表的期望層數為 $\log_{\frac{1}{p}} n$ 層 期望在第 $i$ 層走一步相當於在最底層走 $\frac{1}{p^{i-1}}$ 步 可以想像成把目標位置拆成 $\frac{1}{p}$ 進制 所以總共就是 $\log_{\frac{1}{p}} n$ 步 可得單次詢問的期望複雜度為 $\mathcal{O}(\log n)$ ### 空間複雜度 >第 $i$ 層期望會有 $np^{i-1}$ 個元素 故整個跳表的期望層數為 $\log_{\frac{1}{p}} n$ 層 就用力地把它們加起來 $\frac{n(1-p^{\log_{\frac{1}{p}} n})}{1-p}$ (等比級數公式) $=\frac{n-1}{1-p}$ 因為 $p$ 是常數,所以它的期望空間複雜度為 $\mathcal{O}(n)$ ### 實作 隨便寫一個 $\text{set}$ 試試 把節點訂好 ```cpp= struct node { LL value; list<node>::iterator down; node(LL _value, list<node>::iterator _down) : value(_value), down(_down) {} node(LL _value) : value(_value) {} }; ``` 懶得判邊界所以先塞兩個點進去 ```cpp= skip_list() { lis.rz(max_level); lis[0].pb(node(-INF)); lis[0].pb(node(INF)); for (int i = 1; i < max_level; i++) { lis[i].pb(node(-INF, lis[i - 1].begin())); lis[i].pb(node(INF, ++lis[i - 1].begin())); } }; ``` 先寫隨機層數 ```cpp= const LL p = 4; // 1/p const LL max_level = 20; vector<list<node>> lis; LL random_level() { LL lv = 1; while (rng() % p == 0) lv++; return min(max_level, lv); } ``` 然後寫插入 ```cpp= void insert(LL v) { vector<list<node>::iterator> stk; LL cur_level = max_level - 1; auto cur = lis[max_level - 1].begin(); while (1) { cur++; if (cur->value == v) return; if (cur->value > v) { if (cur_level != 0) { stk.pb(cur--); cur = cur->down; cur_level--; } else break; } } LL lv = random_level(); auto last = lis[0].insert(cur, node(v)); for (int i = 1; i < lv; i++) { last = lis[i].insert(stk.back(), node(v, last)); stk.pop_back(); } return; } ``` 再寫刪除 ```cpp= bool erase(LL v) { LL cur_level = max_level - 1; auto cur = lis[max_level - 1].begin(); while (1) { cur++; if (cur->value == v) { break; } if (cur->value > v) { if (cur_level != 0) { cur--; cur = cur->down; cur_level--; } else return 0; } } while (cur_level >= 0) { auto tmp = cur->down; lis[cur_level].erase(cur); cur = tmp; cur_level--; } return 1; } ``` 跟插入差不多 最後寫個 $\text{count}$ ```cpp= bool count(LL v) { LL cur_level = max_level - 1; auto cur = lis[max_level - 1].begin(); while (1) { cur++; if (cur->value == v) return 1; if (cur->value > v) { if (cur_level != 0) { cur--; cur = cur->down; cur_level--; } else return 0; } } } ``` 小改一下就可以當 $\text{map}$ 或 $\text{multiset}$ 要有 $\text{lower_bound}$ 和 $\text{upper_bound}$ 也不難,改一下 $\text{count}$ 就好 [**CSES - Towers (solved by skip list)**](https://cses.fi/paste/62b66466a91b99114e77f4/) [**CSES - Subarray Sums II (solved by skip list)**](https://cses.fi/paste/e81259b85f4e5b474e7866/) 除此之外,只要多維護**往右指標的長度**就能做到 $\text{Random access in}$ $\mathcal{O}(\log n)$ 而這個其實也不難維護,只要往右走的時候加一下就好 [**Josephus Problem II (solved by skip list)**](https://cses.fi/paste/4c0805a8afdcfa5d4f58e5/) ### 結論 優點: 1. 簡單好寫,雖然看起來有點長但其實大部分都只要複製貼上就好 2. 複雜度好 3. 很酷啊不覺得嗎 缺點: 1. 學這個幹嘛,我都用 [$\text{__gnu_pbds::tree}$](https://hackmd.io/@Viecon-342524/SJ1C2BUE9) 2. 常數竟然比 $\text{std::multiset}$ 大,吐了 ## 沒了 ![alt image](https://programmerhumor.io/wp-content/uploads/2022/12/programmerhumor-io-programming-memes-814719c7b89148d.jpg) ## 參考資料 * <https://en.cppreference.com/w/cpp/container/list> * <https://oi-wiki.org/ds/skiplist/> * <http://ccf.ee.ntu.edu.tw/~yen/courses/ds20F/chapter-sl.pdf> * <https://www.twblogs.net/a/5b835f1e2b71776c51e2b615> * <https://www.javatpoint.com/skip-list-in-data-structure> * <https://redirect.cs.umbc.edu/courses/undergraduate/341/fall01/Lectures/SkipLists/skip_lists/skip_lists.html> * <https://stackoverflow.com/questions/12733622/the-time-complexity-of-skip-list> * <https://en.wikipedia.org/wiki/Unrolled_linked_list> * <https://brilliant.org/wiki/unrolled-linked-list/> * <https://blog.csdn.net/weixin_39220472/article/details/81294068> * <https://oi-wiki.org/ds/block-list/> * <https://cplusplus.com/reference/vector/vector/> * <https://blog.51cto.com/u_15329201/3392979> * <https://blog.csdn.net/sinat_36645384/article/details/107051519> * <https://en.wikipedia.org/wiki/Doubly_linked_list> * <https://www.geeksforgeeks.org/data-structures/linked-list/singly-linked-list/> * <https://www.javatpoint.com/singly-linked-list> * <https://kheresy.wordpress.com/2012/03/03/c11_smartpointer_p1/> * <https://kheresy.wordpress.com/2012/03/05/c11_smartpointer_p2/> * <https://rust-algo.club/collections/singly_linked_list/> * <https://www.tutorialspoint.com/data_structures_algorithms/circular_linked_list_algorithm.htm>