Singly linked list hanya bisa berjalan maju — dari head ke tail. Operasi seperti hapus_node(pos) tetap perlu traverse O(n) untuk menemukan predecessor, meskipun pointer pos sudah diketahui. Doubly linked list menambahkan pointer prev pada setiap node, memungkinkan traversal dua arah dan penghapusan node dalam O(1) langsung dari pointer node. Trade-off-nya: setiap node membutuhkan satu pointer ekstra (8 byte pada sistem 64-bit). Pada Chapter ini kita akan membangun doubly linked list lengkap, melihat operasi hapus yang lebih elegan, dan membandingkan overhead memorinya dibanding singly.
Struktur Node Doubly #
Setiap node doubly menyimpan tiga hal: data, pointer prev ke node sebelumnya, dan pointer next ke node berikutnya. Invariant penting: bila node->next != nullptr, maka node->next->prev == node. Hubungan ini harus dijaga pada setiap operasi insert/delete.
Panah dua arah pada diagram melambangkan dua pointer — next maju, prev mundur. Pointer prev dari head dan pointer next dari tail selalu nullptr.
Operasi push_back #
Mekanisme: Sambung Dua Arah #
- Alokasikan node baru
n. - Bila list kosong:
head_ = tail_ = n. - Bila tidak, sambungkan dua arah:
n->prev = tail_— node baru tahu predecessornya.tail_->next = n— tail lama menunjuk ke node baru.tail_ = n— perbarui tail.
Operasi tetap O(1) berkat tail pointer.
Operasi push_front #
Mekanisme: Sambung Dua Arah dari Head #
Mirip push_back namun dari sisi head:
- Alokasikan
n. - Bila list kosong:
head_ = tail_ = n. - Bila tidak:
n->next = head_— node baru menunjuk ke head lama.head_->prev = n— head lama tahu node baru sebagai predecessor.head_ = n— perbarui head.
Operasi hapus O(1) dari Pointer Node #
Aturan #
Berbeda dengan singly linked list yang harus traverse untuk menemukan prev, doubly linked list langsung memiliki cur->prev. Jadi penghapusan node berdasarkan pointer adalah O(1).
Mekanisme: Lompati Node Saat Ini #
Bila cur adalah node yang akan dihapus:
cur->prev->next = cur->next— predecessor melompaticur.cur->next->prev = cur->prev— successor melompaticur.- Kasus khusus: bila
cur == head_, updatehead_ = cur->next. Bilacur == tail_, updatetail_ = cur->prev. delete cur.
Kode Lengkap Doubly Linked List #
#include <iostream>
template <typename T>
struct Node {
T nilai;
Node* prev;
Node* next;
Node(const T& v) : nilai(v), prev(nullptr), next(nullptr) {}
};
template <typename T>
class DoublyLinkedList {
public:
DoublyLinkedList() : head_(nullptr), tail_(nullptr), size_(0) {}
~DoublyLinkedList() { clear(); }
void push_back(const T& v) {
Node<T>* n = new Node<T>(v);
if (tail_ == nullptr) {
head_ = tail_ = n;
} else {
n->prev = tail_;
tail_->next = n;
tail_ = n;
}
++size_;
}
void push_front(const T& v) {
Node<T>* n = new Node<T>(v);
if (head_ == nullptr) {
head_ = tail_ = n;
} else {
n->next = head_;
head_->prev = n;
head_ = n;
}
++size_;
}
bool hapus_nilai(const T& v) {
Node<T>* cur = head_;
while (cur != nullptr) {
if (cur->nilai == v) {
if (cur->prev != nullptr) cur->prev->next = cur->next;
else head_ = cur->next;
if (cur->next != nullptr) cur->next->prev = cur->prev;
else tail_ = cur->prev;
delete cur;
--size_;
return true;
}
cur = cur->next;
}
return false;
}
void forward() const {
std::cout << "maju : ";
Node<T>* cur = head_;
while (cur != nullptr) {
std::cout << cur->nilai << " ";
cur = cur->next;
}
std::cout << '\n';
}
void backward() const {
std::cout << "mundur: ";
Node<T>* cur = tail_;
while (cur != nullptr) {
std::cout << cur->nilai << " ";
cur = cur->prev;
}
std::cout << '\n';
}
void clear() {
Node<T>* cur = head_;
while (cur != nullptr) {
Node<T>* nx = cur->next;
delete cur;
cur = nx;
}
head_ = tail_ = nullptr;
size_ = 0;
}
std::size_t size() const { return size_; }
private:
Node<T>* head_;
Node<T>* tail_;
std::size_t size_;
};
int main() {
DoublyLinkedList<int> dl;
dl.push_back(10);
dl.push_back(20);
dl.push_back(30);
dl.push_front(5);
dl.forward();
dl.backward();
std::cout << "hapus 20...\n";
dl.hapus_nilai(20);
dl.forward();
dl.backward();
std::cout << "size = " << dl.size() << '\n';
return 0;
}Output yang Dihasilkan:
maju : 5 10 20 30
mundur: 30 20 10 5
hapus 20...
maju : 5 10 30
mundur: 30 10 5
size = 3- Baris
Node* prev; Node* next;: dua pointer per node — inilah inti doubly. Setiap node tahu predecessor dan successor. - Baris
n->prev = tail_; tail_->next = n; tail_ = n;(dipush_back): sambungan dua arah lengkap dalam tiga baris. Node baru tahu tail lama, tail lama tahu node baru, lalu tail diperbarui. - Baris
if (cur->prev != nullptr) cur->prev->next = cur->next; else head_ = cur->next;(dihapus_nilai): predecessor melompaticur. Bilacuradalah head (tidak punya predecessor), maka head langsung diganti. - Baris
if (cur->next != nullptr) cur->next->prev = cur->prev; else tail_ = cur->prev;(dihapus_nilai): successor melompaticur. Bilacuradalah tail, tail mundur ke predecessor. - Method
backward(): traverse daritail_mengikuti pointerprev. Inilah keunggulan utama doubly — operasi terbalik sama mudahnya dengan forward. - Setelah
hapus 20: node20dihapus dan chain disambung ulang —10->next = 30dan30->prev = 10. Traversal forward dan backward keduanya konsisten.
Perbandingan Singly vs Doubly #
| Aspek | Singly | Doubly |
|---|---|---|
| Pointer per node | 1 (next) | 2 (prev, next) |
| Traverse mundur | Tidak didukung | O(n) langsung |
| Hapus dari pointer | O(n) — cari prev | O(1) — cur->prev ada |
Memori per int node | 12 byte (4+8, padding) | 20 byte (4+8+8, padding) |
| Use case umum | Stack, queue sederhana | LRU cache, browser history, deque |
Trade-off: Mengapa Doubly Worth It #
Pertimbangan memilih doubly:
- Bila perlu hapus node dengan pointer O(1) — mis. LRU cache yang menyimpan iterator ke node. Doubly menghemat traverse O(n).
- Bila perlu traverse mundur — mis. undo/redo, navigasi playlist, algoritma yang memindai dua arah.
- Bila tipe data besar — overhead 8 byte per node relatif kecil dibanding 100+ byte data.
Pilih singly bila:
- Memori ketat — jutaan elemen kecil, overhead pointer mendominasi.
- Operasi dominan push/pop satu arah — stack dan queue sederhana tidak butuh prev.
Ringkasan #
- Doubly linked list menambah pointer
prevper node — memungkinkan traversal dua arah. push_front,push_back, danhapus(dengan pointer node) semuanya O(1).- Trade-off: 8 byte pointer ekstra per node; invariant
node->next->prev == nodeharus dijaga. - Untuk tipe kecil (mis.
int), overhead relatif besar; untuk tipe besar, overhead relatif kecil. - Chapter berikutnya akan membahas circular linked list — varian yang tail menunjuk ke head untuk struktur ring.