Эффективен (реверс. F. Реверс)?

Много раз я вижу функции, которые работают с заголовком списка, например:

trimHead ('\n':xs) = xs
trimHead xs        = xs

то я вижу определение:

trimTail = reverse . trimHead . reverse

то я вижу:

trimBoth = trimHead . trimTail

Они чисты, но эффективны trimTail и trimBoth? Есть ли лучший способ?

Ответ 1

Рассмотрим эту альтернативную реализацию

trimTail2 [] = []
trimTail2 ['\n'] = []
trimTail2 (x:xs) = x : trimTail2 xs

trimBoth2 = trimHead . trimTail2

Легко убедиться, что trimTail и trimBoth требуют, чтобы весь список был оценен, а trimTail2 и trimBoth2 оценивали только столько, сколько необходимо.

*Main> head $ trimTail ('h':undefined)
*** Exception: Prelude.undefined
*Main> head $ trimBoth ('h':undefined)
*** Exception: Prelude.undefined
*Main> head $ trimTail2 ('h':undefined)
'h'
*Main> head $ trimBoth2 ('h':undefined)
'h'

Это означает, что ваша версия будет менее эффективной, если весь результат не понадобится.

Ответ 2

Это неэффективно в том смысле, что потоковая передача невозможна, потому что весь список необходимо оценить, чтобы получить хотя бы один элемент. Но лучшее решение сложно, так как вам нужно оценить остальную часть списка, чтобы узнать, нужно ли обрезать линию или нет. Несколько более эффективным способом было бы заглянуть в будущее, нужно ли обрезать линию и отреагировать соответствующим образом:

trimTail, trimHead, trimBoth :: String -> String
trimTail ('\n':xs) | all (=='\n') xs = ""
trimTail (x:xs)                      = x : trimTail xs

trimHead = dropWhile (=='\n')

trimBoth = trimTail . trimHead

Вышеприведенное решение оценивает только столько, сколько необходимо от строки, чтобы знать, если обрезка строки должна быть обрезана. Еще лучшим методом было бы включение знания, чтобы следующие n символов не были обрезаны. Реализация этого оставлена ​​как упражнение для читателя.

Еще лучший (и более короткий) способ писать trimTail - это путь (по ротсору):

trimTail = foldr step [] where
  step '\n' [] = []
  step x xs = x:xs

Как правило, старайтесь избегать reverse. Обычно есть лучший способ решить проблему.

Ответ 3

Предполагая, что весь список должен быть оценен (если вам не нужен весь список, почему вы обрезаете конец?), он примерно вдвое эффективнее, чем вы можете выйти из неизменяемых списков, но он имеет одинаковый асимптотическая сложность O (n).

Новый список требует не менее:

  • Вы должны найти конец: n обходов указателей.
  • Вы должны изменить конец и, следовательно, какие точки до конца и т.д.: n минусы существующих данных с новыми указателями.

reverse . trimHead . reverse выполняет примерно в два раза:

  • Первый reverse выполняет n обходов указателей и n cons.
  • trimHead возможно выполняет 1 обход указателя.
  • Второй reverse выполняет n обходов указателей и n cons.

Стоит ли беспокоиться об этом? Возможно, в некоторых случаях. Является ли код слишком медленным, и это называется много? В других, может быть, нет. Benchmark! Реализация с помощью reverse хороша и понятна и важна.

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

Ответ 4

Эффективны ли trimHead и trimTail?

Они берут O (n) время (время, прямо пропорциональное размеру списка), так как весь список должен быть пройден дважды, чтобы выполнить два обращения.

Есть ли лучший способ?

Ну, вам нужно использовать списки? С помощью Data.Sequence вы можете изменять любой конец списка в постоянное время. Если вы застряли в списках, ознакомьтесь с другими предлагаемыми здесь решениями. Если вы можете использовать последовательности вместо этого, просто измените ответ FUZxxl, чтобы использовать dropWhileR.