Cho số nguyên, xếp chúng theo thứ tự tăng dần. Cách tự nhiên nhất là cứ thấy hai số kề nhau đứng sai thứ tự thì đổi chỗ, tốn , chỉ đủ cho cỡ vài nghìn. Sắp xếp trộn làm được trong , đủ cho tới ; còn khi các giá trị nhỏ, sắp xếp đếm chỉ tốn với là giá trị lớn nhất.
Khi thi, gần như lúc nào ta cũng gọi sort của C++ (bài 0.4.2). Tuy vậy, hiểu các thuật toán bên trong vẫn rất đáng: nhiều đề hỏi thẳng về quá trình sắp xếp, như số lần đổi chỗ hay trạng thái sau một bước, và các ý tưởng của chúng (đếm nghịch thế khi trộn, phân hoạch quanh một chốt, đếm thay cho so sánh) còn được dùng ở nhiều bài sau. Bài này đi từ ba cách dễ hiểu nhất, qua sắp xếp trộn và sắp xếp nhanh, tới sắp xếp đếm.
IBa cách đơn giản: nổi bọt, chọn, chèn
Trong cả bài, dãy được đánh số từ tới và ta sắp tăng dần. Một cặp vị trí có được gọi là một nghịch thế (inversion): hai số đứng sai thứ tự so với dãy đã sắp. Dãy đã tăng dần có nghịch thế, dãy giảm dần có nhiều nhất, nghịch thế.
Sắp xếp nổi bọt (bubble sort). Đi từ đầu tới cuối dãy, gặp hai số kề nhau mà số trước lớn hơn số sau thì đổi chỗ. Sau một lượt như vậy, số lớn nhất bị đẩy dần về cuối dãy, giống bọt khí nổi lên mặt nước. Lặp lại cho phần còn lại; khi một lượt không đổi chỗ lần nào thì dãy đã tăng dần và ta dừng.
for (ll p = 1; p < n; p++) {
bool changed = false;
for (ll i = 1; i <= n - p; i++)
if (a[i] > a[i + 1]) {
swap(a[i], a[i + 1]);
changed = true;
}
if (!changed) break;
}
Độ phức tạp: , với cần khoảng phép so sánh.
Có một tính chất rất hay của nổi bọt: mỗi lần đổi chỗ hai số kề nhau đang sai thứ tự xóa đi đúng một nghịch thế, vì chỉ có cặp của hai số đó đổi thứ tự, mọi cặp khác vẫn như cũ. Vậy số lần đổi chỗ của nổi bọt đúng bằng số nghịch thế của dãy. Hơn nữa, mọi thuật toán chỉ đổi chỗ hai phần tử kề nhau đều phải đổi ít nhất chừng ấy lần. Ở hình trên, dãy có nghịch thế và nổi bọt đổi chỗ đúng lần.
Sắp xếp chọn (selection sort). Ở bước , tìm số nhỏ nhất trong đoạn rồi đổi nó về vị trí . Sau bước , đoạn chứa số nhỏ nhất của dãy theo đúng thứ tự.
for (ll i = 1; i < n; i++) {
ll best = i;
for (ll j = i + 1; j <= n; j++)
if (a[j] < a[best]) best = j;
swap(a[i], a[best]);
}
Độ phức tạp: phép so sánh nhưng chỉ lần đổi chỗ, với cần khoảng phép so sánh.
Sắp xếp chọn luôn so sánh đủ lần, kể cả khi dãy đã tăng sẵn, nhưng nó đổi chỗ ít nhất trong ba cách. Khi đổi chỗ là việc tốn kém, hay khi đề yêu cầu in ra các lần đổi chỗ và giới hạn số lần đổi, sắp xếp chọn thường là lựa chọn đúng.
Sắp xếp chèn (insertion sort). Giữ đoạn đầu đã tăng dần, rồi chèn vào đúng chỗ của nó: lần lượt dời các số lớn hơn sang phải một ô, rồi đặt vào chỗ trống. Đây cũng là cách nhiều người xếp bài trên tay.
for (ll i = 2; i <= n; i++) {
ll x = a[i], j = i - 1;
while (j >= 1 && a[j] > x) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = x;
}
Độ phức tạp: với là số nghịch thế, tức khi dãy giảm dần nhưng chỉ khi dãy gần như đã sắp xếp.
Mỗi lần dời một số sang phải cũng xóa đúng một nghịch thế, nên sắp xếp chèn chạy rất nhanh khi dãy chỉ có ít số đứng sai chỗ. Vì lý do này, các bản cài sort trong thư viện thường dùng sắp xếp chèn cho các đoạn ngắn.
Hỏi nhanh
Dãy được sắp tăng dần bằng nổi bọt. Không cần chạy thử, chương trình đổi chỗ bao nhiêu lần?
Mỗi lần đổi chỗ hai số kề nhau xóa đúng một nghịch thế, nên số lần đổi chỗ bằng số nghịch thế. Dãy có hai nghịch thế, cả hai đều chứa số : phải đi qua rồi qua , mỗi lần qua là một lần đổi chỗ.
Ví dụNhật Ký Sắp Xếp Nổi Bọt
Đề. Cho một hoán vị của chưa tăng dần. Chạy nổi bọt: xét lần lượt các cặp vị trí , cặp nào sai thứ tự thì đổi chỗ; hết bốn cặp mà dãy chưa tăng dần thì làm lại từ cặp đầu. In ra dãy ngay sau mỗi lần đổi chỗ.
Lời giải. Chỉ cần làm đúng như đề mô tả và in dãy sau mỗi lần đổi chỗ. Vòng lặp ngoài dừng khi một lượt không đổi chỗ lần nào, vì khi đó dãy đã tăng dần. Dãy có năm số nên có nhiều nhất nghịch thế, tức là in nhiều nhất dòng.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll a[6];
signed main() {
for (ll i = 1; i <= 5; i++) cin >> a[i];
bool changed = true;
while (changed) {
changed = false;
for (ll i = 1; i <= 4; i++)
if (a[i] > a[i + 1]) {
swap(a[i], a[i + 1]);
changed = true;
for (ll j = 1; j <= 5; j++) cout << a[j] << (j == 5 ? '\n' : ' ');
}
}
}
5 1 4 2 3
1 5 4 2 3
1 4 5 2 3
1 4 2 5 3
1 4 2 3 5
1 2 4 3 5
1 2 3 4 5
Độ phức tạp: nhiều nhất lần đổi chỗ, mỗi lần in số.
IISắp xếp trộn
Cả ba cách trên đều chỉ đổi chỗ hoặc dời các số kề nhau, nên khi dãy có cỡ nghịch thế thì chúng phải làm cỡ bước. Muốn nhanh hơn, phải cho các số “nhảy xa”.
Sắp xếp trộn (merge sort) làm điều đó bằng cách chia đôi. Để sắp đoạn : nếu đoạn chỉ có một số thì không cần làm gì; ngược lại đặt , sắp riêng hai nửa và bằng chính cách này, rồi trộn hai nửa đã sắp thành một. Hàm gọi lại chính nó trên đoạn nhỏ hơn như vậy được gọi là hàm đệ quy.
Trộn hai dãy đã tăng dần dùng hai con trỏ (bài 0.7), mỗi con trỏ đứng ở số nhỏ nhất chưa lấy của một dãy. Mỗi bước, so hai số đang trỏ, lấy số nhỏ hơn đưa vào kết quả rồi tiến con trỏ đó. Ví dụ trộn với cho . Cách này đúng vì số nhỏ nhất còn lại luôn nằm ở đầu một trong hai dãy; trộn hai dãy dài và tốn .
Độ dài đoạn giảm một nửa sau mỗi tầng chia, nên có khoảng tầng. Ở mỗi tầng, các đoạn rời nhau và phủ cả dãy, nên tổng công trộn của một tầng là . Cả thuật toán vì thế tốn ; với chỉ có tầng.
Trong lúc trộn, ta còn đếm được số nghịch thế. Một nghịch thế hoặc nằm trọn trong nửa trái, hoặc nằm trọn trong nửa phải, hoặc có một số ở nửa trái và một số ở nửa phải. Hai loại đầu do hai lời gọi đệ quy tự đếm. Loại thứ ba đếm ngay khi trộn: lúc lấy một số của nửa phải ra, mọi số của nửa trái chưa được lấy đều lớn hơn và đứng trước nó, nên mỗi số đó tạo với một nghịch thế. Gọi là con trỏ của nửa trái, các số chưa lấy là , tức là có nghịch thế mới.
Hỏi nhanh
Khi trộn, nửa trái là a[l..mid], nửa phải là a[mid+1..r], con trỏ nửa trái đang ở i. Ta vừa lấy a[j] của nửa phải ra trong khi nửa trái chưa hết. Cần cộng bao nhiêu vào số nghịch thế vì a[j]?
Các số nửa trái chưa lấy là a[i], ..., a[mid], tức mid - i + 1 số. Chúng đứng trước a[j] và đều lớn hơn nó, mỗi số tạo một nghịch thế. r - j + 1 đếm số nửa phải còn lại, j - mid đếm số nửa phải đã lấy kể cả a[j]; cả hai không liên quan. mid - i thiếu đúng số a[i] đang trỏ.
Ví dụĐếm Cặp Ngược Thứ Tự
Đề. Cho dãy số nguyên, . Đếm số cặp có ; hai số bằng nhau không tính.
Lời giải. Sắp xếp trộn và cộng mỗi lần lấy một số của nửa phải ra trước. Điều kiện lấy bên trái là a[i] <= a[j]: hai số bằng nhau thì lấy bên trái trước, nên cặp đó không bị đếm, đúng với dấu so sánh chặt của đề. Mảng phụ tmp khai báo toàn cục để mọi lời gọi dùng chung. Số nghịch thế có thể tới , nên cần kiểu long long.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, a[N], tmp[N];
ll solve(ll l, ll r) {
if (l == r) return 0;
ll mid = (l + r) >> 1;
ll res = solve(l, mid) + solve(mid + 1, r);
ll i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) tmp[k++] = a[i++];
else {
res += mid - i + 1;
tmp[k++] = a[j++];
}
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
for (ll x = l; x <= r; x++) a[x] = tmp[x];
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
cout << solve(1, n) << '\n';
}
4
3 1 2 1
4
Độ phức tạp: , với cần khoảng phép tính.
IIISắp xếp nhanh và phần tử thứ k
Sắp xếp nhanh (quicksort) cũng chia để trị nhưng theo chiều ngược lại: phân chia trước, không cần trộn sau. Chọn một số của đoạn làm chốt (pivot), đưa các số nhỏ hơn chốt sang bên trái và các số lớn hơn chốt sang bên phải, rồi sắp riêng hai bên. Ghép bên trái, chốt, bên phải lại là xong, vì mọi số bên trái đều nhỏ hơn mọi số bên phải.
Thời gian chạy phụ thuộc vào chốt. Nếu chốt luôn chia đoạn thành hai phần gần bằng nhau, ta có tầng như sắp xếp trộn, tổng . Nhưng nếu luôn lấy số đầu đoạn làm chốt mà dãy đã tăng dần sẵn, thì bên trái luôn rỗng, mỗi tầng chỉ bớt đi một số, và cả thuật toán tốn . Cách chữa quen thuộc là chọn chốt ngẫu nhiên trong đoạn: khi đó không có dữ liệu nào làm thuật toán chậm một cách cố định, và thời gian trung bình là .
Ví dụNhật Ký Sắp Xếp Nhanh Hậu Tự
Đề. Cho số đôi một khác nhau. Sắp xếp nhanh với chốt là số đầu đoạn: chia đoạn thành dãy các số nhỏ hơn chốt và dãy các số lớn hơn chốt (giữ nguyên thứ tự xuất hiện), sắp bên trái, rồi sắp bên phải, rồi ghép bên trái, chốt, bên phải. Mỗi đoạn có ít nhất hai số, sau khi sắp xong thì in ra trên một dòng.
Lời giải. Viết đúng hàm đệ quy như đề, nhận một dãy và trả về dãy đã sắp. Lệnh in đặt sau hai lời gọi con, nên đoạn nhỏ được in trước đoạn chứa nó. Với dãy ở hình trên, đoạn xong trước, rồi tới , cuối cùng là cả dãy; đoạn chỉ có số hay số thì không in.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n;
vector<ll> qs(vector<ll> a) {
if (a.size() <= 1) return a;
ll p = a[0];
vector<ll> lo, hi;
for (ll i = 1; i < (ll)a.size(); i++) {
if (a[i] < p) lo.push_back(a[i]);
else hi.push_back(a[i]);
}
lo = qs(lo);
hi = qs(hi);
vector<ll> res = lo;
res.push_back(p);
for (ll i = 0; i < (ll)hi.size(); i++) res.push_back(hi[i]);
for (ll i = 0; i < (ll)res.size(); i++) cout << res[i] << (i + 1 == (ll)res.size() ? '\n' : ' ');
return res;
}
signed main() {
cin >> n;
vector<ll> a(n);
for (ll i = 0; i < n; i++) cin >> a[i];
qs(a);
}
5
4 1 5 2 3
2 3
1 2 3
1 2 3 4 5
Độ phức tạp: trong trường hợp xấu nhất vì chốt cố định là số đầu; với cần khoảng phép tính.
Ý tưởng phân hoạch còn cho một thuật toán rất có ích: tìm phần tử nhỏ thứ mà không cần sắp cả dãy. Sau khi phân hoạch quanh chốt, ta biết chốt đứng ở vị trí thứ mấy. Nếu vị trí đó đúng là thì xong; nếu không, phần tử cần tìm nằm hẳn ở một bên, nên chỉ cần làm tiếp ở bên đó. Mỗi tầng chỉ đi tiếp một nhánh, nên với chốt ngẫu nhiên, tổng công việc trung bình là , tức . Thư viện C++ có sẵn hàm làm việc này:
nth_element(a + 1, a + k, a + n + 1);
Độ phức tạp: trung bình , với cần khoảng vài triệu phép tính.
Sau lệnh trên, a[k] là số nhỏ thứ của dãy, các số đứng trước nó đều không lớn hơn nó và các số đứng sau đều không nhỏ hơn nó, nhưng hai phần đó chưa được sắp xếp.
Hỏi nhanh
Sắp xếp nhanh luôn lấy số đầu đoạn làm chốt. Với , dãy nào dưới đây làm thuật toán chạy chậm nhất?
Với dãy tăng dần, chốt là số nhỏ nhất của đoạn, nên phần nhỏ hơn chốt luôn rỗng và phần lớn hơn chỉ ngắn đi một số. Thuật toán đi qua tầng, tầng thứ tốn khoảng phép so sánh, tổng cỡ .
Với dãy ngẫu nhiên, chốt thường rơi gần giữa và thời gian là . Dãy xen kẽ chỉ làm vài tầng đầu lệch. Dấu của số không ảnh hưởng tới tốc độ so sánh.
IVSắp xếp đếm
Có số nguyên trong . sort cần cỡ phép so sánh, trong khi chỉ có giá trị khác nhau. Ta chỉ cần biết mỗi giá trị xuất hiện bao nhiêu lần: đếm vào mảng cnt[v], rồi viết ra đúng cnt[v] lần theo thứ tự tăng của . Cách này gọi là sắp xếp đếm (counting sort), tốn khi các giá trị nằm trong .
Sắp xếp đếm chỉ đi qua mảng hai lượt, mỗi lượt là một phép cộng vào ô nhớ, còn sort phải so sánh và đổi chỗ cỡ lần, chậm hơn khoảng 30 lần.
Khi mỗi phần tử còn mang dữ liệu khác (tên, chỉ số) và ta sắp theo một khóa nguyên nhỏ, cần giữ thứ tự ban đầu của các phần tử cùng khóa. Một cách sắp xếp giữ được điều này gọi là ổn định (stable). Lấy tổng tiền tố (bài 0.3) của mảng đếm để biết khối của mỗi khóa bắt đầu ở đâu, rồi duyệt dãy từ trái sang phải, đặt mỗi phần tử vào ô trống đầu tiên trong khối của khóa nó. Phần tử đến trước nhận vị trí nhỏ hơn, nên thứ tự của các phần tử cùng khóa được giữ nguyên.
for (ll i = 1; i <= n; i++) st[key[i] + 1]++;
st[0] = 1;
for (ll v = 1; v <= V + 1; v++) st[v] += st[v - 1];
for (ll i = 1; i <= n; i++) ord[st[key[i]]++] = i;
Độ phức tạp: , với và cần khoảng phép tính.
Sau đoạn trên, ord[1..n] là các chỉ số theo khóa tăng dần, cùng khóa thì giữ thứ tự ban đầu. Tính ổn định còn cho phép sắp theo nhiều khóa bằng nhiều lượt: sắp theo khóa phụ trước, rồi sắp ổn định theo khóa chính. Áp dụng cho các chữ số từ hàng đơn vị lên hàng cao, ta được sắp xếp theo cơ số (radix sort).
Sắp xếp đếm chỉ đáng dùng khi cỡ trở xuống. Với giá trị tới , mảng đếm cần ô, và khi đó nên dùng sort.
Câu hỏi nhanh dành cho tài khoản Pro
Mở khóa bằng tài khoản ProVí dụDãy Số Theo Thứ Tự Tăng
Đề. Có số nguyên dương , mỗi số một dòng. In dãy sau khi sắp không giảm, mỗi số một dòng.
Lời giải. Giá trị nhỏ còn rất lớn: đếm cnt[v] rồi in đúng cnt[v] lần, . Phần tính toán chỉ vài dòng; phần chậm nhất lại là đọc và ghi, vì đầu vào và đầu ra đều cỡ 60 MB. Ngay cả cin đã tắt đồng bộ cũng mất cỡ một giây cho số, nên ta tự đọc và ghi theo từng khối lớn. Hàm gc lấy từng ký tự từ một khối byte đọc bằng fread, hàm read ghép các chữ số thành một số, hàm put gom ký tự vào một khối rồi ghi ra bằng fwrite khi đầy. Mỗi giá trị được đổi thành xâu một lần bằng to_string rồi ghi cnt[v] lần.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, cnt[10005];
char ibuf[1 << 16], obuf[1 << 16];
ll ip, il, op;
char gc() {
if (ip == il) {
il = fread(ibuf, 1, sizeof ibuf, stdin);
ip = 0;
if (il == 0) return 0;
}
return ibuf[ip++];
}
ll read() {
char c = gc();
while (c < '0') c = gc();
ll x = 0;
while (c >= '0') {
x = x * 10 + c - '0';
c = gc();
}
return x;
}
void put(char c) {
if (op == sizeof obuf) {
fwrite(obuf, 1, op, stdout);
op = 0;
}
obuf[op++] = c;
}
signed main() {
n = read();
for (ll i = 1; i <= n; i++) cnt[read()]++;
for (ll v = 1; v <= 10000; v++) {
string s = to_string(v);
for (ll k = 1; k <= cnt[v]; k++) {
for (char c : s) put(c);
put('\n');
}
}
fwrite(obuf, 1, op, stdout);
}
8
4
1
4
2
10000
2
1
7
1
1
2
2
4
4
7
10000
Độ phức tạp: thời gian và bộ nhớ ngoài hai khối đệm; với cần khoảng phép tính, cộng thời gian đọc ghi.
VVì sao không nhanh hơn n log n
Có thuật toán nào chỉ dựa vào so sánh mà nhanh hơn không? Câu trả lời là không. Một dãy số khác nhau có thứ tự ban đầu, và mỗi thứ tự cần một cách sắp riêng. Mỗi phép so sánh chỉ trả lời “có” hoặc “không”, nên nhiều nhất chia đôi số khả năng còn lại; muốn phân biệt hết khả năng thì cần ít nhất khoảng phép so sánh, mà con số này cỡ . Sắp xếp đếm vượt được giới hạn này vì nó không so sánh hai phần tử nào, mà dùng thẳng giá trị làm chỉ số.
VIVài cách sắp xếp kỳ lạ
Ngoài các thuật toán nghiêm túc ở trên, giới lập trình còn có những cách “sắp xếp” để cười, nhưng mỗi cách lại nhắc một điều đáng nhớ:
- Sắp xếp Stalin: đi từ trái sang, số nào nhỏ hơn số vừa giữ lại thì bị loại khỏi dãy. Chạy và luôn cho dãy tăng dần, chỉ có điều là mất dữ liệu. Thứ nó trả về là một dãy con tăng, chưa chắc dài nhất.
- Sắp xếp Bogo: xáo ngẫu nhiên cả dãy cho tới khi dãy tình cờ tăng dần. Mỗi lần xáo chỉ có xác suất trúng, nên trung bình cần cỡ bước; với đã là vài tỉ.
- Sắp xếp bằng ngủ (sleep sort): với mỗi số , cho một luồng chờ giây rồi in . Số nhỏ thức dậy trước nên được in trước, nhưng thời gian chạy là giá trị lớn nhất chứ không phải số phần tử.
- Sắp xếp mì sợi (spaghetti sort): cắt các sợi mì dài đúng bằng từng số, dựng cả bó đứng trên bàn, hạ bàn tay xuống; sợi đầu tiên chạm tay là sợi dài nhất. Trên giấy là , nhưng chỉ chạy được trên bàn bếp.
Muốn nghe các thuật toán sắp xếp “kêu” ra sao, bạn có thể xem video mô phỏng có âm thanh trong mục Đọc thêm.
Bài nâng cao
Xem 1 bài nâng cao
Ví dụĐảo Ngược Xâu Bằng Hoán Đổi Kề
Đề. Cho xâu gồm chữ cái thường. Mỗi thao tác đổi chỗ hai ký tự kề nhau. Tìm số thao tác ít nhất để biến thành xâu đảo ngược của chính nó.
Lời giải. Gọi là xâu đảo ngược của . Nếu biết ký tự thứ của được lấy từ vị trí nào trong , thì bài toán trở thành: sắp dãy vị trí bằng cách đổi chỗ hai phần tử kề nhau. Theo mục đầu bài, số lần đổi chỗ ít nhất đúng bằng số nghịch thế của dãy .
Còn phải chọn cách ghép. Với mỗi chữ cái, ta ghép lần xuất hiện thứ nhất của nó trong với lần xuất hiện thứ nhất trong , lần thứ hai với lần thứ hai, và cứ thế. Ghép như vậy không bao giờ thiệt: nếu hai chữ giống nhau bị ghép chéo nhau, đổi lại cho khỏi chéo vẫn ra đúng xâu mà bớt đi một nghịch thế. Với $s = $ aabca, ta có $t = $ acbaa và , có nghịch thế, đúng bằng đáp án.
Dãy dài tới nên đếm nghịch thế bằng sắp xếp trộn như ví dụ ở mục hai. Đáp án có thể tới cỡ , nên dùng long long.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, p[N], tmp[N], used[26];
string s;
vector<ll> pos[26];
ll solve(ll l, ll r) {
if (l == r) return 0;
ll mid = (l + r) >> 1;
ll res = solve(l, mid) + solve(mid + 1, r);
ll i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (p[i] <= p[j]) tmp[k++] = p[i++];
else {
res += mid - i + 1;
tmp[k++] = p[j++];
}
}
while (i <= mid) tmp[k++] = p[i++];
while (j <= r) tmp[k++] = p[j++];
for (ll x = l; x <= r; x++) p[x] = tmp[x];
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> s;
for (ll i = 0; i < n; i++) pos[s[i] - 'a'].push_back(i + 1);
for (ll i = 1; i <= n; i++) {
ll c = s[n - i] - 'a';
p[i] = pos[c][used[c]++];
}
cout << solve(1, n) << '\n';
}
5
aabca
3
Độ phức tạp: cho bước đếm nghịch thế, phần ghép chỉ ; với cần khoảng phép tính.
Khi đi thi
- Khi đề chỉ cần một dãy đã sắp, dùng
sort. Tự cài sắp xếp khi đề hỏi về chính quá trình sắp xếp: số lần đổi chỗ, số lần so sánh, trạng thái sau một bước. - Số lần đổi chỗ hai phần tử kề nhau ít nhất để sắp xếp bằng số nghịch thế; với tới thì đếm bằng sắp xếp trộn, và nhớ dùng
long longvì số nghịch thế có thể tới cỡ . - Giá trị nhỏ (tới khoảng ) mà rất lớn thì sắp xếp đếm vừa nhanh vừa ngắn; khi đó thường phải đọc ghi nhanh nữa.
Tóm tắt
- Nổi bọt, chọn, chèn đều ; mỗi lần đổi chỗ hai số kề nhau xóa đúng một nghịch thế, nên số lần đổi chỗ của nổi bọt bằng số nghịch thế.
- Sắp xếp chọn đổi chỗ ít nhất ( lần); sắp xếp chèn nhanh khi dãy gần như đã sắp.
- Sắp xếp trộn chia đôi, sắp hai nửa rồi trộn bằng hai con trỏ, tốn ; lấy một số của nửa phải ra trước thì cộng nghịch thế.
- Sắp xếp nhanh phân hoạch quanh một chốt; nên chọn chốt ngẫu nhiên.
nth_elementtìm phần tử nhỏ thứ trong trung bình. - Sắp xếp đếm tốn và ổn định khi đặt phần tử theo tiền tố của mảng đếm; mọi cách sắp chỉ dựa vào so sánh cần ít nhất cỡ phép so sánh.
Bài tập
Đọc thêm
- Thuật toán sắp xếpVNOI Wiki
- 排序简介OI Wiki
- 归并排序OI Wiki
- SortingVisuAlgo
- 15 Sorting Algorithms in 6 MinutesTimo Bingmann (YouTube)