【C++】競程筆記(資料結構:linked-list)
程式碼範例參考:NTUCPC Guide,此筆記僅為個人學習用途。
鏈結串列(linked-list)
一般的陣列會是在連續的記憶體空間中儲存資料,而鏈結串列雖與陣列同為線性資料結構,但他可以將資料儲存在一個 不連續(non-contiguous) 的記憶體空間中。
linked-list 的定義為數個節點(nodes)的集合,而一個節點就由兩個成員所組成,也就是值跟前後的指標(value and previous / next pointer)。
linked-list 可以有三種型態:
- 單向鏈結串列(Singly Linked List)
- 雙向鏈結串列(Doubly Linked List)
- 循環鏈結串列(Circular Linked List)
那首先最簡單的當然就是單向的 linked-list 了,要如何簡單的理解 linked-list 呢?就如同上課傳紙條一樣,假設有一排座位,某個同學要傳紙條到最前面的同學,勢必要點他前面同學的肩膀叫他接力下去,就有點像下圖那樣:

圖:作者繪製
Head 就是這個鏈結串列的開頭,A 同學經過一連串的同學接力傳紙條後,直到傳給 F 同學為止,而最終鏈結串列結束時要指向空指標 NULL,此時我們可以假設講台是 NULL。
那在這時候(「單向」的鏈結串列)只能朝向一個方向,也就是 Next,並不能往前(Previous)。
而雙向鏈結串列就有點像是點同學肩膀然後那個同學回頭一樣,多了一個往前(Previous)的動作,像是往回看點人的同學,如下圖:

由於雙向鏈結串列多了 Prev 的動作,所以 A 作為開頭也能做 Prev 的動作,只不過 Prev 後就沒東西了,所以是 NULL。
最後是循環鏈結串列,其實他跟單向鏈結串列差不多,只是跑到最後一個節點會回到第一個節點去。
一樣是舉傳紙條的例子,傳到最後一個人的時候,F 同學可能看到某些勁爆的內容,很生氣地把紙條丟回去給 A 同學 XD。

C++ 實作鏈結串列
可用 OOP 實作。
初始化一個節點(Node),包含 data、pointer。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <iostream> using namespace std;
class Node { public: int data; Node* next;
Node(int data) { this->data = data; this->next = nullptr; } };
|
基本操作:
- 串列頭插入節點
1 2 3 4 5
| void insertAtHead(int value) { Node* newNode = new Node(value); newNode->next = head; head = newNode; }
|
把新節點的 next 指向原本的 head,再更新 head 就行。
- 串列尾插入節點
1 2 3 4 5 6 7 8 9 10 11 12
| void insertAtTail(int value) { Node* newNode = new Node(value); if (head == nullptr) { head = newNode; return; } Node* temp = head; while (temp->next != nullptr) { temp = temp->next; } temp->next = newNode; }
|
要先遍歷完整個鏈結串列,直到最後一個節點,把 next 指向新節點;若串列為空,則直接更新 head 就好。
- 刪除串列之頭節點
直接將 head 指向 head->next,並釋放原本的頭節點記憶體。
1 2 3 4 5 6
| void deleteHead() { if (head == nullptr) return; Node* temp = head; head = head->next; delete temp; }
|
- 遍歷鏈結串列並且印出
1 2 3 4 5 6 7 8
| void printList() { Node* temp = head; while (temp != nullptr) { cout << temp->data << " "; temp = temp->next; } cout << endl; }
|
遍歷由 head 開始,依序存取每個節點的 data,直到節點指標為 nullptr 為止。
最後我們把它合起來變成一個完整的程式碼,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| #include <iostream> using namespace std;
class Node { public: int data; Node* next; Node(int data) { this->data = data; this->next = nullptr; } };
class LinkedList { private: Node* head;
public: LinkedList() { head = nullptr; }
void insertAtHead(int value) { Node* newNode = new Node(value); newNode->next = head; head = newNode; }
void insertAtTail(int value) { Node* newNode = new Node(value); if (head == nullptr) { head = newNode; return; } Node* temp = head; while (temp->next != nullptr) temp = temp->next; temp->next = newNode; }
void deleteHead() { if (head == nullptr) return; Node* temp = head; head = head->next; delete temp; }
void deleteAtPosition(int pos) { if (head == nullptr || pos < 1) return; if (pos == 1) { deleteHead(); return; } Node* current = head; for (int i = 1; i < pos - 1 && current->next != nullptr; i++) { current = current->next; } if (current->next == nullptr) return; Node* temp = current->next; current->next = temp->next; delete temp; }
void printList() { Node* temp = head; while (temp != nullptr) { cout << temp->data << " "; temp = temp->next; } cout << endl; } };
int main() { LinkedList list; list.insertAtHead(3); list.insertAtHead(2); list.insertAtTail(4); list.insertAtTail(5); cout << "Initial list: "; list.printList();
list.deleteAtPosition(2); cout << "After deleting position 2: "; list.printList();
list.deleteHead(); cout << "After deleting head: "; list.printList();
return 0; }
|
:::info
註:此程式的鏈結串列是以 1 作為起始索引。
:::
至於在指定位置插入節點的方式,可結合前兩種方式(在串列頭插入節點、在串列尾插入節點):
先遍歷至目標的前一個位置,再調整指標串接新節點。若位置(索引)為 1 則與串列頭插入的方式相同;若超出串列長度則將插入方式改為在串列尾插入節點。
而於指定位置刪除節點的方式:
若位置為 1,與刪除串列頭節點相同;否則遍歷至目標前一個節點,調整其 next 指向目標節點的下一節點,並刪除目標節點記憶體。
C++ STL linked-list
C++ 的 STL 函式庫中也提供了 linked-list 的容器,在 STL 中是一個雙向鏈結串列的實作容器。
使用前需要引入標頭檔 #include <list>。
相關操作如下:
push_back():將元素增加到尾端。push_front():將元素增加到頭端。pop_back():將元素從尾端移除。pop_front():將元素從頭端移除。insert():插入元素。erase():移除某個位置的元素,也可以移除某段範圍的元素。clear():清空容器內的所有元素。empty():回傳容器是否為空。size():回傳容器長度。front():存取頭端元素。back():存取尾端元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| #include <list> #include <iostream>
using namespace std;
class LinkedList { private: list<int> lst;
public: void insertHead(int value) { lst.push_front(value); }
void insertTail(int value) { lst.push_back(value); }
bool insertAt(int index, int value) { if (index < 0 || index > lst.size()) { return false; } auto it = lst.begin(); advance(it, index); lst.insert(it, value); return true; }
bool deleteHead() { if (lst.empty()) { return false; } lst.pop_front(); return true; }
bool deleteTail() { if (lst.empty()) { return false; } lst.pop_back(); return true; }
bool deleteAt(int index) { if (index < 0 || index >= lst.size()) { return false; } auto it = lst.begin(); advance(it, index); lst.erase(it); return true; }
void display() { if (lst.empty()) { cout << "List is empty" << endl; return; } for (const auto& val : lst) { cout << val << " "; } cout << endl; } };
int main() { LinkedList list;
list.insertHead(1); list.insertTail(3); list.insertAt(1, 2); cout << "After insertions: "; list.display();
list.deleteHead(); cout << "After deleting head: "; list.display();
list.deleteTail(); cout << "After deleting tail: "; list.display();
list.insertTail(4); list.insertHead(1); list.deleteAt(1); cout << "After deleting at index 1: "; list.display();
return 0; }
|
Linked-list 與 array 時間複雜度
| 操作類型 | Array(陣列) | Linked List(鏈結串列) |
|---|
| 存取(Access) | O(1) | O(n) |
| 搜尋(Search) | O(n) | O(n) |
| 插入(Insert) | | |
| - 開頭 | O(n)(需移動元素) | O(1) |
| - 中間 | O(n)(需移動元素) | O(n)(需遍歷) |
| - 尾端 | O(1)(若空間足夠) | O(1)(在 tail 指標下) |
| 刪除(Delete) | | |
| - 開頭 | O(n)(需移動元素) | O(1) |
| - 中間 | O(n)(需移動元素) | O(n)(需遍歷) |
| - 尾端 | O(1)(若空間足夠) | O(n)(單向需遍歷) |
| 大小調整 | O(n)(需重新配置) | O(1) |
若為 Doubly Linked List(雙向鏈結串列),尾端刪除可達 O(1),但需額外空間儲存前向指標。
鏈結串列習題
b938. kevin 愛殺殺:https://zerojudge.tw/ShowProblem?problemid=b938
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include <bits/stdc++.h>
using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; list<int> lst; vector<list<int>::iterator> vec(n + 1); vector<bool> alive(n + 1, true); for (int i = 1; i <= n; i++) { vec[i] = lst.insert(lst.end(), i); } for (int j = 0; j < m; j++) { int k; cin >> k; if (!alive[k]) { cout << "0u0 ...... ?" << "\n"; } else { auto it = vec[k]; if (next(it) == lst.end()) { cout << "0u0 ...... ?" << "\n"; } else { auto next_it = next(it); int removed_value = *next_it; cout << removed_value << "\n"; lst.erase(next_it); alive[removed_value] = false; } } } return 0; }
|