【问题标题】:Find all indices of a search term in a string在字符串中查找搜索词的所有索引
【发布时间】:2017-09-27 21:58:14
【问题描述】:

我需要一种快速的方法来查找可能出现在字符串中的搜索词的所有索引。我尝试了这种“蛮力”String 扩展方法:

// Note: makes use of ExSwift
extension String
{
    var length: Int { return count(self) }

    func indicesOf(searchTerm:String) -> [Int] {
        var indices = [Int]()
        for i in 0 ..< self.length {
            let segment = self[i ... (i + searchTerm.length - 1)]
            if (segment == searchTerm) {
                indices.append(i)
            }
        }
        return indices;
    }
}

...但它的速度非常慢,尤其是搜索词越短。快速找到所有索引的更好方法是什么?

【问题讨论】:

  • 您使用的是哪个 Xcode 版本?您的代码无法在 Xcode 6.3.2 或 Xcode 7 beta 中编译。
  • Xcode 6.3.2。也许您缺少 String.length? var length: Int { return count(self) }
  • String 在我的 Xcode 6.3.2 中没有属性长度(而且你不能用整数为字符串下标)。也许您正在使用一些扩展?
  • 我更新了代码以使其有意义。
  • 哦,对了,下标……我用的是 ExSwift。

标签: string swift


【解决方案1】:

正如 Martin 所说,您可以在字符串匹配中实现一些众所周知的最快算法,Knuth–Morris–Pratt 字符串搜索算法(或 KMP 算法)搜索“单词”W 的出现在一个主要的“文本字符串”S中。

算法复杂度O(n),其中nS的长度,Obig-O notation

extension String {

    // Build pi function of prefixes
    private func build_pi(str: String) -> [Int] {

       var n = count(str)
       var pi = Array(count: n + 1, repeatedValue: 0)
       var k = -1
       pi[0] = -1

       for (var i = 0; i < n; ++i) {
           while (k >= 0 && str[k] != str[i]) {
              k = pi[k]
           }
           pi[i + 1] = ++k
       }

       return pi
    }

    // Knuth-Morris Pratt algorithm
    func searchPattern(pattern: String) -> [Int] {

       var matches = [Int]()
       var n = count(self)

       var m = count(pattern)
       var k = 0
       var pi = build_pi(pattern)

       for var i = 0; i < n; ++i {
           while (k >= 0 && (k == m || pattern[k] != self[i])) {
              k = pi[k]
           }
           if ++k == m {
              matches.append(i - m + 1)
           }
       }

       return matches
    }

    subscript (i: Int) -> Character {
        return self[advance(self.startIndex, i)]
    }
}

那么你可以通过以下方式使用它:

var string = "apurba mandal loves ayoshi loves"
var pattern = "loves"

println(string.searchPattern(pattern))

输出应该是:

[14, 27]

属于字符串内部模式出现的起始索引。希望对您有所帮助。

编辑:

正如 Martin 在他的评论中所说,您需要避免使用 advance 函数通过 Int 来索引 String,因为它是 O(索引位置)

一种可能的解决方案是将String 转换为Character 的数组,然后访问索引是O(1)

那么extension可以改成这个:

extension String {

   // Build pi function of prefixes
   private func build_pi(str: [Character]) -> [Int] {

      var n = count(str)
      var pi = Array(count: n + 1, repeatedValue: 0)
      var k = -1
      pi[0] = -1

      for (var i = 0; i < n; ++i) {
          while (k >= 0 && str[k] != str[i]) {
              k = pi[k]
          }
          pi[i + 1] = ++k
      }

      return pi
   }

   // Knuth-Morris Pratt algorithm
   func searchPattern(pattern: String) -> [Int] {

      // Convert to Character array to index in O(1)
      var patt = Array(pattern)
      var S = Array(self)

      var matches = [Int]()
      var n = count(self)

      var m = count(pattern)
      var k = 0
      var pi = build_pi(patt)

      for var i = 0; i < n; ++i {
         while (k >= 0 && (k == m || patt[k] != S[i])) {
             k = pi[k]
         }
         if ++k == m {
             matches.append(i - m + 1)
         }
      }

      return matches
   }
}

【讨论】:

  • 请注意,advance(self.startIndex, i) for strings 在 O(i) 中执行,因此也必须考虑到这一点。如果可能的话,使用String.Index 而不是Int 索引可能会更好。
  • 是的,你是对的@MartinR 我会更新我的答案。感谢您的观察。
  • 我做了一些简单的性能测试,你更新的代码比原来的版本快了大约 250 倍。它比我的(基于简单 rangeOfString 的)方法慢还是快似乎取决于搜索字符串以及它出现的频率。通常,对于更频繁出现的较短字符串,您的方法更快。我假设 rangeOfString 在内部也使用了一些复杂的搜索方法。
  • 确切知道使用什么算法rangeOfString 可能非常有趣,我试图找出答案,但到目前为止还没有运气。如果我们想在许多不同的文本中重复搜索相同的模式,Knuth-Morris-Pratt (KMP) 算法是一个不错的选择。
  • 这里有很棒的建议!感谢您的代码示例!目前,Martin 的方法对我的目的来说运行得足够快,但我可能会在以后将 KMP 搜索添加到我的库中。
【解决方案2】:

而不是在字符串的每个位置检查搜索词 您可以使用rangeOfString() 查找下一个事件(希望 rangeOfString() 使用更高级的算法):

extension String {

    func indicesOf(searchTerm:String) -> [Int] {
        var indices = [Int]()
        var pos = self.startIndex
        while let range = self.rangeOfString(searchTerm, range: pos ..< self.endIndex) {
            indices.append(distance(self.startIndex, range.startIndex))
            pos = range.startIndex.successor()
        }
        return indices
    }
}

一般情况下,取决于输入字符串的大小和大小 搜索字符串的哪个算法是“最快的”。你会找到 包含各种算法链接的概述 String searching algorithm.

Swift 3 更新:

extension String {

    func indices(of searchTerm:String) -> [Int] {
        var indices = [Int]()
        var pos = self.startIndex
        while let range = range(of: searchTerm, range: pos ..< self.endIndex) {
            indices.append(distance(from: startIndex, to: range.lowerBound))
            pos = index(after: range.lowerBound)
        }
        return indices
    }
}

【讨论】:

  • 您的方法要快得多,我正在测试它的文本有 1000 到 2000 个字符。感谢您提供此解决方案。我可以再次将正则表达式实验放回抽屉。
  • @Martin R 而不是 indices.append(distance(from: startIndex, to: range.lowerBound)) 可以是 arrayOfIndices.append(range.lowerBound.encodedOffset) 更快吗?
  • @VYT:可能是这样,但它给出了不同的结果。试试"?!".indices(of: "!")
  • @Martin R 是的,对于 emojis 这不是很好。 (我没有使用带有表情符号的字符串代码)。
【解决方案3】:

在 Swift 4 中使用 NSRegularExpression,你可以这样做。 NSRegularExpression 一直存在,在大多数情况下,它可能是比使用自己的算法更好的选择。

let text = "The quieter you become, the more you can hear."
let searchTerm = "you"

let regex = try! NSRegularExpression(pattern: searchTerm, options: [])
let range: NSRange = NSRange(text.startIndex ..< text.endIndex, in: text)
let matches: [NSTextCheckingResult] = regex.matches(in: text, options: [], range: range)
let ranges: [NSRange] = matches.map { $0.range }
let indices: [Int] = ranges.map { $0.location }
let swiftRanges = ranges.map { Range($0, in: text) }
let swiftIndices: [String.Index] = swiftRanges.flatMap { $0?.lowerBound }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-05-29
    • 1970-01-01
    • 2013-11-04
    • 2021-09-09
    • 2018-11-20
    • 2021-02-15
    • 2012-11-15
    • 1970-01-01
    相关资源
    最近更新 更多