Comments 85
Сравнение поисков в ширину и в глубину
Выбирайте тип поиска в соответствии с размером и формой дерева.
Для широких, мелких деревьев используйте поиск в ширину.
Для глубоких, узких деревьев используйте поиск в глубину.
Почему?
Согласен с вопросом. Написана дичь. Способ обхода надо выбирать в зависимости от того, как ни странно, в каком порядке вы желаете обойти граф! Внимание, любой граф, а не только дерево.
Поэтому есть 4 нерекурсивных обхода: preorder, inorder, postorder, BFS. И с ними нужно уметь работать из головы, сходу.
Но BFS работает быстрее и лучшеТак почему быстрее то? Из-за вызова функции для всех рёбер (либо вершин)? Если да, то ок. Можно конечно сделать свой стек, но обход в ширину, думаю, проще. У меня просто не было задач, где важна такая микро-оптимизация при обходе, поэтому не могу сказать, что проще.
А то бывали случаи, когда простой перебор занимает секунду, а построение структуры — 20 минут
- обычно, перед тем, как «городить вспомогательные структуры», разработчики анализируют асимптотику и, соответственно, имплементят более сложный алгоритм только тогда, когда это понижает асимптотику (и когда при этом объемы данных такие, что это реально принесёт пользу, само собой). И для этого не надо имплементить два алгоритма и сравнивать реальный runtime
- что у вас за такая фантастическая задача, что простой перебор занимает секунду, а алгоритм с лучшей асимптотикой работает 20 минут?
К сожалению, сложность далеко не единственная вещь, описывающая алгоритм. Есть еще необходимая дополнительная память, время на подготовку. Да и та самая константа, которую все игнорируют по очевидным причинам (потому что на бесконечности она не решает) может играть роль на малых значениях непосредственно n. 100n^2 < 1000nlogn при n < 4. Во всяком случае, C# предпочитает использовать сортировку вставками при n < 16, что действительно имеет смысл, не смотря на большую алгоритмическую сложность.
разработчики анализируют асимптотику
Для теории хорошо, но на практике лучше проверить с таймером.
алгоритм с лучшей асимптотикой работает 20 минут
Был просто случай. Посоветовал мне один профессор библиотеку, которая строит пространственный индекс. Хотел валить на защите за то, что я не использую индексы. Библиотека хорошая, но у меня тупым перебором матрицу инцедентности строило за несколько секунд простым перебором для миллиона объектов. А хитрый индекс оказался слишком долгим. Я до этого не подозревал, что тупые методы тащат за счет меньшего количества накладных расходов на весьма большом количестве объектов.
Сударь, вы же осознаёте, что асимптотику двух алгоритмов можно прикинуть и сравнить до имплементации, а «с таймером», как вы выражается — это тоже однозначно важная проверка (и называется это performance testing чаще всего), но если вы хотите так сравнить два алгоритма, вам надо будет оба закодить?
Второй важный момент из моего комментария, который я повторю: само собой, только плохой разработчик ориентируется на одну лишь асимптотику. И, внезапно, именно для проверки подобного на собеседованиях задают вопросы в духе «когда merge sort/bubble sort/counting sort использовать выгоднее, чем quick sort?».
но если вы хотите так сравнить два алгоритма, вам надо будет оба закодить?
Закодить один, но проверить, — на каком количестве он дает выигрыш. А то может и его не надо было кодить, так как количество объектов в системе такое, что вполне обрабатывается тупым методом. Вы например, можете назвать число элементов, на котором quck sort дает выигрыш? Я не думаю, что есть универсальное число. На каждой реализации надо измерять.
С другой стороны большинство разработчиков никогда в жизни не будут писать алгоритм сортировки вручную — для нас это уже сделали разработчики библиотек языка, поэтому в C# часто достаточно написать что-то в виде list.Sort().
То же самое могу сказать и про массивы/связанные списки. Я думаю, что никому не придётся реализовывать двусвязанные списки вручную… Хотя преимущества хранения в Хэш таблице по сравнению с массивом или двусвязанным списком спросить не вредно, т.к. знание этого позволяет программистам выбрать правильную структуру данных для хранения.
Первое правило технического интервью должно быть таким: не спрашивайте то, что вы не применяете в своей работе и не собираетесь применять в ближайшем будущем.
Вот считает он, что нужна операция сравнения на хэш-таблиц. А на результате-то это как скажется?
Особенно для тех, кто пишет прикладные вещи типа приложений, фронта и тп.
У меня просто всегда запекалось с таких вопросов на собеседовании. А потом в таких компаниях появляются продукты, где интерфейс весь сикось-накось, соединение теряется постоянно и батарейка утекает как не в себя. Зато вся команда может написать рекурсивный обход бинарного дерева за обедом одной рукой.
Вот и думай после этого, приносят ли подобные вопросы хоть какую-нибудь пользу.
Он переопределит операцию сравнения вместо хеша, и у него неправильно будет работать код. Как это повлияет на работу программы?
Да как угодно:) В зависимости от того, как и зачем используется таблица:)
.Any() и .Count()>0
Это классический залет, встречающийся в продуктовом коде.
Но встречаются (реально встречаются!) образчики и посильнее:
List<SomeType> list = GetSomeTypeList();
foreach (var item in list)
{
int index = list.IndexOf(item);
// Do something with item and index
}
for (int i ...), O(N) vs. O(N*N)? — не, не слышали.
К примеру, для конечных коллекций, которые хранят свой размер, вроде массива, Count() (который внутри Length) будет быстрее, чем Any(), т.к. не создаётся/уничтожается итератор.
Для условно бесконечных коллекций (вроде IEnumerable) Any() конечно будет быстрее.
Поправка: IEnumerable — не коллекция, а интерфейс. Правильнее говорить "для IEnumerable который не является ICollection"
Для тех, кто хранит свой размер, лучше использовать свойство Count
вместо Linq-метода Count()
. Это поможет избежать как минимум двух проверок: на null
и на принадлежность к ICollection<T>
.
!Any() -> на IsEmpty()
Any(Func<TSource, bool> match) -> на Contains(Func<TSource, bool> match) или ContainsAny(Func<T, bool> match)
— и по смыслу этот метод гораздо ближе к уже имеющемуся
Contains(TSource item)
Count() -> на GetCount() или CalculateCount()
Когда разработчики работают не с IEnumerable, а с явными списком, коллекцией, или даже с массивом, то часто в в коде путают/пишут, не особо задумываясь, — Count, Count(), Length вперемешку.
Почему я должен знать DFS? А алгоритм А* — не нужно? Почему? Я либо буду использовать в работе все графовые алгоритмы, либо ни один из них.
Нужно знать жадные алгоритмы. А линейное программироваие не нужно? И так далее. Нужно выбирать алгоритмы по какому-то принципу, а не то, что все проходят в институте.
И поэтому линейное программирование обычно программисту действительно знать не нужно, а вот жадные алгоритмы и динамику — было бы неплохо.
Например, в рамках выпуска подкаста devzen.ru/episode-0180 Алексей twitter.com/paaleksey рассказывал, как linear programming solver можно использовать для написания build-тула.
С другой стороны, нам нужна и топологическая сортировка для build-тула. Хм, получается что обе эти темы могут появится и не появится в рамках практической жизни.
Плюс хотелось бы услышать, где их проходят в курсе алгоритмов и структур данных, потому что везде, где я вообще слышал про изучение линейного программирования, оно даётся либо в рамках отдельного курса, либо в рамках математических дисциплин.
Мой основной поинт — сравнение крайне некорректное, потому что линейное программирование дефолтный бекендщик никогда в своей карьере не встретит, а вот жадные алгоритмы, например, раз в пару лет могут и попасться.
Я поясню свой инитный коммнетарий. Я не говорил, что это сравнимые темы. Я говорю, что это темы курса Алгоритмы и Структуры Данных, и в среднем можно взять от туда любые алгоритмы и сказать, что это темы для собеседования.
Мне же больше нравится позиция, что на собеседовании спрашивают то, что будет требоваться на данной вакансии.
Не надо это никаким архитекторам
Например, вы пишите систему для Убер. Она определяет k ближайших к вам заказов. Нужно чтобы было ок на 100 000+. Так, для прикола. Типа ищем по всему миру. Такой тест-кейс. N*N не прокатит. Итого вы делаете квик селект по k, а потом ищите за N тех, кто ближе чем k. И получаете линейную сложность.
Подавляющее большинство всех остальных разработчиков не изобретает велосипеды, а использует готовые реализации алгоритмов и программирует рутину.
Но, например, у нас в отделе(mail.ru) рядовому разработчику(не алгоритмистам и не звездам) приходится и классические алгоритмы, и ml время от времени использовать. И даже если есть готовая реализация чего-то, нужно подумать что именно готовое использовать.
Когда собеседовался как раз давали 5 алгоритмических задачек, и удивительно, но задачи релевантны тому, что приходится использовать в работе.
В ML никакого rocket science нет. Немного теории в области статистики + навыки использования ML фреймворков — и всё, специалист по данным готов. Вообще, в ML в bigdata высока роль случая и простого везения — чем больше специалистов занимаются одним и тем же, чем более вероятно, что хотя бы один из них случайно получит хороший результат.
Аналогично с алгоритмами: на практике экзотика встречается довольно редко, подавляющее большинство алгоритмов — это хэш-таблицы, сортировка, возможно двоичные деревья поиска. Если вы — пользователь алгоритмов, а не их разработчик, то достаточно просто умений применять библиотечные алгоритмы в нужных местах.
Человек хотел знать, кому из многочисленных разработчиков Убер нужна О-нотация. Рядовому бэкенд разработчку, который пишет под большие данные, нужно знать что такое О-нотация. Без этого знания, ни писать алгоритмы, ни использовать не получится.
Возможно в убере есть отдельные люди которые решают ВСЕ задачи связанные с алгоритмами(я сомневаюсь), но у нас на проекте и пишутся и используются алгоритмы рядовыми разработчиками, исследователей на всё не хватит, без О-нотации далеко не уйдешь.
Если кто выше не понимает смысл таких вопросов, то вот объяснение.
Если вы в жизни не писали с нуля системы на миллионы терабайт данных, то и не поймете зачем это нужно. Даже если взять Кормена, то это ничто. Кормен очень поверхностная книга. В Кормене даже SuffixTree/SuffixArray нету. Тоесть такой человек не напишет эффективный поиск. Инвертированного индекса тоже нет. Поиска k ближайших точек за O(N) тоже не напишет. Как говорил мой знакомый из гугла:«Если хотите понять сущность алгоритмов, то увеличите число данных до миллиарда на 1 машине. И придумайте эффективный способ работы». У вас сразу полезет наружу всё. И коллизии в хешах. И поиск в ширину-глубину. И вообще всё перестанет работать. А с этим нужно еще как-то жить и дружить.
Загуглите
Distributed Computing Principles, Algorithms, and Systems
Ajay D. Kshemkalyani and Mukesh Singhal
или
Principles of Distributed Computing
Roger Wattenhofer
Ну как читается? Легко?
Кстати, DarkTiger, когда думаете догонять Амазон какой-нибудь?
— Никак не могу найти себе помощника — пожаловался однажды Эдисон Эйнштейну, — Каждый день заходят молодые люди, но ни один не подходит.
— А как вы определяете их пригодность? — поинтересовался Эйнштейн.
Эдисон показал ему листок с вопросами.
— Кто на них ответит, тот и станет моим помощником.
«Сколько миль от Нью-Йорка до Чикаго?» — прочел Эйнштейн, — «Нужно заглянуть в железнодорожный справочник».
«Из чего делают нержавеющую сталь?» — «Об этом можно узнать в справочнике по металловедению».
Пробежав глазами остальные вопросы, Эйнштейн сказал:
— Не дожидаясь отказа, свою кандидатуру снимаю сам.
Никто не знает как нанимать программистов. Даже гугл признал нулевую корреляцию качества сотрудников со сложностью собеседования.
Если вы в жизни не писали с нуля системы на миллионы терабайт данных
… то скорее всего не скоро начнете их писать. Сколько этих систем нужно в мире? Не очень много. Типичная задача в типичной компании сегодня — та же самая, что и сорок лет назад: вот у нас есть госпиталь, нам нужно создать систему записи пациентов на прием к докторам. Только сорок лет назад эту систему писали для мейнфрейма на коболе, а сегодня — для облака на вебсервисах с ангуляром.
И мало того что в конкретном частном случае я не верю в объемы данных, которые бы требовали знаний устройства системного кеша (тем более что вне контекста я просто не помню). Дело в том что как правило во всех местах где меня просят оптимизировать какой то лютый треш в архитектуре бд, жуткие запросы, страшный оверхед орм и прочее. Я просто не могу себе представить где посреди всего этого дело вдруг может понадобицо знать «сортировку пузырьком».
Уж простите меня коллеги. Я правда верю, что то есть проекты с нормальным подходом над которыми работают профессионалы. Но видимо, чтобы это увидеть, нужно устроиться в гугл как минимум.
;-З
На какую должность с такими вопросами собеседуете?
Дерево — это такая структура данных, в которой каждый узел имеет как минимум два дочерних элемента.
Вы это серьёзно? Каждый узел, минимум два дочерних элемента? Оно бесконечное что-ли? А что тогда будет листом в таком дереве? Если речь идёт о дереве в общем, то там могут быть узлы и с одним дочерним элементом (вы даже дальше приводите пример вырожденного дерева). Если же о двоичном дереве (учитывая заголовок параграфа, из которого взята цитата), то там будет максимум два дочерних элемента.
Однозначно, очень вредная шпаргалка. Дальше читать не стал, простите.
Больше похоже на шпаргалку на втором курсе по теме Алгоритмы и структуры данных
Вопросы про алгоритмы, а на картинке пых и сырой SQL. Собственно, что ты и будешь делать следующие пять лет, ответив про сортировки и деревья.
Основная разновидность — линейные массивы, или одноразмерные.
Их размер статичен, то есть при объявлении линейного массива задаётся фиксированный размер.
Неясно, почему линейные массивы противопоставляются динамическим. Подразумевались, очевидно, статические массивы.
Вставка: линейный массив — недопустимо, динамический массив — O(n).
Действительно, вставка в динамический массив происходит за O(n), но эта информация не очень полезна без того факта, что в силу мультипликативной стратегии реаллокации амортизиованная сложность вставки получается O(1), то есть последовательная вставка n элементов произойдёт не за O(n^2), как можно было бы подумать, а за O(n).
Данные хранятся в узлах, указывающих на другие узлы.
Узел содержит один элемент данных и одну ссылку (на другой узел).
Связный список соединяет узлы друг с другом с помощью ссылок от одного узла к другому.
Под определение, которое дано в статье подходит всякая ересь, например:
Оптимизированный поиск: связный список — O(n).
Неясно, что такое оптимизированный поиск в связном списке.
(оффтоп: чутка развив идею списка можно делать на нём аналог двоичного поиска, гуглить skip list).
Этот процесс называется хэшированием: однозначным сопоставлением друг другу входных и выходных данных.
Вот это жесть, конечно. Конечно, хэширование не занимается однозначным сопоставлением, потому что значений хэшей должно быть меньше, чем значений хэшируемых объектов, чтобы это имело хоть какой-то смысл.
Хэш-коллизиями называются ситуации, когда для двух разных входных данных функция возвращает одинаковые выходные данные.
Эта проблема свойственна всем хэш-функциям.
Часто она решается с помощью увеличения хэш-таблиц до огромного размера.
Решается она не так, конечно, а с помощью использования стратегий разрешения коллизий. Гуглить цепочки, линейное разрешение, двойное хэширование, хэширование кукушки.
Двоичное дерево — это такая структура данных, в которой каждый узел имеет максимум два дочерних элемента.
Дочерние элементы бывают левым и правым.
При этом у каждой вершины не больше одного левого сына и не больше одного правого.
Индексирование: двоичное дерево поиска — O(log n).
Поиск: двоичное дерево поиска — O(log n).
Вставка: двоичное дерево поиска — O(log n).
Все эти операции работают за O(высота дерева). Если дерево сбалансированное, то тогда это действительно O(log N), но само по себе дерево сбалансированным навряд ли станет.
Существуют хитрые двоичные деревья, которые всегда остаются сбалансированными. Гуглить, например, красно-чёрное дерево, AVL-дерево, splay-дерево.
Поиск в ширину оптимален для поиска по дереву, чья ширина превышает глубину.
Это почему вдруг? С точки зрения времени алгоритм всегда обработает каждую вершину 1 раз, а каждое ребро – 2 или 1 раз (в зависимости от того, ориентированный граф или нет). Если же хочется пооптимизировать память, то поиск в ширину наоборот тратит меньше памяти на очередь на «глубоких узких» графах (потому что слои получаются маленькие).
В связи с использованием очереди такой метод поиска потребляет больше памяти, чем поиск в глубину.
Ну неясно. Поиск в глубину же стек поддерживает пока ходит (неявно, через рекурсию). Те же самые O(V) памяти.
E — количество рёбер (граней?).
Рёбер. Грань – область, ограниченная рёбрами в плоском графе и не содержащая внутри себя вершин и рёбер графа.
[Поиск в глубину] Алгоритм оптимален для поиска по дереву, чья глубина превышает ширину.
Аналогично, с точки зрения времени всё то же самое, а с точки зрения памяти всё ровно наоборот.
Сравнение поисков в ширину и в глубину
Выбирайте тип поиска в соответствии с размером и формой дерева.
Для широких, мелких деревьев используйте поиск в ширину.
Для глубоких, узких деревьев используйте поиск в глубину.
Если уж заморачиваться, то ровно наоборот.
Вообще, поиск в ширину и поиск в глубину – это базовые алгоритмы, из которых потом растут свои ответвления, например:
- с помощью поиска в ширину можно искать кратчайшие пути в невзвешенном графе
- с помощью поиска в глубину можно искать мосты и точки сочленения.
Сравнение данных с помощью алгоритма сортировки:
Весь набор данных делится минимум на две группы.
Пары значений сравниваются между собой, наименьшее перемещается влево.
После сортировки внутри всех пар, сравниваются левые значения двух левых пар. Таким образом, создаётся группа из четырёх значений: два наименьшие — слева, наибольшие — справа.
Процесс повторяется до тех пор, пока не останется только один набор.
Каша, мёд, говно и пчёлы. Сначала, я так понял, автор начал описывать рекурсивную реализацию, а потом описал итеративную.
«Весь набор данных делится минимум на две группы» – первый шаг рекурсивного алгоритма. Затем каждая часть рекурсивно сортируется и сливается.
Наилучший вариант сортировки: сортировка слиянием — O(n).
Средний вариант сортировки: сортировка слиянием — O(n log n).
Худший вариант сортировки: сортировка слиянием — O(n log n).
Сортировка слиянием всегда работает ровно за O(n log n).
Жадные алгоритмы используются для поиска оптимального решения данной проблемы.
Все алгоритмы в идеале используются для поиска оптимального решения конкретной проблемы. Специфика жадных алгоритмов в том, что их наоборот часто используют для поиска хоть каких-то решений в задачах, в которых искать оптимальное решение не представляется возможным.
Псевдокод жадного алгоритма для поиска самой большой разницы между двумя числами в массиве
Псевдокод жадного алгоритма для поиска самой большой разницы между двумя соседними числами в массиве.
:(
Из такого банального процесса как собеседование в последние лет 5 сделали какой-то культ — скоро будут в институтах преподавать. И мне чувствовать себя на собеседе как на экзамене в универе — тоже пришлось. Но иной раз, слушая вопросы оппонента, складывается впечатление, что пришёл в контору пишущую софт для управления реакторами а не банальные формочки на php.
За 20 лет работы, программировать приходилось в разном режиме (и как в виде основной деятельности и как вспомогательной) но вот ни разу мне не понадобилось знание работы алгоритмов сортировки. Видать я не правильный «сварщик».
Собеседовать кандидатов тоже приходилось. И не понимаю к чему весь этот ажиотаж? Извините за сумбур.
Лично я нашел неточности, начиная с «Массив» и вплоть до сортировок. В каждом мелком подразделе. Прочитав комментарии, понял что там где я не нашел неточности — это скорее от моего незнания. Есть повод повторить сортировки и поиск в ширину/глубину — там сходу ошибок не нашел, есть куда расти. Так что спасибо за статью, получился неплохой тест :)
Но главное — не переусердствовать. Бинарные деревья на практике, скорее всего, никогда не встретятся. Сортировка пузырьком — тоже. Про всякую экзотику в виде RB, AVL, декартовых деревьев я вообще молчу.
А вот умение написать эффективную хэш-функцию, функцию сравнения — это важные умения.
Массив
Определение:
Хранит элементы данных на основе последовательного индекса, чаще всего с нулевой базой.
Что значит "хранит на основе индекса"? Лучше возьмите определение из википедии.
В его основе лежат кортежи из теории множеств.
Откуда вы это взяли?? Массив и кортеж — совершенно разные понятия в контексте программирования.
Извините, но что за бред-то?
Рекурсивные алгоритмы
Определение:
Как следует из определения, этот алгоритм вызывает самого себя.
:)
Не давно проходил собеседование в Яндекс на разработчика интерфейсов. Собеседованием остался доволен, на первом этапе спрашивали солянку из html, css и особенностей javascript, ничего сверхъестественного и заковыристого. На следующем этапе было 4 интервью, которые были в виде практических задач. Совсем чуть-чуть теории спрашивали, а потом решали задачи. Считаю это самым правильным вариантом. Кандидат прекрасно может знать структуры данных, крутые алгоритмы и прочие теоретические вещи, но какой в этом толк, если он не может применить их на практике. Конкретные задачи как раз помогают выяснить, что умеет и знает кандидат. Как он подходит к решению проблемы, какие алгоритмы и структуры данных использует. Ведь в итоге именно за этим и ищут человека, что бы он решал конкретно поставленные задачи, а не хвастался, что знает все алгоритмы обхода графа
Этот алгоритм предпочитают использовать в архитектурах вычислительных систем.
Что черт возьми должна означать эта фраза??
Если при работе рекурсивного алгоритма вы столкнулись с чем-то из перечисленного, значит, вы всё испортили.Я аж чаем поперхнулся. Спасибо за капельку позитива с утра пораньше! :)
Шпаргалка для технического собеседования