มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง
ดูแลฮีปสองชุด (ฮีปสูงสุดของครึ่งล่างและฮีปต่ำสุดของครึ่งบน) เพื่ออัปเดตมัธยฐานในเวลา O(log n) และผสานลิสต์ที่เรียงแล้ว k ลิสต์ด้วยฮีป
มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ปัญหามัธยฐานจากสตรีมข้อมูล
ค้นหามัธยฐานจากสตรีมข้อมูล (LeetCode #295) กำหนดให้รองรับการดำเนินการสองอย่างอย่างมีประสิทธิภาพ ได้แก่ addNum(num) สำหรับ add ตัวเลข และ findMedian() สำหรับ get ค่ามัธยฐานปัจจุบัน มัธยฐานของรายการที่มีความยาวเป็นเลขคู่คือค่าเฉลี่ยของค่าตรงกลางสองค่า รายการที่เรียงลำดับด้วยวิธีตรงไปตรงมาจะมีการแทรก O(n) และการหามัธยฐาน O(1) วิธีที่เหมาะที่สุดใช้ ฮีปสองชุด เพื่อให้การแทรกมีความซับซ้อน O(log n) และการหามัธยฐานมีความซับซ้อน O(1)
import heapq
# Strategy: maintain two halves of the data
# max_heap: lower half (stores negated values for max behavior)
# min_heap: upper half
# Invariant: len(max_heap) == len(min_heap) or len(max_heap) == len(min_heap) + 1
# Invariant: max(max_heap) <= min(min_heap)
# Median:
# odd count: max_heap[0] (top of lower half)
# even count: average of tops of both halves
print('Two-heap strategy for O(log n) insert, O(1) median')การใช้งาน MedianFinder ด้วยฮีปสองชุด
ดูแลรักษา ฮีปสูงสุดสำหรับครึ่งล่าง และ ฮีปต่ำสุดสำหรับครึ่งบน ตรวจสอบให้แน่ใจเสมอว่าฮีปสูงสุดมีขนาดเท่ากับฮีปต่ำสุด หรือมีสมาชิกมากกว่าหนึ่งรายการ เมื่อเพิ่มตัวเลข ให้ push ไปยังฮีปสูงสุด จากนั้น balance ด้วยการย้ายสมาชิกบนสุดของฮีปสูงสุดไปยังฮีปต่ำสุด หากสมาชิกบนสุดมีค่ามากกว่าค่าต่ำสุดของฮีปต่ำสุด และปรับสมดุลขนาดหากจำเป็น
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (negated) for lower half
self.hi = [] # min-heap for upper half
def addNum(self, num):
heapq.heappush(self.lo, -num) # push to lower half
# Ensure max of lower <= min of upper
if self.hi and -self.lo[0] > self.hi[0]:
heapq.heappush(self.hi, -heapq.heappop(self.lo))
# Balance sizes: lo can have at most 1 more than hi
if len(self.lo) > len(self.hi) + 1:
heapq.heappush(self.hi, -heapq.heappop(self.lo))
elif len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def findMedian(self):
if len(self.lo) > len(self.hi):
return -self.lo[0] # odd count: top of lower half
return (-self.lo[0] + self.hi[0]) / 2
mf = MedianFinder()
for n in [1, 2, 3, 4, 5]: mf.addNum(n)
print(mf.findMedian()) # 3.0ติดตามขั้นตอนของ MedianFinder
การเข้าใจเหตุผลที่ต้องรักษาเงื่อนไขคงที่ของฮีปสองชุดมีความสำคัญอย่างยิ่งต่อการอธิบายวิธีแก้ปัญหาในการสัมภาษณ์ ลองติดตามการเพิ่มค่า [5, 15, 1, 3] ทีละขั้นตอน หลังการแทรกแต่ละครั้ง ให้ balance เพื่อให้ฮีปสูงสุดของครึ่งล่างเก็บค่าครึ่งที่น้อยกว่าไว้ เงื่อนไขคงที่จะรับประกันว่า max(lo) <= min(hi) เป็นจริงเสมอ ทำให้เข้าถึงมัธยฐานได้โดยตรงจากสมาชิกบนสุดของฮีปใดฮีปหนึ่งหรือทั้งสองฮีป
import heapq
# Manual trace for [5, 15, 1, 3]:
# add 5: lo=[-5] hi=[] median=5
# add 15: lo=[-5] hi=[15] median=(5+15)/2=10
# add 1: lo=[-5,-1] hi=[15] median=5
# add 3: lo=[-5,-3,-1] hi=[15] -- lo too big
# -> lo=[-5,-3] hi=[1,15] -- wait, wrong direction
# Actually:
# add 1: push to lo -> lo=[-5,-1], then 1>lo? No, -lo[0]=5>15? No
# lo has 2, hi has 1: balance -> move lo top to hi
# lo=[-1], hi=[5,15]
# Median = (-lo[0] + hi[0])/2 = (1+5)/2 = 3
mf2 = MedianFinder()
for n, expected in [(5, 5.0), (15, 10.0), (1, 5.0), (3, 4.0)]:
mf2.addNum(n)
print(f'After adding {n}: median={mf2.findMedian()} (expected ~{expected})')มัธยฐานของหน้าต่างเลื่อน
มัธยฐานของหน้าต่างเลื่อน (LeetCode #480) เป็นรูปแบบที่ยากขึ้น โดยให้หามัธยฐานของทุกหน้าต่างขนาด k ขณะที่หน้าต่างเลื่อนไปตามอาร์เรย์ วิธีใช้ฮีปสองชุดสามารถขยายได้ด้วยเซต การลบแบบขี้เกียจ เพื่อจัดการสมาชิกที่เลื่อนออกจากหน้าต่าง เมื่อสมาชิกออกจากหน้าต่าง ให้ทำเครื่องหมายสมาชิกนั้นในเซตการลบ และเมื่อสมาชิกดังกล่าวขึ้นมาอยู่บนสุดของฮีปใดฮีปหนึ่ง ให้ทิ้งสมาชิกนั้น
import heapq
def median_sliding_window(nums, k):
lo = [] # max-heap (negated)
hi = [] # min-heap
removed = {}
result = []
def balance():
# Move valid tops to correct side
while lo and removed.get(-lo[0], 0) > 0:
removed[-lo[0]] -= 1; heapq.heappop(lo)
while hi and removed.get(hi[0], 0) > 0:
removed[hi[0]] -= 1; heapq.heappop(hi)
for i, num in enumerate(nums):
heapq.heappush(lo, -num)
heapq.heappush(hi, -heapq.heappop(lo))
if len(hi) > len(lo): heapq.heappush(lo, -heapq.heappop(hi))
if i >= k:
out = nums[i - k]
removed[out] = removed.get(out, 0) + 1
balance()
if len(lo) > len(hi): heapq.heappush(hi, -heapq.heappop(lo))
if i >= k - 1:
if len(lo) > len(hi): result.append(float(-lo[0]))
else: result.append((-lo[0] + hi[0]) / 2.0)
return result
print(median_sliding_window([1,3,-1,-3,5,3,6,7], 3)) # [1,-1,-1,3,5,6]การผสานแบบ k ทาง: ปัญหา
การผสานรายการที่เรียงลำดับแล้ว k รายการ (LeetCode #23) เป็นปัญหาพื้นฐานที่ประยุกต์ใช้กับการเรียงลำดับภายนอก การ merge ฐานข้อมูล และระบบแบบกระจาย เมื่อกำหนดรายการเชื่อมโยงที่เรียงลำดับแล้ว k รายการ ซึ่งมีโหนดรวมทั้งหมด n โหนด ให้ merge เป็นรายการเรียงลำดับเดียว วิธีพื้นฐานคือ merge ครั้งละสองรายการ ซึ่งมีความซับซ้อน O(kn) หรือ O(n log k) หากใช้การแบ่งแล้วพิชิต วิธีใช้ฮีปจะประมวลผลแต่ละโหนดเพียงครั้งเดียว โดยใช้เวลา O(log k) ต่อโหนด รวมเป็น O(n log k)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Build a linked list from a Python list
def build_list(arr):
dummy = ListNode(0)
curr = dummy
for val in arr:
curr.next = ListNode(val)
curr = curr.next
return dummy.next
# Convert linked list to Python list for printing
def to_list(head):
result = []
while head:
result.append(head.val)
head = head.next
return result
print('K-way merge: O(n log k) using a min-heap of k heads')การผสานแบบ k ทางด้วยฮีปต่ำสุด
เริ่มต้นฮีปด้วย โหนดแรกของแต่ละรายการ ในแต่ละขั้นตอน ให้ pop ค่าต่ำสุด เพิ่มค่านั้นลงในผลลัพธ์ และ push โหนดถัดไปจากรายการนั้น หากมี ฮีปจะมีสมาชิกไม่เกิน k รายการเสมอ โดยมีหัวรายการหนึ่งรายการต่อรายการที่ยังทำงานอยู่ เนื่องจากเราประมวลผลโหนดทั้งหมด n โหนด และแต่ละโหนดใช้การดำเนินการฮีป O(log k) เวลารวมจึงเป็น O(n log k) และพื้นที่สำหรับฮีปเป็น O(k)
import heapq
def merge_k_lists(lists):
dummy = ListNode(0)
curr = dummy
heap = []
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
while heap:
val, i, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
lists = [
build_list([1, 4, 5]),
build_list([1, 3, 4]),
build_list([2, 6])
]
result = merge_k_lists(lists)
print(to_list(result)) # [1, 1, 2, 3, 4, 4, 5, 6]ช่วงที่เล็กที่สุดซึ่งครอบคลุมรายการ k รายการ
ช่วงที่เล็กที่สุด (LeetCode #632) ใช้ค้นหาช่วง [lo, hi] ที่เล็กที่สุด ซึ่งมีสมาชิกอย่างน้อยหนึ่งรายการจากแต่ละรายการที่เรียงลำดับแล้ว k รายการอยู่ภายในช่วงนั้น ให้ใช้ฮีปต่ำสุดที่เริ่มต้นด้วยสมาชิกแรกของแต่ละรายการ และติดตามค่าสูงสุดปัจจุบัน ลดขนาดช่วงโดยเลื่อนรายการที่มีค่าต่ำสุดปัจจุบันไปข้างหน้าเสมอ หยุดเมื่อรายการใดรายการหนึ่งหมด
import heapq
def smallest_range(nums):
heap = []
current_max = float('-inf')
for i, lst in enumerate(nums):
heapq.heappush(heap, (lst[0], i, 0))
current_max = max(current_max, lst[0])
best = [float('-inf'), float('inf')]
while heap:
current_min, list_idx, elem_idx = heapq.heappop(heap)
if current_max - current_min < best[1] - best[0]:
best = [current_min, current_max]
if elem_idx + 1 >= len(nums[list_idx]):
break # one list exhausted
next_val = nums[list_idx][elem_idx + 1]
heapq.heappush(heap, (next_val, list_idx, elem_idx + 1))
current_max = max(current_max, next_val)
return best
print(smallest_range([[4,10,15,24,26],[0,9,12,20],[5,18,22,30]]))
# [20, 24]สมาชิกที่เล็กเป็นอันดับ k ในเมทริกซ์
สมาชิกที่เล็กเป็นอันดับ k ในเมทริกซ์ที่เรียงลำดับแล้ว (LeetCode #378): เมทริกซ์ n×n ที่แต่ละแถวและคอลัมน์เรียงลำดับแล้ว ให้ค้นหาสมาชิกที่เล็กเป็นอันดับ k มองแต่ละแถวเป็นรายการที่เรียงลำดับแล้ว และใช้การผสานแบบ k ทางด้วยฮีป อีกทางเลือกหนึ่งคือการค้นหาแบบทวิภาคในช่วงของค่า วิธีใช้ฮีปมีความซับซ้อน O(k log n) ซึ่งมีประสิทธิภาพเมื่อ k มีขนาดเล็ก ส่วนการค้นหาแบบทวิภาคมีความซับซ้อน O(n log(max-min)) และเหมาะกับกรณีที่ k มีขนาดใหญ่กว่า
import heapq
def kth_smallest_matrix(matrix, k):
n = len(matrix)
heap = [(matrix[0][0], 0, 0)]
count = 0
visited = {(0, 0)}
while heap:
val, r, c = heapq.heappop(heap)
count += 1
if count == k:
return val
# Push right neighbor
if c + 1 < n and (r, c+1) not in visited:
heapq.heappush(heap, (matrix[r][c+1], r, c+1))
visited.add((r, c+1))
# Push bottom neighbor
if r + 1 < n and (r+1, c) not in visited:
heapq.heappush(heap, (matrix[r+1][c], r+1, c))
visited.add((r+1, c))
return -1
matrix = [[1,5,9],[10,11,13],[12,13,15]]
print(kth_smallest_matrix(matrix, 8)) # 13ฮีปสองชุดสำหรับสถิติแบบต่อเนื่อง
รูปแบบฮีปสองชุดสามารถนำไปใช้ได้กว้างกว่ามัธยฐาน คุณสามารถใช้รูปแบบนี้เพื่อดูแลรักษา quantile แบบต่อเนื่อง เช่น เปอร์เซ็นไทล์ที่ 25 โดยกำหนดขนาดฮีปล่างให้เก็บสมาชิก p*n รายการ และฮีปบนให้เก็บสมาชิก (1-p)*n รายการ ทุกครั้งที่เพิ่มสมาชิก ให้ปรับสมดุลเช่นเดียวกับก่อนหน้านี้ รูปแบบนี้ปรากฏในปัญหาสถิติแบบสตรีม ซึ่งต้องการการแทรกที่มีประสิทธิภาพและการค้นหา quantile พร้อมกัน
import heapq
# Generalised two-heap for arbitrary quantile p
# lo contains floor(p * count) elements
# hi contains the remaining elements
class QuantileFinder:
def __init__(self, p):
self.p = p # quantile (e.g., 0.5 for median)
self.lo = [] # max-heap
self.hi = [] # min-heap
self.count = 0
def add(self, num):
self.count += 1
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
# Target: lo should have floor(p * count) elements
target_lo = int(self.p * self.count)
while len(self.lo) < target_lo:
heapq.heappush(self.lo, -heapq.heappop(self.hi))
while len(self.lo) > target_lo:
heapq.heappush(self.hi, -heapq.heappop(self.lo))
def quantile(self):
return -self.lo[0] if self.lo else self.hi[0]
qf = QuantileFinder(0.5) # median
for n in [1, 2, 3, 4, 5, 6]: qf.add(n)
print(qf.quantile()) # 3 (median of 1-6)ค้นหาจุด k จุดที่ใกล้จุดกำเนิดที่สุด
จุด k จุดที่ใกล้จุดกำเนิดที่สุด (LeetCode #973) ใช้ฮีปสูงสุดขนาด k ให้ push ระยะทางยกกำลังสองของแต่ละจุดเพื่อหลีกเลี่ยงการคำนวณรากที่สอง เมื่อขนาดฮีปเกิน k ให้ pop จุดที่อยู่ไกลที่สุดออก จุด k จุดที่เหลือคือจุดที่ใกล้ที่สุด k จุด วิธีนี้มีความซับซ้อน O(n log k) อีกทางเลือกหนึ่งคือวิธีเลือกอย่างรวดเร็วซึ่งมีความซับซ้อนเฉลี่ย O(n) แต่วิธีใช้ฮีปนั้นนำไปใช้อย่างถูกต้องและอธิบายระหว่างการสัมภาษณ์ได้ง่ายกว่า
import heapq
def k_closest(points, k):
heap = [] # max-heap via negation
for x, y in points:
dist_sq = x*x + y*y
heapq.heappush(heap, (-dist_sq, x, y))
if len(heap) > k:
heapq.heappop(heap) # remove farthest
return [[x, y] for _, x, y in heap]
points = [[1,3], [-2,2], [5,8], [0,1], [-1,-1]]
print(k_closest(points, 2))
# Two closest to origin: [0,1] (dist=1) and [-1,-1] (dist=2)
# Verify by distances:
for x, y in points:
print(f'({x},{y}): dist^2 = {x*x+y*y}')การวิเคราะห์เวลาและพื้นที่ของฮีปสองชุด
วิธีใช้ฮีปสองชุดสำหรับมัธยฐานมีความซับซ้อน O(log n) ต่อการดำเนินการ addNum และ O(1) ต่อการดำเนินการ findMedian พื้นที่ที่ใช้คือ O(n) สำหรับจัดเก็บสมาชิกทั้งหมด การผสานแบบ k ทางใช้เวลา O(n log k) และใช้พื้นที่ O(k) สำหรับฮีป วิธีเหล่านี้เกือบจะเหมาะที่สุดแล้ว โดยสามารถพิสูจน์ขอบเขตล่างแบบโอเมกาสำหรับการเปรียบเทียบได้ว่าเป็น Omega(n log k) สำหรับการผสานแบบ k ทาง แสดงให้เห็นว่าวิธีใช้ฮีปเหมาะที่สุดในเชิงลำดับการเติบโต ควรระบุความซับซ้อนเหล่านี้ให้ชัดเจนเสมอในการสัมภาษณ์
# Complexity summary for heap applications:
# Problem | Time per op | Space
# ----------------------|--------------|------
# MedianFinder.addNum | O(log n) | O(n)
# MedianFinder.find | O(1) | -
# Merge k sorted lists | O(n log k) | O(k)
# Kth smallest matrix | O(k log n) | O(n)
# K closest points | O(n log k) | O(k)
# Task scheduler | O(n log 26) | O(26)
# Kth largest stream | O(log k) | O(k)
# Sliding window median | O(n log k) | O(k)
print('Heap problems: identify k (heap size) vs n (input size)')ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์เขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้เกี่ยวกับ MedianFinder ด้วยฮีปสองชุด ซึ่งทำให้การแทรกมีความซับซ้อน O(log n) และการหามัธยฐานมีความซับซ้อน O(1) การ merge แบบ k ทาง ด้วยฮีปต่ำสุดที่ใช้เวลา O(n log k) และพื้นที่ O(k) รวมถึงการประยุกต์ใช้เพิ่มเติม เช่น มัธยฐานของหน้าต่างเลื่อน ช่วงที่เล็กที่สุด และจุด k จุดที่ใกล้ที่สุด บทถัดไปเราจะสำรวจการแทนกราฟและการเตรียมการท่องกราฟ
คำถามที่พบบ่อย
บทเรียน “มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง”
ดูแลฮีปสองชุด (ฮีปสูงสุดของครึ่งล่างและฮีปต่ำสุดของครึ่งบน) เพื่ออัปเดตมัธยฐานในเวลา O(log n) และผสานลิสต์ที่เรียงแล้ว k ลิสต์ด้วยฮีป คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- คุณสมบัติฮีปและการแทนด้วยอาร์เรย์
- สร้างฮีป เลื่อนเข้า และเลื่อนออกตั้งแต่ต้น
- heapq ของ Python และเทคนิคฮีปสูงสุด
- มัธยฐานจากกระแสข้อมูลและการผสาน k ทาง