Bài 30.4: Bài tập thực hành chuỗi nâng cao trong C++

Chào mừng bạn đến với bài thực hành tiếp theo trong chuỗi bài học về C++. Sau khi đã làm quen với những thao tác cơ bản như khai báo, gán giá trị hay nối chuỗi, hôm nay chúng ta sẽ cùng "nhúng tay" vào các bài tập "khó nhằn" hơn một chút với string - công cụ đắc lực khi làm việc với văn bản trong C++. Chuỗi trong C++ không chỉ đơn thuần là một dãy ký tự, mà nó còn cung cấp rất nhiều phương thức (methods) mạnh mẽ giúp chúng ta thao tác hiệu quả với dữ liệu dạng văn bản.

Bài viết này sẽ tập trung vào việc thực hành các kỹ thuật nâng cao hơn như tìm kiếm, thay thế, trích xuất, chèn, xóa và tách chuỗi. Thông qua các ví dụ minh họa cụ thể, bạn sẽ hiểu rõ hơn cách áp dụng các phương thức của string vào giải quyết các vấn đề thực tế.

Hãy cùng khám phá sức mạnh tiềm ẩn của string qua các ví dụ thực tế nhé!

1. Tìm kiếm và Định vị trong Chuỗi

Trong lập trình thực tế, việc tìm kiếm một ký tự hoặc một chuỗi con bên trong một chuỗi lớn là cực kỳ phổ biến. Thư viện chuẩn C++ cung cấp cho chúng ta nhiều phương thức mạnh mẽ để làm điều này với string.

Tìm kiếm chuỗi con: findrfind

Phương thức find giúp chúng ta tìm vị trí xuất hiện đầu tiên của một chuỗi con (hoặc một ký tự) trong chuỗi hiện tại. Nếu tìm thấy, nó trả về chỉ số (index) của vị trí tìm được. Nếu không tìm thấy, nó trả về một giá trị đặc biệt là string::npos.

Phương thức rfind hoạt động tương tự, nhưng nó tìm kiếm vị trí xuất hiện cuối cùng.

Ví dụ:

#include <iostream>
#include <string>

int main() {
    string text = "Hello world, welcome to the C++ world!";
    string search_word = "world";
    string not_found_word = "programming";

    // Tìm kiếm lần đầu
    size_t pos_first = text.find(search_word);
    if (pos_first != string::npos) {
        cout << "Tim thay '" << search_word << "' lan dau tai vi tri: " << pos_first << endl;
    } else {
        cout << "'" << search_word << "' khong tim thay." << endl;
    }

    // Tìm kiếm lần cuối
    size_t pos_last = text.rfind(search_word);
    if (pos_last != string::npos) {
        cout << "Tim thay '" << search_word << "' lan cuoi tai vi tri: " << pos_last << endl;
    } else {
        cout << "'" << search_word << "' khong tim thay." << endl;
    }

    // Tim mot tu khong co trong chuoi
    size_t pos_not_found = text.find(not_found_word);
     if (pos_not_found != string::npos) {
        cout << "Tim thay '" << not_found_word << "' tai vi tri: " << pos_not_found << endl;
    } else {
        cout << "'" << not_found_word << "' khong tim thay." << endl; // Đây là kết quả mong đợi
    }

    return 0;
}

Giải thích: Trong ví dụ trên, chúng ta sử dụng find để tìm vị trí đầu tiên của "world" và rfind để tìm vị trí cuối cùng. Kết quả được lưu vào biến kiểu size_t (kiểu dữ liệu thích hợp cho kích thước và chỉ số). Chúng ta kiểm tra kết quả với string::npos để biết liệu chuỗi con có được tìm thấy hay không trước khi in ra vị trí.

Tìm kiếm ký tự thuộc/không thuộc một tập hợp: find_first_of, find_first_not_of

Các phương thức này giúp tìm vị trí của ký tự đầu tiên (hoặc cuối cùng) thỏa mãn một điều kiện nhất định, dựa trên một tập hợp các ký tự cho trước.

  • find_first_of(set): Tìm vị trí ký tự đầu tiên trong chuỗi set.
  • find_first_not_of(set): Tìm vị trí ký tự đầu tiên không có trong chuỗi set.
  • Tương tự có find_last_offind_last_not_of.

Ví dụ:

#include <iostream>
#include <string>

int main() {
    string sentence = "C++ Programming is FUN!";
    string vowels = "AEIOUaeiou";
    string digits = "0123456789";

    // Tìm nguyên âm đầu tiên
    size_t first_vowel_pos = sentence.find_first_of(vowels);
    if (first_vowel_pos != string::npos) {
        cout << "Nguyen am dau tien o vi tri: " << first_vowel_pos << " ('" << sentence[first_vowel_pos] << "')" << endl;
    }

    // Tìm ký tự KHÔNG phải chữ cái/số đầu tiên (tức là ký tự đặc biệt, khoảng trắng...)
    string alpha_numeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    size_t first_non_alphanumeric_pos = sentence.find_first_not_of(alpha_numeric);
     if (first_non_alphanumeric_pos != string::npos) {
        cout << "Ky tu khong phai chu cai/so dau tien o vi tri: " << first_non_alphanumeric_pos << " ('" << sentence[first_non_alphanumeric_pos] << "')" << endl;
    }

    return 0;
}

Giải thích: Trong ví dụ này, chúng ta tìm vị trí nguyên âm đầu tiên trong chuỗi sentence bằng cách sử dụng find_first_of với tập hợp vowels. Sau đó, chúng ta tìm vị trí ký tự đầu tiên không phải là chữ cái hoặc số bằng find_first_not_of.

2. Trích xuất và Thay thế Chuỗi con

Thường xuyên chúng ta cần lấy ra một phần của chuỗi hoặc thay đổi một phần của nó. string cung cấp các phương thức substrreplace để thực hiện các thao tác này.

Trích xuất chuỗi con: substr

Phương thức substr(pos, len) tạo và trả về một chuỗi mới chứa len ký tự, bắt đầu từ vị trí pos trong chuỗi gốc. Nếu len được bỏ qua, nó sẽ trích xuất từ pos đến hết chuỗi.

Ví dụ:

#include <iostream>
#include <string>

int main() {
    string full_string = "C++ is a powerful language.";

    // Trích xuất 3 ký tự từ vị trí 0
    string part1 = full_string.substr(0, 3); // "C++"
    cout << "Trich xuat (0, 3): '" << part1 << "'" << endl;

    // Trích xuất từ vị trí 7 đến hết
    string part2 = full_string.substr(7); // "a powerful language."
    cout << "Trich xuat (7 den het): '" << part2 << "'" << endl;

    return 0;
}

Giải thích: substr(0, 3) lấy 3 ký tự đầu tiên (từ index 0). substr(7) lấy từ vị trí thứ 7 (index 7) cho đến cuối chuỗi. Phương thức này không thay đổi chuỗi gốc mà trả về một chuỗi mới.

Thay thế chuỗi con: replace

Phương thức replace(pos, len, str) thay thế len ký tự trong chuỗi gốc, bắt đầu từ vị trí pos, bằng nội dung của chuỗi str. Có nhiều phiên bản replace khác nhau (thay thế bằng một phần của chuỗi khác, thay thế bằng N ký tự, v.v.), nhưng phiên bản thay thế một đoạn bằng một chuỗi khác là phổ biến nhất.

Ví dụ:

#include <iostream>
#include <string>

int main() {
    string sentence = "I like C#. I like Java.";

    // Thay thế "C#" bằng "C++"
    size_t pos = sentence.find("C#"); // Tìm vị trí "C#"
    if (pos != string::npos) {
        sentence.replace(pos, 2, "C++"); // Thay thế 2 ký tự từ pos bằng "C++"
    }

    // Thay thế "Java" bằng "Python" (tìm và thay thế lần thứ 2)
    // Tìm "like" thứ 2
    size_t pos2 = sentence.find("like", pos + 1); // Bắt đầu tìm từ vị trí sau "C++"
    if (pos2 != string::npos) {
        // Tìm "Java" sau "like" thứ 2
        size_t pos_java = sentence.find("Java", pos2);
         if (pos_java != string::npos) {
            sentence.replace(pos_java, 4, "Python"); // Thay thế 4 ký tự từ pos_java bằng "Python"
         }
    }

    cout << "Chuoi sau khi thay the: " << sentence << endl; // Kết quả: I like C++. I like Python.

    return 0;
}

Giải thích: Chúng ta tìm vị trí của "C#" và sử dụng replace(pos, 2, "C++") để thay thế 2 ký tự tại vị trí đó bằng "C++". Tương tự, chúng ta tìm và thay thế "Java" bằng "Python". Lưu ý rằng replace thay đổi trực tiếp chuỗi gốc và có thể thay đổi kích thước của chuỗi.

3. Chèn và Xóa Ký tự/Chuỗi con

Bạn cũng có thể chèn thêm nội dung vào giữa chuỗi hoặc xóa bớt đi một phần không mong muốn.

Chèn: insert

Phương thức insert(pos, str) chèn chuỗi str vào chuỗi gốc tại vị trí pos. Có nhiều phiên bản khác, ví dụ insert(pos, count, char) chèn count ký tự char vào vị trí pos.

Ví dụ:

#include <iostream>
#include <string>

int main() {
    string base = "Hello world!";
    string to_insert = "beautiful ";

    // Chèn "beautiful " vào sau "Hello " (vị trí 6, tức là sau ký tự index 5)
    base.insert(6, to_insert);

    cout << "Chuoi sau khi chen: " << base << endl; // Kết quả: Hello beautiful world!

    // Chen 3 ky tu 'X' vao vi tri 5 (sau ký tự index 4)
    base.insert(5, 3, 'X');
    cout << "Chuoi sau khi chen 'X': " << base << endl; // Kết quả: HelloXXX beautiful world!

    return 0;
}

Giải thích: insert(6, to_insert) chèn chuỗi to_insert vào vị trí index 6. insert(5, 3, 'X') chèn 3 ký tự 'X' vào vị trí index 5. Phương thức này thay đổi trực tiếp chuỗi gốc.

Xóa: erase

Phương thức erase(pos, len) xóa len ký tự khỏi chuỗi gốc, bắt đầu từ vị trí pos. Nếu len được bỏ qua, nó xóa từ pos đến hết chuỗi. Phiên bản erase(iterator) xóa ký tự tại vị trí iterator.

Ví dụ:

#include <iostream>
#include <string>
#include <algorithm> // Can dung cho remove

int main() {
    string data = "Some extra text here.";

    // Xóa 5 ký tự từ vị trí 0 ("Some ")
    data.erase(0, 5);
    cout << "Sau khi xoa 5 ky tu dau: " << data << endl; // Kết quả: extra text here.

    // Xóa từ vị trí 6 đến hết (" text here.")
    // Chuỗi hiện tại là "extra text here." (độ dài 18)
    // Vị trí 6 là ký tự 'x' trong "text".
    data.erase(6);
     cout << "Sau khi xoa tu vi tri 6 den het: " << data << endl; // Kết quả: extra

    string another = "abcdefg";
    // Xoa ky tu tai vi tri 2 ('c') su dung iterator
    another.erase(another.begin() + 2);
    cout << "Sau khi xoa ky tu 'c': " << another << endl; // Ket qua: abdefg

    // Xoa tat ca cac ky tu khoang trang su dung erase va remove
    string sentence_with_spaces = "  Hello   world  !";
    // remove chuyen cac phan tu can xoa ra cuoi vector/string
    // no tra ve iterator chi den phan tu dau tien can xoa
    sentence_with_spaces.erase(remove(sentence_with_spaces.begin(), sentence_with_spaces.end(), ' '), sentence_with_spaces.end());
    cout << "Sau khi xoa khoang trang: '" << sentence_with_spaces << "'" << endl; // Ket qua: 'Helloworld!'


    return 0;
}

Giải thích: erase(0, 5) xóa 5 ký tự đầu tiên. erase(6) xóa từ vị trí 6 đến cuối. erase(another.begin() + 2) sử dụng iterator để xóa ký tự tại vị trí index 2. Ví dụ cuối cùng cho thấy cách kết hợp remove (từ thư viện <algorithm>) với erase để xóa tất cả các lần xuất hiện của một ký tự cụ thể một cách hiệu quả.

4. Tách Chuỗi (String Splitting)

Một bài tập phổ biến khác là chia một chuỗi lớn thành nhiều chuỗi con dựa trên một ký tự phân tách (delimiter), ví dụ như dấu phẩy, dấu chấm phẩy hoặc khoảng trắng. Mặc dù string không có phương thức split "có sẵn" như Python hay Java, chúng ta hoàn toàn có thể tự xây dựng hoặc sử dụng các công cụ khác trong C++.

Cách thông dụng và hiệu quả là sử dụng stringstream, đặc biệt khi phân tách bằng khoảng trắng hoặc các ký tự phân tách đơn giản. Hoặc chúng ta có thể tự viết hàm sử dụng findsubstr.

Sử dụng stringstream (Tách theo khoảng trắng mặc định)

Ví dụ:

#include <iostream>
#include <string>
#include <sstream> // Can include for stringstream
#include <vector>

int main() {
    string sentence = "C++ programming is powerful and flexible";
    stringstream ss(sentence); // Tạo stringstream từ chuỗi
    string word;
    vector<string> words; // Lưu các từ sau khi tách

    // Đọc từng từ (phân tách bởi khoảng trắng) từ stringstream
    while (ss >> word) {
        words.push_back(word);
    }

    cout << "Cac tu sau khi tach:" << endl;
    for (const auto& w : words) {
        cout << "- " << w << endl;
    }

    return 0;
}

Giải thích: Chúng ta tạo một stringstream từ chuỗi cần tách. Sử dụng toán tử >> của stringstream sẽ tự động đọc các "từ" (các chuỗi được phân tách bởi khoảng trắng) cho đến khi hết luồng. Mỗi từ đọc được sẽ được lưu vào biến word và sau đó thêm vào vector<string>. Đây là cách rất tiện lợi để tách chuỗi theo khoảng trắng hoặc các ký tự phân tách "trắng" khác.

Tự xây dựng hàm tách theo ký tự phân tách tùy chỉnh

Nếu bạn muốn tách chuỗi theo một ký tự phân tách cụ thể (ví dụ: dấu phẩy trong file CSV), bạn có thể tự xây dựng một hàm dựa trên findsubstr.

Ví dụ:

#include <iostream>
#include <string>
#include <vector>

// Ham tach chuoi theo ky tu phan tach
vector<string> split(const string& s, char delimiter) {
    vector<string> tokens;
    string token;
    size_t start = 0;
    size_t end = s.find(delimiter); // Tim vi tri delimiter dau tien

    // Lap qua cac vi tri cua delimiter
    while (end != string::npos) {
        // Trich xuat token tu start den end-start
        token = s.substr(start, end - start);
        tokens.push_back(token);

        // Cap nhat start va tim delimiter tiep theo
        start = end + 1;
        end = s.find(delimiter, start); // Tim tu vi tri moi
    }

    // Them phan con lai sau delimiter cuoi cung (hoac toan bo chuoi neu khong co delimiter)
    // start luc nay la vi tri ngay sau delimiter cuoi cung, hoac 0 neu khong co delimiter
    token = s.substr(start);
    tokens.push_back(token);

    return tokens;
}

int main() {
    string csv_line = "apple,banana,cherry,date";
    char delimiter = ',';

    vector<string> fruits = split(csv_line, delimiter);

    cout << "Cac phan sau khi tach boi '" << delimiter << "':" << endl;
    for (const auto& fruit : fruits) {
        cout << "- " << fruit << endl;
    }

    string sentence = "This.is.a.sentence.with.dots";
    char dot_delimiter = '.';
    vector<string> parts = split(sentence, dot_delimiter);
     cout << "Cac phan sau khi tach boi '" << dot_delimiter << "':" << endl;
    for (const auto& part : parts) {
        cout << "- " << part << endl;
    }

    return 0;
}

Giải thích: Hàm split này sử dụng vòng lặp và phương thức find cùng substr. Nó tìm vị trí của ký tự phân tách, trích xuất chuỗi con trước đó, rồi tiếp tục tìm từ vị trí sau ký tự phân tách. Vòng lặp dừng khi không còn tìm thấy ký tự phân tách nữa, và phần cuối cùng của chuỗi sẽ được thêm vào kết quả. Đây là một kỹ thuật phổ biến khi bạn cần kiểm soát chính xác ký tự phân tách là gì.

5. Bài tập tự luyện

Lý thuyết và ví dụ giúp bạn hiểu cách thức hoạt động, nhưng thực hành mới là yếu tố quyết định để bạn làm chủ được các kỹ năng này. Dưới đây là một vài bài tập để bạn thử sức, kết hợp các kỹ thuật đã học:

  1. Đếm số lần xuất hiện của từ: Viết chương trình nhập vào một chuỗi (đoạn văn) và một từ cần tìm. Đếm xem từ đó xuất hiện bao nhiêu lần trong chuỗi (bạn có thể tùy chọn có phân biệt chữ hoa/thường hay không). Gợi ý: Sử dụng find trong vòng lặp và cập nhật vị trí tìm kiếm sau mỗi lần tìm thấy.
  2. Đảo ngược từng từ trong câu: Nhập vào một câu. Hãy đảo ngược thứ tự các chữ cái trong mỗi từ của câu, nhưng giữ nguyên vị trí của các từ và khoảng trắng. Ví dụ: "Hello world" -> "olleH dlrow". Gợi ý: Tách câu thành các từ, đảo ngược từng từ, rồi ghép lại.
  3. Kiểm tra Palindrome (nâng cao): Nhập vào một chuỗi. Kiểm tra xem chuỗi đó có phải là palindrome hay không khi bỏ qua khoảng trắng, dấu câu và không phân biệt chữ hoa/thường. Ví dụ: "A man, a plan, a canal: Panama" là palindrome. Gợi ý: Tạo một chuỗi "sạch" chỉ chứa các chữ cái thường, sau đó so sánh nó với chuỗi đảo ngược của chính nó.
  4. Trích xuất thông tin từ chuỗi định dạng: Giả sử bạn có các chuỗi theo định dạng "Tên: [Tên], Tuổi: [Tuổi], Nghề nghiệp: [Nghề nghiệp]". Viết chương trình để trích xuất Tên, Tuổi, Nghề nghiệp từ các chuỗi này. Gợi ý: Dùng find để tìm các nhãn ("Tên:", "Tuổi:", ...) và substr để lấy phần thông tin sau đó.

Hãy tự mình cố gắng giải các bài tập này trước khi tìm kiếm lời giải nhé! Đó là cách tốt nhất để củng cố kiến thức và kỹ năng của bạn với string.

Comments

There are no comments at the moment.