Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 10.215 phút đọc

Nhập và xuất ma trận

Sau bài này bạn sẽ làm được

  • Viết vòng lặp lồng nhập xuất ma trận đúng thứ tự
  • Căn cột bằng độ rộng trường trong printf
  • Kiểm tra kết quả trả về của scanf khi nhập ma trận
  • In ma trận kèm chỉ số hàng và cột

Nhập xuất ma trận là công việc bạn lặp lại ở mọi bài còn lại của chương này. Viết cho tử tế một lần rồi dùng lại, và trong lúc viết hãy để ý một chi tiết ảnh hưởng tới tốc độ: thứ tự của hai vòng lặp.

#Vòng lặp lồng chuẩn

for (size_t i = 0; i < hang; ++i)        /* vòng ngoài chạy theo hàng */
    for (size_t j = 0; j < cot; ++j)     /* vòng trong chạy theo cột */
        xu_ly(m[i][j]);

Quy ước gần như tuyệt đối trong C: biến i là chỉ số hàng, biến j là chỉ số cột, vòng ngoài chạy theo i. Giữ đúng quy ước này giúp bạn và người đọc không phải suy nghĩ lại mỗi lần.

LượtijPhần tử được xử lý
100m[0][0]
201m[0][1]
302m[0][2]
410m[1][0]
...

#Nhập ma trận

nhap.c
/* Trả về 0 nếu nhập đủ, âm một nếu dữ liệu vào hỏng. */
int nhap_ma_tran(int *m, size_t hang, size_t cot)
{
    for (size_t i = 0; i < hang; ++i)
        for (size_t j = 0; j < cot; ++j)
            if (scanf("%d", &m[i * cot + j]) != 1) {
                fprintf(stderr,
                        "Loi doc phan tu [%zu][%zu]\n", i, j);
                return -1;
            }

    return 0;
}

Nhập kích thước trước, có kiểm tra biên

nhap-kich-thuoc.c
enum { TOI_DA = 20 };

int main(void)
{
    static int m[TOI_DA][TOI_DA];
    size_t hang, cot;

    printf("So hang va so cot: ");

    if (scanf("%zu %zu", &hang, &cot) != 2) {
        fprintf(stderr, "Can hai so nguyen\n");
        return 1;
    }

    if (hang == 0 || hang > TOI_DA || cot == 0 || cot > TOI_DA) {
        fprintf(stderr, "Kich thuoc phai tu 1 toi %d\n", TOI_DA);
        return 1;
    }

    printf("Nhap %zu phan tu:\n", hang * cot);

    if (nhap_ma_tran(&m[0][0], hang, cot) != 0)
        return 1;

    return 0;
}
an-toan.c
enum { TOI_DA = 20 };

int nhap_ma_tran(int m[][TOI_DA], size_t hang, size_t cot)
{
    for (size_t i = 0; i < hang; ++i)
        for (size_t j = 0; j < cot; ++j)
            if (scanf("%d", &m[i][j]) != 1)
                return -1;

    return 0;
}

#Xuất ma trận có căn cột

Không căn cột
for (size_t i = 0; i < h; ++i) {
    for (size_t j = 0; j < c; ++j)
        printf("%d ", m[i][j]);

    putchar('\n');
}

/* Kết quả xô lệch:
   1 22 333 4
   5555 6 77 8   */
Căn cột bằng độ rộng trường
for (size_t i = 0; i < h; ++i) {
    for (size_t j = 0; j < c; ++j)
        printf("%6d", m[i][j]);

    putchar('\n');
}

/* Kết quả thẳng hàng:
        1    22   333     4
     5555     6    77     8   */

Định dạng %6d căn phải trong đúng sáu ký tự. Bài 3.2 đã trình bày đầy đủ về độ rộng trường. Với ma trận, đây không phải chuyện thẩm mỹ mà là chuyện đọc được kết quả hay không.

Bản in có nhãn hàng và cột

in-co-nhan.c
void in_ma_tran(const int *m, size_t hang, size_t cot, const char *ten)
{
    printf("%s (%zu x %zu)\n", ten, hang, cot);

    /* Dòng tiêu đề với chỉ số cột */
    printf("     ");

    for (size_t j = 0; j < cot; ++j)
        printf("%6zu", j);

    printf("\n");

    for (size_t i = 0; i < hang; ++i) {
        printf("%4zu ", i);

        for (size_t j = 0; j < cot; ++j)
            printf("%6d", m[i * cot + j]);

        putchar('\n');
    }
}
terminal
./in-co-nhan
A (3 x 4)
          0     1     2     3
   0      1     2     3     4
   1      5     6     7     8
   2      9    10    11    12

Xuất ra file bằng cùng một hàm

/* Thay printf bằng fprintf và nhận thêm tham số luồng.
   Cùng một hàm dùng được cho cả màn hình lẫn file. */
void ghi_ma_tran(FILE *f, const int *m, size_t hang, size_t cot)
{
    for (size_t i = 0; i < hang; ++i) {
        for (size_t j = 0; j < cot; ++j)
            fprintf(f, "%d%c", m[i * cot + j], j + 1 < cot ? ' ' : '\n');
    }
}

ghi_ma_tran(stdout, &m[0][0], 3, 4);      /* ra màn hình */

FILE *f = fopen("kq.txt", "w");

if (f != NULL) {
    ghi_ma_tran(f, &m[0][0], 3, 4);       /* ra file */
    fclose(f);
}

#Thứ tự duyệt và tốc độ

Hai đoạn mã sau cho ra kết quả giống hệt nhau nhưng chạy nhanh chậm khác nhau rõ rệt trên ma trận lớn.

thu-tu.c
/* Duyệt theo hàng: đọc bộ nhớ liền mạch */
for (size_t i = 0; i < N; ++i)
    for (size_t j = 0; j < N; ++j)
        tong += m[i][j];

/* Duyệt theo cột: mỗi bước nhảy N * 4 byte */
for (size_t j = 0; j < N; ++j)
    for (size_t i = 0; i < N; ++i)
        tong += m[i][j];
terminal
# Với ma trận 4000 nhân 4000, tức 64 MB
./do-thu-tu
theo hang: 0.021 s
theo cot : 0.183 s
ti le    : 8.7 lan

#Chương trình đầy đủ

ma-tran.c
#include <stdio.h>

enum { TOI_DA = 20 };

static int nhap(int m[][TOI_DA], size_t hang, size_t cot)
{
    for (size_t i = 0; i < hang; ++i)
        for (size_t j = 0; j < cot; ++j)
            if (scanf("%d", &m[i][j]) != 1) {
                fprintf(stderr, "Loi tai [%zu][%zu]\n", i, j);
                return -1;
            }

    return 0;
}

static void xuat(const int m[][TOI_DA], size_t hang, size_t cot)
{
    for (size_t i = 0; i < hang; ++i) {
        for (size_t j = 0; j < cot; ++j)
            printf("%6d", m[i][j]);

        putchar('\n');
    }
}

int main(void)
{
    static int m[TOI_DA][TOI_DA];
    size_t hang, cot;

    if (scanf("%zu %zu", &hang, &cot) != 2
        || hang == 0 || hang > TOI_DA
        || cot  == 0 || cot  > TOI_DA) {
        fprintf(stderr, "Kich thuoc khong hop le\n");
        return 1;
    }

    if (nhap(m, hang, cot) != 0)
        return 1;

    puts("Ma tran vua nhap:");
    xuat(m, hang, cot);

    return 0;
}
terminal
printf '2 3\n1 2 3\n4 5 6\n' | ./ma-tran
Ma tran vua nhap:
     1     2     3
     4     5     6

Chú ý cách dùng printf của trình bao để đưa dữ liệu vào mà không phải gõ tay. Kỹ thuật này tiết kiệm rất nhiều thời gian khi bạn phải chạy đi chạy lại một chương trình cần nhiều dữ liệu vào.

Tự làm thử

  1. Viết chương trình nhập ma trận m nhân n và in lại, có kiểm tra kích thước và kiểm tra kết quả scanf.
  2. Nhập ma trận rồi cố tình gõ một chữ cái ở giữa. Quan sát chương trình xử lý ra sao khi có và không có kiểm tra.
  3. Viết hàm in tự tính độ rộng cột theo giá trị lớn nhất, thử với ma trận chứa cả số âm và số có nhiều chữ số.
  4. Đo thời gian duyệt theo hàng và theo cột trên ma trận 4000 nhân 4000, rồi lặp lại với ma trận 50 nhân 50 và so sánh tỷ lệ.
  5. Viết hàm ghi ma trận ra file và hàm đọc lại từ file, xác nhận dữ liệu khớp nhau.
  6. Viết chương trình đọc ma trận từ file có dòng đầu ghi kích thước, xử lý đủ các trường hợp file thiếu dữ liệ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

  • Quy ước là i chạy theo hàng ở vòng ngoài, j chạy theo cột ở vòng trong.
  • Luôn kiểm tra giá trị trả về của scanf ở mỗi phần tử, vì một ký tự sai làm hỏng cả ma trận mà không báo gì.
  • Khi mảng khai báo lớn hơn kích thước đang dùng, phải tính chỉ số theo số cột khai báo chứ không theo số cột đang dùng.
  • Dùng độ rộng trường như %6d để các cột thẳng hàng, hoặc %*d để lấy độ rộng từ tham số.
  • Duyệt theo hàng nhanh hơn duyệt theo cột nhiều lần trên ma trận lớn, vì bộ nhớ đệm đọc theo khối liền mạch.