Bài 6.5: Bài tập thực hành vòng lặp lồng nhau trong C++

Chào mừng các bạn quay trở lại với chuỗi bài viết về C++ của FullhouseDev!

Trong các bài trước, chúng ta đã làm quen với các loại vòng lặp cơ bản như for, while, do-while. Chúng là những công cụ cực kỳ mạnh mẽ giúp chúng ta tự động hóa các công việc lặp đi lặp lại. Hôm nay, chúng ta sẽ nâng cấp sức mạnh đó lên một tầm cao mới bằng cách tìm hiểu và thực hành về vòng lặp lồng nhau (nested loops).

Vòng lặp lồng nhau đơn giản là việc đặt một vòng lặp bên trong một vòng lặp khác. Điều này cho phép chúng ta giải quyết các vấn đề có tính chất lặp đi lặp lại trên nhiều cấp độ, chẳng hạn như xử lý dữ liệu dạng lưới (ma trận), vẽ các hình mẫu phức tạp, hoặc thực hiện các thao tác cần duyệt qua tất cả các cặp phần tử trong một tập hợp.

Hãy cùng đi sâu vào lý thuyết và thực hành qua các ví dụ cụ thể nhé!

Vòng lặp lồng nhau hoạt động như thế nào?

Khi bạn có một vòng lặp bên ngoài và một vòng lặp bên trong:

  1. Vòng lặp bên ngoài bắt đầu chạy.
  2. Với mỗi một lần lặp của vòng lặp bên ngoài, vòng lặp bên trong sẽ chạy toàn bộ các lần lặp của nó từ đầu đến cuối.
  3. Sau khi vòng lặp bên trong hoàn thành, vòng lặp bên ngoài mới tiếp tục lần lặp kế tiếp (nếu có).
  4. Quá trình này lặp lại cho đến khi vòng lặp bên ngoài kết thúc.

Hãy hình dung như kim đồng hồ: Kim giờ (vòng lặp ngoài) nhích đi một nấc, thì kim phút (vòng lặp trong) phải quay hết một vòng (60 nấc). Kim giờ nhích nấc tiếp theo, kim phút lại quay hết một vòng nữa, cứ thế cho đến khi kim giờ hoàn thành vòng quay của nó.

Cú pháp chung có thể trông như thế này:

// Vòng lặp bên ngoài
for (khoi_tao_ngoai; dieu_kien_ngoai; cap_nhat_ngoai) {
    // Các lệnh xử lý cho vòng lặp bên ngoài (nếu có)

    // Vòng lặp bên trong
    for (khoi_tao_trong; dieu_kien_trong; cap_nhat_trong) {
        // Các lệnh xử lý cho vòng lặp bên trong
        // Các lệnh này sẽ chạy mỗi khi vòng lặp bên trong thực hiện 1 lần
        // và sẽ chạy TOÀN BỘ số lần lặp của vòng trong
        // cho MỖI lần lặp của vòng ngoài.
    }

    // Các lệnh xử lý khác sau khi vòng lặp bên trong kết thúc cho lần lặp hiện tại của vòng ngoài
}

Bạn có thể lồng ghép bất kỳ loại vòng lặp nào (for, while, do-while) vào với nhau. Tuy nhiên, phổ biến nhất cho các bài toán có số lần lặp xác định trước (như in hình, duyệt mảng 2D) là sử dụng for lồng for.

Tổng số lần thực hiện các lệnh bên trong vòng lặp bên trong sẽ bằng số lần lặp của vòng ngoài nhân với số lần lặp của vòng trong. Ví dụ, nếu vòng ngoài chạy 5 lần và vòng trong chạy 10 lần (cho mỗi lần của vòng ngoài), thì tổng cộng vòng trong sẽ chạy 5 * 10 = 50 lần.

Bài tập thực hành 1: In hình chữ nhật cơ bản

Bài tập kinh điển nhất để bắt đầu với vòng lặp lồng nhau là in ra một hình chữ nhật sử dụng một ký tự bất kỳ. Chúng ta cần chỉ định số hàng và số cột.

  • Vòng lặp ngoài sẽ điều khiển số hàng.
  • Vòng lặp trong sẽ điều khiển số ký tự (cột) trên mỗi hàng.

Sau khi vòng lặp trong kết thúc (hoàn thành in một hàng), chúng ta cần xuống dòng để bắt đầu hàng tiếp theo.

Hãy xem code C++:

#include <iostream> // Thư viện nhập xuất cơ bản

int main() {
    int rows = 5;    // Số hàng của hình chữ nhật
    int cols = 8;    // Số cột của hình chữ nhật
    char symbol = '*'; // Ký tự sẽ dùng để vẽ

    // Vòng lặp NGOÀI: Điều khiển số hàng
    // Biến 'i' sẽ chạy từ 0 đến rows-1
    for (int i = 0; i < rows; ++i) {
        // Vòng lặp TRONG: Điều khiển số ký tự trên MỖI hàng
        // Biến 'j' sẽ chạy từ 0 đến cols-1 cho MỖI giá trị của 'i'
        for (int j = 0; j < cols; ++j) {
            cout << symbol; // In ký tự symbol
        }
        // Sau khi vòng lặp trong (in xong 1 hàng) kết thúc, xuống dòng
        cout << endl;
    }

    return 0; // Kết thúc chương trình thành công
}

Giải thích:

  • Vòng lặp for (int i = 0; i < rows; ++i) chạy rows lần. Mỗi lần lặp này tương ứng với việc xử lý một hàng.
  • Bên trong vòng lặp ngoài, vòng lặp for (int j = 0; j < cols; ++j) chạy cols lần. Vòng lặp này chạy hoàn toàn từ j=0 đến j=cols-1 cho mỗi giá trị của i. Nó có nhiệm vụ in ra cols ký tự trên cùng một dòng.
  • cout << symbol; nằm trong vòng lặp trong, nên nó sẽ được thực thi rows * cols lần, in ra tổng số ký tự bằng số hàng nhân số cột.
  • cout << endl; nằm sau vòng lặp trong nhưng trong vòng lặp ngoài. Điều này đảm bảo rằng sau khi in đủ cols ký tự cho một hàng, chương trình sẽ xuống dòng trước khi bắt đầu in hàng tiếp theo.

Kết quả chạy chương trình với rows = 5cols = 8 sẽ như sau:

********
********
********
********
********

Tuyệt vời! Bạn đã in được hình chữ nhật đầu tiên bằng vòng lặp lồng nhau.

Bài tập thực hành 2: In hình tam giác vuông

Bây giờ, hãy thử một hình phức tạp hơn một chút: hình tam giác vuông. Với hình tam giác, số ký tự trên mỗi hàng sẽ thay đổi. Ví dụ, hàng đầu tiên có 1 ký tự, hàng thứ hai có 2, v.v... cho đến hàng cuối cùng có số ký tự bằng chiều cao của tam giác.

Điều này có nghĩa là số lần lặp của vòng lặp bên trong sẽ phụ thuộc vào biến đếm của vòng lặp bên ngoài.

Hãy xem code:

#include <iostream>

int main() {
    int height = 6; // Chiều cao của tam giác
    char symbol = '#'; // Ký tự sẽ dùng để vẽ

    // Vòng lặp NGOÀI: Điều khiển số hàng (từ 0 đến height-1)
    for (int i = 0; i < height; ++i) {
        // Vòng lặp TRONG: Điều khiển số ký tự trên hàng HIỆN TẠI (i)
        // Số ký tự trên hàng i (bắt đầu từ i=0) là i+1
        // Do đó, biến 'j' sẽ chạy từ 0 đến i
        for (int j = 0; j <= i; ++j) {
            cout << symbol; // In ký tự symbol
        }
        // Sau khi in xong 1 hàng, xuống dòng
        cout << endl;
    }

    return 0;
}

Giải thích:

  • Vòng lặp ngoài for (int i = 0; i < height; ++i) vẫn điều khiển số hàng, chạy height lần. Biến i sẽ lần lượt là 0, 1, 2, ..., height-1.
  • Điểm mấu chốt là vòng lặp trong: for (int j = 0; j <= i; ++j). Điều kiện lặp j <= i cho thấy rằng số lần lặp của vòng trong phụ thuộc vào giá trị hiện tại của i.
    • Khi i = 0 (hàng đầu tiên), vòng trong chạy khi j <= 0, tức là chỉ 1 lần (j=0). In ra 1 ký tự.
    • Khi i = 1 (hàng thứ hai), vòng trong chạy khi j <= 1, tức là 2 lần (j=0, j=1). In ra 2 ký tự.
    • Khi i = 2 (hàng thứ ba), vòng trong chạy khi j <= 2, tức là 3 lần (j=0, j=1, j=2). In ra 3 ký tự.
    • ... và cứ thế cho đến khi i = height - 1, vòng trong chạy height lần.
  • cout << endl; vẫn đảm bảo xuống dòng sau mỗi hàng.

Kết quả chạy chương trình với height = 6 sẽ là:

#
##
###
####
#####
######

Bằng cách thay đổi điều kiện hoặc phạm vi của vòng lặp trong dựa trên biến của vòng lặp ngoài, bạn có thể tạo ra vô số các hình mẫu khác nhau.

Bài tập thực hành 3: In bảng cửu chương

Vòng lặp lồng nhau không chỉ dùng để in hình. Một ứng dụng cổ điển khác là in ra bảng cửu chương. Để in bảng cửu chương từ 1 đến 10, chúng ta cần một vòng lặp ngoài cho số nhân (từ 1 đến 10) và một vòng lặp trong cho số bị nhân (cũng từ 1 đến 10). Kết quả của phép nhân sẽ được in ra bên trong vòng lặp trong.

Để bảng cửu chương được căn chỉnh đẹp mắt, chúng ta có thể sử dụng setw() từ thư viện <iomanip>.

#include <iostream>
#include <iomanip> // Cần cho setw()

int main() {
    int max_multiplier = 10; // Bảng cửu chương đến số nào

    // Tùy chọn: In tiêu đề cột để dễ nhìn
    cout << "   |"; // Khoảng trống cho cột đầu tiên
    for (int i = 1; i <= max_multiplier; ++i) {
        cout << setw(4) << i; // In số cột với độ rộng 4 ký tự
    }
    cout << endl;
    cout << "---+"; // Đường phân cách
    for (int i = 0; i < max_multiplier; ++i) {
        cout << "----";
    }
    cout << endl;

    // Vòng lặp NGOÀI: Số nhân (từ 1 đến max_multiplier)
    for (int i = 1; i <= max_multiplier; ++i) {
        // In số nhân ở cột đầu tiên, căn chỉnh
        cout << setw(3) << i << "|";

        // Vòng lặp TRONG: Số bị nhân (từ 1 đến max_multiplier)
        // Chạy cho MỖI số nhân i
        for (int j = 1; j <= max_multiplier; ++j) {
            // In kết quả i * j, căn chỉnh
            cout << setw(4) << (i * j);
        }
        // Xuống dòng sau khi hoàn thành 1 hàng (bảng cửu chương của số i)
        cout << endl;
    }

    return 0;
}

Giải thích:

  • Vòng lặp ngoài for (int i = 1; i <= max_multiplier; ++i) chạy từ 1 đến 10 (nếu max_multiplier là 10). Biến i đóng vai trò là số nhân.
  • Trước khi vào vòng lặp trong, cout << setw(3) << i << "|"; in ra số nhân hiện tại (i) ở đầu mỗi hàng, được căn chỉnh đẹp mắt.
  • Vòng lặp trong for (int j = 1; j <= max_multiplier; ++j) chạy từ 1 đến 10 cho mỗi giá trị của i. Biến j đóng vai trò là số bị nhân.
  • cout << setw(4) << (i * j); tính kết quả của i * j và in ra, cũng được căn chỉnh. Lệnh này nằm trong vòng lặp trong, nên nó sẽ được thực thi 10 * 10 = 100 lần (cho max_multiplier = 10).
  • cout << endl; xuống dòng sau khi in xong tất cả các kết quả nhân với số i hiện tại.
  • Phần code ở đầu trước vòng lặp ngoài là để in ra tiêu đề cột và đường phân cách, giúp bảng cửu chương dễ đọc hơn. setw(n) thiết lập độ rộng tối thiểu cho phần tử tiếp theo được in ra là n ký tự.

Kết quả sẽ là một bảng cửu chương được căn chỉnh:

   |   1   2   3   4   5   6   7   8   9  10
---+----------------------------------------
  1|   1   2   3   4   5   6   7   8   9  10
  2|   2   4   6   8  10  12  14  16  18  20
  3|   3   6   9  12  15  18  21  24  27  30
  4|   4   8  12  16  20  24  28  32  36  40
  5|   5  10  15  20  25  30  35  40  45  50
  6|   6  12  18  24  30  36  42  48  54  60
  7|   7  14  21  28  35  42  49  56  63  70
  8|   8  16  24  32  40  48  56  64  72  80
  9|   9  18  27  36  45  54  63  72  81  90
 10|  10  20  30  40  50  60  70  80  90 100

Bài tập này cho thấy vòng lặp lồng nhau rất hữu ích khi bạn cần kết hợp các giá trị từ hai tập hợp khác nhau (số nhân và số bị nhân trong trường hợp này).

Bài tập thực hành 4: Duyệt mảng 2 chiều (Ma trận)

Một trong những ứng dụng quan trọng nhất của vòng lặp lồng nhau là làm việc với các cấu trúc dữ liệu nhiều chiều, điển hình là mảng 2 chiều (hay ma trận). Mảng 2 chiều có hàng và cột, rất giống với cấu trúc của vòng lặp lồng nhau.

Để duyệt qua tất cả các phần tử trong một mảng 2 chiều, chúng ta sẽ sử dụng vòng lặp ngoài để duyệt qua các hàng và vòng lặp trong để duyệt qua các cột trong mỗi hàng đó.

#include <iostream>

int main() {
    // Khởi tạo một mảng 2 chiều (ma trận) 3x4
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    int rows = 3; // Số hàng
    int cols = 4; // Số cột

    cout << "Duyet va in cac phan tu cua ma tran:\n";

    // Vòng lặp NGOÀI: Duyệt qua các hàng
    for (int i = 0; i < rows; ++i) {
        // Vòng lặp TRONG: Duyệt qua các cột trong hàng HIỆN TẠI (i)
        for (int j = 0; j < cols; ++j) {
            // Truy cập và in phần tử tại hàng 'i', cột 'j'
            cout << matrix[i][j] << " ";
        }
        // Xuống dòng sau khi in xong 1 hàng
        cout << endl;
    }

    // Ví dụ khác: Tính tổng các phần tử trong ma trận
    int sum = 0;
    cout << "\nTinh tong cac phan tu cua ma tran:\n";
     for (int i = 0; i < rows; ++i) { // Duyệt qua hàng
        for (int j = 0; j < cols; ++j) { // Duyệt qua cột
            sum += matrix[i][j]; // Cộng giá trị của phần tử hiện tại vào tổng
        }
    }
    cout << "Tong cac phan tu: " << sum << endl;


    return 0;
}

Giải thích:

  • Chúng ta khai báo một mảng 2 chiều matrix có 3 hàng và 4 cột.
  • Vòng lặp ngoài for (int i = 0; i < rows; ++i) chạy từ i = 0 đến rows - 1. Mỗi giá trị của i tương ứng với chỉ số của một hàng.
  • Vòng lặp trong for (int j = 0; j < cols; ++j) chạy từ j = 0 đến cols - 1 cho mỗi giá trị của i. Mỗi giá trị của j tương ứng với chỉ số của một cột trong hàng hiện tại.
  • matrix[i][j] truy cập đến phần tử tại hàng có chỉ số i và cột có chỉ số j. Lệnh này nằm trong vòng lặp trong, nên nó sẽ được thực thi cho mọi cặp (i, j), tức là duyệt qua tất cả các phần tử của ma trận.
  • Ví dụ tính tổng minh họa cách bạn có thể xử lý từng phần tử khi duyệt qua mảng 2 chiều bằng vòng lặp lồng nhau.

Kết quả chương trình:

Duyet va in cac phan tu cua ma tran:
1 2 3 4
5 6 7 8
9 10 11 12

Tinh tong cac phan tu cua ma tran:
Tong cac phan tu: 78

Mảng 2 chiều là một cấu trúc dữ liệu cực kỳ phổ biến, đặc biệt trong các bài toán liên quan đến bảng biểu, hình ảnh, hoặc các trò chơi như cờ vua, caro. Vòng lặp lồng nhau là công cụ không thể thiếu để làm việc với chúng.

Những lưu ý khi làm việc với vòng lặp lồng nhau

  • Hiểu luồng thực thi: Luôn nhớ rằng vòng lặp bên trong chạy hoàn toàn cho mỗi lần lặp của vòng lặp bên ngoài. Điều này giúp bạn dự đoán được tổng số thao tác và kết quả.
  • Biến đếm: Các biến đếm của vòng lặp (ví dụ: i, j) thường là độc lập với nhau. Biến j trong vòng trong được khởi tạo lại từ đầu cho mỗi lần lặp mới của vòng ngoài.
  • breakcontinue: Bạn có thể sử dụng break hoặc continue bên trong vòng lặp lồng nhau. break sẽ thoát khỏi vòng lặp gần nhất chứa nó (thường là vòng lặp trong). continue sẽ bỏ qua các lệnh còn lại trong lần lặp hiện tại của vòng lặp gần nhất và chuyển sang lần lặp tiếp theo của nó.
  • Hiệu suất: Vòng lặp lồng nhau làm tăng đáng kể số lượng thao tác. Nếu bạn có hai vòng lặp lồng nhau chạy N lần và M lần, tổng số thao tác là khoảng N * M. Với ba vòng lặp lồng nhau, sẽ là N * M * K, v.v... Hãy cẩn trọng với việc lồng quá nhiều cấp độ hoặc chạy trên tập dữ liệu rất lớn, vì nó có thể ảnh hưởng đến hiệu suất chương trình.

Tự thực hành thêm

Để thành thạo vòng lặp lồng nhau, không gì tốt hơn là tự tay viết code và thử nghiệm. Hãy thử sức với những bài tập sau:

  1. In hình tam giác vuông ngược (số ký tự giảm dần theo từng hàng).
  2. In hình tam giác cân rỗng hoặc đặc.
  3. In hình thoi.
  4. Trên một mảng 2 chiều, hãy tìm giá trị lớn nhất, nhỏ nhất.
  5. Trên một mảng 2 chiều, hãy tính tổng các phần tử trên đường chéo chính.
  6. (Nâng cao) In hình bàn cờ vua (xen kẽ 2 ký tự khác nhau).

Hãy cố gắng tự giải các bài tập này trước khi tìm kiếm đáp án. Việc "vật lộn" với code chính là cách tốt nhất để hiểu sâu vấn đề.

Chúc các bạn thực hành hiệu quả và làm chủ được kỹ thuật vòng lặp lồng nhau này!

Comments

There are no comments at the moment.