Linked Lists: ปฐมบทแห่งโครงสร้างข้อมูลแบบไดนามิก และขบวนรถไฟแห่งโลกหน่วยความจำ
บทนำ: เส้นทางที่ปรับเปลี่ยนได้เสมอในโลกหน่วยความจำ
สวัสดีครับน้องๆ วิศวกรและเพื่อนนักพัฒนาชาว www.123microcontroller.com ทุกคน! วันนี้วิศวกรขอบตาดำๆ จะพาทุกคนไปทำความรู้จักกับโครงสร้างข้อมูลที่เป็นเสมือน “กระดูกสันหลัง” ของการเขียนโปรแกรมระดับระบบ (System Programming) นั่นก็คือ Linked Lists (รายการเชื่อมโยง) ครับ
ในบทความก่อนๆ เราพูดถึง Array กันไปแล้ว ซึ่งเปรียบเสมือนตู้ล็อกเกอร์ที่ถูกสร้างมาแบบตายตัวและเรียงติดกัน แต่ในโลกของฮาร์ดแวร์และซอฟต์แวร์ที่ซับซ้อน บางครั้งเราไม่รู้ล่วงหน้าว่าจะมีข้อมูลเข้ามามากน้อยแค่ไหน การจอง Array ขนาดใหญ่เผื่อไว้ก็ทำให้เปลือง RAM ของไมโครคอนโทรลเลอร์ไปอย่างเปล่าประโยชน์ Linked Lists จึงถือกำเนิดขึ้นมาเพื่อแก้ปัญหานี้ครับ วันนี้เราจะมาเจาะลึกกันว่า ในภาพรวมของ Data Structures ระดับโลก แหล่งข้อมูลชั้นครูกล่าวถึง Linked Lists ไว้อย่างไร และทำไมมันถึงเป็นจิ๊กซอว์ชิ้นสำคัญในการสร้างโครงสร้างข้อมูลอื่นๆ ครับ!
Linked Lists ในมุมมองของ Data Structures
ในบริบทที่กว้างขึ้นของ Data Structures นั้น Linked List ไม่ได้เป็นแค่รูปแบบการเก็บข้อมูลธรรมดา แต่เป็น “บล็อกตัวต่อ (Building Block)” ที่ให้ความยืดหยุ่นสูงสุดในการจัดการหน่วยความจำแบบไดนามิก (Dynamic Memory Management) แหล่งข้อมูลได้อธิบายบทบาทของมันไว้ดังนี้ครับ:
- 1. สถาปัตยกรรมแบบ Self-Referential Structure:
Linked List เกิดจากการสร้างโครงสร้างข้อมูลแบบอ้างอิงตัวเอง (Self-referential structures) ในภาษา C ซึ่งก็คือโครงสร้าง
structที่มี Pointer ชี้ไปยังโครงสร้างประเภทเดียวกันกับตัวมันเอง ข้อมูลแต่ละก้อนใน Linked List จะถูกเรียกว่า “Node (โหนด)” โดยแบ่งเป็น 2 ส่วนหลักๆ คือ ส่วนเก็บข้อมูล (Data) และส่วนที่เก็บ Pointer (Link) เพื่อชี้ไปยังโหนดถัดไป - 2. ขบวนรถไฟแห่งหน่วยความจำ (Scattered Memory):
ต่างจาก Array ที่ต้องใช้พื้นที่หน่วยความจำต่อเนื่องกัน (Contiguous memory) Linked List ใช้การจองหน่วยความจำแบบไดนามิก (เช่น
malloc()) โหนดแต่ละโหนดจึงสามารถกระจัดกระจาย (Scattered) อยู่ที่ไหนก็ได้ใน Heap memory Pointer จะทำหน้าที่เป็นเหมือน “ข้อต่อขบวนรถไฟ” ที่เกี่ยวข้อมูลที่กระจัดกระจายเหล่านั้นเข้าด้วยกันเป็นสายโซ่ทางตรรกะ - 3. ความได้เปรียบเหนือ Array (Pros & Cons):
จุดเด่นที่สุดของ Linked List คือความรวดเร็วในการแทรก (Insert) หรือลบ (Delete) ข้อมูล หากเราต้องการแทรกข้อมูลตรงกลาง เราเพียงแค่ “ตัดต่อสายไฟ (Pointer)” ใหม่ โดยไม่ต้องเลื่อน (Shift) ข้อมูลตัวอื่นๆ เลย ซึ่งต่างจาก Array ที่ต้องย้ายข้อมูลทั้งแผง แต่ข้อเสียที่ต้องแลกมาคือ มันไม่มี Random Access เราไม่สามารถกระโดดไปอ่านข้อมูลตำแหน่งที่ 10 ได้ทันทีแบบ
arr[10]แต่เราต้องเริ่มเดินจากโหนดแรก (Head) ไปตาม Pointer ทีละโหนด (Sequential access) - 4. รากฐานของ Abstract Data Types (ADTs) อื่นๆ: ความยืดหยุ่นของ Linked List ทำให้มันถูกนำไปใช้เป็นรากฐานในการสร้าง Data Structures ชนิดอื่นๆ เกือบทั้งหมด ตัวอย่างเช่น การนำไปสร้าง Stacks (LIFO), Queues (FIFO), การใช้ทำ Hash Tables แบบ Chaining เพื่อแก้ปัญหาการชนกันของคีย์ หรือแม้แต่การสร้างโครงสร้างแบบ Non-linear อย่าง Trees และ Graphs (Adjacency lists)
- 5. รูปแบบย่อยที่ทรงพลัง (Variations): นอกจาก Singly Linked List ที่เดินหน้าทางเดียวแล้ว ยังมีการดัดแปลงโครงสร้างเป็น Doubly Linked List (มี Pointer ชี้ทั้งเดินหน้าและถอยหลัง) และ Circular Linked List (โหนดสุดท้ายชี้กลับมาที่โหนดแรก) เพื่อรองรับอัลกอริทึมที่ซับซ้อนขึ้นด้วย

ตัวอย่างการสร้าง Singly Linked List ระดับพื้นฐาน
มาดูตัวอย่างการสร้าง Singly Linked List พื้นฐานแบบ Clean Code พร้อมการแทรกโหนดใหม่ที่ตำแหน่งหัว (Head) ของ List กันครับ (ตัวอย่างนี้มีการใช้ malloc() ซึ่งในหน้างาน Embedded จริงมักจะปรับเป็นการจองจาก Memory Pool แบบที่เราเคยคุยกันไปครับ)
#include <stdio.h>
#include <stdlib.h>
/* 1. สร้าง Self-Referential Structure สำหรับ Node */
typedef struct Node {
int data; // ส่วนเก็บข้อมูล
struct Node* next; // Pointer ชี้ไปยัง Node ถัดไป
} Node;
/* ฟังก์ชันเพิ่มโหนดใหม่ไว้ที่ด้านหน้าสุด (Push Front) */
Node* insert_at_head(Node* head, int new_data) {
/* 2. จองหน่วยความจำแบบ Dynamic สำหรับ Node ใหม่ */
Node* new_node = (Node*)malloc(sizeof(Node));
/* 🛡️ Best Practice: ตรวจสอบเสมอว่า Memory เต็มหรือไม่ */
if (new_node == NULL) {
printf("Error: Memory allocation failed!\n");
return head; // คืนค่า head เดิมกลับไป
}
/* 3. กำหนดค่าและเชื่อมต่อ Pointer */
new_node->data = new_data;
new_node->next = head; // ให้โหนดใหม่ชี้ไปที่ head เดิม
/* คืนค่าโหนดใหม่ เพื่อให้กลายเป็น head คนใหม่ของ List */
return new_node;
}
/* ฟังก์ชันสำหรับทำลาย List และคืนหน่วยความจำ */
void free_list(Node* head) {
Node* current = head;
Node* next_node;
/* เดินไปตามสายโซ่ และคืนหน่วยความจำทีละโหนด */
while (current != NULL) {
next_node = current->next; // เก็บที่อยู่ถัดไปไว้ก่อน
free(current); // ลบโหนดปัจจุบัน
current = next_node; // ขยับไปโหนดถัดไป
}
}
int main(void) {
Node* head = NULL; // เริ่มต้นด้วย List ว่าง (NULL pointer)
// เพิ่มข้อมูลเข้า Linked List
head = insert_at_head(head, 30);
head = insert_at_head(head, 20);
head = insert_at_head(head, 10);
// เมื่อเลิกใช้งาน ต้องคืนหน่วยความจำเสมอ
free_list(head);
return 0;
}
กฎเหล็กและการจัดการ Pointers ให้พ้นภัย
การจัดการ Linked List คือการเล่นกับ Pointers และ Dynamic Memory โดยตรง ซึ่งเป็นแหล่งรวมบั๊กสุดอันตราย มาตรฐานการเขียนโค้ดแนะนำข้อควรระวังดังนี้ครับ:
- จุดอ่อนของสายโซ่ (The Weakest Link): การทำ Linked List เหมือนสุภาษิตที่ว่า “คุณจะแข็งแกร่งเท่ากับจุดที่อ่อนแอที่สุดของคุณ” หากคุณเผลอเขียนโค้ดที่ทำ Pointer ขาดหายไปเพียงเส้นเดียว (เช่น ลืมเซฟค่า Pointer ถัดไปก่อนทำการลบโหนด) คุณจะไม่สามารถเข้าถึงข้อมูลที่เหลือใน List ได้อีกเลยตลอดกาล (และ RAM ส่วนนั้นก็หลุดลอยตามไปด้วย)
- ระวังปีศาจ Memory Leak: Linked List ถูกสร้างขึ้นมาด้วย
malloc()(หรือการจองจาก Pool) ดังนั้นเมื่อใช้งานเสร็จ คุณต้องเขียนฟังก์ชัน (เช่นfree_list) เพื่อวิ่งวนลูปสั่งfree()โหนดทุกตัวเสมอ การปล่อยให้โปรแกรมจบไป (หรือ Task ถูกทำลาย) โดยไม่คืน Memory จะทำให้ RAM ของระบบหายไปเรื่อยๆ จนดับกลางอากาศ - Dangling Pointers (Pointer ผีดิบ): หลังจากที่คุณ
free()โหนดใดๆ ไปแล้ว ห้ามนำ Pointer ของโหนดนั้นมาดึงค่า (Dereference) อีกเด็ดขาด เพราะอาจทำให้ระบบ Crash หรือเกิดพฤติกรรมที่ไม่สามารถคาดเดาได้ (Undefined Behavior) กฎที่ดีคือควรเซต Pointer ตัวนั้นให้เป็นNULLทันทีที่เราทำลายมัน - เช็ค NULL เสมอ: กฎเหล็กของการท่องไปใน Linked List (Traversal) คือต้องเช็คว่าโหนดปัจจุบัน และโหนด
nextมีค่าเป็นNULLหรือไม่ก่อนดึงข้อมูลเสมอ เพื่อป้องกันปัญหา Null Pointer Dereference ที่ทำให้ไมโครคอนโทรลเลอร์สั่ง Hard Fault Reset ตัวเองทันที
สรุป (Conclusion)
ในจักรวาลของ Data Structures นั้น Linked Lists เปรียบเสมือน “จิ๊กซอว์ชิ้นแรก” ที่โปรแกรมเมอร์สาย C ต้องต่อให้เป็นครับ มันสอนให้เราเข้าใจถึงพลังของการจัดการหน่วยความจำแบบไดนามิก และการใช้ Pointers สร้างความสัมพันธ์ระหว่างข้อมูล แม้มันจะไม่ได้อ่านข้อมูลได้เร็วปรี๊ดแบบ Array แต่มันคือโครงสร้างพื้นฐานที่มอบความยืดหยุ่นในการสร้างโครงสร้างขั้นสูงอื่นๆ อย่าง Stacks, Queues และ Trees ให้กับเราครับ
หวังว่าบทความนี้จะช่วยให้น้องๆ เข้าใจลอจิกของการต่อสายไฟให้กับข้อมูลใน RAM ได้อย่างทะลุปรุโปร่งมากขึ้นนะครับ! ใครมีคำถามเกี่ยวกับการประยุกต์ใช้ Linked List ในงานเซ็นเซอร์ หรืออยากดูโค้ด Doubly Linked List แบบเต็มๆ แวะเข้ามาตั้งกระทู้พูดคุยกันต่อได้ที่บอร์ด www.123microcontroller.com ของพวกเราได้เลยครับ แล้วพบกันใหม่ในบทความหน้า Happy Coding ครับทุกคน!