+3

Sắp xếp với thời gian tuyến tính

Mở đầu

Ta đã làm quen với rất nhiều thuật toán sắp xếp. Độ phức tạp trung bình nhanh nhất trong các thuật toán sắp xếp là O(nlogn)O(n \log n). Bạn có bao giờ tự hỏi rằng, ta có thể sắp xếp một mảng với độ phức tạp thời gian tuyến tính O(n)O(n) không 😄

Giải pháp

Counting sort

Nếu một mảng AA bao gồm nn số nguyên với phạm vi nhỏ [L..R][L..R] (Ví dụ như tuổi thọ của một người chẳng hạn 😄). Ta có thể sử dụng thuật toán Counting Sort. Giả sử ta có mảng AA{2,5,2,2,3,3}\{2,5,2,2,3,3\}. Ý tưởng của Counting Sort là:

  1. Chuẩn bị một ‘mảng tần số’ ff với kích thước k=RL+1k = R-L + 1 và khởi tạo ff bằng các số 00. Trên mảng ví dụ đã cho ở trên, chúng ta có L=2,R=5L = 2, R = 5k=4k = 4.

  2. Chúng ta thực hiện một lần duyệt qua mảng AA và cập nhật tần số của mỗi số nguyên mà chúng ta thấy, tức là với mỗi i[0..n1]i ∈ [0.. n-1], ta thực hiện f[A[i]L]++f [A [i] -L]++. Trên mảng ví dụ trên, chúng ta có f[0]=3,f[1]=2,f[2]=0,f[3]=1f [0] = 3, f [1] = 2, f [2] = 0, f [3] = 1.

  3. Khi chúng ta biết tần số của mỗi số nguyên trong phạm vi nhỏ đó, ta tính tổng tiền tố của mỗi ii, tức là f[i]=[f1]+f[i]f [i] = [f-1] + f [i] i[1..k1]∀i ∈ [1..k-1]. Bây giờ, f[i]f [i] chứa số phần tử nhỏ hơn hoặc bằng ii. Trên mảng ví dụ trên, chúng ta có f[0]=3,f[1]=5,f[2]=5,f[3]=6f [0] = 3, f [1] = 5, f [2] = 5, f [3] = 6.

  4. Tiếp theo, quay ngược lại từ i=n1i = n-1 xuống i=0i = 0. Chúng ta đặt A[i]A [i] tại chỉ số f[A[i]L]1f [A [i] -L] -1 vì nó là vị trí chính xác của A[i]A [i]. Chúng ta giảm f[A[i]L]f [A [i] -L] 11 đơn vị để giá trị bản sao tiếp theo của A[i]A [i] (nếu có) sẽ được đặt ngay trước A[i]A [i] hiện tại. Trên mảng ví dụ ở trên, đầu tiên chúng ta đặt A[5]=3A [5] = 3 vào chỉ số f[A[5]2]1=f[1]1=51=4f [A [5] -2] -1 = f [1] -1 = 5-1 = 4 và giảm f[1]f [1] xuống 44 đơn vị. Tiếp theo, đặt A[4]=3A [4] = 3 — cùng giá trị với A[5]=3A [5] = 3 — bây giờ ở chỉ số f[A[4]2]1=f[1]1=41=3f [A [4] -2] -1 = f [1] -1 = 4-1 = 3 và giảm f[1]f [1] xuống 33 đơn vị. Sau đó, đặt A[3]=2A [3] = 2 vào chỉ số f[A[3]2]1=2f [A [3] -2] -1 = 2 và giảm f[0]f [0] xuống 22 đơn vị. Chúng ta lặp lại ba bước tiếp theo cho đến khi có một mảng đã sắp xếp: {2,2,2,3,3,5}\{2, 2, 2, 3, 3, 5\}.

Độ phức tạp thời gian của Counting sort là O(n+k)O (n + k). Khi k=O(n)k = O (n), về mặt lý thuyết, thuật toán này chạy trong thời gian tuyến tính bằng cách không thực hiện so sánh các số nguyên. Tuy nhiên, trong môi trường cuộc thi lập trình thi đấu, thường kk không được quá lớn để tránh Vượt quá giới hạn bộ nhớ. Ví dụ: Counting sort sẽ gặp vấn đề khi sắp xếp mảng AA với n=3n = 3 gồm các phần tử {1,1000000000,2}\{1, 1000000000, 2\} vì nó có kk lớn.

Radix sort

Nếu mảng AA chứa nn số nguyên không âm với phạm vi tương đối rộng [L..R][L..R] nhưng nó bao gồm số chữ số tương đối nhỏ, chúng ta có thể sử dụng thuật toán Radix sort.

Ý tưởng về Radix sort rất đơn giản. Đầu tiên, chúng ta tạo tất cả các số nguyên có dd chữ số — trong đó dd là số chữ số lớn nhất trong số nguyên lớn nhất trong AA — bằng cách thêm các số 00 nếu cần. Sau đó, Radix Sort sẽ sắp xếp các số này theo từng chữ số, bắt đầu từ chữ số nhỏ nhất đến chữ số lớn nhất. Nó sử dụng một thuật toán stable sort khác như một quy trình con để sắp xếp các chữ số, chẳng hạn như Counting sort O(n+k)O (n + k) được mô tả ở trên. Ví dụ:

  1. Input: d=4d = 4: {323,1257,13,322}\{323, 1257, 13, 322\}
  2. Thêm chữ số 00: {0323,1257,0013,0322}\{0323, 1257, 0013, 0322\}
  3. Sắp xếp theo chữ số thứ 4: {032(2),032(3),001(3),125(7)}\{032(2), 032(3), 001(3), 125(7) \}
  4. Sắp xếp theo chữ số thứ 3: {00(1)3,03(2)2,03(2)3,12(5)7}\{00(1)3, 03(2)2, 03(2)3, 12(5)7\}
  5. Sắp xếp theo chữ số thứ 2: {0(0)13,1(2)57,0(3)22,0(3)23}\{0(0)13,1(2)57,0(3)22,0(3)23 \}
  6. Sắp xếp theo chữ số thứ 1: {(0)013,(0)322,(0)323,(1)257}\{(0)013,(0)322,(0)323, (1)257\}

Đối với một mảng gồm nn số nguyên có dd chữ số, chúng ta sẽ thực hiện O(d)O (d) lần Counting sort có độ phức tạp thời gian là O(n+k)O (n + k) mỗi lần. Do đó, độ phức tạp về thời gian của Radix Sort là O(d×(n+k))O (d × (n + k)). Nếu chúng ta sử dụng Radix Sort để sắp xếp nn số nguyên có dấu 32 bit (d=10≈ d = 10 chữ số) và k=10k = 10. Thuật toán Radix Sort này chạy trong O(10×(n+10))O (10 × (n + 10)). Nó vẫn có thể được coi là chạy trong thời gian tuyến tính nhưng có hệ số không đổi cao.

Hãy thử áp dụng vào bài toán sau:

Bạn được cung cấp tuổi (tính theo năm) của tất cả người dân của một quốc gia có ít nhất 1 tuổi. Biết rằng không có cá nhân nào ở quốc gia đó sống từ 100 tuổi trở lên. Bây giờ, bạn được giao một nhiệm vụ rất đơn giản là sắp xếp tất cả các độ tuổi theo thứ tự tăng dần.

Input: Bao gồm nhiều testcases. Mỗi testcase bắt đầu bằng số nguyên n(0<n2000000)n (0 <n ≤ 2000000), tổng số người. Trong dòng tiếp theo, có nn số nguyên cho biết tuổi. Đầu vào được kết thúc với trường hợp n=0n = 0.

Output: Đối với mỗi testcase, in ra một dòng với nn số nguyên cách nhau khoảng trắng. Các số nguyên này là tuổi của quốc gia đó được sắp xếp theo thứ tự tăng dần.

Chú ý: Input của bài toán khá lớn (∼ 25 MB), do đó bạn nên sử dụng faster IO.

Sample Input:

5
3 4 2 1 5
5
2 3 2 3 1
0

Sample Output:

1 2 3 4 5
1 2 2 3 3

Solution:

#include <iostream>

using namespace std;

const int Max = 100;
const int Min = 1;

// No need to use std::sort since can just store counts in array
int count[105];

int main()
{
    int N;
    while (cin >> N, N)
    {
        int age;

        for (int i = 0; i < N; ++i)
        {
            cin >> age;
            ++count[age];
        }

        string sep = "";
        for (int i = Min; i <= Max; ++i)
        {
            // This will counteract how --count[i] works
            // While leaving the count[i] == 0 after this iteration
            ++count[i];

            while (--count[i])
            {
                cout << sep << i;
                sep = " ";
            }
        }

        cout << '\n';
    }
}

Tổng kết

Xét sự phức tạp của việc viết mã cho Radix Sort so với việc gọi hàm sort chuẩn O(nlogn)O (n log n) C ++ STL (hoặc Java Collections.sort), thuật toán Radix Sort này hiếm khi được sử dụng trong các cuộc thi lập trình thi đấu. Do đó, việc gọi hàm vẫn là một lựa chọn tối ưu, nhanh và khỏe 😄

Tài liệu tham khảo

  1. Giải thuật và lập trình - Thầy Lê Minh Hoàng
  2. cp-algorithms.com
  3. Handbook Competitive Programming - Antti Laaksonen
  4. Competitve programming 3 - Steven Halim, Felix Halim
  5. onlinejudge.org
  6. Algorithms in a nutshell - Geogre T. Heineman, Gary Pollice & Stanley Selkow
  7. Tài liệu giáo khoa chuyên tin 2 - Hồ Sĩ Đàm, Đỗ Đức Đông, Lê Minh Hoàng & Nguyễn Thanh Hùng.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí