回收计算快速优化

时间:2016-10-20 16:16:06

标签: swift algorithm string-algorithm

嘿,我有一个关于优化回文计数算法的问题

  

任务:查找字符串中的回文数。

在我的功能中我使用“在额头”方法,它像O(n ^ 2) 你能帮助他们在O(n)或O(nlogn)中做到吗

func isPalindrome(string: String) -> Bool {
    let str = (string.lowercased())
    let strWithoutSpace = str.components(separatedBy: .whitespaces).joined(separator: "")
    let strArray = Array(strWithoutSpace.characters)
    var i = 0
    var j = strArray.count-1
    while i <= j {
        if strArray[i] != strArray[j] { return false }
        i+=1
        j-=1
    }
    return true
}
func palindromsInString(string: String) -> Int {
    var arrayOfChars = Array(string.characters)
    var count = 0
    for i in 0..<arrayOfChars.count-1 {
        for x in i+1..<arrayOfChars.count {
            if isPalindrome(string: String(arrayOfChars[i...x])) {
                count+=1
            }
        }
    }
    return count
}

并且在我的实例中是的,一封信不能成为回文

3 个答案:

答案 0 :(得分:2)

您可以使用Manacher的算法在线性时间内解决它。该算法通常用于寻找最长的回文,但它计算回文的最大长度,该回归在字符串中的每个位置具有特定位置的中心。

您可以在this question中找到此算法的说明和实现。

答案 1 :(得分:2)

我对Manacher的算法并不熟悉,但我总是很喜欢找出有效的算法,所以我觉得我对此有所了解。

您确定字符串是否为回文的算法看起来像我提出的类型,所以我决定只使用您的isPalindrome函数,尽管我将其更改为一个函数相反,String的扩展,我删除了空白删除逻辑,因为我觉得需要在调用调用而不是在回文确定函数本身。

extension String {
    func isPalindrome() -> Bool {
        if length < 2 { return false }
        let str = lowercased()
        let strArray = Array(str.characters)
        var i = 0
        var j = strArray.count-1
        while i <= j {
            if strArray[i] != strArray[j] { return false }
            i+=1
            j-=1
        }
        return true
    }
}

在查看您的palindromsInString解决方案后,它看起来像是一个标准的蛮力,但简单易读的解决方案。

我对不同算法的第一个想法也是相当暴力的,但这是一种完全不同的方法,所以我称之为Naive解决方案。

Naive解决方案的想法是创建原始字符串的子串的数组,并检查每个子字符串是否是回文。我确定子字符串的方法是从最大的子字符串开始(原始字符串),然后获得长度为string.length-1的2个子字符串,然后是string.length-2,依此类推,直到最后我得到所有子字符串长度为2(我忽略了长度为1的子串,因为你说长度为1的字符串不能成为回文)。

  

ie:&#34; test&#34;的子串大于1的长度将是:

     
    

[&#34;测试&#34;]     [&#34; tes&#34;,&#34; est&#34;]     [&#34; te&#34;,&#34; es&#34;,&#34; st&#34;]

  

所以你只需循环遍历每个数组并检查是否有任何回文,如果是,则增加计数:

天真的解决方案:

extension String {
    var length: Int { return characters.count }

    func substringsOfLength(length: Int) -> [String] {
        if self.length == 0 || length > self.length { return [] }

        let differenceInLengths = self.length - length

        var firstIndex = startIndex
        var lastIndex = index(startIndex, offsetBy: length)
        var substrings: [String] = []

        for _ in 0..<differenceInLengths {
            substrings.append(substring(with: Range(uncheckedBounds: (firstIndex, lastIndex))))
            firstIndex = index(after: firstIndex)
            lastIndex = index(after: lastIndex)
        }
        substrings.append(substring(with: Range(uncheckedBounds: (firstIndex, lastIndex))))

        return substrings
    }
}

extension String {
    func containsAPalindromeNaive(ignoringWhitespace: Bool = true) -> Int {
        let numChars = length

        if numChars < 2 { return 0 }

        let stringToCheck = (ignoringWhitespace ? self.components(separatedBy: .whitespaces).joined(separator: "") : self).lowercased()
        var outerLoop = numChars
        var count: Int = 0

        while outerLoop > 0 {
            let substrings = stringToCheck.substringsOfLength(length: outerLoop)
            for substring in substrings {
                if substring.isPalindrome() {
                    count += 1
                }
            }
            outerLoop -= 1
        }

        return count
    }
}

我完全清楚这个算法会很慢,但我想把它作为我真实解决方案的第二个基线来实现。

我将此解决方案称为智能解决方案。它是一种多通道解决方案,它利用字符串中字符的数量和位置。

在第一遍中,我生成了我称之为字符映射的内容。字符映射是将Character映射到索引数组的字典。因此,您将遍历字符串并将每个字符的索引添加到存储在其下的字符值作为键的数组中。

这个想法是,回文只能存在于由同一个字母书写的字符串中。因此,您只需要在特定字母的索引处检查字符串中的子字符串。在&#34;纹身&#34;中,你有3个不同的字母:&#34; t&#34;,&#34; a&#34;,&#34; o&#34;。字符映射看起来像这样:

t: [0,2,3]
a: [1]
o: [4,5]

我现在知道回文只能存在于(0,2),(2,3)和(4,5)之间的这个词中。所以我只需要检查3个子串 (0,2),(0,3),(2,3)和(4,5)。所以我只需要检查4个子串。这就是想法。一旦您检查了特定字母所预订的所有可能的子字符串,您就可以忽略从该字母开头遇到的任何其他子字符串,因为您已经检查过它们。

在第二遍中,我浏览了字符串中的每个字符,如果我还没有检查过那个字母,我会查看由<{1}}生成的排列索引对< / strong>字符映射中的索引并检查子字符串以查看它们是否是回文。然后我在这里进行2次优化。首先,如果起始字符索引和结束字符索引之间的距离小于3,则它必须是回文(距离1表示它们是连续的,距离2表示它们之间是单个字母) ,因此也保证是回文)。其次,因为我已经知道第一个和最后一个字符是相同的,所以我不需要检查整个子字符串,只需从第二个字母开始直到第二个字母到最后一个字母。因此,如果子字符串是&#34; test&#34;并且我总是保证自己子字符串由同一个字母书写,我实际上不需要检查&#34; test&# 34;,而我只需检查&#34; es&#34;。它是一个较小的优化,但仍然很好。

智能解决方案:

generateOrderedPairIndexPermutations

我觉得这个解决方案很不错。 但我不知道它到底有多快。 真的很快

使用XCTest测量性能,我通过一些性能测试运行每个算法。使用这个字符串作为基础来源:&#34;这里有多个回文&#34; &#34;它是我看过的汽车还是猫?&#34;,基于更新关于使用更严格的输入字符串的建议,当删除空格并且它是小写时, 33 19 字符长(&#34;还有多个方面的内容&#34; &#34; wasitacaroracatisaw&#34; ),我还创建了这个字符串时间为2的副本(&#34;还有多个方面,其中还有多个方面的内容&#34; wasitacaroracatisawwasitacaroracatisaw ),次数4,次数8和次数10.由于我们正在尝试确定算法的O(),因此缩放字母数量并测量比例因子是要走的路。

为了获得更准确的数据,我通过10次迭代运行每个测试(我会更喜欢更多的迭代,但原始解决方案和我的Naive解决方案都没有及时完成以上测试4)。这是我收集的时间数据(电子表格的屏幕截图比在此处再次输入更容易):

<强>已更新 Timings Table

<强>已更新 格林是作者;红色是天真的解决方案;橙色是智能解决方案 Timings Graph

正如您所看到的,您的原始解决方案和我的Naive解决方案都以二次方式运行(您的解决方案具有二次相关系数r = 0.9997,而我的朴素解决方案的系数r = 0.9999;因此,非常明显的二次方!)我的天真解决方案似乎在你的解决方案之下,但它们都是二次增加,因此我们已经知道它们是O(n ^ 2)。

关于我的智能解决方案的有趣部分是线性的!我通过回归计算器设置了小的5点数据,它的相关系数为r = 0.9917!因此,如果它不是线性的,它就是如此接近,以至于我不在乎。

所有解决方案现在都以二次方式运行。 Le叹息。但至少这个漏洞被发现,解决了,科学占了上风。让我的&#34;智能解决方案&#34;实际上并没有线性化。但是,我会注意到,如果输入字符串已经不是一个巨大的回文(就像我最终改变它的那个),那么&#34; Smart Solution&#34;的优化使得它表现得更快,尽管仍处于二次时间。

我不知道我的算法是否比Manacher的算法更容易理解,但我希望我能很好地解释它。结果非常有希望,所以我希望你能从中找到一个好的用途。 这实际上仍然是真的。我认为这是一个很有前途的算法。也许我的extension Collection { func generateOrderedPairIndexPermutations() -> [(Index,Index)] { if count < 2 { return [] } var perms: [(Index,Index)] = [] var firstIndex = startIndex while firstIndex != endIndex { var secondIndex = index(firstIndex, offsetBy: 1) while secondIndex != endIndex { perms.append((firstIndex,secondIndex)) secondIndex = index(secondIndex, offsetBy: 1) } firstIndex = index(firstIndex, offsetBy: 1) } return perms } } extension String { func generateCharacterMapping() -> [Character : [Int]] { var characterMapping: [Character : [Int]] = [:] for (index, char) in characters.enumerated() { if let indicesOfChar = characterMapping[char] { characterMapping[char] = indicesOfChar + [index] } else { characterMapping[char] = [index] } } return characterMapping } func containsAPalindromeSmart(ignoringWhitespace: Bool = true) -> Int { let numChars = length if numChars < 2 { return 0 } let stringToCheck = (ignoringWhitespace ? self.components(separatedBy: .whitespaces).joined(separator: "") : self).lowercased() let characterMapping = stringToCheck.generateCharacterMapping() var count: Int = 0 var checkedChars: Set<Character> = Set() for char in stringToCheck.characters { if checkedChars.contains(char) == false { if let characterIndices = characterMapping[char], characterIndices.count > 1 { let perms = characterIndices.generateOrderedPairIndexPermutations() for (i,j) in perms { let startCharIndex = characterIndices[i] let endCharIndex = characterIndices[j] if endCharIndex - startCharIndex < 3 { count += 1 } else { let substring = stringToCheck.substring(with: Range(uncheckedBounds: (stringToCheck.index(stringToCheck.startIndex, offsetBy: startCharIndex+1), stringToCheck.index(stringToCheck.startIndex, offsetBy: endCharIndex)))) if substring.isPalindrome() { count += 1 } } } checkedChars.insert(char) } } } return count } } 代码并不是最好的。

已更新,以解决kraskevich

发现的错误

答案 2 :(得分:0)

这是&#34;功能编程&#34;解决方案受到过程指数性质的影响要小于接受的答案。 (也少了很多代码)

短(19)弦的速度提高了80%,而较长(19)的弦提高了90倍(190)。我还没有正式证明它,但似乎是线性的O(n)?。

public func countPalindromes(in text:String) -> Int
{
   let words  = text.lowercased()
                    .components(separatedBy:CharacterSet.letters.inverted)
                    .filter{!$0.isEmpty}
                    .joined(separator:"") 

   let sdrow  = String(words.characters.reversed())

   let splits = zip( sdrow.characters.indices.dropFirst().reversed(),
                     words.characters.indices.dropFirst() 
                   )
                .map{ (sdrow.substring(from:$0),words.substring(from:$1), words[$1...$1] ) }

   let count  = splits.map{$0.1.commonPrefix(with:$0.0)}  // even
                      .filter{ !$0.isEmpty }
                      .reduce(0){$0 + $1.characters.count}
              + splits.map{ $1.commonPrefix(with:$2 + $0)} // odd
                      .filter{$0.characters.count > 1 }
                      .reduce(0){$0 + $1.characters.count - 1}
   return count
}

// how it works ...

// words   contains the stripped down text (with only letters)
//
// sdrow   is a reversed version of words
//
// splits  creates split pairs for each character in the string.
//         Each tuple in the array contains a reversed left part, a right part
//         and the splitting character
//         The right part includes the splitting character 
//         but the left part does not.
//
//         [(String,String,String)] for [(left, right, splitChar)]
//
//         The sdrow string produces the left part in reversed letter order .
//         This "mirrored" left part will have a common prefix with the
//         right part if the split character's position is in the middle (+1)
//         of a palindrome that has an even number of characters
//
//         For palindromes with an odd number of characters, 
//         the reversed left part needs to add the splitting character
//         to match its common prefix with the right part.
//
// count   computes the total of odd and even palindromes from the
//         size of common prefixes. Each of the common prefix can produce
//         as many palindromes as its length (minus one for the odd ones)

[编辑]为了进行比较,我还制作了一个程序版本,因为他知道编译器可以比声明性对应物更好地优化过程代码。

它是Array类型的扩展(因此它可以计算任何可比较的回文)。

extension Array where Element:Comparable
{
   public func countPalindromes() -> Int
   {
      if count < 2 { return 0 }

      var result = 0

      for splitIndex in (1..<count)
      {
         var leftIndex      = splitIndex - 1
         var rightIndex     = splitIndex
         var oddPalindrome  = true
         var evenPalindrome = true
         while leftIndex >= 0 && rightIndex < count
         {
            if evenPalindrome  
            && self[leftIndex] == self[rightIndex]
            { result += 1 }
            else
            { evenPalindrome = false }

            if oddPalindrome   
            && rightIndex < count - 1
            && self[leftIndex] == self[rightIndex+1]
            { result += 1 }
            else
            { oddPalindrome = false }

            guard oddPalindrome || evenPalindrome
            else { break }

            leftIndex  -= 1
            rightIndex += 1
         }
      }      
      return result
   }

} 

public func countPalindromesFromArray(in text:String) -> Int
{
   let words  = text.lowercased()
                    .components(separatedBy:CharacterSet.letters.inverted)
                    .filter{!$0.isEmpty}
                    .joined(separator:"") 
   return Array(words.characters).countPalindromes()
}

它比声明的速度快5到13倍,比接受的答案快1200倍。

声明性解决方案的性能差异越来越大,告诉我它不是O(n)。程序版本可能是O(n),因为它的时间会随着回文的数量而变化,但会与数组的大小成正比。