Sắp xếp cơ bản
Sau bài này bạn sẽ làm được
- Cài đúng sắp xếp nổi bọt, chọn và chèn
- Giải thích tính ổn định và vì sao nó quan trọng
- So sánh ba thuật toán trên ba loại dữ liệu đầu vào
- Dùng qsort của thư viện chuẩn cho dữ liệu lớn
Ba thuật toán trong bài này đều chậm, đều tốn thời gian tỷ lệ với bình phương số phần tử, và trong công việc thật bạn sẽ dùng qsort thay vì chúng. Nhưng đây là ba thuật toán bạn phải tự cài được, vì chúng dạy ba ý tưởng khác nhau mà mọi thuật toán sắp xếp nhanh sau này đều dựa vào.
#Sắp xếp nổi bọt
void noi_bot(int *a, size_t n)
{
for (size_t i = 0; i + 1 < n; ++i) {
int da_doi = 0;
/* Sau lượt i, i phần tử cuối đã đúng chỗ nên không cần xét nữa. */
for (size_t j = 0; j + 1 < n - i; ++j)
if (a[j] > a[j + 1]) {
int t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
da_doi = 1;
}
if (!da_doi)
break; /* không đổi lần nào nghĩa là đã sắp xong */
}
}Chạy tay đủ mọi lượt
| Lượt | Mảng sau lượt | Số phép so sánh |
|---|---|---|
| đầu vào | 5 1 4 2 8 | |
| 1 | 1 4 2 5 8 | 4 |
| 2 | 1 2 4 5 8 | 3 |
| 3 | 1 2 4 5 8 | 2, không đổi lần nào nên dừng |
#Sắp xếp chọn
void sap_xep_chon(int *a, size_t n)
{
for (size_t i = 0; i + 1 < n; ++i) {
size_t vt_nho = i;
for (size_t j = i + 1; j < n; ++j)
if (a[j] < a[vt_nho])
vt_nho = j;
if (vt_nho != i) {
int t = a[i];
a[i] = a[vt_nho];
a[vt_nho] = t;
}
}
}| Lượt | Phần đã sắp | Phần chưa sắp | Nhỏ nhất tìm được |
|---|---|---|---|
| 0 | 5 1 4 2 8 | 1, đổi với 5 | |
| 1 | 1 | 5 4 2 8 | 2, đổi với 5 |
| 2 | 1 2 | 4 5 8 | 4, đã đúng chỗ |
| 3 | 1 2 4 | 5 8 | 5, đã đúng chỗ |
| xong | 1 2 4 5 8 |
Nhược điểm lớn nhất là nó không nhanh lên với dữ liệu đã sắp sẵn. Vòng lặp trong luôn phải quét hết phần còn lại để chắc chắn đã tìm đúng phần tử nhỏ nhất, nên cả ba trường hợp tốt nhất, trung bình và xấu nhất đều là n².
#Sắp xếp chèn
void sap_xep_chen(int *a, size_t n)
{
for (size_t i = 1; i < n; ++i) {
int khoa = a[i];
size_t j = i;
/* Đẩy mọi phần tử lớn hơn khoa sang phải một ô. */
while (j > 0 && a[j - 1] > khoa) {
a[j] = a[j - 1];
--j;
}
a[j] = khoa;
}
}Chú ý thuật toán này không đổi chỗ mà dịch chuyển. Nó lấy phần tử ra giữ trong biến khoa, dịch cả một dãy sang phải bằng các phép gán đơn, rồi đặt khoa vào đúng chỗ. Cách này tốn ít phép ghi hơn so với đổi chỗ liên tiếp.
| i | khoa | Mảng trước | Mảng sau |
|---|---|---|---|
| 1 | 1 | 5 1 4 2 8 | 1 5 4 2 8 |
| 2 | 4 | 1 5 4 2 8 | 1 4 5 2 8 |
| 3 | 2 | 1 4 5 2 8 | 1 2 4 5 8 |
| 4 | 8 | 1 2 4 5 8 | 1 2 4 5 8 |
#Tính ổn định
typedef struct {
char ten[16];
int lop;
} SinhVien;
SinhVien sv[4] = {
{ "An", 2 },
{ "Binh", 1 },
{ "Cuong", 2 },
{ "Dung", 1 },
};
/* Sắp theo lớp. Kết quả với thuật toán ổn định:
Binh 1, Dung 1, An 2, Cuong 2 <- An vẫn trước Cuong
Kết quả với thuật toán không ổn định có thể là:
Dung 1, Binh 1, Cuong 2, An 2 <- thứ tự cũ bị đảo */Tính ổn định trở nên quan trọng khi bạn sắp xếp theo nhiều tiêu chí. Muốn sắp danh sách theo lớp, rồi trong mỗi lớp sắp theo tên, bạn chỉ cần sắp theo tên trước, rồi sắp theo lớp bằng một thuật toán ổn định. Thứ tự tên trong mỗi lớp sẽ được giữ nguyên.
| Thuật toán | Ổn định | Vì sao |
|---|---|---|
| Nổi bọt | Có | Chỉ đổi khi a[j] lớn hơn thật sự a[j+1], bằng nhau thì giữ nguyên |
| Chèn | Có | Vòng while dừng khi gặp phần tử bằng, nên phần tử mới đứng sau |
| Chọn | Không | Phép đổi chỗ xa có thể nhảy một phần tử qua phần tử bằng nó |
| qsort của thư viện chuẩn | Không được bảo đảm | Chuẩn C không yêu cầu, tùy cài đặt của từng thư viện |
#So sánh ba thuật toán
| Thuật toán | Tốt nhất | Trung bình | Xấu nhất | Bộ nhớ | Ổn định | Số phép đổi chỗ |
|---|---|---|---|---|---|---|
| Nổi bọt | O(n) | O(n²) | O(n²) | O(1) | Có | O(n²) |
| Chọn | O(n²) | O(n²) | O(n²) | O(1) | Không | O(n) |
| Chèn | O(n) | O(n²) | O(n²) | O(1) | Có | O(n²) |
Bài đo thời gian thật
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static void sinh_ngau_nhien(int *a, size_t n)
{
for (size_t i = 0; i < n; ++i) a[i] = rand();
}
static void sinh_da_sap(int *a, size_t n)
{
for (size_t i = 0; i < n; ++i) a[i] = (int)i;
}
static void sinh_sap_nguoc(int *a, size_t n)
{
for (size_t i = 0; i < n; ++i) a[i] = (int)(n - i);
}
static double do_thoi_gian(void (*sap)(int *, size_t),
const int *goc, size_t n)
{
int *a = malloc(n * sizeof *a);
if (a == NULL) return -1.0;
memcpy(a, goc, n * sizeof *a);
clock_t t0 = clock();
sap(a, n);
double giay = (double)(clock() - t0) / CLOCKS_PER_SEC;
/* Dùng kết quả để trình biên dịch không xóa mất lời gọi khi bật -O2. */
volatile int chan = a[n / 2];
(void)chan;
free(a);
return giay;
}n = 50000, du lieu ngau nhien noi bot : 4.812 s chon : 1.934 s chen : 0.951 s n = 50000, da sap xep san noi bot : 0.000 s chon : 1.928 s chen : 0.000 s n = 50000, sap nguoc noi bot : 5.104 s chon : 1.931 s chen : 1.903 s
#qsort của thư viện chuẩn
Với dữ liệu thật, hãy dùng qsort. Nó chạy nhanh hơn hàng nghìn lần trên mảng lớn và đã được kiểm thử kỹ hơn bất cứ thứ gì bạn tự viết.
#include <stdlib.h>
static int so_sanh_int(const void *x, const void *y)
{
int a = *(const int *)x;
int b = *(const int *)y;
return (a > b) - (a < b);
}
int a[] = { 5, 1, 4, 2, 8 };
size_t n = sizeof a / sizeof a[0];
qsort(a, n, sizeof a[0], so_sanh_int);noi bot : bo qua, uoc tinh hon 30 phut qsort : 0.118 s
Sắp xếp mảng struct theo nhiều tiêu chí
typedef struct {
char ten[32];
int lop;
double diem;
} SinhVien;
/* Sắp theo lớp tăng dần, cùng lớp thì theo điểm giảm dần,
cùng điểm thì theo tên. */
static int so_sanh_sv(const void *x, const void *y)
{
const SinhVien *a = x;
const SinhVien *b = y;
if (a->lop != b->lop)
return (a->lop > b->lop) - (a->lop < b->lop);
if (a->diem != b->diem)
return (a->diem < b->diem) - (a->diem > b->diem); /* giảm dần */
return strcmp(a->ten, b->ten);
}
qsort(sv, n, sizeof sv[0], so_sanh_sv);Chương 27 sẽ dạy các thuật toán sắp xếp nhanh: sắp xếp trộn, sắp xếp nhanh và sắp xếp vun đống, cùng cách chứng minh giới hạn dưới n log n cho mọi thuật toán dựa trên so sánh.
Tự làm thử
- Cài cả ba thuật toán, thêm biến đếm số phép so sánh và số phép đổi chỗ, in ra sau mỗi lần chạy.
- Chạy bài đo thời gian với n bằng 1 000, 10 000 và 50 000 trên ba loại dữ liệu. Lập bảng và giải thích từng ô.
- Bỏ biến
da_doikhỏi sắp xếp nổi bọt, đo lại với mảng đã sắp sẵn và so sánh. - Chứng minh bằng thực nghiệm rằng sắp xếp chọn không ổn định: sắp mảng struct có khóa trùng nhau và in thứ tự trước sau.
- Sắp mảng sinh viên theo ba tiêu chí như ví dụ trong bài bằng
qsort. - Cài hàm so sánh sai kiểu
return a - b;với hai giá trịINT_MAXvà âm một, chạy dưới-fsanitize=undefined. - Đo ngưỡng n mà sắp xếp chèn còn nhanh hơn
qsorttrên máy bạn.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Ba thuật toán đều là O(n bình phương) trung bình, dùng O(1) bộ nhớ phụ, và đều sắp xếp tại chỗ.
- Nổi bọt và chèn về O(n) với dữ liệu đã sắp sẵn, sắp xếp chọn thì không bao giờ nhanh lên.
- Sắp xếp chọn thực hiện ít phép đổi chỗ nhất, hữu ích khi phép ghi đắt.
- Sắp xếp chèn nhanh nhất với n nhỏ và với dữ liệu gần sắp xếp, nên nó là bước cuối của các thuật toán nhanh.
- Ổn định nghĩa là giữ nguyên thứ tự tương đối của các phần tử bằng nhau, quan trọng khi sắp theo nhiều tiêu chí.
- Với dữ liệu thật hãy dùng
qsort, và viết hàm so sánh bằng(a > b) - (a < b).