Menu
CoddyTech

Quick sort (sắp xếp nhanh)

Cập nhật lần cuối

Quicksort (sắp xếp nhanh) là một thuật toán chia để trị, sắp xếp xoay quanh một "phần tử chốt" (pivot). Thuật toán chọn một phần tử chốt, rồi phân hoạch mảng sao cho mọi phần tử nhỏ hơn nằm trước nó và mọi phần tử lớn hơn nằm sau nó, nhờ đó phần tử chốt được cố định ở đúng vị trí cuối cùng. Sau đó thuật toán gọi đệ quy cho phần bên trái và bên phải. Mô phỏng này dùng cách phân hoạch Lomuto với phần tử cuối làm chốt. Nhấn Phát để xem quá trình phân hoạch và đặt phần tử chốt.

Trong thực tế, quicksort thường là thuật toán sắp xếp đa dụng nhanh nhất nhờ tận dụng cache tốt và phân hoạch tại chỗ, trung bình đạt O(n log n). Trường hợp xấu nhất của nó là O(n²) (ví dụ mảng đã sắp xếp với cách chọn chốt kém), điều mà các chiến lược chọn chốt tốt như trung vị của ba hoặc chọn ngẫu nhiên giúp tránh được.

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

Trường hợpĐộ phức tạpGhi chú
Tốt nhấtO(n log n)Các phần phân hoạch cân bằng
Trung bìnhO(n log n)Thứ tự ngẫu nhiên
Xấu nhấtO(n²)Phần tử chốt liên tục gây mất cân bằng
Không gianO(log n)Ngăn xếp đệ quy (phân hoạch tại chỗ)
Ổn địnhKhôngCác lần đổi chỗ khi phân hoạch làm đảo thứ tự phần tử bằng nhau

Từng bước

BướcĐiều gì xảy ra
1Chọn phần tử chốt (ở đây là phần tử cuối của đoạn).
2Phân hoạch: chuyển mọi phần tử nhỏ hơn phần tử chốt sang bên trái nó.
3Đổi phần tử chốt vào ranh giới: giờ nó đã ở vị trí cuối cùng.
4Gọi đệ quy quicksort cho phần bên trái.
5Gọi đệ quy quicksort cho phần bên phải.

Ví dụ minh họa

Sắp xếp [5, 2, 4, 1] bằng cách phân hoạch Lomuto (phần tử cuối làm chốt):

LượtMảngThao tác
Bắt đầu[5, 2, 4, 1]Phân hoạch toàn bộ đoạn; phần tử chốt là 1 (phần tử cuối).
1[1, 2, 4, 5]Không có gì nhỏ hơn 1, nên đổi 1 vào chỉ số 0; phần tử chốt 1 đã cố định. Gọi đệ quy bên phải với [2, 4, 5].
2[1, 2, 4, 5]Phân hoạch [2, 4, 5] với chốt 5; cả 2 và 4 đều nhỏ hơn, nên 5 ở yên cuối mảng và đã cố định. Gọi đệ quy bên trái với [2, 4].
3[1, 2, 4, 5]Phân hoạch [2, 4] với chốt 4; 2 nhỏ hơn, nên 4 ở yên và đã cố định. 2 chỉ có một phần tử, nên đã được sắp xếp.
Xong[1, 2, 4, 5]Mọi phần tử chốt đã cố định; mảng đã được sắp xếp.

Khi nào nên dùng quicksort

Nên dùng khiTránh dùng khi
Bạn cần một thuật toán sắp xếp trong bộ nhớ nhanh, đa dụng, với hệ số hằng nhỏ.Bạn cần bảo đảm thời gian xấu nhất O(n log n) (hãy dùng heap sort hoặc merge sort).
Bộ nhớ hạn hẹp: phân hoạch diễn ra tại chỗ và chỉ cần O(log n) bộ nhớ ngăn xếp.Bạn cần sắp xếp ổn định, giữ nguyên thứ tự của các khóa bằng nhau.
Dữ liệu có thứ tự ngẫu nhiên hoặc không rõ, và bạn chọn chốt ngẫu nhiên hoặc theo trung vị của ba.Dữ liệu đầu vào đã hoặc gần như đã sắp xếp và phần tử chốt cố định, gây ra O(n²).
Tính cục bộ của cache quan trọng, vì quicksort truy cập bộ nhớ tuần tự.Bạn sắp xếp danh sách liên kết, khi đó merge sort tránh được việc truy cập ngẫu nhiên mà quicksort cần.

Code Quick Sort

Một cách cài đặt Quick Sort gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C, Pseudocode. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.

Code Quick Sort bằng Python

Python
1def quick_sort(a, low=0, high=None):2    if high is None:3        high = len(a) - 14    if low < high:5        p = partition(a, low, high)6        quick_sort(a, low, p - 1)7        quick_sort(a, p + 1, high)8    return a9
10
11def partition(a, low, high):12    # Lomuto partition: everything < pivot moves left of it13    pivot = a[high]14    i = low15    for j in range(low, high):16        if a[j] < pivot:17            a[i], a[j] = a[j], a[i]18            i += 119    a[i], a[high] = a[high], a[i]20    return i21
22
23nums = [10, 7, 8, 9, 1, 5]24print("Before:", nums)25quick_sort(nums)26print("After: ", nums)
Chạy code này trong Playground Python

Câu hỏi thường gặp về quick sort

Độ phức tạp thời gian của quicksort là bao nhiêu?
Quicksort trung bình tốn O(n log n) và cũng là O(n log n) trong trường hợp tốt nhất, nhưng suy giảm thành O(n²) trong trường hợp xấu nhất khi các phần phân hoạch liên tục mất cân bằng. Chọn chốt ngẫu nhiên hoặc theo trung vị của ba khiến trường hợp xấu nhất rất khó xảy ra.
Quicksort có ổn định không?

Không. Cách phân hoạch tại chỗ tiêu chuẩn đổi chỗ các phần tử ở xa nhau, có thể làm thay đổi thứ tự tương đối của các khóa bằng nhau. Có những biến thể ổn định nhưng chúng đánh mất ưu điểm sắp xếp tại chỗ của quicksort.

Tại sao quicksort thường nhanh hơn merge sort?
Quicksort phân hoạch tại chỗ, tận dụng cache rất tốt và không cần vùng đệm phụ, nên hệ số hằng của nó nhỏ. Merge sort có cùng cận O(n log n) nhưng phải trả giá cho vùng đệm O(n) và di chuyển dữ liệu nhiều hơn.
Quicksort hay merge sort, nên dùng thuật toán nào?
Chọn quicksort để sắp xếp mảng tại chỗ trong bộ nhớ một cách nhanh chóng, nơi hệ số hằng nhỏ của nó thường thắng thế. Chọn merge sort khi bạn cần sắp xếp ổn định, bảo đảm trường hợp xấu nhất O(n log n), hoặc khi sắp xếp danh sách liên kết hay dữ liệu bên ngoài không vừa RAM.
Tại sao quicksort trở thành O(n²) với mảng đã sắp xếp?
Với phần tử chốt cố định như phần tử đầu hoặc cuối, dữ liệu đã sắp xếp khiến mỗi lần phân hoạch chỉ tách ra được một phần tử, tạo ra n tầng đệ quy thay vì log n. Chọn chốt ngẫu nhiên hoặc theo trung vị của ba sẽ phá vỡ quy luật này và đưa thuật toán về O(n log n).
Cách phân hoạch Lomuto và Hoare khác nhau như thế nào?

Cách Lomuto dùng một chỉ số duy nhất quét từ trái sang phải và dễ viết code hơn, vì vậy mô phỏng này dùng nó. Cách Hoare dùng hai con trỏ di chuyển vào giữa và thường đổi chỗ ít hơn, nên nhanh hơn trong thực tế, nhưng nó không đặt phần tử chốt vào vị trí cuối cùng trong bước phân hoạch.

Hình minh họa các ngôn ngữ lập trình của Coddy

Thành thạo thuật toán cùng Coddy

Bắt đầu