Sắp xếp đếm
Cập nhật lần cuối
Sắp xếp đếm (counting sort) là thuật toán sắp xếp không dựa trên so sánh, dành cho các số nguyên nằm trong một khoảng đã biết và có giới hạn. Thuật toán đếm số lần xuất hiện của từng giá trị, rồi dùng các số đếm đó để ghi thẳng từng giá trị vào đúng vị trí đã sắp xếp, không cần so sánh nào. Nhấn Phát ở trên để xem các giá trị được kiểm đếm rồi đặt lại theo thứ tự.
Sắp xếp đếm chạy trong thời gian O(n + k), với k là khoảng giá trị của dữ liệu đầu vào. Khi k không lớn hơn n quá nhiều, thuật toán cực kỳ nhanh và có thể vượt các thuật toán sắp xếp bằng so sánh O(n log n), nhưng nếu khoảng giá trị quá lớn, mảng đếm O(k) khiến nó không còn thực tế.
Độ phức tạp thời gian và không gian
| Tiêu chí | Độ phức tạp | Ghi chú |
|---|---|---|
| Thời gian | O(n + k) | n phần tử, k = khoảng giá trị |
| Không gian | O(n + k) | Mảng đếm + mảng kết quả |
| Ổn định | Có | Khi đặt từ phải sang trái bằng tổng tiền tố |
| Dựa trên so sánh? | Không | Sắp xếp bằng cách đếm, không so sánh |
| Phù hợp nhất cho | Khoảng số nguyên nhỏ | k xấp xỉ n |
Từng bước thực hiện
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Tìm giá trị lớn nhất để xác định kích thước mảng đếm. |
| 2 | Đếm số lần xuất hiện của từng giá trị. |
| 3 | (Không bắt buộc) Chuyển số đếm thành tổng tiền tố để thuật toán ổn định. |
| 4 | Ghi mỗi giá trị vào mảng kết quả đúng bằng số lần nó xuất hiện. |
| 5 | Mảng kết quả giờ đã được sắp xếp hoàn toàn. |
Ví dụ minh họa
Sắp xếp [1, 4, 1, 2, 4] (giá trị nằm trong khoảng từ 0 đến 4, nên mảng đếm có 5 ô):
| Lượt | Trạng thái | Thao tác |
|---|---|---|
| Quét đầu vào | count = [0, 2, 1, 0, 2] | Kiểm đếm số lần xuất hiện: 1 xuất hiện hai lần, 2 một lần, 4 hai lần. |
| Tổng tiền tố | count = [0, 2, 3, 3, 5] | Mỗi ô giờ chứa số lượng giá trị <= chỉ số của ô đó, cho ra vị trí cuối cùng. |
Đặt 4 | output = [_, _, _, _, 4] | Đọc từ phải sang trái: count[4] = 5, nên 4 vào chỉ số 4; giảm xuống 4. |
Đặt 2 | output = [_, _, 2, _, 4] | count[2] = 3, nên 2 vào chỉ số 2; giảm xuống 2. |
Đặt 1 | output = [_, 1, 2, _, 4] | count[1] = 2, nên 1 vào chỉ số 1; giảm xuống 1. |
Đặt 4 | output = [_, 1, 2, 4, 4] | count[4] = 4, nên 4 này vào chỉ số 3; giảm xuống 3. |
Đặt 1 | output = [1, 1, 2, 4, 4] | count[1] = 1, nên 1 này vào chỉ số 0. Mảng đã được sắp xếp. |
Khi nào nên dùng sắp xếp đếm
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn sắp xếp số nguyên (hoặc khóa ánh xạ được sang số nguyên) trong một khoảng nhỏ, đã biết. | Khoảng giá trị k lớn hơn rất nhiều so với số phần tử n. |
Bạn cần thời gian tuyến tính O(n + k) và có thể chấp nhận các mảng phụ. | Bộ nhớ eo hẹp: mảng đếm tốn O(k) bất kể n là bao nhiêu. |
| Bạn cần một thuật toán sắp xếp ổn định làm bước con (ví dụ bên trong radix sort). | Khóa là số thực, chuỗi hoặc đối tượng bất kỳ không ánh xạ được sang số nguyên. |
| Giá trị lớn nhất có giới hạn và tính trước được với chi phí thấp. | Khoảng giá trị không biết trước hoặc không giới hạn, nên không xác định được kích thước mảng đếm. |
Code Counting Sort
Một cách cài đặt Counting Sort gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Counting Sort bằng Python
1def counting_sort(a):2 # Works for non-negative integers with a small max value3 counts = [0] * (max(a) + 1)4 for value in a:5 counts[value] += 16 # Prefix sums turn counts into final positions7 for i in range(1, len(counts)):8 counts[i] += counts[i - 1]9 out = [0] * len(a)10 for value in reversed(a): # reversed keeps equal values stable11 counts[value] -= 112 out[counts[value]] = value13 return out14
15
16nums = [4, 2, 2, 8, 3, 3, 1]17print("Before:", nums)18print("After: ", counting_sort(nums))Code Counting Sort bằng JavaScript
1function countingSort(arr) {2 // Count occurrences of each value, then rebuild in order3 const max = Math.max(...arr);4 const count = new Array(max + 1).fill(0);5 for (const x of arr) count[x]++;6 const out = [];7 count.forEach((c, value) => {8 for (let k = 0; k < c; k++) out.push(value);9 });10 return out;11}12
13const data = [4, 2, 9, 2, 7, 4, 1, 4];14console.log("Before:", data);15console.log("Sorted:", countingSort(data));Code Counting Sort bằng Java
1import java.util.Arrays;2
3public class Main {4 static int[] countingSort(int[] arr) {5 int max = 0;6 for (int v : arr) max = Math.max(max, v);7 int[] count = new int[max + 1];8 for (int v : arr) count[v]++;9 // Prefix sums turn counts into final positions10 for (int i = 1; i <= max; i++) count[i] += count[i - 1];11 int[] out = new int[arr.length];12 // Walk backwards so equal values keep their order (stable)13 for (int i = arr.length - 1; i >= 0; i--) {14 out[--count[arr[i]]] = arr[i];15 }16 return out;17 }18
19 public static void main(String[] args) {20 int[] arr = {4, 2, 2, 8, 3, 3, 1};21 System.out.println("Before: " + Arrays.toString(arr));22 int[] sorted = countingSort(arr);23 System.out.println("After: " + Arrays.toString(sorted));24 }25}Code Counting Sort bằng C++
1#include <algorithm>2#include <iostream>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
10void countingSort(std::vector<int>& a) {11 if (a.empty()) return;12 int maxVal = *std::max_element(a.begin(), a.end());13 // count[v] = how many times v appears14 std::vector<int> count(maxVal + 1, 0);15 for (int x : a) ++count[x];16 // Rebuild the array from the counts17 size_t idx = 0;18 for (int v = 0; v <= maxVal; ++v) {19 while (count[v]-- > 0) a[idx++] = v;20 }21}22
23int main() {24 std::vector<int> data = {4, 2, 2, 8, 3, 3, 1, 7};25 std::cout << "Before: ";26 printVec(data);27 countingSort(data);28 std::cout << "After: ";29 printVec(data);30 return 0;31}Code Counting Sort bằng C
1#include <stdio.h>2#include <stdlib.h>3
4void printArr(const int a[], int n) {5 for (int i = 0; i < n; i++) printf("%d ", a[i]);6 printf("\n");7}8
9void countingSort(int a[], int n) {10 int maxVal = a[0];11 for (int i = 1; i < n; i++) {12 if (a[i] > maxVal) maxVal = a[i];13 }14 // count[v] = how many times v appears15 int* count = calloc(maxVal + 1, sizeof(int));16 for (int i = 0; i < n; i++) count[a[i]]++;17 // Rebuild the array from the counts18 int idx = 0;19 for (int v = 0; v <= maxVal; v++) {20 while (count[v]-- > 0) a[idx++] = v;21 }22 free(count);23}24
25int main(void) {26 int data[] = {4, 2, 2, 8, 3, 3, 1, 7};27 int n = sizeof(data) / sizeof(data[0]);28 printf("Before: ");29 printArr(data, n);30 countingSort(data, n);31 printf("After: ");32 printArr(data, n);33 return 0;34}Câu hỏi thường gặp về sắp xếp đếm
Độ phức tạp thời gian của sắp xếp đếm là bao nhiêu?
O(n + k), với n là số phần tử và k là khoảng các giá trị có thể có. Khi k = O(n), đây là thời gian tuyến tính. Thuật toán dùng thêm O(n + k) bộ nhớ.Sắp xếp đếm có ổn định không?
Có thể. Phiên bản ổn định tính tổng tiền tố của các số đếm và đặt phần tử từ phải sang trái, nhờ đó giữ nguyên thứ tự tương đối của các khóa bằng nhau. Phiên bản đơn giản "ghi lại theo giá trị" ở đây cho kết quả sắp xếp đúng nhưng chủ yếu dùng cho các số nguyên thuần túy.
Khi nào nên dùng sắp xếp đếm?
k lớn hơn nhiều so với số phần tử, mảng đếm sẽ lãng phí bộ nhớ và một thuật toán sắp xếp bằng so sánh sẽ tốt hơn.Sắp xếp đếm và radix sort khác nhau như thế nào?
10 với chữ số thập phân). Radix sort xử lý được các khoảng giá trị lớn mà một lần sắp xếp đếm đơn lẻ không làm nổi.Vì sao sắp xếp đếm không phải lúc nào cũng nhanh hơn quicksort?
O(n + k), nên chỉ thắng khi khoảng giá trị k tương đương với n. Nếu k cực lớn, chẳng hạn sắp xếp 100 giá trị trong khoảng từ 0 đến 1,000,000,000, mảng đếm O(k) sẽ chiếm phần lớn chi phí và lãng phí bộ nhớ, trong khi một thuật toán sắp xếp bằng so sánh O(n log n) như quicksort vẫn nhanh và tiết kiệm bộ nhớ.Sắp xếp đếm có xử lý được số âm không?
value - min, để giá trị nhỏ nhất ứng với chỉ số 0. Kích thước mảng đếm trở thành max - min + 1. Quên độ dời này là một bug phổ biến khiến chương trình bị lỗi khi gặp đầu vào âm.