#haskell #complexity-theory #ghci #cumulative-sum
#haskell #сложность-теория #ghci #кумулятивная сумма
Вопрос:
Рассмотрим следующие две функции кумулятивной суммы (cumsum):
cumsum :: Num a => [a] -> [a]
cumsum [] = []
cumsum [x] = [x]
cumsum (x:y:ys) = x : (cumsum $ (x y) : ys)
и
cumsum' :: Num a => [a] -> [a]
cumsum' x = [sum $ take k x | k <- [1..length x]]
Конечно, я предпочитаю определение cumsum
cumsum'
и я понимаю, что первое имеет линейную сложность.
Но почему cumsum'
также имеет линейную сложность? take
сама по себе имеет линейную сложность по длине своего аргумента и k
выполняется от 1
до length x
. Поэтому я ожидал бы квадратичной сложности для cumsum'
.
Более того, константа cumsum'
меньше, чем у cumsum
. Связано ли это с рекурсивным добавлением последней в список?
ПРИМЕЧАНИЕ: приветствуется любое разумное определение кумулятивной суммы.
РЕДАКТИРОВАТЬ: я измеряю время выполнения с помощью (после включения :set s
в GHCi):
last $ cumsum [1..n]
Комментарии:
1. Я подозреваю, что вы ошибаетесь в своих измерениях — я не вижу, как
cumsum'
может быть линейная сложность.2. FWIW,
scanl ( ) 0
3. Я только что измерил это сам, и это определенно квадратично. Вы сделали что-то слишком ленивое, чтобы избежать распечатки выходных данных?
4. Это действительно слишком лениво. Он не будет вычислять промежуточные суммы. Я использовал
max
вместоlast
.5.
x -> [take k x | k <- [1..length x]]
являетсяtail . inits
, за исключением того, что последняя не вычисляет длину и не начинает заново создавать каждый подсписок. Ваша функцияmap sum . tail . inits
Ответ №1:
Это ошибка измерения, вызванная ленью.
Каждое значение в Haskell является ленивым: оно не вычисляется до тех пор, пока это необходимо. Это включает в себя подструктуру значений — так, например, когда мы видим шаблон ( x:xs
), это только заставляет оценивать список достаточно далеко, чтобы определить, что список непустой, но это не влияет на начало x
или конец xs
.
Определение last
выглядит примерно так:
last [x] = x
last (x:xs) = last xs
Таким образом, когда last
применяется к результату cumsum'
, он рекурсивно проверяет понимание списка, но только настолько, чтобы отследить последнюю запись. Это не приводит к принудительному вводу ни одной из записей, но возвращает последнюю.
Когда эта последняя запись печатается в ghci или любом другом, выполняется принудительное выполнение, что, как и ожидалось, занимает линейное время. Но другие записи никогда не вычисляются, поэтому мы не видим «ожидаемого» квадратичного поведения.
Использование maximum
вместо last
демонстрирует, что cumnorm'
является квадратичным, тогда как cumnorm
является линейным.
[Примечание: это объяснение несколько размыто от руки: на самом деле оценка полностью определяется тем, что необходимо для конечного результата, поэтому even last
вычисляется вообще только потому, что необходим его результат. Найдите такие вещи, как «Порядок оценки Haskell» и «Нормальная форма слабой головки», чтобы получить более точное объяснение.]