如何在不知道实际模式的情况下检查字符串中的重复模式?
How can I check for a reoccurring pattern in a string without knowing the actual pattern?
例如,我有一个字符串,“fbrtfuifigfbrt”。我想查找一个字符序列是否在字符串中重复出现,但我不知道该字符序列是什么。在本例中,它是 fbrt。
我考虑过将字符串分解成一堆单独的单词,然后检查这些单词是否相同,但是当解析较长的字符串时,这很快就会变得低效。
目前,我实现了上述想法,但肯定还有更好的想法。
String s = "fbrtfuifigfbrt";
ArrayList<String> words = new ArrayList<String>(s.length() * s.length());
for(int outerLoop = 0; outerLoop <= s.length(); outerLoop++){
for(int nestedLoop = 0; nestedLoop <= s.length(); nestedLoop++){
words.add(fileContents.substring(outerLoop, nestedLoop));
}
}
//I could dump the ArrayList in a HashSet and check if they are the same size,
//then find those elements, etc.
//but that goes along with the above code, and I would prefer to use a more efficient method
您需要有两个迭代器,第一个指针是整个字符串的全局迭代器,第二个迭代器用作搜索指针。假设第一个迭代器指向示例中的 char "f" 。我们需要找到全局迭代器之后所有 "f" 的位置。对于在全局迭代器之后找到的每个 "f",我们需要在全局迭代器和局部迭代器之后一个一个地比较字符(将此视为两个指针以相同的速度移动,直到它们指向不同的字符)。一旦本地迭代器到达字符串的末尾,您可以将全局迭代器向前移动一个字符(是的,如果您的字符串中有 n 个字符,您需要这样做 n 次)。
很抱歉,代码是用 C++ 编写的,但逻辑在 Java 中是相同的。
更新:
还有另一种方法来执行任务。一种流行的解决方案是使用后缀树来存储文本。然后,您可以使用任何给定子字符串搜索后缀树,以查找给定子字符串在整个文本中的出现次数。树的构建是 O(n),搜索子字符串取决于字母表的大小,如果您只使用英文字母,则为 26。所以如果你想找到所有重复出现的模式,你只需要对给定文本的每个子字符串执行搜索。这只会是 O(n^2)。所以这个算法比我提出的算法有整体优势。但如果你不需要性能,我的算法肯定能满足你的需要,因为它简单易行。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main(int argc, const char * argv[]) {
string s = "sdfssdddfssss";
int pairCount = 0;
vector<string> rep;
for (int i = 0; i < s.length(); i++)
{
vector<int> idx;
//find all index of all same char as s[i] after i
//Note: You can optimize this by creating a map of index of 26 letters.
for (int j = i+1; j < s.length(); j++)
if (s[i] == s[j]) idx.push_back(j);
int offset = 0;
for (int j = 0; j < idx.size(); j++)
{
while (s[i+offset] == s[idx[j]+offset])
{
cout << "Pair found! " << s.substr(i, offset+1) << " " << i << " " << idx[j] << " " << offset + 1 << endl;
pairCount++;
offset++;
}
offset = 0;
}
}
cout << "Pair count: " << pairCount;
return 0;
}
这方面没有很好的优化。你最终会得到某种蛮力解决方案。
类似于:
String myString = "abcabcbbb";
//for each char
for (int i = 0; i < myString.length(); i++) {
//for each substring starting with that char
int maxSubStringLen = Math.floorDiv(myString.length() - i, 2);
for (int j = 1; j <= maxSubStringLen; j++) {
//get the substring
String subString = myString.substring(i, i + j);
int repetitionIndex = i + j;
String repetition = myString.substring(repetitionIndex, repetitionIndex + subString.length());
//does the substring repeat?
if (subString.equals(repetition)) {
System.out.println(subString);
}
}
}
这只是打印了 mach 的所有子字符串。您可以将 print 语句替换为您实际想要对它们执行的任何操作。
Java 中的工作解决方案:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
String test1 = "fbrtfuifigfbrt";
String test2 = "abcdabcd";
String test3 = "fbrtxibrjkfbrt";
System.out.println(findRepetitions(test1));
System.out.println(findRepetitions(test2));
System.out.println(findRepetitions(test3));
}
private static List<String> findRepetitions(String string) {
List<String> patternsList = new ArrayList<>();
int length = string.length();
for (int i = 0; i < length; i++) { // search the first half
int limit = (length - i) / 2; // candidates can't be longer than half the remaining length
for (int j = 1; j <= limit; j++) {
int candidateEndIndex = i + j;
String candidate = string.substring(i, candidateEndIndex);
if (string.substring(candidateEndIndex).contains(candidate)) {
patternsList.add(candidate);
}
}
}
return patternsList;
}
}
输出:
[f, fb, fbr, fbrt, b, br, brt, r, rt, t, f, i, f]
[a, ab, abc, abcd, b, bc, bcd, c, cd, d]
[f, fb, fbr, fbrt, b, br, brt, r, rt, t, b, br, r]
正如其他人所说,如果您不知道模式的长度或任何其他适用的限制,就没有简单的优化。
如果你想天真地丢弃像f
、fb
、fbr
这样的子模式,因为它们是最长的 fbrt
模式,你可以使内部 for
向下计数,从 limit
向下计数到 1,这样你会先找到更长的模式,然后检查下一个模式是否是子串已经找到的,然后再将它们添加到列表中。像这样:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
String test1 = "fbrtfuifigfbrt";
String test2 = "abcdabcd";
String test3 = "fbrtxibrjkfbrt"; // "br" is a pattern but this version won't find it
System.out.println(findRepetitions(test1));
System.out.println(findRepetitions(test2));
System.out.println(findRepetitions(test3));
}
private static List<String> findRepetitions(String string) {
List<String> patternsList = new ArrayList<>();
int length = string.length();
for (int i = 0; i < length; i++) { // search the first half
int limit = (length - i) / 2; // candidates can't be longer than half the remaining length
for (int j = limit; j >= 1; j--) {
int candidateEndIndex = i + j;
String candidate = string.substring(i, candidateEndIndex);
if (string.substring(candidateEndIndex).contains(candidate)) {
boolean notASubpattern = true;
for (String pattern : patternsList) {
if (pattern.contains(candidate)) {
notASubpattern = false;
break;
}
}
if (notASubpattern) {
patternsList.add(candidate);
}
}
}
}
return patternsList;
}
}
然而,这会阻止您在 fbrtxzbrjkfbrt
中找到 br
,如输出所示(并且对于具有许多不同模式的字符串,它也会使算法变慢):
[fbrt, i]
[abcd]
[fbrt]
因此 天真地 部分。当然,您可以包含更多内部循环,以确保在实际丢弃它们之前,在原始字符串中找不到 "on their own" 被丢弃的候选对象……等等。这取决于您希望搜索的详细程度成为。
例如,我有一个字符串,“fbrtfuifigfbrt”。我想查找一个字符序列是否在字符串中重复出现,但我不知道该字符序列是什么。在本例中,它是 fbrt。
我考虑过将字符串分解成一堆单独的单词,然后检查这些单词是否相同,但是当解析较长的字符串时,这很快就会变得低效。
目前,我实现了上述想法,但肯定还有更好的想法。
String s = "fbrtfuifigfbrt";
ArrayList<String> words = new ArrayList<String>(s.length() * s.length());
for(int outerLoop = 0; outerLoop <= s.length(); outerLoop++){
for(int nestedLoop = 0; nestedLoop <= s.length(); nestedLoop++){
words.add(fileContents.substring(outerLoop, nestedLoop));
}
}
//I could dump the ArrayList in a HashSet and check if they are the same size,
//then find those elements, etc.
//but that goes along with the above code, and I would prefer to use a more efficient method
您需要有两个迭代器,第一个指针是整个字符串的全局迭代器,第二个迭代器用作搜索指针。假设第一个迭代器指向示例中的 char "f" 。我们需要找到全局迭代器之后所有 "f" 的位置。对于在全局迭代器之后找到的每个 "f",我们需要在全局迭代器和局部迭代器之后一个一个地比较字符(将此视为两个指针以相同的速度移动,直到它们指向不同的字符)。一旦本地迭代器到达字符串的末尾,您可以将全局迭代器向前移动一个字符(是的,如果您的字符串中有 n 个字符,您需要这样做 n 次)。
很抱歉,代码是用 C++ 编写的,但逻辑在 Java 中是相同的。
更新: 还有另一种方法来执行任务。一种流行的解决方案是使用后缀树来存储文本。然后,您可以使用任何给定子字符串搜索后缀树,以查找给定子字符串在整个文本中的出现次数。树的构建是 O(n),搜索子字符串取决于字母表的大小,如果您只使用英文字母,则为 26。所以如果你想找到所有重复出现的模式,你只需要对给定文本的每个子字符串执行搜索。这只会是 O(n^2)。所以这个算法比我提出的算法有整体优势。但如果你不需要性能,我的算法肯定能满足你的需要,因为它简单易行。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main(int argc, const char * argv[]) {
string s = "sdfssdddfssss";
int pairCount = 0;
vector<string> rep;
for (int i = 0; i < s.length(); i++)
{
vector<int> idx;
//find all index of all same char as s[i] after i
//Note: You can optimize this by creating a map of index of 26 letters.
for (int j = i+1; j < s.length(); j++)
if (s[i] == s[j]) idx.push_back(j);
int offset = 0;
for (int j = 0; j < idx.size(); j++)
{
while (s[i+offset] == s[idx[j]+offset])
{
cout << "Pair found! " << s.substr(i, offset+1) << " " << i << " " << idx[j] << " " << offset + 1 << endl;
pairCount++;
offset++;
}
offset = 0;
}
}
cout << "Pair count: " << pairCount;
return 0;
}
这方面没有很好的优化。你最终会得到某种蛮力解决方案。
类似于:
String myString = "abcabcbbb";
//for each char
for (int i = 0; i < myString.length(); i++) {
//for each substring starting with that char
int maxSubStringLen = Math.floorDiv(myString.length() - i, 2);
for (int j = 1; j <= maxSubStringLen; j++) {
//get the substring
String subString = myString.substring(i, i + j);
int repetitionIndex = i + j;
String repetition = myString.substring(repetitionIndex, repetitionIndex + subString.length());
//does the substring repeat?
if (subString.equals(repetition)) {
System.out.println(subString);
}
}
}
这只是打印了 mach 的所有子字符串。您可以将 print 语句替换为您实际想要对它们执行的任何操作。
Java 中的工作解决方案:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
String test1 = "fbrtfuifigfbrt";
String test2 = "abcdabcd";
String test3 = "fbrtxibrjkfbrt";
System.out.println(findRepetitions(test1));
System.out.println(findRepetitions(test2));
System.out.println(findRepetitions(test3));
}
private static List<String> findRepetitions(String string) {
List<String> patternsList = new ArrayList<>();
int length = string.length();
for (int i = 0; i < length; i++) { // search the first half
int limit = (length - i) / 2; // candidates can't be longer than half the remaining length
for (int j = 1; j <= limit; j++) {
int candidateEndIndex = i + j;
String candidate = string.substring(i, candidateEndIndex);
if (string.substring(candidateEndIndex).contains(candidate)) {
patternsList.add(candidate);
}
}
}
return patternsList;
}
}
输出:
[f, fb, fbr, fbrt, b, br, brt, r, rt, t, f, i, f]
[a, ab, abc, abcd, b, bc, bcd, c, cd, d]
[f, fb, fbr, fbrt, b, br, brt, r, rt, t, b, br, r]
正如其他人所说,如果您不知道模式的长度或任何其他适用的限制,就没有简单的优化。
如果你想天真地丢弃像f
、fb
、fbr
这样的子模式,因为它们是最长的 fbrt
模式,你可以使内部 for
向下计数,从 limit
向下计数到 1,这样你会先找到更长的模式,然后检查下一个模式是否是子串已经找到的,然后再将它们添加到列表中。像这样:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
String test1 = "fbrtfuifigfbrt";
String test2 = "abcdabcd";
String test3 = "fbrtxibrjkfbrt"; // "br" is a pattern but this version won't find it
System.out.println(findRepetitions(test1));
System.out.println(findRepetitions(test2));
System.out.println(findRepetitions(test3));
}
private static List<String> findRepetitions(String string) {
List<String> patternsList = new ArrayList<>();
int length = string.length();
for (int i = 0; i < length; i++) { // search the first half
int limit = (length - i) / 2; // candidates can't be longer than half the remaining length
for (int j = limit; j >= 1; j--) {
int candidateEndIndex = i + j;
String candidate = string.substring(i, candidateEndIndex);
if (string.substring(candidateEndIndex).contains(candidate)) {
boolean notASubpattern = true;
for (String pattern : patternsList) {
if (pattern.contains(candidate)) {
notASubpattern = false;
break;
}
}
if (notASubpattern) {
patternsList.add(candidate);
}
}
}
}
return patternsList;
}
}
然而,这会阻止您在 fbrtxzbrjkfbrt
中找到 br
,如输出所示(并且对于具有许多不同模式的字符串,它也会使算法变慢):
[fbrt, i]
[abcd]
[fbrt]
因此 天真地 部分。当然,您可以包含更多内部循环,以确保在实际丢弃它们之前,在原始字符串中找不到 "on their own" 被丢弃的候选对象……等等。这取决于您希望搜索的详细程度成为。