Data Structures Visual Notes
สรุปบทที่ 23 – 27 · Introduction to Java Programming and Data Structures (Liang, 11e)

ดูโค้ดวิ่งทีละบรรทัด แล้วจะเข้าใจโครงสร้างข้อมูลเอง

ทุกหัวข้อมี "ตัวไล่โค้ด" เหมือนกด debug ใน IDE: โค้ดแบ่งเป็นบล็อกสี ไฮไลต์บรรทัดที่กำลังทำ ภาพขยับตาม ค่าตัวแปรเปลี่ยนให้เห็น และมีคำอธิบายภาษาง่าย ๆ ทุกขั้น

ใช้งานยังไง

1 · ใส่ข้อมูลหรือกดปุ่มคำสั่ง

หัวข้อเรียงข้อมูลให้พิมพ์ตัวเลขเอง หรือกด "สุ่ม" · หัวข้อโครงสร้างข้อมูล (list, tree, hash) มีปุ่มคำสั่ง เช่น insert(e) delete(e) กดต่อกันได้เรื่อย ๆ

2 · ไล่ทีละขั้น

กด ถัดไป ▶ / ◀ ก่อนหน้า หรือ ⏵ เล่น ให้วิ่งเอง ลากแถบเลื่อนไปขั้นไหนก็ได้ · คลิกในกล่องแล้วใช้ปุ่ม ← → และ Space ได้

3 · ดูให้ครบ 4 จุด

  • บรรทัดสีส้ม ▶ = บรรทัดที่กำลังทำ
  • บล็อกสี = โค้ดส่วนนั้นทำหน้าที่อะไร
  • ป้ายตัวแปร กล่องที่เปลี่ยนสีคือค่าที่เพิ่งเปลี่ยน
  • 💬 = อธิบายว่าเกิดอะไรขึ้นและทำไม

4 · ทบทวนท้ายเล่ม

มีตาราง Big-O รวมพร้อมกราฟ, Flashcards และข้อสอบปรนัย 50 ข้อ (ทำไปอ่านไปได้ คำตอบถูกบันทึกไว้)

5 บทนี้เรียนอะไร

หมายเหตุเรื่องโค้ดโค้ดในเว็บนี้เขียนใหม่ให้อ่านง่าย ใช้ชื่อคลาส/เมธอด/ตัวแปรเดียวกับหนังสือ (เช่น currentElement, pivot, rightMost, balancePath) จึงเทียบกับหนังสือได้ตรง ๆ บางที่ย่อ เช่นใช้ swap(a, b) แทนการสลับ 3 บรรทัด
บทที่ 23 · Sorting · 23.2

Insertion Sort — เรียงแบบเสียบไพ่

หยิบข้อมูลทีละตัวจากทางขวา แล้ว "เสียบ" ลงในส่วนซ้ายที่เรียงแล้วให้ถูกที่

🃏

นึกภาพตามนี้

เหมือนจั่วไพ่มาทีละใบ: ไพ่ในมือเรียงอยู่แล้ว พอได้ใบใหม่ก็เลื่อนใบที่ใหญ่กว่าออกไปทางขวา แล้วเสียบใบใหม่ลงช่องว่าง

ขั้นตอน

  1. เริ่มที่ตัวที่ 2 (i = 1) เพราะตัวแรกตัวเดียวถือว่าเรียงแล้ว
  2. หยิบ list[i] ขึ้นมาถือไว้ (currentElement)
  3. ตัวทางซ้ายที่ใหญ่กว่า ให้เลื่อนไปขวาทีละช่อง แล้ววางตัวที่ถือลงช่องที่ว่าง
จุดที่มักออกสอบ
  • แย่สุด (กลับหัว) O(n²) · ดีสุด (เรียงแล้ว) O(n) เพราะลูปในหยุดทันที
  • หลังรอบที่ i ส่วน list[0..i] เรียงแล้ว แต่ยังไม่ใช่ค่าสุดท้ายเสมอไป
  • เรียงในที่เดิม ไม่ใช้ array เพิ่ม
ลองเองกดปุ่ม "เรียงอยู่แล้ว" กับ "กลับหัว" แล้วดูจำนวนขั้นมุมขวาล่างเทียบกัน จะเห็นว่าข้อมูลเริ่มต้นมีผลกับเวลามาก
บทที่ 23 · Sorting · 23.3

Bubble Sort — ฟองอากาศตัวใหญ่ลอยขึ้นท้าย

เทียบคู่ที่อยู่ติดกันไล่จากซ้ายไปขวา ถ้าผิดลำดับก็สลับ ทำครบหนึ่งรอบ ตัวที่ใหญ่สุดจะไหลไปอยู่ท้ายสุด

🫧

นึกภาพตามนี้

ฟองอากาศใหญ่ลอยขึ้นผิวน้ำก่อน · แต่ละรอบ ตัวที่ใหญ่สุดในส่วนที่เหลือจะ "ลอย" ไปท้าย ส่วนท้ายจึงค่อย ๆ เรียงจากขวามาซ้าย

ขั้นตอน

  1. รอบที่ k: เทียบ list[i] กับ list[i+1] ตั้งแต่ i = 0 ถึง n−k−1
  2. ถ้า list[i] > list[i+1] สลับกัน และจดไว้ว่า needNextPass = true
  3. ถ้าทั้งรอบไม่มีการสลับเลย แปลว่าเรียงแล้ว หยุดได้ทันที
จุดที่มักออกสอบ
  • หลังรอบที่ k ตัวท้าย k ตัวอยู่ในตำแหน่งสุดท้ายแล้ว รอบถัดไปจึงเทียบน้อยลงทีละ 1
  • needNextPass คือ "แบบปรับปรุง" — ถ้าเรียงอยู่แล้ว ใช้แค่ O(n)
  • แย่สุดและเฉลี่ย O(n²)
บทที่ 23 · Sorting · 23.4

Merge Sort — แบ่งแล้วค่อยรวม

แบ่ง list ออกครึ่ง ๆ ไปเรื่อย ๆ จนเหลือกองละตัวเดียว (ซึ่งเรียงอยู่แล้ว) แล้วรวมกองเล็ก ๆ กลับเป็นกองใหญ่แบบเรียงไปด้วย

✂️

นึกภาพตามนี้

ครูแบ่งกองข้อสอบให้นักเรียน 2 คนไปเรียง แต่ละคนก็แบ่งต่อให้เพื่อนอีก จนแต่ละคนได้แผ่นเดียว พอส่งคืน ครูแค่ "รวมสองกองที่เรียงแล้ว" โดยหยิบแผ่นบนสุดที่น้อยกว่าทีละแผ่น

ขั้นตอน

  1. ถ้ามีมากกว่า 1 ตัว: ก๊อปครึ่งแรกเป็น firstHalf ครึ่งหลังเป็น secondHalf
  2. เรียก mergeSort กับแต่ละครึ่ง (recursion — method เรียกตัวเอง)
  3. merge: เทียบหัวแถวสองฝั่ง หยิบตัวที่น้อยกว่าไปก่อน ฝั่งไหนเหลือก็ต่อท้าย
จุดที่มักออกสอบ
  • O(n log n) ทุกกรณี — แบ่งได้ log n ชั้น ทุกชั้น merge รวม n ตัว
  • ใช้หน่วยความจำเพิ่ม (array firstHalf / secondHalf)
  • เป็นตัวอย่างของ divide and conquer
ดู call stackใต้ภาพมีแถบ "call stack" บอกว่าตอนนี้อยู่ในการเรียก mergeSort ชั้นไหน กล่องขอบเขียวคือก้อนที่กำลังทำ กล่องขอบม่วงประคือก้อนที่รอลูกทำเสร็จ
บทที่ 23 · Sorting · 23.5

Quick Sort — เลือกตัวแบ่ง (pivot) แล้วแยกสองฝั่ง

เลือก pivot หนึ่งตัว จัดให้ตัวที่ไม่เกิน pivot ไปอยู่ซ้าย ตัวที่มากกว่าไปอยู่ขวา (partition) pivot จะได้ที่ถาวรทันที แล้วทำแบบเดียวกันกับสองฝั่ง

📏

นึกภาพตามนี้

ครูให้นักเรียนคนแรกยืนเป็นหลัก แล้วสั่ง "ใครเตี้ยกว่าหรือเท่าไปยืนซ้าย ใครสูงกว่าไปขวา" คนที่เป็นหลักก็ได้ตำแหน่งที่ถูกเลย จากนั้นแต่ละฝั่งก็เลือกหลักใหม่ทำซ้ำ

partition ทำงานยังไง

  1. pivot = ตัวแรก · low เดินจากซ้าย หยุดที่ตัวที่ มากกว่า pivot
  2. high เดินจากขวา หยุดที่ตัวที่ ไม่มากกว่า pivot · ถ้ายังไม่สวนกันก็สลับสองตัวนั้น
  3. สวนกันแล้ว วาง pivot ลงที่ high แล้วคืนตำแหน่งนั้น
จุดที่มักออกสอบ
  • เฉลี่ย O(n log n) · แย่สุด O(n²) เมื่อ pivot แบ่งไม่สมดุลทุกครั้ง (เช่น list เรียงอยู่แล้วและเลือกตัวแรก)
  • เรียงในที่เดิม ไม่ต้องใช้ array เพิ่มแบบ merge sort
  • หลัง partition: ซ้าย ≤ pivot < ขวา แต่สองฝั่งยังไม่เรียง
สีในภาพม่วง = pivot · ส้ม = ตัวที่กำลังเทียบ · แดง = กำลังสลับ · เขียว = อยู่ที่ถาวรแล้ว · กรอบเขียว = ช่วงที่กำลังทำ
บทที่ 23 · Sorting · 23.6

Heap และ Heap Sort

heap คือ binary tree ที่ (1) เต็มทีละชั้นจากซ้ายไปขวา และ (2) พ่อมากกว่าหรือเท่ากับลูกเสมอ → ตัวใหญ่สุดอยู่ที่ root เสมอ

🏢

นึกภาพตามนี้

ผังบริษัทที่หัวหน้าต้องเก่งกว่าลูกน้องเสมอ คนใหม่เข้ามาที่ล่างสุดก่อน ถ้าเก่งกว่าหัวหน้าก็สลับขึ้นไปเรื่อย ๆ

เก็บใน ArrayList ได้เลย

ไม่ต้องมีลูกศร เพราะคำนวณตำแหน่งได้:

  • ลูกซ้ายของช่อง i = 2i + 1
  • ลูกขวาของช่อง i = 2i + 2
  • พ่อของช่อง i = (i − 1) / 2

2 งานหลัก

  • add: ต่อท้าย แล้ว "ลอยขึ้น" สลับกับพ่อจนถูกกฎ
  • remove: เอา root ออก เอาตัวท้ายขึ้นแทน แล้ว "จมลง" สลับกับลูกตัวที่ใหญ่กว่า
  • ทั้งคู่ O(log n) = ความสูงต้นไม้

Heap sort

ใส่ทุกตัวลง heap แล้ว remove() ทีละตัว จะได้ตัวใหญ่สุดออกมาก่อนเสมอ เอามาวางจากท้าย list ไล่มาหน้า ก็ได้ list ที่เรียงแล้ว

จุดที่มักออกสอบ
  • heap sort O(n log n) ทุกกรณี (สร้าง heap n ครั้ง + เอาออก n ครั้ง ครั้งละ O(log n))
  • ความสูงของ heap ที่มี n ตัว = O(log n) เพราะเต็มทีละชั้น
  • ระวังสับสน: heap แค่ "พ่อ ≥ ลูก" ไม่ได้บอกว่าซ้ายน้อยกว่าขวาแบบ BST
บทที่ 23 · Sorting · 23.7

Bucket Sort และ Radix Sort — เรียงโดยไม่ต้องเทียบกัน

วิธีก่อนหน้าทั้งหมดเรียงด้วยการเทียบค่า (เร็วสุดได้แค่ O(n log n)) แต่ถ้า key เป็นจำนวนเต็ม ใช้ "ถัง" แจกได้เลยโดยไม่ต้องเทียบ

📮

นึกภาพตามนี้

บุรุษไปรษณีย์แยกจดหมายลงช่องตามรหัสไปรษณีย์ทีละหลัก ไม่ต้องเอาจดหมายมาเทียบกันเลย

Bucket sort

key อยู่ในช่วง 0..t → เตรียมถัง t+1 ใบ ใส่ตัวที่ key = i ลงถัง i แล้วเก็บจากถัง 0 ถึง t · ใช้เวลา O(n + t) แต่ถ้า t ใหญ่มาก (เช่น 0..1,000,000) เปลืองถังเกินไป

Radix sort

ใช้ถังแค่ 10 ใบ (0–9) แต่ทำหลายรอบ ทีละหลัก เริ่มจากหลักหน่วย · ถังต้องเป็นแบบ stable (มาก่อนอยู่ก่อน) ลำดับจากรอบก่อนจึงไม่เสีย · O(dn)

จุดที่มักออกสอบ
  • radix sort เริ่มจากหลักที่มีค่าน้อยสุด (หลักหน่วย) ไปหลักที่มากขึ้น
  • stable sort = ค่าที่เท่ากัน ลำดับเดิมไม่เปลี่ยน
  • เหมาะกับ key ที่เป็นจำนวนเต็ม · ตัวอย่างในหนังสือ: 331, 454, 230, 34, 343, 45, 59, 453, 345, 231, 9
บทที่ 23 · Sorting · 23.8

External Sort — เรียงไฟล์ที่ใหญ่เกินหน่วยความจำ

ถ้าไฟล์มีข้อมูล 2 ล้านตัวแต่ array ใส่ได้ทีละ 100,000 ตัว ก็โหลดมาเรียงทั้งหมดในครั้งเดียวไม่ได้ ต้องเรียงทีละก้อน แล้วค่อย merge

🗂️

นึกภาพตามนี้

เรียงข้อสอบ 2,000 แผ่นบนโต๊ะที่วางได้ทีละ 100 แผ่น: เรียงทีละ 100 แผ่นแล้ววางเป็นกอง ๆ จากนั้นรวมกองที่เรียงแล้วเป็นคู่ ๆ (เหมือน merge) จนเหลือกองเดียว

2 phase

  1. Phase I: อ่านเข้า array ทีละก้อน (MAX_ARRAY_SIZE) → เรียงในหน่วยความจำ → เขียนลง f1.dat เป็นก้อน S1, S2, …
  2. Phase II: ย้ายครึ่งแรกของก้อนไป f2.dat แล้ว merge ก้อนคู่กัน (S1 กับ S(k/2+1), …) ลง f3.dat
  3. ก้อนใหญ่ขึ้นเท่าตัวทุกรอบ ทำซ้ำจนเหลือก้อนเดียว
จุดที่มักออกสอบ
  • เป็น merge sort แบบหนึ่งที่ทำบนไฟล์ · ความซับซ้อนรวม O(n log n)
  • ไม่จำเป็นต้อง merge ก้อนที่ติดกัน — merge S1 กับ S5, S2 กับ S6 ได้ ทำให้เขียนโค้ดง่ายขึ้น (แค่ย้ายครึ่งแรกไปอีกไฟล์)
บทที่ 24 · Lists · 24.2 – 24.3

MyList และ Array List — ล็อกเกอร์ที่เรียงติดกัน

"ออกแบบโครงสร้างข้อมูล = ออกแบบคลาส" มีฟิลด์เก็บข้อมูล และเมธอดสำหรับใส่ ลบ ค้นหา · บทนี้สร้าง MyArrayList เองโดยข้างในเป็น array ธรรมดา

🗄️

นึกภาพตามนี้

ตู้ล็อกเกอร์ที่มีเลขช่องติดกัน หาช่องที่ 5 ได้ทันที แต่ถ้าจะแทรกของไว้ช่องแรก ต้องย้ายของทุกช่องถอยไปหนึ่งช่อง

MyList<E> (interface)

สัญญาว่า list ทุกแบบต้องมี: add(index, e) get set remove(index) indexOf lastIndexOf และมี default method เช่น add(e) = ใส่ที่ index size()

ข้างใน MyArrayList

  • E[] data — array เก็บของ
  • size — มีของกี่ตัว
  • data.length — ความจุ (capacity) ≥ size
  • array ขยายตัวเองไม่ได้ → ต้องสร้างใหม่ใหญ่กว่าแล้วก๊อป
จุดที่มักออกสอบ
  • get / set / add ท้าย = O(1) · add / remove ที่หน้าหรือกลาง = O(n) (ต้องเลื่อน)
  • ensureCapacity สร้าง array ใหม่ขนาด size * 2 + 1 ("+1" กันกรณี size = 0)
  • หนังสือใช้ INITIAL_CAPACITY = 16 · trimToSize() ตัดช่องว่างให้ capacity = size
  • Java สร้าง new E[n] ไม่ได้ (generic) จึงต้องเขียน (E[]) new Object[n]
บทที่ 24 · Lists · 24.4

Linked List — ขบวนรถไฟที่ต่อกันด้วยตะขอ

ของไม่ได้อยู่ติดกันในหน่วยความจำ แต่ละโหนดเก็บค่า (element) และ "ตะขอ" next ที่ชี้ไปโหนดถัดไป · มี head ชี้หัวขบวน tail ชี้ท้ายขบวน

🚃

นึกภาพตามนี้

เพิ่มตู้ที่หัวขบวนง่ายมาก แค่เกี่ยวตะขอใหม่ · แต่ถ้าอยากไปตู้ที่ 7 ต้องเดินผ่านตู้ 1–6 ไปเรื่อย ๆ เพราะไม่มีเลขช่องให้กระโดด

กฎทองของการต่อสาย

  1. ต่อสายใหม่ "ก่อน" ตัดสายเดิมเสมอ ไม่งั้นส่วนที่เหลือของขบวนหายไป
  2. อย่าลืมกรณีพิเศษ: list ว่าง (head = tail = null) และมีตัวเดียว
  3. ทุกครั้งที่แก้ head/tail ต้องเช็คว่าอีกตัวยังถูกอยู่ไหม

แบบอื่นของ linked list

circular singly — ท้ายชี้กลับหัว ABC doubly — มีทั้ง next และ previous เดินย้อนได้ ABC — next- - previous circular doubly — รวมสองแบบ ABC

ระบบปฏิบัติการใช้ circular list วนแบ่งเวลาให้ผู้ใช้หลายคน · doubly linked list ทำให้ removeLast เป็น O(1) เพราะย้อนจาก tail ได้ (java.util.LinkedList เป็นแบบ doubly)

เทียบเวลา (ตาราง 24.1)

เมธอดMyArrayListMyLinkedListทำไม
add(e) ต่อท้ายO(1)O(1)array มีที่ว่าง / linked list มี tail
get(index)O(1)O(n)array คำนวณตำแหน่งได้ · linked list ต้องเดิน
addFirst / removeFirstO(n)O(1)array ต้องเลื่อนทุกตัว · linked list แค่ย้าย head
add(index, e) / remove(index)O(n)O(n)array เลื่อน · linked list เดินหา
removeLast()O(1)O(n)singly linked list ต้องเดินหาตัวรองสุดท้าย
contains / indexOfO(n)O(n)ต้องไล่ดูทีละตัวทั้งคู่

หมายเหตุ: ตาราง 24.1 ในหนังสือพิมพ์ set(index, e) ของ ArrayList เป็น O(n) แต่จริง ๆ array เข้าช่องได้ทันที จึงเป็น O(1) (ส่วน LinkedList เป็น O(n))

บทที่ 24 · 24.5

Stack และ Queue

ทั้งคู่คือ list ที่ "จำกัดว่าใส่และเอาออกได้ที่ไหน" · stack เข้า-ออกทางเดียว · queue เข้าท้าย ออกหัว

🍽️

Stack = กองจาน (LIFO)

วางจานใบใหม่ไว้บนสุด หยิบก็หยิบใบบนสุด — เข้าทีหลังออกก่อน (Last In First Out) · ใช้ทำปุ่ม Undo, ตรวจวงเล็บ, call stack ของ method

🧍‍♂️🧍‍♀️

Queue = แถวซื้อข้าว (FIFO)

มาก่อนได้ก่อน (First In First Out) ต่อท้ายแถว คนหน้าสุดออกก่อน · ใช้กับคิวงานพิมพ์ คิวคำขอ breadth-first search

ทำไมเลือกต่างกัน
  • stack ใส่/เอาออกที่ท้ายอย่างเดียว → ArrayList ทำได้ O(1)
  • queue เอาออกที่หัว → LinkedList.removeFirst() O(1) แต่ ArrayList ต้องเลื่อนทุกตัว O(n)
Inheritance vs Compositionสร้าง stack ด้วยการ extends ArrayList ได้ แต่จะติดเมธอดที่ไม่ควรมีมาด้วย (เช่น add ตรงกลาง) หนังสือจึงใช้ composition: เอา list มาเป็นฟิลด์ข้างใน แล้วเปิดให้ใช้แค่ push/pop/peek
บทที่ 24 · 24.6

Priority Queue — คิวที่ออกตามความสำคัญ

ไม่สนว่าใครมาก่อน ตัวที่สำคัญที่สุด (priority สูงสุด) ได้ออกก่อนเสมอ · หนังสือทำด้วย heap จากบทที่ 23

🚑

นึกภาพตามนี้

ห้องฉุกเฉินโรงพยาบาล: คนอาการหนักได้เข้าก่อน แม้จะมาทีหลัง

MyPriorityQueue = ห่อ Heap

  • enqueue(e) → heap.add(e) ตัวที่สำคัญลอยขึ้นบน
  • dequeue() → heap.remove() เอา root (สำคัญสุด) ออก
  • ทั้งคู่ O(log n) · Patient ต้อง implements Comparable เพื่อเทียบ priority
จุดที่มักออกสอบ
  • ถ้าใช้ list ธรรมดา ต้องไล่หาตัวสำคัญสุดทุกครั้ง O(n) · heap ทำให้เหลือ O(log n)
  • ตัวอย่างหนังสือ: John(2), Jim(1), Tim(5), Cindy(7) → dequeue ได้ Cindy, Tim, John, Jim
บทที่ 25 · Binary Search Trees · 25.2

BST — ต้นไม้ที่ซ้ายน้อย ขวามาก

binary tree = ทุกโหนดมีลูกได้ไม่เกิน 2 (ซ้าย/ขวา) · binary search tree เพิ่มกฎ: ทุกค่าในฝั่งซ้าย < ตัวเอง < ทุกค่าในฝั่งขวา และไม่มีค่าซ้ำ

🔢

นึกภาพตามนี้

เกมทายเลข 1–100: ทายว่า 50 แล้วได้คำใบ้ "น้อยกว่า" ก็ตัดครึ่งบนทิ้งได้ทันที · BST ทำแบบเดียวกัน ทุกครั้งที่เลือกซ้ายหรือขวา ตัดข้อมูลทิ้งไปทั้งฝั่ง

คำศัพท์ของต้นไม้

60 55100 4557 67107 ← rootใบ (leaf) = ไม่มีลูก ชั้น 0ชั้น 1ชั้น 2 55 = พ่อของ 45, 5745, 57 = พี่น้อง

depth ของโหนด = จำนวนเส้นจาก root ลงมาถึงมัน (60 → 0, 45 → 2) · height ของต้นไม้ = ทางยาวสุดจาก root ถึงใบ (ต้นนี้ = 2) · ต้นไม้ย่อย (subtree) = โหนดหนึ่งพร้อมลูกหลานทั้งหมด

จุดที่มักออกสอบ
  • search / insert / delete ใช้เวลา O(h) h = ความสูง · ต้นไม้สมดุล h ≈ log n แต่ถ้าใส่ข้อมูลเรียงกัน ต้นไม้เบี้ยว h = n → O(n)
  • BST ในหนังสือ: class BST<E extends Comparable<E>> implements Tree<E> มีฟิลด์ root และ size · โหนดคือ TreeNode<E> (element, left, right)
  • path(e) คืน list ของโหนดจาก root ไปถึง e (AVL ใช้เมธอดนี้)
บทที่ 25 · 25.2.4 Tree Traversal · 25.4 MVC · 25.5 Iterators

การท่องต้นไม้ (traversal) และ Iterator

traversal = เยี่ยมทุกโหนดครั้งละหนึ่งครั้ง ต่างกันแค่ "เยี่ยมตัวเองตอนไหน" เทียบกับลูกซ้าย/ขวา

inorder

ซ้าย → ตัวเอง → ขวา
ได้ค่าเรียงน้อยไปมาก

45 55 57 60 67 100 107

preorder

ตัวเอง → ซ้าย → ขวา
= depth-first

60 55 45 57 100 67 107

postorder

ซ้าย → ขวา → ตัวเอง
ลูกก่อนพ่อเสมอ

45 57 55 67 107 100 60

breadth-first

ทีละชั้น บน→ล่าง ซ้าย→ขวา ใช้ queue

60 55 100 45 57 67 107

Iterator (25.5)

วัตถุที่ให้ "ไล่ทีละตัว" แบบเดียวกันทุกโครงสร้าง: hasNext() ยังมีตัวถัดไปไหม · next() คืนตัวถัดไป · BST ใน Java ใช้ for (E e : tree) ได้เพราะ implements Iterable

InorderIterator ในหนังสือทำง่าย ๆ: เรียก inorder เก็บใส่ ArrayList ไว้ก่อน แล้วค่อยหยิบทีละตัว · ลองกด iterator.next() ด้านบนหลายครั้ง

Tree visualization + MVC (25.4)

โปรแกรมแสดงต้นไม้ (BSTAnimation) แยกเป็น 3 ส่วน:

  • Model = BST (ข้อมูล)
  • View = BTView วาดต้นไม้ (วาดซ้ำแบบ recursion: วาดโหนด แล้ววาดต้นไม้ย่อยซ้าย/ขวา)
  • Controller = ปุ่ม Insert/Delete ที่รับคำสั่งแล้วสั่ง model และให้ view วาดใหม่
จุดที่มักออกสอบ
  • ทุกแบบใช้เวลา O(n) เพราะเยี่ยมแต่ละโหนดครั้งเดียว
  • ให้ต้นไม้มาแล้วถามผล preorder / inorder / postorder — ฝึกจากปุ่มด้านบนจนจับรูปแบบได้
  • inorder ของ BST = ข้อมูลเรียงแล้ว (ใช้ตรวจว่าเป็น BST จริงไหมก็ได้)
บทที่ 25 · 25.3

ลบโหนดออกจาก BST — 2 กรณี

ลบแล้วต้นไม้ต้องยังเป็น BST ถูกต้อง · หนังสือแบ่งตามว่าโหนดที่จะลบ (current) มีลูกซ้ายหรือไม่

กรณี 1: ไม่มีลูกซ้าย

ให้พ่อ (parent) ชี้ไปที่ลูกขวาของ current แทน แล้ว current ก็หลุดไปเอง · ถ้า current เป็นใบ ลูกขวาคือ null ก็จบ · ถ้า current เป็น root ให้ root = current.right

กรณี 2: มีลูกซ้าย

หา rightMost = ตัวขวาสุดในต้นไม้ย่อยซ้าย (ตัวใหญ่สุดที่ยังน้อยกว่า current) → ก๊อปค่ามาใส่ current → ตัด rightMost ทิ้งโดยต่อ rightMost.left ให้พ่อของมัน · rightMost ไม่มีลูกขวาแน่นอน แต่อาจมีลูกซ้าย

จุดที่มักออกสอบ
  • กรณีพิเศษ: ถ้า current.left ไม่มีลูกขวาเลย rightMost = current.left และ parentOfRightMost = current → ต้องต่อที่ parentOfRightMost.left ไม่ใช่ .right
  • ลบใบเข้ากรณี 1 · ใช้เวลา O(h)
  • ตัวอย่างหนังสือ (รูป 25.11): ลบ 20 → rightMost คือ 16 → 20 ถูกแทนด้วย 16 และ 14 ไปต่อกับ 10
บทที่ 25 · 25.6 Case Study: Data Compression

Huffman Coding — ใช้ต้นไม้บีบอัดข้อความ

ปกติทุกตัวอักษรใช้ 8 บิตเท่ากัน · Huffman ให้ตัวที่โผล่บ่อยได้รหัสสั้น ตัวที่โผล่น้อยได้รหัสยาว ไฟล์รวมจึงเล็กลง

📦

นึกภาพตามนี้

คำที่พิมพ์บ่อยอย่าง "ครับ" ถ้ามีปุ่มลัดสั้น ๆ ก็พิมพ์เร็วขึ้นมาก · Huffman แจก "ปุ่มลัด" สั้นที่สุดให้ตัวอักษรที่ใช้บ่อยที่สุด

สร้างต้นไม้ (greedy)

  1. นับความถี่ แต่ละตัวอักษรเป็นต้นไม้ 1 โหนด น้ำหนัก = ความถี่
  2. หยิบ 2 ต้นที่เบาที่สุด รวมเป็นต้นใหม่ (น้ำหนักบวกกัน) ทำซ้ำจนเหลือต้นเดียว
  3. เดินจาก root: ซ้าย = 0 ขวา = 1 ถึงใบก็ได้รหัสของตัวนั้น
จุดที่มักออกสอบ
  • prefix property: ไม่มีรหัสไหนเป็นส่วนหน้าของรหัสอื่น จึงถอดรหัสได้ไม่กำกวม (เดินต้นไม้ตามบิตจนถึงใบ)
  • เป็น greedy algorithm — เลือกสิ่งที่ดีที่สุดตอนนี้ (2 ต้นที่เบาสุด) แล้วได้ผลดีที่สุดโดยรวม
  • หนังสือ: Mississippi → M=000, p=001, s=01, i=1 รวม 21 บิต
ทำไมรหัสอาจไม่ตรงหนังสือเป๊ะเมื่อมีต้นที่น้ำหนักเท่ากัน หรือสลับซ้าย/ขวา รหัสแต่ละตัวจะต่างได้ (เว็บนี้ให้ตัวเบากว่าอยู่ซ้าย) แต่จำนวนบิตรวมเท่ากันเสมอ (Mississippi = 21 บิตเหมือนกัน) เพราะทุกแบบเป็นคำตอบที่ดีที่สุดเท่ากัน
บทที่ 26 · AVL Trees · 26.1 – 26.5

Balance Factor และการหมุน 4 แบบ

AVL tree = BST ที่คอยรักษาสมดุล: ทุกโหนด ความสูงฝั่งซ้ายกับขวาต่างกันไม่เกิน 1 → ต้นไม้สูงแค่ O(log n) ค้นหาเร็วเสมอ

⚖️

นึกภาพตามนี้

ไม้กระดกที่มีคนนั่ง ถ้าฝั่งหนึ่งหนักเกินก็ "หมุน" ย้ายจุดหมุนให้กลับมาสมดุล โดยลำดับซ้าย-ขวาไม่เสีย

Balance factor (bf)

bf = ความสูงขวา − ความสูงซ้าย

  • ใบ (leaf) สูง 0 · ต้นไม้ว่าง (null) สูง −1
  • bf = −1, 0, +1 → สมดุล
  • bf ≤ −1 = เอียงซ้าย (left-heavy) · ≥ +1 = เอียงขวา
  • bf = −2 หรือ +2 → ต้องหมุน

เลือกการหมุน

Aลูกฝั่งหนักหมุน
−2ลูกซ้าย −1 หรือ 0LL
−2ลูกซ้าย +1LR
+2ลูกขวา +1 หรือ 0RR
+2ลูกขวา −1RL

ภาพรวมการหมุน (T1–T4 = ต้นไม้ย่อย)

LL → หมุนขวาครั้งเดียว (RR คือกลับด้าน)

Abf −2 B T3 T1 T2 หมุนขวา B T1 A T2 T3

B ขึ้นเป็นพ่อ · A ลงไปเป็นลูกขวาของ B · T2 ย้ายข้างจากลูกขวาของ B ไปเป็นลูกซ้ายของ A (ค่าใน T2 อยู่ระหว่าง B กับ A พอดี)

LR → หมุนสองต่อ ให้ C ขึ้นเป็นพ่อ (RL คือกลับด้าน)

Abf −2 B+1 T4 T1 C T2 T3 C ขึ้นแทน C BA T1 T2 T3 T4

ทางเอียงหักมุม (ซ้ายแล้วขวา) หมุนครั้งเดียวไม่หาย → C ซึ่งอยู่ระหว่าง B กับ A ขึ้นเป็นพ่อของทั้งคู่ · T2 ไปเป็นลูกขวาของ B · T3 ไปเป็นลูกซ้ายของ A

ลองกดปุ่ม LL / RR / LR / RL ด้านล่าง: ระบบจะใส่ตัวเลขทุกตัวยกเว้นตัวสุดท้ายให้ก่อน แล้วไล่ทีละขั้นตอนใส่ตัวสุดท้ายซึ่งทำให้เสียสมดุล ตัวเลขใต้แต่ละโหนดคือ bf (สีแดง = ต้องหมุน)

บทที่ 26 · 26.3 – 26.9

AVL insert / delete และ balancePath

AVLTree extends BST: ใส่/ลบแบบ BST ตามปกติก่อน (super.insert) แล้วค่อยไล่ "ซ่อม" สมดุลบนเส้นทางกลับขึ้นไปถึง root

AVLTreeNode

สืบทอดจาก TreeNode แล้วเพิ่มฟิลด์ height เพื่อคำนวณ bf ได้เร็วโดยไม่ต้องนับใหม่ทั้งต้นไม้ · override createNewNode ให้สร้าง AVLTreeNode

balancePath(e)

  1. ขอ path จาก root ถึงโหนด e
  2. ไล่จากล่างขึ้นบน: updateHeight แล้วดู bf
  3. bf = ±2 → หมุนแบบที่ตรงกรณี

ทำไมเร็ว

ความสูง AVL ≤ ประมาณ 1.44 log(n+2) → O(log n) · การหมุนแต่ละครั้งแก้ลูกศรไม่กี่เส้น O(1) · insert, delete, search จึงเป็น O(log n) ทั้งหมด

จุดที่มักออกสอบ
  • หลัง insert ตรวจเฉพาะโหนดบนเส้นทางจากโหนดใหม่ขึ้นไป root (ตัวอื่นความสูงไม่เปลี่ยน)
  • delete: ลบแบบ BST แล้ว balancePath จาก parent (กรณี 1) หรือ parentOfRightMost (กรณี 2)
  • TestAVLTree ในหนังสือ: ใส่ 25, 20, 5 → เกิด LL ที่ 25 ได้ root เป็น 20
  • ลองปุ่ม "ใส่เรียง 1–7" เทียบกับ BST ธรรมดาที่จะเบี้ยวเป็นเส้นตรง
บทที่ 27 · Hashing · 27.1 – 27.3

Hashing — คำนวณตำแหน่งจาก key ได้ทันที

map เก็บคู่ key → value · ถ้าใช้ hashing จะค้นหา ใส่ ลบ ได้เฉลี่ย O(1) เพราะไม่ต้องไล่ค้น แค่คำนวณว่า key นี้ต้องอยู่ช่องไหน

🔐

นึกภาพตามนี้

ตู้ล็อกเกอร์ที่ช่องของคุณคำนวณจากชื่อ เช่น นับตัวอักษรแล้วหารเอาเศษด้วยจำนวนช่อง ไม่ต้องเดินเปิดดูทุกช่อง · ถ้าสองคนได้ช่องเดียวกัน เรียกว่า ชน (collision)

"John"key hashCode()ได้ int (อาจติดลบ/ใหญ่มาก) บีบ (compress)% N หรือ & (N−1) 012 ✓3… hash table

hashCode ของแต่ละชนิด (27.3)

ชนิด keyhashCode
byte, short, int, charค่าตัวมันเองเป็น int เลย (Integer 42 → 42)
long (64 บิต)folding: เอาครึ่งบน XOR ครึ่งล่าง (int)(key ^ (key >>> 32))
float / doubleแปลงบิตเป็น int/long ก่อน (Double.doubleToLongBits) แล้ว folding
Stringpolynomial hash code: s₀·b^(n−1) + s₁·b^(n−2) + … + s_(n−1) โดย b = 31 คำนวณแบบ Horner: h = 31*h + c
จุดที่มักออกสอบ
  • hash function ที่ดี: คำนวณเร็ว กระจาย index สม่ำเสมอ ชนน้อย · perfect hash function = ไม่ชนเลย (หายาก)
  • ถ้าแค่บวกรหัสตัวอักษร "cat", "act", "tac" จะชนกันหมด จึงคูณ 31 ตามตำแหน่ง
  • ใช้ % N ให้ N เป็นจำนวนเฉพาะจะกระจายดี · Java ใช้ N = 2^k แล้ว & (N − 1) ซึ่งเร็วกว่า + ใช้ supplementalHash ผสมบิตบนลงมาก่อน
บทที่ 27 · 27.4

Open Addressing — ชนแล้วหาช่องว่างอื่น

ทุก key ต้องอยู่ในตารางเอง ถ้าช่องที่คำนวณได้มีคนอยู่แล้ว ก็ลองช่องอื่นตามสูตร (เรียกว่า probing) จนเจอช่องว่าง · ตารางถือว่าวนรอบ (ท้ายต่อหัว)

Linear probing

ลอง k, k+1, k+2, … (ทีละ 1)

ข้อเสีย: clustering ของจับกลุ่มเป็นก้อนยาว ยิ่งก้อนใหญ่ยิ่งชนบ่อย ค้นหาช้า · ข้อดี: ถ้าตารางไม่เต็ม เจอที่ว่างแน่นอน

Quadratic probing

ลอง k, k+1², k+2², k+3², … (k+1, k+4, k+9)

แก้ clustering แบบติดกันได้ แต่ยังมี secondary clustering (key ที่เริ่มช่องเดียวกันไปทางเดียวกัน) และไม่รับประกันว่าจะเจอช่องว่าง

Double hashing

ลอง k, k+h'(key), k+2h'(key), … เช่น h'(key) = 7 − key % 7

ระยะกระโดดขึ้นกับ key แต่ละตัว กระจายดีที่สุดในสามแบบ · h'(key) ห้ามเป็น 0

เลือกแบบ probing ในกล่องด้านบนซ้ายของตัวไล่โค้ด (ตารางจะรีเซ็ต) แล้วกดปุ่มชุดตัวอย่าง จากนั้นลอง put(40) เทียบกันแต่ละแบบ

จุดที่มักออกสอบ
  • ตัวอย่างหนังสือ double hashing: N = 11, ใส่ 45, 58, 4, 28, 21 แล้วใส่ 12 → h(12) = 1, h'(12) = 2 → ลองช่อง 1, 3, 5 ได้ช่อง 5
  • แบบฝึกหัดที่ออกบ่อย: ใส่ 34, 29, 53, 44, 120, 39, 45, 40 ลงตาราง 11 ช่องด้วยแต่ละวิธี
  • open addressing ควรรักษา load factor ให้ต่ำกว่า 0.5
บทที่ 27 · 27.5 – 27.8

Separate Chaining, Load Factor และ MyHashMap

อีกวิธีรับมือการชน: ช่องหนึ่งเก็บได้หลายตัว โดยทำเป็น "ถัง" (bucket) เช่น LinkedList ใครได้ index เดียวกันก็ต่อท้ายถังเดียวกัน

🪣

นึกภาพตามนี้

ตู้จดหมายคอนโดที่แต่ละช่องเป็นถุง ใครได้เลขช่องเดียวกันก็ใส่ถุงเดียวกัน หาของก็ไปดูแค่ถุงเดียว

Load factor λ = n / N

n = จำนวนข้อมูล · N = ขนาดตาราง · λ สูง = แน่น ชนบ่อย

  • open addressing ควร λ < 0.5
  • separate chaining ควร λ < 0.9
  • java.util.HashMap ใช้เกณฑ์ 0.75

Rehashing

เกินเกณฑ์ → สร้างตารางใหม่ใหญ่ขึ้น (อย่างน้อย 2 เท่า) แล้ว put ทุกตัวใหม่ เพราะ index ขึ้นกับขนาดตาราง · ช้า O(capacity) แต่เกิดนาน ๆ ครั้ง put โดยเฉลี่ยจึงยัง O(1)

MyMap / MyHashMap / MyHashSet

ข้างใน MyHashMap

  • LinkedList<MyMap.Entry<K,V>>[] table — ตาราง ช่องละหนึ่งถัง
  • capacity เป็นกำลังของ 2 เสมอ (เริ่ม 4 · trimToPowerOf2 ปัดให้เป็น 2^k) สูงสุด 1 << 30
  • loadFactorThreshold ค่าเริ่ม 0.75f
  • hash(h) = supplementalHash(h) & (capacity − 1)
  • เงื่อนไข rehash ใน put: size >= capacity * loadFactorThreshold

MyHashSet

แนวเดียวกับ MyHashMap แต่เก็บแค่ key (ไม่มี value) ใช้ทำ set ที่ไม่มีค่าซ้ำ: add เช็คก่อนว่ามีอยู่แล้วหรือไม่ · contains / add / remove เฉลี่ย O(1)

เมธอด (ตาราง 27.1)เวลา
get, put, remove, containsKeyO(1)
clear, containsValue, entrySet, keySet, values, rehashO(capacity)
จุดที่มักออกสอบ
  • ตาราง capacity 4 ใส่ได้ถึง size 3 (4 × 0.75) ตัวที่ 4 จะทำให้ rehash เป็น 8
  • containsValue ต้องไล่ทุกถัง → O(capacity) ไม่ใช่ O(1)
  • ถ้า hash แย่จนทุก key ลงถังเดียว จะช้าเหมือน linked list O(n)
ทบทวน

ตาราง Big-O รวมทุกบท

Big-O บอกว่า "เมื่อข้อมูลเพิ่มขึ้น งานเพิ่มเร็วแค่ไหน" ไม่ได้บอกเวลาเป็นวินาที · ลากแถบดูว่าเมื่อ n ใหญ่ขึ้น แต่ละแบบห่างกันขนาดไหน

ตัวอย่าง: n = 1,000,000 → O(log n) ≈ 20 ขั้น · O(n log n) ≈ 20 ล้านขั้น · O(n²) = 1 ล้านล้านขั้น

Sorting (บทที่ 23)

algorithmดีสุดเฉลี่ยแย่สุดใช้ที่เพิ่มจำง่าย ๆ
InsertionO(n)O(n²)O(n²)O(1)เสียบไพ่ · เกือบเรียงแล้วเร็วมาก
Bubble (มี needNextPass)O(n)O(n²)O(n²)O(1)ตัวใหญ่ลอยไปท้ายทีละรอบ
MergeO(n log n)O(n log n)O(n log n)O(n)คงที่ทุกกรณี แต่กินหน่วยความจำ
QuickO(n log n)O(n log n)O(n²)O(log n) stackเร็วในทางปฏิบัติ · pivot แย่ = ช้า
HeapO(n log n)O(n log n)O(n log n)O(1)**ในหนังสือใช้ Heap แยก = O(n)
BucketO(n + t)O(n + t)t = ช่วงของ key
RadixO(dn)O(n)d = จำนวนหลัก · ไม่เทียบค่า
ExternalO(n log n)ไฟล์ชั่วคราวmerge sort บนไฟล์

โครงสร้างข้อมูล (บทที่ 24 – 27)

โครงสร้างค้นหาใส่ลบหมายเหตุ
Array listO(n)O(1) ท้าย · O(n) หน้า/กลางO(n)get(index) O(1)
Linked listO(n)O(1) หัว/ท้ายO(1) หัว · O(n) ท้ายget(index) O(n)
Stack / Queue—O(1)O(1)push/pop · enqueue/dequeue
Heap / Priority queueO(n)O(log n)O(log n) (root)ดูตัวสูงสุด O(1)
BSTO(h)O(h)O(h)h = log n ถึง n (ถ้าเบี้ยว)
AVL treeO(log n)O(log n)O(log n)หมุนให้สมดุลเสมอ
Hash map / setO(1)O(1)O(1)เฉลี่ย · แย่สุด O(n) ถ้าชนหมด · rehash O(capacity)
Tree traversalO(n)เยี่ยมทุกโหนดครั้งเดียว
ทบทวน

Flashcards

กดการ์ดเพื่อพลิกดูคำอธิบาย เลือกบทหรือสุ่มลำดับได้

ทบทวน

ข้อสอบรวม 50 ข้อ

ปรนัย 4 ตัวเลือก บทละ 10 ข้อ สุ่มลำดับข้อและตัวเลือกทุกครั้ง · ออกไปอ่านหน้าอื่นระหว่างทำได้ กลับมาต่อจากปุ่มมุมขวาล่าง