🔴 520213-165 กลุ่ม 2

Fundamental of Data Structures
โครงสร้างข้อมูลพื้นฐานและการประยุกต์

สรุปเนื้อหาเลกเชอร์ที่ตรงกับสไลด์อาจารย์ 100% พร้อมเฉลยแบบฝึกหัดแล็บ โค้ดภาษา Java และแนวข้อสอบกลางภาค

📅 สอบข้อเขียน Lec: 5 ก.ย. 2569 (09:00 - 12:00) 📅 สอบปฏิบัติ Lab: 5 ก.ย. 2569 (13:00 - 16:00)

📂 สไลด์และแล็บต้นฉบับ (PDF)

id="lec-ch-1">

1 ตัวแปร, หน่วยความจำ และการเขียน/อ่านไฟล์ข้อมูล

🎯 ครูบอก: บทนี้ต้องเข้าใจ Primitive vs Reference, Stack vs Heap, และ Serialization ให้แม่น เพราะเป็นรากฐานของทุกบทต่อไป!

1. Primitive vs Reference Types

หัวข้อPrimitive TypeReference Type
ตัวอย่างint, double, boolean, char, byte, long, float, shortString, int[], Object, ArrayList
เก็บที่Stack — เก็บค่าโดยตรงStack (เก็บ reference) + Heap (เก็บ object จริง)
Default value0, false, '\0' ฯลฯnull
Copy behaviorCopy by value (แยกกัน)Copy by reference (ชี้ object เดียวกัน)

2. Stack vs Heap Memory

📚 Stack
• เก็บ local variables และ Primitive
• เก็บ reference ของ Objects
• LIFO — สร้าง/ทำลายตาม method call
• ขนาดจำกัด (StackOverflowError)
• เร็วกว่า Heap
🏪 Heap
• เก็บ Object จริงทั้งหมด
• ใหญ่กว่า Stack มาก
• Garbage Collector ดูแลการคืนหน่วยความจำ
• ช้ากว่า Stack เล็กน้อย
• เกิด OutOfMemoryError เมื่อเต็ม
int x = 10;          // x อยู่ใน Stack, ค่า 10 อยู่ใน Stack
String s = "Hello"; // s (reference) อยู่ใน Stack, Object "Hello" อยู่ใน Heap
int[] arr = {1,2,3}; // arr (reference) อยู่ใน Stack, array จริงอยู่ใน Heap

3. Garbage Collection

Java ไม่ต้องจัดการ Memory เอง — JVM มี Garbage Collector (GC) ที่คอยเก็บ Object ที่ไม่มีตัวแปรอ้างอิงอีกต่อไป

String s = new String("Hello");  // Object ถูกสร้าง
s = null;                         // Reference ถูกลบ
// ตอนนี้ Object "Hello" ถูก GC เก็บได้แล้ว (eligible for GC)

4. File I/O & Serialization

Serialization คือการแปลง Object เป็น byte stream เพื่อบันทึกลงไฟล์หรือส่งผ่าน Network

⚠️ เงื่อนไข Serialization: Class ต้อง implement java.io.Serializable (เป็น Marker Interface ไม่มี method)
ตัวแปรที่ไม่ต้องการบันทึกใช้ keyword transient
import java.io.*;

// --- Class ที่จะ Serialize ---
class Student implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    int age;
    transient String password; // จะไม่ถูกบันทึก!
    
    public Student(String name, int age, String password) {
        this.name = name; this.age = age; this.password = password;
    }
}

// --- บันทึก Object ลงไฟล์ (Serialize) ---
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("student.dat"))) {
    Student s = new Student("Alice", 20, "secret123");
    oos.writeObject(s);
    System.out.println("Saved!");
} catch (IOException e) { e.printStackTrace(); }

// --- อ่าน Object จากไฟล์ (Deserialize) ---
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("student.dat"))) {
    Student s = (Student) ois.readObject();
    System.out.println(s.name + ", " + s.age);   // Alice, 20
    System.out.println(s.password);               // null (transient!)
} catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }

🧠 แบบฝึกหัด — Lec 1: Memory & File I/O

ข้อ 1: จงอธิบายความแตกต่างระหว่าง Stack Memory และ Heap Memory ด้วยตัวอย่างโค้ด Java
ข้อ 2: transient keyword ใน Java มีผลอย่างไรต่อ Serialization?
ข้อ 3: จงเขียนโค้ด Java เพื่อ Serialize ArrayList ของ String ลงไฟล์ "names.dat"
id="lec-ch-2">

2 การใช้งานคลาส Arrays, อาร์เรย์แบบยืดหยุ่น และฟังก์ชันเรียกตัวเอง

🎯 ครูบอก: Recursion ออกสอบแน่นอน ต้องเข้าใจ Base Case + Recursive Step และวาด Call Stack ได้ ส่วน Dynamic Array ต้องเข้าใจ grow strategy!

1. Array พื้นฐานใน Java

// Declaration & Initialization
int[] arr = new int[5];         // [0,0,0,0,0] (default 0)
int[] arr2 = {10, 20, 30};     // shorthand initialization

// Access: O(1) - ความลับคือ memory contiguous
int x = arr2[1];  // 20

// 2D Array
int[][] matrix = new int[3][4]; // 3 rows, 4 cols
matrix[0][0] = 1;

// Arrays utility class
import java.util.Arrays;
Arrays.sort(arr2);           // เรียง ascending
Arrays.fill(arr, 99);        // ใส่ค่า 99 ทุก element
System.out.println(Arrays.toString(arr2)); // "[10, 20, 30]"
int idx = Arrays.binarySearch(arr2, 20); // หาด้วย Binary Search (ต้องเรียงก่อน)
OperationTime Complexityเหตุผล
Access by indexO(1)Memory address = base + index × size
Search (unsorted)O(n)อาจต้องดูทุก element
Search (sorted + Binary)O(log n)แบ่งครึ่งทุก iteration
Insert at endO(1)ถ้ามีที่ว่าง
Insert at middleO(n)ต้อง shift elements

2. Dynamic Array (ArrayList)

Array ธรรมดามีขนาดคงที่ — Dynamic Array แก้ปัญหานี้ด้วยการ "ขยายตัวเอง" เมื่อเต็ม:

class DynamicArray {
    private int[] data;
    private int size;     // จำนวน element จริง
    private int capacity; // ขนาดของ array ที่จอง

    public DynamicArray(int initialCap) {
        capacity = initialCap;
        data = new int[capacity];
        size = 0;
    }

    public void add(int value) {
        if (size == capacity) {
            grow(); // ขยายก่อน!
        }
        data[size++] = value;
    }

    private void grow() {
        capacity *= 2; // ขยายเป็น 2 เท่า (Amortized O(1))
        int[] newData = new int[capacity];
        System.arraycopy(data, 0, newData, 0, size);
        data = newData;
        System.out.println("Grew to capacity: " + capacity);
    }

    public int get(int index) {
        if (index < 0 || index >= size) throw new IndexOutOfBoundsException();
        return data[index];
    }
    
    public int size() { return size; }
}
💡 Amortized O(1): การ grow เป็น 2 เท่าทุกครั้ง ทำให้ต้น copy เฉลี่ยต่อการ add = O(1) (ไม่ใช่ O(n) ทุกครั้ง)

3. Recursion — ฟังก์ชันเรียกตัวเอง

ทุก Recursive function ต้องมี 2 ส่วน:

  • 🛑 Base Case — เงื่อนไขหยุด (ป้องกัน infinite loop → StackOverflow)
  • 🔄 Recursive Step — เรียกตัวเองด้วยปัญหาที่เล็กลง
// Factorial: n! = n × (n-1)!
public static long factorial(int n) {
    if (n <= 1) return 1;            // Base Case
    return n * factorial(n - 1);    // Recursive Step
}
// factorial(4) = 4 × factorial(3) = 4 × 3 × 2 × 1 = 24

// Fibonacci: fib(n) = fib(n-1) + fib(n-2)
public static int fib(int n) {
    if (n <= 1) return n;             // Base Case: fib(0)=0, fib(1)=1
    return fib(n-1) + fib(n-2);      // Recursive Step (O(2^n) -- ช้า!)
}

// String Reverse
public static String reverse(String s) {
    if (s.isEmpty()) return s;        // Base Case
    return reverse(s.substring(1)) + s.charAt(0); // Recursive Step
}
// reverse("ABC") = reverse("BC") + 'A' = "CBA"

🧠 แบบฝึกหัด — Lec 2: Arrays & Recursion

ข้อ 1: Dynamic Array ต่างจาก Array ธรรมดาอย่างไร? และทำไม grow เป็น 2 เท่าถึง efficient กว่าการขยายทีละ 1?
ข้อ 2: จงเขียน Recursive function คำนวณผลรวมของตัวเลข 1 ถึง n และวาด Call Stack สำหรับ sum(4)
ข้อ 3: StackOverflowError เกิดขึ้นเมื่อไหร่? และจะป้องกันได้อย่างไรเมื่อเขียน Recursive function?

3 โครงสร้างข้อมูล Singly Linked List (รายการเชื่อมโยงเดี่ยว)

1. โครงสร้างและการจัดสรรหน่วยความจำแรม (Memory Layout vs Array)

พวกเรามาดูก่อนครับว่า **Singly Linked List** แตกต่างจาก **Array** อย่างไรในหน่วยความจำแรม:

  • Array (หน่วยความจำต่อเนื่อง): จองพื้นที่บล็อกแรมติดกันเป็นพืด ข้อดีคือคำนวณตำแหน่งได้ทันทีทำให้เข้าถึงดัชนีใดๆ ได้เร็ว O(1) (เรียกว่า Random Access) แต่ข้อเสียคือต้องระบุขนาดล่วงหน้า และขยายขนาดไม่ได้ง่ายๆ
  • Linked List (หน่วยความจำกระจัดกระจาย): โหนดของลิงก์ลิสต์แต่ละโหนดสามารถสร้างกระจัดกระจายอยู่ที่ใดก็ได้ในแรมกองส่วน Heap โดยแต่ละโหนดจะเชื่อมต่อกันด้วย **พอยน์เตอร์ตัวชี้ (Reference pointer)** ชี้ตามกันไปทีละตัว ทำให้ขยายขนาดได้ยืดหยุ่นไม่มีข้อจำกัด

โครงสร้างของโหนด (Node Structure):

ประกอบด้วย 2 ส่วนหลักคือ:

  1. Data (ส่วนข้อมูล): เก็บข้อมูลวัตถุ เช่น วัตถุ Student, ตัวเลข, หรือตัวอักษร
  2. Next (ส่วนตัวชี้): เป็นพอยน์เตอร์อ้างอิงตำแหน่งแรมที่เก็บตัวแปรชี้ไปยัง **โหนดตัวถัดไป** ในรายการ โดยโหนดสุดท้ายสุดของลิสต์ ตัวชี้ Next จะเก็บค่าว่างเปล่าเป็น `null`

2. ตารางเปรียบเทียบประสิทธิภาพเชิงเวลา (Time Complexity Comparison)

การดำเนินการ (Operations) Array (ขนาดคงที่ N) Singly Linked List (ขนาด N) เหตุผลประกอบ (ลอจิกพอยน์เตอร์)
เข้าถึงตามดัชนี (Access by Index) O(1) (เร็วที่สุด) O(N) (ต้องเริ่มวนลูปไล่จากหัว) Linked List ไม่มี Random Access ต้องวนลูป p = p.next ไปทีละขั้นตอน
เพิ่ม/ลบที่จุดเริ่มต้น (Head) O(N) (ต้องขยับของในอาเรย์หลบ) O(1) (เร็วที่สุด) Linked List แค่เปลี่ยนทิศทางพอยน์เตอร์ตัวชี้ของ head ใหม่โดยไม่ต้องย้ายข้อมูลอื่น
เพิ่ม/ลบที่จุดสิ้นสุด (Tail) O(1) (หากอาร์เรย์ไม่เต็ม) O(N) (ต้องวนลูปหาตัวรองสุดท้าย) ยกเว้นในกรณีที่เรามีการเก็บพอยน์เตอร์ตัวชี้ท้าย tail พิเศษไว้จะลบ/เพิ่มได้เร็วกว่าเดิม

3. อัลกอริทึมการจัดการโหนด (Linked List Algorithms)

  • การเพิ่มข้อมูลที่หัว (Prepend): สร้างโหนดใหม่ ➜ ชี้โหนดใหม่ไปที่ head ปัจจุบัน ➜ อัปเดต head ให้ชี้มาที่โหนดใหม่ (ทำงานในเวลาคงที่ O(1))
  • การเพิ่มข้อมูลที่ท้าย (Append): สร้างโหนดใหม่ ➜ วนลูปตรวจสอบจากหัวไปเรื่อยๆ จนเจอโหนดที่ next == null (โหนดท้ายสุด) ➜ ชี้ Next ของโหนดท้ายนั้นมาที่โหนดใหม่
  • การลบโหนดใดๆ (Delete Node): ค้นหาโหนดเป้าหมายโดยถือพอยน์เตอร์ตัวก่อนหน้า (Previous Node) ➜ สั่งเปลี่ยนตัวชี้ Next ของตัวก่อนหน้าให้ข้ามข้ามไปชี้โหนดถัดไปของตัวที่ต้องการลบ (โหนดเป้าหมายจะหลุดการเชื่อมต่อและโดน Garbage Collector เคลียร์แรมทิ้งอัตโนมัติ)
💡 จุดระวังในการเขียนโค้ดเพื่อสอบ: NullPointerException
ทุกครั้งที่มีการวนลูปท่องไปในลิสต์ (เช่น p = p.next) หรือก่อนสลับตัวชี้ ต้องตรวจสอบให้มั่นใจเสมอว่า `p != null` และ `p.next != null` ไม่เช่นนั้น JVM จะพังและโยน NullPointerException เด้งออกมาทันที

🏋️ โจทย์ซ้อมการไล่พอยน์เตอร์ (Conceptual Practice)

ลองมาไล่ทิศทางชี้ของพอยน์เตอร์ในแรมผ่านโจทย์วิเคราะห์สถานการณ์เพื่อสอบทฤษฎีกันครับ:

📝 บันทึกส่วนตัว — บทที่ 3

✓ บันทึกอัตโนมัติแล้ว
id="lec-ch-4">

4 ลิงก์ลิสต์สองทิศทางและโครงสร้างสแตก (Doubly Linked List & Stack ADT)

🎯 ครูบอก: Doubly Linked List เน้น pointer 2 ทิศทาง (prev & next) และ O(1) deletion! Stack เน้น LIFO และการประยุกต์ใช้ (Infix→Postfix) ออกสอบแน่นอน!

1. Doubly Linked List (DLL)

แต่ละ Node มี 3 ส่วน: prev pointer, data, และ next pointer

null ← [prev|10|next] ↔ [prev|20|next] ↔ [prev|30|next] → null
         head                                       tail

2. DLL vs Singly Linked List

OperationSingly LLDoubly LL
Access by indexO(n)O(n)
Insert/Delete ที่รู้ Node แล้วO(1) insert, O(n) delete (ต้องหา prev)O(1) ทั้ง insert & delete!
Traverse แบบย้อนกลับ❌ ไม่ได้✅ ได้ (ผ่าน prev)
Memoryน้อยกว่ามากกว่า (เก็บ prev ด้วย)
class DNode {
    int data;
    DNode prev, next;
    DNode(int data) { this.data = data; }
}

class DoublyLinkedList {
    DNode head, tail;

    // O(1) — เพิ่มหน้า
    public void addFirst(int data) {
        DNode newNode = new DNode(data);
        if (head == null) { head = tail = newNode; return; }
        newNode.next = head;
        head.prev = newNode;
        head = newNode;
    }

    // O(1) — ลบท้าย (DLL ทำได้ LL เดี่ยวทำไม่ได้ O(1))
    public void removeLast() {
        if (tail == null) return;
        if (head == tail) { head = tail = null; return; }
        tail = tail.prev;
        tail.next = null;
    }
}

3. Stack ADT (Abstract Data Type)

Stack คือโครงสร้างข้อมูล LIFO (Last In, First Out) — อันสุดท้ายที่เข้าคืออันแรกที่ออก

📌 Analogy: Stack = ตะกร้าซ้อนจาน — วางจานซ้อนกัน จะหยิบได้เฉพาะจานบนสุด
OperationความหมายComplexity
push(x)ใส่ x ที่ topO(1)
pop()เอาออกจาก top และคืนค่าO(1)
peek()ดูค่า top โดยไม่เอาออกO(1)
isEmpty()ตรวจสอบว่าว่างหรือไม่O(1)

4. การประยุกต์ใช้ Stack: Infix → Postfix

Infix: A + B * C → Postfix: ABC*+ (ไม่ต้องใช้วงเล็บ!)

/* Algorithm สำหรับ Infix → Postfix:
   1. อ่านสัญลักษณ์ทีละตัว
   2. ถ้าเป็น operand (ตัวเลข/ตัวอักษร) → output ทันที
   3. ถ้าเป็น '(' → push ลง stack
   4. ถ้าเป็น ')' → pop จนเจอ '(' แล้ว discard '('
   5. ถ้าเป็น operator → pop operator ที่ priority >= ตัวเอง แล้ว push ตัวเอง
   6. เมื่ออ่านหมดแล้ว → pop ทุกอันที่เหลือใน stack
*/

// ตัวอย่าง: "A+B*C"
// อ่าน A → output: A
// อ่าน + → push: [+]
// อ่าน B → output: AB
// อ่าน * → * > + → push: [+,*]
// อ่าน C → output: ABC
// หมด → pop * แล้ว + → output: ABC*+

🧠 แบบฝึกหัด — Lec 4: DLL & Stack

ข้อ 1: ทำไม Doubly Linked List ถึง delete node ที่รู้แล้วได้เป็น O(1) แต่ Singly Linked List ไม่ได้?
ข้อ 2: จงแปลง Infix expression (A+B)*(C-D) ให้เป็น Postfix แบบ step-by-step
id="lec-ch-5">

5 โครงสร้างข้อมูลคิวแบบวงกลมและตัวจัดเก็บ (Queue ADT & Circular Queue)

🎯 ครูบอก: Queue เน้น FIFO และ Circular Queue เน้นสูตร (rear+1) % capacity ต้องเข้าใจว่าทำไมต้องใช้ Modulo และวาด diagram ได้!

1. Queue ADT (First In, First Out)

Queue = คิวต่อแถว — คนที่มาก่อนได้รับบริการก่อน

OperationความหมายComplexity
enqueue(x)ใส่ x ที่ท้าย (rear)O(1)
dequeue()เอาออกจากหน้า (front) และคืนค่าO(1)
peek()ดูค่า front โดยไม่เอาออกO(1)
isEmpty()ตรวจว่าว่างหรือไม่O(1)

2. ปัญหาของ Linear Queue กับ Array

// Linear Queue ปัญหา: "False Full"
// [_][_][_][10][20] → dequeue 2 ครั้ง → [_][_][_][_][20]
// front=4, rear=4 ดูเหมือนเต็มแต่มีช่องว่างหน้า!
//                                         ↑front

// แก้ด้วย Circular Queue: ใช้ index แบบ modulo %

3. Circular Queue (แก้ปัญหา False Full)

class CircularQueue {
    int[] data;
    int front, rear, size, capacity;
    
    CircularQueue(int capacity) {
        this.capacity = capacity;
        data = new int[capacity];
        front = rear = size = 0;
    }
    
    // เต็มหรือว่าง?
    boolean isFull()  { return size == capacity; }
    boolean isEmpty() { return size == 0; }
    
    // ใส่ที่ท้าย
    void enqueue(int val) {
        if (isFull()) throw new RuntimeException("Queue Full!");
        data[rear] = val;
        rear = (rear + 1) % capacity;  // ← กุญแจสำคัญ! Wrap around
        size++;
    }
    
    // เอาออกจากหน้า
    int dequeue() {
        if (isEmpty()) throw new RuntimeException("Queue Empty!");
        int val = data[front];
        front = (front + 1) % capacity;  // ← Wrap around
        size--;
        return val;
    }
}
💡 ทำไมต้องใช้ % capacity?
เพราะเมื่อ rear ถึงท้าย array จะ "หมุนวน" กลับมาที่ index 0 ได้ทันที
ตัวอย่าง: capacity=5, rear=4 → enqueue → rear=(4+1)%5=0 (กลับมาหัว!)

🧠 แบบฝึกหัด — Lec 5: Queue

ข้อ 1: อธิบาย "False Full" ปัญหาของ Linear Queue กับ Array และ Circular Queue แก้ปัญหานี้อย่างไร
ข้อ 2: Circular Queue ที่มี capacity=5 ทำงานต่อไปนี้: enqueue(A), enqueue(B), enqueue(C), dequeue(), enqueue(D), enqueue(E). front, rear, size มีค่าเป็นอะไร?
id="lec-ch-6">

6 คิวจัดลำดับความสำคัญและโครงสร้างโครงต้นไม้ฮีป (Priority Queue & Heaps)

🎯 ครูบอก: Priority Queue + Heap ออกสอบเรื่อง Array Representation และ heapify! ต้องจำสูตร index: left child = 2i+1, right child = 2i+2, parent = (i-1)/2 และเข้าใจ heapify-up/down!

1. Priority Queue ADT

Priority Queue คือ Queue ที่ให้บริการตาม Priority (ลำดับความสำคัญ) ไม่ใช่ลำดับการมาถึง

  • Min-Priority Queue — element ที่มี priority น้อยสุดออกก่อน
  • Max-Priority Queue — element ที่มี priority มากสุดออกก่อน
📌 ตัวอย่างชีวิตจริง: ห้องฉุกเฉิน — ผู้ป่วยวิกฤต (priority สูง) ได้รับบริการก่อน ไม่ว่าจะมาถึงทีหลัง

2. Binary Heap

Heap คือ Complete Binary Tree ที่มี Heap-Order Property:

  • Max-Heap: Parent ≥ Children ทุก node
  • Min-Heap: Parent ≤ Children ทุก node
  • Shape Property: เติม node จากซ้ายไปขวาในแต่ละ level (Complete Binary Tree)

3. Array Representation (สำคัญมาก!)

เก็บ Heap เป็น Array โดยไม่ต้องใช้ pointer!

       90          ← index 0
      /   \
    75     80       ← index 1, 2
   /  \   /
  40  60 70        ← index 3, 4, 5

Array: [90, 75, 80, 40, 60, 70]
Index:   0   1   2   3   4   5

สูตร (สำคัญ!):
- Left child ของ i  → index 2i + 1
- Right child ของ i → index 2i + 2
- Parent ของ i      → index (i - 1) / 2  (integer division)

4. Heap Operations

// Heapify-Up (bubble-up): หลัง insert
// ใส่ element ใหม่ที่ท้าย แล้ว swap กับ parent ถ้า > parent (Max-Heap)
void heapifyUp(int[] heap, int i) {
    while (i > 0) {
        int parent = (i - 1) / 2;
        if (heap[i] > heap[parent]) {   // Max-Heap condition
            swap(heap, i, parent);
            i = parent;
        } else break;
    }
}

// Heapify-Down (sift-down): หลัง extractMax
// เอา root ออก ใส่ tail ที่ root แล้ว swap กับ child ที่ใหญ่กว่า
void heapifyDown(int[] heap, int i, int size) {
    while (2*i + 1 < size) {           // มี left child
        int largest = i;
        int left = 2*i + 1, right = 2*i + 2;
        if (left < size && heap[left] > heap[largest])   largest = left;
        if (right < size && heap[right] > heap[largest]) largest = right;
        if (largest == i) break;       // ถูกที่แล้ว
        swap(heap, i, largest);
        i = largest;
    }
}
OperationTime Complexityเหตุผล
insertO(log n)heapify-up ขึ้น tree สูง log n ชั้น
extractMax/MinO(log n)heapify-down ลง tree สูง log n ชั้น
peek (getMax/Min)O(1)root เสมอ
build heap (n elements)O(n)bottom-up heapify

🧠 แบบฝึกหัด — Lec 6: Priority Queue & Heap

ข้อ 1: Heap ถูกเก็บในรูปแบบ array [100, 50, 80, 30, 40, 70] (Max-Heap). จงหา Parent และ Children ของ node ที่ index 2
ข้อ 2: เมื่อ insert ค่า 90 ลง Max-Heap [80, 70, 60, 50, 40] จงแสดง heapify-up process ทีละขั้น
ข้อ 3: จงอธิบาย Time Complexity ของ insert และ extractMax ใน Binary Heap และเปรียบเทียบกับ Sorted Array

01 การเขียนโค้ดบันทึกอาร์เรย์และจัดการ Serialization ใน Java

1. การเขียนอาร์เรย์วัตถุลงไฟล์ Plain Text ด้วย PrintWriter

ตัวอย่างการบันทึกข้อมูลอาร์เรย์ Product ลงไฟล์ "data.txt" คั่นข้อมูลแต่ละบรรทัดด้วยคอมมา:

import java.io.FileWriter;
import java.io.PrintWriter;

public class SaveTextFile {
    public static void main(String[] args) {
        Product[] products = {
            new Product("Notebook", 25000.0),
            new Product("Mouse", 850.0),
            new Product("Keyboard", 1500.0)
        };

        try (PrintWriter out = new PrintWriter(new FileWriter("data.txt"))) {
            for (Product p : products) {
                out.println(p.getName() + "," + p.getPrice());
            }
            System.out.println("บันทึกข้อมูลแบบ Text File สำเร็จ!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. การทำ Object Serialization (บันทึกวัตถุก้อนแรมลงไฟล์ .ser)

คลาสโมเดลข้อมูลจำลองจำต้อง implements Serializable ก่อนจึงจะใช้ ObjectOutputStream ได้:

import java.io.Serializable;

class Product implements Serializable {
    private static final long serialVersionUID = 1L; // ป้องกันคลาสเสื่อมรุ่น
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
    public String getName() { return name; }
    public double getPrice() { return price; }
    @Override
    public String toString() { return name + " (" + price + ")"; }
}

โค้ดฝั่งเขียนวัตถุลงไฟล์ไบนารี (Serialization):

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializeObjects {
    public static void main(String[] args) {
        Product[] list = { new Product("Tablet", 12000.0), new Product("Phone", 18000.0) };
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"))) {
            oos.writeObject(list); // เขียนอาร์เรย์ทั้งชุดลงไฟล์
            System.out.println("ทำ Serialization สำเร็จ!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. การทำ Object Deserialization (การอ่านวัตถุกลับคืนจากไฟล์ .ser)

การอ่านข้อมูลวัตถุไบนารีที่ถูกเซฟไว้ในไฟล์ .ser กลับคืนมาเป็นออบเจกต์บนหน่วยความจำแรม พร้อมการ Cast ชนิดข้อมูลและการปิดสตรีมข้อมูลอัตโนมัติด้วย Try-with-Resources:

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class DeserializeObjects {
    public static void main(String[] args) {
        // ใช้ Try-with-Resources เพื่อปิดไฟล์อัตโนมัติ
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"))) {
            
            // อ่านก้อนข้อมูลออบเจกต์และจำเป็นต้องแคสต์ชนิดข้อมูล (Object Casting) ให้ตรงกับแม่แบบคลาส
            Product[] list = (Product[]) ois.readObject();
            
            System.out.println("ทำ Deserialization สำเร็จ! ข้อมูลวัตถุที่ดึงกลับมา:");
            for (Product p : list) {
                System.out.println(p);
            }
            
        } catch (FileNotFoundException e) {
            System.err.println("ข้อผิดพลาด: ไม่พบไฟล์ที่ระบุสำหรับโหลดวัตถุ!");
        } catch (IOException e) {
            System.err.println("ข้อผิดพลาด: เกิดข้อขัดข้องในการอ่านข้อมูล - " + e.getMessage());
        } catch (ClassNotFoundException e) {
            System.err.println("ข้อผิดพลาด: หาเทมเพลตของคลาส Product ในระบบไม่เจอ!");
        }
    }
}

4. การจัดการข้อผิดพลาดทางข้อมูล (Java Exception Handling in File I/O)

เวลาทำแล็บและข้อสอบเขียนโปรแกรม ทุกการกระทำที่แตะกับฮาร์ดดิสก์หรือระบบเครือข่าย อาจทำให้โปรแกรมล่มได้ตลอดเวลาหากเกิดข้อผิดพลาดทางกายภาพ Java จึงบังคับให้เราจัดการกับข้อยกเว้น (Checked Exceptions) เหล่านี้:

💡 ทริคดูแลลูกศิษย์จากครู: เพื่อความกะทัดรัดและปลอดภัย ครูแนะนำให้เราใช้ Try-with-Resources (ใส่การเปิดสตรีมไว้ในวงเล็บหลัง try (...)) เพราะระบบจะช่วยปิดทรัพยากรให้ทันทีเมื่อรันเสร็จ โดยไม่ต้องมาคอยเช็ค finally { if(stream != null) stream.close(); } ให้รกรุงรังและป้องกันปัญหาไฟล์ล็อก (File Lock) ได้เป็นปลิดทิ้งเลยครับ!

🏋️ โจทย์ซ้อมเขียนโค้ดแล็บ Serialization & Exception Handling

โจทย์ทบทวนและฝึกวิเคราะห์แนวข้อสอบปฏิบัติเพื่อให้นักเรียนพร้อมลุยสนามสอบจริงครับ:

📝 จดบันทึกแล็บย่อ

✓ บันทึกเรียบร้อย

02 เฉลยโจทย์สเขียนคำนวณและ Recursion (Dynamic Array & String Reverse)

1. แบบฝึกหัดที่ 1: การเขียนเมธอด addFirst() บน Dynamic Array ที่ลบข้อบกพร่อง

สร้างอาร์เรย์ชั่วคราวขยายขนาดขึ้น 1 ช่อง ย้ายข้อมูลแบบเลื่อนดัชนีไปทางขวา และแทรกค่าใหม่ไว้ที่ index 0:

public static int[] addFirst(int[] source, int x) {
    // 1. สร้างอาร์เรย์ใหม่ขนาดบวกหนึ่ง
    int[] temp = new int[source.length + 1];
    
    // 2. นำข้อมูลใหม่ไปแทรกที่หน้าสุด
    temp[0] = x;
    
    // 3. คัดลอกข้อมูลเดิมไปวางถัดไป (i+1)
    for (int i = 0; i < source.length; i++) {
        temp[i + 1] = source[i];
    }
    
    return temp;
}

2. โค้ดขยายขนาดความจุของ Custom ArrayList (The grow() Method)

เมื่อเพิ่มข้อมูลลงใน ArrayList จนเต็มขอบเขตของ Static Array ปัจจุบัน เราจะต้องสร้างอาร์เรย์ใหม่ที่มีขนาดกว้างขึ้นเป็น 2 เท่า แล้วนำอาร์เรย์เดิมย้ายเข้าสู่บ้านใหม่ครับ:

public class MyArrayList {
    private int[] data;
    private int size;
    private int capacity;

    public MyArrayList() {
        this.capacity = 5; // กำหนดค่าเริ่มต้นความจุไว้ที่ 5 ช่อง
        this.data = new int[capacity];
        this.size = 0;
    }

    // เมธอดสำหรับขยายขนาดอาร์เรย์โดยคูณ 2 เพื่อลดจำนวนครั้งการจัดสรรแรมใหม่ (Amortized O(1))
    private void grow() {
        capacity = capacity * 2;
        int[] temp = new int[capacity];
        for (int i = 0; i < size; i++) {
            temp[i] = data[i]; // ทำการย้ายฝากข้อมูล
        }
        data = temp; // เบน reference ไปชี้ที่หน่วยความจำก้อนใหม่
        System.out.println("ขยายขนาดโครงสร้างข้อมูลสำเร็จ! ความจุใหม่: " + capacity);
    }

    public void add(int element) {
        if (size == capacity) {
            grow(); // เรียกใช้เมธอดขยายขนาด
        }
        data[size++] = element;
    }
}

3. เมธอดเขียนโปรแกรมเรียกตัวเองเพื่อกลับตัวอักษร (String Reverse Recursion)

โจทย์ปฏิบัติที่มักนำมาออกสอบทฤษฎีข้อเขียน จงเขียนเมธอด Recursion เพื่อสลับกลับด้าน String:

public class ReverseRecursion {
    public static String reverse(String str) {
        // Base Case (จุดยุติ): สตริงว่างหรือเหลือตัวอักษรเดียว ส่งกลับคืนตรงๆ
        if (str == null || str.length() <= 1) {
            return str;
        }
        // Recursive Step: นำอักขระตัวหลังสุดบวกกับค่าที่เรียกตัวเองด้วยขอบเขตย่อ
        return str.charAt(str.length() - 1) + reverse(str.substring(0, str.length() - 1));
    }

    public static void main(String[] args) {
        System.out.println(reverse("Structure")); // ผลลัพธ์: erutcurtS
    }
}

4. การประยุกต์ใช้แนวคิดแบบ Recursion อื่นๆ (Factorial, Fibonacci & Tower of Hanoi)

หัวใจสำคัญของ Recursion คือ 1. จุดยุติ (Base Case) ป้องกัน Stack Overflow และ 2. Recursive Step ที่ย่นย่อขนาดปัญหาลงเรื่อยๆ ลองมาดูตัวอย่างยอดฮิตสามชุดที่นักเรียนต้องเจอแน่นอนครับ:

// 1. ฟังก์ชันหาค่า Factorial (n!)
public static int factorial(int n) {
    if (n <= 1) return 1; // Base case: 0! และ 1! มีค่าเท่ากับ 1
    return n * factorial(n - 1); // Recursive step
}

// 2. ฟังก์ชันหาค่าลำดับฟีโบนัชชี (Fibonacci Number)
public static int fibonacci(int n) {
    if (n == 0) return 0; // Base case 1
    if (n == 1) return 1; // Base case 2
    return fibonacci(n - 1) + fibonacci(n - 2); // Recursive step
}

// 3. ปัญหาไขปริศนาหอคอยฮานอย (Tower of Hanoi)
public static void solveHanoi(int n, char fromRod, char toRod, char auxRod) {
    if (n == 1) { // Base Case: ถ้ามีจานเดียว ย้ายไปตรงๆ ได้เลย
        System.out.println("ย้ายจาน 1 จากเสา " + fromRod + " ไปยังเสา " + toRod);
        return;
    }
    // ขั้นตอนที่ 1: ย้ายจานที่ซ้อนกัน n-1 จาน จากเสาเริ่ม ไปพักเสาช่วย (auxRod)
    solveHanoi(n - 1, fromRod, auxRod, toRod);
    // ขั้นตอนที่ 2: ย้ายจานขนาดใหญ่ที่สุดจานสุดท้ายตรงฐาน ไปยังเสาเป้าหมาย
    System.out.println("ย้ายจาน " + n + " จากเสา " + fromRod + " ไปยังเสา " + toRod);
    // ขั้นตอนที่ 3: ย้ายจาน n-1 จานที่เคยพักไว้เสาช่วย กลับทับบนเสาเป้าหมาย
    solveHanoi(n - 1, auxRod, toRod, fromRod);
}

🏋️ โจทย์ซ้อมเขียนโค้ดแล็บ Dynamic Array & Recursion

โจทย์จำลองวิเคราะห์แนวทางข้อสอบทฤษฎีและเขียนโปรแกรมอัตนัยของคลาสอาร์เรย์และฟังก์ชันเวียนเกิดครับ:

📝 จดบันทึกแล็บย่อ

✓ บันทึกเรียบร้อย

03 Singly Linked List Implementation

1. โค้ดภาษา Java ของ Singly Linked List แบบสมบูรณ์

นี่คือรูปแบบการสร้างคลาสเชื่อมโยงเดี่ยวในภาษา Java พร้อมระบบคำนวณค่าเฉลี่ยและการจัดการโหนดที่ถูกต้อง:

SinglyLinkedList.java
class Node {
    int data;
    Node next;

    public Node(int data) {
        this.data = data;
        this.next = null;
    }
}

public class SinglyLinkedList {
    Node head = null;

    public void addFirst(int data) {
        Node newNode = new Node(data);
        newNode.next = head;
        head = newNode;
    }

    public void addLast(int data) {
        Node newNode = new Node(data);
        if (head == null) {
            head = newNode;
            return;
        }
        Node current = head;
        while (current.next != null) {
            current = current.next;
        }
        current.next = newNode;
    }

    public boolean remove(int key) {
        if (head == null) return false;
        
        if (head.data == key) {
            head = head.next;
            return true;
        }
        
        Node current = head;
        while (current.next != null && current.next.data != key) {
            current = current.next;
        }
        
        if (current.next != null) {
            current.next = current.next.next;
            return true;
        }
        return false;
    }

    public double getAverage() {
        if (head == null) return 0.0;
        int sum = 0;
        int count = 0;
        Node current = head;
        while (current != null) {
            sum += current.data;
            count++;
            current = current.next;
        }
        return (double) sum / count;
    }
}

🏋️ โจทย์ซ้อมเขียนโค้ดแล็บระดับข้อสอบปฏิบัติจริง

โจทย์จำลองข้อสอบเขียนโปรแกรมภาษา Java สำหรับการดัดแปลงและต่อสลับพอยน์เตอร์ในแล็บครับ:

📝 จดบันทึกแล็บย่อ

✓ บันทึกเรียบร้อย

04 โครงสร้างและแนวคำตอบปฏิบัติ Stack (Infix to Postfix & Postfix Evaluation)

1. การสร้างคลาส Stack ด้วย Array แบบสมบูรณ์ (Custom Array-Based Stack)

ตัวอย่างการจองหน่วยความจำแรมแบบ Static Array เพื่อใช้งานจัดเก็บคลาสจัดคิวแบบ LIFO (Last-In, First-Out) ซึ่งมีประสิทธิภาพในเรื่องความเร็วเป็น O(1) ทุกเมธอดหลัก:

import java.util.EmptyStackException;

public class MyArrayStack {
    private int[] stackArray;
    private int top;         // ชี้ตำแหน่งบนสุดปัจจุบัน
    private int capacity;

    public MyArrayStack(int capacity) {
        this.capacity = capacity;
        this.stackArray = new int[capacity];
        this.top = -1; // ค่าลบหนึ่งแสดงถึงสแตกว่างเปล่า
    }

    public void push(int value) {
        if (isFull()) {
            throw new StackOverflowError("ไม่สามารถเพิ่มข้อมูลได้เนื่องจากสแตกเต็ม!");
        }
        stackArray[++top] = value; // เลื่อนดัชนีขึ้นและนำค่าไปวาง
    }

    public int pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return stackArray[top--]; // ดึงค่าออกไปใช้งานและลดดัชนีลง
    }

    public int peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return stackArray[top]; // ขอดูตัวบนสุดเฉยๆ โดยไม่หยิบออก
    }

    public boolean isEmpty() {
        return top == -1;
    }

    public boolean isFull() {
        return top == capacity - 1;
    }
}

2. ขั้นตอนการแปลงสมการแบบสากล Infix to Postfix (Step-by-Step Conversion Table)

การแสดงลอจิกตารางขั้นตอนการอ่านข้อมูลตัวสัญลักษณ์และตัวโอเปอเรเตอร์เพื่อจัดระเบียบสมการคณิตศาสตร์จาก A + B * C โดยให้ * มีความสำคัญสูงกว่า + ดังตารางนี้จ้า:

Token ที่อ่าน Stack Status (จากล่างขึ้นบน) Output String (Postfix) คำอธิบายลอจิกปฏิบัติงาน (Action)
A [ ] (ว่าง) A เป็นตัวถูกดำเนินการ (Operand) เขียนต่อท้ายข้อความ Output ทันที
+ [ + ] A เป็นเครื่องหมาย (Operator) พลักลงสแตกเพราะว่างเปล่าอยู่
B [ + ] A B เขียนต่อท้ายข้อความ Output ทันที
* [ +, * ] A B เนื่องจากเครื่องหมาย * มีสิทธิ์สำคัญสูงกว่า + ในสแตก จึง Push สะสมเข้าไปได้เลย
C [ +, * ] A B C เขียนต่อท้ายข้อความ Output ทันที
(จบสมการ) [ + ] A B C * สมการหมดแล้ว ทำการดึงเครื่องหมายตัวบนสุดของสแตก (Pop) ไปต่อท้าย Output
(จบสมการ) [ ] (ว่าง) A B C * + ดึงเครื่องหมายชิ้นสุดท้ายไปต่อจนหมดสแตก สำเร็จผลลัพธ์เป็น ABC*+

3. โค้ดประยุกต์: การหาคำตอบจากสมการ Postfix (Postfix Expression Evaluation)

ตัวอย่างการไล่ลูปและดึงค่าออกจาก Stack ใน Java เพื่อคำนวณผลลัพธ์คณิตศาสตร์:

import java.util.Stack;

public class PostfixEval {
    public static int evaluate(String exp) {
        Stack<Integer> stack = new Stack<>();
        
        // วนลูปอ่านทีละตัวอักษร
        for (int i = 0; i < exp.length(); i++) {
            char c = exp.charAt(i);
            
            // ข้ามช่องว่าง
            if (c == ' ') continue;
            
            // หากเป็นตัวเลข ให้ push ลงสแตก
            if (Character.isDigit(c)) {
                stack.push(c - '0'); // แปลง char เป็น int
            } else {
                // ดึงตัวเลขบนสุด 2 ตัวมาคำนวณเมื่อเจอเครื่องหมาย
                int operand2 = stack.pop(); // ตัวที่หยิบมาตัวแรกคือตัวขวา
                int operand1 = stack.pop(); // ตัวถัดมาคือตัวซ้าย
                
                switch (c) {
                    case '+': stack.push(operand1 + operand2); break;
                    case '-': stack.push(operand1 - operand2); break;
                    case '*': stack.push(operand1 * operand2); break;
                    case '/': stack.push(operand1 / operand2); break;
                }
            }
        }
        return stack.pop(); // ผลลัพธ์สุดท้าย
    }

    public static void main(String[] args) {
        System.out.println(evaluate("5 3 2 * +")); // 5 + (3 * 2) = 11
    }
}

🏋️ โจทย์ซ้อมเขียนโค้ดแล็บ Stack Applications

แบบฝึกหัดทบทวนสำหรับการสลับข้อมูลและการประยุกต์โครงสร้างสแต็กในข้อสอบปฏิบัติการเรียนครับ:

📝 จดบันทึกแล็บย่อ

✓ บันทึกเรียบร้อย

05 โค้ดใช้งานคิวและเปรียบเทียบลำดับ Priority ใน Java

1. โค้ดการสร้างคิวอย่างง่ายด้วยอาร์เรย์ (Array-Based Queue)

ตัวอย่างการนำอาร์เรย์แบบคงที่มาสร้างคิวธรรมดา โดยชี้พิกัด Front ในการดึงออก และ Rear ในการเพิ่มเข้าจ้า:

public class ArrayQueue {
    private int[] queueArray;
    private int front;
    private int rear;
    private int size;
    private int capacity;

    public ArrayQueue(int capacity) {
        this.capacity = capacity;
        this.queueArray = new int[capacity];
        this.front = 0;
        this.rear = -1;
        this.size = 0;
    }

    public void enqueue(int x) {
        if (size == capacity) {
            throw new IllegalStateException("คิวเต็มความจุแล้ว!");
        }
        rear++;
        queueArray[rear] = x;
        size++;
    }

    public int dequeue() {
        if (size == 0) {
            throw new IllegalStateException("คิวว่างเปล่า ดึงข้อมูลออกไม่ได้!");
        }
        int removed = queueArray[front];
        front++; // ขยับชี้ตัวถัดไป
        size--;
        return removed;
    }

    public int peek() {
        if (size == 0) throw new IllegalStateException("คิวว่าง!");
        return queueArray[front];
    }
}

2. การรันคิวแบบวงกลมเพื่อประหยัดหน่วยความจำ (Circular Queue Implementation)

ข้อจำกัดของ Array คิวธรรมดาคือ Front และ Rear จะเดินขวาไปเรื่อยๆ จนหลุดขอบอาร์เรย์ทำให้เสียช่องว่างข้างหน้าไปโดยเปล่าประโยชน์ circular queue จึงถูกพัฒนาขึ้นมาเพื่อนำพอยน์เตอร์หมุนวนกลับมาใช้ซ้ำแบบครบรอบวงกลมโดยใช้ตัวดำเนินการทางคณิตศาสตร์ Modulo % ดังนี้ครับ:

public class CircularQueue {
    private int[] data;
    private int front;
    private int rear;
    private int size;
    private int capacity;

    public CircularQueue(int capacity) {
        this.capacity = capacity;
        this.data = new int[capacity];
        this.front = 0;
        this.rear = -1;
        this.size = 0;
    }

    public boolean isFull() { return size == capacity; }
    public boolean isEmpty() { return size == 0; }

    public void enqueue(int element) {
        if (isFull()) {
            throw new IllegalStateException("Circular Queue is Full!");
        }
        // เลื่อน rear แบบครบรอบวงกลมด้วย Modulo
        rear = (rear + 1) % capacity;
        data[rear] = element;
        size++;
    }

    public int dequeue() {
        if (isEmpty()) {
            throw new IllegalStateException("Circular Queue is Empty!");
        }
        int value = data[front];
        // ขยับ front แบบครบรอบวงกลม
        front = (front + 1) % capacity;
        size--;
        return value;
    }
}

3. เฉลยตัวอย่างเปรียบเทียบวัตถุแบบกรรมาธิการคิว (Student Priority Queue with Custom Comparator)

แสดงการตั้งค่าและเขียนคลาส Student และ Comparator ในการคัดเลือกโหนดความสำคัญตามเกรดเฉลี่ย GPAX:

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

class Student {
    private String name;
    private double gpax;

    public Student(String name, double gpax) {
        this.name = name;
        this.gpax = gpax;
    }
    public String getName() { return name; }
    public double getGpax() { return gpax; }
    @Override
    public String toString() { return name + ":" + gpax; }
}

// คลาสตรวจสอบเปรียบเทียบเกรดเฉลี่ยจากสูงสุดไปต่ำสุด
class StudentGpaxComparator implements Comparator<Student> {
    @Override
    public int compare(Student s1, Student s2) {
        // หากต้องการเรียงลำดับ GPAX จากมากไปน้อย (ให้คน GPAX สูง ได้ออกมาก่อน)
        if (s1.getGpax() < s2.getGpax()) return 1;
        if (s1.getGpax() > s2.getGpax()) return -1;
        return 0;
    }
}

public class PriorityQueueDemo {
    public static void main(String[] args) {
        // ส่ง Comparator เข้าตอนสร้างตัวแปร
        Queue<Student> pq = new PriorityQueue<>(new StudentGpaxComparator());
        
        pq.add(new Student("Jeff", 2.50));
        pq.add(new Student("Ben", 3.85));
        pq.add(new Student("Tim", 3.12));
        
        // เวลาถอดค่าออกจากคิว จะแสดงผลตามเกรดเฉลี่ยที่สูงที่สุดออกมาเสมอก่อนเพื่อน
        while (!pq.isEmpty()) {
            System.out.println(pq.poll());
        }
    }
}

4. การเปรียบเทียบข้อมูลด้วย Comparable Interface

นอกจาก Comparator ที่เขียนแยกไฟล์แล้ว เราสามารถเขียนให้ตัววัตถุชิ้นนั้นๆ เรียนรู้วิธีเปรียบเทียบตัวเองได้โดยตรงผ่านการเชื่อมต่อ Comparable และโอเวอร์ไรด์เมธอด compareTo ครับ:

💡 ความแตกต่างที่ออกข้อสอบบ่อย:
- Comparable (เทียบตนเอง): เหมาะสำหรับการเรียงลำดับดั้งเดิมที่เป็นธรรมชาติที่สุด เช่น รหัสประจำตัว หรือ ลำดับอายุ (ใช้เมธอด compareTo)
- Comparator (คนตัดสินภายนอก): เป็นคลาสแยกต่างหาก เหมาะเมื่อต้องการเรียงลำดับได้หลากหลายวิธีแบบยืดหยุ่น เช่น จะเรียงตามคะแนน หรือตามตัวอักษรก็เปลี่ยนคลาสไป (ใช้เมธอด compare)

🏋️ โจทย์ซ้อมเขียนโค้ดแล็บ Queue & Priority Queue

โจทย์ปฏิบัติที่เตรียมสำหรับการออกแบบระบบคิวในงานต่างๆ พร้อมระบบเฉลยภาษา Java ครับ:

📝 จดบันทึกแล็บย่อ

✓ บันทึกเรียบร้อย

📝 ทดสอบความพร้อม: Data Structures Mock Exam

แบบทดสอบปรนัย (Multiple Choice) จำนวน 15 ข้อ ครอบคลุมทั้งทฤษฎี ลำดับการคำนวณโค้ด แรมพอยน์เตอร์ และลอจิกคิว/สแตกจำลองข้อสอบจริง

⚙️ เครื่องมือฝึกปฏิบัติจำลองข้อสอบแล็บ Java GUI & Data Structures

ข้อสอบแล็บปลายภาคส่วนใหญ่ออกแบบให้อ้างอิงตาม NetBeans JFrame Swing App โดยนักศึกษาจะต้องเขียนสร้างคลาสข้อมูล และพัฒนาคลาสจัดเก็บโครงสร้างข้อมูลขึ้นมาใช้งานเอง จากนั้นนำมาเชื่อมโยงเข้ากับปุ่มกดในฟอร์ม GUI (เช่น add(), get(), remove()/pop()) หน้าต่างจำลองด้านล่างนี้จะช่วยให้เข้าใจพฤติกรรมในหน่วยความจำ RAM และสเปก Java Event Listeners ไปพร้อมๆ กัน!

MyJavaApp (MainFrame.java) - NetBeans Form Preview
Student Management System
Student ID:
Name:
GPAX:
Select Structure:
Index / Query:
(สำหรับปุ่ม Get Data)
JTextArea Output (SystemConsole):
--- ยินดีต้อนรับเข้าสู่โปรแกรมจำลองข้อสอบแล็บ --- กรุณากรอกข้อมูล นักศึกษา (ID, Name, GPA) เลือกโครงสร้างข้อมูล และกดทดสอบการทำงานปุ่มต่างๆ

🧠 RAM Memory Heap Layout

💻 NetBeans Output / Java Execution

// ระบบพร้อมรัน Java Action Event Listener...

📂 โครงสร้างไฟล์โค้ดภาษา Java ใน NetBeans (เทมเพลตมาตรฐานสำหรับสอบแล็บ)

คลิกเลือกหัวข้อไฟล์เพื่อศึกษาโค้ดที่ถูกต้องที่ต้องนำไปเขียนในโปรแกรม NetBeans ข้อสอบจะสั่งให้สร้างเมธอดสำคัญ เช่น การขยายอาร์เรย์เป็น 2 เท่า, การผูกลิงก์ Node ใน List และลอจิกการแกะข้อมูลจากช่อง Text Field

คลาสเก็บค่าข้อมูลพื้นฐานมาตรฐาน (Data Entity) มักมีฟิลด์ตัวแปรประเภทคอนสตรัคเตอร์ (Constructor) เมธอด Getter/Setter และการเขียนทับเมธอดพิมพ์สายข้อมูล (toString())

public class Student {
    private int id;
    private String name;
    private double gpax;

    public Student(int id, String name, double gpax) {
        this.id = id;
        this.name = name;
        this.gpax = gpax;
    }

    public int getId() { return id; }
    public String getName() { return name; }
    public double getGpax() { return gpax; }

    @Override
    public String toString() {
        return "Student[ID=" + id + ", Name=" + name + ", GPAX=" + gpax + "]";
    }
}

ลอจิกเบื้องหลังการจำลอง Dynamic Array: หากข้อมูลเต็มความจุ (Capacity) จะทำการสร้างอาร์เรย์ใหม่ที่มีขนาดใหญ่เป็น 2 เท่า คัดลอกข้อมูลเก่าไปวาง แล้วสลับ Reference

public class MyArrayList {
    private Student[] data;
    private int size;
    private int capacity;

    public MyArrayList() {
        this.capacity = 3; // กำหนดขนาดตั้งต้นให้น้อยเพื่อทดสอบการขยายขนาด
        this.data = new Student[capacity];
        this.size = 0;
    }

    public int size() { return size; }
    public int capacity() { return capacity; }

    public void add(Student student) {
        if (size == capacity) {
            ensureCapacity(); // เรียกคำสั่งขยายอาเรย์ถ้าเต็ม
        }
        data[size++] = student;
    }

    private void ensureCapacity() {
        capacity = capacity * 2;
        Student[] temp = new Student[capacity];
        for (int i = 0; i < size; i++) {
            temp[i] = data[i]; // คัดลอกข้อมูล
        }
        data = temp; // เบน Reference หลักมาชี้ที่อาเรย์ใหม่
    }

    public Student get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("ดัชนีเกินขอบเขตจริง!");
        }
        return data[index];
    }

    public void remove(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("ดัชนีเกินขอบเขต!");
        }
        for (int i = index; i < size - 1; i++) {
            data[i] = data[i+1]; // ขยับตัวขวามาทดแทนฝั่งซ้าย
        }
        data[--size] = null; // เคลียร์พอยน์เตอร์ตำแหน่งท้ายสุด
    }
}

ประกอบไปด้วยสองคลาสย่อยคือคลาสโหนด Node และคลาสลิสต์หลัก มักมีเมธอดการท่องโหนดเพื่อดึงข้อมูลทีละดัชนี (Get Data) หรือแทรกโหนดที่จุดเริ่มต้น/จุดสุดท้าย

public class SinglyLinkedList {
    private class Node {
        Student student;
        Node next;
        
        Node(Student student) {
            this.student = student;
            this.next = null;
        }
    }

    private Node head = null;
    private int size = 0;

    public int size() { return size; }

    public void addLast(Student student) {
        Node newNode = new Node(student);
        if (head == null) {
            head = newNode;
        } else {
            Node current = head;
            while (current.next != null) {
                current = current.next; // เดินโหนดจนถึงท้ายสุด
            }
            current.next = newNode;
        }
        size++;
    }

    public Student get(int index) {
        if (index < 0 || index >= size) return null;
        Node current = head;
        for (int i = 0; i < index; i++) {
            current = current.next; // ท่องไปตามสายตัวชี้
        }
        return current.student;
    }

    public void removeFirst() {
        if (head != null) {
            head = head.next; // เบนพอยน์เตอร์ขยับไปตัวถัดไป
            size--;
        }
    }
}

โหนดย่อยจะมีทั้งตัวชี้ชี้ไปข้างหน้า (next) และตัวชี้ชี้ย้อนหลังกลับ (prev) ช่วยทำให้ท่องเดินหน้าและถอยหลังได้สะดวก

public class DoublyLinkedList {
    private class Node {
        Student student;
        Node next;
        Node prev;
        
        Node(Student student) {
            this.student = student;
            this.next = null;
            this.prev = null;
        }
    }

    private Node head = null;
    private Node tail = null;
    private int size = 0;

    public void addLast(Student student) {
        Node newNode = new Node(student);
        if (head == null) {
            head = tail = newNode;
        } else {
            tail.next = newNode;
            newNode.prev = tail;
            tail = newNode; // ย้ายตำแหน่งหางลิสต์
        }
        size++;
    }

    public Student get(int index) {
        if (index < 0 || index >= size) return null;
        Node current = head;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        return current.student;
    }

    public void removeLast() {
        if (tail == null) return;
        if (head == tail) {
            head = tail = null;
        } else {
            tail = tail.prev;
            tail.next = null; // ตัดตัวชี้ตัวสุดท้ายทิ้ง
        }
        size--;
    }
}

การสแตกสามารถเขียนอ้างอิงลิงก์ลิสต์ (Linked List) หรืออาร์เรย์ก็ได้ ตัวอย่างนี้เขียนอ้างอิงการพ่วงโหนดของลิงก์ลิสต์ ซึ่งการเพิ่มและถอนข้อมูลจะกระทำเฉพาะตรงหัวสแตก (Top) เท่านั้น

public class MyStack {
    private class Node {
        Student student;
        Node next;
        Node(Student student) { this.student = student; }
    }

    private Node top = null;
    private int size = 0;

    public boolean isEmpty() { return top == null; }
    public int size() { return size; }

    public void push(Student student) {
        Node newNode = new Node(student);
        newNode.next = top; // ให้โหนดใหม่ชี้ไปที่หัวเก่า
        top = newNode; // เลื่อนตำแหน่งหัวสแตกมาชี้ที่ตัวใหม่
        size++;
    }

    public Student pop() {
        if (isEmpty()) return null;
        Student s = top.student;
        top = top.next; // ย้ายตัวสแตกส่วนบนสุดลงล่างไปหนึ่งสเต็ป
        size--;
        return s;
    }

    public Student peek() {
        if (isEmpty()) return null;
        return top.student;
    }
}

นี่คือลอจิกสำคัญที่ใช้เขียนผูกกับปุ่มบนฟอร์ม NetBeans ในฟังก์ชั่นคลิกปุ่ม (Action Performed) เพื่อแกะข้อมูลค่าสตริงมาแปลงชนิดข้อมูลแล้วส่งเข้าไปใน Data Structure รวมถึงปุ่ม "Get Data"

import javax.swing.JOptionPane;

public class MainFrame extends javax.swing.JFrame {
    // การประกาศตัวแปรโครงสร้างแบบ Global
    private MyArrayList list = new MyArrayList();
    private MyStack stack = new MyStack();
    
    // โค้ด Event Listener ของปุ่ม "Insert / Add"
    private void btnInsertActionPerformed(java.awt.event.ActionEvent evt) {
        try {
            // 1. แกะข้อมูลจากกล่องข้อความ JTextField
            int id = Integer.parseInt(txtId.getText().trim());
            String name = txtName.getText().trim();
            double gpax = Double.parseDouble(txtGpax.getText().trim());
            
            if (name.isEmpty() || gpax < 0.0 || gpax > 4.0) {
                throw new IllegalArgumentException();
            }
            
            // 2. สร้างออบเจ็กต์ Student
            Student newStudent = new Student(id, name, gpax);
            
            // 3. ใส่เข้าไปในโครงสร้างข้อมูลที่เลือกในดรอปดาวน์
            list.add(newStudent);
            stack.push(newStudent);
            
            // 4. แสดงความสำเร็จและล้างฟิลด์กรอกข้อมูล
            txtAreaOutput.append("Added: " + newStudent.toString() + "\n");
            txtId.setText("");
            txtName.setText("");
            txtGpax.setText("");
            
        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this, "กรุณากรอกตัวเลข ID และ GPAX ให้ถูกต้อง!", "ข้อผิดพลาด", JOptionPane.ERROR_MESSAGE);
        } catch (IllegalArgumentException e) {
            JOptionPane.showMessageDialog(this, "กรุณากรอกชื่อและ GPAX (0.00-4.00) ให้ถูกต้อง!", "ข้อผิดพลาด", JOptionPane.WARNING_MESSAGE);
        }
    }

    // โค้ด Event Listener ของปุ่ม "Get Data"
    private void btnGetDataActionPerformed(java.awt.event.ActionEvent evt) {
        try {
            int idx = Integer.parseInt(txtIndex.getText().trim());
            
            // ตรวจหาพนักงานหรือนักศึกษาจากดัชนีในอาร์เรย์ลิสต์
            if (idx >= 0 && idx < list.size()) {
                Student s = list.get(idx);
                txtAreaOutput.append("Retrieving element [" + idx + "]: " + s.toString() + "\n");
            } else {
                JOptionPane.showMessageDialog(this, "ไม่พบข้อมูลที่ดัชนีดังกล่าว!", "ผลการค้นหา", JOptionPane.INFORMATION_MESSAGE);
            }
        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this, "กรุณาระบุดัชนีเป็นตัวเลข!", "ข้อผิดพลาด", JOptionPane.ERROR_MESSAGE);
        }
    }
}