Arrays vs. Linked Lists: ศึกสายเลือดของโครงสร้างข้อมูลพื้นฐาน

1. 🎯 ตอนที่ 3: Arrays vs. Linked Lists ศึกสายเลือดของโครงสร้างข้อมูลพื้นฐาน
2. 📖 เปิดฉาก (The Hook)
“ถ้าระบบของเราต้องมีการเพิ่มและลบข้อมูลบ่อยๆ ตลอดเวลา คุณจะเลือกใช้ Array หรือ Linked List? เพราะอะไร?”
นี่คือหนึ่งในคำถามสัมภาษณ์งานสุดคลาสสิกของบริษัทระดับ FAANG ที่ปราบโปรแกรมเมอร์ตกม้าตายมานักต่อนักครับ! หลายคนอาจจะคิดว่า “ก็ใช้ Array (หรือ List ใน Python) ไปสิ ง่ายดี พิมพ์แค่ .append() หรือ .insert() ก็จบแล้ว” แต่หารู้ไม่ว่า ภายใต้คำสั่งสั้นๆ นั้น หากข้อมูลมีขนาดระดับล้านเรคคอร์ด การใช้โครงสร้างข้อมูลผิดประเภทอาจทำให้ระบบเกิดอาการ “คอขวด” รันช้าเป็นเต่าคลานจนเซิร์ฟเวอร์พังได้เลย
วันนี้พี่จะพาน้องๆ ไปเจาะลึกเบื้องหลังการทำงานในระดับหน่วยความจำ (Memory) เพื่อดูว่า “ศึกสายเลือด” ระหว่าง 2 โครงสร้างข้อมูลที่ทรงอิทธิพลที่สุดในโลกโปรแกรมมิ่งอย่าง Array และ Linked List ใครจะอยู่ ใครจะไป และเราควรหยิบอาวุธชิ้นไหนไปใช้ให้เหมาะกับสถานการณ์ครับ!
3. 🧠 แก่นวิชา (Core Concepts)
เพื่อให้เห็นภาพชัดเจนที่สุด เราต้องมาดูว่าคอมพิวเตอร์จัดเก็บข้อมูลพวกนี้ยังไงใน Memory ครับ ให้จินตนาการว่า Memory ของคอมพิวเตอร์คือ “ล็อกเกอร์ฝากของ” หรือ “ลิ้นชัก” ที่เรียงติดกันเป็นแผงใหญ่ๆ แต่ละช่องมีที่อยู่ (Address) กำกับไว้
Array (อาร์เรย์): จองที่นั่งโรงหนังแบบยกแก๊ง การเก็บข้อมูลแบบ Array คือการจองพื้นที่ใน Memory แบบ “ต้องอยู่ติดกันเสมอ (Contiguous)”
- เปรียบเทียบ: เหมือนน้องไปดูหนังกับเพื่อน 5 คน น้องก็ต้องหาที่นั่งว่าง 5 ที่ “ติดกัน” เท่านั้น ถ้านั่งติดกันไม่ได้ ก็แปลว่าจองไม่ได้
- ปัญหา: ถ้าจองไว้ 5 ที่นั่ง แล้วจู่ๆ มีเพื่อนคนที่ 6 ขอตามมาด้วย แต่นั่งข้างๆ ดันมีคนอื่นนั่งไปแล้ว สิ่งที่น้องต้องทำคือ “ต้องลุกไปหาที่นั่งใหม่ทั้งแก๊ง” ที่มี 6 ที่ติดกัน! นี่แหละคือเหตุผลว่าทำไมการขยายขนาด Array ถึงกินเวลามหาศาล
Linked List (ลิงก์ลิสต์): ล่าขุมทรัพย์ลายแทง Linked List แก้ปัญหาของ Array โดยการที่ข้อมูล “ไม่จำเป็นต้องอยู่ติดกัน” ใน Memory ครับ ข้อมูลแต่ละตัว (เราเรียกว่า Node) จะถูกกระจายไปอยู่ตรงไหนก็ได้ที่มีที่ว่าง แต่ละ Node จะเก็บข้อมูล 2 ส่วนคือ 1. ตัวข้อมูลเอง และ 2. Pointer (ลายแทง) ที่ชี้ไปยังที่อยู่ของ Node ถัดไป,
- เปรียบเทียบ: เหมือนการเล่นเกมล่าขุมทรัพย์ น้องเปิดล็อกเกอร์แรก เจอคำใบ้บอกให้ไปดูล็อกเกอร์ที่ 123 พอไปเปิดล็อกเกอร์ที่ 123 ก็เจอคำใบ้ให้ไปล็อกเกอร์ที่ 847 ต่อไปเรื่อยๆ ไม่ว่าเพื่อนจะมาเพิ่มกี่คน ก็แค่แทรกคำใบ้ชี้ไปหาเพื่อนคนนั้นได้เลย ไม่ต้องย้ายที่ใหม่!
ตารางเปรียบเทียบพลังรบ (Time Complexity - Big O),
| Operation (การทำงาน) | Array | Linked List | ผู้ชนะ / เหตุผล |
|---|---|---|---|
| Reading (อ่านข้อมูล) | $O(1)$ | $O(n)$ | Array ชนะเลิศ! เพราะเราคำนวณ Address ล่วงหน้าและกระโดดไปหา Index ที่ต้องการได้ทันที (Random Access) ส่วน Linked List ต้องไล่ตามลายแทงทีละโหนด (Sequential Access) |
| Insertion (แทรกข้อมูล) | $O(n)$ | $O(1)$ | Linked List ชนะ! การแทรกโหนดใหม่ทำได้ง่ายๆ แค่เปลี่ยนทิศทางของ Pointer แต่ Array ต้องขยับ (Shift) ข้อมูลตัวอื่นๆ ถอยหลังไปทั้งหมดเพื่อให้เกิดช่องว่าง |
| Deletion (ลบข้อมูล) | $O(n)$ | $O(1)$ | Linked List ชนะ! แค่ดึง Pointer ข้ามโหนดที่ต้องการลบทิ้งไป โหนดนั้นก็จะหายไปจากระบบทันที ส่วน Array ก็ต้องเลื่อนข้อมูลตัวอื่นขึ้นมาอุดรอยรั่ว |

4. 💻 ร่ายมนต์โค้ด (Show me the Code)
ลองมาดูการร่ายมนต์สร้าง Linked List ขั้นพื้นฐาน (Singly Linked List) ในภาษา Python เพื่อให้เห็นว่า “ลายแทง” หรือ Pointer มันเชื่อมต่อกันยังไงครับ
# 1. สร้างพิมพ์เขียวของ Node (มี 2 ส่วน: ข้อมูล และ Pointer)
class Node:
def __init__(self, data):
self.data = data # เก็บตัวข้อมูล (Data)
self.next = None # ลายแทงชี้ไปหาโหนดถัดไป (Pointer เริ่มต้นเป็น None)
# 2. ลองสร้างตัวแปรโหนดมา 3 ตัว (กระจายอยู่ตรงไหนก็ได้ใน Memory)
node_a = Node("Alice")
node_b = Node("Bob")
node_c = Node("Charlie")
# 3. เริ่มผูกลายแทง (Linked) เข้าด้วยกัน! (นี่คือจุดที่ Array ไม่มี)
node_a.next = node_b # A ชี้ไป B
node_b.next = node_c # B ชี้ไป C
# ตอนนี้เราได้ Linked List หน้าตา: Alice -> Bob -> Charlie -> None
# 4. แทรกข้อมูล "Zack" ระหว่าง A กับ B ด้วย Big-O ระดับ O(1)
node_z = Node("Zack")
# เวทมนตร์เปลี่ยนลายแทง (ไม่ต้อง Shift ข้อมูลคนอื่นเลย!)
node_a.next = node_z # ให้ A ชี้มาหา Z ก่อน
node_z.next = node_b # แล้วให้ Z ชี้กลับไปหา B
# กลายเป็น: Alice -> Zack -> Bob -> Charlie -> None ทันที!5. 🛡️ เคล็ดลับจากคัมภีร์ลับ (Under the Hood / Pro-Tips)
เห็นตารางด้านบน น้องๆ หลายคนอาจจะคิดว่า “ถ้างั้นใช้ Linked List ตลอดเลยดีกว่า แทรกและลบไวตั้ง $O(1)$!” พี่ขอเบรกไว้ก่อนเลยครับ! ในคัมภีร์ The Algorithm Design Manual และตำรา Grokking Algorithms ได้ให้ข้อควรระวังตัวโตๆ ไว้ดังนี้:
- Memory Locality (ความเป็นมิตรกับ CPU Cache): แม้ Array จะลบ/แทรกช้า แต่การที่ข้อมูลเรียง “ติดกัน” ในระดับฮาร์ดแวร์ ทำให้ CPU สามารถดึงข้อมูลขึ้นมาอ่านรวดเดียวได้ (Cache-friendly) โปรแกรมเมอร์สายรีด Performance มักจะเลือก Array เพราะมันรันเร็วกว่า Linked List มากในความเป็นจริงหากเป็นการเข้าถึงข้อมูลแบบ Sequential หรือ Random,
- Pointer Overhead (กินพื้นที่เปล่าๆ): Linked List ต้องเสียพื้นที่ Memory ในการเก็บ Pointer (ลายแทง) ยิ่งถ้าเป็น Doubly Linked List ต้องเก็บทั้งชี้ไปข้างหน้าและชี้กลับหลัง ถ้าข้อมูลเรามีขนาดเล็กนิดเดียว การมาเสียพื้นที่ให้ Pointer อาจจะเป็นการผลาญทรัพยากรโดยใช่เหตุ (Wasted memory),
- ข้อกังขาเรื่องเวลา $O(1)$ ของ Linked List: การแทรกหรือลบจะเป็น $O(1)$ ก็ต่อเมื่อ “เรารู้ตำแหน่งของโหนดนั้นและมี Pointer ชี้อยู่แล้ว” เท่านั้นนะครับ! ถ้าจู่ๆ น้องสั่งว่า “จงลบค่า Charlie” น้องก็ต้องเสียเวลา $O(n)$ เพื่อวิ่งหา Charlie ตั้งแต่หัวแถวอยู่ดี,
6. 🏁 บทสรุป (To be continued…)
สรุปแบบง่ายๆ ไว้ตอบสัมภาษณ์: “ใช้ Array เมื่อระบบเน้นการค้นหาและอ่านข้อมูล (Read-heavy) แต่ใช้ Linked List เมื่อระบบเน้นการแทรกและลบข้อมูลบ่อยๆ โดยไม่ได้สุ่มอ่าน (Write/Insert-heavy)” การเลือกเครื่องมือให้ถูกชิ้นคือศิลปะของการเป็น Senior Engineer ที่แท้จริงครับ
โครงสร้างทั้งสองนี้ยังเป็น “รากฐาน (Building Blocks)” นำไปประกอบร่างเป็นโครงสร้างข้อมูลที่ซับซ้อนขึ้นอย่าง Stacks, Queues, Hash Tables หรือ Trees อีกด้วย ในตอนหน้า เราจะมาดูการเอา Array และ Linked List ไปอัปเกรดเป็น Stacks & Queues (เข้าก่อนออกก่อน vs เข้าหลังออกก่อน) กันต่อ อย่าลืมไปลองเขียนโค้ดผูก Node เล่นๆ ดูกันนะครับ!
ต้องการที่ปรึกษาด้านการออกแบบ System Architecture และพัฒนาระบบซอฟต์แวร์สเกลใหญ่ให้กับองค์กรของคุณ? ทีมงาน WP Solution พร้อมให้บริการออกแบบและพัฒนาซอฟต์แวร์แบบครบวงจร ดูรายละเอียดบริการของเราได้ที่: www.wpsolution2017.com หรือพูดคุยปรึกษาเบื้องต้นได้ที่ Line: wisit.p