Какой контейнер действительно имитирует std::vector в Haskell?
Проблема
Я ищу контейнер, который используется для сохранения частичных результатов проблем n - 1
для вычисления n
th. Это означает, что размер контейнера в конце всегда будет n
.
Каждый элемент i
контейнера зависит от не менее 2 и до 4 предыдущих результатов.
Контейнер должен предоставить:
- вставки времени в начале или конце (один из двух, не обязательно оба)
- индексирование постоянного времени в середине
или, альтернативно (при инициализации O(n)
):
- редактирование отдельных элементов с постоянным временем
- индексирование постоянного времени в середине
Что такое std::vector
и почему это релевантно
Для тех из вас, кто не знает С++, std::vector
представляет собой массив с динамическим размером. Это идеально подходит для этой проблемы, потому что она способна:
- резервное пространство при строительстве
- предлагает индексирование постоянного времени в середине
- предлагает постоянную установку времени в конце (с зарезервированным пространством)
Поэтому эта проблема разрешима в сложности O(n)
в С++.
Почему Data.Vector
не std::vector
Data.Vector
вместе с Data.Array
обеспечивают аналогичную функциональность std::vector
, но не совсем то же самое. Оба они, конечно, предлагают индексирование по постоянному времени в середине, но они не предлагают ни постоянной модификации времени ((//)
, например, не менее O(n)
), ни постоянной установки времени в начале начала.
Заключение
Какой контейнер действительно имитирует std::vector
в Haskell? Или, каков мой лучший снимок?
Ответы
Ответ 1
Из reddit предлагается использовать Data.Vector.constructN
:
O (n) Построить вектор с n элементами, повторно применяя генераторную функцию к уже построенной части вектора.
constructN 3 f = let a = f <> ; b = f <a> ; c = f <a,b> in f <a,b,c>
Например:
λ import qualified Data.Vector as V
λ V.constructN 10 V.length
fromList [0,1,2,3,4,5,6,7,8,9]
λ V.constructN 10 $ (1+) . V.sum
fromList [1,2,4,8,16,32,64,128,256,512]
λ V.constructN 10 $ \v -> let n = V.length v in if n <= 1 then 1 else (v V.! (n - 1)) + (v V.! (n - 2))
fromList [1,1,2,3,5,8,13,21,34,55]
Это, безусловно, подходит для решения проблемы, как вы описали ее выше.
Ответ 2
Первые структуры данных, которые приходят мне на ум, - это Карты из Data.Map
или Последовательности из Data.Sequence
.
Update
Последовательности представляют собой постоянные структуры данных, которые обеспечивают большую эффективность операций, позволяя только конечные последовательности. Их реализация основана на пальцах, если вы заинтересованы. Но какие качества у него есть?
- O (1) вычисление длины
- O (1) вставить спереди/сзади с операторами
<|
и |>
соответственно.
- O (n) из списка с помощью
fromlist
- O (log (min (n1, n2))) конкатенация для последовательностей длины n1 и n2.
- O (log (min (i, n-i))) индексирование для элемента в позиции
i
в последовательности длины n.
Кроме того, эта структура поддерживает множество известных и удобных функций, которые вы ожидаете от структуры, подобной списку: replicate
, zip
, null
, scan
s, sort
, take
, drop
, splitAt
и многие другие. Из-за этих сходств вы должны выполнить либо квалифицированный импорт, либо скрыть функции в Prelude
, имеющие одно и то же имя.
Data.Map
Maps
являются стандартной рабочей лошадкой для реализации соответствия между "вещами", то, что вы могли бы назвать массивом Hashmap или associave на других языках программирования, называются "Карты" в Haskell; кроме как говорят, что Python Maps
чист - поэтому обновление возвращает новую карту и не изменяет исходный экземпляр.
Карты поставляются в двух вариантах: strict и lazy.
Цитата из документации
Строгий
API этого модуля является строгим как для ключей, так и для значений.
Ленивый
API этого модуля является строгим в ключах, но ленив в значениях.
Итак, вам нужно выбрать то, что лучше подходит для вашего приложения. Вы можете попробовать обе версии и тесты с criterion
.
Вместо перечисления функций Data.Map
я хочу перейти на
Что может повлиять на то, что ключи являются целыми числами, чтобы выжать более высокую производительность
Прежде всего отметим:
Многие операции имеют худшую сложность O (min (n, W)). Это означает, что операция может стать линейной по числу элементов с максимумом W - количеством бит в Int (32 или 64).
Итак, каковы характеристики для IntMaps
- O (min (n, W)) для (небезопасной) индексации
(!)
, небезопасно в том смысле, что вы получите сообщение об ошибке, если ключ/индекс не существует. Это то же поведение, что и Data.Sequence
.
- O (n) вычисление
size
- O (min (n, W)) для безопасной индексации
lookup
, которая возвращает Nothing
, если ключ не найден и Just a
в противном случае.
- O (min (n, W)) для
insert
, delete
, adjust
и update
Итак, вы видите, что эта структура менее эффективна, чем Sequences
, но обеспечивает немного большую безопасность и большую выгоду, если вам действительно не нужны все записи, например представление разреженного графика, где узлы являются целыми числами.
Для полноты я хотел бы упомянуть пакет под названием persistent-vector
, который реализует векторы clojure -style, но, кажется, заброшен как последняя загрузка от (2012).
Заключение
Итак, для вашего случая использования я настоятельно рекомендую Data.Sequence
или Data.Vector
, к сожалению, у меня нет опыта с последним, поэтому вам нужно попробовать это для себя. Из всего, что я знаю, это мощная вещь, называемая потоковым фьюжнгом, которая оптимизирует выполнение нескольких функций в одном жестком "цикле" вместо запуска цикла для каждой функции. Учебник для Vector
можно найти здесь.
Ответ 3
При поиске функциональных контейнеров с определенными асимптотическим временем выполнения я всегда выхожу Edison.
Обратите внимание, что есть результат, что на строгом языке с неизменяемыми структурами данных всегда существует логарифмическое замедление для реализации изменчивой структуры данных поверх них. Это открытая проблема, может ли ограниченная мутация, скрытая за ленью, избежать этого замедления. Там также проблема постоянного или переходного...
Окасаки по-прежнему хорошо читается для фона, но пальцевые деревья или что-то более сложное, как дерево RRB, должно быть доступно "готово" и решить вашу проблему.
Ответ 4
Я ищу контейнер, который используется для сохранения частичных результатов n - 1 задач, чтобы вычислить n-й.
Каждый элемент я контейнера зависит от не менее 2 и до 4 предыдущих результатов.
Давайте рассмотрим очень маленькую программу. который вычисляет числа фибоначчи.
fib 1 = 1
fib 2 = 1
fib n = fib (n-1) + fib (n-2)
Это отлично подходит для малых N, но ужасно для n > 10. На этом этапе вы наткнетесь на этот камень:
fib n = fibs !! n where fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
У вас может возникнуть соблазн воскликнуть, что это темная магия (бесконечное создание списка референтных списков и zipping? wth!), но это действительно отличный пример связывания узла и использования ленивости, чтобы гарантировать, что значения вычисляются как необходимо.
Аналогично, мы можем использовать массив, чтобы связать узел тоже.
import Data.Array
fib n = arr ! 10
where arr :: Arr Int Int
arr = listArray (1,n) (map fib' [1..n])
fib' 1 = 1
fib' 2 = 1
fib' n = arr!(n-1) + arr!(n-2)
Каждый элемент массива - это thunk, который использует другие элементы массива для вычисления его значения. Таким образом, мы можем построить единый массив, не имея необходимости выполнять конкатенацию, и вызывать значения из массива по желанию, оплачивая только расчет до этой точки.
Красота этого метода заключается в том, что вам нужно не только смотреть за собой, но и смотреть перед собой.