---
# System prepended metadata

title: Linked list (鏈結串列)

---

# 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>
