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

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:
- 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.
- 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.
- 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.
- 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>
string xoaKt(const string& s) {
size_t dau = s.find_first_not_of(" \t\n\r");
if (string::npos == dau) return s;
size_t cuoi = s.find_last_not_of(" \t\n\r");
return s.substr(dau, (cuoi - dau + 1));
}
int main() {
string s = " \n Đây là một chuỗi lộn xộn có khoảng trắng thừa ở đầu và cuối. \t ";
string sSach = xoaKt(s);
cout << "Chuỗi gốc: '" << s << "'\n";
cout << "Chuỗi sau khi trim: '" << sSach << "'\n";
return 0;
}
Output:
Chuỗi gốc: '
Đây là một chuỗi lộn xộn có khoảng trắng thừa ở đầu và cuối. '
Chuỗi sau khi trim: 'Đây là một chuỗi lộn xộn có khoảng trắng thừa ở đầu và cuối.'
Giải thích code:
- Hàm
xoaKt
nhận một chuỗi làm đối số. s.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.s.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.s.substr(dau, (cuoi - dau + 1))
: Trả về một chuỗi con (substring) bắt đầu từ vị trídau
và có độ dài làcuoi - dau + 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>
string xoaKtNoiBo(const string& s) {
stringstream ss(s);
string tu;
string kq = "";
while (ss >> tu) {
if (!kq.empty()) kq += " ";
kq += tu;
}
return kq;
}
int main() {
string s = "Đây là một chuỗi với nhiều khoảng trắng thừa.";
string sSach = xoaKtNoiBo(s);
cout << "Chuỗi gốc: '" << s << "'\n";
cout << "Chuỗi sau khi xóa khoảng trắng nội bộ: '" << sSach << "'\n";
return 0;
}
Output:
Chuỗi gốc: 'Đây là một chuỗi với nhiều khoảng trắng thừa.'
Chuỗi sau khi xóa khoảng trắng nội bộ: 'Đây là một chuỗi với nhiều khoảng trắng thừa.'
Giải thích code:
stringstream ss(s);
: 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ặccout
.while (ss >> tu)
: Vòng lặp này đọc từng "từ" từ stringstream vào biếntu
. 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 (!kq.empty()) { kq += " "; }
: Điều kiện này đảm bảo rằng một khoảng trắng chỉ được thêm vàokq
trước mỗi từ, ngoại trừ từ đầu tiên.kq += tu;
: Thêm từ đã đọc vào chuỗi kết quả.
Kết quả là chuỗi kq
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>
#include <cctype>
string chuThg(string s) {
transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return tolower(c); });
return s;
}
string chuHoa(string s) {
transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return toupper(c); });
return s;
}
int main() {
string sGoc = "Đây Là MộT ChUỖi ViẾt LỘn XộN KiỂu ChỮ.";
string sThg = chuThg(sGoc);
string sHoa = chuHoa(sGoc);
cout << "Chuỗi gốc: '" << sGoc << "'\n";
cout << "Chuỗi chữ thường: '" << sThg << "'\n";
cout << "Chuỗi chữ hoa: '" << sHoa << "'\n";
return 0;
}
Output:
Chuỗi gốc: 'Đây Là MộT ChUỖi ViẾt LỘn XộN KiỂu ChỮ.'
Chuỗi chữ thường: 'đây là một chuỗi viết lộn xộn kiểu chữ.'
Chuỗi chữ hoa: 'ĐÂY LÀ MỘT CHUỖI VIẾT LỘN XỘN KIỂU CHỮ.'
Giải thích code:
transform(s.begin(), s.end(), s.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 (s.begin()
đếns.end()
) và lưu kết quả vào một phạm vi đích (s.begin()
). Ở đây, phạm vi đích trùng với phạm vi nguồn, tức là chuỗis
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 sangunsigned char
để tương thích tốt hơn vớitolower
/toupper
qua các locale) và trả về ký tự đó sau khi đã chuyển sang chữ thường bằngtolower
.- Hàm
chuHoa
hoạt động tương tự nhưng sử dụngtoupper
.
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). tolower
và toupper
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>
#include <cctype>
string xoaDauCau(string s) {
s.erase(remove_if(s.begin(), s.end(), ::ispunct), s.end());
return s;
}
int main() {
string s = "Xin chào thế giới!!!, Đây là một câu có dấu câu.";
string sSach = xoaDauCau(s);
cout << "Chuỗi gốc: '" << s << "'\n";
cout << "Chuỗi sau khi xóa dấu câu: '" << sSach << "'\n";
return 0;
}
Output:
Chuỗi gốc: 'Xin chào thế giới!!!, Đây là một câu có dấu câu.'
Chuỗi sau khi xóa dấu câu: 'Xin chào thế giới Đây là một câu có dấu câu'
Giải thích code:
remove_if(s.begin(), s.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ự là 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.s.erase(..., s.end())
: Hàmerase
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_if
vàs.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>
string xoaKt(const string& s) {
size_t dau = s.find_first_not_of(" \t\n\r");
if (string::npos == dau) return s;
size_t cuoi = s.find_last_not_of(" \t\n\r");
return s.substr(dau, (cuoi - dau + 1));
}
string xoaKtNoiBo(const string& s) {
stringstream ss(s);
string tu;
string kq = "";
while (ss >> tu) {
if (!kq.empty()) kq += " ";
kq += tu;
}
return kq;
}
string chuThg(string s) {
transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return tolower(c); });
return s;
}
string xoaDauCau(string s) {
s.erase(remove_if(s.begin(), s.end(), ::ispunct), s.end());
return s;
}
string chuanHoaChuoi(string s) {
s = xoaDauCau(s);
s = chuThg(s);
s = xoaKtNoiBo(s);
s = xoaKt(s);
return s;
}
int main() {
string s = " !!! Đây Là . . . MộT ChUỖi RấT Lộn XộN !!!. ";
string sSach = chuanHoaChuoi(s);
cout << "Chuỗi gốc: '" << s << "'\n";
cout << "Chuỗi sau khi chuẩn hóa: '" << sSach << "'\n";
return 0;
}
Output:
Chuỗi gốc: ' !!! Đây Là . . . MộT ChUỖi RấT Lộn XộN !!!. '
Chuỗi sau khi chuẩn hóa: 'đây là một chuỗi rất lộn xộn'
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
chuanHoaChuoi
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.
Comments