การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python
สำรวจการเรียงแบบนับและการเรียงแบบฐานสำหรับอาร์เรย์จำนวนเต็ม พร้อมทำความเข้าใจการทำงานภายในของ Timsort ของ Python เมื่อเรียกใช้การเรียงในตัว
การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ขอบเขตล่าง O(n log n) สำหรับการเปรียบเทียบ
อัลกอริทึมการเรียงลำดับใดก็ตามที่กำหนดลำดับ โดยอาศัยการเปรียบเทียบสมาชิกเท่านั้น จะต้องเปรียบเทียบอย่างน้อย Ω(n log n) ครั้งในกรณีแย่ที่สุด สิ่งนี้พิสูจน์ได้ด้วยการโต้แย้งจากต้นไม้การตัดสินใจ: การเรียงลำดับสมาชิก n ตัวต้องแยกแยะลำดับที่เป็นไปได้ทั้งหมด n! แบบ ต้นไม้การตัดสินใจแบบทวิภาค (แต่ละโหนดคือการเปรียบเทียบ) ต้องมีอย่างน้อย log₂(n!) ≈ n log₂(n) ระดับ หากต้องการทำลายขอบเขตนี้ เราต้องมีข้อมูลเพิ่มเติมเกี่ยวกับสมาชิก เช่น การที่สมาชิกเป็นจำนวนเต็มที่มีขอบเขตจำกัด
import math
for n in [5, 10, 100, 1000]:
lower_bound = n * math.log2(n)
factorial_log = sum(math.log2(i) for i in range(1, n+1))
print(f'n={n}: n*log2(n)={lower_bound:.1f}, log2(n!)={factorial_log:.1f}')
# n log n is a tight bound on comparison-based sortingการเรียงลำดับแบบนับ: เรียงตามความถี่
การเรียงลำดับแบบนับทำงานโดยนับความถี่ของแต่ละค่า จากนั้นสร้างอาร์เรย์ที่เรียงลำดับแล้วขึ้นใหม่จากจำนวนที่นับได้ วิธีนี้ต้องทราบช่วง [0, k) ของค่าต่าง ๆ ล่วงหน้า ความซับซ้อนด้านเวลา: O(n + k) ความซับซ้อนด้านพื้นที่: O(k) เมื่อ k มีขนาดเล็กเมื่อเทียบกับ n (เช่น การเรียงลำดับอายุ 0-120 ปีหรือเลขหลักเดียว) การเรียงลำดับแบบนับจะเร็วกว่าการเรียงลำดับด้วยการเปรียบเทียบทุกแบบ แต่เมื่อ k มีขนาดใหญ่ ต้นทุนพื้นที่ O(k) จะทำให้วิธีนี้ไม่เหมาะสม
def counting_sort(arr, k=None):
if not arr: return []
if k is None: k = max(arr) + 1
count = [0] * k
for n in arr:
count[n] += 1
result = []
for val, freq in enumerate(count):
result.extend([val] * freq)
return result
arr = [4, 2, 2, 8, 3, 3, 1]
print(counting_sort(arr)) # [1, 2, 2, 3, 3, 4, 8]
# O(n + k) where k = 9 (max value + 1)การเรียงลำดับแบบนับที่เสถียรด้วยจำนวนสะสม
สำหรับการเรียงลำดับแบบนับที่เสถียร (สำคัญเมื่อเรียงลำดับออบเจ็กต์ตามคีย์) ให้คำนวณจำนวนสะสมเพื่อให้ cum[v] ระบุตำแหน่งเริ่มต้นของค่า v ในผลลัพธ์ จากนั้นสแกนอาร์เรย์ข้อมูลเข้าจากขวาไปซ้าย โดยวางสมาชิกแต่ละตัวที่ตำแหน่ง cum[key] - 1 แล้วลดค่าตำแหน่งนั้นลง วิธีนี้ทำให้ได้การเรียงลำดับที่เสถียร กล่าวคือ สมาชิกที่มีคีย์เดียวกันจะยังคงลำดับสัมพัทธ์เดิมไว้
def counting_sort_stable(arr, k):
count = [0] * k
for n in arr: count[n] += 1
# Cumulative counts: count[v] = first position for value v
for i in range(1, k): count[i] += count[i-1]
output = [0] * len(arr)
# Fill from right to maintain stability
for n in reversed(arr):
count[n] -= 1
output[count[n]] = n
return output
print(counting_sort_stable([4,2,2,8,3,3,1], 9))
# [1, 2, 2, 3, 3, 4, 8]การเรียงลำดับแบบเรดิกซ์: เรียงทีละหลัก
การเรียงลำดับแบบเรดิกซ์ จะเรียงจำนวนเต็มทีละหลัก ตั้งแต่หลักที่มีนัยสำคัญน้อยที่สุด (LSD) ไปจนถึงหลักที่มีนัยสำคัญมากที่สุด (MSD) โดยใช้การเรียงลำดับที่เสถียร (เช่น การเรียงลำดับแบบนับ) ในแต่ละตำแหน่งหลัก หลังจากทำงาน d รอบ (รอบละหนึ่งหลัก) อาร์เรย์จะถูกเรียงลำดับอย่างสมบูรณ์ ความซับซ้อนด้านเวลา: O(d × (n + k)) โดย d = จำนวนหลัก และ k = ฐาน (โดยทั่วไปคือ 10) สำหรับจำนวนเต็ม n จำนวนที่มีขอบเขตไม่เกิน W จะได้ d = log_k(W) ทำให้มีความซับซ้อนรวมเป็น O(n log_k(W))
def radix_sort(arr):
if not arr: return []
max_val = max(arr)
exp = 1 # current digit position (1, 10, 100, ...)
while max_val // exp > 0:
arr = counting_sort_by_digit(arr, exp)
exp *= 10
return arr
def counting_sort_by_digit(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for n_ in arr: count[(n_ // exp) % 10] += 1
for i in range(1, 10): count[i] += count[i-1]
for n_ in reversed(arr):
d = (n_ // exp) % 10
count[d] -= 1
output[count[d]] = n_
return output
print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))
# [2, 24, 45, 66, 75, 90, 170, 802]การเรียงลำดับแบบบักเก็ต: กระจายลงบักเก็ต
การเรียงลำดับแบบบักเก็ต จะแจกจ่ายสมาชิกลงในบักเก็ตจำนวนคงที่ตามช่วงของค่า เรียงลำดับสมาชิกในแต่ละบักเก็ต (โดยใช้การเรียงลำดับแบบแทรกสำหรับบักเก็ตขนาดเล็ก) แล้วนำมาต่อรวมกัน สำหรับข้อมูลที่กระจายตัวสม่ำเสมอในช่วง [0, 1) การใช้บักเก็ต n ใบจะมีเวลาเฉลี่ยเป็น O(n) เวลา: O(n + k) โดยเฉลี่ย และ O(n²) ในกรณีแย่ที่สุด (สมาชิกทั้งหมดอยู่ในบักเก็ตเดียวกัน) วิธีนี้มีประโยชน์มากที่สุดเมื่อทราบการกระจายของข้อมูลและการกระจายนั้นใกล้เคียงสม่ำเสมอ
def bucket_sort(arr):
if not arr: return []
n = len(arr)
min_v, max_v = min(arr), max(arr)
if min_v == max_v: return arr[:]
buckets = [[] for _ in range(n)]
# Map each value to a bucket index
for v in arr:
idx = int((v - min_v) / (max_v - min_v + 1e-9) * n)
idx = min(idx, n - 1)
buckets[idx].append(v)
result = []
for bucket in buckets:
bucket.sort() # insertion sort for small buckets
result.extend(bucket)
return result
print(bucket_sort([0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21]))
# sorted listTimsort ของ Python เบื้องหลังการทำงาน
sorted() และ list.sort() ของ Python ใช้ Timsort ซึ่งออกแบบโดย Tim Peters ในปี 2002 Timsort เป็นการผสมผสานระหว่างการเรียงลำดับแบบผสานและการเรียงลำดับแบบแทรก โดยจะค้นหา “ช่วงข้อมูลที่เรียงลำดับอยู่แล้วตามธรรมชาติ” และใช้การเรียงลำดับแบบแทรกเพื่อสร้างช่วงข้อมูลที่มีสมาชิกได้สูงสุด 64 ตัว จากนั้นจะผสานช่วงข้อมูลเหล่านี้ด้วยการเรียงลำดับแบบผสาน พร้อมการปรับปรุงหลายอย่าง ได้แก่ การข้ามแบบก้าวกระโดด (ข้ามสมาชิกจำนวนมากเมื่อช่วงข้อมูลหนึ่งมีอิทธิพลมากกว่า) และการจัดวางซ้อนตามความยาวของช่วงข้อมูล
# Timsort properties:
# - Stable
# - O(n log n) worst case
# - O(n) best case (data already sorted)
# - O(n) auxiliary space
# - Highly optimised for real-world data with runs
import time
# Nearly sorted data: Timsort is extremely fast
nearly_sorted = list(range(10000))
nearly_sorted[-1] = 0 # one mis-placed element
t = time.perf_counter()
not_used = sorted(nearly_sorted)
elapsed = time.perf_counter() - t
print(f'Timsort on nearly-sorted n=10000: {elapsed*1000:.3f} ms')sort() ของ Python เทียบกับ sorted(): ความแตกต่างสำคัญ
list.sort() เรียงลำดับภายในออบเจ็กต์เดิม คืนค่า None และใช้ได้กับลิสต์เท่านั้น ส่วน sorted(iterable) ใช้ได้กับสิ่งที่วนซ้ำได้ทุกชนิด (เช่น ทูเพิล ตัวสร้าง และพจนานุกรม) และคืนค่าลิสต์ใหม่ ทั้งสองแบบรับพารามิเตอร์ key และ reverse ข้อผิดพลาดที่พบบ่อยคือการกำหนดค่าที่คืนจาก lst.sort() ให้กับตัวแปร แล้วสงสัยว่าเหตุใดค่าจึงเป็น None ควรใช้ sorted() เสมอเมื่อต้องการเวอร์ชันที่เรียงลำดับแล้วและต้องการเก็บข้อมูลเดิมไว้
nums = [3, 1, 4, 1, 5, 9]
# in-place: returns None
result = nums.sort()
print(result) # None (common bug!)
print(nums) # [1, 1, 3, 4, 5, 9] (modified)
nums2 = [3, 1, 4, 1, 5, 9]
# out-of-place: returns new list
result2 = sorted(nums2)
print(result2) # [1, 1, 3, 4, 5, 9]
print(nums2) # [3, 1, 4, 1, 5, 9] (unchanged)คีย์การเรียงลำดับแบบกำหนดเองในการสัมภาษณ์
การเรียงลำดับของ Python รับฟังก์ชัน key ซึ่งจะประเมินหนึ่งครั้งต่อสมาชิก (ต่างจากตัวเปรียบเทียบของ C ที่ถูกเรียกสำหรับสมาชิกทุกคู่) คีย์การเรียงลำดับที่พบบ่อยในการสัมภาษณ์ ได้แก่ len สำหรับความยาวสตริง lambda x: -x สำหรับเรียงจากมากไปน้อย lambda x: (x[1], x[0]) สำหรับการเรียงลำดับหลายคีย์ และ str.lower สำหรับการไม่คำนึงถึงตัวพิมพ์เล็กใหญ่ การเรียงลำดับของ Python รับประกันว่าเสถียร ดังนั้นการเรียงลำดับหลายคีย์จึงทำงานได้อย่างถูกต้อง
# Sort by length, then alphabetically
words = ['banana', 'fig', 'apple', 'date', 'kiwi']
print(sorted(words, key=lambda w: (len(w), w)))
# ['fig', 'date', 'kiwi', 'apple', 'banana']
# Sort integers as strings (largest concatenation first)
nums = [3, 30, 34, 5, 9]
print(sorted(map(str, nums), key=lambda a: a*10, reverse=True))
# ['9', '5', '34', '3', '30'] => '9534330'
# Descending sort
print(sorted([3,1,4,1,5], reverse=True)) # [5,4,3,1,1]ควรใช้การเรียงลำดับแต่ละแบบเมื่อใดในการสัมภาษณ์
เลือกการเรียงลำดับให้เหมาะกับบริบท:
- ใช้ sorted()/list.sort() ของ Python: เป็นค่าเริ่มต้นสำหรับโจทย์สัมภาษณ์ทั้งหมด เพราะ Timsort มีประสิทธิภาพเหมาะสมที่สุด
- การเรียงลำดับแบบนับ: เมื่อค่าเป็นจำนวนเต็มขนาดเล็กที่มีขอบเขตจำกัด (ตั้งแต่ 0 ถึง k โดย k มีขนาดเล็ก)
- การเรียงลำดับแบบเรดิกซ์: เมื่อต้องเรียงจำนวนเต็มจำนวนมากที่ทราบจำนวนบิตหรือจำนวนหลัก
- การเรียงลำดับแบบบักเก็ต: เมื่อข้อมูลเป็นจำนวนจริงที่กระจายตัวสม่ำเสมอในช่วงที่ทราบ
- เขียนการเรียงลำดับแบบผสาน: เมื่อโจทย์ขอให้เขียนการเรียงลำดับ O(n log n) ที่เสถียรขึ้นมาเอง
# Problem: sort array of 0s, 1s, 2s efficiently
# Counting sort: O(n), O(1) space (k=3 is tiny)
def sort_012(arr):
count = [0, 0, 0]
for n in arr:
count[n] += 1
i = 0
for val in range(3):
for _ in range(count[val]):
arr[i] = val; i += 1
arr = [2, 0, 2, 1, 1, 0]
sort_012(arr)
print(arr) # [0, 0, 1, 1, 2, 2]ไม่ต้องเรียงทั้งชุด: ค้นหา k อันดับแรกด้วยฮีป
โจทย์สัมภาษณ์จำนวนมากต้องการผลลัพธ์ที่ “คล้ายการเรียงลำดับ” โดยไม่จำเป็นต้องเรียงข้อมูลทั้งหมด การค้นหาสมาชิก k อันดับแรกด้วยฮีปต่ำสุดขนาด k ใช้เวลา O(n log k) ซึ่งเร็วกว่าการใช้เวลา O(n log n) เมื่อ k เล็กมากเมื่อเทียบกับ n การค้นหาสมาชิกอันดับที่ k จากมากไปน้อยใช้ quickselect ซึ่งมีเวลาเฉลี่ย O(n) ส่วนการค้นหาค่ามัธยฐานใช้วิธีฮีปสองชุด โดยใช้เวลา O(log n) ต่อการแทรกหนึ่งครั้ง ควรรู้จักวิธีการเรียงลำดับบางส่วนเหล่านี้ไว้ เพราะเป็นทางเลือกที่เร็วกว่าการเรียงลำดับทั้งหมด
import heapq
# Top-k with heap: O(n log k)
def top_k(nums, k):
return heapq.nlargest(k, nums) # uses heap of size k internally
print(top_k([3,2,1,5,6,4], 2)) # [6, 5]
# kth largest: quickselect O(n) average
import random
def kth_largest(nums, k):
def _select(lo, hi, target):
if lo >= hi: return nums[lo]
rand_i = random.randint(lo, hi)
nums[rand_i], nums[hi] = nums[hi], nums[rand_i]
pivot = nums[hi]; i = lo - 1
for j in range(lo, hi):
if nums[j] >= pivot: i+=1; nums[i],nums[j]=nums[j],nums[i]
nums[i+1],nums[hi]=nums[hi],nums[i+1]
p = i + 1
if p == target: return nums[p]
return _select(lo, p-1, target) if target < p else _select(p+1, hi, target)
return _select(0, len(nums)-1, k-1)
print(kth_largest([3,2,1,5,6,4], 2)) # 5ความเสถียรของการเรียงลำดับแบบหลายคีย์
ความเสถียรช่วยให้การเรียงลำดับหลายคีย์ทำงานได้ถูกต้อง โดยเรียงตามคีย์รองก่อน (แบบเสถียร) แล้วจึงเรียงตามคีย์หลัก (แบบเสถียร) ลำดับของคีย์รองจะยังคงอยู่สำหรับสมาชิกที่มีคีย์หลักเท่ากัน เทคนิคนี้ใช้ในฐานข้อมูล (ORDER BY col1, col2) และในการเรียงลำดับแบบเรดิกซ์ (การเรียงในแต่ละหลักต้องเสถียร เพื่อให้อัลกอริทึมโดยรวมถูกต้อง) การเรียงลำดับของ Python มีความเสถียรเสมอ ดังนั้นรูปแบบนี้จึงทำงานได้อย่างน่าเชื่อถือ
data = [
('Alice', 'Math', 90),
('Bob', 'Science', 85),
('Carol', 'Math', 90),
('Dave', 'Science', 90),
]
# Sort by score DESC, then by subject ASC (for ties)
# Step 1: sort by subject (secondary)
data.sort(key=lambda x: x[1])
# Step 2: sort by score DESC (primary, stable)
data.sort(key=lambda x: x[2], reverse=True)
for row in data:
print(row)
# All score=90 rows: Math before Science (preserved from step 1)ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ได้เรียนรู้ว่า การเรียงลำดับที่ใช้การเปรียบเทียบมีขอบเขตล่างเป็น O(n log n) — การทำให้ดีกว่าขอบเขตนี้ต้องอาศัยข้อมูลที่ไม่ใช่การเปรียบเทียบ เช่น จำนวนเต็มที่มีขอบเขตจำกัด การเรียงลำดับแบบนับทำเวลา O(n + k) ได้ด้วยการนับความถี่ การเรียงลำดับแบบเรดิกซ์ประมวลผลทีละหลักด้วยเวลารวม O(d × (n + k)) และการเรียงลำดับแบบบักเก็ตใช้ประโยชน์จากการกระจายตัวสม่ำเสมอเพื่อให้มีเวลาเฉลี่ย O(n) และ Timsort ของ Python เป็นค่าเริ่มต้นที่เหมาะกับการใช้งานจริง — มีความเสถียร มีกรณีแย่ที่สุดเป็น O(n log n) กรณีดีที่สุดเป็น O(n) และเร็วกว่าทางเลือกที่เขียนขึ้นเองสำหรับข้อมูลจริง บทถัดไปจะเรียนรู้การค้นหาแบบทวิภาคแบบคลาสสิก
คำถามที่พบบ่อย
บทเรียน “การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python”
สำรวจการเรียงแบบนับและการเรียงแบบฐานสำหรับอาร์เรย์จำนวนเต็ม พร้อมทำความเข้าใจการทำงานภายในของ Timsort ของ Python เมื่อเรียกใช้การเรียงในตัว คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การเรียงแบบฟองและการเรียงแบบแทรก
- การเรียงแบบผสาน: แบ่ง เรียง ผสาน
- การเรียงแบบเร็วและการเลือกหมุด
- การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python