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ạp | Ghi chú |
|---|---|---|
| Tốt nhất | O(n log n) | Các phần phân hoạch cân bằng |
| Trung bình | O(n log n) | Thứ tự ngẫu nhiên |
| Xấu nhất | O(n²) | Phần tử chốt liên tục gây mất cân bằng |
| Không gian | O(log n) | Ngăn xếp đệ quy (phân hoạch tại chỗ) |
| Ổn định | Không | Cá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 |
|---|---|
| 1 | Chọn phần tử chốt (ở đây là phần tử cuối của đoạn). |
| 2 | Phâ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. |
| 4 | Gọi đệ quy quicksort cho phần bên trái. |
| 5 | Gọ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ượt | Mảng | Thao 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 khi | Trá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
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)Code Quick Sort bằng JavaScript
1function quickSort(a, lo = 0, hi = a.length - 1) {2 if (lo >= hi) return a;3 const p = partition(a, lo, hi);4 quickSort(a, lo, p - 1);5 quickSort(a, p + 1, hi);6 return a;7}8
9// Lomuto partition: last element is the pivot10function partition(a, lo, hi) {11 const pivot = a[hi];12 let i = lo;13 for (let j = lo; j < hi; j++) {14 if (a[j] < pivot) {15 [a[i], a[j]] = [a[j], a[i]];16 i++;17 }18 }19 [a[i], a[hi]] = [a[hi], a[i]];20 return i;21}22
23const data = [5, 2, 9, 1, 7, 3];24console.log("Before:", data);25console.log("Sorted:", quickSort([...data]));Code Quick Sort bằng Java
1import java.util.Arrays;2
3public class Main {4 static void quickSort(int[] arr, int low, int high) {5 if (low >= high) return;6 int p = partition(arr, low, high);7 quickSort(arr, low, p - 1);8 quickSort(arr, p + 1, high);9 }10
11 // Lomuto partition: last element is the pivot12 static int partition(int[] arr, int low, int high) {13 int pivot = arr[high];14 int i = low - 1;15 for (int j = low; j < high; j++) {16 if (arr[j] < pivot) swap(arr, ++i, j);17 }18 swap(arr, i + 1, high);19 return i + 1;20 }21
22 static void swap(int[] arr, int a, int b) {23 int tmp = arr[a];24 arr[a] = arr[b];25 arr[b] = tmp;26 }27
28 public static void main(String[] args) {29 int[] arr = {10, 7, 8, 9, 1, 5};30 System.out.println("Before: " + Arrays.toString(arr));31 quickSort(arr, 0, arr.length - 1);32 System.out.println("After: " + Arrays.toString(arr));33 }34}Code Quick Sort bằng C++
1#include <iostream>2#include <utility>3#include <vector>4
5void printVec(const std::vector<int>& a) {6 for (int x : a) std::cout << x << " ";7 std::cout << "\n";8}9
10// Lomuto partition: place the pivot in its final position11int partition(std::vector<int>& a, int lo, int hi) {12 int pivot = a[hi];13 int i = lo;14 for (int j = lo; j < hi; ++j) {15 if (a[j] < pivot) std::swap(a[i++], a[j]);16 }17 std::swap(a[i], a[hi]);18 return i;19}20
21void quickSort(std::vector<int>& a, int lo, int hi) {22 if (lo >= hi) return;23 int p = partition(a, lo, hi);24 quickSort(a, lo, p - 1);25 quickSort(a, p + 1, hi);26}27
28int main() {29 std::vector<int> data = {10, 7, 8, 9, 1, 5};30 std::cout << "Before: ";31 printVec(data);32 quickSort(data, 0, static_cast<int>(data.size()) - 1);33 std::cout << "After: ";34 printVec(data);35 return 0;36}Code Quick Sort bằng C
1#include <stdio.h>2
3void printArr(const int a[], int n) {4 for (int i = 0; i < n; i++) printf("%d ", a[i]);5 printf("\n");6}7
8void swap(int* x, int* y) {9 int tmp = *x;10 *x = *y;11 *y = tmp;12}13
14// Lomuto partition: place the pivot in its final position15int partition(int a[], int lo, int hi) {16 int pivot = a[hi];17 int i = lo;18 for (int j = lo; j < hi; j++) {19 if (a[j] < pivot) swap(&a[i++], &a[j]);20 }21 swap(&a[i], &a[hi]);22 return i;23}24
25void quickSort(int a[], int lo, int hi) {26 if (lo >= hi) return;27 int p = partition(a, lo, hi);28 quickSort(a, lo, p - 1);29 quickSort(a, p + 1, hi);30}31
32int main(void) {33 int data[] = {10, 7, 8, 9, 1, 5};34 int n = sizeof(data) / sizeof(data[0]);35 printf("Before: ");36 printArr(data, n);37 quickSort(data, 0, n - 1);38 printf("After: ");39 printArr(data, n);40 return 0;41}Code Quick Sort bằng Pseudocode
1DECLARE nums : ARRAY[1:7] OF INTEGER2DECLARE n : INTEGER3n ← 74nums[1] ← 75nums[2] ← 36nums[3] ← 97nums[4] ← 18nums[5] ← 59nums[6] ← 810nums[7] ← 211DECLARE i : INTEGER12
13FUNCTION partition(lo : INTEGER, hi : INTEGER) RETURNS INTEGER14 DECLARE pivot : INTEGER15 DECLARE a : INTEGER16 DECLARE b : INTEGER17 DECLARE temp : INTEGER18 pivot ← nums[hi]19 a ← lo - 120 FOR b ← lo TO hi - 121 IF nums[b] <= pivot THEN22 a ← a + 123 temp ← nums[a]24 nums[a] ← nums[b]25 nums[b] ← temp26 ENDIF27 NEXT b28 temp ← nums[a + 1]29 nums[a + 1] ← nums[hi]30 nums[hi] ← temp31 RETURN a + 132ENDFUNCTION33
34PROCEDURE quickSort(lo : INTEGER, hi : INTEGER)35 DECLARE p : INTEGER36 IF lo < hi THEN37 // Place the pivot, then sort each side of it38 p ← partition(lo, hi)39 CALL quickSort(lo, p - 1)40 CALL quickSort(p + 1, hi)41 ENDIF42ENDPROCEDURE43
44CALL quickSort(1, n)45
46FOR i ← 1 TO n47 OUTPUT nums[i]48NEXT iCâu hỏi thường gặp về quick sort
Độ phức tạp thời gian của quicksort là bao nhiêu?
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?
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?
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?
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.