Bài 26.120 phút đọc
Tìm tuyến tính
Sau bài này bạn sẽ làm được
- Cài tìm tuyến tính trả về chỉ số hoặc con trỏ
- Hiểu mẹo lính gác và giới hạn của nó
- Biết khi nào không đáng sắp xếp trước
- Đo số phép so sánh trung bình
Tìm tuyến tính là thuật toán đơn giản nhất trong cả khóa học: đi qua từng phần tử cho tới khi gặp. Nó đáng học kỹ vì nó là chuẩn để so, và vì trong nhiều tình huống thực tế nó vẫn là lựa chọn đúng.
#Vòng lặp cơ bản
tim-tuyen-tinh.c
#include <stddef.h>
/* Trả về chỉ số phần tử đầu tiên bằng x, hoặc -1 nếu không có. */
long tim_tuyen_tinh(const int *a, size_t n, int x)
{
for (size_t i = 0; i < n; ++i)
if (a[i] == x)
return (long)i;
return -1;
}#Trả về chỉ số hay con trỏ
Ba biến thể
/* Biến thể 1: chỉ số, quy ước trả về n */
size_t tim_chi_so(const int *a, size_t n, int x);
/* Biến thể 2: con trỏ, quy ước trả về NULL */
int *tim_con_tro(int *a, size_t n, int x)
{
for (size_t i = 0; i < n; ++i)
if (a[i] == x) return &a[i];
return NULL;
}
/* Biến thể 3: tổng quát cho mọi kiểu, giống lfind của POSIX */
void *tim_tong_quat(const void *khoa, const void *mang,
size_t n, size_t co,
int (*so_sanh)(const void *, const void *))
{
const char *p = mang;
for (size_t i = 0; i < n; ++i, p += co)
if (so_sanh(khoa, p) == 0)
return (void *)p;
return NULL;
}| Chỉ số | Con trỏ | Tổng quát | |
|---|---|---|---|
| Dấu hiệu không thấy | n | NULL | NULL |
| Dùng được với kiểu bất kỳ | Không | Không | Có |
| Tốc độ | Nhanh nhất | Nhanh | Chậm, mỗi phần tử một lời gọi |
| Tính được vị trí tương đối | Có ngay | Phải trừ con trỏ | Phải trừ rồi chia |
| Dùng khi | Cần biết vị trí | Cần sửa phần tử | Viết thư viện |
#Mẹo lính gác
Lính gác
Đặt giá trị cần tìm vào ngay sau phần tử cuối, để vòng lặp chắc chắn dừng. Nhờ vậy bỏ được phép kiểm biên
i < n, còn đúng một phép so sánh mỗi vòng thay vì hai.linh-gac.c
/* Mảng phải có ÍT NHẤT n+1 ô, ô thứ n dùng làm chỗ đặt lính gác.
Hàm sửa a tạm thời rồi khôi phục, nên a không được là const. */
size_t tim_linh_gac(int *a, size_t n, size_t suc_chua, int x)
{
if (suc_chua <= n) return n; /* không có chỗ cho lính gác */
int cuu = a[n];
a[n] = x; /* đặt lính gác */
size_t i = 0;
while (a[i] != x) ++i; /* KHÔNG cần kiểm i < n */
a[n] = cuu; /* khôi phục */
return i; /* i bằng n nghĩa là không tìm thấy */
}terminal
gcc -std=c17 -O0 do-linh-gac.c -o t0 && ./t0 100000000
khong linh gac : 0.412 s co linh gac : 0.284 s nhanh hon : 1.45 lan
# Nhưng với -O2 thì chênh lệch gần như biến mất
gcc -std=c17 -O2 do-linh-gac.c -o t2 && ./t2 100000000
khong linh gac : 0.038 s co linh gac : 0.037 s nhanh hon : 1.03 lan
#Số phép so sánh trung bình
| Trường hợp | Số phép so sánh | Ghi chú |
|---|---|---|
| Tốt nhất | 1 | Phần tử đầu tiên khớp |
| Xấu nhất khi có | n | Phần tử cuối cùng khớp |
| Trung bình khi có | (n + 1) / 2 | Giả sử mọi vị trí đều khả năng như nhau |
| Không tìm thấy | n | Luôn phải quét hết |
do-trung-binh.c
#include <stdio.h>
#include <stdlib.h>
static long dem;
static long tim_dem(const int *a, long n, int x)
{
for (long i = 0; i < n; ++i) {
++dem;
if (a[i] == x) return i;
}
return -1;
}
int main(void)
{
const long n = 10000;
int *a = malloc((size_t)n * sizeof *a);
if (a == NULL) return 1;
for (long i = 0; i < n; ++i) a[i] = (int)i;
srand(2024);
/* Tìm phần tử chắc chắn có */
dem = 0;
for (long k = 0; k < 100000; ++k) tim_dem(a, n, rand() % (int)n);
printf("tim thay : %.1f phep so sanh TB, ly thuyet %.1f\n",
(double)dem / 100000.0, (n + 1) / 2.0);
/* Tìm phần tử chắc chắn không có */
dem = 0;
for (long k = 0; k < 100000; ++k) tim_dem(a, n, -1);
printf("khong tim thay: %.1f phep so sanh TB, ly thuyet %ld\n",
(double)dem / 100000.0, n);
free(a);
return 0;
}terminal
gcc -std=c17 -O2 do-trung-binh.c -o t && ./t
tim thay : 5000.4 phep so sanh TB, ly thuyet 5000.5 khong tim thay: 10000.0 phep so sanh TB, ly thuyet 10000
#Khi nào tuyến tính là lựa chọn đúng
| Tình huống | Vì sao tuyến tính thắng |
|---|---|
| n nhỏ, dưới khoảng 50 | Hằng số ẩn nhỏ hơn, và mảng nhỏ nằm gọn trong bộ nhớ đệm |
| Chỉ tìm một hoặc vài lần | Sắp xếp trước tốn O(n log n), không bù lại được |
| Dữ liệu thay đổi liên tục | Giữ mảng luôn sắp xếp tốn O(n) mỗi lần chèn |
| Tiêu chí tìm không sắp xếp được | Ví dụ tìm phần tử đầu tiên thỏa một vị từ phức tạp |
| Cấu trúc không truy cập ngẫu nhiên được | Danh sách liên kết chỉ đi được tuần tự |
| Cần tìm MỌI phần tử khớp | Vẫn phải quét hết, tìm nhị phân không giúp gì |
Tự làm thử
- Cài ba biến thể của tìm tuyến tính và đo thời gian của chúng trên một trăm triệu phép tìm.
- Cài bản có lính gác, đo với
-O0và-O2, rồi giải thích vì sao chênh lệch biến mất. - Đếm số phép so sánh thực tế khi tìm thấy và khi không tìm thấy, so với công thức lý thuyết.
- Cài mẹo chuyển lên trước, rồi đo trên phân bố đều và phân bố Zipf.
- Tìm điểm hòa vốn giữa tìm tuyến tính và sắp xếp cộng tìm nhị phân trên máy bạn, với
nbằng một triệu.
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
- Tìm tuyến tính là O(n) và trung bình tốn
(n+1)/2phép so sánh khi phần tử có mặt. - Trả về
nlàm dấu hiệu không tìm thấy là quy ước tốt nhất khi chỉ số có kiểusize_t. - Mẹo lính gác từng đáng giá nhưng nay gần như vô nghĩa với
-O2, và nó sửa mảng nên không dùng được với dữ liệu chỉ đọc. - Bản tổng quát nhận con trỏ hàm chậm hơn khoảng mười lăm lần vì mỗi phần tử tốn một lời gọi không nội tuyến được.
- Sắp xếp trước chỉ đáng khi số lần tìm vượt khoảng
log2(n). Dưới ngưỡng đó, tìm tuyến tính thắng.