Bài 25.2: Duyệt mảng 2 chiều trong C++

Chào mừng trở lại với series học lập trình C++ cùng FullhouseDev! Sau khi đã nắm vững khái niệm mảng 1 chiều và cách duyệt chúng, hôm nay chúng ta sẽ nâng cấp độ khó một chút và tìm hiểu về mảng 2 chiều.

Mảng 2 chiều (hay còn gọi là ma trận trong toán học) là một cấu trúc dữ liệu cực kỳ hữu ích để biểu diễn dữ liệu dưới dạng lưới, bảng, hoặc các đối tượng có chiều ngang và chiều dọc, ví dụ như hình ảnh (pixel), bản đồ (ô), hay bảng tính. Và kỹ năng quan trọng nhất khi làm việc với mảng 2 chiều chính là duyệt (iteration) - truy cập và xử lý từng phần tử trong mảng.

Mảng 2 chiều là gì?

Hãy tưởng tượng mảng 2 chiều như một tấm lưới hoặc một bảng tính Excel. Nó có các hàng (rows) và các cột (columns). Mỗi phần tử được xác định bằng chỉ số hàng và chỉ số cột của nó.

Trong C++, bạn có thể khai báo mảng 2 chiều theo hai cách phổ biến:

  1. Mảng kiểu C cổ điển (fixed size):

    int a[3][4];
    

    Kích thước phải được xác định tại thời điểm biên dịch.

  2. Sử dụng vector<vector<T>>: Linh hoạt hơn, kích thước có thể thay đổi động.

    #include <vector>
    vector<vector<int>> a(3, vector<int>(4));
    

    Cách này thường được ưa chuộng hơn trong C++ hiện đại vì tính linh hoạt.

Kỹ thuật Duyệt Mảng 2 Chiều

Để duyệt qua tất cả các phần tử trong mảng 2 chiều, chúng ta cần một cách để truy cập từng vị trí (hàng, cột). Phương pháp phổ biến và cơ bản nhất chính là sử dụng vòng lặp for lồng nhau.

Phương pháp 1: Vòng Lặp for Lồng Nhau (Index-based)

Đây là cách tiếp cận trực tiếp nhất. Chúng ta sẽ sử dụng:

  • Một vòng lặp for bên ngoài để duyệt qua các hàng.
  • Một vòng lặp for bên trong để duyệt qua các cột cho mỗi hàng hiện tại.

Cấu trúc chung sẽ trông như thế này:

for (int i = 0; i < m; ++i) {
    for (int j = 0; j < n; ++j) {
        // Xu ly a[i][j]
    }
}

Trong đó, i là chỉ số hàng (thường bắt đầu từ 0) và j là chỉ số cột (cũng bắt đầu từ 0). a[i][j] sẽ cho phép bạn truy cập phần tử tại hàng i và cột j.

Ví dụ 1: Duyệt và in Mảng Cổ Điển

Hãy xem xét một ví dụ với mảng cố định kích thước:

#include <iostream>

int main() {
    using namespace std;
    int a[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    cout << "Duyet mang 2 chieu (fixed size) bang nested for loops:\n";

    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j) {
            cout << a[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

Output:

Duyet mang 2 chieu (fixed size) bang nested for loops:
1 2 3 4 
5 6 7 8 
9 10 11 12
Ví dụ 2: Duyệt và in với vector<vector<int>>

Với vector<vector<int>>, chúng ta có thể lấy kích thước hàng và cột một cách linh hoạt bằng phương thức .size().

#include <iostream>
#include <vector>

int main() {
    using namespace std;
    vector<vector<int>> a = {
        {10, 20, 30},
        {40, 50, 60},
        {70, 80, 90},
        {100, 110, 120}
    };

    int m = a.size();
    int n = a[0].size();

    cout << "Duyet mang 2 chieu (vector<vector>) bang nested for loops:\n";

    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            cout << a[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

Output:

Duyet mang 2 chieu (vector<vector>) bang nested for loops:
10 20 30 
40 50 60 
70 80 90 
100 110 120
Phương pháp 2: Vòng Lặp range-based for Lồng Nhau (chỉ với vector<vector<T>>)

Với vector<vector<T>>, bạn có thể sử dụng cú pháp range-based for của C++11 trở lên để duyệt mảng. Cách này trừu tượng hơn chỉ số i, j, nhưng có thể gọn gàng hơn khi bạn chỉ cần truy cập giá trị của phần tử mà không cần biết vị trí chính xác của nó.

  • Vòng lặp range-based for bên ngoài sẽ duyệt qua từng vector (hàng) trong vector<vector<T>> lớn.
  • Vòng lặp range-based for bên trong sẽ duyệt qua từng phần tử trong vector (hàng) hiện tại.
for (const auto& dong : a) {
    for (int pt : dong) {
        // Xu ly pt
    }
}
Ví dụ 3: Duyệt và in với Range-Based For
#include <iostream>
#include <vector>

int main() {
    using namespace std;
    vector<vector<int>> a = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    cout << "Duyet mang 2 chieu (vector<vector>) bang nested range-based for loops:\n";

    for (const auto& dong : a) {
        for (int pt : dong) {
            cout << pt << " ";
        }
        cout << endl;
    }
    return 0;
}

Output:

Duyet mang 2 chieu (vector<vector>) bang nested range-based for loops:
1 2 3 
4 5 6 
7 8 9

Các Ví Dụ Thực Tế Hơn Khi Duyệt Mảng 2 Chiều

Vòng lặp lồng nhau không chỉ để in mảng. Chúng cho phép bạn thực hiện mọi thao tác với từng phần tử. Dưới đây là một vài ví dụ khác:

Ví dụ 4: Tính Tổng Tất Cả Các Phần Tử
#include <iostream>
#include <vector>

int main() {
    using namespace std;
    vector<vector<int>> a = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    long long tong = 0;

    cout << "\nTinh tong cac phan tu trong mang:\n";

    for (int i = 0; i < a.size(); ++i) {
        for (int j = 0; j < a[i].size(); ++j) {
            tong += a[i][j];
        }
    }

    cout << "Tong tat ca cac phan tu la: " << tong << endl;
    return 0;
}

Output:

Tinh tong cac phan tu trong mang:
Tong tat ca cac phan tu la: 45
Ví dụ 5: Tìm Kiếm Một Phần Tử Cụ Thể
#include <iostream>
#include <vector>

int main() {
    using namespace std;
    vector<vector<int>> a = {
        {10, 20, 30},
        {40, 50, 60},
        {70, 80, 90}
    };

    int x = 50;
    bool timThay = false;
    int dong = -1, cot = -1;

    cout << "\nTim kiem phan tu " << x << " trong mang:\n";

    for (int i = 0; i < a.size(); ++i) {
        for (int j = 0; j < a[i].size(); ++j) {
            if (a[i][j] == x) {
                timThay = true;
                dong = i;
                cot = j;
            }
        }
    }

    if (timThay) {
        cout << "Tim thay phan tu " << x << " tai vi tri [" << dong << "][" << cot << "]" << endl;
    } else {
        cout << "Khong tim thay phan tu " << x << " trong mang." << endl;
    }
    return 0;
}

Output:

Tim kiem phan tu 50 trong mang:
Tim thay phan tu 50 tai vi tri [1][1]
Ví dụ 6: Khởi Tạo Mảng với Giá Trị Dựa Trên Vị Trí

Bạn cũng có thể dùng vòng lặp lồng nhau để gán giá trị cho mảng, ví dụ như điền số từ 0 đến nm-1, hoặc giá trị dựa trên công thức `i10 + j`.

#include <iostream>
#include <vector>

int main() {
    using namespace std;
    int m = 5;
    int n = 6;

    vector<vector<int>> a(m, vector<int>(n));

    cout << "\nKhoi tao mang voi gia tri dua tren vi tri (i, j) va in ra:\n";

    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            a[i][j] = i * 10 + j;
        }
    }

    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            cout << a[i][j] << "\t";
        }
        cout << endl;
    }
    return 0;
}

Output:

Khoi tao mang voi gia tri dua tren vi tri (i, j) va in ra:
0   1   2   3   4   5   
10  11  12  13  14  15  
20  21  22  23  24  25  
30  31  32  33  34  35  
40  41  42  43  44  45

Khi Nào Sử Dụng Cách Nào?

  • Sử dụng vòng lặp for lồng nhau với chỉ số (int i, int j) khi bạn cần truy cập phần tử dựa trên vị trí chính xác của nó (hàng i, cột j) hoặc khi bạn làm việc với mảng C cố định kích thước. Đây là phương pháp linh hoạt nhấtmạnh mẽ nhất cho mảng 2 chiều.
  • Sử dụng vòng lặp range-based for lồng nhau khi bạn làm việc với vector<vector<T>> và chỉ cần truy cập giá trị của từng phần tử mà không quan tâm đến chỉ số i, j của nó. Cách này giúp code gọn gàng và dễ đọc hơn trong trường hợp đơn giản.

Những Điểm Cần Lưu Ý

  • Luôn nhớ kích thước của mảng (số hàng và số cột) khi sử dụng vòng lặp index-based để tránh truy cập ngoài giới hạn mảng (out-of-bounds access), dẫn đến lỗi runtime nghiêm trọng. Với vector, luôn sử dụng .size() để lấy kích thước động.
  • Thứ tự duyệt thông thường là duyệt hết các cột của một hàng rồi mới sang hàng tiếp theo (row-major order). Thứ tự này tương ứng với cách mảng 2 chiều thường được lưu trữ trong bộ nhớ và thường là hiệu quả nhất.

Bài tập ví dụ: C++ Bài 11.A2: Số lớn nhất trong ma trận

Viết chương trình nhập vào ma trận số nguyên gồm \(n\) dòng \(n\) cột. Tìm và thông báo số lớn nhất trên mỗi hàng..

INPUT FORMAT

Dòng đầu là hai số nguyên dương \(n\) \((1 \leq n \leq 1000)\)

Các dòng tiếp theo là ma trận số nguyên \(A\).

OUTPUT FORMAT

In ra \(n\) dòng, mỗi dòng là số lớn nhất trên dòng đó.

Ví dụ:

Input
3
1 3 4
8 4 2
4 2 8
Output
4
8
8
Giải thích ví dụ mẫu
Tìm giá trị lớn nhất trên mỗi hàng của ma trận và in ra kết quả.

Ý tưởng chính:

Bài toán yêu cầu xử lý từng hàng của ma trận một cách độc lập để tìm giá trị lớn nhất. Vì vậy, chúng ta sẽ lặp qua từng hàng của ma trận, và với mỗi hàng, chúng ta sẽ tìm giá trị lớn nhất trong hàng đó rồi in ra.

Các bước thực hiện:

  1. Đọc kích thước ma trận:

    • Đầu tiên, bạn cần đọc số nguyên n từ đầu vào để biết kích thước của ma trận (n dòng, n cột).
  2. Xử lý từng hàng:

    • Sử dụng một vòng lặp để duyệt qua từng dòng của ma trận, từ dòng 0 đến dòng n-1.
    • Bên trong vòng lặp này, bạn sẽ xử lý cho một dòng hiện tại.
  3. Đọc và tìm giá trị lớn nhất trên một hàng:

    • Đối với dòng hiện tại, bạn cần đọc n số nguyên là các phần tử của dòng đó.
    • Có nhiều cách để tìm giá trị lớn nhất trên hàng này:
      • Cách thủ công: Khởi tạo một biến max_val với giá trị của phần tử đầu tiên trên hàng. Sau đó, duyệt qua các phần tử còn lại trên hàng, so sánh từng phần tử với max_val. Nếu phần tử hiện tại lớn hơn max_val, cập nhật max_val.
      • Sử dụng max_element: Đây là cách C++ hiện đại sử dụng thư viện chuẩn và thường ngắn gọn hơn. Bạn có thể đọc các phần tử của dòng hiện tại vào một container như vector<int>. Sau đó, sử dụng hàm max_element (có trong header <algorithm>) để tìm con trỏ (hoặc iterator) tới phần tử lớn nhất trong vector đó. Hàm này nhận vào hai iterator chỉ phạm vi cần tìm (ví dụ: vector.begin()vector.end()). Giá trị lớn nhất chính là giá trị mà iterator trả về trỏ tới (dùng toán tử * để lấy giá trị).
  4. In kết quả:

    • Sau khi đã tìm được giá trị lớn nhất (max_val) của dòng hiện tại, bạn cần in giá trị này ra màn hình.
    • Mỗi giá trị lớn nhất của mỗi dòng nên được in trên một dòng riêng, theo đúng định dạng đầu ra yêu cầu.
  5. Bao gồm các thư viện cần thiết:

    • Bạn sẽ cần <iostream> cho việc nhập/xuất dữ liệu.
    • Nếu sử dụng vector, bạn cần <vector>.
    • Nếu sử dụng max_element, bạn cần <algorithm>.

Gợi ý về cấu trúc code (không phải code hoàn chỉnh):

#include <iostream>
#include <limits>

int main() {
    using namespace std;
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    for (int i = 0; i < n; ++i) {
        int mx = numeric_limits<int>::min();
        for (int j = 0; j < n; ++j) {
            int g;
            cin >> g;
            if (g > mx) {
                mx = g;
            }
        }
        cout << mx << endl;
    }

    return 0;
}

Output cho ví dụ mẫu:

4
8
8

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.