Медленная скорость строки

Я пытаюсь решить вопрос о разделении Палиндрома. Вы можете найти вопрос в https://leetcode.com/problems/palindrome-partitioning/.

И я придумал решение:

func partition(_ s: String) -> [[String]] {

    var result: [[String]] = []

    func dfs(string: String, partiton: [String]) {

        if string.characters.count == 0 {
            result.append(partiton)
            return
        }

        for length in 1...string.characters.count {

            let endIndex =  string.index(string.startIndex, offsetBy: length-1)
            let part = string[string.startIndex...endIndex]


            if isPalindrome(part) {
                let leftPart = string[string.index(after: endIndex)..<string.endIndex]
                print("string: \(string) part: \(part) leftpart: \(leftPart)")
                dfs(string: leftPart, partiton: partiton + [part])
            }
        }
    }

    func isPalindrome(_ s: String) -> Bool {
        if String(s.characters.reversed()) == s {
            return true
        } else {
            return false
        }
    }

    dfs(string: s, partiton: [])
    return result
}

Но производительность плохая. Превышен лимит времени.

Но та же идея с реализацией Python может пройти:

def partition(self, s):
    res = []
    self.dfs(s, [], res)
    return res

def dfs(self, s, path, res):
    if not s:
        res.append(path)
        return
    for i in range(1, len(s)+1):
        if self.isPal(s[:i]):
            self.dfs(s[i:], path+[s[:i]], res)

def isPal(self, s):
    return s == s[::-1]

Мне интересно, как улучшить быструю реализацию и почему быстрая реализация медленнее, чем python.

Ответ 1

A Swift String представляет собой набор из Character s, a Character представляет собой один расширенный кластер графем, который может быть одним или несколькими Сканеры Unicode. Это делает некоторые операции индекса, такие как "пропустить первые N символов" медленными.

Но первое улучшение заключается в "коротком замыкании" isPalindrome() функция. Вместо того, чтобы полностью построить обратную строку, сравните последовательность символов с измененной последовательностью и остановка как только как различие найдено:

func isPalindrome(_ s: String) -> Bool {
    return !zip(s.characters, s.characters.reversed()).contains { $0 != $1 }
}

s.characters.reversed() не создает новую коллекцию в обратном порядке order, он просто перечисляет персонажей изнутри назад. Однако с String(s.characters.reversed()), как в вашем методе, вы принудительно создаете новую коллекцию для инвертированной строки, что замедляет работу.

Для строки с 110 символами

let string = String(repeating: "Hello world", count: 10)

это уменьшает время вычисления от 6 сек до 1,2 с в моем тесте.

Затем избегайте вычислений индекса, например

let endIndex = string.index(string.startIndex, offsetBy: length-1)

и вместо этого перебираем сам индекс символа:

func partition(_ s: String) -> [[String]] {

    var result: [[String]] = []

    func dfs(string: String, partiton: [String]) {
        if string.isEmpty {
            result.append(partiton)
            return
        }

        var idx = string.startIndex
        repeat {
            string.characters.formIndex(after: &idx)
            let part = string.substring(to: idx)
            if isPalindrome(part) {
                let leftPart = string.substring(from: idx)
                dfs(string: leftPart, partiton: partiton + [part])
            }
        } while idx != string.endIndex
    }

    func isPalindrome(_ s: String) -> Bool {
        return !zip(s.characters, s.characters.reversed()).contains { $0 != $1 }
    }

    dfs(string: s, partiton: [])
    return result
}

Время вычисления составляет 0,7 с.

Следующий шаг - полностью исключить индексацию строк и работать с массив символов, потому что индексирование массива выполняется быстро. Даже лучше, использовать срезы массива, которые быстро создают и ссылаются на оригинал элементы массива:

func partition(_ s: String) -> [[String]] {

    var result: [[String]] = []

    func dfs(chars: ArraySlice<Character>, partiton: [String]) {

        if chars.isEmpty {
            result.append(partiton)
            return
        }

        for length in 1...chars.count {
            let part = chars.prefix(length)
            if isPalindrome(part) {
                let leftPart = chars.dropFirst(length)
                dfs(chars: leftPart, partiton: partiton + [String(part)])
            }
        }
    }

    func isPalindrome(_ c: ArraySlice<Character>) -> Bool {
        return !zip(c, c.reversed()).contains { $0 != $1 }
    }

    dfs(chars: ArraySlice(s.characters), partiton: [])
    return result
}

Время вычисления составляет 0,08 с.

Если ваша строка содержит только символы в "базовой многоязычной плоскости" (т.е. <= U + FFFF), вы можете вместо этого работать с кодовыми точками UTF-16:

func partition(_ s: String) -> [[String]] {

    var result: [[String]] = []

    func dfs(chars: ArraySlice<UInt16>, partiton: [String]) {

        if chars.isEmpty {
            result.append(partiton)
            return
        }

        for length in 1...chars.count {
            let part = chars.prefix(length)
            if isPalindrome(part) {
                let leftPart = chars.dropFirst(length)
                part.withUnsafeBufferPointer {
                    dfs(chars: leftPart, partiton: partiton + [String(utf16CodeUnits: $0.baseAddress!, count: length)])
                }
            }
        }
    }

    func isPalindrome(_ c: ArraySlice<UInt16>) -> Bool {
        return !zip(c, c.reversed()).contains { $0 != $1 }
    }

    dfs(chars: ArraySlice(s.utf16), partiton: [])
    return result
}

Время вычисления составляет 0,04 с для тестовой строки 110 символов.


Итак, некоторые советы, которые потенциально могут улучшить производительность при работе со строками Swift, это

  • Итерации по символам/индексам последовательно. Избегайте "прыгать" к n-й позиции.
  • Если вам нужен "случайный" доступ ко всем символам, преобразуйте строку к массиву в первую очередь.
  • Работа с представлением UTF-16 строки может быть более быстрой, чем работа с видом символов.

Конечно, это зависит от фактического использования. В этом приложении, мы смогли сократить время вычислений с 6 с до 0,04 с, что составляет 150.