Learn Algorithms

เรียนรู้ Algorithms

เรียน Algorithms + Python ตั้งแต่เริ่มต้น — คลิกคำภาษาอังกฤษที่ไฮไลท์เพื่อฟังเสียงอ่าน

0. เริ่มต้นใช้งาน Python

ก่อนที่เราจะเรียน algorithms เราต้องติดตั้ง Python บนคอมพิวเตอร์ของเราก่อน Python เป็นภาษาโปรแกรมที่เรียนง่ายที่สุดภาษาหนึ่ง และตัวอย่างทั้งหมดในบทเรียนนี้ใช้ Python

ขั้นตอนที่ 1: ติดตั้ง Python

  1. ไปที่ python.org
  2. คลิก "Downloads" แล้วดาวน์โหลดเวอร์ชันล่าสุด
  3. เปิดตัวติดตั้ง — ติ๊กช่อง "Add Python to PATH"
  4. คลิก "Install Now"

ขั้นตอนที่ 2: เปิด Command Prompt

วิธีเปิด Command Prompt บน Windows:
  1. คลิกปุ่ม Start (มุมล่างซ้าย) หรือกดปุ่ม Windows บนคีย์บอร์ด
  2. พิมพ์ cmd
  3. คลิกที่ Command Prompt ที่ปรากฏขึ้น
  4. จะเห็นหน้าต่างสีดำ — นี่คือ Command Prompt! พิมพ์คำสั่งได้เลย

พิมพ์คำสั่งนี้แล้วกด Enter เพื่อตรวจสอบว่า Python ติดตั้งสำเร็จ:

python --version

ถ้าเห็นข้อความเช่น Python 3.12.0 แปลว่าสำเร็จ!

ขั้นตอนที่ 3: เลือกวิธีเขียนโค้ด

มี 3 วิธีเขียน Python ให้เลือก:

วิธีที่ 1: ใช้ IDLE (แนะนำสำหรับมือใหม่)

IDLE ติดตั้งมาพร้อมกับ Python แล้ว! เปิดโดย:

  1. คลิก Start แล้วพิมพ์ IDLE
  2. คลิกที่ IDLE (Python)
  3. จะเห็นหน้าต่าง Python Shell — พิมพ์โค้ดได้เลย!
  4. ถ้าอยากเขียนโปรแกรมยาวๆ: คลิก File > New File จะได้หน้าต่างเขียนโค้ด
  5. พิมพ์โค้ดเสร็จ กด F5 หรือ Run > Run Module เพื่อรันโปรแกรม
วิธีที่ 2: ใช้ Notepad + Command Prompt
  1. เปิด Notepad (คลิก Start แล้วพิมพ์ Notepad)
  2. พิมพ์โค้ด Python ลงไป
  3. คลิก File > Save As — เลือกที่เก็บไฟล์ (เช่น Desktop)
  4. ตั้งชื่อไฟล์ลงท้ายด้วย .py เช่น hello.py
  5. สำคัญ: ที่ช่อง "Save as type" เลือก "All Files" ไม่ใช่ "Text Documents"
  6. เปิด Command Prompt แล้วพิมพ์: cd Desktop แล้ว python hello.py
วิธีที่ 3: ใช้ Online IDE (ไม่ต้องติดตั้งอะไร!)

ถ้าไม่อยากยุ่งกับการติดตั้ง ใช้เว็บเขียน Python ได้เลย:

  • online-python.com — ง่ายมาก พิมพ์โค้ดแล้วกด Run ได้เลย
  • Replit.com — สร้างบัญชีฟรี เขียนโค้ดได้ บันทึกได้ แชร์ได้
  • Google Colab — ใช้บัญชี Google เขียน Python ได้ฟรี บันทึกใน Google Drive

ขั้นตอนที่ 4: โปรแกรมแรกของคุณ

ลองเขียนและรันโค้ดนี้ (ใช้วิธีไหนก็ได้จากข้างบน):

print("Hello, World!")
โค้ดนี้ทำอะไร?

print() คือคำสั่งที่บอกให้ Python แสดงข้อความบนหน้าจอ ข้อความที่อยู่ในเครื่องหมายคำพูด "..." คือสิ่งที่จะถูกแสดง ลองเปลี่ยนข้อความเป็นอะไรก็ได้แล้วรันดู!

ถ้าใช้ Command Prompt กับไฟล์ hello.py:

python hello.py

พื้นฐาน Python ที่ต้องรู้

Variables (ตัวแปร) — ใช้เก็บข้อมูล:

name = "Alice"
age = 25
price = 19.99
is_student = True
ลองทำตาม: เปิด IDLE หรือ online IDE แล้วพิมพ์โค้ดด้านบนทีละบรรทัด จากนั้นพิมพ์ print(name) แล้วกด Enter — จะเห็น "Alice" ปรากฏ! ลองเปลี่ยน "Alice" เป็นชื่อของคุณดู

อธิบาย: เครื่องหมาย = ใน Python แปลว่า "เก็บค่า" ไม่ใช่ "เท่ากับ" เช่น age = 25 แปลว่า "เก็บเลข 25 ไว้ในตัวแปรชื่อ age"

Lists (ลิสต์) — เก็บข้อมูลหลายตัวเรียงลำดับ:

fruits = ["apple", "banana", "cherry"]
print(fruits[0])   # "apple"  (index เริ่มที่ 0)
print(len(fruits)) # 3
อธิบาย: List ใช้เครื่องหมาย [ ] ครอบข้อมูลหลายตัว คั่นด้วยลูกน้ำ ตำแหน่ง (index) เริ่มนับจาก 0 ไม่ใช่ 1 ดังนั้น fruits[0] คือตัวแรก ("apple") และ fruits[1] คือตัวที่สอง ("banana")

ลองพิมพ์ fruits.append("mango") แล้ว print(fruits) — จะเห็นว่า "mango" ถูกเพิ่มเข้าไปท้ายลิสต์!

If statements (เงื่อนไข) — ใช้ตัดสินใจ:

if age >= 18:
    print("You are an adult")
else:
    print("You are a minor")
อธิบาย: if ตรวจสอบเงื่อนไข ถ้าจริง ทำบรรทัดที่เยื้องข้างใน ถ้าไม่จริง ทำส่วน else สังเกตเครื่องหมาย : ต้องมีทุกครั้ง! ลองเปลี่ยนค่า age = 15 แล้วรันอีกครั้ง — ผลลัพธ์จะเปลี่ยน

For loops (ลูป) — ทำซ้ำ:

for fruit in fruits:
    print(fruit)
อธิบาย: for จะวนทำซ้ำกับทุกตัวใน list ตัวแปร fruit จะเปลี่ยนค่าทุกรอบ: รอบแรกเป็น "apple" รอบสองเป็น "banana" รอบสามเป็น "cherry" แล้วหยุด

Functions (ฟังก์ชัน) — บล็อกโค้ดที่ใช้ซ้ำได้:

def greet(name):
    return "Hello, " + name + "!"

print(greet("Alice"))  # "Hello, Alice!"
อธิบาย: def สร้างฟังก์ชัน — เหมือนสูตรอาหาร เขียนครั้งเดียวแล้วใช้ซ้ำได้เรื่อยๆ name ในวงเล็บคือ "วัตถุดิบ" ที่ต้องใส่ return คือ "ผลลัพธ์" ที่ส่งกลับมา

ลองเรียก print(greet("Bob")) — จะได้ "Hello, Bob!"

เคล็ดลับ: Python ใช้ การเยื้อง (indentation) คือการเว้นวรรค เพื่อกำหนดบล็อกโค้ด ใช้ 4 เว้นวรรคสำหรับแต่ละระดับ ซึ่งต่างจากภาษาอื่นที่ใช้วงเล็บปีกกา {}
Big O Notation

2. Big O Notation (สัญกรณ์บิ๊กโอ)

Big O notation (สัญกรณ์บิ๊กโอ — วิธีวัดความเร็วของ algorithm โดยดูว่าจำนวนการทำงานเพิ่มขึ้นอย่างไรเมื่อข้อมูลเพิ่ม) บอกว่า algorithm (อัลกอริทึม — ชุดคำสั่งสำหรับทำงานให้เสร็จ) เร็วแค่ไหน มันไม่ได้บอกความเร็วเป็นวินาที แต่บอกว่าจำนวนการทำงานเพิ่มขึ้นอย่างไรเมื่อข้อมูลเพิ่ม

ทำไมถึงสำคัญ? เพราะ algorithm ที่ทำงานดีกับข้อมูล 10 ตัว อาจช้ามากกับข้อมูล 1 ล้านตัว

Big O ที่พบบ่อย

จากเร็วสุดไปช้าสุด:

Big Oชื่อตัวอย่าง
O(1)Constant time (เวลาคงที่)ค้นหาด้วย index
O(log n)Logarithmic time (เวลาลอการิทึม)Binary search
O(n)Linear time (เวลาเชิงเส้น)Simple search, อ่านทุกตัว
O(n log n)Log-linear timeการเรียงเร็ว (quicksort, merge sort)
O(n²)Quadratic time (เวลากำลังสอง)การเรียงช้า (selection sort)
O(n!)Factorial time (เวลาแฟกทอเรียล)ปัญหาพนักงานขายเดินทาง
Operations needed for a list of 16 items: O(1) → 1 O(log n) → 4 O(n) → 16 O(n log n) → 64 O(n²) → 256 O(n!) → 20,922,789,888,000 (!!)

กฎสำคัญ

อธิบาย Logarithm แบบง่ายๆ

logarithm (ลอการิทึม — ตรงข้ามกับเลขยกกำลัง) คือ "ต้องคูณ 2 กี่ครั้งจึงจะได้ 8?" คำตอบคือ 3 เพราะ 2 × 2 × 2 = 8 ดังนั้น log₂(8) = 3

นี่คือเหตุผลที่ binary search เป็น O(log n): แต่ละขั้นตัดปัญหาลงครึ่ง

# Visualize how O(n) vs O(log n) grow differently:
import math

for size in [10, 100, 1000, 10000]:
    linear = size
    logarithmic = math.ceil(math.log2(size))
    print(f"n={size:>6}  |  O(n)={linear:>6}  |  O(log n)={logarithmic:>3}")
Selection Sort

3. Selection Sort (การเรียงลำดับแบบเลือก)

ก่อนเรียนการเรียงลำดับ เราต้องเข้าใจโครงสร้างข้อมูลสองแบบ: arrays (อาร์เรย์ — ข้อมูลเรียงติดกันในหน่วยความจำ เข้าถึงได้ทันทีด้วยตำแหน่ง) และ linked lists (ลิงก์ลิสต์ — ข้อมูลกระจายในหน่วยความจำ แต่ละตัวชี้ไปยังตัวถัดไป)

Arrays กับ Linked Lists

การดำเนินการArrayLinked List
อ่าน (ด้วย index)O(1) เร็ว!O(n) ช้า
แทรกO(n) ช้าO(1) เร็ว!
ลบO(n) ช้าO(1) เร็ว!

Arrays: ข้อมูลทั้งหมดอยู่ติดกันใน memory (หน่วยความจำ) เหมือนที่นั่งจองในโรงหนัง อ่านตำแหน่งไหนก็ได้เร็ว แต่แทรกช้าเพราะต้องขยับทุกตัว

Linked lists: ข้อมูลอยู่ที่ไหนก็ได้ในหน่วยความจำ แต่ละตัวชี้ไปยังตัวถัดไป เหมือนเกมตามล่าสมบัติ เพิ่มตัวใหม่ง่าย แต่กระโดดไปตัวที่ 50 ช้า

Selection Sort ทำงานอย่างไร

Selection sort (การเรียงแบบเลือก — หาตัวน้อยที่สุดแล้ววางไว้ข้างหน้า ทำซ้ำจนเรียงเสร็จ) หาตัวน้อยที่สุดแล้ววางไว้ตำแหน่งแรก จากนั้นหาตัวน้อยที่สุดถัดไป ทำแบบนี้ไปเรื่อยๆ

Sorting [64, 25, 12, 22, 11]: Step 1: Find min=11 → [11, 25, 12, 22, 64] Step 2: Find min=12 → [11, 12, 25, 22, 64] Step 3: Find min=22 → [11, 12, 22, 25, 64] Step 4: Find min=25 → [11, 12, 22, 25, 64] Done! → [11, 12, 22, 25, 64]

โค้ด Python

# หา index ของสมาชิกตัวเล็กที่สุด
def find_smallest(arr):
    smallest = arr[0]
    smallest_index = 0
    for i in range(1, len(arr)):
        if arr[i] < smallest:
            smallest = arr[i]
            smallest_index = i
    return smallest_index

# Selection sort: หาตัวเล็กสุดแล้วย้ายไปข้างหน้า
def selection_sort(arr):
    new_arr = []
    for i in range(len(arr)):
        smallest_index = find_smallest(arr)
        new_arr.append(arr.pop(smallest_index))
    return new_arr

print(selection_sort([64, 25, 12, 22, 11]))
# [11, 12, 22, 25, 64]
ลองรันเลย! บันทึกโค้ดเป็น selection_sort.py แล้วรันด้วย IDLE หรือ Command Prompt

โค้ดทำอะไร:

  • find_smallest(arr) = วนดูทุกตัว หาตัวที่เล็กที่สุด ส่งตำแหน่งกลับมา
  • arr.pop(smallest_index) = ดึงตัวเล็กสุดออกจากรายการเดิม
  • new_arr.append(...) = เอาตัวเล็กสุดไปต่อท้ายรายการใหม่
  • ทำซ้ำจนรายการเดิมหมด → รายการใหม่เรียงจากน้อยไปมาก!

ลองเปลี่ยนตัวเลขใน list เป็นของคุณเอง แล้วดูว่ามันเรียงถูกไหม

Selection sort ใช้เวลา O(n²) — เข้าใจง่ายแต่ช้าสำหรับรายการใหญ่

Recursion

4. Recursion (การเรียกซ้ำ)

Recursion (การเรียกซ้ำ — เทคนิคที่ฟังก์ชันเรียกตัวเองซ้ำเพื่อแก้ปัญหาที่เล็กลงเรื่อยๆ) คือเมื่อฟังก์ชันเรียกตัวเอง ฟังดูแปลก แต่เป็นเทคนิคที่มีประโยชน์มาก

ลองนึกภาพเปิดกล่องซ้อนกัน: เปิดกล่อง เจอกล่องข้างใน เปิดอีก เจอกล่องอีก... จนเจอสิ่งที่หา การ "เปิดกล่อง" เป็นการกระทำเดิม แค่กล่องเล็กลง

Base Case และ Recursive Case

ทุกฟังก์ชัน recursive ต้องมีสองส่วน:

# Countdown using recursion
def countdown(n):
    if n <= 0:        # Base case: stop!
        print("Go!")
        return
    print(n)
    countdown(n - 1)  # Recursive case: call yourself

countdown(5)
# Prints: 5, 4, 3, 2, 1, Go!
ลองรัน! บันทึกเป็น countdown.py แล้วรัน

อธิบาย: ฟังก์ชัน countdown เรียกตัวเอง แต่ทุกครั้ง n จะลดลง 1 เมื่อ n ถึง 0 มันหยุด (base case) ลองเปลี่ยน countdown(5) เป็น countdown(10) ดู!

ลองลบบรรทัด if n <= 0: ออก แล้วรัน — จะเห็น RecursionError เพราะไม่มี base case!

The Call Stack

คอมพิวเตอร์ใช้ stack (สแตก — โครงสร้างข้อมูลแบบเข้าหลังออกก่อน เหมือนกองจาน) เพื่อติดตามการเรียกฟังก์ชัน ลองนึกภาพกองโน้ตกาว — เพิ่มด้านบน เอาออกจากด้านบน

Call stack for factorial(3): ┌──────────────┐ │ factorial(1) │ ← returns 1 ├──────────────┤ │ factorial(2) │ ← returns 2 × 1 = 2 ├──────────────┤ │ factorial(3) │ ← returns 3 × 2 = 6 └──────────────┘
# Factorial: 5! = 5 × 4 × 3 × 2 × 1 = 120
def factorial(n):
    if n == 1:       # Base case
        return 1
    return n * factorial(n - 1)  # Recursive case

print(factorial(5))  # 120
คำเตือน: ถ้าลืม base case ฟังก์ชันจะทำงานไม่รู้จบ (จนกว่า Python จะหยุดด้วย RecursionError) ให้กำหนดเงื่อนไขหยุดก่อนเสมอ!
Quicksort

5. Quicksort (การเรียงลำดับแบบเร็ว)

Quicksort (ควิกซอร์ต — การเรียงลำดับที่เร็ว โดยเลือกตัวอ้างอิงแล้วแบ่งข้อมูลออกเป็นกลุ่มเล็กกว่าและใหญ่กว่า) เป็น algorithm เรียงลำดับที่เร็ว ใช้ divide and conquer (แบ่งแล้วเอาชนะ — แยกปัญหาใหญ่เป็นปัญหาเล็กๆ แก้แต่ละส่วน แล้วรวมผลลัพธ์)

Divide and Conquer (D&C)

D&C เป็นกลยุทธ์ที่มีสองขั้นตอน:

  1. หา base case — กรณีที่ง่ายที่สุด
  2. แบ่งปัญหาจนกลายเป็น base case
# D&C example: sum a list using recursion
def sum_list(arr):
    if len(arr) == 0:   # Base case
        return 0
    return arr[0] + sum_list(arr[1:])  # First + sum of rest

print(sum_list([2, 4, 6]))  # 12

Quicksort ทำงานอย่างไร

  1. เลือก pivot (ตัวอ้างอิง — สมาชิกที่ใช้เป็นจุดแบ่ง) เช่น ตัวแรก
  2. Partition (แบ่ง): วางตัวที่น้อยกว่าไว้ซ้าย ตัวที่มากกว่าไว้ขวา
  3. เรียก quicksort ซ้ำ (recursion) กับอาร์เรย์ซ้ายและขวา
Quicksort [33, 10, 55, 71, 29, 15]: Pivot = 33 Less: [10, 29, 15] Greater: [55, 71] Quicksort [10, 29, 15]: Pivot = 10 Less: [] Greater: [29, 15] Quicksort [29, 15]: Pivot = 29, Less: [15], Greater: [] → [15, 29] → [10, 15, 29] Quicksort [55, 71]: Pivot = 55, Less: [], Greater: [71] → [55, 71] Result: [10, 15, 29] + [33] + [55, 71] = [10, 15, 29, 33, 55, 71]

โค้ด Python

def quicksort(arr):
    if len(arr) < 2:
        return arr  # Base case: เรียงแล้ว

    pivot = arr[0]
    less = [x for x in arr[1:] if x <= pivot]     # ตัวที่น้อยกว่า pivot
    greater = [x for x in arr[1:] if x > pivot]   # ตัวที่มากกว่า pivot

    return quicksort(less) + [pivot] + quicksort(greater)

print(quicksort([33, 10, 55, 71, 29, 15]))
# [10, 15, 29, 33, 55, 71]
ลองรัน! บันทึกเป็น quicksort.py

อธิบายโค้ด:

  • if len(arr) < 2 = ถ้ามีแค่ 0 หรือ 1 ตัว มันเรียงอยู่แล้ว (base case)
  • pivot = arr[0] = เลือกตัวแรกเป็นตัวอ้างอิง
  • [x for x in arr[1:] if x <= pivot] = สร้าง list ใหม่เฉพาะตัวที่น้อยกว่าหรือเท่ากับ pivot (เรียกว่า list comprehension — วิธีสร้าง list แบบสั้นของ Python)
  • เรียก quicksort ซ้ำกับทั้งสองฝั่ง แล้วรวมผลลัพธ์ด้วย +

ลองเพิ่ม print(f"pivot={pivot} less={less} greater={greater}") ก่อนบรรทัด return เพื่อดูว่า quicksort แบ่งข้อมูลอย่างไรในแต่ละรอบ!

Quicksort โดยเฉลี่ยใช้เวลา O(n log n) — เร็วกว่า selection sort ที่ O(n²) มาก ในกรณีแย่สุด (รายการเรียงอยู่แล้วกับ pivot ที่ไม่ดี) อาจเป็น O(n²) แต่เกิดขึ้นน้อยในทางปฏิบัติ

Hash Tables

6. Hash Tables (ตารางแฮช)

hash table (ตารางแฮช — โครงสร้างข้อมูลที่เก็บคู่ key-value ค้นหาได้ทันที) เหมือนพจนานุกรมวิเศษ ให้ key (คีย์ เช่น ชื่อ) แล้วได้ value (ค่า เช่น เบอร์โทร) ทันที ใน Python เรียกว่า dictionaries (ดิกชันนารี) หรือ dict

Hash Tables ทำงานอย่างไร

hash function (ฟังก์ชันแฮช — แปลง key เป็นตำแหน่งในอาร์เรย์) แปลง key ให้เป็น index ของอาร์เรย์ มันจะคืนตำแหน่งเดิมสำหรับ key เดิมเสมอ

Hash function maps names to slots: "apple" → hash → slot 3 → $0.67 "milk" → hash → slot 0 → $1.49 "eggs" → hash → slot 7 → $2.99 Lookup "apple"? → hash → slot 3 → $0.67 (instant!)

Python Code

# Python dictionaries ARE hash tables!
phone_book = {}
phone_book["Alice"] = "0812345678"
phone_book["Bob"] = "0898765432"
phone_book["Charlie"] = "0856781234"

# Look up a number instantly:
print(phone_book["Alice"])  # "0812345678"

# Check if someone exists:
if "Bob" in phone_book:
    print("Found Bob's number!")
ลองเปิด IDLE แล้วพิมพ์ทีละบรรทัด!

Dictionary ใน Python ใช้เครื่องหมาย { } — ทุกค่ามีรูปแบบ "key": "value"

  • phone_book["Alice"] = "081..." = เพิ่มหรืออัปเดตค่า
  • phone_book["Alice"] = ค้นหาค่าด้วย key ทันที!
  • "Bob" in phone_book = ตรวจว่า key อยู่ใน dictionary ไหม (ได้ True/False)

ลองพิมพ์ print(phone_book) เพื่อดูข้อมูลทั้งหมด แล้วลองเพิ่มเบอร์ตัวเองเข้าไป!

กรณีใช้งาน

# Caching example: remember pages we've already fetched
cache = {}

def get_page(url):
    if url in cache:
        return cache[url]      # Return cached data (fast!)
    data = "...fetch from internet..."  # Slow operation
    cache[url] = data          # Save for next time
    return data

Collisions (การชน)

collision (การชน — เมื่อ key สองตัวแปลงเป็นตำแหน่งเดียวกัน) เกิดเมื่อ key สองตัว map ไปที่ slot เดียวกัน hash table ที่ดีจัดการเรื่องนี้ได้ แต่ถ้าชนมากเกินจะช้าลง

การดำเนินการกรณีเฉลี่ยกรณีแย่สุด
ค้นหาO(1)O(n)
แทรกO(1)O(n)
ลบO(1)O(n)
เคล็ดลับ: เมื่อคิดว่า "ต้องค้นหาอะไรเร็วๆ" ให้นึกถึง hash table ใน Python ก็แค่ใช้ dictionary {}
Breadth-First Search

7. Breadth-First Search (การค้นหาแบบกว้าง)

Graphs (กราฟ — โครงสร้างข้อมูลที่แสดงความเชื่อมต่อระหว่างสิ่งต่างๆ) ใช้จำลองความเชื่อมต่อ เครือข่ายสังคมคือกราฟ: คนคือ nodes (โหนด — จุดหรือสิ่งต่างๆ ในกราฟ) และมิตรภาพคือ edges (เส้นเชื่อม — ความสัมพันธ์ระหว่างโหนด)

Breadth-first search (BFS — การค้นหาแบบกว้าง สำรวจทีละชั้นจากใกล้ไปไกล) ตอบคำถามสองข้อ:

  1. มีเส้นทางจาก A ไป B ไหม?
  2. เส้นทางสั้นสุดจาก A ไป B คืออะไร?

BFS ทำงานอย่างไร

BFS สำรวจออกไปทีละชั้น เหมือนระลอกน้ำในสระ ตรวจเพื่อนของคุณก่อน จากนั้นเพื่อนของเพื่อน แล้วก็เพื่อนของเพื่อนของเพื่อน...

Finding a mango seller in your network: YOU / | \ Alice Bob Claire ← check friends first (layer 1) | | | Dave Eve Frank ← then friends of friends (layer 2) | Grace ← then layer 3...

BFS ใช้ queue (คิว — เข้าก่อนออกก่อน เหมือนแถวซื้อของ) เพิ่มคนเข้าคิว แล้วตรวจตามลำดับที่เข้ามา

Python Code

from collections import deque

# Define the graph as a dictionary
graph = {}
graph["you"]     = ["alice", "bob", "claire"]
graph["alice"]   = ["dave"]
graph["bob"]     = ["eve"]
graph["claire"]  = ["frank"]
graph["dave"]    = []
graph["eve"]     = []
graph["frank"]   = ["grace"]
graph["grace"]   = []

def is_mango_seller(name):
    return name == "frank"  # Frank is our target

def bfs(start):
    queue = deque()
    queue.append(start)
    searched = set()  # Track who we already checked

    while queue:
        person = queue.popleft()
        if person in searched:
            continue
        if is_mango_seller(person):
            print(f"Found: {person}!")
            return True
        queue.extend(graph[person])
        searched.add(person)

    return False

bfs("you")  # Found: frank!
ลองรัน! บันทึกเป็น bfs.py — โค้ดนี้ใช้ from collections import deque ซึ่งเป็น library ที่มากับ Python อยู่แล้ว ไม่ต้องติดตั้งเพิ่ม

อธิบาย:

  • graph = dictionary ที่เก็บว่าแต่ละคนรู้จักใครบ้าง
  • deque() = คิว (queue) แบบพิเศษที่เร็ว
  • queue.popleft() = ดึงคนแรกออกจากคิว (เข้าก่อนออกก่อน)
  • searched = เก็บรายชื่อคนที่ตรวจแล้ว เพื่อไม่ให้ตรวจซ้ำ

ลองเปลี่ยนเป้าหมายใน is_mango_seller เป็นชื่ออื่น แล้วดูว่า BFS หาเจอไหม!

BFS ใช้เวลา O(V + E) โดยที่ V = จำนวนโหนด (vertices) และ E = จำนวนเส้นเชื่อม (edges)

8. Dijkstra's Algorithm (อัลกอริทึมของไดก์สตรา)

BFS หาเส้นทางที่มีจำนวนก้าวน้อยที่สุด แต่ถ้าแต่ละเส้นเชื่อมมี น้ำหนัก (ต้นทุน) ต่างกันล่ะ? Dijkstra's algorithm (อัลกอริทึมของไดก์สตรา — หาเส้นทางที่มีต้นทุนรวมน้อยที่สุดในกราฟที่มีน้ำหนัก) หาเส้นทางที่มีต้นทุนรวมน้อยที่สุด

ตัวอย่าง: คุณต้องการขับรถจากบ้านไปที่ทำงาน BFS จะหาเส้นทางที่ผ่านถนนน้อยที่สุด Dijkstra จะหาเส้นทางเร็วที่สุด (โดยพิจารณาระยะทางของแต่ละถนน)

วิธีการทำงาน

  1. เริ่มที่โหนดต้นทาง ตั้งต้นทุนเป็น 0 โหนดอื่นเป็นอนันต์ (∞)
  2. เลือกโหนดที่ยังไม่เยี่ยมชมที่มีต้นทุนต่ำสุด
  3. อัปเดตต้นทุนของเพื่อนบ้านถ้าเจอเส้นทางถูกกว่า
  4. ทำเครื่องหมายว่าเยี่ยมชมแล้ว ทำซ้ำจากขั้นตอนที่ 2
Finding cheapest path from Start to Finish: 6 1 Start ───→ A ───→ Finish | ↑ └──→ B ───────────┘ 2 5 Path 1: Start→A→Finish = 6+1 = 7 Path 2: Start→B→Finish = 2+5 = 7 Both cost 7! Dijkstra finds this efficiently.

Python Code

def dijkstra(graph, costs, parents):
    visited = set()

    # Find the cheapest unvisited node
    node = find_lowest_cost(costs, visited)

    while node is not None:
        cost = costs[node]
        neighbors = graph[node]
        for neighbor in neighbors:
            new_cost = cost + neighbors[neighbor]
            if new_cost < costs[neighbor]:
                costs[neighbor] = new_cost
                parents[neighbor] = node
        visited.add(node)
        node = find_lowest_cost(costs, visited)

def find_lowest_cost(costs, visited):
    lowest = float("inf")
    lowest_node = None
    for node in costs:
        if costs[node] < lowest and node not in visited:
            lowest = costs[node]
            lowest_node = node
    return lowest_node

# Set up the graph
graph = {
    "start": {"a": 6, "b": 2},
    "a": {"finish": 1},
    "b": {"a": 3, "finish": 5},
    "finish": {}
}
costs = {"a": 6, "b": 2, "finish": float("inf")}
parents = {"a": "start", "b": "start", "finish": None}

dijkstra(graph, costs, parents)
print(f"Cheapest cost to finish: {costs['finish']}")  # 5
print(f"Path: start → b → a → finish")  # via parents
ลองรัน! บันทึกเป็น dijkstra.py

อธิบาย: โค้ดนี้ยาวหน่อย แต่แนวคิดง่าย: เริ่มจากจุดเริ่มต้น ค่อยๆ ดูทุกโหนดที่ไปถึงได้ ถ้าเจอทางที่ถูกกว่า ก็อัปเดตต้นทุน

  • float("inf") = ค่าอนันต์ (infinity) ใน Python — ใช้แทน "ยังไม่รู้ต้นทุน"
  • costs = dictionary เก็บต้นทุนต่ำสุดที่รู้ไปยังแต่ละโหนด
  • parents = dictionary เก็บว่ามาจากโหนดไหน (เพื่อย้อนดูเส้นทาง)

ลองเพิ่มโหนดใหม่เข้าไปใน graph แล้วดูว่าเส้นทางเปลี่ยนไหม!

สำคัญ: Dijkstra's algorithm ใช้ไม่ได้กับ negative-weight edges (เส้นเชื่อมที่มีน้ำหนักติดลบ) สำหรับกรณีนั้นให้ใช้ Bellman-Ford algorithm แทน

9. Greedy Algorithms (อัลกอริทึมแบบละโมบ)

greedy algorithm (อัลกอริทึมแบบละโมบ — เลือกทางเลือกที่ดีที่สุดในแต่ละขั้น โดยหวังว่าจะนำไปสู่ผลลัพธ์ที่ดีที่สุดโดยรวม) เลือกสิ่งที่ดีที่สุดในแต่ละขั้น ง่ายและมักจะดีพอ!

ตัวอย่าง: การจัดตารางห้องเรียน

คุณมีรายชื่อวิชาและห้องเรียนหนึ่งห้อง ต้องการใส่วิชาให้มากที่สุด วิธี greedy: เลือกวิชาที่จบเร็วที่สุดเสมอ

Classes: Art: 9:00 - 10:00 ✓ (ends earliest, pick it) Math: 9:30 - 10:30 ✗ (overlaps with Art) English: 10:00 - 11:00 ✓ (starts after Art ends) CS: 10:30 - 11:30 ✗ (overlaps with English) Music: 11:00 - 12:00 ✓ (starts after English ends) Result: Art, English, Music (3 classes fit!)

ปัญหา Set-Covering

สมมติต้องเลือกสถานีวิทยุให้ครอบคลุมทุกรัฐ แต่ละสถานีครอบคลุมบางรัฐ ต้องการจำนวนสถานีน้อยที่สุด ปัญหานี้คือ NP-complete (เอ็นพีสมบูรณ์ — ปัญหาที่ไม่มีวิธีแก้ที่เร็วเป็นที่รู้จัก) ใช้ approximation algorithm (อัลกอริทึมประมาณค่า — หาคำตอบที่ใกล้เคียงดีพอแทนคำตอบที่สมบูรณ์แบบ) แบบ greedy ได้ดี:

states_needed = {"mt", "wa", "or", "id", "nv", "ut", "ca", "az"}

stations = {
    "kone":   {"id", "nv", "ut"},
    "ktwo":   {"wa", "id", "mt"},
    "kthree": {"or", "nv", "ca"},
    "kfour":  {"nv", "ut"},
    "kfive":  {"ca", "az"}
}

final_stations = set()

while states_needed:
    # Greedy: pick station that covers the most uncovered states
    best_station = None
    states_covered = set()
    for station, states in stations.items():
        covered = states_needed & states  # intersection
        if len(covered) > len(states_covered):
            best_station = station
            states_covered = covered
    states_needed -= states_covered
    final_stations.add(best_station)

print(final_stations)  # A small set of stations

ปัญหา NP-Complete

บางปัญหาไม่มีวิธีแก้ที่เร็ว ปัญหา traveling salesperson (พนักงานขายเดินทาง — เยี่ยมทุกเมืองด้วยระยะทางรวมสั้นที่สุด) เป็นหนึ่งในนั้น 5 เมือง: 120 เส้นทาง 10 เมือง: 3,628,800 เส้นทาง เพิ่มขึ้นแบบ O(n!)

ถ้าสงสัยว่าปัญหาเป็น NP-complete ให้ใช้ approximation algorithm แทนการหาคำตอบที่สมบูรณ์แบบ

Dynamic Programming

10. Dynamic Programming (การโปรแกรมเชิงพลวัต)

Dynamic programming (DP — การโปรแกรมเชิงพลวัต แก้ปัญหาซับซ้อนโดยแยกเป็นปัญหาย่อย แก้แต่ละปัญหาย่อยครั้งเดียวแล้วเก็บผลลัพธ์ไว้) แก้ปัญหาซับซ้อนโดยแยกเป็น subproblems (ปัญหาย่อย) แก้แต่ละปัญหาย่อยครั้งเดียว แล้วเก็บผลลัพธ์ในตาราง

ปัญหา Knapsack (กระเป๋าเป้)

คุณเป็นขโมยที่มีกระเป๋าเป้บรรจุได้ 4 กก. จะขโมยของอะไรให้ได้มูลค่าสูงสุด?

Items: Guitar: 1 kg, $1500 Laptop: 3 kg, $2000 Stereo: 4 kg, $3000 Knapsack grid (rows=items, columns=weight capacity): 1 kg 2 kg 3 kg 4 kg Guitar $1500 $1500 $1500 $1500 Laptop $1500 $1500 $2000 $3500 ← Guitar+Laptop! Stereo $1500 $1500 $2000 $3500 Best: Guitar + Laptop = $3500 (4 kg)

Python Code

# 0/1 Knapsack using Dynamic Programming
def knapsack(capacity, weights, values, n):
    # Create a grid of (n+1) x (capacity+1)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(1, capacity + 1):
            if weights[i-1] <= w:
                # Max of: not taking item vs taking item
                dp[i][w] = max(
                    dp[i-1][w],
                    dp[i-1][w - weights[i-1]] + values[i-1]
                )
            else:
                dp[i][w] = dp[i-1][w]

    return dp[n][capacity]

weights = [1, 3, 4]
values = [1500, 2000, 3000]
print(knapsack(4, weights, values, 3))  # 3500
ลองรัน! บันทึกเป็น knapsack.py

อธิบาย: DP สร้างตาราง 2 มิติ (dp) แต่ละช่องคือ "ค่าสูงสุดที่ได้ ถ้ามีของ i ชิ้นแรก และกระเป๋ารับน้ำหนักได้ w กก."

  • dp[i][w] = max(ไม่เอาของชิ้นนี้, เอาของชิ้นนี้)
  • ถ้าของหนักเกินกว่ากระเป๋า → ไม่เอา ใช้ค่าเดิม

ลองเพิ่มของชิ้นที่ 4 เช่น iPhone น้ำหนัก 1 กก. ราคา $2000 แล้วดูว่าผลลัพธ์เปลี่ยนไหม!

Longest Common Substring (สตริงย่อยร่วมยาวสุด)

DP ยังหา longest common substring (สตริงย่อยร่วมที่ยาวที่สุด — ลำดับอักขระต่อเนื่องที่ยาวที่สุดที่พบในทั้งสองคำ) ระหว่างสองคำได้ ใช้ประโยชน์สำหรับการตรวจคำสะกด การเปรียบเทียบ DNA และเครื่องมือ diff

def longest_common_substring(a, b):
    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    longest = 0

    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            if a[i-1] == b[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
                longest = max(longest, dp[i][j])

    return longest

print(longest_common_substring("fish", "fosh"))  # 2 ("sh")
เมื่อไหร่ควรใช้ DP: ถ้าปัญหาเกี่ยวกับการหาค่าที่ดีที่สุดและแบ่งเป็นปัญหาย่อยที่ซ้อนทับกันได้ ให้ลอง dynamic programming กุญแจสำคัญคือการสร้างตาราง
K-Nearest Neighbors

11. K-Nearest Neighbors (เพื่อนบ้านใกล้สุด K ตัว)

K-nearest neighbors (KNN — เพื่อนบ้านใกล้สุด K ตัว จัดหมวดสิ่งใหม่โดยดูจากข้อมูลที่ใกล้เคียงที่สุด K ตัว) เป็น machine learning (การเรียนรู้ของเครื่อง — สอนคอมพิวเตอร์ให้เรียนรู้จากข้อมูลโดยไม่ต้องเขียนโปรแกรมทุกกรณี) algorithm ที่ง่าย ในการจัดหมวดสิ่งหนึ่ง ให้ดูจุดข้อมูลที่ใกล้ที่สุด K ตัว แล้วเลือกหมวดที่พบมากที่สุด

ตัวอย่าง: ส้มหรือส้มโอ?

คุณมีผลไม้ลึกลับ วัดขนาดและสี แล้วเปรียบเทียบกับผลไม้ที่รู้จักแล้ว

Plot of fruits by size and redness: Redness ↑ 5 | 🍊 🍊 4 | 🍊 ❓ ← mystery fruit 3 | 🍊 🟡 2 | 🟡 1 | 🟡 🟡 └──────────────→ Size 🍊 = Orange 🟡 = Grapefruit K=3: 3 nearest neighbors = 2 oranges + 1 grapefruit → Classify as Orange!

การวัดระยะทาง

ใช้ Pythagorean theorem (ทฤษฎีพีทาโกรัส — สูตรคำนวณระยะทางระหว่างสองจุด) เพื่อวัดระยะทาง:

import math

def distance(point1, point2):
    sum_sq = 0
    for i in range(len(point1)):
        sum_sq += (point1[i] - point2[i]) ** 2
    return math.sqrt(sum_sq)

# Example: distance between two fruits
fruit_a = [4, 3]  # size=4, redness=3
fruit_b = [1, 1]  # size=1, redness=1
print(distance(fruit_a, fruit_b))  # ~3.6

KNN สำหรับระบบแนะนำ

Netflix ใช้แนวคิดคล้าย KNN: หาผู้ใช้ที่มีรสนิยมคล้ายกัน (เพื่อนบ้านใกล้สุด) แล้วแนะนำสิ่งที่พวกเขาชอบ

# Simple KNN classifier
def knn_classify(data, labels, new_point, k=3):
    distances = []
    for i, point in enumerate(data):
        d = distance(point, new_point)
        distances.append((d, labels[i]))

    distances.sort()  # Sort by distance
    nearest = distances[:k]

    # Count votes
    votes = {}
    for d, label in nearest:
        votes[label] = votes.get(label, 0) + 1

    return max(votes, key=votes.get)

# Training data: [size, redness]
data   = [[3,5],[4,5],[3,4],[2,1],[1,1],[2,2]]
labels = ["orange","orange","orange","grape","grape","grape"]

mystery = [3.5, 4]
print(knn_classify(data, labels, mystery))  # "orange"
ลองรัน! บันทึกทั้ง distance() และ knn_classify() ในไฟล์เดียวชื่อ knn.py (คัดลอกทั้งสองบล็อกโค้ดด้านบน)

อธิบาย:

  • distance() = คำนวณระยะห่างระหว่างจุดสองจุด (ยิ่งใกล้ยิ่งคล้ายกัน)
  • distances.sort() = เรียงจากใกล้สุดไปไกลสุด
  • distances[:k] = เอาแค่ k ตัวที่ใกล้ที่สุด
  • นับว่า k ตัวนี้เป็นหมวดไหนมากสุด → นั่นคือคำตอบ!

ลองเพิ่มผลไม้ใหม่ใน data และ labels แล้วเปลี่ยน mystery เพื่อดูว่าจัดหมวดถูกไหม!

Feature Extraction (การสกัดคุณลักษณะ)

การเลือก features (คุณลักษณะ — คุณสมบัติของข้อมูลที่ใช้ในการเปรียบเทียบ) ที่ถูกต้องสำคัญมาก สำหรับแนะนำหนัง features ที่ดี เช่น: คะแนนตลก คะแนนแอ็คชัน คะแนนดราม่า features ที่ไม่ดี: วันฉาย ความยาวหนัง

KNN ใช้สำหรับ: Classification (จัดหมวด — เช่น อีเมลนี้เป็น spam ไหม?), regression (ทำนายค่า — เช่น ราคาบ้าน), และ recommendations (แนะนำ — ผู้ใช้ควรดูอะไรต่อ?)

สรุป: ตารางเปรียบเทียบ Algorithm

Algorithmทำอะไรเวลา
Binary Searchค้นหาในรายการที่เรียงแล้วO(log n)
Selection Sortเรียงโดยหาค่าน้อยสุดซ้ำๆO(n²)
Quicksortเรียงเร็วด้วยแบ่งแล้วเอาชนะO(n log n)
Hash Tableค้นหาทันทีด้วย keyO(1)
BFSเส้นทางสั้นสุด (ไม่มีน้ำหนัก)O(V+E)
Dijkstraเส้นทางสั้นสุด (มีน้ำหนัก)O(V+E)log V
Greedyประมาณค่าสำหรับปัญหายากแตกต่างกัน
Dynamic Programmingหาค่าที่ดีที่สุดด้วยปัญหาย่อยแตกต่างกัน
KNNจัดหมวดด้วยเพื่อนบ้านใกล้สุดO(n)