Bài 30.2: Bài tập thực hành chuẩn hóa chuỗi trong C++

Trong thế giới lập trình, việc làm việc với dữ liệu chuỗi là vô cùng phổ biến. Tuy nhiên, dữ liệu chuỗi chúng ta nhận được thường không "sạch sẽ": có thể có khoảng trắng thừa, ký tự đặc biệt không mong muốn, hoặc sự khác biệt về chữ hoa/chữ thường. Đây chính là lúc chuẩn hóa chuỗi (string normalization) trở nên cần thiết.

Chuẩn hóa chuỗi là quá trình biến đổi một chuỗi về một định dạng nhất quán, loại bỏ sự "nhiễu" để dữ liệu dễ dàng được xử lý, so sánh và lưu trữ hơn. Hãy tưởng tượng bạn cần so sánh tên sản phẩm hoặc địa chỉ nhập từ nhiều nguồn khác nhau. Nếu không chuẩn hóa, " Apple ", "apple", "APPLE.", và "apple" sẽ bị coi là khác nhau, dẫn đến kết quả sai lệch trong tìm kiếm hoặc phân tích.

Bài viết này không chỉ giới thiệu khái niệm, mà sẽ đưa bạn đi sâu vào thực hành các kỹ thuật chuẩn hóa chuỗi phổ biến nhất trong C++ thông qua các bài tập cụ thể. Hãy cùng nhau "làm sạch" dữ liệu chuỗi của chúng ta!

Các Tác Vụ Chuẩn Hóa Chuỗi Phổ Biến

Trước khi bắt tay vào code, hãy điểm qua một số tác vụ chuẩn hóa chuỗi mà chúng ta sẽ thực hành:

  1. Xóa khoảng trắng thừa ở đầu và cuối chuỗi: Khoảng trắng (spaces, tabs, newlines) không có ý nghĩa ở hai đầu chuỗi.
  2. Xóa khoảng trắng thừa giữa các từ: Biến đổi nhiều khoảng trắng liên tiếp thành chỉ một khoảng trắng duy nhất.
  3. Chuyển đổi kiểu chữ: Đưa toàn bộ chuỗi về chữ hoa hoặc chữ thường để việc so sánh không phân biệt chữ hoa/thường.
  4. Xóa ký tự không mong muốn: Có thể là dấu câu, ký tự đặc biệt, v.v.

Giờ thì, hãy chuẩn bị môi trường C++ của bạn và cùng giải quyết từng bài tập!

Bài Tập 1: Xóa khoảng trắng thừa ở đầu và cuối chuỗi

Đây là một tác vụ cơ bản nhưng rất quan trọng. Khoảng trắng ở đầu (leading) và cuối (trailing) thường xuất hiện do người dùng nhập liệu hoặc từ các nguồn dữ liệu không được định dạng tốt.

Chúng ta có thể sử dụng các hàm tìm kiếm vị trí ký tự và cắt chuỗi có sẵn trong C++.

Ví dụ:

#include <iostream>
#include <string>
#include <algorithm>
#include <cctype> // Đối với hàm isspace

// Hàm xóa khoảng trắng ở đầu và cuối chuỗi
string trim(const string& str) {
    // Tìm vị trí ký tự không phải khoảng trắng đầu tiên
    size_t first = str.find_first_not_of(" \t\n\r");
    if (string::npos == first) {
        // Chuỗi rỗng hoặc chỉ chứa khoảng trắng
        return str;
    }

    // Tìm vị trí ký tự không phải khoảng trắng cuối cùng
    size_t last = str.find_last_not_of(" \t\n\r");

    // Cắt chuỗi từ first đến last
    return str.substr(first, (last - first + 1));
}

int main() {
    string messy_string = "   \n  Đây là một chuỗi lộn xộn có khoảng trắng thừa ở đầu và cuối.   \t ";
    string clean_string = trim(messy_string);

    cout << "Chuỗi gốc: '" << messy_string << "'\n";
    cout << "Chuỗi sau khi trim: '" << clean_string << "'\n";

    return 0;
}

Giải thích code:

  • Hàm trim nhận một chuỗi làm đối số.
  • str.find_first_not_of(" \t\n\r"): Tìm vị trí của ký tự đầu tiên không phải là khoảng trắng (`), tab (\t), xuống dòng (\n), hoặc ký tự carriage return (\r).string::npos` là một giá trị đặc biệt chỉ ra không tìm thấy.
  • str.find_last_not_of(" \t\n\r"): Tương tự, tìm vị trí của ký tự cuối cùng không phải là các loại khoảng trắng.
  • str.substr(first, (last - first + 1)): Trả về một chuỗi con (substring) bắt đầu từ vị trí first và có độ dài là last - first + 1.

Đây là một cách tiếp cận đơn giản và hiệu quả. Bạn cũng có thể sử dụng các iterator và algorithm với isspace để làm điều này, nhưng cách dùng substr thường dễ đọc hơn cho tác vụ trim cơ bản.

Bài Tập 2: Xóa khoảng trắng thừa giữa các từ

Sau khi đã xử lý khoảng trắng ở hai đầu, giờ là lúc làm gọn các khoảng trắng bên trong. Chuỗi "Hello world" nên trở thành "Hello world".

Một kỹ thuật hiệu quả là sử dụng stringstream. Nó có khả năng đọc từng "từ" (các chuỗi được phân tách bởi khoảng trắng) từ một chuỗi, giúp chúng ta dễ dàng xây dựng lại chuỗi mà chỉ giữa các từ có một khoảng trắng duy nhất.

Ví dụ:

#include <iostream>
#include <string>
#include <sstream> // Đối với stringstream

// Hàm xóa khoảng trắng thừa giữa các từ
string remove_extra_internal_spaces(const string& str) {
    stringstream ss(str);
    string word;
    string result = "";

    // Đọc từng từ một từ stringstream
    while (ss >> word) {
        if (!result.empty()) {
            // Nếu result đã có từ, thêm khoảng trắng trước khi thêm từ mới
            result += " ";
        }
        result += word;
    }

    return result;
}

int main() {
    string messy_string = "Đây   là    một   chuỗi  với   nhiều    khoảng   trắng    thừa.";
    string clean_string = remove_extra_internal_spaces(messy_string);

    cout << "Chuỗi gốc: '" << messy_string << "'\n";
    cout << "Chuỗi sau khi xóa khoảng trắng nội bộ: '" << clean_string << "'\n";

    return 0;
}

Giải thích code:

  • stringstream ss(str);: Khởi tạo một stringstream với chuỗi đầu vào. Stringstream cho phép bạn coi chuỗi như một luồng (stream), giống như cin hoặc cout.
  • while (ss >> word): Vòng lặp này đọc từng "từ" từ stringstream vào biến word. Toán tử >> cho stringstream tự động bỏ qua các khoảng trắng (bao gồm space, tab, newline) và trích xuất chuỗi ký tự tiếp theo không phải khoảng trắng.
  • if (!result.empty()) { result += " "; }: Điều kiện này đảm bảo rằng một khoảng trắng chỉ được thêm vào result trước mỗi từ, ngoại trừ từ đầu tiên.
  • result += word;: Thêm từ đã đọc vào chuỗi kết quả.

Kết quả là chuỗi result sẽ chứa các từ gốc được phân tách bởi đúng một khoảng trắng.

Bài Tập 3: Chuyển đổi kiểu chữ

Đôi khi, sự khác biệt giữa "APPLE" và "apple" là không quan trọng trong ngữ cảnh dữ liệu. Chuyển đổi toàn bộ chuỗi về cùng một kiểu chữ (thường là chữ thường) giúp việc so sánh trở nên dễ dàng hơn.

Chúng ta có thể sử dụng thuật toán transform kết hợp với hàm ::tolower (hoặc ::toupper) để làm điều này một cách hiệu quả.

Ví dụ:

#include <iostream>
#include <string>
#include <algorithm> // Đối với transform
#include <cctype>    // Đối với ::tolower

// Hàm chuyển đổi chuỗi thành chữ thường
string to_lowercase(string str) { // Truyền bằng giá trị để có bản sao chỉnh sửa
    transform(str.begin(), str.end(), str.begin(),
                   [](unsigned char c){ return tolower(c); }); // Sử dụng lambda và tolower
    return str;
}

// Hàm chuyển đổi chuỗi thành chữ hoa (ví dụ thêm)
string to_uppercase(string str) { // Truyền bằng giá trị để có bản sao chỉnh sửa
    transform(str.begin(), str.end(), str.begin(),
                   [](unsigned char c){ return toupper(c); }); // Sử dụng lambda và toupper
    return str;
}


int main() {
    string mixed_case_string = "Đây Là MộT ChUỖi ViẾt LỘn XộN KiỂu ChỮ.";
    string lowercase_string = to_lowercase(mixed_case_string);
    string uppercase_string = to_uppercase(mixed_case_string);


    cout << "Chuỗi gốc: '" << mixed_case_string << "'\n";
    cout << "Chuỗi chữ thường: '" << lowercase_string << "'\n";
    cout << "Chuỗi chữ hoa: '" << uppercase_string << "'\n";


    return 0;
}

Giải thích code:

  • transform(str.begin(), str.end(), str.begin(), ...): Thuật toán này áp dụng một phép biến đổi cho từng phần tử trong một phạm vi (str.begin() đến str.end()) và lưu kết quả vào một phạm vi đích (str.begin()). Ở đây, phạm vi đích trùng với phạm vi nguồn, tức là chuỗi str sẽ được thay đổi trực tiếp.
  • [](unsigned char c){ return tolower(c); }: Đây là một lambda function. Nó nhận một ký tự c (được ép kiểu sang unsigned char để tương thích tốt hơn với tolower/toupper qua các locale) và trả về ký tự đó sau khi đã chuyển sang chữ thường bằng tolower.
  • Hàm to_uppercase hoạt động tương tự nhưng sử dụng toupper.

Lưu ý: Việc chuyển đổi kiểu chữ có thể phức tạp hơn với các ký tự không thuộc bảng mã ASCII cơ bản (ví dụ: ký tự có dấu trong tiếng Việt, hoặc các ngôn ngữ khác). tolowertoupper hoạt động dựa trên locale hiện tại của chương trình. Đối với các ứng dụng quốc tế hóa, bạn có thể cần sử dụng các thư viện hoặc kỹ thuật xử lý Unicode phức tạp hơn. Tuy nhiên, với các chuỗi tiếng Anh hoặc ASCII cơ bản, cách này là đủ.

Bài Tập 4: Xóa ký tự không mong muốn (ví dụ: dấu câu)

Trong nhiều trường hợp, các dấu câu hoặc ký tự đặc biệt khác không cần thiết cho việc xử lý dữ liệu chính. Chúng ta có thể loại bỏ chúng.

Thuật toán remove_if kết hợp với hàm kiểm tra ký tự như ::ispunct là lựa chọn tuyệt vời cho việc này. remove_if sẽ di chuyển các phần tử không thỏa mãn điều kiện về phía đầu phạm vi, và trả về một iterator trỏ đến vị trí cuối cùng hợp lệ mới. Sau đó, chúng ta dùng string::erase để xóa phần tử còn lại.

Ví dụ:

#include <iostream>
#include <string>
#include <algorithm> // Đối với remove_if
#include <cctype>    // Đối với ::ispunct

// Hàm xóa dấu câu
string remove_punctuation(string str) { // Truyền bằng giá trị để có bản sao chỉnh sửa
    // remove_if di chuyển các ký tự KHÔNG PHẢI dấu câu về phía đầu
    str.erase(remove_if(str.begin(), str.end(), ::ispunct), str.end());
    return str;
}

int main() {
    string messy_string = "Xin chào thế giới!!!, Đây là một câu có dấu câu.";
    string clean_string = remove_punctuation(messy_string);

    cout << "Chuỗi gốc: '" << messy_string << "'\n";
    cout << "Chuỗi sau khi xóa dấu câu: '" << clean_string << "'\n";

    return 0;
}

Giải thích code:

  • remove_if(str.begin(), str.end(), ::ispunct): Thuật toán này lặp qua chuỗi. Với mỗi ký tự, nó gọi hàm ::ispunct để kiểm tra xem đó có phải là dấu câu không. Nếu ::ispunct trả về false (ký tự không phải dấu câu), ký tự đó được giữ lại và di chuyển về phía đầu chuỗi (nếu cần). Nếu trả về true (ký tự dấu câu), ký tự đó bị "đánh dấu" để xóa (bằng cách di chuyển các ký tự sau nó lên đè lên). Thuật toán trả về một iterator trỏ đến vị trí đầu tiên của các ký tự "rác" cần xóa ở cuối chuỗi.
  • str.erase(..., str.end()): Hàm erase của string nhận hai iterator: vị trí bắt đầu và vị trí kết thúc của phạm vi cần xóa. Chúng ta truyền iterator trả về từ remove_ifstr.end() để xóa tất cả các ký tự từ vị trí đó đến cuối chuỗi.

Tương tự như tolower/toupper, hàm ::ispunct cũng phụ thuộc vào locale và có thể không nhận diện hết các loại dấu câu trong mọi ngôn ngữ.

Bài Tập 5: Kết hợp các bước chuẩn hóa

Trong thực tế, việc chuẩn hóa thường bao gồm nhiều bước. Thứ tự các bước có thể quan trọng. Ví dụ, bạn có thể muốn xóa dấu câu trước khi xóa khoảng trắng nội bộ để tránh trường hợp ". " trở thành "." thay vì "".

Hãy kết hợp các hàm đã tạo để thực hiện một quy trình chuẩn hóa hoàn chỉnh.

Ví dụ:

#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
#include <sstream>

// (Sao chép các hàm trim, remove_extra_internal_spaces, to_lowercase, remove_punctuation từ các bài tập trước vào đây)

// Hàm xóa khoảng trắng ở đầu và cuối chuỗi (tái sử dụng từ BT1)
string trim(const string& str) {
    size_t first = str.find_first_not_of(" \t\n\r");
    if (string::npos == first) return str;
    size_t last = str.find_last_not_of(" \t\n\r");
    return str.substr(first, (last - first + 1));
}

// Hàm xóa khoảng trắng thừa giữa các từ (tái sử dụng từ BT2)
string remove_extra_internal_spaces(const string& str) {
    stringstream ss(str);
    string word;
    string result = "";
    while (ss >> word) {
        if (!result.empty()) result += " ";
        result += word;
    }
    return result;
}

// Hàm chuyển đổi chuỗi thành chữ thường (tái sử dụng từ BT3)
string to_lowercase(string str) {
    transform(str.begin(), str.end(), str.begin(),
                   [](unsigned char c){ return tolower(c); });
    return str;
}

// Hàm xóa dấu câu (tái sử dụng từ BT4)
string remove_punctuation(string str) {
    str.erase(remove_if(str.begin(), str.end(), ::ispunct), str.end());
    return str;
}

// Hàm chuẩn hóa chuỗi tổng hợp
string normalize_string(string str) {
    // Bước 1: Xóa dấu câu
    str = remove_punctuation(str);
    // Bước 2: Chuyển về chữ thường
    str = to_lowercase(str);
    // Bước 3: Xóa khoảng trắng nội bộ thừa (quan trọng là sau khi xóa dấu câu)
    str = remove_extra_internal_spaces(str);
    // Bước 4: Xóa khoảng trắng đầu cuối (sau khi xóa nội bộ cũng có thể sinh ra khoảng trắng đầu cuối)
    str = trim(str);

    return str;
}

int main() {
    string very_messy_string = "  !!!  Đây Là . . .    MộT   ChUỖi   RấT Lộn XộN !!!.  ";
    string normalized_string = normalize_string(very_messy_string);

    cout << "Chuỗi gốc: '" << very_messy_string << "'\n";
    cout << "Chuỗi sau khi chuẩn hóa: '" << normalized_string << "'\n";

    return 0;
}

Giải thích code:

  • Chúng ta gom các hàm chuẩn hóa từ các bài tập trước lại.
  • Hàm normalize_string gọi các hàm con theo một trình tự logic. Thứ tự này có thể tùy chỉnh tùy theo yêu cầu cụ thể của việc chuẩn hóa (ví dụ: có cần xóa dấu câu không, có cần chuyển kiểu chữ không, v.v.). Trình tự trong ví dụ này là một cách tiếp cận hợp lý cho nhiều trường hợp sử dụng chung.
  • Mỗi bước xử lý trả về một chuỗi đã được làm sạch, và chuỗi này được dùng làm đầu vào cho bước tiếp theo.

Thực hành kết hợp các bước này giúp bạn xây dựng các quy trình chuẩn hóa phức tạp và tùy chỉnh cho nhu cầu dữ liệu của mình.

Tầm Quan Trọng Của Chuẩn Hóa Chuỗi

Qua các bài tập này, hy vọng bạn đã thấy rõ hơn cách thực hiện chuẩn hóa chuỗi trong C++. Việc này mang lại nhiều lợi ích:

  • Tính nhất quán: Dữ liệu trở nên đồng nhất, dễ dàng so sánh và xử lý.
  • Độ chính xác: Cải thiện kết quả tìm kiếm, lọc và phân tích dữ liệu.
  • Hiệu quả lưu trữ: Có thể giảm bớt không gian lưu trữ (ví dụ: khi xóa khoảng trắng thừa).
  • Đơn giản hóa logic xử lý: Code xử lý dữ liệu sau chuẩn hóa thường đơn giản hơn nhiều.

Việc chuẩn hóa chuỗi là một kỹ năng cực kỳ quan trọng khi làm việc với dữ liệu "thực tế". Bằng cách nắm vững các kỹ thuật cơ bản này, bạn đã có trong tay những công cụ mạnh mẽ để làm sạch và chuẩn bị dữ liệu chuỗi cho các tác vụ tiếp theo trong các dự án C++ của mình.

Comments

There are no comments at the moment.