剣の指す先はオファー 19. 正規表現マッチング
「.」と「*」を含む正規表現をマッチングする機能を実装してください。パターン内の文字「.」は任意の文字を表し、「*」はその前の文字が任意の回数 (0 回を含む) 出現できることを表します。この質問では、一致とは、文字列のすべての文字がパターン全体と一致することを意味します。たとえば、文字列「aaa」はパターン「aa」および「ab*ac*a」には一致しますが、「aa.a」にも「ab*a」にも一致しません。
例 1:
入力:
s = "aa"
p = "a"
出力: false
説明: "a" は文字列 "aa" 全体と一致することはできません。
例 2:
入力:
s = "aa"
p = "a*"
出力: true
説明: 「*」は、0 個以上の前の要素と一致する可能性があることを意味するため、ここでの前の要素は「a」です。したがって、文字列「aa」は「a」が 1 回繰り返されたものと考えることができます。
例 3:
入力:
s = "ab"
p = ".*"
出力: true
説明: ".*" は、0 個以上の ('*') 任意の文字 ('.') と一致することを意味します。
例 4:
入力:
s = "aab"
p = "c*a*b"
出力: true
説明: 「*」は 0 以上を意味するため、ここでは「c」は 0 で、「a」は 1 回繰り返されます。したがって、文字列「aab」と一致する可能性があります。
例 5:
入力:
s = "mississippi"
p = "mis*is*p*."
出力: false
s は空であり、az からの小文字のみが含まれる場合があります。
p は空であり、a ~ z からの小文字と . および * のみが含まれ、連続する '* は含まれません。
著者: Krahets
リンク: https://leetcode.cn/leetbook/read/illustration-of-algorithm/9a1ypc/
出典: LeetCode
著作権は著者に属します。商業転載の場合は作者に許可を、非商業転載の場合は出典を明記してください。
解決策: 以下のように。
class Solution {
public:
bool isMatch(string s, string p) {
std::regex reg(p);
return std::regex_match(s, reg);
}
};
結果:
合格
詳細を表示
実行時間: 172 ミリ秒、すべての C++ 送信の 5.32% を上回りました
メモリ消費量: 9 MB、すべての C++ 送信のユーザーの 11.58% を上回ります
合格したテスト ケース: 448/448
ライブラリ関数を使用すると実行時間が少し遅いようです。それは問題ではありませんが、C++ 正規表現を学ぶことも素晴らしいことです。
通常のプログラム ライブラリ (正規表現)
「正規表現」はルールを表す一連の式であり、さまざまな複雑な操作を処理するために特に使用されます。
std::regex は、「正規表現」を表すために C++ で使用されるライブラリです。C++11 で追加されました。これは、char 型用のクラス std::basic_regex<> の特殊化であり、wchar_t 用のクラスの特殊化です。 std::wregex です。
通常の文法 (正規表現構文)
std::regex は、デフォルトで ECMAScript 文法を使用します。この文法は使いやすく、強力です。一般的な記号の意味は次のとおりです:
シンボル | 意義 |
---|---|
^ | 行の先頭と一致します |
$ | 行末に一致 |
。 | 任意の 1 文字と一致します |
[…] | [] 内の任意の文字と一致します |
(…) | グループを設定する |
\ | エスケープ文字 |
\d | 一致番号 [0-9] |
\D | \d 否定します |
\w | 文字 [az]、数字、アンダースコアと一致します |
\W | \w 取反 |
\s | スペースを一致させる |
\S | \s を否定します |
+ | 前の要素が 1 回以上繰り返されます |
* | 前の要素を何度でも繰り返します |
? | 前の要素が 0 回または 1 回繰り返されます |
{n} | 前の要素が n 回繰り返されます |
{n,} | 前の要素が少なくとも n 回繰り返されます |
{n,m} | 前の要素が少なくとも n 回、最大で m 回繰り返されます |
| | 論理的または |
上記の記号は非常に一般的に使用されており、ほとんどの問題を解決するのに十分です。
マッチ
文字列処理で一般的に使用される操作は「マッチング」です。つまり、文字列とルールが正確に一致し、マッチングに使用される関数は関数テンプレートである std::regex_match() です。例を直接見てみましょう。
std::regex reg("<.*>.*</.*>");
bool ret = std::regex_match("<html>value</html>", reg);
assert(ret);
ret = std::regex_match("<xml>value<xml>", reg);
assert(!ret);
std::regex reg1("<(.*)>.*</\\1>");
ret = std::regex_match("<xml>value</xml>", reg1);
assert(ret);
ret = std::regex_match("<header>value</header>", std::regex("<(.*)>value</\\1>"));
assert(ret);
// 使用basic文法
std::regex reg2("<\\(.*\\)>.*</\\1>", std::regex_constants::basic);
ret = std::regex_match("<title>value</title>", reg2);
assert(ret);
この小さな例では、regex_match() を使用して、XML 形式 (または HTML 形式) の文字列を照合します。照合が成功すると、true が返されます。意味は非常に簡単です。意味がわからない場合は、次を参照してください。先ほどの文法部分。
\\ は、\ をエスケープする必要があるため、ステートメントに表示されます。C++11 以降ではネイティブ文字がサポートされているため、次のように使用することもできます。
std::regex reg1(R"(<(.*)>.*</\1>)");
auto ret = std::regex_match("<xml>value</xml>", reg1);
assert(ret);
ただし、C++03以前はサポートされていないため、使用する場合は注意が必要です。
一致する結果を取得したい場合は、別のオーバーロード形式の regex_match() を使用できます。
std::cmatch m;
auto ret = std::regex_match("<xml>value</xml>", m, std::regex("<(.*)>(.*)</(\\1)>"));
if (ret)
{
std::cout << m.str() << std::endl;
std::cout << m.length() << std::endl;
std::cout << m.position() << std::endl;
}
std::cout << "----------------" << std::endl;
// 遍历匹配内容
for (auto i = 0; i < m.size(); ++i)
{
// 两种方式都可以
std::cout << m[i].str() << " " << m.str(i) << std::endl;
}
std::cout << "----------------" << std::endl;
// 使用迭代器遍历
for (auto pos = m.begin(); pos != m.end(); ++pos)
{
std::cout << *pos << std::endl;
}
出力は次のとおりです。
<xml>value</xml>
16
0
----------------
<xml>value</xml> <xml>value</xml>
xml xml
value value
xml xml
----------------
<xml>value</xml>
xml
value
xml
cmatch は、C 文字用のクラス テンプレート std::match_result<> の特殊バージョンです。文字列の場合は、文字列用の smatch の特殊バージョンを使用する必要があります。対応するワイド文字バージョンの wcmatch および wsmatch もサポートされています。
match_result を regex_match() の 2 番目のパラメータとして渡して、一致する結果を取得します。この例では、結果は cmatch に保存され、cmatch はこれらの結果を操作するための多くの関数を提供します。ほとんどのメソッドは string のメソッドと同じです. 方法は似ているので、より簡単に使用できます。
m[0] には、一致結果のすべての文字が保存されます。一致結果の部分文字列を保存したい場合は、() を使用して「正規表現」内の部分文字列をマークする必要があるため、ここにさらにいくつかの括弧が追加されます:
std::regex("<(.*)>(.*)</(\\1)>")
このようにして、これらの部分文字列は m[0] の後に順番に保存され、各部分文字列には m[1]、m[2]、... を通じて順番にアクセスできます。
検索
"Search" は "match" に非常に似ています。対応する関数は std::regex_search で、これも関数テンプレートです。その使用方法は regex_match と同じです。違いは、"search" はターゲットが表示される限り返されることです。文字列内で完全に「一致」します。
例を挙げてみましょう:
std::regex reg("<(.*)>(.*)</(\\1)>");
std::cmatch m;
auto ret = std::regex_search("123<xml>value</xml>456", m, reg);
if (ret)
{
for (auto& elem : m)
std::cout << elem << std::endl;
}
std::cout << "prefix:" << m.prefix() << std::endl;
std::cout << "suffix:" << m.suffix() << std::endl;
出力は次のとおりです。
<xml>value</xml>
xml
value
xml
prefix:123
suffix:456
ここで regex_match に変更すると、regex_match は完全一致ですが、ここの文字列の前後にさらにいくつかの文字が追加されるため、一致は失敗します。
「検索」の場合、一致結果のプレフィックスとサフィックスからそれぞれプレフィックスとサフィックスを取得できます。プレフィックスは一致したコンテンツの前のコンテンツ、サフィックスは一致したコンテンツの後のコンテンツです。
では、条件を満たすコンテンツが複数ある場合、どのようにしてすべての情報を取得するのでしょうか? 小さな例を通してそれを見てみましょう:
std::regex reg("<(.*)>(.*)</(\\1)>");
std::string content("123<xml>value</xml>456<widget>center</widget>hahaha<vertical>window</vertical>the end");
std::smatch m;
auto pos = content.cbegin();
auto end = content.cend();
for (; std::regex_search(pos, end, m, reg); pos = m.suffix().first)
{
std::cout << "----------------" << std::endl;
std::cout << m.str() << std::endl;
std::cout << m.str(1) << std::endl;
std::cout << m.str(2) << std::endl;
std::cout << m.str(3) << std::endl;
}
出力は次のとおりです。
----------------
<xml>value</xml>
xml
value
xml
----------------
<widget>center</widget>
widget
center
widget
----------------
<vertical>window</vertical>
vertical
window
vertical
ここでは、regex_search 関数の別のオーバーロード形式が使用されています (regex_match 関数にも同じオーバーロード形式があります)。実際、すべての部分文字列オブジェクトは std::pair<> から派生し、その最初 (つまり、ここではプレフィックス) は最初の文字の位置、2 番目 (ここではサフィックス) は最後の文字の次の位置です。
一連の検索が完了した後は、接尾語から検索を継続することができ、内容に合致した情報を全て取得することができます。
トークン化
他にも「切り取り」という操作があり、例えば、多数のメールアカウントをカンマで区切ったデータの集合がある場合、区切り文字にカンマを指定することで、内容を切り取ってアカウントごとに取得することができます。
C++ の通常のルールでは、この操作は Tokenize と呼ばれ、テンプレート クラス regex_token_iterator<> を使用して単語セグメンテーション イテレータが提供されます。
std::string mail("[email protected],[email protected],[email protected],[email protected]");
std::regex reg(",");
std::sregex_token_iterator pos(mail.begin(), mail.end(), reg, -1);
decltype(pos) end;
for (; pos != end; ++pos)
{
std::cout << pos->str() << std::endl;
}
このようにして、すべてのメールボックスをカンマで区切って取得できます。
[email protected]
[email protected]
[email protected]
[email protected]
sregex_token_iterator は文字列型の特殊化です。注目すべきは最後のパラメータです。このパラメータは、関心のあるコンテンツを表す一連の整数値を指定できます。ここでの -1 は、一致する前の部分文字列を意味します正規表現 シーケンスが対象です。0 を指定すると、一致する正規表現に関心があることを意味し、ここで「,」が表示されます。正規表現をグループ化して、対応する任意の数値を入力することもできます。指定したグループに追加します。試してみましょう。
交換する
最後の操作は「置換」と呼ばれ、正規表現の内容を指定された内容に置き換えます。正規表現ライブラリは、テンプレート関数 std::regex_replace を使用して「置換」操作を提供します。
さて、「he...ll..o, worldr..d!」というデータがあったとして、誤って入力した「.」を削除する方法を考えてみましょう。
何かアイデアはありますか?通常の解決策を見てみましょう。
char data[] = "he...ll..o, worl..d!";
std::regex reg("\\.");
// output: hello, world!
std::cout << std::regex_replace(data, reg, "");
グループ化関数を使用することもできます。
char data[] = "001-Neo,002-Lucia";
std::regex reg("(\\d+)-(\\w+)");
// output: 001 name=Neo,002 name=Lucia
std::cout << std::regex_replace(data, reg, "$1 name=$2");
グループ化機能を使用すると、$N を通じてグループ化内容を取得できるので、非常に便利です。
例
1.メールを確認する
この要件は、ユーザー入力の正当性を検出するために登録およびログインするときによく使用されます。
マッチング精度が高くない場合は、次のように記述できます。
std::string data = "[email protected],[email protected],[email protected],[email protected]";
std::regex reg("\\w+@\\w+(\\.\\w+)+");
std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
decltype(pos) end;
for (; pos != end; ++pos)
{
std::cout << pos->str() << std::endl;
}
ここでは、通常の検索を実行する別の方法が使用されています。この方法では、正規表現イテレータを使用して反復処理が行われ、match を使用するよりも効率的です。ここの正規表現は弱い一致ですが、一般的なユーザー入力には問題ありません。重要なのは単純さです。出力は次のとおりです。
[email protected]
[email protected]
[email protected]
[email protected]
ただし、「[email protected]」と入力しても、正常に一致します。これは明らかに不正な電子メール アドレスです。より正確な正規表現は次のように記述する必要があります。
std::string data = "[email protected], \
[email protected], \
[email protected], \
[email protected], \
[email protected] \
[email protected]";
std::regex reg("[a-zA-z0-9_]+@[a-zA-z0-9]+(\\.[a-zA-z]+){1,3}");
std::sregex_iterator pos(data.cbegin(), data.cend(), reg);
decltype(pos) end;
for (; pos != end; ++pos)
{
std::cout << pos->str() << std::endl;
}
出力は次のとおりです。
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
2.IPの一致
192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30 という IP アドレスの文字列があり、これらの
IP アドレスを取り出して、アドレスセグメントの順序で IP アドレスを出力する必要があります。
少し遅いので詳しくは説明しませんが、参考までに直接の答えを以下に示します。
std::string ip("192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30");
std::cout << "原内容为:\n" << ip << std::endl;
// 1. 位数对齐
ip = std::regex_replace(ip, std::regex("(\\d+)"), "00$1");
std::cout << "位数对齐后为:\n" << ip << std::endl;
// 2. 有0的去掉
ip = std::regex_replace(ip, std::regex("0*(\\d{3})"), "$1");
std::cout << "去掉0后为:\n" << ip << std::endl;
// 3. 取出IP
std::regex reg("\\s");
std::sregex_token_iterator pos(ip.begin(), ip.end(), reg, -1);
decltype(pos) end;
std::set<std::string> ip_set;
for (; pos != end; ++pos)
{
ip_set.insert(pos->str());
}
std::cout << "------\n最终结果:\n";
// 4. 输出排序后的数组
for (auto elem : ip_set)
{
// 5. 去掉多余的0
std::cout << std::regex_replace(elem,
std::regex("0*(\\d+)"), "$1") << std::endl;
}
出力は次のとおりです。
原内容为:
192.68.1.254 102.49.23.013 10.10.10.10 2.2.2.2 8.109.90.30
位数对齐后为:
00192.0068.001.00254 00102.0049.0023.00013 0010.0010.0010.0010 002.002.002.002 008.00109.0090.0030
去掉0后为:
192.068.001.254 102.049.023.013 010.010.010.010 002.002.002.002 008.109.090.030
------
最终结果:
2.2.2.2
8.109.90.30
10.10.10.10
102.49.23.13
192.68.1.254
ブログパークのボスの方々のおかげでとても勉強になりました!!