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

สร้างฮีป เลื่อนเข้า และเลื่อนออกตั้งแต่ต้น

สร้างการปรับฮีปขึ้นสำหรับการเลื่อนเข้าและการปรับฮีปลงสำหรับการเลื่อนออก แล้วสร้างฮีปจากอาร์เรย์ที่ไม่เรียงลำดับในเวลา O(n) ด้วยอัลกอริทึมของ Floyd

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

การสร้างคลาส MinHeap

การสร้างฮีปตั้งแต่ต้นช่วยแสดงให้เห็นว่าคุณเข้าใจกลไกพื้นฐานอย่างถ่องแท้ และบางครั้งก็เป็นหัวข้อที่ถามในการสัมภาษณ์ระดับอาวุโส คลาส MinHeapห่อหุ้มอาร์เรย์ไว้และเปิดให้ใช้งานการดำเนินการ push, pop, peek และ size ภายในคลาสจะรักษาคุณสมบัติของฮีปด้วยการเรียกการเลื่อนขึ้นหลัง push และการเลื่อนลงหลัง pop เมื่อเข้าใจการทำงานนี้แล้ว โมดูล heapq ของไพธอนก็จะเข้าใจได้อย่างโปร่งใส

class MinHeap:
    def __init__(self):
        self._data = []

    def push(self, val):
        self._data.append(val)
        self._sift_up(len(self._data) - 1)

    def pop(self):
        if len(self._data) == 1:
            return self._data.pop()
        min_val = self._data[0]
        self._data[0] = self._data.pop()  # move last to root
        self._sift_down(0)
        return min_val

    def peek(self):
        return self._data[0] if self._data else None

    def size(self):
        return len(self._data)

    def _parent(self, i): return (i - 1) // 2
    def _left(self, i):   return 2 * i + 1
    def _right(self, i):  return 2 * i + 2

print('MinHeap class skeleton defined')

การนำการเลื่อนขึ้นไปใช้งาน

การเลื่อนขึ้นจะเปรียบเทียบโหนดกับ parent ของมัน และสลับขึ้นด้านบนตราบใดที่คุณสมบัติของฮีป (parent <= child สำหรับฮีปต่ำสุด) ยังถูกละเมิด จุดสำคัญคือองค์ประกอบที่เพิ่งแทรกอยู่ท้ายสุดและจะลอยขึ้นไปยังตำแหน่งที่ถูกต้อง ลูป while ทำงานไม่เกิน floor(log n) ครั้ง ซึ่งเท่ากับความสูงของต้นไม้ ในแต่ละขั้นตอนให้กำหนด i = parent เพื่อเลื่อนต่อขึ้นด้านบน

class MinHeap:
    def __init__(self):
        self._data = []

    def _parent(self, i): return (i - 1) // 2
    def _left(self, i):   return 2 * i + 1
    def _right(self, i):  return 2 * i + 2

    def _sift_up(self, i):
        while i > 0:
            p = self._parent(i)
            if self._data[p] > self._data[i]:  # parent > child: swap
                self._data[p], self._data[i] = self._data[i], self._data[p]
                i = p
            else:
                break  # heap property satisfied

    def push(self, val):
        self._data.append(val)
        self._sift_up(len(self._data) - 1)

h = MinHeap()
for v in [5, 3, 8, 1, 4]:
    h.push(v)
print(h._data)  # valid min-heap

การนำการเลื่อนลงไปใช้งาน

การเลื่อนลงจะดันโหนดลงด้านล่างด้วยการสลับกับโหนดลูกที่มีค่าน้อยที่สุดซ้ำ ๆ (สำหรับฮีปต่ำสุด) จนไม่มีโหนดลูกใดมีค่าน้อยกว่าอีกต่อไป หรือจนกว่าโหนดจะไปถึงใบ ให้เปรียบเทียบกับโหนดลูกทั้งสองเสมอ และสลับกับโหนดที่มีค่าน้อยกว่าเพื่อรักษาคุณสมบัติของฮีป อย่าลืมตรวจสอบว่าดัชนีของโหนดลูกอยู่ภายในขอบเขตก่อนเปรียบเทียบค่า

def _sift_down(data, i):
    n = len(data)
    while True:
        smallest = i
        l = 2 * i + 1
        r = 2 * i + 2
        if l < n and data[l] < data[smallest]:
            smallest = l
        if r < n and data[r] < data[smallest]:
            smallest = r
        if smallest == i:
            break  # already the smallest among i, l, r
        data[i], data[smallest] = data[smallest], data[i]
        i = smallest

# Test: put a large value at root and sift down
heap = [10, 1, 2, 3, 4, 5, 6]
print('Before sift-down:', heap)
_sift_down(heap, 0)
print('After sift-down:', heap)  # 1 should reach top, 10 sink

เติมการนำออกให้ MinHeap สมบูรณ์

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

class MinHeap:
    def __init__(self):
        self._data = []

    def push(self, val):
        self._data.append(val)
        i = len(self._data) - 1
        while i > 0:
            p = (i - 1) // 2
            if self._data[p] > self._data[i]:
                self._data[p], self._data[i] = self._data[i], self._data[p]
                i = p
            else: break

    def pop(self):
        if not self._data: return None
        if len(self._data) == 1: return self._data.pop()
        result = self._data[0]
        self._data[0] = self._data.pop()  # last -> root
        i, n = 0, len(self._data)
        while True:
            s, l, r = i, 2*i+1, 2*i+2
            if l < n and self._data[l] < self._data[s]: s = l
            if r < n and self._data[r] < self._data[s]: s = r
            if s == i: break
            self._data[i], self._data[s] = self._data[s], self._data[i]
            i = s
        return result

h = MinHeap()
for v in [5, 3, 8, 1, 4, 2]: h.push(v)
print([h.pop() for _ in range(6)])  # [1,2,3,4,5,8] sorted

อัลกอริทึม heapify ของฟลอยด์

อัลกอริทึมของฟลอยด์สร้างฮีปต่ำสุดจากอาร์เรย์ที่ยังไม่ได้เรียงในเวลา O(n) ด้วยการเรียกการเลื่อนลงกับโหนดที่ไม่ใช่ใบทุกโหนด โดยเริ่มจากโหนดภายในโหนดสุดท้าย (n//2 - 1) และเคลื่อนไปยังราก โหนดใบเป็นฮีปที่มีองค์ประกอบเดียวและถูกต้องอยู่แล้ว เวลา O(n) มาจากข้อเท็จจริงที่ว่าโหนดส่วนใหญ่อยู่ใกล้ด้านล่างของต้นไม้ และต้องเลื่อนลงเพียงระยะสั้น ๆ เท่านั้น

def heapify(arr):
    n = len(arr)
    # Start from last non-leaf: index n//2 - 1
    # Work backward to root (index 0)
    for i in range(n // 2 - 1, -1, -1):
        # Sift down node at index i
        j = i
        while True:
            s = j
            l, r = 2*j+1, 2*j+2
            if l < n and arr[l] < arr[s]: s = l
            if r < n and arr[r] < arr[s]: s = r
            if s == j: break
            arr[j], arr[s] = arr[s], arr[j]
            j = s
    return arr

arr = [9, 7, 5, 3, 1, 8, 2, 4, 6]
print('Before:', arr)
heapify(arr)
print('After (min-heap):', arr)  # arr[0] should be 1

เหตุใดอัลกอริทึมของฟลอยด์จึงเป็น O(n)

การพิสูจน์ O(n): ต้นไม้มีโหนด n/2^(k+1) ที่ความสูง k โหนดแต่ละโหนดที่ความสูง k จะสลับตำแหน่งระหว่างการเลื่อนลงได้ไม่เกิน k ครั้ง งานทั้งหมด = ผลรวมตามความสูง k ทั้งหมด: n/2^(k+1) * k อนุกรมเรขาคณิตนี้ลู่เข้าสู่ O(n) ลองเปรียบเทียบกับการแทรกทีละรายการแบบตรงไปตรงมา: push แต่ละครั้งใช้เวลา O(log n) ดังนั้น push n ครั้งจึงใช้เวลา O(n log n) อัลกอริทึมของฟลอยด์ดีกว่าอย่างเคร่งครัดสำหรับการสร้างแบบกลุ่ม

import time
import random

# Compare: O(n) heapify vs O(n log n) one-by-one
n = 100000
data = list(range(n, 0, -1))  # reverse sorted = worst case for push

# Method 1: Floyd's O(n)
data1 = data[:]
start = time.time()
for i in range(n // 2 - 1, -1, -1):
    j = i
    while True:
        s = j; l, r = 2*j+1, 2*j+2
        if l < n and data1[l] < data1[s]: s = l
        if r < n and data1[r] < data1[s]: s = r
        if s == j: break
        data1[j], data1[s] = data1[s], data1[j]; j = s
print(f'Floyd heapify: {time.time()-start:.4f}s')

# Method 2: One-by-one insertion
import heapq
start = time.time()
heap = []
for x in data: heapq.heappush(heap, x)
print(f'Push one-by-one: {time.time()-start:.4f}s')

การ push ฮีปลงในคอลเลกชันที่มีอยู่

heapq.heappushpop และ heapq.heapreplace ของไพธอนเป็นการดำเนินการแบบรวมที่มีประสิทธิภาพ heappushpop(heap, item) จะ push รายการใหม่แล้ว pop ค่าที่น้อยที่สุดออกทันที ซึ่งมีประสิทธิภาพกว่าการเรียกสองครั้งแยกกัน heapreplace(heap, item) จะ pop ค่าที่น้อยที่สุดและ push รายการใหม่ในการทำงานรอบเดียว (รายการใหม่ต้องมีค่า >= ค่าต่ำสุดเดิมจึงจะถูกต้อง) การดำเนินการเหล่านี้มีประโยชน์ในอัลกอริทึมแบบสตรีมสำหรับการค้นหา k อันดับแรก

import heapq

heap = [1, 3, 5, 7, 9]
heapq.heapify(heap)

# heappushpop: push 2, then pop minimum
# More efficient than push + pop separately
result = heapq.heappushpop(heap, 2)
print('heappushpop(2):', result, '| heap:', heap)

# heapreplace: pop minimum, then push new item
# New item does NOT need to be larger (different from heappushpop)
result2 = heapq.heapreplace(heap, 4)
print('heapreplace(4):', result2, '| heap:', heap)

# Use case: maintaining a fixed-size top-k heap
# heappushpop is the standard pattern

การนำ MaxHeap ไปใช้งานตั้งแต่ต้น

MaxHeapกลับทิศทางการเปรียบเทียบ กล่าวคือ parent ต้องมีค่ามากกว่าหรือเท่ากับโหนดลูกหลานทั้งหมด เพียงกลับทิศทางการเปรียบเทียบในการเลื่อนขึ้นและการเลื่อนลง อีกทางเลือกหนึ่งคือห่อหุ้มค่าไว้ในคลาสที่ทำการทำให้เป็นค่าติดลบ หรือทำให้จำนวนเต็มติดลบเช่นเดียวกับที่ทำกับ heapq ของไพธอน การสร้างตั้งแต่ต้นแสดงให้เห็นว่าฮีปต่ำสุดและฮีปสูงสุดมีโครงสร้างเหมือนกัน ต่างกันเพียงตัวดำเนินการเปรียบเทียบเท่านั้น

class MaxHeap:
    def __init__(self):
        self._data = []

    def push(self, val):
        self._data.append(val)
        i = len(self._data) - 1
        while i > 0:
            p = (i - 1) // 2
            if self._data[p] < self._data[i]:  # FLIP: parent < child = violation
                self._data[p], self._data[i] = self._data[i], self._data[p]
                i = p
            else: break

    def pop(self):
        if not self._data: return None
        if len(self._data) == 1: return self._data.pop()
        result = self._data[0]
        self._data[0] = self._data.pop()
        i, n = 0, len(self._data)
        while True:
            g = i; l, r = 2*i+1, 2*i+2
            if l < n and self._data[l] > self._data[g]: g = l  # FLIP
            if r < n and self._data[r] > self._data[g]: g = r  # FLIP
            if g == i: break
            self._data[i], self._data[g] = self._data[g], self._data[i]; i = g
        return result

h = MaxHeap()
for v in [5, 3, 8, 1, 4, 2]: h.push(v)
print([h.pop() for _ in range(6)])  # [8,5,4,3,2,1]

ลบองค์ประกอบใดก็ได้จากฮีป

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

def delete_at_index(heap, i):
    n = len(heap)
    heap[i] = heap[n - 1]
    heap.pop()
    if i >= len(heap):
        return  # deleted the last element
    # Try sift-up first
    p = (i - 1) // 2
    if i > 0 and heap[i] < heap[p]:
        while i > 0:
            p = (i - 1) // 2
            if heap[p] > heap[i]:
                heap[p], heap[i] = heap[i], heap[p]; i = p
            else: break
    else:  # sift down
        j = i; n2 = len(heap)
        while True:
            s = j; l, r = 2*j+1, 2*j+2
            if l < n2 and heap[l] < heap[s]: s = l
            if r < n2 and heap[r] < heap[s]: s = r
            if s == j: break
            heap[j], heap[s] = heap[s], heap[j]; j = s

heap = [1, 3, 2, 7, 4, 5, 6]
print('Before:', heap)
delete_at_index(heap, 2)  # delete element at index 2 (value=2)
print('After:', heap)  # 2 removed, heap still valid

องค์ประกอบที่พบบ่อยที่สุด K อันดับแรกในฮีป

องค์ประกอบที่พบบ่อยที่สุด K อันดับแรก (LeetCode #347) ใช้ฮีปต่ำสุดขนาด k รักษาฮีปต่ำสุดที่แต่ละรายการมีรูปแบบ (frequency, element) ประมวลผลองค์ประกอบที่ไม่ซ้ำกันแต่ละรายการ: หากฮีปมีองค์ประกอบน้อยกว่า k รายการ ให้ push หากไม่ใช่ และความถี่ขององค์ประกอบใหม่มากกว่าค่าต่ำสุดในฮีป ให้ pop แล้ว push องค์ประกอบใหม่ ฮีปสุดท้ายจะมีองค์ประกอบที่พบบ่อยที่สุด k รายการในเวลา O(n log k)

import heapq
from collections import Counter

def top_k_frequent(nums, k):
    count = Counter(nums)
    # Min-heap of (frequency, num)
    heap = []
    for num, freq in count.items():
        heapq.heappush(heap, (freq, num))
        if len(heap) > k:
            heapq.heappop(heap)  # remove least frequent
    return [num for freq, num in heap]

print(top_k_frequent([1,1,1,2,2,3], 2))  # [1, 2]
print(top_k_frequent([4,4,4,3,3,2,1], 2)) # [4, 3]

การประยุกต์ใช้ฮีปในการจัดตารางเวลา

นอกเหนือจากการเขียนโปรแกรมแข่งขันแล้ว ฮีปยังเป็นกลไกสำคัญของ ระบบจัดตารางเวลา ในโลกจริง ตัวจัดตารางงานของระบบปฏิบัติการใช้คิวลำดับความสำคัญ (ฮีป) เพื่อเรียกใช้กระบวนการที่พร้อมและมีลำดับความสำคัญสูงสุดก่อนเสมอ การจำลองแบบขับเคลื่อนด้วยเหตุการณ์จะประมวลผลเหตุการณ์ตามลำดับเวลาโดยใช้ฮีปต่ำสุดที่จัดลำดับตามเวลาเหตุการณ์ ตัวจัดตารางแพ็กเก็ตเครือข่ายจะจัดลำดับความสำคัญของการรับส่งข้อมูลตามคลาสคุณภาพการให้บริการ การทำความเข้าใจฮีปจะช่วยให้คุณมีแบบจำลองทางความคิดสำหรับระบบเหล่านี้ทั้งหมด และมักปรากฏในการสัมภาษณ์ด้านการออกแบบระบบเกี่ยวกับคิวและการจัดตารางเวลา

import heapq

# Simple event-driven simulation using a heap
events = []  # (time, event_description)

def schedule(time, event):
    heapq.heappush(events, (time, event))

def process_next():
    time, event = heapq.heappop(events)
    print(f't={time}: {event}')
    return time, event

# Schedule events out of order:
schedule(10, 'Send email')
schedule(3,  'Open app')
schedule(7,  'Process request')
schedule(1,  'Start server')

# Process in time order:
while events:
    process_next()
# Output: t=1, t=3, t=7, t=10 -- always in time order

ตรวจสอบความเข้าใจ

ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์เขียนโปรแกรมจากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้เกี่ยวกับ MinHeap และ MaxHeap ตั้งแต่ต้น ด้วยการเลื่อนขึ้นและเลื่อนลง อัลกอริทึม heapify ของฟลอยด์ที่มีความซับซ้อน O(n) และเหตุผลที่มีประสิทธิภาพมากกว่าการแทรกทีละรายการซึ่งมีความซับซ้อน O(n log n) รวมถึง การประยุกต์ใช้จริง เช่น การหาสมาชิกที่พบบ่อยที่สุด k อันดับแรกและการลบตามดัชนี บทถัดไปเราจะสำรวจโมดูล heapq ของไพธอนและเทคนิคการจำลองฮีปสูงสุด

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

บทเรียน “สร้างฮีป เลื่อนเข้า และเลื่อนออกตั้งแต่ต้น” ฟรีหรือไม่

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

คุณจะเรียนรู้อะไรในบทเรียน “สร้างฮีป เลื่อนเข้า และเลื่อนออกตั้งแต่ต้น”

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

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

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

บทเรียน “สร้างฮีป เลื่อนเข้า และเลื่อนออกตั้งแต่ต้น” ใช้เวลานานแค่ไหน

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

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

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

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

  1. คุณสมบัติฮีปและการแทนด้วยอาร์เรย์
  2. สร้างฮีป เลื่อนเข้า และเลื่อนออกตั้งแต่ต้น
  3. heapq ของ Python และเทคนิคฮีปสูงสุด
  4. มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง
← กลับไปที่ Coding Interview Prep