Bài 10.1. Các thao tác cơ bản trên chuỗi

Bài 10.1. Các thao tác cơ bản trên chuỗi
Chào mừng trở lại với chuỗi bài viết về Cấu trúc dữ liệu và Giải thuật!
Trong thế giới lập trình, chúng ta không chỉ làm việc với các con số khô khan hay cấu trúc dữ liệu phức tạp như cây, đồ thị, mà còn phải xử lý một loại dữ liệu vô cùng phổ biến và thiết yếu: đó chính là chuỗi ký tự (string). Từ tên người dùng, nội dung bài viết, câu lệnh code, cho đến dữ liệu truyền qua mạng, tất cả đều có thể biểu diễn dưới dạng chuỗi.
Dù bản thân chuỗi ký tự có vẻ đơn giản, nhưng việc nắm vững các thao tác cơ bản trên chúng là nền tảng cực kỳ quan trọng. Nó không chỉ giúp bạn xử lý dữ liệu văn bản hàng ngày mà còn là kỹ năng cần thiết khi làm việc với các thuật toán xử lý chuỗi phức tạp hơn sau này (ví dụ: tìm kiếm mẫu, nén chuỗi, phân tích cú pháp...).
Trong bài viết này, chúng ta sẽ cùng nhau đi sâu vào các thao tác cơ bản nhất trên chuỗi trong ngôn ngữ C++, sử dụng đối tượng std::string
mạnh mẽ và linh hoạt.
Hãy cùng bắt đầu!
1. Khởi tạo và Gán giá trị cho Chuỗi
Trước khi thực hiện bất kỳ thao tác nào, chúng ta cần biết cách tạo ra một chuỗi. Trong C++, std::string
cung cấp nhiều cách để làm điều này.
Khởi tạo chuỗi rỗng:
#include <iostream> #include <string> int main() { std::string empty_string; // Chuỗi rỗng std::cout << "Chuoi rong: '" << empty_string << "'" << std::endl; // In ra chuỗi rỗng return 0; }
Giải thích: Dòng
std::string empty_string;
gọi constructor mặc định củastd::string
, tạo ra một chuỗi không chứa ký tự nào.Khởi tạo từ một chuỗi ký tự cố định (string literal):
#include <iostream> #include <string> int main() { std::string greeting = "Xin chao!"; // Khởi tạo trực tiếp từ literal std::string message("Lap trinh that thu vi."); // Sử dụng constructor std::cout << "Greeting: " << greeting << std::endl; std::cout << "Message: " << message << std::endl; return 0; }
Giải thích: Bạn có thể dùng toán tử gán
=
hoặc truyền literal vào constructor()
để tạo chuỗi từ một chuỗi ký tự cố định.Khởi tạo từ một chuỗi khác:
#include <iostream> #include <string> int main() { std::string original = "Hello World"; std::string copy_of_original(original); // Sử dụng constructor sao chép std::string another_copy = original; // Sử dụng toán tử gán std::cout << "Original: " << original << std::endl; std::cout << "Copy 1: " << copy_of_original << std::endl; std::cout << "Copy 2: " << another_copy << std::endl; return 0; }
Giải thích: Chuỗi có thể được sao chép dễ dàng bằng constructor hoặc toán tử gán
=
.Khởi tạo với N ký tự lặp lại:
#include <iostream> #include <string> int main() { std::string stars(10, '*'); // Tạo chuỗi gồm 10 ký tự '*' std::cout << "Stars: " << stars << std::endl; return 0; }
Giải thích: Constructor
std::string(count, char)
tạo ra một chuỗi gồmcount
lần lặp lại của ký tựchar
.Gán giá trị sau khi khởi tạo:
#include <iostream> #include <string> int main() { std::string my_string; my_string = "Day la mot chuoi moi."; // Gán giá trị std::cout << "My string: " << my_string << std::endl; return 0; }
Giải thích: Bạn có thể dùng toán tử gán
=
để thay đổi giá trị của một chuỗi đã tồn tại.
2. Truy cập các ký tự trong Chuỗi
Mỗi ký tự trong chuỗi được lưu trữ tại một "vị trí" hay chỉ mục (index). Trong C++ (và hầu hết các ngôn ngữ khác), chỉ mục bắt đầu từ 0. Ký tự đầu tiên có chỉ mục là 0, ký tự thứ hai là 1, và cứ thế tiếp diễn.
Bạn có thể truy cập các ký tự riêng lẻ bằng hai cách chính:
Sử dụng toán tử
[]
:#include <iostream> #include <string> int main() { std::string text = "Hello"; char first_char = text[0]; // Truy cập ký tự đầu tiên (chỉ mục 0) char third_char = text[2]; // Truy cập ký tự thứ ba (chỉ mục 2) std::cout << "Chuoi: " << text << std::endl; std::cout << "Ky tu dau tien: " << first_char << std::endl; std::cout << "Ky tu thu ba: " << third_char << std::endl; // Thay đổi ký tự text[0] = 'J'; std::cout << "Chuoi sau khi thay doi: " << text << std::endl; // Output: Jello // Cẩn thận với chỉ mục nằm ngoài phạm vi! Hành vi không xác định. // char invalid_char = text[10]; // Lỗi tiềm ẩn nếu chỉ mục không hợp lệ return 0; }
Giải thích: Toán tử
[]
cho phép truy cập nhanh đến ký tự tại chỉ mục được chỉ định. Tuy nhiên, nó không kiểm tra xem chỉ mục đó có hợp lệ (nằm trong phạm vi từ 0 đếnlength() - 1
) hay không. Nếu bạn truy cập một chỉ mục không hợp lệ, chương trình có thể gặp lỗi hoặc có hành vi không mong muốn.Sử dụng phương thức
.at()
:#include <iostream> #include <string> int main() { std::string text = "World"; try { char second_char = text.at(1); // Truy cập ký tự thứ hai (chỉ mục 1) std::cout << "Ky tu thu hai: " << second_char << std::endl; // Thay đổi ký tự text.at(4) = 'D'; std::cout << "Chuoi sau khi thay doi: " << text << std::endl; // Output: WorlD // Thử truy cập chỉ mục không hợp lệ char invalid_char = text.at(10); // Sẽ ném ra ngoại lệ std::out_of_range std::cout << "Ky tu khong hop le (se khong in ra): " << invalid_char << std::endl; } catch (const std::out_of_range& e) { std::cerr << "Loi truy cap: " << e.what() << std::endl; // In ra thông báo lỗi } return 0; }
Giải thích: Phương thức
.at()
cũng truy cập ký tự tại chỉ mục, nhưng nó có kiểm tra phạm vi chỉ mục. Nếu chỉ mục không hợp lệ, nó sẽ ném ra một ngoại lệstd::out_of_range
, cho phép bạn bắt và xử lý lỗi một cách an toàn hơn. Trong các ứng dụng cần độ tin cậy cao,.at()
thường được ưu tiên.
3. Lấy chiều dài (độ dài) của Chuỗi
Biết được số lượng ký tự trong chuỗi là một thao tác rất thường gặp. std::string
cung cấp hai phương thức cho mục đích này:
.length()
: Trả về số lượng ký tự trong chuỗi..size()
: Cũng trả về số lượng ký tự trong chuỗi.
Hai phương thức này thường tương đương nhau trong các triển khai std::string
hiện đại.
#include <iostream>
#include <string>
int main() {
std::string sentence = "Day la mot cau van ban.";
int len1 = sentence.length();
int len2 = sentence.size();
std::cout << "Chuoi: '" << sentence << "'" << std::endl;
std::cout << "Chieu dai (length()): " << len1 << std::endl;
std::cout << "Kich thuoc (size()): " << len2 << std::endl;
std::string empty;
std::cout << "Chieu dai chuoi rong: " << empty.length() << std::endl; // Output: 0
return 0;
}
Giải thích: Cả .length()
và .size()
đều trả về một giá trị kiểu size_t
(một kiểu số nguyên không âm, thường là unsigned long
) biểu diễn số ký tự. Kết quả này rất hữu ích khi bạn muốn lặp qua chuỗi hoặc kiểm tra điều kiện dựa trên độ dài.
4. Nối Chuỗi (Concatenation)
Kết hợp hai hoặc nhiều chuỗi thành một là một thao tác phổ biến.
Sử dụng toán tử
+
và+=
:#include <iostream> #include <string> int main() { std::string part1 = "Hello, "; std::string part2 = "World"; std::string part3 = "!"; std::string full_message = part1 + part2 + part3; // Nối nhiều chuỗi std::cout << "Full message: " << full_message << std::endl; // Output: Hello, World! std::string greeting = "Hi"; greeting += ", "; // Nối thêm literal greeting += part2; // Nối thêm chuỗi khác greeting += '!'; // Nối thêm ký tự std::cout << "Greeting: " << greeting << std::endl; // Output: Hi, World! return 0; }
Giải thích: Toán tử
+
tạo ra một chuỗi mới là kết quả nối của hai chuỗi/literal/ký tự. Toán tử+=
nối chuỗi/literal/ký tự vào cuối chuỗi hiện tại (thay đổi chuỗi gốc).Sử dụng phương thức
.append()
:#include <iostream> #include <string> int main() { std::string base = "Learning"; base.append(" C++"); // Nối thêm literal base.append(" is fun!"); // Nối thêm literal khác base.append(5, '!'); // Nối thêm 5 ký tự '!' std::cout << "Base string: " << base << std::endl; // Output: Learning C++ is fun!!!!! return 0; }
Giải thích: Phương thức
.append()
cũng thực hiện nối chuỗi vào cuối chuỗi hiện tại, tương tự như+=
. Nó cung cấp các phiên bản khác nhau để nối chuỗi khác, literal, ký tự, hoặc một phần của chuỗi khác.
5. Trích xuất Chuỗi con (Substring)
Thường bạn chỉ cần làm việc với một phần của chuỗi gốc. Phương thức .substr()
cho phép bạn làm điều này.
Cú pháp cơ bản là: str.substr(pos, len)
pos
: Chỉ mục bắt đầu của chuỗi con (0-based).len
: Số lượng ký tự bạn muốn trích xuất từ vị trípos
. Nếu bỏ qualen
,.substr()
sẽ trích xuất từpos
đến hết chuỗi gốc.
#include <iostream>
#include <string>
int main() {
std::string sentence = "Programming is interesting and useful.";
// Trích xuất từ chỉ mục 0, lấy 11 ký tự
std::string part1 = sentence.substr(0, 11);
std::cout << "Part 1: '" << part1 << "'" << std::endl; // Output: 'Programming'
// Trích xuất từ chỉ mục 12 (sau khoảng trắng), lấy 2 ký tự
std::string part2 = sentence.substr(12, 2);
std::cout << "Part 2: '" << part2 << "'" << std::endl; // Output: 'is'
// Trích xuất từ chỉ mục 25 đến hết chuỗi
std::string part3 = sentence.substr(25);
std::cout << "Part 3: '" << part3 << "'" << std::endl; // Output: 'useful.'
// Xử lý trường hợp chỉ mục không hợp lệ (có thể ném ngoại lệ out_of_range)
try {
std::string invalid_part = sentence.substr(100); // Chỉ mục 100 nằm ngoài phạm vi
} catch (const std::out_of_range& e) {
std::cerr << "Loi khi trich xuat chuoi con: " << e.what() << std::endl;
}
return 0;
}
Giải thích: .substr(pos, len)
trả về một chuỗi mới là chuỗi con được trích xuất. Chuỗi gốc không bị thay đổi. Lưu ý rằng .substr()
có thể ném ngoại lệ std::out_of_range
nếu chỉ mục pos
không hợp lệ.
6. Tìm kiếm trong Chuỗi
Tìm kiếm sự xuất hiện của một ký tự hoặc một chuỗi con khác trong chuỗi là một thao tác rất phổ biến. Phương thức .find()
là công cụ chính cho việc này.
Cú pháp cơ bản: str.find(substring, pos)
substring
: Chuỗi con hoặc ký tự bạn muốn tìm.pos
(tùy chọn): Chỉ mục bắt đầu tìm kiếm (mặc định là 0).
Phương thức .find()
trả về chỉ mục của lần xuất hiện đầu tiên của substring
kể từ vị trí pos
. Nếu không tìm thấy, nó trả về giá trị đặc biệt std::string::npos
.
#include <iostream>
#include <string>
int main() {
std::string sentence = "Ban co the tim kiem mot chuoi trong mot chuoi khac.";
// Tìm kiếm chuỗi con "tim kiem"
size_t found_pos1 = sentence.find("tim kiem");
if (found_pos1 != std::string::npos) {
std::cout << "Tim thay 'tim kiem' tai chi muc: " << found_pos1 << std::endl; // Output: 10
} else {
std::cout << "'tim kiem' khong duoc tim thay." << std::endl;
}
// Tìm kiếm ký tự 'o'
size_t found_pos2 = sentence.find('o');
if (found_pos2 != std::string::npos) {
std::cout << "Tim thay ky tu 'o' tai chi muc: " << found_pos2 << std::endl; // Output: 4
}
// Tìm kiếm "chuoi" bat dau tu chi muc 20
size_t found_pos3 = sentence.find("chuoi", 20);
if (found_pos3 != std::string::npos) {
std::cout << "Tim thay 'chuoi' bat dau tu chi muc 20 tai chi muc: " << found_pos3 << std::endl; // Output: 36
} else {
std::cout << "'chuoi' khong duoc tim thay tu chi muc 20." << std::endl;
}
// Tìm kiếm mot chuoi khong ton tai
size_t not_found_pos = sentence.find("xyz");
if (not_found_pos == std::string::npos) {
std::cout << "'xyz' khong duoc tim thay trong chuoi." << std::endl;
}
return 0;
}
Giải thích: std::string::npos
là một hằng số đặc biệt, thường có giá trị lớn nhất có thể của kiểu size_t
, được sử dụng để biểu thị rằng thao tác tìm kiếm đã không thành công. Luôn so sánh kết quả của .find()
với std::string::npos
để biết liệu chuỗi con có được tìm thấy hay không.
Ngoài .find()
, std::string
còn có các biến thể khác như:
.rfind()
: Tìm kiếm lần xuất hiện cuối cùng..find_first_of()
: Tìm vị trí của bất kỳ ký tự nào trong một tập hợp ký tự cho trước (tìm ký tự đầu tiên trong tập)..find_last_of()
: Tìm vị trí của bất kỳ ký tự nào trong một tập hợp ký tự cho trước (tìm ký tự cuối cùng trong tập)..find_first_not_of()
: Tìm vị trí của ký tự đầu tiên không nằm trong tập hợp ký tự cho trước..find_last_not_of()
: Tìm vị trí của ký tự cuối cùng không nằm trong tập hợp ký tự cho trước.
Các phương thức tìm kiếm này rất linh hoạt và hữu ích cho nhiều bài toán xử lý chuỗi.
7. So sánh Chuỗi
So sánh hai chuỗi không chỉ đơn giản là kiểm tra xem chúng có bằng nhau hoàn toàn hay không, mà còn là so sánh theo thứ tự từ điển (lexicographical order), giống như cách các từ được sắp xếp trong từ điển.
Sử dụng các toán tử so sánh (
==
,!=
,<
,>
,<=
,>=
):#include <iostream> #include <string> int main() { std::string s1 = "apple"; std::string s2 = "banana"; std::string s3 = "apple"; std::string s4 = "Apple"; // 'A' khác 'a' if (s1 == s3) { std::cout << "s1 va s3 bang nhau." << std::endl; // Output: s1 va s3 bang nhau. } if (s1 != s2) { std::cout << "s1 va s2 khac nhau." << std::endl; // Output: s1 va s2 khac nhau. } if (s1 < s2) { std::cout << "s1 dung truoc s2 trong tu dien." << std::endl; // Output: s1 dung truoc s2 trong tu dien. ('a' < 'b') } if (s1 > s4) { std::cout << "s1 dung sau s4 trong tu dien." << std::endl; // Output: s1 dung sau s4 trong tu dien. ('a' > 'A' trong ASCII) } return 0; }
Giải thích: Các toán tử so sánh thực hiện so sánh theo thứ tự từ điển dựa trên giá trị ASCII (hoặc bộ mã ký tự khác) của các ký tự. So sánh này phân biệt chữ hoa và chữ thường.
Sử dụng phương thức
.compare()
:Phương thức
.compare()
cung cấp khả năng so sánh chi tiết hơn và linh hoạt hơn. Cú pháp cơ bản:str.compare(other_str)
- Trả về 0 nếu chuỗi gốc bằng
other_str
. - Trả về một số âm nếu chuỗi gốc đứng trước
other_str
trong từ điển. - Trả về một số dương nếu chuỗi gốc đứng sau
other_str
trong từ điển.
#include <iostream> #include <string> int main() { std::string s1 = "hello"; std::string s2 = "world"; std::string s3 = "hello"; int comp1 = s1.compare(s2); // s1 < s2 int comp2 = s1.compare(s3); // s1 == s3 int comp3 = s2.compare(s1); // s2 > s1 std::cout << "s1 so sanh voi s2: " << comp1 << " (Am neu s1 < s2)" << std::endl; std::cout << "s1 so sanh voi s3: " << comp2 << " (0 neu s1 == s3)" << std::endl; std::cout << "s2 so sanh voi s1: " << comp3 << " (Duong neu s2 > s1)" << std::endl; // So sánh một phần của chuỗi std::string full = "programming"; std::string prefix = "pro"; // So sánh 3 ký tự đầu tiên của full với prefix int comp4 = full.compare(0, prefix.length(), prefix); if (comp4 == 0) { std::cout << "'" << full << "' bat dau bang '" << prefix << "'." << std::endl; } return 0; }
Giải thích: Phương thức
.compare()
hữu ích khi bạn cần biết chính xác mối quan hệ từ điển giữa hai chuỗi (nhỏ hơn, bằng, hay lớn hơn) hoặc khi bạn muốn so sánh chỉ một phần của chuỗi.- Trả về 0 nếu chuỗi gốc bằng
8. Nhập và Xuất Chuỗi
Làm việc với chuỗi thường bao gồm việc đọc chúng từ bàn phím (input) hoặc hiển thị chúng lên màn hình (output).
Xuất chuỗi ra console: Sử dụng
std::cout
với toán tử<<
.#include <iostream> #include <string> int main() { std::string my_name = "FullhouseDev"; std::cout << "Ten cua toi la: " << my_name << std::endl; return 0; }
Giải thích: Tương tự như in các kiểu dữ liệu cơ bản, bạn chỉ cần dùng
std::cout
và toán tử<<
.Nhập chuỗi từ console:
Sử dụng
std::cin
với toán tử>>
: Phương pháp này đơn giản nhưng có một hạn chế quan trọng: nó chỉ đọc đến khoảng trắng đầu tiên (dấu cách, tab, xuống dòng).#include <iostream> #include <string> int main() { std::string first_word; std::cout << "Nhap mot tu: "; std::cin >> first_word; // Chi doc den khoang trang std::cout << "Tu da nhap: " << first_word << std::endl; // Nếu bạn nhập "Lap trinh C++", chỉ "Lap" sẽ được đọc vào first_word return 0; }
Giải thích: Hữu ích khi bạn chỉ cần đọc một từ duy nhất.
Sử dụng
std::getline()
: Đây là cách an toàn và phổ biến nhất để đọc toàn bộ một dòng văn bản từ input, bao gồm cả khoảng trắng, cho đến khi gặp ký tự xuống dòng.#include <iostream> #include <string> #include <limits> // Can de xoa bo dem cin neu can int main() { std::string full_line; std::cout << "Nhap mot dong van ban: "; // Can xu ly bo dem neu co cac lenh cin >> truoc do // std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); std::getline(std::cin, full_line); // Doc ca dong std::cout << "Dong da nhap: " << full_line << std::endl; // Neu ban nhap "Lap trinh C++ that tuyet", toan bo dong se duoc doc vao full_line return 0; }
Giải thích:
std::getline(input_stream, string_variable)
đọc từinput_stream
(thường làstd::cin
) vàostring_variable
cho đến khi gặp ký tự kết thúc dòng (mặc định là'\n'
). Nếu trướcgetline
có sử dụngcin >>
, có thể còn ký tự xuống dòng'\n'
còn lại trong bộ đệm input, khiếngetline
đọc ngay ký tự đó và kết thúc mà không chờ nhập liệu. Dòngstd::cin.ignore(...)
được dùng để xóa bỏ bộ đệm này trước khi gọigetline
nếu cần.
9. Các thao tác khác (Nâng cao hơn một chút nhưng vẫn cơ bản)
std::string
còn cung cấp nhiều phương thức khác như:
.insert(pos, str)
: Chèn chuỗistr
vào vị trípos
.std::string s = "Lap trinh!"; s.insert(3, " toi yeu"); // s tro thanh "Lap toi yeu trinh!"
.erase(pos, len)
: Xóalen
ký tự bắt đầu từ vị trípos
. Nếu bỏ qualen
, xóa từpos
đến hết.std::string s = "Xoa mot phan"; s.erase(4, 4); // Xoa " mot" -> s tro thanh "Xoa phan"
.replace(pos, len, str)
: Thay thếlen
ký tự bắt đầu từ vị trípos
bằng chuỗistr
.std::string s = "Toi thich tao."; s.replace(4, 5, "cam"); // Thay "thich" bang "cam" -> s tro thanh "Toi cam tao."
.clear()
: Xóa tất cả nội dung, biến chuỗi thành rỗng.std::string s = "Co noi dung"; s.clear(); // s tro thanh rong
.empty()
: Trả vềtrue
nếu chuỗi rỗng,false
nếu ngược lại.std::string s1 = ""; std::string s2 = "abc"; std::cout << "s1 rong? " << s1.empty() << std::endl; // Output: 1 (true) std::cout << "s2 rong? " << s2.empty() << std::endl; // Output: 0 (false)
Các thao tác này cho phép bạn tùy chỉnh và biến đổi chuỗi theo nhiều cách khác nhau.
Bài tập ví dụ:
[Xâu kí tự].Tần suất xuất hiện của ký tự.
Cho một xâu kí tự s ,hãy đếm tần suất xuất hiện của các kí tự trong xâu và in ra theo yêu cầu.
Input Format
Xâu kí tự có độ dài không quá 1000 chỉ bao gồm chữ cái.
Constraints
.
Output Format
Đầu tiên in ra các ký tự và tần suất xuất hiện của các ký tự ở trong xâu theo thứ tự từ điển tăng dần, sau đó cách ra một dòng và in ra tần suất xuất hiện của các ký tự theo thứ tự xuất hiện trong xâu(mỗi kí tự chỉ in 1 lần)
Ví dụ:
Dữ liệu vào
bacedcasbdf
Dữ liệu ra
a 2
b 2
c 2
d 2
e 1
f 1
s 1
b 2
a 2
c 2
e 1
d 2
s 1
f 1
Tuyệt vời! Đây là hướng dẫn giải bài tập này bằng C++ một cách ngắn gọn, tập trung vào ý tưởng chính mà không đưa ra code hoàn chỉnh:
Đếm tần suất: Dùng một
std::map<char, int>
(hoặc một mảng nếu chỉ xử lý chữ cái tiếng Anh a-z) để lưu trữ tần suất xuất hiện của mỗi ký tự. Duyệt qua xâu đầu vào, với mỗi ký tự, tăng giá trị tương ứng trong map lên 1.In theo thứ tự từ điển: Duyệt qua
std::map
từ đầu đến cuối.std::map
tự động sắp xếp các phần tử theo khóa (ở đây là ký tự), nên việc duyệt tuần tự sẽ cho kết quả theo thứ tự từ điển tăng dần. In ra ký tự và tần suất của nó.In dòng trống: Sau khi in xong phần đầu tiên, in ra một dòng trống.
In theo thứ tự xuất hiện (duy nhất): Duyệt lại xâu gốc từ đầu. Cần một cách để theo dõi các ký tự đã được in ra để tránh in lặp. Có thể dùng một
std::set<char>
hoặc mộtstd::map<char, bool>
. Với mỗi ký tự trong xâu gốc:- Kiểm tra xem ký tự đó đã có trong tập/map theo dõi các ký tự đã in chưa.
- Nếu chưa, in ký tự đó và tần suất của nó (lấy từ map đã tính ở bước 1), sau đó thêm ký tự đó vào tập/map theo dõi.
- Nếu đã có rồi (nghĩa là nó là lần xuất hiện thứ 2 trở đi của ký tự đó trong xâu), thì bỏ qua.
Comments