搜索流的字符串的有效方法

时间:2009-05-10 21:43:27

标签: java algorithm string search stream

我们假设有一个文本流(或Java中的Reader),我想检查特定的字符串。文本流可能非常大,所以只要找到搜索字符串,我就想返回true并尝试避免将整个输入存储在内存中。

天真地,我可能会尝试做这样的事情(在Java中):

public boolean streamContainsString(Reader reader, String searchString) throws IOException {
    char[] buffer = new char[1024];
    int numCharsRead;
    while((numCharsRead = reader.read(buffer)) > 0) {
        if ((new String(buffer, 0, numCharsRead)).indexOf(searchString) >= 0)
            return true;
    }
    return false;
}

当然,如果它出现在1k缓冲区的边界上,则无法检测到给定的搜索字符串:

搜索文字:“stackoverflow”
流缓冲区1:“abc ......... stack”
流缓冲区2:“溢出....... xyz”

如何修改此代码,以便正确地在缓冲区边界找到给定的搜索字符串,但不将整个流加载到内存中?

编辑:注意在搜索字符串的流时,我们尝试最小化流中的读取次数(以避免网络/磁盘中的延迟)无论流中的数据量如何,都要保持内存使用率不变string matching algorithm的实际效率是次要的,但显然,找到使用这些算法中效率更高的解决方案会更好。

15 个答案:

答案 0 :(得分:14)

这里有三个很好的解决方案:

  1. 如果你想要一些容易且速度相当快的东西,那么就不要使用缓冲区,而是实现一个简单的非确定性有限状态机。您的状态将是您正在搜索的字符串的索引列表,您的逻辑看起来像这样(伪代码):

    String needle;
    n = needle.length();
    
    for every input character c do
      add index 0 to the list
      for every index i in the list do
        if c == needle[i] then
          if i + 1 == n then
            return true
          else
            replace i in the list with i + 1
          end
        else
          remove i from the list
        end
      end
    end
    

    这将找到字符串,如果它存在,你将永远不需要 缓冲液中。

  2. 稍微多一些工作但也更快:执行NFA到DFA转换,提前确定哪些索引列表是可能的,并将每个索引分配给一个小整数。 (如果您阅读维基百科上的字符串搜索,这称为 powerset构建。)然后您有一个状态,并在每个传入字符上进行状态到状态的转换。您想要的NFA只是字符串的DFA,其前面的状态是非确定性地删除字符或尝试使用当前字符。您还需要一个明确的错误状态。

  3. 如果你想要更快的东西,创建一个大小至少为n两倍的缓冲区,用户Boyer-Moore从needle编译一个状态机。你会有很多额外的麻烦,因为Boyer-Moore实现起来并不容易(尽管你会在网上找到代码),因为你必须安排将字符串滑过缓冲区。你必须构建或找到一个循环缓冲区,它可以“滑动”而不需要复制;否则你可能会给Boyer-Moore带来任何性能提升。

答案 1 :(得分:9)

Knuth-Morris-Pratt search algorithm永不支持;这只是您想要进行流搜索的属性。我以前使用它来解决这个问题,尽管使用可用的Java库可能有更简单的方法。 (当我找到这个时,我在90年代就在C工作。)

KMP本质上是一种快速建立字符串匹配DFA的方法,就像Norman Ramsey的建议#2。

答案 2 :(得分:9)

我对Knuth Morris Pratt算法进行了一些部分搜索更改。由于实际比较位置总是小于或等于下一个位置,因此不需要额外的存储器。带有Makefile的代码也可以在github上获得,它是用Haxe编写的,用于同时定位多种编程语言,包括Java。

我还写了一篇相关的文章:searching for substrings in streams: a slight modification of the Knuth-Morris-Pratt algorithm in Haxe。文章提到了Jakarta RegExp,现在退休并在Apache阁楼休息。 RE类中的Jakarta Regexp库“match”方法使用CharacterIterator作为参数。

class StreamOrientedKnuthMorrisPratt {
    var m: Int;
    var i: Int;
    var ss:
    var table: Array<Int>;

    public function new(ss: String) {
        this.ss = ss;
        this.buildTable(this.ss);
    }

    public function begin() : Void {
        this.m = 0;
        this.i = 0;
    }

    public function partialSearch(s: String) : Int {
        var offset = this.m + this.i;

        while(this.m + this.i - offset < s.length) {
            if(this.ss.substr(this.i, 1) == s.substr(this.m + this.i - offset,1)) {
                if(this.i == this.ss.length - 1) {
                    return this.m;
                }
                this.i += 1;
            } else {
                this.m += this.i - this.table[this.i];
                if(this.table[this.i] > -1)
                    this.i = this.table[this.i];
                else
                    this.i = 0;
            }
        }

        return -1;
    }

    private function buildTable(ss: String) : Void {
        var pos = 2;
        var cnd = 0;

        this.table = new Array<Int>();
        if(ss.length > 2)
            this.table.insert(ss.length, 0);
        else
            this.table.insert(2, 0);

        this.table[0] = -1;
        this.table[1] = 0;

        while(pos < ss.length) {
            if(ss.substr(pos-1,1) == ss.substr(cnd, 1))
            {
                cnd += 1;
                this.table[pos] = cnd;
                pos += 1;
            } else if(cnd > 0) {
                cnd = this.table[cnd];
            } else {
                this.table[pos] = 0;
                pos += 1;
            }
        }
    }

    public static function main() {
        var KMP = new StreamOrientedKnuthMorrisPratt("aa");
        KMP.begin();
        trace(KMP.partialSearch("ccaabb"));

        KMP.begin();
        trace(KMP.partialSearch("ccarbb"));
        trace(KMP.partialSearch("fgaabb"));

    }
}

答案 3 :(得分:5)

这个答案适用于问题的初始版本,其中关键是只在必要时读取流以匹配String,如果该String存在。此解决方案不符合保证固定内存利用率的要求,但如果您发现此问题且不受该约束约束,则可能值得考虑。

如果您受常量内存使用约束的约束,Java会在堆上存储任何类型的数组,因此对引用进行归零不会以任何方式释放内存;我认为任何涉及循环中的数组的解决方案都会占用堆上的内存并需要GC。


对于简单的实现,也许Java 5的Scanner可以接受InputStream并使用java.util.regex.Pattern来搜索输入,这可能会让您担心实现细节。

以下是潜在实施的示例:

public boolean streamContainsString(Reader reader, String searchString)
            throws IOException {
      Scanner streamScanner = new Scanner(reader);
      if (streamScanner.findWithinHorizon(searchString, 0) != null) {
        return true;
      } else {
        return false;
      }
}

我正在考虑正则表达式,因为它听起来像是一个有限状态自动机的工作,它在初始状态下开始,逐个字符地改变状态,直到它拒绝字符串(不匹配)或进入接受状态。

我认为这可能是您可以使用的最有效的匹配逻辑,并且您如何组织信息的读取可以与匹配逻辑分离以进行性能调整。

这也是正则表达式的工作原理。

答案 4 :(得分:4)

使用实现循环缓冲区的抽象,而不是让缓冲区成为数组。您的索引计算将为buf[(next+i) % sizeof(buf)],您必须小心一次将缓冲区填满一半。但只要搜索字符串适合缓冲区的一半,您就会找到它。

答案 5 :(得分:4)

我认为解决这个问题的最佳方法是尽量保持简单。请记住,因为我正在从流中读取数据,所以我希望将流中的读取次数保持在最低限度(因为网络或磁盘延迟可能是一个问题),同时保持所使用的内存量不变(因为流可能是非常大的尺寸)。字符串匹配的实际效率不是第一目标(因为已经studied to death)。

根据AlbertoPL的建议,这是一个简单的解决方案,它将缓冲区与逐字符的搜索字符串进行比较。关键是因为搜索一次只完成一个字符,所以不需要后向跟踪,因此不需要循环缓冲区或特定大小的缓冲区。

现在,如果某人能够提出基于Knuth-Morris-Pratt search algorithm的类似实现,那么我们就会有一个很好的有效解决方案;)

public boolean streamContainsString(Reader reader, String searchString) throws IOException {
    char[] buffer = new char[1024];
    int numCharsRead;
    int count = 0;
    while((numCharsRead = reader.read(buffer)) > 0) {
        for (int c = 0; c < numCharsRead; c++) {
            if (buffer[c] == searchString.charAt(count))
                count++;
            else
                count = 0;
            if (count == searchString.length()) return true;
        }
    }
    return false;
}

答案 6 :(得分:1)

实施滑动窗口。放置缓冲区,将缓冲区中的所有元素向前移动,最后在缓冲区中输入一个新字符。如果缓冲区等于您搜索到的单词,则包含该缓冲区。

当然,如果你想提高效率,你可以看一种防止缓冲区中所有元素移动的方法,例如通过使用循环缓冲区和“循环”相同的字符串表示缓冲区的方式,所以你只需要检查内容相等性。这样可以节省移动缓冲区中的所有元素。

答案 7 :(得分:1)

我认为你需要在缓冲区之间的边界缓冲一小部分。

例如,如果您的缓冲区大小为1024且SearchString的长度为10,那么除了搜索每个1024字节的缓冲区之外,您还需要搜索两个缓冲区之间的每个18字节转换(距离结尾的9个字节)前一个缓冲区与下一个缓冲区开头的9个字节连接起来。)

答案 8 :(得分:1)

我会说逐字符解决方案,在这种情况下,您将扫描目标文本中的第一个字符,然后当您发现该字符增加一个计数器并查找下一个字符时。每次你没有找到下一个连续的字符重启计数器。它会像这样工作:

public boolean streamContainsString(Reader reader, String searchString) throws IOException {
char[] buffer = new char[1024];
int numCharsRead;
int count = 0;
while((numCharsRead = reader.read(buffer)) > 0) {
    if (buffer[numCharsRead -1] == searchString.charAt(count))
        count++;
    else
        count = 0;

    if (count == searchString.size())    
     return true;
}
return false; 
}

唯一的问题是当你正在浏览角色时......在这种情况下,需要有一种方法来记住你的计数变量。除了作为整个班级的私有变量之外,我没有看到这样做的简单方法。在这种情况下,您不会在此方法中实例化计数。

答案 9 :(得分:1)

如果您不喜欢使用Reader,那么您可以使用Java的NIO API高效地加载文件。例如(未经测试,但应该接近工作):

public boolean streamContainsString(File input, String searchString) throws IOException {
    Pattern pattern = Pattern.compile(Pattern.quote(searchString));

    FileInputStream fis = new FileInputStream(input);
    FileChannel fc = fis.getChannel();

    int sz = (int) fc.size();
    MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz);

    CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
    CharBuffer cb = decoder.decode(bb);

    Matcher matcher = pattern.matcher(cb);

    return matcher.matches();
}

这基本上是mmap()的搜索文件,依赖于操作系统来做关于缓存和内存使用的正确事情。但请注意,对于文件小于10 KiB的文件,只需将文件读入大缓冲区,map()就会更加昂贵。

答案 10 :(得分:1)

在Ujorm框架的RingBuffer类中实现了对流的快速搜索。见样本:

 Reader reader = RingBuffer.createReader("xxx ${abc} ${def} zzz");

 String word1 = RingBuffer.findWord(reader, "${", "}");
 assertEquals("abc", word1);

 String word2 = RingBuffer.findWord(reader, "${", "}");
 assertEquals("def", word2);

 String word3 = RingBuffer.findWord(reader, "${", "}");
 assertEquals("", word3);

SourceForge上提供了单一类实现: 有关详细信息,请参阅link

答案 11 :(得分:0)

您可以使用某些string search algorithm

来提高搜索非常大的字符串的速度

答案 12 :(得分:0)

如果你正在寻找一个恒定的子串而不是一个正则表达式,我会推荐Boyer-Moore。互联网上有很多源代码。

另外,使用循环缓冲区,以避免对缓冲区边界考虑太多。

麦克

答案 13 :(得分:0)

我也有类似的问题:从InputStream中跳过字节,直到指定字符串(或字节数组)。这是基于循环缓冲区的简单代码。效率不高,但能满足我的需求:

  private static boolean matches(int[] buffer, int offset, byte[] search) {
    final int len = buffer.length;
    for (int i = 0; i < len; ++i) {
      if (search[i] != buffer[(offset + i) % len]) {
        return false;
      }
    }
    return true;
  }

  public static void skipBytes(InputStream stream, byte[] search) throws IOException {
    final int[] buffer = new int[search.length];
    for (int i = 0; i < search.length; ++i) {
      buffer[i] = stream.read();
    }

    int offset = 0;
    while (true) {
      if (matches(buffer, offset, search)) {
        break;
      }
      buffer[offset] = stream.read();
      offset = (offset + 1) % buffer.length;
    }
  }

答案 14 :(得分:0)

您可以使用快速傅立叶变换实现非常快速的解决方案,如果实施得当,它允许您在时间O(nlog(m))中进行字符串匹配,其中n是较长字符串的长度匹配,m是较短字符串的长度。例如,您可以在收到长度为m的流输入后立即执行FFT,如果匹配,则可以返回,如果不匹配,则可以丢弃流输入中的第一个字符,等待要通过流显示新字符,然后再次执行FFT。