Bài 27.2: Bài tập thực hành duyệt mảng 2 chiều trong C++

Chào mừng bạn quay trở lại với series C++! Hôm nay chúng ta sẽ cùng nhau đi sâu vào thế giới của mảng 2 chiều - một cấu trúc dữ liệu cực kỳ mạnh mẽ và linh hoạt, thường được ví như những "bảng tính" hay "ma trận" trong lập trình. Mảng 2 chiều giúp chúng ta tổ chức dữ liệu theo cấu trúc lưới hoặc bảng, rất hữu ích cho nhiều bài toán khác nhau, từ xử lý hình ảnh đơn giản đến các trò chơi trên lưới.

Để làm chủ mảng 2 chiều, kỹ năng duyệt (traversal) là hoàn toàn không thể thiếu. Duyệt mảng 2 chiều đơn giản là quá trình truy cập đến mọi phần tử của mảng theo một thứ tự nhất định để thực hiện một thao tác nào đó (ví dụ: in giá trị, tính tổng, tìm kiếm, sửa đổi...). Bài viết này sẽ tập trung vào các kỹ thuật và bài tập thực hành cơ bản nhất về duyệt mảng 2 chiều trong C++.

Mảng 2 Chiều Là Gì?

Trước khi duyệt, hãy nhắc lại một chút về mảng 2 chiều. Mảng 2 chiều trong C++ về cơ bản là "một mảng của các mảng". Nó được khai báo với hai cặp dấu ngoặc vuông [][], cặp đầu tiên chỉ số hàng (row) và cặp thứ hai chỉ số cột (column).

Ví dụ khai báo một mảng 2 chiều cố định kích thước 3 hàng, 4 cột:

int arr[3][4];

Để truy cập một phần tử cụ thể, chúng ta cần cung cấp cả chỉ số hàng và chỉ số cột: arr[chi_so_hang][chi_so_cot]. Chỉ số luôn bắt đầu từ 0.

Kỹ Thuật Duyệt Mảng 2 Chiều Cơ Bản: Vòng Lặp Lồng Nhau

Nguyên tắc cốt lõi để duyệt qua tất cả các phần tử trong mảng 2 chiều là sử dụng hai vòng lặp lồng nhau. Vòng lặp bên ngoài thường quản lý chỉ số hàng, và vòng lặp bên trong quản lý chỉ số cột.

Hãy xem ví dụ kinh điển: in ra tất cả các phần tử của mảng.

Ví dụ 1: Duyệt và In Mảng Theo Thứ Tự Hàng-Cột (Row-Major Order)

Đây là cách duyệt phổ biến và trực quan nhất, đi từ trái sang phải trên từng hàng, hết hàng này rồi sang hàng kế tiếp.

#include <iostream>

int main() {
    // Khai báo và khởi tạo mảng 2 chiều
    const int SO_HANG = 3;
    const int SO_COT = 4;
    int mang[SO_HANG][SO_COT] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    cout << "In mang theo thu tu hang-cot:" << endl;

    // Duyệt mảng
    for (int i = 0; i < SO_HANG; ++i) { // Vòng lặp duyệt qua cac hang (chi so i)
        for (int j = 0; j < SO_COT; ++j) { // Vòng lặp duyệt qua cac cot trong tung hang (chi so j)
            // Truy cap va in phan tu tai arr[i][j]
            cout << mang[i][j] << " ";
        }
        // Xuong dong sau khi in xong mot hang
        cout << endl;
    }

    return 0;
}

Giải thích code:

  • Chúng ta định nghĩa số hàng (SO_HANG) và số cột (SO_COT) bằng const int để dễ quản lý kích thước.
  • Mảng mang được khởi tạo ngay khi khai báo. Các cặp ngoặc nhọn bên trong {} biểu thị từng hàng.
  • Vòng lặp ngoài for (int i = 0; i < SO_HANG; ++i) chạy từ i = 0 đến SO_HANG - 1, đại diện cho chỉ số của từng hàng.
  • Với mỗi giá trị của i, vòng lặp trong for (int j = 0; j < SO_COT; ++j) chạy từ j = 0 đến SO_COT - 1, đại diện cho chỉ số của từng cột trong hàng i hiện tại.
  • Trong vòng lặp trong cùng, cout << mang[i][j] << " "; truy cập và in giá trị của phần tử tại hàng i, cột j, theo sau là một khoảng trắng.
  • Sau khi vòng lặp trong (duyệt hết các cột của một hàng) kết thúc, cout << endl; sẽ xuống dòng, chuẩn bị in hàng tiếp theo.

Kết quả in ra sẽ giống với hình ảnh của mảng ban đầu:

In mang theo thu tu hang-cot:
1 2 3 4 
5 6 7 8 
9 10 11 12
Ví dụ 2: Duyệt và In Mảng Theo Thứ Tự Cột-Hàng (Column-Major Order)

Đôi khi, bạn có thể cần xử lý dữ liệu theo cột thay vì theo hàng. Bạn có thể thay đổi thứ tự của các vòng lặp.

#include <iostream>

int main() {
    const int SO_HANG = 3;
    const int SO_COT = 4;
    int mang[SO_HANG][SO_COT] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    cout << "In mang theo thu tu cot-hang (in tung cot):" << endl;

    // Duyệt mảng theo cột
    for (int j = 0; j < SO_COT; ++j) { // Vòng lặp duyệt qua cac cot (chi so j)
        for (int i = 0; i < SO_HANG; ++i) { // Vòng lặp duyệt qua cac hang trong tung cot (chi so i)
            // Truy cap va in phan tu tai arr[i][j]
            cout << mang[i][j] << " "; // Van dung arr[i][j], nhung thu tu truy cap khac
        }
        // Xuong dong sau khi in xong mot cot
        cout << endl;
    }

    return 0;
}

Giải thích code:

  • Điểm khác biệt chính là thứ tự của hai vòng lặp. Vòng lặp ngoài giờ đây duyệt qua các cột (j), và vòng lặp trong duyệt qua các hàng (i) trong cột hiện tại.
  • Mặc dù thứ tự duyệt khác nhau, chúng ta vẫn truy cập phần tử bằng mang[i][j] (hàng i, cột j).
  • Sau khi vòng lặp trong kết thúc (duyệt hết các hàng trong một cột), chúng ta xuống dòng để in cột tiếp theo.

Kết quả in ra sẽ trông như thế này (mỗi dòng là một cột từ mảng gốc):

In mang theo thu tu cot-hang (in tung cot):
1 5 9 
2 6 10 
3 7 11 
4 8 12

Lưu ý: Cách in này chỉ thể hiện thứ tự truy cập phần tử theo cột. Để in mảng dưới dạng chuyển vị (transpose), bạn sẽ cần cấu trúc lại cách in hoặc lưu trữ.

Các Bài Tập Thực Hành Duyệt Mảng 2 Chiều

Với kỹ thuật vòng lặp lồng nhau cơ bản đã nắm vững, chúng ta có thể thực hiện nhiều thao tác khác trên mảng 2 chiều. Dưới đây là một số bài tập thực hành điển hình.

Bài Tập 1: Tính Tổng Tất Cả Các Phần Tử

Để tính tổng, chúng ta chỉ cần duyệt qua tất cả các phần tử và cộng giá trị của chúng vào một biến tổng.

#include <iostream>

int main() {
    const int SO_HANG = 3;
    const int SO_COT = 4;
    int mang[SO_HANG][SO_COT] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    int tong = 0; // Bien luu tru tong

    // Duyet mang de tinh tong
    for (int i = 0; i < SO_HANG; ++i) {
        for (int j = 0; j < SO_COT; ++j) {
            tong += mang[i][j]; // Cong gia tri cua phan tu vao tong
        }
    }

    cout << "Tong cua tat ca cac phan tu trong mang la: " << tong << endl; // Ket qua 1+..+12 = 78

    return 0;
}

Giải thích code:

  • Một biến tong được khởi tạo bằng 0.
  • Chúng ta sử dụng cấu trúc vòng lặp lồng nhau theo kiểu hàng-cột (hoặc cột-hàng, kết quả tổng không đổi).
  • Trong vòng lặp trong cùng, tong += mang[i][j]; cộng giá trị của phần tử hiện tại vào biến tong.
  • Sau khi duyệt hết mảng, tong sẽ chứa tổng của tất cả các phần tử.
Bài Tập 2: Tìm Phần Tử Lớn Nhất (hoặc Nhỏ Nhất)

Để tìm phần tử lớn nhất, chúng ta cần một biến để lưu trữ giá trị lớn nhất tìm được cho đến hiện tại và một biến (hoặc hai biến) để lưu vị trí của nó.

#include <iostream>
#include <limits> // Can thiet de su dung numeric_limits

int main() {
    const int SO_HANG = 3;
    const int SO_COT = 4;
    int mang[SO_HANG][SO_COT] = {
        {1, 2, 25, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // Khoi tao gia tri lon nhat voi gia tri rat nho
    int max_val = numeric_limits<int>::min();
    int max_row = -1;
    int max_col = -1;

    // Duyet mang de tim phan tu lon nhat
    for (int i = 0; i < SO_HANG; ++i) {
        for (int j = 0; j < SO_COT; ++j) {
            if (mang[i][j] > max_val) {
                max_val = mang[i][j]; // Cap nhat gia tri lon nhat
                max_row = i;         // Cap nhat chi so hang
                max_col = j;         // Cap nhat chi so cot
            }
        }
    }

    cout << "Phan tu lon nhat la: " << max_val
              << " tai vi tri [" << max_row << "][" << max_col << "]" << endl;

    return 0;
}

Giải thích code:

  • max_val được khởi tạo với giá trị nhỏ nhất có thể của kiểu int (numeric_limits<int>::min()) để đảm bảo mọi phần tử trong mảng đều lớn hơn giá trị khởi tạo này.
  • max_rowmax_col lưu trữ chỉ số của phần tử lớn nhất. Khởi tạo bằng -1 hoặc bất kỳ giá trị không hợp lệ nào.
  • Trong quá trình duyệt, nếu mang[i][j] lớn hơn max_val hiện tại, chúng ta cập nhật cả max_val, max_row, và max_col.
  • Sau khi duyệt xong, max_val sẽ là giá trị lớn nhất và max_row, max_col là vị trí của nó.

Bài tập tương tự: Bạn có thể dễ dàng biến đổi code này để tìm phần tử nhỏ nhất bằng cách khởi tạo min_val với numeric_limits<int>::max() và sử dụng điều kiện if (mang[i][j] < min_val).

Bài Tập 3: Tính Tổng Các Phần Tử Trên Một Hàng Cụ Thể

Đôi khi bạn chỉ cần xử lý dữ liệu trên một hàng nhất định, không cần duyệt toàn bộ mảng.

#include <iostream>

int main() {
    const int SO_HANG = 3;
    const int SO_COT = 4;
    int mang[SO_HANG][SO_COT] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    const int HANG_CAN_TINH_TONG = 1; // Tinh tong hang thu 2 (chi so 1)

    if (HANG_CAN_TINH_TONG >= 0 && HANG_CAN_TINH_TONG < SO_HANG) {
        int tong_hang = 0;
        // Duyet chi cac phan tu tren hang cu the
        // Vong lap ngoai co chi so hang co dinh
        for (int j = 0; j < SO_COT; ++j) { // Duyet qua tat ca cac cot tren hang do
            tong_hang += mang[HANG_CAN_TINH_TONG][j];
        }
        cout << "Tong cac phan tu tren hang " << HANG_CAN_TINH_TONG << " la: " << tong_hang << endl; // Ket qua 5+6+7+8 = 26
    } else {
        cout << "Chi so hang khong hop le." << endl;
    }

    return 0;
}

Giải thích code:

  • Chúng ta định nghĩa HANG_CAN_TINH_TONG là chỉ số hàng mà chúng ta muốn tính tổng.
  • Thêm kiểm tra if để đảm bảo chỉ số hàng hợp lệ.
  • Bên trong khối if, chúng ta chỉ cần một vòng lặp for (int j = 0; j < SO_COT; ++j) để duyệt qua tất cả các cột (j).
  • Trong vòng lặp này, chỉ số hàng luôn là HANG_CAN_TINH_TONG, còn chỉ số cột là j. Chúng ta truy cập mang[HANG_CAN_TINH_TONG][j].
Bài Tập 4: Tính Tổng Các Phần Tử Trên Một Cột Cụ Thể

Tương tự, bạn có thể tính tổng trên một cột bằng cách cố định chỉ số cột và lặp qua các chỉ số hàng.

#include <iostream>

int main() {
    const int SO_HANG = 3;
    const int SO_COT = 4;
    int mang[SO_HANG][SO_COT] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    const int COT_CAN_TINH_TONG = 0; // Tinh tong cot thu 1 (chi so 0)

    if (COT_CAN_TINH_TONG >= 0 && COT_CAN_TINH_TONG < SO_COT) {
        int tong_cot = 0;
        // Duyet chi cac phan tu tren cot cu the
        // Vong lap ngoai co chi so cot co dinh
        for (int i = 0; i < SO_HANG; ++i) { // Duyet qua tat ca cac hang tren cot do
            tong_cot += mang[i][COT_CAN_TINH_TONG];
        }
        cout << "Tong cac phan tu tren cot " << COT_CAN_TINH_TONG << " la: " << tong_cot << endl; // Ket qua 1+5+9 = 15
    } else {
        cout << "Chi so cot khong hop le." << endl;
    }

    return 0;
}

Giải thích code:

  • Chúng ta định nghĩa COT_CAN_TINH_TONG là chỉ số cột cần xử lý.
  • Thêm kiểm tra if cho chỉ số cột.
  • Bên trong khối if, chúng ta chỉ cần một vòng lặp for (int i = 0; i < SO_HANG; ++i) để duyệt qua tất cả các hàng (i).
  • Trong vòng lặp này, chỉ số cột luôn là COT_CAN_TINH_TONG, còn chỉ số hàng là i. Chúng ta truy cập mang[i][COT_CAN_TINH_TONG].
Bài Tập 5: Duyệt Mảng 2 Chiều Sử Dụng vector<vector<T>>

Trong thực tế, việc sử dụng mảng 2 chiều cố định kích thước (int arr[3][4]) khá hạn chế. vector<vector<T>> là cách phổ biến hơn để làm việc với mảng 2 chiều có kích thước động. Kỹ thuật duyệt về cơ bản vẫn giống hệt, chỉ khác cách lấy kích thước.

#include <iostream>
#include <vector>

int main() {
    // Khai bao va khoi tao vector 2 chieu
    vector<vector<int>> ma_tran = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // Lay kich thuoc
    int so_hang = ma_tran.size(); // So luong vector ben ngoai (hang)
    // Can dam bao ma_tran khong rong truoc khi truy cap hang dau tien
    int so_cot = (so_hang > 0) ? ma_tran[0].size() : 0; // So luong phan tu trong vector dau tien (cot)

    cout << "In ma tran vector 2 chieu:" << endl;

    // Duyet vector 2 chieu
    for (int i = 0; i < so_hang; ++i) { // Duyet qua cac hang
        for (int j = 0; j < so_cot; ++j) { // Duyet qua cac cot trong tung hang
            cout << ma_tran[i][j] << " ";
        }
        cout << endl;
    }

    // Ví dụ tính tổng với vector
    int tong_vector = 0;
     for (int i = 0; i < so_hang; ++i) {
        for (int j = 0; j < so_cot; ++j) {
            tong_vector += ma_tran[i][j];
        }
    }
    cout << "Tong cua tat ca cac phan tu trong ma tran vector la: " << tong_vector << endl;


    return 0;
}

Giải thích code:

  • Chúng ta khai báo và khởi tạo vector<vector<int>> gọi là ma_tran. Cấu trúc khởi tạo cũng tương tự như mảng cố định.
  • Kích thước số hàng được lấy bằng ma_tran.size().
  • Kích thước số cột được lấy bằng ma_tran[0].size(). Chúng ta cần kiểm tra so_hang > 0 để tránh truy cập vào một vector rỗng gây lỗi.
  • Các vòng lặp lồng nhau và cách truy cập phần tử ma_tran[i][j] hoàn toàn giống với mảng cố định. Logic duyệt không thay đổi.
  • Thêm ví dụ tính tổng để minh họa rằng các thao tác khác cũng thực hiện tương tự.

Sử dụng vector mang lại sự linh hoạt hơn nhiều khi bạn không biết chính xác kích thước mảng cần dùng khi viết code.

Tổng Kết Về Duyệt Mảng 2 Chiều

Qua các ví dụ trên, bạn có thể thấy kỹ thuật vòng lặp lồng nhaucốt lõi để duyệt mảng 2 chiều. Bằng cách thay đổi thứ tự các vòng lặp hoặc cố định một chỉ số, bạn có thể tùy biến quá trình duyệt để giải quyết các bài toán khác nhau như:

  • In mảng (theo hàng, theo cột).
  • Tính tổng, trung bình các phần tử.
  • Tìm phần tử lớn nhất/nhỏ nhất.
  • Tính tổng/trung bình trên một hàng hoặc cột cụ thể.
  • Tìm kiếm một giá trị nào đó.
  • Đếm số lần xuất hiện của một giá trị.
  • Thực hiện các phép toán trên ma trận (cộng, trừ...).

Comments

There are no comments at the moment.