Prepend = O(1) — เร็วสุด, Append มี tail = O(1)
สอง operation พื้นฐานที่ linked list ทำได้ดีกว่า array — ใช้เมื่อ workload เน้น insert/delete ที่ขอบรายการ
Linked List
รวมงานพื้นฐานของ linked list ทั้ง insert, delete, update, วัดขนาด และเช็กสถานะว่างแบบทีละขั้น
การจัดการ linked list มีชุด operations พื้นฐานที่คุณต้องทำเป็น: Insert (เพิ่มที่หัว, ท้าย, กลาง), Delete (ลบที่หัว, ท้าย, กลาง), Search (ค้นหา), Update (อัปเดตค่า), isEmpty (ตรวจว่าว่าง), และ Size (หาขนาด) — มาดูวิธีการทำแต่ละ operation อย่างละเอียด
Prepend คือการเพิ่ม node ใหม่ไว้ข้างหน้าสุดของ list — เป็น operation ที่เร็วที่สุดของ linked list เพราะใช้เวลา O(1) ไม่ต้องเดินหาเลย
สร้าง node ใหม่ ชี้ next ไป head เดิม เลื่อน head — จบใน 1 step
function prepend(head, tail, value) {
const node = { value, next: head };
if (tail === null) {
return { head: node, tail: node };
}
return { head: node, tail };
}
// Example
let head = null;
let tail = null;
const result1 = prepend(head, tail, 30);
head = result1.head; tail = result1.tail; // [30]
const result2 = prepend(head, tail, 20);
head = result2.head; tail = result2.tail; // [20 → 30]
const result3 = prepend(head, tail, 10);
head = result3.head; tail = result3.tail; // [10 → 20 → 30]ข้อควรระวัง: ถ้า list ว่าง (head เป็น null) เราต้องตั้ง tail ให้ชี้ node ใหม่ด้วย — เพราะ node ใหม่เป็นทั้ง head และ tail ของ list ตัวเดียว
Append คือการเพิ่ม node ใหม่ที่ท้ายสุดของ list — ความเร็วขึ้นอยู่กับว่ามี tail reference หรือไม่
ความต่างมีนัยสำคัญ — ควรเก็บ tail ไว้เสมอถ้างานเน้น append
// เวอร์ชันมี tail — O(1)
function appendWithTail(head, tail, value) {
const node = { value, next: null };
if (head === null) {
return { head: node, tail: node };
}
tail.next = node;
return { head, tail: node };
}
// เวอร์ชันไม่มี tail — O(n) ต้องเดินจาก head
function appendNoTail(head, value) {
const node = { value, next: null };
if (head === null) return node;
let current = head;
while (current.next !== null) {
current = current.next;
}
current.next = node;
return head;
}| กรณี | Time Complexity | เหตุผล |
|---|---|---|
| มี tail reference | O(1) | เข้าถึง tail โดยตรง — ไม่ต้องเดิน |
| ไม่มี tail reference | O(n) | ต้องเดินจาก head ผ่านทุก node จนสุด |
| List ว่าง (head = null) | O(1) | ไม่ต้องเดิน — node ใหม่เป็นทั้ง head และ tail |
| มี tail + doubly linked list | O(1) | เหมือนมี tail ใน singly + เพิ่ม prev ได้ทันที |
การแทรก node ที่ตำแหน่งกลาง list ประกอบด้วย 2 ขั้นตอน: (1) เดินหา node ก่อนตำแหน่งแทรก → O(n) และ (2) ปรับ next pointer → O(1) — โดยรวมใช้เวลา O(n)
เดิน i-1 ก้าวหา node ก่อนจุดแทรก แล้วสลับ pointer
function insertAt(head, index, value) {
const node = { value, next: null };
// แทรกที่หัว (index 0)
if (index === 0) {
node.next = head;
return node;
}
// เดินหาตำแหน่งก่อนจุดแทรก
let current = head;
for (let i = 0; i < index - 1; i += 1) {
if (current === null) return head; // index เกินขนาด
current = current.next;
}
if (current === null) return head;
// แทรก: node.next = current.next, current.next = node
node.next = current.next;
current.next = node;
return head;
}Delete head คือการลบ node แรกสุด — ใช้เวลา O(1) โดยการเลื่อน head ไป next ของ head เดิม (node ที่ถูกลบจะถูก garbage collector จัดการเอง)
head = head.next — จบ แค่นี้ก็ลบ node แรกออกแล้ว
function deleteHead(head, tail) {
if (head === null) return { head: null, tail: null };
const newHead = head.next;
// ถ้า list เหลือ node เดียว → tail ก็เป็น null ด้วย
if (newHead === null) {
return { head: null, tail: null };
}
// รีไวร์ prev (ถ้าเป็น doubly)
if (newHead.prev) newHead.prev = null;
return { head: newHead, tail };
}การลบ tail ใน singly linked list ต้องเดินจาก head จนถึง node ก่อน tail (O(n)) เพราะเราต้อง set `next` ของ node นั้นเป็น `null` — ไม่สามารถเข้าถึง node ก่อนหน้าโดยตรงได้ (ต่างจาก doubly linked list ที่ใช้ `tail.prev` → O(1))
ต้องเดินหา node ก่อน tail เพื่อตั้ง next = null
function deleteTail(head, tail) {
if (head === null) return { head: null, tail: null };
if (head.next === null) return { head: null, tail: null };
let current = head;
while (current.next !== tail) {
current = current.next;
}
current.next = null;
return { head, tail: current };
}การลบ node ที่ตำแหน่งใด ๆ ใน list: เดินหา node ก่อนตำแหน่งลบ (O(n)) แล้วรีไวร์ next ให้ข้าม node นั้น (O(1)) — โดยรวมใช้ O(n)
เดิน i-1 ก้าว → current.next = current.next.next เพื่อข้าม
function deleteAt(head, tail, index) {
if (head === null) return { head: null, tail: null };
// ลบ head
if (index === 0) {
const newHead = head.next;
if (newHead && newHead.prev) newHead.prev = null;
const newTail = newHead === null ? null : tail;
return { head: newHead, tail: newTail };
}
// เดินหา node ก่อนตำแหน่งลบ
let current = head;
for (let i = 0; i < index - 1; i += 1) {
if (current.next === null) return { head, tail };
current = current.next;
}
if (current.next === null) return { head, tail };
// ลบ: ข้าม node เป้าหมาย
const deleted = current.next;
current.next = deleted.next;
// ถ้าลบ tail → อัปเดต tail
const newTail = current.next === null ? current : tail;
return { head, tail: newTail };
}การค้นหาและอัปเดตค่าใน linked list ใช้หลักการเดียวกับ array — ไล่หาจนเจอ node เป้าหมาย แล้วแก้ไข `value` โดยตรง ไม่ต้องจัดการ pointer
Search = O(n), Update (เมื่อรู้ตำแหน่งแล้ว) = O(n) เพื่อหา + O(1) เพื่อแก้
function find(head, target) {
let current = head;
let index = 0;
while (current !== null) {
if (current.value === target) return index;
current = current.next;
index += 1;
}
return -1;
}
function updateAt(head, index, newValue) {
let current = head;
for (let i = 0; i < index && current !== null; i += 1) {
current = current.next;
}
if (current === null) return false;
current.value = newValue;
return true;
}
const head = makeList([10, 20, 30]);
console.log(find(head, 20)); // 1
console.log(updateAt(head, 1, 99)); // true
// head กลายเป็น 10 → 99 → 30ตรวจ head === null = O(1); นับ size แบบเดิน = O(n); เก็บ counter ในคลาส = O(1) แต่ต้องอัปเดตทุก operation
// isEmpty — ตรวจดูที่ head ทันที O(1)
function isEmpty(head) {
return head === null;
}
// size (เดินนับ) — O(n) เพราะต้องนับทุก node
function sizeByWalk(head) {
let count = 0;
let current = head;
while (current !== null) {
count += 1;
current = current.next;
}
return count;
}
// size (เก็บ counter) — O(1) แต่ต้องอัปเดต counter ทุก insert/delete
class TrackedList {
constructor() {
this.head = null;
this.tail = null;
this._size = 0;
}
prepend(value) {
const node = { value, next: this.head };
this.head = node;
if (!this.tail) this.tail = node;
this._size += 1;
}
get size() {
return this._size;
}
}| วิธี | Time (อ่าน) | Time (ดูแล) | เหมาะกับ |
|---|---|---|---|
| เดินนับทุกครั้ง | O(n) | O(0) — ไม่ต้องดูแล | list ที่ size เปลี่ยนน้อยแต่ query บ่อย |
| เก็บ counter ในคลาส | O(1) | O(1) — อัปเดตทุก insert/delete | list ที่ query size บ่อยมาก |
| isEmpty (head === null) | O(1) | O(0) | ใช้ได้เสมอ ไม่เปลี่ยน complexity |
ถึงเวลาลงมือ implement operations บน linked list — เริ่มจากเขียน insert และ delete function
หัวใจสำคัญของทุก operation บน linked list
สอง operation พื้นฐานที่ linked list ทำได้ดีกว่า array — ใช้เมื่อ workload เน้น insert/delete ที่ขอบรายการ
การเดินหาตำแหน่งคือต้นทุนหลัก — operation ที่ตัว node เองใช้ O(1) แต่การหา node ใช้ O(n) ทำให้โดยรวมยังเป็น O(n)
เวลา insert: ต้องชี้ next ของ node ใหม่ไปยัง node ถัดไปก่อน แล้วค่อยเปลี่ยน next ของ current — ถ้าทำกลับกันคุณจะสูญเสีย reference ไป node ถัดไปทันที
ทุกครั้งที่คุณเปลี่ยน next ต้องแน่ใจว่าค่าที่กำลังเปลี่ยนมีอยู่จริง — การใช้ .next บน null ทำให้เกิด TypeError ทันที
ถ้าใช้คลาสเก็บ tail ไว้ ต้องอัปเดต tail ให้ชี้ถูกต้องหลัง insert/delete — ถ้าไม่ทำ tail อาจชี้ไป node ที่ถูกลบไปแล้วหรือไม่ได้อัปเดต
ทดสอบความเข้าใจก่อนไปเรียนรู้ pattern ขั้นสูงของ linked list