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à . 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 không
Giải pháp
Counting sort
Nếu một mảng bao gồm số nguyên với phạm vi nhỏ (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 là . Ý tưởng của Counting Sort là:
-
Chuẩn bị một ‘mảng tần số’ với kích thước và khởi tạo bằng các số . Trên mảng ví dụ đã cho ở trên, chúng ta có và .
-
Chúng ta thực hiện một lần duyệt qua mảng 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 , ta thực hiện . Trên mảng ví dụ trên, chúng ta có .
-
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 , tức là . Bây giờ, chứa số phần tử nhỏ hơn hoặc bằng . Trên mảng ví dụ trên, chúng ta có .
-
Tiếp theo, quay ngược lại từ xuống . Chúng ta đặt tại chỉ số vì nó là vị trí chính xác của . Chúng ta giảm đơn vị để giá trị bản sao tiếp theo của (nếu có) sẽ được đặt ngay trước hiện tại. Trên mảng ví dụ ở trên, đầu tiên chúng ta đặt vào chỉ số và giảm xuống đơn vị. Tiếp theo, đặt — cùng giá trị với — bây giờ ở chỉ số và giảm xuống đơn vị. Sau đó, đặt vào chỉ số và giảm xuống đơ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: .
Độ phức tạp thời gian của Counting sort là . Khi , 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 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 với gồm các phần tử vì nó có lớn.
Radix sort
Nếu mảng chứa số nguyên không âm với phạm vi tương đối rộng 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ó chữ số — trong đó là số chữ số lớn nhất trong số nguyên lớn nhất trong — bằng cách thêm các số 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 được mô tả ở trên. Ví dụ:
- Input: :
- Thêm chữ số :
- Sắp xếp theo chữ số thứ 4:
- Sắp xếp theo chữ số thứ 3:
- Sắp xếp theo chữ số thứ 2:
- Sắp xếp theo chữ số thứ 1:
Đối với một mảng gồm số nguyên có chữ số, chúng ta sẽ thực hiện lần Counting sort có độ phức tạp thời gian là mỗi lần. Do đó, độ phức tạp về thời gian của Radix Sort là . Nếu chúng ta sử dụng Radix Sort để sắp xếp số nguyên có dấu 32 bit ( chữ số) và . Thuật toán Radix Sort này chạy trong . 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 , tổng số người. Trong dòng tiếp theo, có số nguyên cho biết tuổi. Đầu vào được kết thúc với trường hợp .
Output: Đối với mỗi testcase, in ra một dòng với 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 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
- Giải thuật và lập trình - Thầy Lê Minh Hoàng
- cp-algorithms.com
- Handbook Competitive Programming - Antti Laaksonen
- Competitve programming 3 - Steven Halim, Felix Halim
- onlinejudge.org
- Algorithms in a nutshell - Geogre T. Heineman, Gary Pollice & Stanley Selkow
- 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