ทุกครั้งที่เปลี่ยน next ต้องอัปเดต prev ด้วย
เวลา insert/delete ใน doubly list ต้องจัดการทั้ง next และ prev ของ node ที่เกี่ยวข้อง — ถ้าลืมอัปเดต prev โครงสร้างจะเสียหายแต่ยังเดินหน้าได้ (silent bug ที่หาจับยาก)
Linked List
ทำความเข้าใจ prev pointer, การเดินหน้าและถอยหลัง, และจุดต่างระหว่าง singly กับ doubly linked list
Doubly Linked List คือ linked list ที่ node แต่ละตัวมี pointer สองทาง: **next** (ชี้ไป node ถัดไป) และ **prev** (ชี้กลับไป node ก่อนหน้า) — ทำให้เดินหน้า-ถอยหลังได้อย่างอิสระ เปรียบเหมือนถนนสองทิศทางที่วิ่งได้ทั้งไปและกลับ
next ชี้ไปข้างหน้า, prev ชี้กลับหลัง — เชื่อมโยงสองทิศทาง
type DoublyNode<T> = {
value: T;
next: DoublyNode<T> | null;
prev: DoublyNode<T> | null;
};
// ต่างจาก SinglyNode: เพิ่ม prev pointer เพื่อเดินย้อนกลับ
const node_a = { value: "A", next: null, prev: null };
const node_b = { value: "B", next: null, prev: null };
const node_c = { value: "C", next: null, prev: null };
node_a.next = node_b;
node_b.prev = node_a;
node_b.next = node_c;
node_c.prev = node_b;
const head = node_a;
const tail = node_c;
// head → A ⇄ B ⇄ C ← tailDoubly Linked List: เดินไปข้างหน้าผ่าน next และเดินกลับหลังผ่าน prev
ใน singly linked list ถ้าต้องการลบ node ตรงกลาง คุณต้องหา node ก่อน node เป้าหมาย — ซึ่งหมายความว่าต้องเดินจาก head มาทั้งแถว แต่ใน doubly linked list เมื่อคุณมี reference ไปยัง node เป้าหมายแล้ว คุณรู้ทันทีว่า node ก่อนหน้าคือตัวไหนผ่าน `node.prev` — ไม่ต้องเดินจาก head ใหม่
prev pointer = flexibility แต่แลกมาด้วย memory
ทุกครั้งที่สร้าง DoublyNode คุณต้องเก็บ prev ด้วย (4-8 bytes ต่อ node) — ถ้า linked list มี 100,000 nodes, overhead จาก prev อย่างเดียวประมาณ 800 KB ถึง 1.6 MB ขึ้นกับสถาปัตยกรรม CPU
การ traverse ใน doubly linked list ทำได้สองแบบ: forward traversal (จาก head → tail ผ่าน next) และ backward traversal (จาก tail → head ผ่าน prev)
เดินหน้าผ่าน next จาก head, เดินกลับผ่าน prev จาก tail
// Forward traversal (head → tail) — เหมือน singly
function printForward(head) {
let current = head;
while (current !== null) {
console.log(current.value);
current = current.next;
}
}
// Backward traversal (tail → head) — ใช้ prev เพื่อย้อนกลับ
function printBackward(tail) {
let current = tail;
while (current !== null) {
console.log(current.value);
current = current.prev;
}
}
// ตัวอย่าง
const head = makeDoublyList(["A", "B", "C"]);
const tail = head.next.next; // node_c
printForward(head); // A B C
printBackward(tail); // C B Aความสามารถในการเดินย้อนกลับทำให้ doubly linked list มีประโยชน์มากในสถานการณ์ที่ต้อง undo, backtracking, หรือเดินดูข้อมูลสองทิศทาง เช่น browser history (เดินหน้า-ถอยหลัง)
ลงมือสร้างและจัดการ doubly linked list — เริ่มจากฝึกสร้างและเชื่อม node สองทิศทาง
| หัวข้อ | Singly Linked List | Doubly Linked List |
|---|---|---|
| Pointer ต่อ Node | 1 (next) | 2 (next + prev) |
| เดินทาง | ทางเดียว (head → tail) | สองทาง (head ⇄ tail) |
| Memory Overhead | น้อย (1 pointer) | มาก (2 pointers) — ประมาณ 2 เท่า |
| Delete Node (รู้ตำแหน่ง) | ต้องรู้ node ก่อนหน้า → O(n) | ใช้ node.prev → O(1) |
| Delete Tail | O(n) — ต้องเดินหา node ก่อน tail | O(1) — tail.prev ชี้ทันที |
| Insert ก่อน Node | ต้องรู้ node ก่อนหน้า → O(n) | ใช้ node.prev → O(1) |
| Reverse Traversal | ต้อง reverse ทั้ง list → O(n) | ใช้ prev → O(n) ได้เลย |
| เหมาะกับ | งานที่เดินไปข้างหน้าเท่านั้น, memory จำกัด | งานที่ต้องการเดินสองทิศทาง, ลบ/แทรกกลางซับซ้อน |
เลือก Singly หรือ Doubly?
**เลือก Singly** เมื่อคุณมั่นใจว่าเดินทางเดียวพอ, ต้องการประหยัด memory, หรือทำงานกับ dataset ขนาดใหญ่มากที่ทุก byte มีความหมาย **เลือก Doubly** เมื่อคุณต้องการความสะดวกในการลบ/แทรกกลาง, ต้องเดินย้อนกลับ, หรือใช้ใน data structure ที่ซับซ้อน (เช่น LRU cache, deque)
หัวใจสำคัญของ doubly linked list
เวลา insert/delete ใน doubly list ต้องจัดการทั้ง next และ prev ของ node ที่เกี่ยวข้อง — ถ้าลืมอัปเดต prev โครงสร้างจะเสียหายแต่ยังเดินหน้าได้ (silent bug ที่หาจับยาก)
Trade-off ชัดเจน: จ่าย 4-8 bytes ต่อ node เพื่อให้การลบเมื่อรู้ reference node ทำได้ O(1) แทนที่จะต้องเดินจาก head ใหม่
head.prev ต้อง null (ไม่มีใครอยู่ก่อนหัว) และ tail.next ต้อง null (ไม่มีใครอยู่หลังท้าย) — ถ้าหลุด constraint นี้ traverse อาจเกิด infinite loop
เวลาเดินย้อนกลับ (current = current.prev) — ต้องเช็ก null เช่นเดียวกับ next ไม่เช่นนั้นจะ crash หรือวนไม่หยุด
ทดสอบความเข้าใจก่อนไปเรียนรู้การดำเนินการต่าง ๆ บน linked list