เก็บของเก่าก่อนเปลี่ยน pointer
ก่อนเปลี่ยน connector node: เก็บ reference ที่จะหายไปไว้ในตัวแปรก่อนเสมอ — `const saved = current.next;` แล้วค่อยเปลี่ยน `current.next = ...`
Linked List
ต่อยอดสู่ pattern ที่เจอบ่อย เช่น reverse, middle node, cycle, slow-fast pointers และการเลือกใช้ linked list ให้เหมาะกับโจทย์
หลังจากเรียนรู้พื้นฐานและการดำเนินการต่าง ๆ แล้ว — มาต่อด้วย pattern ที่พบบ่อยในโจทย์ linked list: Reverse (กลับทิศ), Middle Node (หาจุดกึ่งกลาง), Cycle Detection (ตรวจจับวงวน), และ Slow/Fast Pointer Technique — pattern เหล่านี้เป็นเครื่องมือสำคัญที่ใช้แก้ปัญหาได้หลากหลาย
การ reverse linked list คือการกลับทิศ next pointer ของทุก node — จาก A→B→C กลายเป็น C→B→A — เป็น operation พื้นฐานที่ใช้บ่อยมากในการแก้ปัญหา
ใช้ 3 ตัวแปรวนลูปกลับทิศ next pointer ทีละ node
function reverse(head) {
let prev = null;
let current = head;
while (current !== null) {
const nextNode = current.next; // เก็บ next ไว้ก่อน
current.next = prev; // กลับทิศ next ให้ชี้กลับไป prev
prev = current; // ขยับ prev ไป current
current = nextNode; // ขยับ current ไป next ที่เก็บไว้
}
return prev; // prev คือ head ใหม่
}
const head = makeList([10, 20, 30]);
const reversed = reverse(head);
// reversed: 30 → 20 → 10Reverse linked list ทำงานใน O(n) และใช้พื้นที่เพิ่ม O(1) — เพราะเราใช้แค่ 3 ตัวแปร (prev, current, nextNode) โดยไม่ต้องสร้างโครงสร้างข้อมูลใหม่
การหา node ตรงกลางของ linked list สามารถทำได้โดยการเดินนับขนาดก่อน (2 pass: pass 1 นับ n, pass 2 เดินไป n/2) — แต่วิธีที่ elegant กว่าคือใช้ **slow/fast pointers**: slow เดินทีละ 1 ก้าว, fast เดินทีละ 2 ก้าว — เมื่อ fast ถึงปลายทาง slow จะอยู่ตรงกลางพอดี
slow เดิน ½ ของ fast → เมื่อ fast จบ slow = middle
function findMiddle(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next; // เต่าคลานทีละ 1
fast = fast.next.next; // กระต่ายกระโดดทีละ 2
}
return slow; // เต่าอยู่ตรงกลาง
}
// Example
const head = makeList([10, 20, 30, 40, 50]);
console.log(findMiddle(head).value); // 30
const head2 = makeList([10, 20, 30, 40]);
console.log(findMiddle(head2).value); // 30 (ตัวที่สองของครึ่งแรก)Slow (เต่า) & Fast (กระต่าย): เมื่อกระต่ายถึงเส้นชัย เต่าจะอยู่กลางทาง
เทคนิคนี้ใช้ได้เพราะ: ถ้า fast เดิน 2 เท่าของ slow — เมื่อ fast ถึงปลายทาง (null) slow จะเดินได้ครึ่งทางพอดี — ใช้เวลา O(n) เดินครั้งเดียว ไม่ต้องนับขนาดก่อน ^: หมายเหตุ: ถ้า linked list มีจำนวน node เป็นเลขคู่ middle จะตกที่ node ที่ n/2 (ตัวที่สองของครึ่งซ้าย) — เพราะ condition `fast.next !== null` จะทำให้ลูปหยุดเมื่อ fast อยู่ที่ node ก่อนสุดท้าย
Cycle (วงวน) ใน linked list เกิดขึ้นเมื่อ next pointer ของ node บางตัวชี้กลับไปยัง node ก่อนหน้า ทำให้ traverse แล้วไม่มีวันจบ — การตรวจจับ cycle ใช้ **Floyd's Cycle Detection** (Tortoise & Hare): ถ้า slow กับ fast เดินด้วยความเร็วต่างกันแล้วมาเจอกันในลูป แปลว่ามี cycle
slow กับ fast เจอกันใน cycle → true; fast จบปลายทาง → false
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
return true; // slow กับ fast เจอกัน → มี cycle
}
}
return false; // fast จบท้ายได้ → ไม่มี cycle
}
// Example
const a = { value: "A", next: null };
const b = { value: "B", next: null };
const c = { value: "C", next: null };
a.next = b; b.next = c; c.next = b; // cycle: A→B→C→B
console.log(hasCycle(a)); // trueทำไม Floyd's Algorithm ถึงรับประกันว่าเจอกันถ้ามี cycle?
ใน cycle: ระยะห่างระหว่าง slow กับ fast ลดลงทีละ 1 ในแต่ละรอบ (fast ได้เปรียบ slow 1 ก้าวต่อรอบ) — เมื่อระยะห่างลดถึง 0 ทั้งคู่ก็จะเจอกัน ถ้าไม่มี cycle fast จะวิ่งถึง null ก่อน
ลงมือ implement reverse linked list และหา middle node ด้วย slow/fast
| Operation | Singly | Doubly | Array | หมายเหตุ |
|---|---|---|---|---|
| Access by Index | O(n) | O(n) | O(1) | Linked list ต้องเดิน; array คำนวณ offset |
| Search | O(n) | O(n) | O(log n)* | *array เท่านั้นถ้า sorted |
| Prepend | O(1) | O(1) | O(n) | Linked list จุดเด่น — แค่ขยับ head |
| Append (มี tail) | O(1) | O(1) | O(1)* | *array amortized; linked list ต้องมี tail |
| Insert at Index | O(n) | O(n) | O(n) | ทุก DS ต้อง shift/เดินหาตำแหน่ง |
| Delete Head | O(1) | O(1) | O(n) | Linked list O(1), array ต้อง shift |
| Delete Tail | O(n) | O(1) | O(1) | Doubly ชนะเพราะมี tail.prev |
| Delete at Index | O(n) | O(n) | O(n) | หา + ลบ = O(n) ทั้งหมด |
| Reverse | O(n) | O(n) | O(n) | Linked list O(1) space, array O(n) space |
| Find Middle | O(n) | O(n) | O(1) | Array = arr[length/2]; linked list = slow/fast |
| Cycle Detection | O(n) | O(n) | N/A | Array ไม่มี cycle concept |
การเลือกโครงสร้างข้อมูลขึ้นอยู่กับ workload — ไม่มีโครงสร้างไหนดีที่สุดเสมอ ต่อไปนี้คือแนวทางเลือกใช้ linked list ในสถานการณ์จริง
| สถานการณ์ | โครงสร้างที่แนะนำ | เหตุผล |
|---|---|---|
| Implement Queue | Singly Linked List | enqueue = append O(1) มี tail, dequeue = delete head O(1) |
| Implement Deque | Doubly Linked List | insert/delete ทั้งสองด้าน O(1) ด้วย head+tail+prev |
| Implement LRU Cache | Doubly Linked List + Hash Map | move-to-front = O(1) delete + prepend |
| Undo/Redo Stack | Doubly Linked List | เดินไปมา history ได้ด้วย prev/next |
| Music Playlist | Doubly Linked List | next/previous track สะดวกด้วย prev pointer |
| Task ที่ Insert/Delete บ่อย | Linked List (ชนิดใดก็ได้) | แก้ pointer O(1) แทนที่ shift array O(n) |
| Task ที่ Random Access บ่อย | Array | O(1) เข้าถึง index ทันที — linked list ไม่เหมาะ |
Linked List ในโลกความจริง
Linked list มักไม่ได้ใช้เดี่ยว ๆ ในการเขียนโปรแกรมจริง — มักใช้เป็น building block ใน data structure ที่ซับซ้อนกว่า เช่น hash table (separate chaining), adjacency list ของ graph, และ file system (doubly linked list สำหรับ directory entries) — การเข้าใจ linked list เป็นพื้นฐานสำคัญสำหรับโครงสร้างข้อมูลขั้นสูง
กฎที่ต้องจำทุกครั้งเมื่อทำงานกับ pointer ใน linked list
ก่อนเปลี่ยน connector node: เก็บ reference ที่จะหายไปไว้ในตัวแปรก่อนเสมอ — `const saved = current.next;` แล้วค่อยเปลี่ยน `current.next = ...`
ถ้าจะใช้ `.next`, `.prev`, `.value` — ตรวจก่อนว่า object ไม่ใช่ null — `if (node !== null) { node.next = ... }`
ทดสอบโค้ดทุกครั้งกับ: head = null, head.next = null (1 node), และการ insert/delete ที่ index = length (node สุดท้าย) — ถ้าผ่าน 3 เคสนี้โอกาสรอดสูง
แบบทดสอบสุดท้าย — ทบทวน pattern และการเลือกใช้ linked list