1 ตัวแปร, หน่วยความจำ และการเขียน/อ่านไฟล์ข้อมูล
1. Primitive vs Reference Types
| หัวข้อ | Primitive Type | Reference Type |
|---|---|---|
| ตัวอย่าง | int, double, boolean, char, byte, long, float, short | String, int[], Object, ArrayList |
| เก็บที่ | Stack — เก็บค่าโดยตรง | Stack (เก็บ reference) + Heap (เก็บ object จริง) |
| Default value | 0, false, '\0' ฯลฯ | null |
| Copy behavior | Copy by value (แยกกัน) | Copy by reference (ชี้ object เดียวกัน) |
2. Stack vs Heap Memory
• เก็บ local variables และ Primitive
• เก็บ reference ของ Objects
• LIFO — สร้าง/ทำลายตาม method call
• ขนาดจำกัด (StackOverflowError)
• เร็วกว่า 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
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
transient keyword ใน Java มีผลอย่างไรต่อ Serialization?
2 การใช้งานคลาส Arrays, อาร์เรย์แบบยืดหยุ่น และฟังก์ชันเรียกตัวเอง
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 (ต้องเรียงก่อน)
| Operation | Time Complexity | เหตุผล |
|---|---|---|
| Access by index | O(1) | Memory address = base + index × size |
| Search (unsorted) | O(n) | อาจต้องดูทุก element |
| Search (sorted + Binary) | O(log n) | แบ่งครึ่งทุก iteration |
| Insert at end | O(1) | ถ้ามีที่ว่าง |
| Insert at middle | O(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; }
}
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
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 ส่วนหลักคือ:
- Data (ส่วนข้อมูล): เก็บข้อมูลวัตถุ เช่น วัตถุ Student, ตัวเลข, หรือตัวอักษร
- 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 เคลียร์แรมทิ้งอัตโนมัติ)
ทุกครั้งที่มีการวนลูปท่องไปในลิสต์ (เช่น
p = p.next) หรือก่อนสลับตัวชี้ ต้องตรวจสอบให้มั่นใจเสมอว่า `p != null` และ `p.next != null` ไม่เช่นนั้น JVM จะพังและโยน NullPointerException เด้งออกมาทันที
🏋️ โจทย์ซ้อมการไล่พอยน์เตอร์ (Conceptual Practice)
ลองมาไล่ทิศทางชี้ของพอยน์เตอร์ในแรมผ่านโจทย์วิเคราะห์สถานการณ์เพื่อสอบทฤษฎีกันครับ:
โจทย์ข้อที่ 1: ไล่สายพอยน์เตอร์การลบโหนดกลางลิสต์
คำสั่งโจทย์: ลิงก์ลิสต์มีโหนดเรียงกันดังนี้: A -> B -> C -> null
หากต้องการลบโหนด B ออกจากลิสต์ โดยกำหนดให้ตัวแปรพอยน์เตอร์อ้างอิงชี้อยู่ที่ A (คือตัวแปร curr = A)
จงเขียนคำสั่งเปลี่ยนทางของพอยน์เตอร์เพียง 1 บรรทัดเพื่อลบโหนด B ออกอย่างสมบูรณ์
โจทย์ข้อที่ 2: วิเคราะห์ปัญหาหัวลิสต์หาย (Head Disconnection)
คำสั่งโจทย์: นักเรียนคนหนึ่งเขียนโค้ดเพิ่มโหนดใหม่ไว้ที่หัวลิงก์ลิสต์ (Prepend) ดังนี้:
head = newNode;
newNode.next = head;
โค้ดนี้ส่งผลเสียอย่างไรต่อโครงสร้างลิงก์ลิสต์ในแรมจำลอง? และลอจิกที่ถูกต้องคืออะไร?
โจทย์ข้อที่ 3: ความซับซ้อนของหัวเดี่ยวเมื่อ List ว่าง
คำสั่งโจทย์: ในการเขียนเมธอดลบโหนดแรก (removeFirst) ออกจากลิสต์ หากโหนดแรกมีค่าเดียว หรือลิสต์ว่างเปล่า โค้ดใดต่อไปนี้จะเกิดปัญหา และวิธีเขียนป้องกันที่รัดกุมที่สุดคืออะไร?
📝 บันทึกส่วนตัว — บทที่ 3
4 ลิงก์ลิสต์สองทิศทางและโครงสร้างสแตก (Doubly Linked List & Stack ADT)
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
| Operation | Singly LL | Doubly LL |
|---|---|---|
| Access by index | O(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) — อันสุดท้ายที่เข้าคืออันแรกที่ออก
| Operation | ความหมาย | Complexity |
|---|---|---|
push(x) | ใส่ x ที่ top | O(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
(A+B)*(C-D) ให้เป็น Postfix แบบ step-by-step
5 โครงสร้างข้อมูลคิวแบบวงกลมและตัวจัดเก็บ (Queue ADT & 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
6 คิวจัดลำดับความสำคัญและโครงสร้างโครงต้นไม้ฮีป (Priority Queue & Heaps)
1. Priority Queue ADT
Priority Queue คือ Queue ที่ให้บริการตาม Priority (ลำดับความสำคัญ) ไม่ใช่ลำดับการมาถึง
- Min-Priority Queue — element ที่มี priority น้อยสุดออกก่อน
- Max-Priority Queue — element ที่มี 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;
}
}
| Operation | Time Complexity | เหตุผล |
|---|---|---|
| insert | O(log n) | heapify-up ขึ้น tree สูง log n ชั้น |
| extractMax/Min | O(log n) | heapify-down ลง tree สูง log n ชั้น |
| peek (getMax/Min) | O(1) | root เสมอ |
| build heap (n elements) | O(n) | bottom-up heapify |