Vòng lặp lồng nhau
Sau bài này bạn sẽ làm được
- Xác định số lần chạy của thân vòng lặp lồng nhau
- Vẽ được các hình tam giác sao bằng vòng lặp lồng
- Giải thích vì sao lồng hai tầng cho độ phức tạp bình phương
- Giải thích vì sao duyệt theo hàng nhanh hơn duyệt theo cột
Đặt một vòng lặp bên trong vòng lặp khác là cách xử lý dữ liệu hai chiều: bảng, ma trận, hình vẽ. Nhưng số lần chạy nhân lên chứ không cộng lại, và với dữ liệu lớn thì cả thứ tự duyệt cũng ảnh hưởng tới tốc độ tới mức bất ngờ.
#Cơ chế
for (int i = 0; i < 3; ++i) { /* vòng ngoài: HÀNG */
for (int j = 0; j < 4; ++j) { /* vòng trong: CỘT */
printf("%d,%d ", i, j);
}
printf("\n");
}0,0 0,1 0,2 0,3 1,0 1,1 1,2 1,3 2,0 2,1 2,2 2,3
Điểm mấu chốt: mỗi lần vòng ngoài chạy một lượt thì vòng trong chạy trọn vẹn từ đầu tới cuối. Biến j được khởi tạo lại về 0 ở mỗi lượt của i.
#Vẽ hình bằng vòng lặp
Bài tập vẽ hình bằng dấu sao có vẻ nhàm chán, nhưng nó rèn đúng kỹ năng cần thiết: xác định quan hệ giữa số hàng và số cột.
Tam giác vuông tăng dần
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= i; ++j) { /* số cột PHỤ THUỘC hàng */
putchar('*');
}
putchar('\n');
}*
**
***
****
*****Tam giác cân
int n = 5;
for (int i = 1; i <= n; ++i) {
for (int k = 0; k < n - i; ++k) { /* khoảng trắng canh giữa */
putchar(' ');
}
for (int j = 0; j < 2 * i - 1; ++j) { /* số sao lẻ: 1, 3, 5, 7, 9 */
putchar('*');
}
putchar('\n');
} *
***
*****
*******
*********#Độ phức tạp nhân lên
Đây là chỗ vòng lặp lồng nhau trở nên nguy hiểm. Thêm một tầng lồng là nhân số phép tính lên, không phải cộng thêm.
| Số tầng | Độ phức tạp | n = 1 000 | n = 100 000 |
|---|---|---|---|
| 1 | O(n) | 10³ phép | 10⁵ phép |
| 2 | O(n²) | 10⁶ phép | 10¹⁰ phép |
| 3 | O(n³) | 10⁹ phép | 10¹⁵ phép |
Máy tính hiện đại làm khoảng 10⁸ tới 10⁹ phép tính mỗi giây. Nhìn bảng trên: với n bằng 100 000, hai tầng lồng nhau cần khoảng 10¹⁰ phép, tức là vài chục giây tới vài phút. Ba tầng thì phải tính bằng ngày.
Vòng trong ngắn dần vẫn là bậc hai
for (int i = 0; i < n; ++i) {
for (int j = i; j < n; ++j) { /* vòng trong ngắn dần */
lam_gi_do();
}
}
/* Tổng số lượt: n + (n-1) + (n-2) + ... + 1 = n(n+1)/2
Vẫn là O(n²), chỉ nhanh hơn đúng hai lần. */Bỏ hằng số là quy tắc của Big-O, nên nhanh hơn hai lần không đổi được bậc. Với n lớn thì hai lần không cứu được gì.
#Thứ tự duyệt quyết định tốc độ
Đây là phần thú vị nhất của bài. Hai đoạn mã dưới đây làm đúng cùng một số phép tính, nhưng chênh nhau nhiều lần về thời gian chạy.
for (int j = 0; j < N; ++j) {
for (int i = 0; i < N; ++i) {
tong += a[i][j];
}
}for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
tong += a[i][j];
}
}Tự đo trên máy của bạn
#include <stdio.h>
#include <time.h>
#define N 4096
static int a[N][N];
int main(void)
{
long long s = 0;
clock_t t0;
t0 = clock();
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
s += a[i][j];
}
}
double theo_hang = (double)(clock() - t0) / CLOCKS_PER_SEC;
t0 = clock();
for (int j = 0; j < N; ++j) {
for (int i = 0; i < N; ++i) {
s += a[i][j];
}
}
double theo_cot = (double)(clock() - t0) / CLOCKS_PER_SEC;
printf("theo hàng: %.3f giây\n", theo_hang);
printf("theo cột : %.3f giây\n", theo_cot);
printf("chậm hơn %.1f lần (s = %lld)\n", theo_cot / theo_hang, s);
return 0;
}theo hàng: 0.042 giây theo cột : 0.310 giây chậm hơn 7.4 lần (s = 0)
Con số cụ thể phụ thuộc máy, nhưng chênh lệch năm tới mười lần là bình thường. Hãy nhớ: cùng số phép tính, cùng thuật toán, chỉ khác thứ tự hai dòng for.
Tự làm thử
- In bảng cửu chương đầy đủ từ 1 tới 9 dạng lưới 9 hàng 9 cột, căn thẳng cột.
- Vẽ đủ sáu dạng tam giác sao: vuông tăng, vuông giảm, vuông lệch phải, tam giác cân, tam giác ngược, và hình thoi.
- In tam giác Pascal 10 hàng, canh giữa.
- Đếm số cặp phần tử trong mảng có tổng bằng một giá trị cho trước, dùng hai vòng lồng nhau. Đo thời gian với n bằng 1 000, 5 000 và 10 000, kiểm chứng thời gian tăng theo bình phương.
- Chạy chương trình
cache.cở trên và ghi lại tỉ lệ chênh lệch trên máy bạn. - Viết chương trình tìm mọi bộ ba số nguyên dương nhỏ hơn 100 thỏa định lý Pythagoras. Ước lượng số lượt lặp trước khi chạy.
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
- Mỗi lượt của vòng ngoài kéo theo vòng trong chạy trọn vẹn từ đầu tới cuối.
- Tổng số lần chạy thân là tích số lượt của các tầng, không phải tổng.
- Lồng hai tầng cho O(n²), ba tầng cho O(n³). Với n lớn thì chênh lệch là hàng giờ.
- Vòng trong ngắn dần vẫn là O(n²), chỉ nhanh hơn đúng hai lần.
- Mảng hai chiều trong C lưu liên tiếp theo hàng, nên duyệt theo hàng nhanh hơn nhiều.
- Quy tắc thực hành: với
a[i][j]thìjphải là vòng lặp trong cùng.