0Pricing
Coding Interview Prep · บทเรียน

สัญกรณ์ Big-O ตั้งแต่พื้นฐาน

ทำความเข้าใจเหตุผลที่ต้องสนใจการเติบโตเชิงเส้นกำกับ วิธีตัดค่าคงที่และพจน์อันดับต่ำกว่า รวมถึงวิธีอ่าน Big-O ได้อย่างรวดเร็ว

สัญกรณ์ Big-O ตั้งแต่พื้นฐาน เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

เหตุใดจึงต้องวัดประสิทธิภาพของอัลกอริทึม

โปรแกรมสองโปรแกรมอาจให้ผลลัพธ์ถูกต้องเหมือนกัน แต่โปรแกรมหนึ่งทำงานเสร็จในพริบตา ขณะที่อีกโปรแกรมใช้เวลาหลายชั่วโมง ความซับซ้อนด้านเวลา อธิบายว่าเวลาทำงานเพิ่มขึ้นอย่างไรเมื่อข้อมูลนำเข้ามีขนาดใหญ่ขึ้น

# O(n) approach
def find_max_linear(nums):
    m = nums[0]
    for n in nums:
        if n > m: m = n
    return m

# O(n^2) approach (unnecessary double loop)
def find_max_quadratic(nums):
    for i in range(len(nums)):
        is_max = all(nums[i] >= nums[j] for j in range(len(nums)))
        if is_max: return nums[i]

print(find_max_linear([3, 1, 4, 1, 5, 9]))  # 9

Big-O: ขอบเขตบนเชิงเส้นกำกับ

Big-O อธิบายขอบเขตบนในกรณีเลวร้ายที่สุดของการเติบโตของต้นทุนการทำงาน เคล็ดลับคือให้ตัดค่าคงที่และพจน์ที่มีขนาดเล็กกว่าออก เพราะเมื่อขนาดข้อมูลเพิ่มขึ้น มีเพียงพจน์เด่นเท่านั้นที่สำคัญ ดูตัวอย่างในโค้ด

# T(n) = 3n^2 + 5n + 100 is O(n^2)
# because the n^2 term dominates for large n

# T(n) = 2n + 1000 is O(n)
# the constant 1000 becomes negligible

# Rule: drop constants and lower-order terms
# 5n^3 + 2n^2 + n + 1  =>  O(n^3)
# 100 * log(n) + n      =>  O(n)
print('O(n^2) example: counting iterations')
n = 1000
count = sum(1 for i in range(n) for j in range(n))
print(count)  # 1_000_000 = n^2

คลาสความซับซ้อนที่พบบ่อย

จากเร็วที่สุดไปช้าที่สุด: O(1), O(log n), O(n), O(n log n), O(n^2), O(2^n), O(n!) การรู้จักความซับซ้อนเหล่านี้ช่วยให้คุณเลือกแนวทางที่เหมาะสมได้ก่อนเขียนโค้ดแม้แต่บรรทัดเดียว

import math

n = 1000
print(f'O(1):       {1}')
print(f'O(log n):   {int(math.log2(n))}')
print(f'O(n):       {n}')
print(f'O(n log n): {int(n * math.log2(n))}')
print(f'O(n^2):     {n**2}')
# O(2^n) for n=1000 is astronomically large
# O(n!) even larger

การตัดค่าคงที่: เหตุใดจึงสำคัญ

การทำงาน 5n ขั้นตอนหรือ 2n ขั้นตอนต่างก็เป็น O(n) — ค่าคงที่ขึ้นอยู่กับฮาร์ดแวร์ ไม่ใช่อัลกอริทึม Big-O จึงตัดค่าคงที่เหล่านี้ออก เพื่อให้เปรียบเทียบการเพิ่มขนาดของงานได้อย่างเท่าเทียม

# Both are O(n) — different constants
def count_a(n):
    total = 0
    for i in range(n):   # n ops
        total += 1
    for i in range(n):   # n ops
        total += 1
    return total  # T(n) = 2n  =>  O(n)

def count_b(n):
    total = 0
    for i in range(5 * n):  # 5n ops
        total += 1
    return total  # T(n) = 5n  =>  O(n)

print(count_a(10), count_b(10))  # 20 50

กรณีดีที่สุด กรณีเฉลี่ย และกรณีเลวร้ายที่สุด

Big-O คือ กรณีเลวร้ายที่สุด; Omega คือกรณีดีที่สุด; Theta คือขอบเขตที่รัดกุมของทั้งสองกรณี เมื่อผู้สัมภาษณ์ถามถึง "ความซับซ้อน" พวกเขาแทบจะหมายถึงกรณีเลวร้ายที่สุดเสมอ

def linear_search(nums, target):
    for i, n in enumerate(nums):
        if n == target:
            return i  # best case: target at index 0 => O(1)
    return -1         # worst case: not found => O(n)

# Best case O(1): target is first element
print(linear_search([5,1,2,3], 5))   # 0

# Worst case O(n): target not in list
print(linear_search([1,2,3,4], 9))   # -1

O(log n): การลดพื้นที่ค้นหาลงครึ่งหนึ่ง

อัลกอริทึมจะเป็น O(log n) เมื่อมันลดข้อมูลเข้าลงครึ่งหนึ่งในแต่ละขั้น เช่น การค้นหาแบบทวิภาค แม้จะมีข้อมูลถึงหนึ่งพันล้านรายการ ก็ใช้เพียงประมาณ 30 ขั้นตอนเท่านั้น — เร็วอย่างเหลือเชื่อ ดูโค้ดได้เลย

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    steps = 0
    while lo <= hi:
        steps += 1
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid, steps
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, steps

import math
arr = list(range(1000))
idx, s = binary_search(arr, 999)
print(f'Found at {idx} in {s} steps (log2(1000)~={math.log2(1000):.1f})')

O(n log n): ขอบเขตล่างของการเรียงลำดับ

การเรียงลำดับแบบเปรียบเทียบใด ๆ ต้องใช้เวลาอย่างน้อย O(n log n) ในกรณีเลวร้ายที่สุด — นี่คือขอบเขตล่างทางคณิตศาสตร์ที่พิสูจน์ได้จริง ดังนั้นการเรียงลำดับก่อนแล้วค่อยสแกนจึงมีความซับซ้อนรวมเป็น O(n log n) ไม่ใช่ O(n^2) โค้ดนี้แสดงการเรียงลำดับแบบผสาน

# Merge sort: O(n log n)
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(a, b):
    res, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]: res.append(a[i]); i+=1
        else:             res.append(b[j]); j+=1
    return res + a[i:] + b[j:]

print(merge_sort([5,2,8,1,9,3]))  # [1,2,3,5,8,9]

ความซับซ้อนแบบเฉลี่ยสะสม

การวิเคราะห์แบบ เฉลี่ยสะสมจะหาค่าใช้จ่ายเฉลี่ยจากการทำงานหลายครั้งรวมกัน append ของ Python มีความซับซ้อนแบบเฉลี่ยสะสมเป็น O(1): โดยปกติจะทำงานทันที แต่บางครั้งจะต้องปรับขนาดเป็น O(n) ซึ่งเมื่อนำไปเฉลี่ยกับการทำ append ทั้งหมดแล้ว ผลกระทบจะน้อยมาก

# Dynamic array append is O(1) amortised
import sys

lst = []
capacities = []
for i in range(16):
    lst.append(i)
    capacities.append(sys.getsizeof(lst))

# Size jumps show reallocation events
for i, c in enumerate(capacities):
    if i > 0 and capacities[i] != capacities[i-1]:
        print(f'Realloc at i={i}, new size={c} bytes')

การมองความซับซ้อนจากโค้ด

กฎง่าย ๆ คือให้นับลูป ลูปหนึ่งชั้นเป็น O(n) ลูปซ้อนกันสองชั้นเป็น O(n^2) และลูปที่ลดข้อมูลลงครึ่งหนึ่งเป็น O(log n) การทำงานที่แยกจากกันให้ บวกกัน ส่วนลูปที่ซ้อนกันเท่านั้นที่ให้ คูณกัน ดูโค้ดได้เลย

# Two independent passes: O(n) + O(n) = O(n)
def two_passes(nums):
    total = sum(nums)           # O(n)
    mean = total / len(nums)
    diffs = [abs(n - mean) for n in nums]  # O(n)
    return max(diffs)           # O(n)
# Overall: O(n) -- NOT O(n^2)

# Nested loops: O(n) * O(n) = O(n^2)
def all_pairs(nums):
    pairs = []
    for i in range(len(nums)):       # O(n)
        for j in range(i+1, len(nums)): # O(n)
            pairs.append((nums[i], nums[j]))
    return pairs  # O(n^2)

พื้นฐานความซับซ้อนด้านพื้นที่

ความซับซ้อนด้านพื้นที่ติดตามหน่วยความจำเพิ่มเติมที่คุณใช้ นอกเหนือจากข้อมูลเข้า การกลับลำดับแบบทำในข้อมูลเดิมมีความซับซ้อนเป็น O(1) ส่วนตารางแฮชมีความซับซ้อนเป็น O(n) เมื่อแลกเวลาเพื่อประหยัดพื้นที่หรือแลกพื้นที่เพื่อประหยัดเวลา ต้องระบุทั้งสองค่าเสมอ

# O(1) space: reverse in-place
def reverse_inplace(arr):
    l, r = 0, len(arr) - 1
    while l < r:
        arr[l], arr[r] = arr[r], arr[l]
        l += 1; r -= 1

# O(n) space: create reversed copy
def reverse_copy(arr):
    return arr[::-1]

a = [1, 2, 3, 4, 5]
reverse_inplace(a)
print(a)  # [5, 4, 3, 2, 1]

การพูดถึงความซับซ้อนในการสัมภาษณ์

เสนอ ความซับซ้อนออกมาเองเสมอโดยไม่ต้องรอให้ถูกถาม: "วิธีนี้ใช้เวลา O(n log n) และใช้พื้นที่ O(n)" จากนั้นจึงเสนอทางเลือกที่เร็วกว่า นิสัยนี้แสดงให้เห็นถึงความเป็นมืออาชีพในระดับสูงอย่างแท้จริง

# Example of explaining complexity step by step
def two_sum(nums, target):
    # O(n) time: one pass through nums
    # O(n) space: hash map stores up to n elements
    seen = {}  # value -> index
    for i, n in enumerate(nums):
        complement = target - n
        if complement in seen:   # O(1) lookup
            return [seen[complement], i]
        seen[n] = i
    return []

print(two_sum([2, 7, 11, 15], 9))  # [0, 1]

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ตรวจสอบความเข้าใจอย่างรวดเร็ว — แสดงสิ่งที่คุณซึมซับเกี่ยวกับ Big-O และคลาสความซับซ้อน หนึ่งคำถามเท่านั้น คุณทำได้แน่นอน 🎯

ทบทวนบทเรียน

ทบทวน: Big-O แสดงการเติบโตในกรณีเลวร้ายที่สุดโดยตัดค่าคงที่ออก คุณรู้จักคลาสตั้งแต่ O(1) ถึง O(n!) แล้ว และรู้ว่าลูปที่แยกจากกันต้องบวกกัน ส่วนลูปที่ซ้อนกันต้องคูณกัน

คำถามที่พบบ่อย

บทเรียน “สัญกรณ์ Big-O ตั้งแต่พื้นฐาน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “สัญกรณ์ Big-O ตั้งแต่พื้นฐาน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “สัญกรณ์ Big-O ตั้งแต่พื้นฐาน”

ทำความเข้าใจเหตุผลที่ต้องสนใจการเติบโตเชิงเส้นกำกับ วิธีตัดค่าคงที่และพจน์อันดับต่ำกว่า รวมถึงวิธีอ่าน Big-O ได้อย่างรวดเร็ว คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน

บทเรียน “สัญกรณ์ Big-O ตั้งแต่พื้นฐาน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม

ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. สัญกรณ์ Big-O ตั้งแต่พื้นฐาน
  2. วิเคราะห์ลูปและลูปซ้อน
  3. การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ
  4. ความซับซ้อนด้านพื้นที่และการแลกเปลี่ยน
← กลับไปที่ Coding Interview Prep