2.38. คิวลำดับความสำคัญ¶
คิวลำดับความสำคัญคือคอนเทนเนอร์ที่รู้เสมอว่ารายการใดในนั้นเล็กที่สุด (หรือใหญ่ที่สุด) โดยไม่ต้องเรียงลำดับทั้งหมด heapq นำสิ่งนี้มาใช้งานใน MicroPython โดยใช้ list ธรรมดาเป็นพื้นที่จัดเก็บและใช้อัลกอริทึมที่อิงจากโครงสร้างต้นไม้ที่เรียกว่า min-heap เพื่อคงรายการที่เล็กที่สุดไว้ที่ดัชนี 0
2.38.1. แบบจำลองทางความคิด¶
min-heap คือลิสต์ที่จัดเรียงให้รายการในทุกดัชนีมีค่าน้อยกว่าหรือเท่ากับรายการที่ดัชนี 2*i + 1 และ 2*i + 2 การจัดเรียงแบบนี้ทำให้การดำเนินการสามอย่างมีต้นทุนต่ำ:
การหารายการที่เล็กที่สุด -- อยู่ที่
heap[0]เป็นการค้นหาด้วยดัชนีเพียงครั้งเดียวโดยไม่ต้องสแกนการเพิ่มรายการ -- รายการจะ bubble up ผ่านต้นไม้โดยเปรียบเทียบกับ parent หนึ่งโหนดต่อระดับ หนึ่งพันรายการใช้การเปรียบเทียบประมาณสิบครั้ง ส่วนหนึ่งล้านรายการใช้ประมาณยี่สิบครั้ง
การลบรายการที่เล็กที่สุด -- เดินย้อนทางเดิมโดยใช้การเปรียบเทียบจำนวนเล็กน้อยเท่ากัน
ลิสต์นี้ ไม่ได้ ถูกเรียงลำดับ การวนซ้ำผ่านลิสต์จะให้รายการในลำดับที่ไม่แน่นอน มีเพียง heap[0] เท่านั้นที่รับประกันว่าเป็นค่าต่ำสุด
2.38.2. ฟังก์ชันสามตัวใน MicroPython¶
โมดูล heapq ของ MicroPython เปิดเผยฟังก์ชันเพียงสามตัว:
heapq.heappush()-- แทรกรายการโดยรักษา heap invariant ไว้heapq.heappop()-- ลบและคืนค่ารายการที่เล็กที่สุดheapq.heapify()-- จัดเรียงลิสต์ที่มีอยู่เป็น heap ในตำแหน่งเดิม
นั่นคือขอบเขตทั้งหมด ฟังก์ชัน heappushpop, heapreplace, nlargest, nsmallest, และ merge ของ CPython ไม่มี ใน MicroPython แบบแผนด้านล่างแสดงวิธีสร้างฟังก์ชันที่ใช้บ่อยด้วยตนเอง
เริ่มต้นใหม่:
>>> import heapq
>>> heap = []
>>> heapq.heappush(heap, 5)
>>> heapq.heappush(heap, 1)
>>> heapq.heappush(heap, 3)
>>> heap
[1, 5, 3]
>>> heapq.heappop(heap)
1
>>> heapq.heappop(heap)
3
>>> heapq.heappop(heap)
5
เริ่มต้นจากข้อมูลที่มีอยู่:
>>> samples = [5, 1, 3, 2, 4]
>>> heapq.heapify(samples)
>>> samples
[1, 2, 3, 5, 4]
>>> heapq.heappop(samples)
1
การทำ Heapify จะสัมผัสแต่ละรายการหนึ่งครั้งและเร็วกว่าการ push รายการทีละตัวอย่างเห็นได้ชัด
2.38.3. Top-N จากสตรีม¶
การใช้งานคิวลำดับความสำคัญที่เป็นธรรมชาติ: ติดตาม N รายการที่ใหญ่ที่สุดในสตรีมที่อ่านได้เพียงครั้งเดียว
เทคนิคคือการเก็บ min-heap ขนาด N ค่าใหม่ทุกค่าจะถูก push เข้า ถ้า heap เติบโตเกิน N ให้ตัดรายการที่เล็กที่สุดทิ้ง รายการที่เล็กที่สุดอยู่ที่ heap[0] เสมอ ดังนั้นการเปรียบเทียบครั้งเดียวตัดสินว่าจะเก็บหรือทิ้งแต่ละรายการที่เข้ามา:
def top_n_brightest(readings, n):
heap = []
for r in readings:
if len(heap) < n:
heapq.heappush(heap, r)
elif r > heap[0]:
heapq.heappop(heap)
heapq.heappush(heap, r)
return sorted(heap, reverse=True)
heap ไม่เติบโตเกิน N ดังนั้นค่าที่เข้ามาแต่ละค่าใช้การเปรียบเทียบจำนวนเล็กน้อยเท่ากันไม่ว่าสตรีมจะยาวแค่ไหน sorted ท้ายสุดเป็นขั้นตอนเดียวที่ต้นทุนขึ้นกับ N โดยตรง
2.38.4. เหตุการณ์ที่กำหนดเวลา¶
คิวลำดับความสำคัญของ tuple แบบ (deadline, payload) คือรูปแบบมาตรฐานสำหรับตัวกำหนดตารางเหตุการณ์ Tuple เปรียบเทียบแบบ lexicographic ดังนั้น tuple ที่มี deadline น้อยที่สุดจะลอยขึ้นสู่จุดบนสุดโดยอัตโนมัติ:
import heapq
import time
queue = []
heapq.heappush(queue, (time.time() + 5, 'check sensors'))
heapq.heappush(queue, (time.time() + 1, 'blink LED'))
heapq.heappush(queue, (time.time() + 3, 'send heartbeat'))
while queue:
deadline, task = queue[0]
now = time.time()
if now < deadline:
time.sleep(deadline - now)
heapq.heappop(queue)
run(task)
ถ้าเหตุการณ์สองตัวมี deadline เดียวกัน การเปรียบเทียบ tuple จะผ่านไปยังองค์ประกอบที่สอง สำหรับ payload ที่ไม่สามารถเปรียบเทียบได้โดยธรรมชาติ ให้เพิ่มตัวนับเป็นองค์ประกอบที่สองของ tuple เพื่อบังคับลำดับที่เสถียร:
counter = 0
def schedule(deadline, task):
global counter
counter += 1
heapq.heappush(queue, (deadline, counter, task))
ตอนนี้พฤติกรรม smallest-deadline-first ยังคงอยู่ การเสมอกันแก้ไขด้วยตัวนับเพื่อให้งานที่กำหนดตารางไว้ก่อนทำงานก่อน และ task ไม่จำเป็นต้องเปรียบเทียบได้
2.38.5. สิ่งที่ heapq ไม่มีให้¶
รูปแบบบางอย่างที่ดูเหมือนว่าควรทำงานได้ แต่ไม่ได้:
การวนซ้ำ
for x in heapไม่ ให้ผลลัพธ์ที่เรียงลำดับ -- ลำดับพื้นฐานของลิสต์คือรูปแบบโครงสร้างต้นไม้ของ heap เพื่อใช้ heap ตามลำดับ ให้heappopจนว่างเปล่าไม่มีวิธี เปลี่ยน ลำดับความสำคัญของรายการที่อยู่ใน heap แล้ว วิธีแก้ปัญหามาตรฐานคือการ push ลำดับความสำคัญใหม่เป็นรายการใหม่และทำเครื่องหมายรายการเก่าว่าถูกยกเลิก (ข้ามไปเมื่อมัน pop ออกมา)
heap เก็บการอ้างอิงถึงรายการ ไม่ใช่สำเนา การเปลี่ยนแปลงรายการหลังจาก push แล้วอาจทำให้ heap invariant เสีย ใช้ค่าที่ไม่เปลี่ยนแปลง (ตัวเลข, tuple) เป็นคีย์สำหรับการเปรียบเทียบ
min-heap บน plain list คือเครื่องมือที่เหมาะสมสำหรับคำถาม "อะไรเล็กที่สุด" หรือ "อะไรเร่งด่วนที่สุด" บนอุปกรณ์ embedded ขนาดเล็ก สามฟังก์ชัน หนึ่งลิสต์ และ heap จัดการส่วนที่เหลือ