Рубрики
Без рубрики

Создание хэша данных

Что случилось со структурами данных Образование?. Tagged Datastructures, Python, Rant, CPP.

Когда я был Замена Peewee на Ponyorm в моем двигателе веб -публикации Я оценивал несколько вариантов, в том числе полностью ухожу от ORM и просто хранение метаданных в индексированных таблицах в памяти. Это также помогло бы решить несколько незначительных раздражающих вопросов дизайна (таких как неправильная инкапсуляция фактического состояния контента в экземпляр приложения), но в итоге я не сделал этого.

Главная причина, по которой на самом деле не кажется полезным библиотеками таблиц, индексированных в памяти, для Python. Или много других языков.

Еще в 90-х годах, когда я был инженером-программистом личинок, обычная практика для обучения структур данных всегда начиналась с основ: связанных списков, затем двоичных деревьев (сначала наивные, затем самобалансирующие такие, как avl или Красные черные деревья , возможно, с наступлением в b trees ) и Тогда на хэш -таблицы, обычно начиная с фиксированных хэшей, а затем перемещаясь к динамическим хэшам. Сами хеш -таблицы часто даже рассматривались как запоздалая мысль, оптимизация производительности, которая не была полезна в общем случае! В то же время алгоритмы, основанные на Бинарный поиск В сортированных списках и тому подобное также было бы основной частью учебной программы.

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

Кроме того, я не думаю, что это имеет какое -либо отношение к опыту, и все, что связано со средой, в которой людей учат структурам данных; мое образование CS было очень сосредоточен на том, как работают структуры данных и алгоритмы, но кажется, что все молодые программисты, с которыми я общаюсь (о боже, я сейчас так старый) учили, что сами структуры данных не так важны, кроме деталей реализации или Основная теория и, кроме того, что компьютеры имеют такие огромные ресурсы, что вы не Действительно Нужно заботиться об этом (о боже, я утра так старый сейчас).

C ++ и Java предоставляют упорядоченные ассоциативные структуры. std:: map и TreeMap , Например. Но всякий раз, когда вы спрашиваете программиста (особенно младшего) о них в наши дни, люди просто отмечают, что они «медленные», потому что они (log₂ n) для поиска ключей, и что вы должны использовать только std:: Unoromeded_map или Hashmap Вместо этого, потому что они O (1).

Но сосредотачиваясь на этом, забывает о нескольких вещах:

  • Фактор сложности Big-O имеет значение для действительно больших значений N, игнорируя постоянные накладные расходы базовых вычислений (которые в случае хэширования струн могут быть довольно медленными, особенно по сравнению с горсткой лексических сравнений)
  • Одиночный поиск-это не единственная черта, которую вы, возможно, захотите сделать на своих данных!

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

Все эти вещи требуют полного сканирования таблицы-означающего операцию O (n)-в сценарии только хеш. Или, если вы хотите сделать запрос на диапазон, а затем отсортировать его в конце, он требует O (n log₂ n), так как вам нужно сначала отфильтровать таблицу (которая является O (n)), а затем сортируйте (что является o (N log₂ n)). Чем это более эффективно, чем просто использовать единовременный поиск O (log₂ n)?

Интервьюируя кандидатов

Одной из моих повторяющихся обязанностей инженера по программированию, работающему на полную ставку, было взять интервью у других инженеров в качестве кандидатов на работу. Одной из моих проблем было написание Boggle решатель. В растворе обычно есть три фазы:

  1. Определить общий алгоритм
  2. Учитывая функцию для определения того, существует ли префикс в списке слов, пройдите доску, чтобы найти решения
  3. Реализовать функцию для определения, существует ли префикс

Фаза 3, как правило, является сложной частью, с которой большинство кандидатов испытывают наибольшую проблему. Однако есть очень простые решения этой проблемы. Вы можете начать с сортировки списка Words в массиве (O (n log₂ n)), а затем сделать предварительный поиск O (log₂ n) для каждого слова, или вы можете сохранить его на карте типа дерева (также O (n log₂ n) для начального хранилища), а затем выполните поиск по более низкому связующему слову O (log₂ n), или вы можете делать то, с кем идет большинство кандидатов, и либо храните список слов в хэш-таблице (O (n)) и выполните поиск по всей таблице для каждой проверки (также O (n)), или они строят Три хранить все слова с флагом относительно того, является ли узел листьем (то есть, если слово завершено), который по сути является начальной фазой O (n) и поиском O (L) (где L – длина слово). Это все приемлемые решения, но Размер кода, который вы должны написать для каждой вещи, очень переменная.

Например, в C ++, вот как вы выполняете поиск префикса на std:: set :

bool has_prefix(const std::set& wordlist, std::string prefix) {
    auto iter = wordlist.lower_bound(prefix);
    return iter != wordlist.end && iter->substr(0, prefix.length()) == prefix;
}

Или вот как вы делаете это на Java с Treeset :

boolean has_prefix(TreeSet wordlist, string prefix) {
    String first = wordlist.floor(prefix);
    return first != null && first.startsWith(prefix);
}

Или же Если вы находитесь на языке без концепции набора на основе дерева, такой как Python, и вы храните свой словарь в отсортированном списке:

def has_prefix(wordlist, prefix):
    import bisect
    index = bisect.bisect_left(wordlist, prefix)
    return index < len(wordlist) and wordlist[index].startswith(prefix)

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

(И я имею в виду, что карта-восстановление также полезен в небольших масштабах, это просто не универсальный способ рассуждения о вещах. Особенно, когда оправдание это сводится к: «Эх, есть запасная сила процессора, зачем беспокоиться?»)

Дальнейшие интервью с разочарованием

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

Почти не в порядке , кандидаты, которых спрашивают о базовой структуре данных, в конечном итоге начинаются с того, что он взял ключ, создавая хеш на нем, а затем Вставьте ключ в двоичное дерево Анкет Это довольно смягчает! Это означает, что они платят за сложность бинарного дерева и Наличие ограниченных возможностей хэш -стола – худшая из обеих мировых ситуаций. Никакой обход по порядку в сочетании с O (log₂ n) поисками. И когда я жалуюсь другим на кандидатов, не понимающих эти основы, отзывы, которые я получаю, заключается в том, что, возможно, я не должен ожидать, что люди помнят вводные курсы по информатике это тоже).

Как мы дошли до этого момента?

C++

В любом случае, просто из любопытства я решил сделать сравнение времени между несколькими различными способами реализации решателя Boggle; Единственная разница между этими алгоритмами заключается в реализации has_prefix на основе рассматриваемой структуры данных. (Также обратите внимание, что сам код – это хрустящий материал, который я написал много лет назад для очень специфической задачи, и все, что я сделал с ним для этой статьи, – это изменить используемые структуры данных. Это, конечно, не качественный код, и я подозреваю, что если бы я на самом деле посмотрел на большую часть кода, я бы сейчас был немного ошеломляющим.

Используя Заказано набор :

$ g++ -O3 -Wall --std=c++11 boggle-orderedset.cpp
$ time ./a.out < board.txt > /dev/null

real    0m0.173s
user    0m0.160s
sys     0m0.010s

Используя Сортированный вектор :

$ g++ -O3 -Wall --std=c++11 boggle-sortedvector.cpp
$ time ./a.out < board.txt > /dev/null

real    0m0.048s
user    0m0.039s
sys     0m0.007s

Используя хэш -таблица :

$ g++ -O3 -Wall --std=c++11 boggle-hashtable.cpp
$ time ./a.out < board.txt > /dev/null

real    0m44.075s
user    0m43.867s
sys     0m0.110s

Используя Ручная снятая Три :

$ g++ -O3 -Wall --std=c++11 boggle-trie.cpp
$ time ./a.out < board.txt > /dev/null

real    0m0.362s
user    0m0.320s
sys     0m0.038s

В приведенных выше решениях удивительно, что сортированный вектор намного быстрее, чем упорядоченный набор; Тем не менее, что неудивительно, так это то, что оба из них на много порядков быстрее, чем подход хеш -стола, и что подход TRIE несколько медленнее, чем упорядоченный набор. Конечно, реализация TRIE может быть немного лучше (например, фактический алгоритм поиска может отслеживать, где он находится в TRIE, а не поиск из корней каждый раз), но сумма написанного кода также важен:

$ wc -l *.cpp | sort -n
     131 boggle-orderedset.cpp
     132 boggle-sortedvector.cpp
     137 boggle-hashtable.cpp
     161 boggle-trie.cpp
     561 total

Таким образом, отсортированный вектор лишь немного сложнее реализовать (на самом деле единственная разница в подсчете строк – это призыв к std:: sort после загрузки словаря – что на самом деле даже не необходимо, если ваш словарь сортируется начать с), и все еще Это самое быстрое из всех этих.

Хорошо, так что алгоритм фактически не использует индексированную структуру данных. Но мыслительный процесс, который приводит к его реализации, соответствует тем же направлениям, что и мыслительные процессы, которые приводят к использованию упорядоченной индексированной структуры данных; По сути, вектор это Индекс, рассматриваемый в большем смысле. И всякий раз, когда я брал интервью у кандидата с этой проблемой, не один Ушел с отсортированным массивом и бинарным поиском! (У меня была пара хотя бы пойти с упорядоченным , хотя. Но почти все идут с Три - и большинство из них никогда не слышали о Три раньше, и просто вроде, как, изобретать это на месте. Что круто, но все еще …)

Кроме того, отсортированный вектор Подход действительно работает только по производительности, если ваш входной набор статичен. Как только вы начнете добавлять к нему вещи, каждое дополнение станет потенциально O (n) операцией, которая может в конечном итоге стать невероятно дорогим очень быстро. Например, если вы пишете, скажем, с балансировщиком программного нагрузки, и ваша таблица отслеживает уровни загрузки серверов ваших серверов, каждое обновление, которое требует изменения строки в индексе, и если у вас много строк ( Скажем, вы работаете в каком-то шкале, где мышление с восстановлением карты вступает во владение), каждое обновление начинает очень быстро складываться.

В любом случае, с просто стандартным C ++ вы можете создать свои собственные индексы самостоятельно, или Вы можете использовать Способствовать росту. Multiindex , который поддерживает произвольно много индексов для вас. Это довольно аккуратно.

Питон

В любом случае, вернемся к моей оригинальной загадке. Я хотел исследовать хранилище данных Govel Publ в таблице в памяти с различными индексами для необходимых критериев сортировки. Самый простой подход состоял в том, чтобы придерживаться базы данных, несмотря на то, что она имеет очень плохую инкапсуляцию (поскольку хранилище объекта по сути является глобальным состоянием). Но что еще я мог сделать?

Когда я спрашивал, все продолжали указывать мне на Коллекции. Заказал диск , который является дикта который поддерживает порядок своих ключей. Но под сохранением порядка »это на самом деле означает поддержание Вставка Заказ, а не какой -либо постоянный заказ сортировки. Это на самом деле не полезно для поддержания индекса. (И даже если бы это было так, это не предоставляет никаких примитивов для выполнения диапазона запроса или итерации брата или что -то еще.)

Однако Блист Пакет является довольно полной реализацией B-Tree, и, в частности, она предоставляет SortedDict , который действительно поддерживает порядок сортировки своих ключей. Это также предоставляет Keysview что позволяет вам bisect_left Сами ключи, чтобы пересечь предметы, начиная с определенной точки. Это, конечно, не самое худшее. Итак, в будущем, если я когда -нибудь решим полностью избавиться от ORM, это то, на чем я, вероятно, сосредоточусь. Тем не менее, это добавляет немного больше сложности в том, что, если вы знаете, что ваш ключ сортировки вообще изменится, вам нужно помнить, чтобы удалить свой элемент из B-деревы, а затем прочитать его после того, как он был обновлен. (К счастью, все равно не так отличается от типичного механизма CRUD.)

И, конечно же, вариант использования Publ требует много чтения и не много записей, так что это также не так уж важно периодически восстанавливать индексы в качестве отсортированного списка и просто использовать bisect_left туда. Это все еще то, чем нужно управлять напрямую.

Может быть, если/когда я решите де-артифицировать опубликовать, я просто в конечном итоге напишу библиотеку, чтобы облегчить управление индексированными таблицами. Я уверен, что, черт возьми, не могу найти ничего, что существует, как есть. (Если кто -то знает, пожалуйста, дайте мне знать!)

(Я имею в виду, если я не смогу найти что-то, что похоже на Беркли DB и фактически поддерживается в Python 3 и совместимо с MIT-лицензией …)

(Примечание к себе: lmdb – хороший кандидат)

Ява

Как упоминалось ранее, Java предоставляет оба TreeMap/Treesset и Hashmap/Hashset . Но по какой -то причине людей постоянно учат использовать только Hashmap/Hashset Версии, и это приводит к тому, что люди никогда не знают, что TREEMAP/TERESET даже существует Или даже подумайте, почему они могут захотеть их использовать. Я нахожу это невероятно сбивающим с толку.

Луа и JavaScript

Луа и JavaScript очень популярны из -за их простоты; Оба они делают одинаковое упрощающее предположение в этом все Структуры данных представляют собой хэш -таблицы, включая основные массивы. Во многих случаях они становятся оптимизированными под капотом, чтобы выступать в качестве основных массивов, но есть также много ситуаций, когда это в конечном итоге разваливается, и именно поэтому в LUA, в частности, вы обычно хотите использовать шрифт вместо Пары , особенно если порядок имеет значение.

Результатом этого является то, что также нет абсолютно никакой концепции индексированного, упорядоченного ассоциативного массива. Либо ваш массив проходит вне порядка (или, в JavaScript, вставленном порядке, так как массив JS действует более или менее похож на коллекции Python Заказал диск , за исключением случаев, когда это не так), или вы получаете все свои ключи от массива, сортируя это, а затем итерация на этом массиве. Который добавляет еще больше слоев сложности и еще больше снижает производительность.

В Lua вы вряд ли будете писать что -либо, что использует индексированные структуры (и если вы есть, вы, вероятно, реализуете этот материал в C или C ++ и вызывая в нем ffi ), но JavaScript? Это используется для запуска Лот веб -сервисов и в то время как node.js предоставляет ffi Связывание, это обычно считается последним средством. Так что же делают люди, когда им нужно обрабатывать индексы в службах node.js?

Что ж, по моему опыту, они, кажется, просто откладывают его на ORM, или пожимают плечами и страдают от плохой работы.

Идти

Я давно не трогал, но в последний раз, когда я это делал, ортодоксальность Голанга заключалась в том, что вам не нужны упорядоченные индексы. Из некоторых беглых веб -исследований я нахожу, что это кажется все еще будет так Анкет

К счастью, там являются Библиотеки контейнеров что исправляет это. Это похоже на Тримп даже теперь предоставляет пол и потолок который затем можно использовать для реализации запросов диапазона. Похоже, что это только недавно приобрело эту функциональность (то есть месяц назад на момент написания этой статьи).

C#

C# предоставляет Словарь учебный класс.

Вы знаете, что вы можете сделать с реальным словарем? Вы можете быстро найти слово, которое вы хотите, а затем посмотреть, какие слова приходят до и после него.

Вы знаете, что вы не можете сделать с C# Словарь ?

Итак, C# также предоставляет Заказандикция Но это, насколько я могу судить, это не дает каких -либо методов итерации для получения следующих или предыдущих записей или любых запросов диапазона вообще. Предположительно это возможны через Linq , хотя.

О, я думаю, я забыл написать вывод

Так да, э -э. Для меня так странно, что это способ, которым прошло программное обеспечение. Все – большая куча хэш -таблиц, и никто не должен относиться к чему -либо по -другому или изучать какие -либо алгоритмы, которые используют другие представления хранения. Я нахожу это очень грустным.

Оригинал: “https://dev.to/fluffy/making-a-hash-of-data-11bc”