Bài 16.3: Thuật toán sắp xếp nổi bọt trong C++

Chào mừng các bạn quay trở lại với chuỗi bài viết về C++! Hôm nay, chúng ta sẽ cùng nhau đi sâu vào một trong những thuật toán sắp xếp đơn giản nhất nhưng cũng quan trọng nhất mà mọi lập trình viên cần biết: Thuật toán sắp xếp nổi bọt (hay còn gọi là Bubble Sort).

Dù không phải là lựa chọn hiệu quả nhất cho dữ liệu lớn, nhưng sự đơn giản của Bubble Sort làm cho nó trở thành một công cụ tuyệt vời để hiểu cách các thuật toán sắp xếp hoạt động. Hãy cùng khám phá!

Sắp xếp là gì và tại sao nó quan trọng?

Trong thế giới lập trình, việc tổ chức dữ liệu là vô cùng cần thiết. Khi dữ liệu được sắp xếp theo một thứ tự nhất định (tăng dần, giảm dần, theo chữ cái, v.v.), việc tìm kiếm, phân tích và xử lý chúng trở nên nhanh chóngdễ dàng hơn rất nhiều. Tưởng tượng bạn tìm một từ trong cuốn từ điển được sắp xếp theo thứ tự ABC, nhanh hơn rất nhiều so với một cuốn từ điển mà các từ được xếp lộn xộn phải không? Đó chính là sức mạnh của sắp xếp!

Thuật toán "Nổi Bọt" là gì?

Thuật toán sắp xếp nổi bọt là một thuật toán so sánh dựa trên việc lặp đi lặp lại việc so sánh hai phần tử liền kềhoán đổi chúng nếu chúng không đúng thứ tự. Quá trình này được lặp lại nhiều lần cho đến khi toàn bộ danh sách được sắp xếp.

Tại sao lại gọi là "Nổi Bọt"? Bởi vì qua mỗi lần "duyệt" qua danh sách, các phần tử lớn hơn sẽ dần dần "nổi" lên vị trí cuối cùng của chúng trong phần danh sách chưa được sắp xếp, giống như những bọt khí lớn hơn nổi lên mặt nước vậy!

Nguyên lý hoạt động: Từ từ "Nổi Bọt" lên đỉnh

Để hiểu rõ hơn, hãy xem nguyên lý hoạt động của Bubble Sort từng bước một:

  1. Bắt đầu từ đầu danh sách.
  2. So sánh phần tử hiện tại với phần tử ngay bên cạnh.
  3. Nếu phần tử hiện tại lớn hơn phần tử kế tiếp (đối với sắp xếp tăng dần), hoán đổi vị trí của chúng.
  4. Di chuyển sang cặp phần tử kế tiếp và lặp lại bước 2 và 3 cho đến khi duyệt hết danh sách.
  5. Sau khi kết thúc một lượt duyệt (một "pass") toàn bộ danh sách, phần tử lớn nhất trong lượt duyệt đó sẽ nằm ở vị trí cuối cùng của phần chưa sắp xếp.
  6. Lặp lại toàn bộ quá trình (từ bước 1) cho phần còn lại của danh sách (trừ các phần tử đã được đưa về đúng vị trí ở cuối).
  7. Quá trình kết thúc khi không còn cặp nào cần hoán đổi trong một lượt duyệt, hoặc khi đã lặp lại n-1 lượt duyệt (với n là số lượng phần tử).

Ví dụ minh họa "bằng tay"

Hãy xem một ví dụ nhỏ với mảng [5, 1, 4, 2, 8] để sắp xếp tăng dần:

Mảng ban đầu: [5, 1, 4, 2, 8]

Lượt duyệt 1 (Pass 1):

  • So sánh (5, 1): 5 > 1, hoán đổi -> [1, 5, 4, 2, 8]
  • So sánh (5, 4): 5 > 4, hoán đổi -> [1, 4, 5, 2, 8]
  • So sánh (5, 2): 5 > 2, hoán đổi -> [1, 4, 2, 5, 8]
  • So sánh (5, 8): 5 < 8, không hoán đổi -> [1, 4, 2, 5, 8] Kết thúc Pass 1: [1, 4, 2, 5, 8]. Phần tử lớn nhất (8) đã "nổi" về cuối.

Lượt duyệt 2 (Pass 2): (Không cần xét số 8 nữa)

  • So sánh (1, 4): 1 < 4, không hoán đổi -> [1, 4, 2, 5, 8]
  • So sánh (4, 2): 4 > 2, hoán đổi -> [1, 2, 4, 5, 8]
  • So sánh (4, 5): 4 < 5, không hoán đổi -> [1, 2, 4, 5, 8] Kết thúc Pass 2: [1, 2, 4, 5, 8]. Phần tử lớn nhất trong phần còn lại (5) đã "nổi" về đúng vị trí.

Lượt duyệt 3 (Pass 3): (Không cần xét số 5 và 8 nữa)

  • So sánh (1, 2): 1 < 2, không hoán đổi -> [1, 2, 4, 5, 8]
  • So sánh (2, 4): 2 < 4, không hoán đổi -> [1, 2, 4, 5, 8] Kết thúc Pass 3: [1, 2, 4, 5, 8]. Phần tử lớn nhất trong phần còn lại (4) đã "nổi".

Lượt duyệt 4 (Pass 4): (Không cần xét số 4, 5 và 8 nữa)

  • So sánh (1, 2): 1 < 2, không hoán đổi -> [1, 2, 4, 5, 8] Kết thúc Pass 4: [1, 2, 4, 5, 8].

Mảng đã được sắp xếp! Notice rằng sau Pass 3, mảng đã thực sự được sắp xếp, nhưng thuật toán cơ bản vẫn cần thêm 1 Pass nữa để chắc chắn hoặc đến khi đủ n-1 Pass.

Cài đặt cơ bản trong C++

Bây giờ, chúng ta hãy viết code C++ cho thuật toán Bubble Sort cơ bản. Chúng ta sẽ sử dụng vector để lưu trữ dữ liệu và swap để hoán đổi hai phần tử.

#include <iostream>
#include <vector>
#include <algorithm> // Bao gồm swap

// Hàm sắp xếp nổi bọt cơ bản cho vector
void bubbleSortBasic(vector<int>& arr) {
    // Lấy kích thước của vector
    int n = arr.size();

    // Vòng lặp ngoài: Điều khiển số lượt duyệt (pass)
    // Chúng ta cần tối đa n-1 lượt duyệt để sắp xếp n phần tử
    for (int i = 0; i < n - 1; ++i) {
        // Vòng lặp trong: So sánh và hoán đổi các cặp phần tử kề nhau
        // Sau mỗi lượt duyệt i, i phần tử cuối cùng đã ở đúng vị trí.
        // Nên chúng ta chỉ cần duyệt đến n - 1 - i.
        for (int j = 0; j < n - 1 - i; ++j) {
            // So sánh phần tử hiện tại với phần tử kế tiếp
            if (arr[j] > arr[j + 1]) {
                // Nếu phần tử hiện tại lớn hơn phần tử kế tiếp (sai thứ tự),
                // thực hiện hoán đổi vị trí của chúng.
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}

// Hàm trợ giúp để in vector
void printVector(const vector<int>& arr) {
    for (int x : arr) {
        cout << x << " ";
    }
    cout << endl;
}

/*
// Cách sử dụng trong hàm main (ví dụ ngắn gọn)
int main() {
    vector<int> myVector = {64, 34, 25, 12, 22, 11, 90};
    cout << "Vector ban dau: ";
    printVector(myVector);

    bubbleSortBasic(myVector);

    cout << "Vector sau khi sap xep (co ban): ";
    printVector(myVector);

    return 0;
}
*/

Giải thích code:

  • Chúng ta có một hàm bubbleSortBasic nhận vào một vector<int>& arr (tham chiếu đến vector để có thể sửa đổi trực tiếp).
  • int n = arr.size(); lấy số lượng phần tử trong vector.
  • Vòng lặp ngoài (for (int i = 0; i < n - 1; ++i)): Vòng lặp này chạy n-1 lần. Biến i đếm số phần tử đã được đưa về đúng vị trí cuối cùng. Sau i lượt duyệt, i phần tử cuối cùng của mảng đã sắp xếp sẽ nằm ở đúng vị trí của chúng.
  • Vòng lặp trong (for (int j = 0; j < n - 1 - i; ++j)): Vòng lặp này thực hiện việc so sánh và hoán đổi các cặp liền kề. Nó chạy từ đầu mảng đến vị trí n - 1 - i. n - 1 là chỉ số cuối cùng của mảng. n - 1 - i là chỉ số cuối cùng của phần mảng chưa được sắp xếp.
  • if (arr[j] > arr[j + 1]): So sánh phần tử hiện tại (arr[j]) với phần tử kế tiếp (arr[j + 1]). Nếu điều kiện đúng (phần tử hiện tại lớn hơn, trong trường hợp sắp xếp tăng dần), chúng ta cần hoán đổi chúng.
  • swap(arr[j], arr[j + 1]);: Sử dụng hàm swap có sẵn trong thư viện <algorithm> để hoán đổi giá trị của hai biến.

Cải tiến hiệu suất: Dừng sớm khi mảng đã sắp xếp

Một điểm yếu của cài đặt cơ bản là nó luôn chạy đủ n-1 lượt duyệt, ngay cả khi mảng đã được sắp xếp từ sớm. Chúng ta có thể cải thiện điều này bằng cách thêm một cờ (flag) để kiểm tra xem có bất kỳ hoán đổi nào xảy ra trong một lượt duyệt hay không. Nếu không có hoán đổi nào, điều đó có nghĩa là mảng đã được sắp xếp và chúng ta có thể dừng thuật toán sớm.

#include <iostream>
#include <vector>
#include <algorithm> // Bao gồm swap

// Hàm sắp xếp nổi bọt được tối ưu
void bubbleSortOptimized(vector<int>& arr) {
    int n = arr.size();
    bool swapped; // Cờ để kiểm tra xem có hoán đổi nào xảy ra không

    // Vòng lặp ngoài: Điều khiển số lượt duyệt
    for (int i = 0; i < n - 1; ++i) {
        swapped = false; // Giả định không có hoán đổi nào trong lượt duyệt này

        // Vòng lặp trong: So sánh và hoán đổi các cặp
        // Duyệt đến n - 1 - i cho phần chưa sắp xếp
        for (int j = 0; j < n - 1 - i; ++j) {
            if (arr[j] > arr[j + 1]) {
                // Hoán đổi nếu sai thứ tự
                swap(arr[j], arr[j + 1]);
                swapped = true; // Đặt cờ báo hiệu có hoán đổi
            }
        }

        // Nếu sau khi hoàn thành vòng lặp trong mà không có bất kỳ hoán đổi nào,
        // điều đó có nghĩa là mảng đã được sắp xếp.
        // Chúng ta có thể dừng thuật toán sớm.
        if (swapped == false) {
            break;
        }
    }
}

/*
// Hàm trợ giúp để in vector (giống như trên)
void printVector(const vector<int>& arr) {
    for (int x : arr) {
        cout << x << " ";
    }
    cout << endl;
}

// Cách sử dụng trong hàm main (ví dụ ngắn gọn)
int main() {
    vector<int> myVector = {64, 34, 25, 12, 22, 11, 90};
    cout << "Vector ban dau: ";
    printVector(myVector);

    bubbleSortOptimized(myVector);

    cout << "Vector sau khi sap xep (toi uu): ";
    printVector(myVector);

    // Ví dụ với mảng đã sắp xếp để thấy optimization
    vector<int> sortedVector = {1, 2, 3, 4, 5};
    cout << "Vector da sap xep ban dau: ";
    printVector(sortedVector);
    cout << "Sap xep lai vector da sap xep: ";
    bubbleSortOptimized(sortedVector); // Vòng lặp ngoài sẽ chỉ chạy 1 lần rồi break
    printVector(sortedVector);

    return 0;
}
*/

Giải thích cải tiến:

  • Chúng ta thêm một biến bool swapped được khởi tạo là false ở đầu mỗi lượt duyệt ngoài.
  • Bất cứ khi nào một hoán đổi xảy ra trong vòng lặp trong, chúng ta đặt swapped = true.
  • Sau khi vòng lặp trong kết thúc, chúng ta kiểm tra giá trị của swapped. Nếu swapped vẫn là false, tức là không có cặp nào cần hoán đổi trong lượt duyệt này, điều đó chứng tỏ mảng đã hoàn toàn được sắp xếp và chúng ta dùng lệnh break để thoát khỏi vòng lặp ngoài.

Cải tiến này giúp thuật toán chạy nhanh hơn đáng kể đối với các mảng gần như đã được sắp xếp hoặc đã được sắp xếp.

Đánh giá hiệu quả: Đơn giản nhưng chậm chạp

Thuật toán Bubble Sort rất dễ hiểu và dễ cài đặt, nhưng hiệu suất của nó không cao, đặc biệt là với các tập dữ liệu lớn.

  • Độ phức tạp thời gian (Time Complexity):
    • Trường hợp xấu nhất (ví dụ: mảng xếp ngược): O(n^2). Mỗi phần tử cần "nổi" qua gần hết danh sách.
    • Trường hợp trung bình: O(n^2).
    • Trường hợp tốt nhất (với cải tiến dừng sớm, mảng đã sắp xếp): O(n). Chỉ cần một lượt duyệt để kiểm tra.
  • Độ phức tạp không gian (Space Complexity): O(1). Thuật toán chỉ cần một lượng không gian cố định (cho các biến i, j, n, swapped, và không gian stack cho hàm) mà không phụ thuộc vào kích thước của mảng. Nó thực hiện sắp xếp "tại chỗ" (in-place).

Do độ phức tạp thời gian O(n^2) trong hầu hết các trường hợp, Bubble Sort trở nên không thực tế cho việc sắp xếp các tập dữ liệu lớn (hàng nghìn hoặc hàng triệu phần tử). Các thuật toán khác như Quick Sort, Merge Sort, hoặc Heap Sort có độ phức tạp thời gian trung bình là O(n log n) sẽ hiệu quả hơn nhiều.

Khi nào nên dùng Bubble Sort?

Mặc dù không hiệu quả, Bubble Sort vẫn có chỗ đứng:

  • Mục đích giáo dục: Đây là một trong những thuật toán sắp xếp dễ hình dung và hiểu nhất, rất tốt để học về nguyên lý sắp xếp.
  • Tập dữ liệu rất nhỏ: Đối với mảng chỉ có vài chục phần tử, sự khác biệt về hiệu suất giữa Bubble Sort và các thuật toán phức tạp hơn là không đáng kể, và sự đơn giản của Bubble Sort có thể là một lợi thế.
  • Mảng gần như đã sắp xếp: Với phiên bản tối ưu, Bubble Sort có thể rất nhanh nếu mảng chỉ cần vài hoán đổi để sắp xếp hoàn chỉnh.

Ví dụ đầy đủ: Đưa thuật toán vào hoạt động

Hãy cùng xem một chương trình C++ hoàn chỉnh sử dụng phiên bản tối ưu của Bubble Sort:

#include <iostream>
#include <vector>
#include <algorithm> // Dùng cho swap
#include <chrono>    // Để đo thời gian (ví dụ)
#include <random>    // Để tạo dữ liệu ngẫu nhiên (ví dụ)

// Hàm sắp xếp nổi bọt được tối ưu (như đã giải thích ở trên)
void bubbleSortOptimized(vector<int>& arr) {
    int n = arr.size();
    bool swapped;
    for (int i = 0; i < n - 1; ++i) {
        swapped = false;
        for (int j = 0; j < n - 1 - i; ++j) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
                swapped = true;
            }
        }
        if (swapped == false) {
            break;
        }
    }
}

// Hàm trợ giúp để in vector
void printVector(const vector<int>& arr) {
    // In tối đa 20 phần tử để tránh output quá dài
    int count = 0;
    for (int x : arr) {
        if (count >= 20) {
            cout << "...";
            break;
        }
        cout << x << " ";
        count++;
    }
    cout << endl;
}

int main() {
    // Ví dụ 1: Mảng ngẫu nhiên nhỏ
    vector<int> data1 = {5, 1, 4, 2, 8, 3, 7, 6};
    cout << "--- Ví dụ 1: Mang ngau nhien nho ---" << endl;
    cout << "Mang ban dau: ";
    printVector(data1);
    bubbleSortOptimized(data1);
    cout << "Mang sau khi sap xep: ";
    printVector(data1);
    cout << endl;

    // Ví dụ 2: Mang da sap xep san (kiem tra optimization)
    vector<int> data2 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    cout << "--- Ví dụ 2: Mang da sap xep san ---" << endl;
    cout << "Mang ban dau: ";
    printVector(data2);
    // Do data2 đã sắp xếp, optimization sẽ dừng sớm sau 1 pass
    bubbleSortOptimized(data2);
    cout << "Mang sau khi sap xep: ";
    printVector(data2);
    cout << endl;

    // Ví dụ 3: Mang xep nguoc (truong hop xau nhat)
    vector<int> data3 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    cout << "--- Ví dụ 3: Mang xep nguoc ---" << endl;
    cout << "Mang ban dau: ";
    printVector(data3);
    bubbleSortOptimized(data3);
    cout << "Mang sau khi sap xep: ";
    printVector(data3);
    cout << endl;

    // Ví dụ 4: Mang co phan tu trung lap
    vector<int> data4 = {5, 2, 8, 2, 5, 1, 8, 10, 5};
    cout << "--- Ví dụ 4: Mang co phan tu trung lap ---" << endl;
    cout << "Mang ban dau: ";
    printVector(data4);
    bubbleSortOptimized(data4);
    cout << "Mang sau khi sap xep: ";
    printVector(data4);
    cout << endl;

    /*
    // Ví dụ 5: Mang lon (canh bao: se rat cham!)
    // Uncomment dong nay de chay, nhung nen can nhac kich thuoc
    const int LARGE_SIZE = 5000; // Thu nghiem voi kich thuoc lon hon co the ton rat nhieu thoi gian
    vector<int> large_data(LARGE_SIZE);
    // Tao du lieu ngau nhien cho mang lon
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    uniform_int_distribution<int> dist(1, LARGE_SIZE * 2);
    for (int i = 0; i < LARGE_SIZE; ++i) {
        large_data[i] = dist(rng);
    }

    cout << "--- Ví dụ 5: Mang lon (" << LARGE_SIZE << " phan tu) ---" << endl;
    cout << "Bat dau sap xep mang lon..." << endl;
    auto start_time = chrono::high_resolution_clock::now();
    bubbleSortOptimized(large_data);
    auto end_time = chrono::high_resolution_clock::now();
    chrono::duration<double> elapsed = end_time - start_time;
    cout << "Ket thuc sap xep mang lon." << endl;
    cout << "Thoi gian thuc hien: " << elapsed.count() << " giay" << endl;
    cout << "Mang lon sau khi sap xep (chi in vai phan tu dau): ";
    printVector(large_data);
    cout << endl;
    */

    return 0;
}

Trong phần main này:

  • Chúng ta định nghĩa và khởi tạo các vector với các dữ liệu khác nhau: ngẫu nhiên, đã sắp xếp, xếp ngược, có phần tử trùng lặp.
  • Sử dụng hàm printVector để hiển thị trạng thái của vector trước và sau khi sắp xếp.
  • Gọi hàm bubbleSortOptimized để thực hiện việc sắp xếp.
  • Ví dụ 5 được comment lại để tránh chương trình chạy quá lâu một cách không mong muốn khi bạn mới bắt đầu. Nếu muốn thử nghiệm hiệu năng với dữ liệu lớn, hãy bỏ comment và chạy thử (nhưng hãy chuẩn bị tinh thần chờ đợi!). Chúng ta cũng có thêm code để đo thời gian thực thi cơ bản cho ví dụ lớn này, sử dụng thư viện <chrono>.

Bài tập ví dụ: C++ Bài 9.B3: Mua sắm

Kabasak1 đến cửa hàng để mua sắm. Có tổng cộng \(n\) mặt hàng trong cửa hàng và giá trị cửa các mặt hàng lần lượt là \(a_1, a_2,... a_n\) đồng. Kasabak1 có tổng cộng \(m\) đồng, anh ấy hy vọng sẽ dùng số tiền đó để mua được nhiều vật phẩm nhất có thể.

Yêu cầu: Hãy tính số vật phẩm tối đa anh ấy có thể mua được.

INPUT FORMAT

Dòng thứ nhất chứa hai số nguyên \(n\) và \(m\) \((1\leq n\leq 10^4; 1\leq m\leq 2\times 10^4)\) - số mặt hàng và số tiền.

Dòng thứ hai chứa \(n\) số nguyên \(a_1, a_2,..., a_n\) \((1\leq a_i\leq 100)\) - giá trị của các mặt hàng.

OUTPUT FORMAT

Ghi ra một số nguyên là số mặt hàng tối đa mà Kasabak1 có thể mua được.

Ví dụ:

Input
5 9 
1 3 1 3 3
Output
4

<br>

1. Phân tích bài toán:

  • Ta có một ngân sách cố định (m) và một danh sách các mặt hàng với giá khác nhau (a_i).
  • Mục tiêu là mua được số lượng mặt hàng nhiều nhất có thể với số tiền m.
  • Giá trị của các mặt hàng là dương.

2. Lựa chọn thuật toán/ý tưởng:

Để mua được nhiều mặt hàng nhất với một ngân sách giới hạn, cách tốt nhất là ưu tiên mua những mặt hàng có giá thấp nhất trước. Đây là một bài toán điển hình có thể giải quyết bằng thuật toán tham lam (greedy algorithm).

Lý do là, bằng cách mua mặt hàng rẻ nhất, ta sử dụng một phần nhỏ nhất của ngân sách cho một mặt hàng, để dành được nhiều tiền hơn cho các mặt hàng tiếp theo, từ đó có cơ hội mua được nhiều mặt hàng hơn về tổng thể.

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

  • Bước 1: Đọc dữ liệu. Đọc số lượng mặt hàng n và số tiền m. Sau đó đọc n giá của các mặt hàng.
  • Bước 2: Sắp xếp giá. Để dễ dàng chọn ra các mặt hàng rẻ nhất, ta cần sắp xếp danh sách giá các mặt hàng theo thứ tự tăng dần.
  • Bước 3: Mua sắm (tham lam). Duyệt qua danh sách giá đã được sắp xếp từ thấp đến cao. Với mỗi mặt hàng, kiểm tra xem ta có đủ tiền để mua nó không.
    • Nếu có đủ tiền: Mua mặt hàng đó (trừ giá của nó khỏi ngân sách) và tăng số lượng mặt hàng đã mua lên 1.
    • Nếu không đủ tiền: Dừng quá trình mua sắm lại, vì các mặt hàng tiếp theo sẽ chỉ có giá bằng hoặc cao hơn, ta cũng không thể mua được chúng.
  • Bước 4: Kết quả. Số lượng mặt hàng đã mua chính là kết quả cần tìm.

4. Sử dụng thư viện chuẩn C++ (std):

  • Để đọc dữ liệu, bạn sẽ sử dụng cin.
  • Để lưu trữ danh sách giá các mặt hàng, vector<int> là lựa chọn phù hợp vì kích thước n có thể lên tới 10^4.
  • Để sắp xếp vector giá, bạn sẽ sử dụng hàm sort từ thư viện <algorithm>.
  • Để in kết quả, bạn sẽ sử dụng cout.

5. Hướng dẫn cấu trúc code (không cung cấp code hoàn chỉnh):

  1. Include các header cần thiết: <iostream>, <vector>, <algorithm>.
  2. Trong hàm main:
    • Khai báo biến n, m.
    • Đọc nm từ input.
    • Khai báo vector<int> prices(n);.
    • Đọc n giá trị vào vector prices.
    • Gọi sort để sắp xếp vector prices.
    • Khai báo biến đếm số mặt hàng đã mua (ví dụ: int items_bought = 0;).
    • Sử dụng một vòng lặp để duyệt qua các phần tử của vector prices đã sắp xếp.
    • Bên trong vòng lặp, kiểm tra điều kiện mua (giá hiện tại <= số tiền còn lại).
    • Cập nhật số tiền còn lại và số lượng mặt hàng đã mua nếu mua được.
    • Thoát khỏi vòng lặp khi không đủ tiền mua mặt hàng hiện tại.
    • In kết quả (items_bought).

Lưu ý: Kích thước n và giá a_i không quá lớn, nên việc sắp xếp vector có kích thước 10^4 là hoàn toàn khả thi về mặt thời gian. Ngân sách m và tổng giá tiền cũng nằm trong giới hạn của kiểu dữ liệu int.

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

Comments

There are no comments at the moment.