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

การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python

สำรวจการเรียงแบบนับและการเรียงแบบฐานสำหรับอาร์เรย์จำนวนเต็ม พร้อมทำความเข้าใจการทำงานภายในของ Timsort ของ Python เมื่อเรียกใช้การเรียงในตัว

การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA 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 list

Timsort ของ 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) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python”

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

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

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

บทเรียน “การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python” ใช้เวลานานแค่ไหน

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

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

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

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

  1. การเรียงแบบฟองและการเรียงแบบแทรก
  2. การเรียงแบบผสาน: แบ่ง เรียง ผสาน
  3. การเรียงแบบเร็วและการเลือกหมุด
  4. การเรียงที่ไม่เปรียบเทียบและ sort() ของ Python
← กลับไปที่ DSA Interview Prep