Русский
Русский
English
Статистика
Реклама

Структуры данных

Перевод Структуры данных и алгоритмы, которыми я пользовался, работая в технологических компаниях

15.08.2020 18:06:03 | Автор: admin
Пользуетесь ли вы структурами данных и алгоритмами в повседневной работе? Я обратил внимание на то, что всё больше и больше людей считает алгоритмы чем-то таким, чем, без особой связи с реальностью, технические компании, лишь по собственной прихоти, интересуются на собеседованиях. Многие жалуются на то, что задачи на алгоритмы это нечто из области теории, имеющей слабое отношение к настоящей работе. Такой взгляд на вещи, определённо, распространился после того, как Макс Хауэлл, автор Homebrew, опубликовал твит о том, что произошло с ним на собеседовании в Google:

Google: 90% наших инженеров пользуются программой, которую вы написали (Homebrew), но вы не можете инвертировать бинарное дерево на доске, поэтому прощайте.

Хотя и у меня никогда не возникало нужды в инверсии бинарного дерева, я сталкивался с примерами реального использования структур данных и алгоритмов в повседневной работе, когда трудился в Skype/Microsoft, Skyscanner и Uber. Сюда входило написание кода и принятие решений, основанное на особенностях структур данных и алгоритмов. Но соответствующие знания я, по большей части, использовал для того чтобы понять то, как созданы некие системы, и то, почему они созданы именно так. Знание соответствующих концепций упрощает понимание архитектуры и реализации систем, в которых эти концепции используются.



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

Здесь речь пойдёт о совсем небольшом количестве алгоритмов, но в том, что касается структур данных, могу отметить, что я коснусь тут практически всех из них. Никого не должно удивлять то, что я не фанат таких вопросов, задаваемых на собеседованиях, в которых алгоритмам уделяется неоправданно много внимания, которые оторваны от практики и нацелены на экзотические структуры данных вроде красно-чёрных деревьев или АВЛ-деревьев. Я никогда такие вопросы на собеседованиях не задавал и задавать их не буду. В конце этой статьи я поделюсь своими мыслями о подобных собеседованиях. Но, несмотря на вышесказанное, знания о базовых структурах данных имеют огромную ценность. Эти знания позволяют подбирать именно то, что нужно для решения определённых практических задач.

А теперь давайте перейдём к примерам использования структур данных и алгоритмов в реальной жизни.

Деревья и обход деревьев: Skype, Uber и UI-фреймворки


Когда мы разрабатывали Skype для Xbox One, код приходилось писать для чистой ОС Xbox, в которой не было необходимых нам библиотек. Мы разрабатывали одно из первых полномасштабных приложений для этой платформы. Нам нужна была система навигации по приложению, с которой можно было бы работать и пользуясь сенсорным экраном, и отдавая приложению голосовые команды.

Мы создали базовый навигационный фреймворк, основанный на WinJS. Для того чтобы это сделать, нам понадобилось поддерживать граф, похожий на DOM, что требовалось для организации наблюдения за элементами, с которыми мог взаимодействовать пользователь. Для поиска таких элементов мы выполняли обход DOM, который сводился к обходу дерева, то есть существующей структуры DOM-элементов. Это классический пример BFS или DFS (breadth-first search или depth-first search поиск в ширину или поиск в глубину).

Если вы занимаетесь веб-разработкой это значит, что вы уже работаете с древовидной структурой, а именно с DOM. У всех узлов DOM могут быть дочерние узлы. Браузер выводит узлы на экран после обхода дерева DOM. Если вам надо найти конкретный элемент, то для решения этой задачи можно воспользоваться встроенными методами DOM. Например методом getElementById. Альтернативой этого может стать разработка собственного BFS- или DFS-решения для обхода узлов и поиска того, что нужно. Например, нечто подобное сделано здесь.

Многие фреймворки, которые рендерят элементы пользовательского интерфейса, используют, в своих недрах, древовидные структуры. Так, React поддерживает виртуальную DOM и использует интеллектуальный алгоритм согласования (сравнения). Это позволяет добиваться высокой производительности за счёт того, что повторному рендерингу подвергаются только изменённые части интерфейса. Здесь можно найти визуализацию этого процесса.

В мобильной архитектуре Uber, RIBs, тоже используются деревья. Это роднит данную архитектуру с большинством других UI-фреймворков, которые выводят на экраны иерархические структуры элементов. В архитектуре RIBs поддерживается, с целью управления состоянием, дерево RIB. Прикрепляя к нему RIBs и открепляя их от него, управляют их рендерингом. Работая с RIBs, мы иногда делали наброски, пытаясь понять, вписывается ли RIBs в существующую иерархию, и обсуждали то, должны ли у RIBs, о котором идёт речь, быть видимые элементы. То есть говорили о том, будет ли эта структура принимать участие в формировании визуального представления интерфейса, или она будет использоваться лишь для управления состоянием.


Переходы между состояниями при использовании RIBs. Здесь можно найти подробности о RIBs

Если вам когда-нибудь понадобится визуализировать иерархические элементы, то знайте, что обычно для этого используются древовидные структуры, их обход и рендеринг посещённых элементов. Я сталкивался со множеством внутренних инструментов, которые используют этот подход. Пример такого инструмента средство для визуализации RIBs, созданное командой Mobile Platform из Uber. Вот доклад на эту тему.

Взвешенные графы и поиск кратчайшего пути: Skyscanner


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

Планирование путешествий с посещением нескольких городов стало одной из возможностей, на реализацию которой у Skyscanner ушло довольно много времени. При этом сложности, в основном, относились к самой разрабатываемой системе. Лучшие предложения такого рода находят, используя алгоритмы поиска кратчайшего пути вроде алгоритма Дейкстры или A*. Маршруты полётов представляют в виде ориентированного графа. Каждому его ребру назначен вес в виде стоимости билета. При поиске наилучшего маршрута нахождение самого дешёвого пути между двумя городами выполняется с использованием реализации модифицированного алгоритма A*. Если вам интересна тема подбора авиабилетов и поиска кратчайших маршрутов, то вот хорошая статья, посвящённая использованию BFS для решения подобных задач.

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

Сортировка: Skype (или что-то вроде этого)


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

В Skype мне, правда, пришлось прибегнуть к практическому использованию моих знаний об алгоритмах сортировки. Один из наших программистов решил реализовать, для вывода контактов, сортировку вставками. В 2013 году, когда Skype подключался к сети, контакты загружались партиями. Для загрузки их всех требовалось некоторое время. В результате тот программист подумал, что лучше будет строить список контактов, упорядоченных по именам, используя сортировку вставками. Мы много обсуждали этот алгоритм, размышляли о том, почему бы просто не использовать что-то такое, что уже реализовано. В результате больше всего времени у нас ушло на то, чтобы как следует протестировать нашу реализацию алгоритма и проверить её производительность. Лично я не видел особого смысла в создании собственной реализации этого алгоритма. Но тогда проект был на такой стадии, на которой у нас было время на подобные дела.

Конечно, есть реальные ситуации, в которых эффективная сортировка играет в некоем проекте очень важную роль. И если разработчик может самостоятельно, основываясь на особенностях данных, выбирать наиболее подходящий алгоритм, это может дать заметный прирост производительности решения. Сортировка вставками может оказаться очень кстати там, где работают с большими наборами данных, передаваемых куда-либо в режиме реального времени, и тут же визуализируют эти данные. Сортировка слиянием может хорошо показывать себя при использовании подходов типа разделяй и властвуй, в том случае, если речь идёт о работе с большими объёмами данных, хранящихся в разных узлах. Мне с подобными системами работать не доводилось, поэтому я пока продолжаю считать алгоритмы сортировки чем-то таким, что имеет ограниченное применение в повседневной работе. Это, правда, не касается важности понимания того, как именно работают разные алгоритмы сортировки.

Хеш-таблицы и хеширование: везде


Структура данных, которой я регулярно пользовался это хеш-таблица. Сюда же можно отнести и хеш-функции. Это полезнейший инструмент, которым можно пользоваться для решения самых разных задач от подсчёта количества неких сущностей, обнаружения дубликатов, кеширования, до сценариев, вроде шардинга, используемых в распределённых системах. Хеш-таблицы это, после массивов, структура данных, которая встречается в программировании чаще всего. Я ей пользовался в бесчисленном количестве ситуаций. Она есть почти во всех языках программирования, и её, если нужно, просто реализовать самостоятельно.

Стеки и очереди: время от времени


Стек это структура данных, которая знакома каждому, кто занимался отладкой кода, написанного на языке, который поддерживает трассировку стека. Если говорить о стеке как о структуре данных, то я, в ходе работы, столкнулся с несколькими задачами, для решения которых она понадобилась. Но надо отметить, что со стеками я как следует познакомился, занимаясь отладкой и профилированием производительности кода. Стеки это, кроме того, естественный выбор структуры данных, используемой при выполнении обхода дерева в глубину.

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

Криптографические алгоритмы: Uber


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

Разбираться в криптографических алгоритмах это очень интересно. При этом для решения неких задач не стоит предлагать собственные алгоритмы. Это одна из худших идей, которая может прийти в голову программисту. Вместо этого берётся существующий, хорошо документированный стандарт и используются встроенные примитивы соответствующих фреймворков. Обычно в качестве стандарта, выбираемого при реализации криптографических решений, выступает AES. Он достаточно надёжен для того чтобы шифровать с его помощью секретную информацию в США. Не существует известных работающих атак на протокол. AES-192 и AES-256 обычно, для решения большинства практических задач, достаточно надёжны.

Когда я пришёл в Uber, мобильная система шифрования и система шифрования веб-приложения уже были реализованы, в их основе лежали вышеописанные механизмы, поэтому у меня был повод для изучения подробностей об AES (Advanced Encryption Standard), о HMAC (Hashed Message Authentication Codes), об алгоритме RSA и о прочих подобных вещах. Интересно было и дойти до понимания того, как доказывают криптостойкость последовательности действий, применяемой при шифровании. Например, если говорить об аутентифицированном шифровании с присоединёнными данными, оказывается, что анализируя режимы encrypt-and-MAC, MAC-then-encrypt и encrypt-then-MAC, можно доказать криптостойкость лишь одного из них, хотя это и не означает того, что остальные не являются криптостойкими.

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

Деревья решений: Uber


В ходе работы над одним из проектов нам, в мобильном приложении, понадобилось реализовать сложную бизнес-логику. А именно, основываясь на полудюжине правил, нужно было показать один из нескольких экранов. Эти правила были очень сложными, что объяснялось тем, что в расчёт надо было принимать результаты последовательности проверок и предпочтения пользователя.

Программист, который приступил к решению этой задачи, сначала попробовал выразить все эти правила в виде инструкций if-else. Это привело к появлению чрезвычайно запутанного кода. В итоге решено было воспользоваться деревом решений. С его помощью было несложно произвести все необходимые проверки. Его было сравнительно просто реализовать. Кроме того, если нужно, его можно было, без особых проблем, изменить. Нам нужно было создать собственную реализацию дерева решений, такого, чтобы проверка условий осуществлялась бы на его рёбрах. Это всё, что нам было нужно от этого дерева. Хотя мы могли бы сэкономить время, потраченное на реализацию дерева, воспользовавшись другим подходом, команда решила, что особое дерево будет легче поддерживать, и приступила к работе. Выглядит это дерево примерно так: рёбра символизируют результаты проверки условий (это бинарные результаты, или результаты, представленные диапазонами значений), а листовые узлы дерева описывают экраны, на которые нужно выполнять переходы.


Структура созданного нами дерева решений, используемого для выяснения того, какой именно экран нужно показать. При принятии решения используется сложный набор правил

В системе сборки мобильной версии приложения Uber, называемой SubmitQueue, тоже использовалось дерево решений, но оно формировалось динамически. Команде Developer Experience нужно было решить сложную проблему выполнения ежедневного слияния (merge) сотен веток-источников изменений с целевой веткой. При этом на выполнение каждой сборки нужно было около 30 минут. Сюда входили компиляция, выполнение модульных и интеграционных тестов, а также тестов интерфейса. Распараллеливание сборок было недостаточно хорошим решением, так как в различных сборках часто были перекрывающиеся изменения, что вызывало конфликты слияния. На практике это означало, что иногда программистам приходилось ждать 2-3 часа, прибегать к команде rebase, и снова запускать процесс слияния веток, надеясь, что в этот раз они не столкнутся с конфликтом.

Команда Developer Experience применила инновационный подход, который заключался в прогнозировании конфликтов слияния и в соответствующем размещении сборок в очереди. Делалось это с использованием особого бинарного дерева принятия решений (speculation tree).


В SubmitQueue используется бинарное дерево принятия решений, рёбра которого снабжены комментариями о прогнозируемых вероятностях. Этот подход позволяет определить то, какие наборы сборок можно выполнять параллельно. Делается это ради сокращения времени между получением и исполнением задач и ради повышения пропускной способности системы. В ветку master при этом должен попадать только полностью проверенный и работоспособный код

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

Шестиугольные сетки, иерархические индексы: Uber


Это последний проект, о котором я хочу тут рассказать. Он был полностью основан на одной особенной структуре данных. Именно занимаясь им я об этой структуре данных и узнал. Речь идёт о шестиугольной сетке с иерархическими индексами.

Одной из самых сложных и интересных проблем в Uber была оптимизация стоимости поездок и распределения заказов по партнёрам. Цены на поездки могут динамически меняться, водители постоянно находятся в движении. Инженерами Uber была создана сеточная система H3. Она предназначена для визуализации и анализа данных по городам в разных масштабах. Структура данных, которая используется для решения вышеозначенных задач, представляет собой шестиугольную сетку с иерархическими индексами. Для визуализации данных используется пара специализированных внутренних инструментов.


Разбиение карты на шестиугольные ячейки

Эта структура данных отличается собственной системой индексирования, обхода, собственными определениями отдельных областей сетки, собственными функциями. Детальное описание этого всего можно найти в документации к соответствующему API. Подробности об H3 можно почитать здесь. Вот исходный код. Здесь можно найти рассказ о том, как и почему была создана эта система.

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

Структуры данных и алгоритмы на собеседованиях


Выше я рассказал о том, с какими структурами данных и алгоритмами я столкнулся, долгое время работая в различных компаниях. Предлагаю сейчас вернуться к тому твиту Макса Хауэлла, который я упоминал в самом начале материала. Там Макс жаловался на то, что на собеседовании в Google ему предложили инвертировать бинарное дерево на доске. Он этого не сделал. Ему указали на дверь. В этой ситуации я на стороне Макса.

Я считаю, что знания о том, как работают популярные алгоритмы, или о том, как устроены экзотические структуры данных, это не те знания, которые нужны для работы в технологической компании. Нужно знать о том, что такое алгоритм, нужно уметь самостоятельно придумывать простые алгоритмы, например, работающие по жадному принципу. Ещё нужно обладать знаниями о самых распространённых структурах данных, вроде хеш-таблиц, очередей и стеков. Но что-то достаточно специфическое, вроде алгоритма Дейкстры или A*, или чего-то ещё более сложного, это не то, что нужно заучивать наизусть. Если вам эти алгоритмы реально понадобятся вы легко сможете найти справочные материалы по ним. Я, например, если говорить об алгоритмах, не относящихся к алгоритмам сортировки, обычно стремился разобраться с ними в общих чертах и понять их сущность. То же касается и экзотических структур данных вроде красно-чёрных деревьев и АВЛ-деревьев. У меня никогда не возникало нужды в их использовании. А если бы они мне и понадобились я всегда мог бы освежить знания о них, прибегнув к соответствующим публикациям. Я, проводя собеседования, как я уже говорил, никогда не задаю подобные вопросы и не планирую их задавать.

Я за практические задачи, имеющие отношение к программированию, при решении которых можно применить множество подходов: от методов грубой силы и жадных вариантов алгоритмов до более продвинутых алгоритмических идей. Например, я полагаю, что вполне нормальной является задача, касающаяся выравнивания текста по ширине, вроде этой. Мне, например, пришлось решать эту задачу при создании компонента для рендеринга текста на Windows Phone. Решить эту задачу можно, просто воспользовавшись массивом и несколькими инструкциями if-else, не прибегая к каким-то мудрёным структурам данным.

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

Если вы из компании, которая нанимает только тех, кто едва ли не с рождения обладает глубокими знаниями сложных алгоритмов, я предлагаю вам хорошо подумать о том, те ли это люди, что вам нужны. Я, например, нанимал прекрасные команды в Skyscanner (Лондон) и в Uber (Амстердам), не задавая соискателям хитрых вопросов по алгоритмам. Я ограничивался самыми обычными структурами данных и проверкой возможностей собеседуемых, имеющих отношение к решению задач. То есть им нужно было знать о распространённых структурах данных и уметь придумывать простые алгоритмы для решения стоящих перед ними задач. Структуры данных и алгоритмы это всего лишь инструменты.

Итоги: структуры данных и алгоритмы это инструменты


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

Структуры данных и алгоритмы это инструменты, которыми вы должны уверенно пользоваться, создавая программы. Зная эти инструменты, вы увидите много такого, что вам уже известно, в кодовых базах, в которых они используются. Кроме того, подобные знания позволят вам с гораздо большей уверенностью решать сложные задачи. Вы будете знать о теоретических ограничениях алгоритмов, о том, каким оптимизациям их можно подвергнуть. Это поможет вам, в итоге, выйти на решение, которое, с учётом всех необходимых компромиссов, окажется настолько хорошим, насколько это возможно.

Если вы хотите лучше изучить структуры данных и алгоритмы вот несколько советов и ресурсов:

  • Почитайте о хеш-таблицах, связных списках, деревьях, графах, кучах, очередях, стеках. Поэкспериментируйте с ними, используя тот язык, на котором пишете. Вот хороший обзор, посвящённый структурам данных. А вот место, где можно попрактиковаться.
  • Почитайте книгу Грокаем алгоритмы. Это, как мне кажется, лучший путеводитель по алгоритмам, предназначенный для самых разных категорий программистов. Это доступное иллюстрированное руководство, в котором содержится всё, что большинству специалистов нужно знать по данной теме. Я уверен в том, что вам не понадобится больше знаний об алгоритмах, чем те, что вы сможете найти в этой книге.
  • Вот ещё пара книг: Алгоритмы. Руководство по разработке и Алгоритмы на Java, 4-е издание. Я воспользовался ими для того чтобы освежить в памяти университетские знания об алгоритмах. Я их, правда, до конца не дочитал. Они показались мне достаточно сухими и неприменимыми к моей повседневной работе.

А с какими структурами данных и алгоритмами сталкивались на практике вы?

Подробнее..

Immutable Trie найди то, не знаю что, но быстро, и не мусори

20.09.2020 10:09:00 | Автор: admin
Про префиксное дерево (Trie) написано немало, в том числе и на Хабре. Вот пример, как оно может выглядеть:


И даже реализаций в коде, в том числе на JavaScript, для него существует немало от каноничной by John Resig и разных оптимизированных версий до серии модулей в NPM.

Зачем же нам понадобилось использовать его для сервиса по сбору и анализу планов PostgreSQL, да еще и велосипедить какую-то новую реализацию?..

Склеенные логи


Давайте посмотрим, на небольшой кусок лога сервера PostgreSQL:

2020-09-11 14:49:53.281 MSK [80927:619/4255507] [explain.tensor.ru] 10.76.182.154(59933) pgAdmin III - ???????????????????? ???????????????? LOG:  duration: 0.016 ms  plan:Query Text: explain analyzeSELECT*FROMpg_classWHERErelname = 'мамамылараму';Index Scan using pg_class_relname_nsp_index on pg_class  (cost=0.29..2.54 rows=1 width=265) (actual time=0.014..0.014 rows=0 loops=1)  Index Cond: (relname = 'мамамылараму'::name)  Buffers: shared hit=2

Определить и отсечь строку заголовка, которая начинается с даты, мы можем по формату, установленному переменной log_line_prefix:

SHOW log_line_prefix;-- "%m [%p:%v] [%d] %r %a "

Потребуется совсем немного магии регулярных выражений
const reTS = "\\d{4}(?:-\\d{2}){2} \\d{2}(?::\\d{2}){2}"  , reTSMS = reTS + "\\.\\d{3}"  , reTZ   = "(?:[A-Za-z]{3,5}|GMT[+\\-]\\d{1,2})";var re = {// : log_line_prefix      '%a' : "(?:[\\x20-\\x7F]{0,63})"    , '%u' : "(?:[\\x20-\\x7F]{0,63})"    , '%d' : "[\\x20-\\x7F]{0,63}?"    , '%r' : "(?:(?:\\d{1,3}(?:\\.\\d{1,3}){3}|[\\-\\.\\_a-z0-9])\\(\\d{1,5}\\)|\\[local\\]|)"    , '%h' : "(?:(?:\\d{1,3}(?:\\.\\d{1,3}){3}|[\\-\\.\\_a-z0-9])|\\[local\\]|)"    , '%p' : "\\d{1,5}"    , '%t' : reTS + ' ' + reTZ    , '%m' : reTSMS + ' ' + reTZ    , '%i' : "(?:SET|SELECT|DO|INSERT|UPDATE|DELETE|COPY|COMMIT|startup|idle|idle in transaction|streaming [0-9a-f]{1,8}\/[0-9a-f]{1,8}|)(?: waiting)?"    , '%e' : "[0-9a-z]{5}"    , '%c' : "[0-9a-f]{1,8}\\.[0-9a-f]{1,8}"    , '%l' : "\\d+"    , '%s' : "\\d{4}(?:-\\d{2}){2} \\d{2}(?::\\d{2}){2} [A-Z]{3}"    , '%v' : "(?:\\d{1,9}\\/\\d{1,9}|)"    , '%x' : "\\d+"    , '%q' : ""    , '%%' : "%"// : log_min_messages    , '%!' : "(?:DEBUG[1-5]|INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC)"// : log_error_verbosity    , '%@' : "(?:DETAIL|HINT|QUERY|CONTEXT|LOCATION|STATEMENT)"    };re['%#'] = "(?:" + re['%!'] + "|" + re['%@'] + ")";// преобразуем log_line_prefix в RegExp для разбора строкиlet lre = self.settings['log_line_prefix'].replace(/([\[\]\(\)\{\}\|\?\$\\])/g, "\\\$1") + '%#:  ';self.tokens = lre.match(new RegExp('(' + Object.keys(re).join('|') + ')', 'g'));let matcher = self.tokens.reduce((str, token) => str.replace(token, '(' + re[token] + ')'), lre);self.matcher = new RegExp('^' + matcher, '');

Но дальше-то у нас идет запрос вместе с планом и как понять, где кончается один и начинается другой?..

Казалось бы, план это текстовое представление дерева, поэтому корень-то должен быть один? То есть первая снизу строка с минимальным отступом (пропуская, Trigger ...) искомый корень и начало плана?

Увы, нет. В нашем примере такой строкой окажется хвост раму'::name) от распавшейся на части multiline string. Как быть?

Use Trie, Luke!


Но заметим, что план обязан начинаться с одного из узлов: Seq Scan, Index Scan, Sort, Aggregate, ... ни много, ни мало, а 133 разных варианта, исключая CTE, InitPlan и SubPlan, которые не могут быть корневыми.

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

Immutable Trie


Но у нашего дерева, которое мы хотим построить, есть несколько особенностей:

  • компактность
    Возможных элементов у нас десятки/сотни вполне ограниченной длины, поэтому не может возникнуть ситуации большого количества очень длинных почти совпадающих ключей, отличающихся только в последнем символе. Самый длинный из наших ключей, наверное 'Parallel Index Only Scan Backward'.
  • иммутабельность
    То есть элементы добавляются только при инициализации. В дальнейшем процессе его существования они уже не добавляются и не удаляются.
  • пропускная способность
    Мы хотим тратить минимум операций на проверку совпадения каждого элемента. Иначе можно было бы просто последовательно сравнивать каждый элемент с началом строки, пока не найдется нужный.
  • отсутствие сайд-эффектов
    Операции поиска не должны создавать новых объектов в памяти, которые потом пришлось бы зачищать Garbage Collector'у.

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

В этом нам помогут две полезные функции:


Строим карту


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

Insert
Index Scan
Index Scan Backward
Index Only Scan
Index Only Scan Backward


Хм да у них же есть одинаковый префикс In!

// определение минимальной длины ключа и Longest Common Prefixlet len, lcp;for (let key of keys) {  // первый элемент  if (lcp === undefined) {    len = key.length;    lcp = key.slice(off);    continue;  }  len = Math.min(len, key.length);  // пропускаем, если уже "обнулили" LCP или он совпадает для этого элемента  if (lcp == '' || key.startsWith(lcp, off)) {    continue;  }  // усечение LCP при несовпадении префикса  for (let i = 0; i < lcp.length; i++) {    if (lcp.charCodeAt(i) != key.charCodeAt(off + i)) {      lcp = lcp.slice(0, i);      break;    }  }}

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

Insert
Index Scan
Index Scan Backward
Index Only Scan
Index Only Scan Backward


В данном случае оказалось неважно, какой символ мы возьмем для разбиения (3-й или 5-й, например) состав групп окажется все равно одинаковым, поэтому точно такую же комбинацию разбиения на группы повторно обрабатывать незачем:

// перебираем варианты выбора номера тестируемого символаlet grp = new Set();res.pos = {};for (let i = off + lcp.length; i < len; i++) {  // группируем ключи по соответствующим значениям [i]-символа  let chr = keys.reduce((rv, key) => {    if (key.length < i) {      return rv;    }    let ch = key.charCodeAt(i);    rv[ch] = rv[ch] || [];    rv[ch].push(key);    return rv;  }, {});  // не обрабатываем повторно уже встречавшуюся комбинацию распределений ключей по группам  let cmb = Object.values(chr)    .map(seg => seg.join('\t'))    .sort()    .join('\n');  if (grp.has(cmb)) {    continue;  }  else {    grp.add(cmb);  }  res.pos[i] = chr;}

Метрика оптимума


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

Тут мы пренебрегаем тем фактом, что часть узлов встречается в реальности в планах много чаще, чем другие, и считаем их равнозначными.

То есть, если мы взяли в строке 3-й символ и обнаружили там 's', то потом нам надо сравнить с помощью startsWith, в худшем случае, еще 6 символов, чтобы убедиться, что там именно Insert.
Итого: 1 (.charCodeAt(2)) + 6 (.startsWith('Insert')) = 7 сравнений.

А вот если там обнаружился 'd', то надо взять еще 7-й, чтобы узнать, будет там 'O' или 'S'. И после этого нам все равно придется перебором проверить список был ли это 'Index Scan Backward' (+19 сравнений) или только 'Index Scan' (+10 сравнений).

Причем, если в строке будет 'Index Scan Backward', то мы используем всего 19 сравнений, а вот если 'Index Scan' тогда 19 + 10 = 29.
Итого: 1 (.charCodeAt(2)) + 1 (.charCodeAt(6)) + 19 + 29 (.startsWith(...)) = 50 сравнений только по этой подветке.

В итоге, для нашего примера оптимальная карта будет выглядеть так:

{  '$pos' : 2 // проверяем 3-й символ, '$chr' : Map {    100 => {         // 'd'      '$pos' : 6 // проверяем 7-й символ    , '$chr' : Map {        79 => [ 'Index Only Scan Backward', 'Index Only Scan' ] // 'O'      , 83 => [ 'Index Scan Backward', 'Index Scan' ]           // 'S'      }    }  , 115 => 'Insert' // 's'  }}

Вжух!


Теперь осталось только свести все воедино, добавить функцию поиска, немного оптимизаций и можно использовать:

// заполнение карты поискаconst fill = (obj, off, hash) => {  off = off || 0;  hash = hash || {};  let keys = obj.src;  // проверка наличия такого списка ключей среди уже обработанных  let H = keys.join('\n');  hash[off] = hash[off] || {};  if (hash[off][H]) {    obj.res = hash[off][H];    return;  }  obj.res = {};  hash[off][H] = obj.res;  let res = obj.res;  // ситуация единственного ключа - возможна только при стартовом вызове  if (keys.length == 1) {    res.lst = [...keys];    res.cmp = res.lst[0].length;    return;  }  // определение минимальной длины ключа и Longest Common Prefix  let len, lcp;  for (let key of keys) {    // первый элемент    if (lcp == undefined) {      len = key.length;      lcp = key.slice(off);      continue;    }    len = Math.min(len, key.length);    // пропускаем, если уже "обнулили" LCP или он совпадает для этого элемента    if (lcp == '' || key.startsWith(lcp, off)) {      continue;    }    // усечение LCP при несовпадении префикса    for (let i = 0; i < lcp.length; i++) {      if (lcp.charCodeAt(i) != key.charCodeAt(off + i)) {        lcp = lcp.slice(0, i);        break;      }    }  }  // если один из ключей является общим префиксом  if (off + lcp.length == len) {    let cmp = 0;    // для двух ключей оптимальный вариант поиска - список    if (keys.length == 2) {      res.lst = [...keys];    }    // выносим "за скобки" слишком короткие ключи    else {      res.src = keys.filter(key => key.length > off + lcp.length);      res.lst = keys.filter(key => key.length <= off + lcp.length);    }    // поиск по списку проходит, начиная с самого длинного ключа, и стоит дорого    res.lst.sort((x, y) => y.length - x.length); // s.length DESC    cmp += res.lst.reduce((rv, key, idx, keys) => rv + (keys.length - idx + 1) * key.length, 0);    // если есть продолжение - копаем дальше    if (res.src && res.src.length) {      fill(res, off + lcp.length + 1, hash);      cmp += res.res.cmp;    }    res.cmp = cmp + 1;    return;  }  // перебираем варианты выбора номера тестируемого символа  let grp = new Set();  res.pos = {};  for (let i = off + lcp.length; i < len; i++) {    // группируем ключи по соответствующим значениям [i]-символа    let chr = keys.reduce((rv, key) => {      if (key.length < i) {        return rv;      }      let ch = key.charCodeAt(i);      rv[ch] = rv[ch] || [];      rv[ch].push(key);      return rv;    }, {});    // не обрабатываем повторно уже встречавшуюся комбинацию распределений ключей по группам    let cmb = Object.values(chr)      .map(seg => seg.join('\t'))      .sort()      .join('\n');    if (grp.has(cmb)) {      continue;    }    else {      grp.add(cmb);    }    let fl = true;    let cmp = 0;    for (let [ch, keys] of Object.entries(chr)) {      // упаковываем группы из единственного ключа      if (keys.length == 1) {        let key = keys[0];        chr[ch] = key;        cmp += key.length;      }      // для групп из нескольких ключей запускаем рекурсию      else {        fl = false;        chr[ch] = {src : keys};        fill(chr[ch], i + 1, hash);        cmp += chr[ch].res.cmp;      }    }    res.pos[i] = {      chr    , cmp    };    // запоминаем позицию "лучшего" символа    if (res.cmp === undefined || cmp + 1 < res.cmp) {      res.cmp = cmp + 1;      res.bst = i;    }    // если за каждым символом остался конкретный ключ, то другие варианты нам не нужны    if (fl) {      res.bst = i;      for (let j = off; j < i; j++) {        delete res.pos[j];      }      break;    }  }};// сжатие карты поиска в минимальный форматconst comp = obj => {  // удаляем служебные ключи  delete obj.src;  delete obj.cmp;  if (obj.res) {    let res = obj.res;    if (res.pos !== undefined) {      // сохраняем только оптимальный вариант проверяемого символа      obj.$pos = res.bst;      let $chr = res.pos[res.bst].chr;      Object.entries($chr).forEach(([key, val]) => {        // упаковываем содержимое ключа        comp(val);        // если внутри символа только список - "поднимаем" его на уровень выше        let keys = Object.keys(val);        if (keys.length == 1 && keys[0] == '$lst') {          $chr[key] = val.$lst;        }      });      // преобразуем объект с ключами-строками в Map с ключами-числами      obj.$chr = new Map(Object.entries($chr).map(([key, val]) => [Number(key), val]));    }    // при наличии списка "поднимаем" вложенные результаты на уровень выше    if (res.lst !== undefined) {      obj.$lst = res.lst;      delete res.lst;      if (res.res !== undefined) {        comp(res);        Object.assign(obj, res);      }    }    delete obj.res;  }};// поиск по карте - циклы вместо рекурсии и замыканийconst find = (str, off, map) => {  let curr = map;  do {    // по символу в позиции    let $pos = curr.$pos;    if ($pos !== undefined) {      let next = curr.$chr.get(str.charCodeAt(off + $pos));      if (typeof next === 'string') {   // значение ключа        if (str.startsWith(next, off)) {          return next;        }      }      else if (next instanceof Array) { // список ключей на проверку        for (let key of next) {          if (str.startsWith(key, off)) {            return key;          }        }      }      else if (next !== undefined) {    // вложенный map, если есть        curr = next;        continue;      }    }    // ищем по дополнительному списку, если он есть    if (curr.$lst) {      for (let key of curr.$lst) {        if (str.startsWith(key, off)) {          return key;        }      }    }    return;  }  while (true);};function ImmutableTrie(keys) {  this.map = {src : keys.sort((x, y) => x < y ? -1 : +1)};  fill(this.map);  comp(this.map);}const p = ImmutableTrie.prototype;p.get = function(line, off) {  return find(line, off || 0, this.map);};p.has = function(line, off) {  return this.get(line, off) !== undefined;};module.exports = ImmutableTrie;

Как можно заметить, при поиске в таком Immutable Trie ни одно животное не пострадало не создается никаких новых объектов в памяти, за которыми потом хотел бы придти GC.

Из бонусов: теперь мы можем получать искомый префикс без необходимости делать .slice на исходной строке, даже если мы знаем, что в самом начале у нее, традиционно для плана, что-то постороннее:

const nodeIT = new ImmutableTrie(...);nodeIT.get('  ->  Parallel Seq Scan on abc', 6); // 'Parallel Seq Scan'

Ну а когда мы уже определились, где план начинается, ровно таким же способом (но уже с помощью Trie названий атрибутов) мы определяем строки, которые являются началом атрибута узла, а какие продолжение multiline string и склеиваем их:

Index Scan using pg_class_relname_nsp_index on pg_class  (cost=0.29..2.54 rows=1 width=265) (actual time=0.014..0.014 rows=0 loops=1)  Index Cond: (relname = 'мама\nмыла\nраму'::name)  Buffers: shared hit=2

Ну а в таком виде его разбирать уже намного проще.
Подробнее..

Перевод Приёмы и хитрости начинающего Java-программиста

05.10.2020 10:14:40 | Автор: admin

Небольшая коллекция практик, трюков и подсказок, с помощью которых вы сэкономите своё время при изучении Java и написании кода на этом языке программирования. Перевод статьиTop 25 Java Tricks, Tips, and Best Practices от специалистов Рексофт.

Организация работы

Чистый код

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

  • Правило 10-50-500. В одном пакете не может быть более 10 классов. Каждый метод должен быть короче 50 строк кода, а каждый класс короче 500 строк.

  • SOLID принципы.

  • Использование паттернов проектирования.

Работа с ошибками

Stack Trace (Трассировка стека)

Выявление ошибок это, пожалуй, самая трудоемкая часть процесса разработки на Java. Трассировка стека позволяет вам точно отслеживать, где именно в проекте возникла ошибка или исключение (exception).

import java.io.*;Exception e = ;java.io.StringWriter sw = new java.io.StringWriter();e.printStackTrace(new java.io.PrintWriter(sw));String trace = sw.getBuffer().toString();

NullPointerException

Исключения, возникающие из-заnullзначений (NullPointerException), довольно часто появляются при попытке вызвать метод у несуществующего объекта.

Возьмем для примера следующий код:

int noOfStudents = school.listStudents().count;private int getListOfStudents(File[] files) {if (files == null)  throw new NullPointerException("File list cannot be null");}

Примечание переводчика: а вот пример от меня как переводчика материала:

String anyString = null;if (anyString.equals("some string")) { // здесь будет null pointer exception, т.к. anyString == null// ...}

Дата и Время

System.currentTimeMillis или System.nanoTime?

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

МетодSystem.currentTimeMillis()возвращает текущее количество миллисекунд с начала эры Unix в формате Long. Его точность составляет от 1 до 15 тысячных долей секунды в зависимости от системы.

long startTime = System.currentTimeMillis();long estimatedTime = System.currentTimeMillis() - startTime;

МетодSystem.nanoTime()имеет точность до одной миллионной секунды (наносекунды) и возвращает текущее значение наиболее точного доступного системного таймера.

long startTime = System.nanoTime();long estimatedTime = System.nanoTime() - startTime;

Таким образом, методSystem.currentTimeMillis()лучше применять для отображения и синхронизации абсолютного времени, аSystem.nanoTime()для измерения относительных интервалов времени.

Валидация Даты из строки

Если необходимо достать объектDateиз обычной строки в Java, можете использовать небольшой утилитный класс, который приведен ниже. Он позаботится обо всех сложностях валидации и преобразовании строки в объектDate.

package net.viralpatel.java;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;public class DateUtil {// List of all date formats that we want to parse.// Add your own format here.private static List; dateFormats = new ArrayList() {{add(new SimpleDateFormat("M/dd/yyyy"));add(new SimpleDateFormat("dd.M.yyyy"));add(new SimpleDateFormat("M/dd/yyyy hh:mm:ss a"));add(new SimpleDateFormat("dd.M.yyyy hh:mm:ss a"));add(new SimpleDateFormat("dd.MMM.yyyy"));add(new SimpleDateFormat("dd-MMM-yyyy"));}};/** * Convert String with various formats into java.util.Date *  * @param input *            Date as a string * @return java.util.Date object if input string is parsed  * successfully else returns null */public static Date convertToDate(String input) {Date date = null;if(null == input) {return null;}for (SimpleDateFormat format : dateFormats) {try {format.setLenient(false);date = format.parse(input);} catch (ParseException e) {//Shhh.. try other formats}if (date != null) {break;}}return date;}}

Пример его использования:

package net.viralpatel.java;public class TestDateUtil {public static void main(String[] args) {System.out.println("10/14/2012" + " = " + DateUtil.convertToDate("10/14/2012"));System.out.println("10-Jan-2012" + " = " + DateUtil.convertToDate("10-Jan-2012"));System.out.println("01.03.2002" + " = " + DateUtil.convertToDate("01.03.2002"));System.out.println("12/03/2010" + " = " + DateUtil.convertToDate("12/03/2010"));System.out.println("19.Feb.2011" + " = " + DateUtil.convertToDate("19.Feb.2011" ));System.out.println("4/20/2012" + " = " + DateUtil.convertToDate("4/20/2012"));System.out.println("some string" + " = " + DateUtil.convertToDate("some string"));System.out.println("123456" + " = " + DateUtil.convertToDate("123456"));System.out.println("null" + " = " + DateUtil.convertToDate(null));}}

Результат:

10/14/2012 = Sun Oct 14 00:00:00 CEST 201210-Jan-2012 = Tue Jan 10 00:00:00 CET 201201.03.2002 = Fri Mar 01 00:00:00 CET 200212/03/2010 = Fri Dec 03 00:00:00 CET 201019.Feb.2011 = Sat Feb 19 00:00:00 CET 20114/20/2012 = Fri Apr 20 00:00:00 CEST 2012some string = null123456 = nullnull = null

Строки

Оптимизация строки

Для конкатенации (сложения) строк в Java используется оператор +, для примера, в циклеforновый объект может создаваться для каждой новой строки, что приводит к потере памяти и увеличению времени работы программы.

Необходимо избегать создания Java строк через конструктор, пример:

// медленное создание строкиString bad = new String ("Yet another string object");// быстрое создание строкиString good = "Yet another string object"

Одинарные и двойные кавычки

Что ты ожидаешь в результате выполнения этого кода?

public class Haha {  public static void main(String args[]) {    System.out.print("H" + "a");    System.out.print('H' + 'a');  }}

Казалось бы, строка должна возвращать HaHa, но на самом деле это будет Ha169.

Двойные кавычки обрабатывают символы как строки, но одинарные кавычки ведут себя иначе. Они преобразуют символьные операнды ('H'и'a') в целые значения посредством расширения примитивных типов получается 169.

Математика

Float или Double?

Программисты часто не могут выбрать необходимую точность для чисел с плавающей запятой. Float требует всего 4 байта, но имеет только 7 значащих цифр, а Double в два раза точнее (15 цифр), но в два раза прожорливее.

Фактически, большинство процессоров могут одинаково эффективно работать как с Float, так и с Double, поэтому воспользуйтесь рекомендацией Бьорна Страуструпа (автор языка С++):

Выбор правильной точности для решения реальных задач требует хорошего понимания природы машинных вычислений. Если у вас его нет, либо посоветуйтесь с кем-нибудь, либо изучите проблему самостоятельно, либо используйте Double и надейтесь на лучшее.

Проверка на нечетность

Можно ли использовать этот код для точного определения нечетного числа?

public boolean oddOrNot(int num) {  return num % 2 == 1;}

Надеюсь, вы заметили хитрость. Если мы решим таким образом проверить отрицательное нечетное число (например, -5), остаток от деления не будет равен единице, поэтому воспользуйтесь более точным методом:

public boolean oddOrNot(int num) {  return (num & 1) != 0;}

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

Возведение в степень

Возвести число в степень можно двумя способами:

  1. простое умножение;

  2. используя методMath.pow()(двойное основание, двойной показатель степени).

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

double result = Math.pow(625, 0.5); // 25.0

Простое умножение в Java работает в 300-600 раз эффективнее, кроме того, его можно дополнительно оптимизировать:

double square = double a * double a;  double cube = double a * double a * double a; // не оптимизированоdouble cube = double a * double square;  // оптимизировано double quad = double a * double a * double a * double a; // не оптимизированоdouble quad = double square * double square; // оптимизировано

JIT оптимизация

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

В качестве примера рассмотрим две простые операции:

// 1n += 2 * i * i; // 2n += 2 * (i * i);

Давайте измерим время выполнения каждого из них:

// 1long startTime1 = System.nanoTime(); int n1 = 0; for (int i = 0; i < 1000000000; i++) {   n1 += 2 * i * i; } double resultTime1 = (double)(System.nanoTime() - startTime1) / 1000000000;System.out.println(resultTime1 + " s");  // 2long startTime2 = System.nanoTime(); int n2 = 0; for (int i = 0; i < 1000000000; i++) {   n2 += 2 * (i * i); } double resultTime2 = (double)(System.nanoTime() - startTime2) / 1000000000;System.out.println(resultTime2 + " s");

Запустив этот код несколько раз, мы получим примерно следующее:

|`2*(i*i)`| `2*i*i`||---------|--------||0.5183738 | 0.6246434||0.5298337 | 0.6049722||0.5308647 | 0.6603363||0.5133458 | 0.6243328||0.5003011 | 0.6541802||0.5366181 | 0.6312638||0.515149  | 0.6241105||0.5237389 | 0.627815 ||0.5249942 | 0.6114252||0.5641624 | 0.6781033||0.538412  | 0.6393969||0.5466744 | 0.6608845||0.531159  | 0.6201077||0.5048032 | 0.6511559||0.5232789 | 0.6544526|

Схема очевидна: группировка переменных в круглые скобки ускоряет работу программы. Это связано с генерацией более эффективного байт-кода при умножении одинаковых значений.

Вы можете узнать больше об этом экспериментездесь. Или можете провести свой собственный тест, используя онлайн-компилятор Java.

Структуры данных

Комбинирование хеш-таблиц

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

import java.util.*;Map m1 = ;Map m2 = ;m2.putAll(m1); // добавить к m2 все элементы из m1

Array или ArrayList?

Выбор междуArrayиArrayListзависит от специфики задачи Java, которую вы хотите решить. Запомните следующие особенности этих типов:

  • Массив имеет фиксированный размер, и память для него выделяется во время объявления, а размерArrayListможет динамически меняться.

  • Массивы Java работают намного быстрее, а вArrayListнамного проще добавлять и удалять элементы.

  • При работе сArrayскорее всего возникнет ошибкаArrayIndexOutOfBoundsException.

  • ArrayList может быть только одномерным, когда массивы Java могут быть многомерными.

import java.util.*; public class ArrayVsArrayList {  public static void main(String[] args) {    // создаем массив    int[] myArray = new int[6];     // ссылаемся на несуществующий индекс    myArray[7]= 10; // ArrayIndexOutOfBoundsException     // создаем ArrayList    List myArrayList = new ArrayList<>();     // простое добавление и удаление элементов    myArrayList.add(1);    myArrayList.add(2);    myArrayList.add(3);    myArrayList.add(4);    myArrayList.add(5);    myArrayList.remove(0);     // перебор элементов ArrayList     for(int i = 0; i < myArrayList.size(); i++) {      System.out.println("Element: " + myArrayList.get(i));    }     //  многомерный массив    int[][][] multiArray = new int[3][3][3];   }}

JSON

Сериализация и Десериализация

JSON невероятно удобный и полезный синтаксис для хранения и обмена данными. Java полностью поддерживает это.

Примечание переводчика: для использования JSON из примера необходимо подключить библиотекуJSON Simple.

Вы можете сериализовать данные следующим образом:

import org.json.simple.JSONObject;import org.json.simple.JSONArray; public class JsonEncodeDemo {  public static void main(String[] args) {    JSONObject obj = new JSONObject();    obj.put("Novel Name", "Godaan");    obj.put("Author", "Munshi Premchand");     JSONArray novelDetails = new JSONArray();    novelDetails.add("Language: Hindi");    novelDetails.add("Year of Publication: 1936");    novelDetails.add("Publisher: Lokmanya Press");            obj.put("Novel Details", novelDetails);      System.out.print(obj);    }}

Получается следующая строка JSON:

{"Novel Name":"Godaan","Novel Details":["Language: Hindi","Year of Publication: 1936","Publisher: Lokmanya Press"],"Author":"Munshi Premchand"}

Десериализация в Java выглядит так:

import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.util.Iterator; import org.json.simple.JSONArray;import org.json.simple.JSONObject;import org.json.simple.parser.JSONParser;import org.json.simple.parser.ParseException; public class JsonParseTest {  private static final String filePath = "//home//user//Documents//jsonDemoFile.json";      public static void main(String[] args) {    try {      // читаем json файл      FileReader reader = new FileReader(filePath);      JSONParser jsonParser = new JSONParser();      JSONObject jsonObject = (JSONObject)jsonParser.parse(reader);                  // берем данные из json объекта      Long id =  (Long) jsonObject.get("id");      System.out.println("The id is: " + id);                  String type = (String) jsonObject.get("type");      System.out.println("The type is: " + type);       String name = (String) jsonObject.get("name");      System.out.println("The name is: " + name);       Double ppu =  (Double) jsonObject.get("ppu");      System.out.println("The PPU is: " + ppu);       // извлекаем массив              System.out.println("Batters:");      JSONArray batterArray= (JSONArray) jsonObject.get("batters");      Iterator i = batterArray.iterator();            // проходимся по всем элементам массива      while (i.hasNext()) {        JSONObject innerObj = (JSONObject) i.next();        System.out.println("ID "+ innerObj.get("id") +             " type " + innerObj.get("type"));      }       System.out.println("Topping:");      JSONArray toppingArray= (JSONArray) jsonObject.get("topping");      Iterator j = toppingArray.iterator();      while (j.hasNext()) {        JSONObject innerObj = (JSONObject) j.next();        System.out.println("ID "+ innerObj.get("id") +             " type " + innerObj.get("type"));      }    } catch (FileNotFoundException|IOException|ParseException|NullPointerException ex) {      ex.printStackTrace();    }  }}

Используемый в примере файл JSON (jsonDemoFile.json):

{  "id": 0001,  "type": "donut",  "name": "Cake",  "ppu": 0.55,  "batters":    [      { "id": 1001, "type": "Regular" },      { "id": 1002, "type": "Chocolate" },      { "id": 1003, "type": "Blueberry" },      { "id": 1004, "type": "Devil's Food" }    ],  "topping":    [      { "id": 5001, "type": "None" },      { "id": 5002, "type": "Glazed" },      { "id": 5005, "type": "Sugar" },      { "id": 5007, "type": "Powdered Sugar" },      { "id": 5006, "type": "Chocolate with Sprinkles" },      { "id": 5003, "type": "Chocolate" },      { "id": 5004, "type": "Maple" }    ]}

Примечание переводчика: в Java проектах очень часто для работы с JSON используют библиотеки Gson от Google или Jackson. Обе библиотеки очень популярны и хорошо поддерживаются. Попробуйте и их.

Ввод и Вывод

FileOutputStream или FileWriter?

Запись файлов в Java осуществляется двумя способами:FileOutputStreamиFileWriter. Какой метод выбрать, зависит от конкретной задачи.

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

File foutput = new File(file_location_string);FileOutputStream fos = new FileOutputStream(foutput);BufferedWriter output = new BufferedWriter(new OutputStreamWriter(fos));output.write("Buffered Content");

УFileWriterдругое призвание: работа с потоками символов. Поэтому, если вы пишете текстовые файлы, выберите этот метод.

FileWriter fstream = new FileWriter(file_location_string);BufferedWriter output = new BufferedWriter(fstream);output.write("Buffered Content");

Производительность и лучшие практики

Пустая коллекция вместо Null

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

import java.util.*public List getLocations() {  List result = someService.getLocationsFromDB();  return result == null ? Collections.emptyList() : result;}

Создание объектов только когда необходимо

Создание объектов одна из самых затратных операций в Java. Лучше всего создавать их только тогда, когда они действительно нужны.

import java.util.ArrayList;import java.util.List; public class Employees {  private List Employees;   public List getEmployees() {    // initialization only if necessary    if(null == Employees) {      Employees = new ArrayList();    }        return Employees;  }}

Deadlocks (Дедлоки)

Взаимная блокировка (Deadlock) потоков может происходить по многим причинам, и полностью защититься от них в Java 8 очень сложно. Чаще всего, это происходит, когда один синхронизируемый объект ожидает ресурсов, которые заблокированы другим синхронизированным объектом.

Вот пример тупика этого потока:

public class DeadlockDemo {  public static Object addLock = new Object();  public static Object subLock = new Object();   public static void main(String args[]) {    MyAdditionThread add = new MyAdditionThread();    MySubtractionThread sub = new MySubtractionThread();    add.start();    sub.start();  }   private static class MyAdditionThread extends Thread {    public void run() {      synchronized (addLock) {        int a = 10, b = 3;        int c = a + b;        System.out.println("Addition Thread: " + c);        System.out.println("Holding First Lock...");        try { Thread.sleep(10); }        catch (InterruptedException e) {}        System.out.println("Addition Thread: Waiting for AddLock...");        synchronized (subLock) {           System.out.println("Threads: Holding Add and Sub Locks...");        }      }    }  }   private static class MySubtractionThread extends Thread {    public void run() {      synchronized (subLock) {        int a = 10, b = 3;        int c = a - b;        System.out.println("Subtraction Thread: " + c);        System.out.println("Holding Second Lock...");        try { Thread.sleep(10); }        catch (InterruptedException e) {}        System.out.println("Subtraction  Thread: Waiting for SubLock...");        synchronized (addLock) {           System.out.println("Threads: Holding Add and Sub Locks...");        }      }    }  }}

Результат этой программы:

=====Addition Thread: 13Subtraction Thread: 7Holding First Lock...Holding Second Lock...Addition Thread: Waiting for AddLock...Subtraction  Thread: Waiting for SubLock...

Взаимоблокировок можно избежать, изменив порядок вызова потоков:

private static class MySubtractionThread extends Thread {  public void run() {    synchronized (addLock) {      int a = 10, b = 3;      int c = a - b;      System.out.println("Subtraction Thread: " + c);      System.out.println("Holding Second Lock...");      try { Thread.sleep(10); }      catch (InterruptedException e) {}      System.out.println("Subtraction  Thread: Waiting for SubLock...");      synchronized (subLock) {        System.out.println("Threads: Holding Add and Sub Locks...");      }    }  }}

Вывод:

=====Addition Thread: 13Holding First Lock...Addition Thread: Waiting for AddLock...Threads: Holding Add and Sub Locks...Subtraction Thread: 7Holding Second Lock...Subtraction  Thread: Waiting for SubLock...Threads: Holding Add and Sub Locks...

Резервирование памяти

Некоторые приложения Java очень ресурсоемки и могут работать медленно. Для повышения производительности вы можете выделить на машине Java больше оперативной памяти.

export JAVA_OPTS="$JAVA_OPTS -Xms5000m -Xmx6000m -XX:PermSize=1024m -XX:MaxPermSize=2048m"
  • Xms минимальный пул выделения памяти;

  • Xmx максимальный пул выделения памяти;

  • XX: PermSize начальный размер, который будет выделен при запуске JVM;

  • XX: MaxPermSize максимальный размер, который можно выделить при запуске JVM.

Примечание переводчика: PermSize и MaxPermSize, начиная с Java 8 уже больше не используются.

Решение распространенных проблем

Содержимое директории

Java позволяет вам получать имена всех подкаталогов и файлов в папке в виде массива, который затем можно последовательно прочитать:

import java.io.*; public class ListContents {  public static void main(String[] args) {    File file = new File("//home//user//Documents/");    String[] files = file.list();     System.out.println("Listing contents of " + file.getPath());    for(int i=0 ; i < files.length ; i++) {      System.out.println(files[i]);    }  }}

Выполнение консольных команд

Java позволяет выполнять консольные команды прямо из кода, используя классRuntime. Очень важно не забывать об обработке исключений.

Например, давайте попробуем открыть файл PDF через терминал Java (на Linuxe):

public class ShellCommandExec {  public static void main(String[] args) {    String gnomeOpenCommand = "gnome-open //home//user//Documents//MyDoc.pdf";     try {      Runtime rt = Runtime.getRuntime();      Process processObj = rt.exec(gnomeOpenCommand);       InputStream stdin = processObj.getErrorStream();      InputStreamReader isr = new InputStreamReader(stdin);      BufferedReader br = new BufferedReader(isr);       String myoutput = "";       while ((myoutput=br.readLine()) != null) {        myoutput = myoutput+"\n";      }      System.out.println(myoutput);    } catch (Exception e) {      e.printStackTrace();    }  }

Воспроизведение звуков

Звук важный компонент многих десктопных приложений и игр. Язык программирования Java предоставляет средства для работы с ним.

import java.io.*;import java.net.URL;import javax.sound.sampled.*;import javax.swing.*; public class PlaySoundDemo extends JFrame {    // constructor   public playSoundDemo() {      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);      this.setTitle("Play Sound Demo");      this.setSize(300, 200);      this.setVisible(true);       try {         URL url = this.getClass().getResource("MyAudio.wav");         AudioInputStream audioIn = AudioSystem.getAudioInputStream(url);         Clip clip = AudioSystem.getClip();         clip.open(audioIn);         clip.start();      } catch (UnsupportedAudioFileException|IOException|LineUnavailableException e) {         e.printStackTrace();      }   }    public static void main(String[] args) {      new PlaySoundDemo();   }}

Отправка email

Отправить электронную почту на Java очень просто. Вам просто нужно установитьJava Mailи указать путь к нему в пути к классам проекта.

import java.util.*;import javax.mail.*;import javax.mail.internet.*; public class SendEmail {  public static void main(String [] args) {        String to = "recipient@gmail.com";    String from = "sender@gmail.com";    String host = "localhost";     Properties properties = System.getProperties();    properties.setProperty("mail.smtp.host", host);    Session session = Session.getDefaultInstance(properties);     try{      MimeMessage message = new MimeMessage(session);      message.setFrom(new InternetAddress(from));       message.addRecipient(Message.RecipientType.TO,new InternetAddress(to));       message.setSubject("My Email Subject");      message.setText("My Message Body");      Transport.send(message);      System.out.println("Sent successfully!");    } catch (MessagingException ex) {      ex.printStackTrace();    }  }}

Получение координат курсора

Чтобы фиксировать события мыши, вам необходимо реализовать интерфейсMouseMotionListener. Когда курсор попадает в определенную область, срабатывает обработчик событияmouseMoved, из которого вы можете получить точные координаты (используя Swing для UI)

import java.awt.event.*;import javax.swing.*; public class MouseCaptureDemo extends JFrame implements MouseMotionListener {  public JLabel mouseHoverStatus;   public static void main(String[] args) {    new MouseCaptureDemo();  }   MouseCaptureDemo() {    setSize(500, 500);    setTitle("Frame displaying Coordinates of Mouse Motion");     mouseHoverStatus = new JLabel("No Mouse Hover Detected.", JLabel.CENTER);    add(mouseHoverStatus);    addMouseMotionListener(this);    setVisible(true);  }   public void mouseMoved(MouseEvent e) {    mouseHoverStatus.setText("Mouse Cursor Coordinates => X:"+e.getX()+" | Y:"+e.getY());  }   public void mouseDragged(MouseEvent e) {  }}
Подробнее..

Перевод Напишем и поймем Decision Tree на Python с нуля! Часть 4. Структуры данных

04.11.2020 14:09:33 | Автор: admin
Данная статья четвертая в серии. Ссылки на предыдущие статьи: первая, вторая, третья

4.1 Структуры данных


Структура данных это представление того, как организованы отдельные данные.

Массив


image

Отдельные данные представлены одним рядом. Чтобы идентифицировать один фрагмент данных, например, каким по порядку является этот отдельный фрагмент массива, необходим его идентификатор.

#Пример реализации массива в Pythona = [2,6,4,5,1,8]


Таблица, двумерный массив


image

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

#Пример реализации таблицы в Pythona = [   [2,6,4,5,1,8],   [4,4,1,3,4,2],   [5,3,6,6,5,3],   [7,8,0,9,5,3],]


Tree, древовидная структура




Это структура данных, где отдельные данные как будто связаны линией.
Эти линии являются маршрутом, напрямую соединяющим одни данные с другими. И один такой маршрут, например, и будет являтся древовидной структурой данных.
Часто такую структуру изображают как схему, напоминающую растущее сверху вниз дерево. Линии называются ребрами или ветвями, данные узлами (node), данные, после которых линия не продолжается, называются листьями(leaf), а данные, располагающиеся на самом верху корневым узлом или просто корнем.

# Пример реализации Tree в Python, содержащий список дочерних узлов.# Записывается в формате [Значение,список дочерних массивов]. Например, дерево с картинки выше будет записываться в порядке сверху вниз, слева направо.# Помимо подобной записи, существуют варианты с использованием классов, родительских нодов и другие.tree = \[2,[     [6,[]],     [4,[          [5,[               [6,[]],          ]],          [8,[]],          [1,[]],     ]],]]# Функция для преобразования древовидной структуры в символьнуюdef tstr(node,indent=""):      s = indent+str(node[0])+"\n"      for c in node[1]: # Цикл на дочернем ноде           s += tstr(c,indent+"+-")           pass      return sprint(tstr(tree))# Вывод# 2# +-6# +-4# +-+-5# +-+-+-6# +-+-8# +-+-1# Можно не создавать все дерево сразу, а создать несколько переменных для нодов# Создаем все ноды-листья. Цифра в переменной указывает на строку нода и его порядковый номер в этой строке слева направо.n10 = [6,[]]n21 = [8,[]]n22 = [1,[]]n40 = [6,[]]# Создаем родительские ноды для уже созданных дочерних.n20 = [5,[n40]]n11 = [4,[n20,n21,n22]]n00 = [2,[n10,n11]]# Выводим получившееся Древо, указав нод, который необходимо считать корневым.print(tstr(n11))# Вывод# 4# +-5# +-+-6# +-8# +-1


Network или графы


image

Это структура, в которой у одного нода может быть несколько ребер. Отдельные данные также называются узлами, а линии ребрами. Зачастую в графах нет корневого узла, как в древовидной структуре.

# Реализация графа на Pythonimport pandas as pd# В нашем случае имя узла и его значение совпадают.# Если имя нода и его значение не совпадают, необходимо будет достать значение нода.nodes = [2,6,4,5,8,1]# Указываем связи между нодами в виде матрицы. Если от нода 2 (первая строка)к ноду 6 (вторая строка) исходит ребро, # Значение 1-ой строки 2-ого столбца матрицы будет 1, а если ребро не проходит, то - 0. Такая матрица называется матрица смежности.df = pd.DataFrame([  # 2,6,4,5,8,1   [ 0,1,1,0,0,0], # от нода 2  [ 1,0,0,1,0,0], # от нода 6  [ 1,0,0,1,1,1], # от нода 4  [ 0,1,1,0,0,0], # от нода 5  [ 0,0,1,0,0,0], # от нода 8  [ 0,0,1,0,0,0], # от нода 1],columns=nodes,index=nodes)print(df)# Вывод#     2  6  4  5  8  1# 2  0  1  1  0  0  0# 6  1  0  0  1  0  0# 4  1  0  0  1  1  1# 5  0  1  1  0  0  0# 8  0  0  1  0  0  0# 1  0  0  1  0  0  0# С помощью библиотек matplotlib и networkx рисуем граф.import matplotlib.pyplot as pltimport networkx as nxplt.figure(figsize=(4,4))plt.axis("off")nx.draw_networkx(nx.from_pandas_adjacency(df))plt.show()


Вывод:
image

4.2 Пример реализации Decision Tree в Python


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

Как показано ниже, корневой узел соединяет все данные. Числовое значение [...], прикрепленное к узлу, представляет собой номер исходных данных, из которых создается это дерево решений. Из корневого узла спускаются вниз только те данные, которые удовлетворяют условиям дочерних узлов. Например, это видно, если посмотреть на узлы иду на гольф и не иду на гольф в первом дереве решений.

image

image

Реализация в python-е происходит, например, следующим образом. Делаем один узел ассоциативным массивом, name это символьное представление состояния этого узла, df это данные, связанные с этим узлом, а ребра это список дочерних узлов.

# Данные древовидной структурыtree = {      # name: название этого узла      "name":"decision tree "+df0.columns[-1]+" "+str(cstr(df0.iloc[:,-1])),      # df: данные, связанные с данным узлом      "df":df0,# edges: список ребер (ветвей), выходящих из данного узла. Если узел листовой, то есть если ребер у него нет, массив остается пустым.    "edges":[],}

Функция tstr, текстифицирующая данную древовидную структуру, будет выглядеть следующим образом.

# Лямбда-выражение для распределения значений, аргумент - pandas.Series,  возвращаемое значение - массив с количеством каждого из значений# Из вводных данных s с помощью value_counts() находим частоту каждого из значений, и пока в нашем словаре есть элементы, будет работать цикл, запускаемый items().# Чтобы выходные данные не менялись с каждым запуском цикла, мы используем sorted,  который меняет порядок от большего к меньшему# В итоге, генерируется массив, содержащий строку из следующих компонентов: ключ (k) и значение (v).cstr = lambda s:[k+":"+str(v) for k,v in sorted(s.value_counts().items())]# Метод текстификации дерева: аргумент - tree (структура данных дерева), indent (отступ дочерних узлов),# А возвращаемое значение будет текстовым отображением tree. Данный метод вызывается рекурсивно для преобразования всего в дереве в символы.def tstr(tree,indent=""):    # Создаем символьное представление этого узла.# Если этот узел является листовым узлом (количество элементов в массиве ребер равно 0),# Частотное распределение последнего столбца данных df, связанных с деревом, преобразуется в символы.s = indent+tree["name"]+str(cstr(tree["df"].iloc[:,-1]) if len(tree["edges"])==0 else "")+"\n"      # Зацикливаем все ветви этого узла.for e in tree["edges"]:# Добавляем символьное представление дочернего узла к символьному представлению родительского узла.# Добавляем еще больше символов к indent этого узла.s += tstr(e,indent+"  ")passreturn s

Decision Tree, текстифицированное функцией tstr, будет выглядеть следующим образом. В корневом узле отображается текст (decision tree Гольф), который мы задали в самом начале при создании переменной tree, а также частотное распределение иду / не иду на гольф со всех данных. Каждый узел представленный ниже, отображает правила ветвления, и в случае, если узел оказывается листовым, отображается частотное распределение иду / не иду на гольф на основе данных, связанных с этим узлом.

decision tree Гольф [':5', ':9']  Погода=Ясно    Влажность=Нормальная[':2']    Влажность=Высокая[':3'] Погода=Облачно[':4'] Погода=Дождь    Ветер=Есть[':2']    Ветер=Нет[':3']


Спасибо за прочтение!

Мы будем очень рады, если вы расскажете нам, понравилась ли вам данная статья, понятен ли перевод, была ли она вам полезна?
Подробнее..

Перевод Пирамидальная сортировка, сортировка слиянием и выпуклая оболочка

07.04.2021 18:21:37 | Автор: admin

Базовые алгоритмы сортировки


Пирамидальная сортировка (или сортировка кучей, Heap Sort)

Куча (heap) это не что иное, как двоичное дерево с некоторыми дополнительными правилами, которым оно должно следовать: во-первых, оно всегда должно иметь структуру кучи, где все уровни двоичного дерева заполняются слева направо, и, во-вторых, оно должно быть упорядочено в виде max-кучи или min-кучи. В качестве примера я буду использовать min-кучу.

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

Ниже приведен псевдокод пирамидальной сортировки:

HeapSort(A[1n]):1 - H = buildHeap(A[1n])2 - for i = 1 to n do:3 -   A[i] = extract-min(H)

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

Ниже приведен псевдокод buildHeap:

buildHeap():1 - Изначально H пустая2 - for i = 1 to n do:3 -   Add(H, A[i])

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

Теперь самый маленький элемент в куче находится в последнем узле, что нам и нужно. Мы знаем, что он находится в отсортирован относительно остальных элементов, поэтому его можно полностью удалить из кучи (функция extract-min). Но нам нужно сделать еще кое-что: убедиться, что новый элемент корневого узла находится на своем месте! Маловероятно, что элемент, который мы сделали корневым узлом, находится на своем месте, поэтому мы переместим элемент корневого узла вниз в его корректное положение, используя функцию, которая обычно называется heapify (приведение к виду кучи).

Ниже приведен псевдокод extract-min и heapify:

extract-min(H):1 - min = H[1]2 - H[1] = H[H.size()]3 - уменьшаем H.size() на 14 - heapify(H, 1)5 - return minheapify():1 - n = H.size()2 - while (LeftChild(i) <= n and H[i] > H[LeftChild(i)]) or (RightChild(i) <= n and H[i] > H[RightChild(i)]) do:3 -   if (H[LeftChild(i)] < H[RightChild(i)]) then:4 -     j = LeftChild(i)5 -   else:6 -     j = RightChild(i)7 -   меняем местами H[i] и H[j]8 -   i = j

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

Сортировка слиянием (Merge Sort)

Алгоритм сортировки слиянием это алгоритм сортировки, который сортирует коллекцию, разбивая ее на половины. Затем он сортирует эти две половины и объединяет их вместе, чтобы сформировать одну полностью отсортированную коллекцию. В большинстве реализаций сортировки слиянием все это делается посредством рекурсии.

Сортировка слиянием это алгоритм разделяй и властвуй, который можно свести к 3 шагам:

  • Разделите (разбейте) задачу на минимально возможные подзадачи одного типа.

  • В первую очередь победите (решите) самые мелкие подзадачи. После того, как вы нашли рабочее решение, используйте ту же самую технику для решения более крупных подзадач другими словами, решайте подзадачи рекурсивно.

  • Объединяйте результаты и соединяйте более мелкие подзадачи, пока вы, наконец, не примените то же решение к более крупной и сложной задаче, с которой вы начинали!

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

Функция mergeSort в свою очередь состоит из двух функций:

  • функции слияния, которая фактически объединяет два списка вместе и сортирует их в правильном порядке.

  • функции mergeSort, которая будет продолжать разделять входной массив снова и снова (рекурсивно), а затем будет вызывать функцию слияния снова и снова (также рекурсивно).

Ниже приведен псевдокод сортировки слиянием:

Merge(A, B):1 - i = 1; j = 1; k = 1;2 - a_(m+1) = ; b_{n+1} = 3 - while (k <= m+n) do:4 -   if (a_i < b_j) then5 -     c_k = a_i; i++;6 -   else7 -     c_k = b_j; j++;8 -   k++;9 - return C = {c_1, c_2, , c_(m+n)}MergeSort(X, n)1 - if (n == 1) return X2 - middle = n/2 (round down)3 - A = {x_1, x_2, , x_middle}4 - B = {x_(middle+1), x_{middle+2), , x_n}5 - As = MergeSort(A, middle)6 - Bs = MergeSort(B, n - middle)7 - return Merge(As, Bs)

Именно потому, что сортировка слиянием реализована рекурсивно, это делает ее быстрее, чем другие алгоритмы, которые мы рассмотрели до сих пор. Сортировка слиянием имеет время выполнения O(n log n).

Выпуклая оболочка (или выпуклый контур, Convex Hull)

Нам дан набор точек на плоскости. Выпуклая оболочка множества это наименьший выпуклый многоугольник, содержащий все его точки.

Подход 1 Упаковка подарков (Gift Wrapping) O(n)

Идея проста, мы начинаем с самой левой точки (или точки с минимальным значением координаты x) и продолжаем оборачивать точки против часовой стрелки. Если точка p является текущей точкой, следующая точка выбирается как точка, которая перекрывает все остальные точки по ориентации против часовой стрелки. Ниже приводится псевдокод:

1 - ConvexHull = пустой список2 - Ищем точку с наименьшим x и присваиваем ее u (:= u_original)3 - Пусть L будет вертикальной линией, проходящей через u3 - Do:4 -   Пусть v будет точкой с наименьшим углом между L и u * v5 -   Добавляем v в ConvexHull6 -   Пусть L = uv линии7 -   u := v8 - Until v = u_original9 - Выводим ConvexHull

Подход 2 Алгоритм Грэхема (Graham Scan) O(n log n)

Этот алгоритм можно разделить на 2 этапа:

  • Этап 1 (сортировка точек): сначала мы находим самую нижнюю точку. Идея состоит в том, чтобы предварительно обработать точки, сортируя их по самой нижней точке. После сортировки точек они образуют простой замкнутый путь. Какими должны быть критерии сортировки? Вычисление фактических углов было бы неэффективным, поскольку тригонометрические функции не являются простыми в вычислении. Идея состоит в том, чтобы использовать ориентацию для сравнения углов без их фактического вычисления.

  • Этап 2 (включении или отбрасывание точек): после того, как у нас есть замкнутый путь, следующим шагом будет прохождение по пути и удаление из него вогнутых точек. Первые две точки в отсортированном массиве всегда являются частью выпуклой оболочки. Для оставшихся точек мы смотрим на последние три точки и находим угол, образованный ими. Пусть это три точки: prev(p), curr(c) и next(n). Если ориентация этих точек (рассматривая их в том же порядке) не против часовой стрелки, мы отбрасываем c, в противном случае мы оставляем ее.

1 - ConvexHull = пустой список2 - Пусть u - точка с наименьшим значением x (если таких точек несколько, выбираем ту, у которой наименьший y)3 - Сортируем оставшиеся точки по угловой фазе в порядке против часовой стрелки вокруг u4 - Добавляем u и первую точку в ConvexHull5 - For i = 2 to n - 1:6 -   While угол между предпоследней, последней и точкой i > 180 градусов:7 -     Удаляем точку из ConvexHull8 -   Добавляем i в ConvexHull9 - Return ConvexHull

Если вам интересно следить за моей работой в области компьютерных наук и машинного обучения, вы можете посетить мои Medium и GitHub, а также другие проекты на https://jameskle.com/. Вы также можете написать мне мне в Твиттере, но электронной почте или найти меня в LinkedIn. Или подписывайтесь на мою рассылку, чтобы получать мои последние статьи прямо на ваш почтовый ящик!


Перевод статьи подготовлен в преддверии старта курса Алгоритмы и структуры данных.

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

Подробнее..

Как я пытался придумать новый подход к изучению алгоритмов через интерактивные визуализации

16.06.2021 22:19:34 | Автор: admin

Представьте человека, который изучает алгоритмы. Чтобы понять как они работают, приходится изучать их код и представлять, как компьютер будет его выполнять. Это странно почему мы должны учиться думать как компьютер, вместо того, чтобы заставить его помогать нам учиться? Какая-то сильная технозависимость.

На мой взгляд, потеть должна машина, а человек учиться, не выворачивая мозги наизнанку. Поэтому я подумал, а почему бы не визуализировать работу алгоритмов? Визуализации помогли бы не закапываться в код, а наглядно показали бы как работают алгоритмы и позволили бы понять их. Что у меня получилось читайте в этой статье.

В результате родился проект Объясняем код. Посмотреть, что это такое можно на code-explained.com. Код проекта выложен на Гитхаб.

Чем я вдохновлялся

Брет Виктор говорит, что творцам нужна мгновенная обратная связь с тем, что они делают. Когда вы создаете что-то или вносите изменения в свое творение, то должны сразу же видеть результат. Иначе, часть идей просто никогда не придут к вам в голову.

Например, если вы редактируете программный код, то результат должен отображаться в реальном времени, без повторной компиляции. Иначе, вы не увидите промежуточные состояния, и у вас не родятся связанные с ними идеи. Я не буду подробно пересказывать эту мысль, тем более, что на Хабре уже подробно о ней рассказали. Почитайте, если еще не успели.

Идея Брета Виктора о мгновенной обратной связи сильно меня вдохновила. Я подумал, что она могла бы изменить подход к изучению программирования, в частности алгоритмов и структур данных. Потому что сегодня этот процесс ужасно технозависим.

Как сегодня изучают алгоритмы

Преподаватель записывает на доске код, а потом по шагам объясняет, как он работает.

В видео на Youtube происходит нечто подобное ведущий берет код алгоритма и отрисовывает этапы его работы

На ИТ-ресурсах создают анимации.

А кто-то даже пытается объяснить работу алгоритма через танец.

Почему эти подходы казались мне неэффективными

У всех перечисленных подходов есть огромный минус. Они пытаются заставить нас думать как компьютер, а не наоборот подчинить железо нашим потребностям. Чтобы понять, как работает код алгоритма, нужно представлять, как машина будет его выполнять. Я решил, что хочу уйти от этого подхода.

Как я перешел от технозависимости к человечности

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

Вместе с командой мы разработали проект Объясняем код. Это такой интерактивный учебник, который с помощью визуализаций объясняет, как работают алгоритмы.

Выглядит это как-то так. Можно вводить свои данные. А затем с помощью плеера запускать и останавливать исполнение кода, перематывать его вперед и назад. При этом визуализация в реальном времени показывает, что делает алгоритм.

Как это технически реализовано

Для кода на Python написан аналогичный код на JS, который после выполнения каждой строчки сохраняет состояние. Такой трюк помогает исполнять код на фронтенде без тяжелого интерпретатора. Чтобы убедиться, что код на JS делает то же самое, что и на Python, есть автотестер он прогоняет код на обоих языках и сравнивает результаты.

Чтобы сэкономить на копировании данных, я применил Immutable.js. Immutable.js это библиотека неизменяемых коллекций. Модификации таких коллекций всегда возвращают новую коллекцию, которую легко сохранить. Так я избегаю глубокого копирования. Не буду вдаваться в подробности, на Хабре про immutable.js уже писали. Для примера кусочек кода с итерированием по хеш-таблице:

while (true) { // Итерируемся по списку    this.addBP('check-not-found'); // Метод сохраняем состояние    if (this.newList.get(this.newListIdx) === null) {        // this.newList -- это немутабельный список        break;    }    this.addBP('check-found'); // Выполнена очередная строчка, сохраняем состояние    if (EQ(this.newList.get(this.newListIdx), this.number)) {        this.addBP('found-key');        return true;    }    this.fmtCollisionCount += 1; // Для динамических комментариев иногда нужно сохранять статистикуу    this.newListIdx = (this.newListIdx + 1) % this.newList.size; // Переходим к следующему индекксу    this.addBP('next-idx');}

В любой момент пользователь может отмотать время слайдером, и мы должны быть готовы анимировать переход. Поэтому анимации сделаны декларативно на React и CSS. На React описываются нужное состояние, а CSS transitions автоматически высчитывают и анимируют переход. Это позволяет избежать совсем сложного императивного кода с запутанным состоянием и ручными вычислениями. Получается тоже не просто, и в итоге анимациями заведует огромный класс на 1000 строк. Но я уверен, что императивно реализовать то же самое у меня вообще не получилось бы.

Много строк возникает из-за особенностей браузерных API и производительности в разных браузерах. Например, большой проблемой оказалось сделать так, чтобы браузеры не склеивали последовательные изменения друг с другом. Если добавить div с определённой начальной позицией, и потом сразу же поменять координаты на конечные, то браузер склеит эти два изменения в одно. Div сразу окажется в конечной позиции без анимации. Чтобы такое не происходило, приходится вставлять задержку в два фрейма анимации с помощью window.requestAnimationFrame().

Если вам интересно подробнее про техническую реализацию, напишите об этом в комментариях. Я сделаю более подробный пост про техническую реализацию.

Код проекта на гитхабе

Что дальше?

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

Подробнее..

Что нам стоит дом построить? (часть 2)

21.06.2021 12:17:59 | Автор: admin

Привет, Хабр. В прошлой статье я рассказал о начальном анализе предметной области и базовом проектировании нашей новой ECM-системы. Теперь я расскажу о первой практической задаче, которую мы решили. А именно - о выборе способа организации структуры хранилища бизнес-атрибутов объектов.

Напомню, что в результате анализа мы пришли к следующей структуре объекта:

Системные и интеграционные поля содержат техническую информацию и редко подвержены модификациям. А вот бизнес-поля - искомые атрибуты объектов - могут и будут различаться для разных типов документов, которых у нас на входе предполагается около 1000. Проанализировав возможность повторного использования полей, мы пришли к выводу, что у разных типов документов может быть не более 15% общих свойств. И при этом всё равно придется скрепя сердце согласиться на неполное соответствие названия и содержания.

Таким образом, есть большое количество сущностей с разнообразным атрибутивным составом, который нужно хранить, тратя как можно меньше ресурсов, и расширять который хотелось бы также с минимальными усилиями.

Какие есть варианты?

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

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

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

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

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

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

Также стоит упомянуть про еще один способ организации данных, с которым я сталкивался. Он используется не в пример реже и больше напоминает "велосипед" - назовем его вертикальный. В хранилище создается таблица, которая содержит, условно, название поля, данные в нем, тип данных и связь с объектом. Возможно создание нескольких таких таблиц для разных типов данных или несколько колонок в одной таблице. Далее, при извлечении такие данные трансформируются из списка в плоский набор полей. Понимаете, почему я упомянул про велосипед?

А что у нас?

Второй вариант - это использование специализированных документоориентированных (или документных, как больше нравится) баз данных, реализующих NoSQL-подход к хранению и обработкенеструктурированной или слабоструктурированной информации. Наиболее часто данные хранятся в виде JSON объектов, но с предоставлением производителями СУБД инструментария для доступа к данным внутри этих структур.

У такого подхода, применительно к проектируемой системе, можно выделить несколько плюсов:

  • можем управлять структурой объектов без программирования, в том числе в реальном времени, путем администрирования. Администратор сможет зайти в некий интерфейс справочника полей и задать новые.

  • ограничиваем ситуации случайного воздействия на данные - модифицировать json объект гораздо сложнее, чем данные в колонке.

  • проще описывать объекты в коде - иногда можно вообще не описывать структуру документа в коде, а работать прямо с полями в JSON.

Но есть и минусы:

  • невозможно нативно реализовать проверки данных при размещении в хранилище.

  • валидацию данных придется проводить в коде.

  • невозможно организовать внутреннюю связность подчиненных данных, например, справочников.

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

Чтобы продемонстрировать применимость такого подхода, мы решили сравнить, насколько оба варианта будут отличаться друг от друга по производительности, расширяемости и объемам хранилища.

Делаем прототип

Возьмем гипотезу, что NoSQL-подход для нашей системы применим как минимум не хуже, чем классический. Для ее проверки создадим прототип, в рамках которого реализуем оба подхода. В качестве СУБД возьмем Postgre, который уже давно умеет хорошо работать с JSON полями.

Создадим следующие таблицы:

Для описания объектов в табличном виде:

  • r_objects, базовые данные по объектам: тип, дата создания и ссылка на хранилище атрибутов.

  • r_attributes. атрибуты, во всех возможные колонки для всех объектов. В идеале конечно же хранилища атрибутов для разных объектов лучше разбить по разным таблицам, но для прототипа это не критично.

Для описания объектов в виде JSON:

  • objects. Данные по объектам, где в поле data формата jsonb хранятся искомые атрибуты.

Остальные таблицы - это различные вспомогательные хранилища.

Реализуем в коде 5 разных типов объектов, для каждого из них описав программную структуру, механизм извлечения данных из хранилища и наполнения этими данными самих объектов.

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

Для тестирования обоих подходов хранения данных используем следующие методы:

  • добавление данных по объекту. Критерий успешности: объект с данными появился в хранилище, метод вернул в ответе его идентификатор.

  • обновление данных по объекту. Критерий успешности: данные в хранилище были изменены, метод вернул отметку об успехе. Удаление данных в системе не предусматривается, поэтому удаление объекта является операцией, аналогичной обновлению.

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

Генерация запросов будет происходить в 20 параллельных потоков по 50 запросов в каждом потоке. Для того, чтобы тестирование было действительно показательным с точки зрения производительности, предварительно наполним базу 200 млн. объектов.

Тестирование показало следующие результаты:

График по тестированию табличного хранилищаГрафик по тестированию табличного хранилищаГрафик по тестированию NoSQL-хранилищаГрафик по тестированию NoSQL-хранилища

Первая (высокая) часть графика - это получение объектов по случайной странице - пагинация. Здесь в обоих случаях пришлось применить небольшой трюк - так как Postgres не агрегирует точное число строк в таблице, то узнать, сколько всего записей на объеме данных теста простым count - это долго, и для получения количества записей пришлось брать статистику данных по таблице. Также время получения данных на страницах свыше 10000-й неприлично велико, поэтому верхняя планка для получения случайного номера страницы была установлена в 10000. Учитывая специфику нашей системы, получение пагинированных данных не будет частой операцией, и поэтому такое извлечение данных применяется исключительно в целях тестирования.

Вторая (средняя) часть графика - вставка или обновление данных.

Третья (низкая) часть графика - получение данных по случайному идентификатору.

В случае табличной структуры данных - линий на графике больше, потому что с точки зрения программной структуры, получение данных для каждого типа объекта идет по разным методам, тогда как для NoSQL - по одинаковым.

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

Результаты тестов на 40000 запросов приведу в виде таблицы:

Табличная

NoSQL

Объем хранилища

74

66

Среднее количество операций в секунду

970

1080

Время тестирования, секунды

42

37

Количество запросов

40000

40000

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

Что получилось?

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

Подробнее..

Понимаем красно-черное дерево. Часть 1. Введение

01.05.2021 18:10:38 | Автор: admin

Довольно долгое время я воевал с красно-черным деревом (далее - кчд). Вся информация, которую я находил, была в духе "листья и корень дерева всегда черные, ПОТОМУ ЧТО", "топ 5 свойств красно-черного дерева" или "3 случая при балансировке и 12 случаев при удалении ноды". Такой расклад меня не устраивал.

Мне не хотелось заучивать свойства дерева, псевдокод и варианты балансировки, я хотел знать: почему. Каким образом цвета помогают при балансировке? Почему у красной ноды не может быть красного потомка? Почему глубину дерева измеряют "черной высотой"?

Ответы на эти вопросы я получил только тогда, когда мне дали ссылку на лекцию про два-три дерево, с которого мы и начнем.

Эта статья разделена на 3 логические части. Я рекомендую прочитать их в указанном порядке. Первая часть (данная) будет направлена на введение в кчд и знакомство с ним. Во второй части мы поговорим о балансировке и вставке в кчд. В третьей, завершающей, части мы разберем процесс удаления ноды. Наберитесь терпения и приятного чтения:)

Дисклеймер

  1. В этой статье не будет информации про плюсы и минусы дерева, его применение и т.д.: информации об асимптотике дерева и работе с ним в интернете полно.

  2. Материал предназначен для тех, кто уже знаком с кчд и теперь хочет их понять, а также для тех, кто только знакомится с ними.

  3. Статья не будет содержать деталей реализации структуры.

  4. Можно считать что эта статья перевод английского видео материала в упрощенный русский текстовый вариант. Все ссылки я оставляю в конце статьи.

Два-три дерево

Чтобы понять красно-черное дерево, нужно понять два-три дерево

Забегая вперед, скажу, что два-три дерево - это, по сути, родитель нашего кчд, поэтому важно начать именно с него. Поймем два-три дерево - поймем и кчд.

Два-три дерево - абстрактный тип данных, напоминающий по структуре дерево. В нодах два-три дерева может быть одно или два значения и два или три потомка (от чего зависит количество значений и потомков ноды, узнаем ниже). Ноду с одним значением и двумя потомками будем называть 2-нода, ноду с двумя значениями и тремя потомками - 3-нода. Объяснение я начну с создания такого дерева: это наглядно и просто. Но некоторые уточнения нужны все же вначале:

  1. Добавляя элемент, мы всегда спускаемся вниз по дереву.

  2. Дерево отсортировано классически - меньшие значения находятся слева, бОльшие - справа.

  3. Два-три дерево - отсортированное, сбалансированное дерево.

Итак, начнем с первой ноды, это число 5. Тут все просто - 5 становится корнем.

Добавим число 12. Число 12 мы так же добавляем в корень (помним, что нода может иметь два значения), но теперь нам нужно "отсортировать" нашу ноду (сортировать два элемента, ха), т.е. уложить их в порядке возрастания. В итоге получается нода 5-12.

Добавим следующее число. Пусть это будет 17. Давайте пока добавим наш элемент в единственную ноду и снова отсортируем ее. Получилась нода 5-12-17.

Внимательный читатель заметит тут подвох - выше я говорил о том, что наши ноды могут содержать не более двух элементов. Вот тут и происходит магия! Мы берем средний элемент нашей ноды и "просачиваем" его наверх. Итог виден на картинке. Корнем стало число 12, левым сыном число 5, правым - 17.

То, что мы сделали выше, можно назвать балансировкой два-три дерева. Правило в этой балансировке простое: если в ноде оказывается три значения, среднее значение мы "просачиваем" вверх. Алгоритм действий зависит от 3 условий:

  1. Нода является корнем. Тогда ничего не остается, как создать новую ноду с одним значением и сделать ее новым корнем (как в нашем случае).

  2. Родительская нода имеет одно значение. Тогда мы просто добавляем значение к родителю и завершаем балансировку (при этом у родителя появляется третий потомок).

  3. Родительская нода имеет два значения. Тогда мы снова просачиваем значение вверх, пока не придем к пункту один или два.

Второй и третий случай балансировки будут рассмотрены ниже.

Окей, идем дальше. Давайте добавим число 3. Так как теперь мы не ограничиваемся одной нодой, спускаемся вниз. Понятно, что спуститься надо влево и добавить 3 к ноде со значением 5. Не забываем расставить 3 и 5 в нужном порядке. В итоге получилась нода 3-5.

Потерпите, мы близимся к самому интересному:)

Давайте добавим число 4 которое также пойдет влево и присоединится к ноде 3-5. Получится нода 3-4-5, которую, как мы уже знаем, нужно привести к нужному виду. Что делаем? Балансируем наше дерево, т.е. "просачиваем" значение 4 наверх.

Теперь самое интересное. Помимо того, что мы добавим 4 к корню, мы так же добавим корню третьего потомка - это будет нода, которая была больше 4. В нашем случае это 5. Картина будет выглядеть так:

Почему 5 не могла остаться на месте в своей ноде? Тут мы вспоминаем правило отсортированного дерева: значения меньше - слева, значения больше - справа. И так как 5 больше 4, мы не может оставить 5 слева, т.к. это нарушит наш инвариант. Поэтому ноде со значением 5 ничего не остается, как "переехать", и стать потомком ноды 4-12 (кстати, если бы у 5 были потомки, они так же "переехали" бы вместе с родителем).

Тут нужно сделать небольшую паузу и объяснить, как ориентироваться в нодах с тремя потомками. Все просто:

  1. Значение, что меньше левого значения в ноде, будет левым потомком.

  2. Значение, что больше левого, но меньше правого значения в ноде, будет средним потомком.

  3. Значение, что больше правого значения в ноде, будет правым потомком.

А теперь посмотрите на то, что получилось. Смело можно заявить, что это отсортированное, сбалансированное два-три дерево. Значения меньше лежат слева, значения больше - справа (тут, как и в случае со значениями 4-5-12, мы считаем, что 5 лежит справа от 4 и слева от 12). Корень имеет два значения и три потомка, что абсолютно соответствует описанию дерева выше. Сейчас вы можете сами попробовать добавить любое значение, чтобы удостовериться, что вы поняли логику (что произойдет с деревом, если добавить значение 7? А затем 9?).

Окей, с самим деревом, я надеюсь, разобрались. Ноды добавляются, дерево балансируется, искать можно, все супер. Но есть нюансы.

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

Красно-черное дерево

Как мы выяснили, главный недостаток два-три дерева - его структура. Тогда давайте попробуем превратить два-три дерево в дерево бинарное. Как мы можем сделать это?

Чтобы добиться этого, нам нужно простое представление для 3-ноды. Давайте разделим 3-ноду на две 2-ноды и свяжем их ссылками. Пометим эти ссылки каким-нибудь свойством, например, цветом, (возьмём красный), чтобы отличать такие ссылки в дереве от всех остальных.

Вообще говоря, мы не можем пометить сами ссылки, поэтому мы помечаем ноды.

Итак, перед вами красно-черное дерево. Далее, мы разберем несколько свойств кчд, которые я считаю важными (но я думаю, что уже из прочитанного выше многое стало ясно).

Свойства красно-черного дерева

Спойлер

Фактически мы разбираем не просто кчд, а левосторонне кчд - одна из имплементаций кчд, в которой красные ноды могут находится только слева, то есть от 3-ноды мы всегда отделям значение меньше (что и было сделано выше). По сути своей левостороннее кчд ничем не отличается от обычного, за исколючением того, что это более простая и понятная имплементация. Аналогично левостороннему кчд, существует и правостороннее кчд(логику его додумайте сами).

Свойство 1.

Две красные ноды не могут идти подряд. Это свойство приобретает смысл, если мы знаем, что красная нода - это по сути часть 3-ноды в 2-3 дереве. Ну а если две красные ноды идут подряд, то получается 4-нода, которой у нас не существует:)

Свойство 2.

Корень дерева всегда черный. Опять же, тут все понятно: красная нода не может существовать без потомка.

Свойство 3.

Все null-ноды (ноды, которые не имеют потомков) - черные. Почему именно так, для себя объяснений я не нашел. Но хочу сказать, что это довольно удобное правило, когда дело доходит до балансировки дерева.

Про потомков

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

Свойство 4.

Высота дерева измеряется только по черным нодам и называется "черной высотой". Тут опять все в целом становится очевидным: красная нода является только дополнением к ноде черной, является ее частью, поэтому высоту принято считать по черным нодам.

На этом введение подходит к концу. В следующей части мы поговорим о том, как вставлять ноды в дерево и балансировать его.

Подробнее..

В поисках упорядоченного множества в Python разбираемся с теорией и выбираем лучшую реализацию

24.05.2021 10:16:39 | Автор: admin


Множество (Set) структура данных, которая позволяет достаточно быстро (в зависимости от реализации) применить операции add, erase и is_in_set. Но иногда этого не достаточно: например, невозможно перебрать все элементы в порядке возрастания, получить следующий / предыдущий по величине или быстро узнать, сколько элементов меньше данного есть в множестве. В таких случаях приходится использовать Упорядоченное множество (ordered_set). О том, как оно работает, и какие реализации есть для питона далее.


Стандартный Set


В языке Python есть стандартная стукрура set, реализованная с помощью хэш-таблиц. Такую структуру обычно называют unordered_set. Данный метод работает так: каждый элемент присваивается какому-то классу элементов (например, класс элементов, имеющих одинаковый остаток от деления на модуль). Все элементы каждого класса хранятся в одтельном списке. В таком случае мы заранее знаем, в каком списке должен находиться элемент, и можем за короткое время выполнить необходимые операции. Равновероятность каждого остатка от деления случайного числа на модуль позволяет сказать, что к каждому классу элементов будет относиться в среднем size / modulo элементов.


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


Что есть в других языках


В языке c++ есть структура std::set, которая поддерживает операции изменения, проверку на наличие, следующий / предыдущий по величине элемент, а также for по всем элементам. Но тут нет операций получения элемента по индексу и индекса по значению, так что надо искать дальше (индекс элемента количество элементов, строго меньших данного)


И решение находится достаточно быстро: tree из pb_ds. Эта структура в дополнение к возможностям std::set имеет быстрые операции find_by_order и order_of_key, так что эта структура именно то, что мы ищем.


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


Таким образом, целью этой статьи станет поиск аналога этой структуры в Python.


Как будем тестировать скорость работы структур данных


Для оценки времени работы я написал программу, которая будет выполнять последовательно несколько типов операций:


  1. Добавление в множество миллиона случайных чисел (при данном сиде среди них будет 999'936 различных)
  2. Проверка миллиона случайных чисел на присутствие в множестве
  3. Прохождение циклом по всем элементам в порядке возрастания
  4. В случайном порядке для каждого элемента массива узнать его индекс (а, соответственно, и количество элементов, меньше данного)
  5. Получение значения i-того по возрастанию элемента для миллиона случайных индексов
  6. Удаление всех элементов множества в случайном порядке

from SomePackage import ordered_setimport randomimport timerandom.seed(12345678)numbers = ordered_set()# adding 10 ** 6 random elements - 999936 uniquelast_time = time.time()for _ in range(10 ** 6):    numbers.add(random.randint(1, 10 ** 10))print("Addition time:", round(time.time() - last_time, 3))# checking is element in set for 10 ** 6 random numberslast_time = time.time()for _ in range(10 ** 6):    is_element_in_set = random.randint(1, 10 ** 10) in numbersprint("Checking time:", round(time.time() - last_time, 3))# for all elementslast_time = time.time()for elem in numbers:    now_elem = elemprint("Cycle time:", round(time.time() - last_time, 3))# getting index for all elementslast_time = time.time()requests = list(numbers)random.shuffle(requests)for elem in requests:    answer = numbers.index(elem)print("Getting indexes time:", round(time.time() - last_time, 3))# getting elements by indexes 10 ** 6 timesrequests = list(numbers)random.shuffle(requests)last_time = time.time()for _ in range(10 ** 6):    answer = numbers[random.randint(0, len(numbers) - 1)]print("Getting elements time:", round(time.time() - last_time, 3))# deleting all elements one by onerandom.shuffle(requests)last_time = time.time()for elem in requests:    numbers.discard(elem)print("Deleting time:", round(time.time() - last_time, 3))

SortedSet.sorted_set.SortedSet


Пакет с многообещающим названием. Используем pip install sortedset


К сожалению, автор не приготовил нам функцию add и erase в каком-либо варианте, поэтому будем использовать объединение и вычитание множеств


Использование:


from SortedSet.sorted_set import SortedSet as ordered_setnumbers = ordered_set()numbers |= ordered_set([random.randint(1, 10 ** 10)])  # добавлениеnumbers -= ordered_set([elem])  # удаление

Протестируем пока на множествах размера 10'000:


Задача Время работы
Добавление 16.413
Проверка на наличие 0.018
Цикл по всем элементам 0.001
Получение индексов 0.008
Получение значений по индексам 0.015
Удаление 30.548

Как так получилось? Давайте загляем в исходный код:


def __init__(self, items=None):    self._items = sorted(set(items)) if items is not None else []def __contains__(self, item):    index = bisect_left(self._items, item)

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


Вывод: почти бесполезно, несколько строчек кода завернули в класс


sortedcontainers.SortedSet


Внеший пакет, для установки можно использовать pip install sortedcontainers. Посмотрим же, что он нам покажет


Задача Время работы
Добавление 3.924
Проверка на наличие 1.198
Цикл по всем элементам 0.162
Получение индексов 3.959
Получение значений по индексам 4.909
Удаление 2.933

Но, не смотря на это, кажется мы нашли то, что искали! Все операции выполняются за приличное время. По сравнению с ordered_set некоторые операции выполняются дольше, но за то операция discard выполняется не за o(n), что очень важно для возможности использования этой структуры.


Также пакет нам предлагает SortedList и SortedDict, что тоже может быть полезно.


И как же оно работает?


На странице пакета мы можем прочитать, что реализована структура не так, как мы предполагали в начале статьи.


Из-за особенностей реализации языка Python, в нём быстро работают list, а также bisect.insort (найти бинарным поиском за o(log n) место, куда нужно вставить элемент, а потом вставить его туда за o(n)). Insert работает достаточно быстро на современных процессорах. Но всё-таки в какой-то момент такой оптимизации не хватает, поэтому структуры реализованы как список списков. Создание или удаление списков происходит достаточно редко, а внутри одного списка можно выполнять операции даже за быструю линию.


Если говорить кратко, то принцип действия похож на корневую оптимизацию.


Проблема с ordered_set


Что вообще такое упорядоченное множество? Это множество, в котором мы можем сравнить любые 2 элемента и найти среди них больший / меньший. В течение всей статьи под операцией сравнения воспринималась операция сравнения двух элеметнов по своему значению. Но все пакеты называющиеся ordered_set считают что один элемент больше другого, если он был добавлен раньше в множество. Так что с формулировкой ordered_set нужно быть аккуратнее и уточнять, имеется ввиду ordered set или sorted set.


Bintrees



Так есть же модуль bintrees! Это же то, что нам нужно? И да, и нет. Его разработка была приостановлена в 2020 году со словами Use sortedcontainers instead.


Пакет предлагает нам несколько структур. К сожалению, ни одна из них не поддерживает операции find_by_order и подобные, так что эти струкруты являются аналогами std::set. Посмотрим же, на что они способны:


pip install bintrees


Название AVLTree говорит само за себя, RBTree красно-чёрное дерево, BinaryTree несбалансированное двоичное дерево, префикс Fast означает реализацию на Cython (соответственно, необходимо наличие Visual C++, если используется на Windows).


Задача AVLTree FastAVLTree RBTree FastRBTree BinaryTree FastBinaryTree
Добавление 21.946 2.285 20.486 2.373 11.054 2.266
Проверка на наличие 5.86 2.821 6.172 2.802 6.775 3.018
Цикл по всем элементам 0.935 0.297 0.972 0.302 0.985 0.295
Удаление 12.835 1.509 25.803 1.895 7.903 1.588

Результаты тестирования отчётливо показывают нам, почему использовать деревья поиска на Python плохая идея в плане производительности. А вот в интеграции с Cython всё становится намного лучше.


Оказывается, эта структура и SortedSet очень похожи по производительности. Все 3 Fast версии структур bintrees достаточно близки, поэтому будем считать, что оттуда мы используем FastAVLTree.


Задача SortedSet FastAVLTree
Добавление 3.924 2.285
Проверка на наличие 1.198 2.821
Цикл по всем элементам 0.162 0.297
Получение индексов 3.959 n/a
Получение значений по индексам 4.909 n/a
Удаление 2.933 1.509

Как мы видим, AVL в полтора раза быстрее в скорости добавления элементов и почти в 2 раза быстрее в операциях удаления. Но он в те же 2 раза медленнее в проверке на наличие и цикле по всем элементам. К тому же не стоит забывать, что 2 операции он выполнять не умеет, то есть не является тем ordered_set, что мы ищем.


Использование:


import bintreesnumbers = bintrees.FastAVLTree()numbers.insert(value, None)  # второй параметр - значение, как в словаре

Что же выбрать


Мои рекомендации звучат так: если вам нужны операции find_by_order и order_of_key, то ваш единственный вариант sortedcontainers.SortedSet. Если вам нужен только аналог std::map, то выбирайте на своё усмотрение между SortedSet и любым из fast контейнеров из bintrees, опираясь на то, каких операций ожидается больше.


Можно ли сделать что-то быстрее


Скорее нет, чем да. Использование Cython один из самых мощных способов оптимизации, а AVL считается очень быстрым решением исходной задачи. Про остальные операции ordered_set можно сказать, что модификация красно-чёрного дерева так, чтобы оно поддерживало эти операции, вряд ли будет быстрее SortedContainers, так что смысла изобретать велосипед я не вижу.




Облачные VPS серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru