Pull to refresh

Comments 22

P.S.: Коллекцию можно ещё больше ускорить за счёт использования красно-чёрного дерева вместо бинарного.


Красно-черное дерево — это реализация сбалансированного бинарного дерева поиска.

. При выборе TreeSet или TreeMap мы будем иметь O(log(n)) для вставки и поиска, но для поиска и удаления минимального будем иметь всего лишь O(1).


В стандартнах классах java.util.TreeMap#getFirstEntry работает за O(log n)

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

Я думал насчет использования min-heap вместо бинарного дерева, но мне показалось, что использование обычного дерева позволит использовать для сравнения любые наборы данных. К тому же, что и min-heap и бинарное дерево имеют одинаковую асимптотику. Использование стандартной коллекции же не даст высокой производительности так как они реализованы для наиболее общих случаев и не используют дополнительные знания о структуре элементов. Что касаемо TreeMap, getFirstEntry наверное и имеет асимптотику O(log n), а вот getFirstKey должен иметь асимптотику как и у TreeSet, т.е. O(1), хотя я могу и ошибаться(чего очень хотелось бы ведь тогда я имею выигрыш в производительности и по этому методу).

1. min-heap тоже может работать с любыми данными, проблема стандартных реализаций min-heap заключается в том, что обычно они не поддерживают эффективное удаление произвольного элемента из кучи, даже если известна позиция элемента в куче;
2. Чтобы асиптотика операций над вашим деревом была логарифмической, высота дерева должна быть логарифмический, что из вашего кода не очевидно, в то время как TreeMap/TreeSet гарантируют логарифмическую сложность.

Кроме того

3. Если вы используете хеш, то не должны ли вы предусмотреть возможность коллизий? Или просто использовать HashSet, который уже умеет это делать.
4. Ваша функция connectNodes выглядит как-то странно, допустим вы хотите объединить два узла, у которых есть дети (в зависимости от реализации getElemeInArray, которую вы нигде не упоминаете, это может произойти при удалении элемента по значению), тогда посмотрим на этот код в самом начале функции:
if (compare(node, parent) < 0) {
    node.left = parent;
    parent.parent = node;
    parent = node;
    return parent;
}

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

Перед тем как заниматься сравнением производительности, не могли бы вы показать более или менее формально, что:
1. ваша структура данных вообще кооректно работает
2. что ваша асимптотика такая, как вы заявляете?
По поводу 1го пункта именно неэффективное удаление произвольного элемента меня и не устраивает. С пунктом 2 соглашусь. По поводу 3-го я указал, что нужно описать хэш-функцию без коллизий для заданного размера коллекции. Напомню, что в начале статьи я указал для чего писал коллекцию, думаю для матрицы m на n не трудно написать хэш-функцию без коллизий, для элемента хранящего целочисленные координаты точки. За 4-й пункт огромное спасибо, исправлюсь.

Для матрицы m на n такая функция без коллизий выглядит как i*n + j и хеш-функцией не является.

Просто функцией, вычисляющей индекс.

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


"Я написал хеш-таблицу… без коллизий" звучит так же как и "я сам построил дом… для кошки".

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

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

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


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

Да, дельное замечание, просто структуру писал как раз под этот самый конкретный случай и мне понадобится и нормальный хэш от элемента.
В будущем возможно доведу до ума, а так пока хочу протестировать под свою задачу.
Я бы на вашем месте сначала посмотрел в исходники. В самом деле, может показаться, что firstKey() работает за единицу, но в исходниках он выглядит ровно так:
public K firstKey() {
    return key(getFirstEntry());
}

Здесь вызов key(entry) действительно работает за единицу, а вот getFirstEntry() работает за logN:
final Entry<K,V> getFirstEntry() {
    Entry<K,V> p = root;
    if (p != null)
        while (p.left != null)
            p = p.left;
    return p;
}
Спасибо за замечание, посмотрел исходники, действительно асимптотика O(log(n)), исправлю.

Куча, она же пирамида, имеет при той же асимптотике намного меньшую скрытую константу нежели сбалансированное бинарное дерево.


Просто за счет простоты алгоритма и локальности обращений к памяти.


PS а чтобы getFirstEntry работало за константное время, дерево должно быть прошитым

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

"Хэш-функция без коллизий" это примерно то же, что и первичный ключ сущности. Строил так дерево категорий по записям из базы. Сначала делаем массив объектов, индексированный по category.id, затем обращаемся по индексу category.parent_id и добавляем текущий объект в свойство children.

Спасибо за замечания. Исправил ошибки с методом, connectNodes, после исправления ошибок коллекция стала работать немного быстрее, но результаты в таблице решил не трогать, проверил асимптотику и добавил её оценку. Над тестированием коллекции ещё работаю.
Кажется, что ваш анализ неправильный. Представим, что мы добавляем в ваше дерево элементы в таком порядке:
— 1, 10, 2 (тут мы получаем дерево высоты 2, в корне 1 в левом поддереве 2, в правом 10)
— 9 (9 станет левым ребенком 2, потому что 9 больше 1 и 2 в левом поддереве, но меньше 10, значит по коду идем в левое поддерево, где вставка уже делается очевидным образом)
— 3 (3 станет левым ребенком 2 как раньше было с 9, а 9 станет правым ребенком 2)
— 8 (8 станет левым ребенком 3)
— 4 (4 станет левым ребенком 3, а 8 станет правым)
— 7 (7 станет левым ребенком 4)
— 5 (5 станет левывм ребенком 4, а 7 станет правым)

Если я правильно понял, тот такой паттерн вырождает ваше дерево практически в бамбук, и при этих вставках
поле k вообще не проверяется. Мне серьезно кажется, что гораздо проще просто использовать TreeSet/TreeMap и после каждого обновления переискивать минимальный элемент.
Вы правильно всё поняли. Действительно вырождает, тогда добавлю весовую оценку и для этого случая. Не хочу использовать TreeSet из-за того, что наличие цепочек в моём дереве улучшает асимптотику операций, а в TreeSet используются операции с гарантированными сложностями методов. Хотя вашу идею с использованием TreeSet/TreeMap и переискиванием минимального элемента стоит попробовать и сравнить с тем, что у меня получится.
Sign up to leave a comment.

Articles

Change theme settings