cv.cm

Lập trình · Thuật toán

Thuật toán sắp xếp chèn: cách hoạt động, mã và độ phức tạp

6 phút đọc

Trả lời nhanh

Sắp xếp chèn xây kết quả đã sắp xếp từng phần tử một. Nó lấy phần tử kế tiếp, dịch mọi phần tử lớn hơn trong phần đã sắp xếp sang phải một vị trí, rồi thả phần tử vào chỗ trống, giống cách nhiều người xếp bài trên tay. Thuật toán là O(n²) ở trường hợp trung bình và xấu nhất, O(n) với đầu vào đã hoặc gần như đã sắp xếp, dùng O(1) bộ nhớ phụ và ổn định. Đây là thuật toán sắp xếp đơn giản nhanh nhất cho mảng nhỏ hoặc gần như đã sắp xếp, vì thế các thuật toán lai như Timsort dùng nó.

Mở Sắp dòng

Ý tưởng bằng lời đơn giản

Coi phần tử đầu tiên là danh sách đã sắp xếp có độ dài một. Lấy phần tử thứ hai và chèn vào trước hoặc sau phần tử đầu. Lấy phần tử thứ ba và chèn vào đúng chỗ giữa hai phần tử đầu. Tiếp tục cho đến khi mọi phần tử đã được chèn.

Để chèn, hãy để phần tử mới sang một bên (gọi là key), rồi đi sang trái qua phần đã sắp xếp. Mọi phần tử lớn hơn key dịch sang phải một vị trí. Khi gặp phần tử không lớn hơn, hoặc đến đầu danh sách, đặt key vào ô trống.

Ví dụ minh họa từng bước

Sắp xếp [12, 11, 13, 5, 6]. Phần trước dấu gạch đã được sắp xếp (nhưng chưa phải vị trí cuối cùng).

BướckeyPhần tử dịch chuyểnDanh sách sau bước
11112 dịch sang phải[11, 12 | 13, 5, 6]
213không có, 12 nhỏ hơn[11, 12, 13 | 5, 6]
3513, 12, 11 dịch sang phải[5, 11, 12, 13 | 6]
4613, 12, 11 dịch sang phải[5, 6, 11, 12, 13]

Lưu ý bước 2: phần tử đã đúng chỗ chỉ tốn một phép so sánh. Đó là lý do sắp xếp chèn nhanh với đầu vào gần như đã sắp xếp.

Cài đặt bằng Python

def insertion_sort(items):
    """Sort a list in place, ascending. Returns the same list."""
    for i in range(1, len(items)):
        key = items[i]
        j = i - 1
        while j >= 0 and items[j] > key:   # strict >, keeps it stable
            items[j + 1] = items[j]        # shift right
            j -= 1
        items[j + 1] = key                 # drop into the gap
    return items
print(insertion_sort([12, 11, 13, 5, 6]))   # [5, 6, 11, 12, 13]
print(insertion_sort([1, 2, 3, 4]))         # [1, 2, 3, 4], 3 comparisons

Dịch chuyển thay vì đổi chỗ chỉ ghi mỗi phần tử được di chuyển một lần, bằng khoảng một nửa công việc của phiên bản đổi chỗ. Bạn cũng có thể tìm điểm chèn bằng tìm kiếm nhị phân như dưới đây. Cách này giảm số phép so sánh xuống O(n log n) tổng cộng, nhưng việc dịch chuyển vẫn là O(n²), nên thời gian tổng thể vẫn là O(n²).

from bisect import insort_right
def binary_insertion_sort(items):
    result = []
    for x in items:
        insort_right(result, x)   # stable: equal items go after
    return result
print(binary_insertion_sort([3, 1, 2, 1]))   # [1, 1, 2, 3]

Độ phức tạp thời gian và không gian

Trường hợpThời gianLý do
Tốt nhất (đã sắp xếp)O(n)mỗi phần tử cần một phép so sánh và không phải dịch chuyển
Trung bình (thứ tự ngẫu nhiên)O(n²)trung bình khoảng n²/4 lần dịch chuyển
Xấu nhất (thứ tự ngược)O(n²)phần tử thứ i dịch qua toàn bộ i phần tử đứng trước
Không gian phụO(1)sắp xếp tại chỗ với một giá trị tạm

Chính xác hơn, thời gian chạy là O(n + I), trong đó I là số nghịch thế (các cặp sai thứ tự). Nếu mọi phần tử cách vị trí cuối cùng của nó tối đa k vị trí, I nhiều nhất là n · k, nên sắp xếp chèn là O(n · k): rất nhanh khi k nhỏ.

So sánh sắp xếp chèn với các thuật toán khác

Thuật toánTốt nhấtTrung bìnhXấu nhấtKhông gian phụỔn định
Sắp xếp chènO(n)O(n²)O(n²)O(1)Có
Sắp xếp nổi bọtO(n)O(n²)O(n²)O(1)Có
Sắp xếp chọnO(n²)O(n²)O(n²)O(1)Không
Sắp xếp trộnO(n log n)O(n log n)O(n log n)O(n)Có
Sắp xếp nhanhO(n log n)O(n log n)O(n²)O(log n)Không
Sắp xếp vun đốngO(n log n)O(n log n)O(n log n)O(1)Không
Timsort (sorted của Python)O(n)O(n log n)O(n log n)O(n)Có

Sắp xếp chèn ổn định vì một phần tử chỉ di chuyển sang trái qua các phần tử thực sự lớn hơn nó. Nó cũng là thuật toán trực tuyến (online): có thể sắp xếp dữ liệu khi nó đến, luôn giữ một tiền tố đã sắp xếp. Với danh sách ngắn, chi phí phụ thấp của nó thắng các thuật toán O(n log n), nên Timsort (dùng trong Python và trong Java cho đối tượng) sắp xếp các đoạn ngắn bằng sắp xếp chèn nhị phân, và nhiều bản cài đặt sắp xếp nhanh chuyển sang sắp xếp chèn khi kích thước nhỏ.

Biến thể và lỗi thường gặp

  • Sắp xếp chèn nhị phân: tìm vị trí bằng tìm kiếm nhị phân rồi mới dịch chuyển. Ít phép so sánh hơn, có lợi khi việc so sánh tốn kém.
  • Shell sort: chạy sắp xếp chèn trên các phần tử cách nhau một khoảng (gap), rồi thu nhỏ khoảng đó xuống 1. Nó di chuyển nhanh các phần tử ở xa và nhanh hơn nhiều so với sắp xếp chèn thường trên mảng lớn.
  • Sắp xếp chèn trên danh sách liên kết: không cần dịch chuyển, nhưng tìm vị trí vẫn là quét tuyến tính.

Khi nào nên dùng sắp xếp chèn

Dùng nó cho mảng nhỏ (vài chục phần tử), cho dữ liệu gần như đã sắp xếp, như list đã sắp xếp rồi thêm vài phần tử ở cuối, và khi bạn cần một thuật toán sắp xếp tại chỗ ổn định đơn giản. Với dữ liệu ngẫu nhiên lớn, hãy dùng thuật toán O(n log n); trong Python đó là sorted() hoặc list.sort(), vốn đã áp dụng sắp xếp chèn cho các đoạn nhỏ bên trong.

Câu hỏi phỏng vấn thường yêu cầu theo dõi các bước, trường hợp tốt nhất và xấu nhất, vì sao nó ổn định, hoặc vì sao nó thắng các thuật toán O(n log n) trên đầu vào nhỏ và gần như đã sắp xếp.

Câu hỏi thường gặp

Sắp xếp chèn có ổn định không?

Có. Phần tử chỉ di chuyển sang trái qua các phần tử thực sự lớn hơn, nên các phần tử bằng nhau giữ nguyên thứ tự ban đầu.

Trường hợp tốt nhất của sắp xếp chèn là gì?

O(n), khi đầu vào đã sắp xếp: mỗi phần tử mới được so sánh một lần với phần tử bên trái và giữ nguyên chỗ.

Vì sao Timsort dùng sắp xếp chèn?

Với các dãy ngắn, chi phí phụ rất thấp khiến nó nhanh hơn sắp xếp trộn hay sắp xếp nhanh, và nó ổn định. Timsort dùng sắp xếp chèn nhị phân để kéo dài các đoạn ngắn trước khi trộn.

Sắp xếp chèn có tốt hơn sắp xếp nổi bọt không?

Có, trong thực tế. Cả hai đều O(n²) và ổn định, nhưng sắp xếp chèn thực hiện ít phép so sánh và ghi hơn, và thích nghi tốt hơn với đầu vào gần như đã sắp xếp.

Tìm kiếm nhị phân có làm sắp xếp chèn thành O(n log n) không?

Không. Nó giảm số phép so sánh xuống O(n log n), nhưng việc dịch phần tử để tạo chỗ vẫn là O(n²) ở trường hợp xấu nhất.

Tất cả hướng dẫn lập trình