รูปปกบทความ

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 (การทำงาน)ArrayLinked 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 ได้ให้ข้อควรระวังตัวโตๆ ไว้ดังนี้:

  1. Memory Locality (ความเป็นมิตรกับ CPU Cache): แม้ Array จะลบ/แทรกช้า แต่การที่ข้อมูลเรียง “ติดกัน” ในระดับฮาร์ดแวร์ ทำให้ CPU สามารถดึงข้อมูลขึ้นมาอ่านรวดเดียวได้ (Cache-friendly) โปรแกรมเมอร์สายรีด Performance มักจะเลือก Array เพราะมันรันเร็วกว่า Linked List มากในความเป็นจริงหากเป็นการเข้าถึงข้อมูลแบบ Sequential หรือ Random,
  2. Pointer Overhead (กินพื้นที่เปล่าๆ): Linked List ต้องเสียพื้นที่ Memory ในการเก็บ Pointer (ลายแทง) ยิ่งถ้าเป็น Doubly Linked List ต้องเก็บทั้งชี้ไปข้างหน้าและชี้กลับหลัง ถ้าข้อมูลเรามีขนาดเล็กนิดเดียว การมาเสียพื้นที่ให้ Pointer อาจจะเป็นการผลาญทรัพยากรโดยใช่เหตุ (Wasted memory),
  3. ข้อกังขาเรื่องเวลา $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