Почему в куче, реализованном массивом, индекс 0 остается неиспользованным?
Я изучаю структуры данных, и каждый источник говорит мне не использовать индекс 0 массива при реализации кучи без объяснения причин. Я искал в Интернете, искал StackExchange и не нашел ответа.
Ответы
Ответ 1
Нет причин, по которым куча, реализованная в массиве, должна оставлять элемент с индексом 0 неиспользованным. Если вы установите корень в 0, то у элемента в array[index]
дочерние элементы в array[index]
array[index*2+1]
и array[index*2+2]
. Узел в array[child]
имеет своего родителя в array[(child-1)/2]
.
Давай посмотрим.
root at 0 root at 1
Left child index*2 + 1 index*2
Right child index*2 + 2 index*2 + 1
Parent (index-1)/2 index/2
Таким образом, наличие корня в 0, а не в 1 стоит вам дополнительного сложения, чтобы найти левого потомка, и дополнительного вычитания, чтобы найти родителя.
Для более общего случая, когда это может быть не двоичная куча, а 3-кучная, 4-кучная и т.д., Где для каждого узла вместо 2 используются дочерние элементы NUM_CHILDREN, используются следующие формулы:
root at 0 root at 1
Left child index*NUM_CHILDREN + 1 index*NUM_CHILDREN
Right child index* NUM_CHILDREN + 2 index*NUM_CHILDREN + 1
Parent (index-1)/NUM_CHILDREN index/NUM_CHILDREN
Я не вижу этих нескольких дополнительных инструкций, которые бы сильно повлияли на время выполнения.
По причинам, по которым я считаю неправильным начинать с 1 на языке с массивами на основе 0, см. fooobar.com/questions/15497843/... и мой пост в блоге. Но это так, как мы всегда это делали!
Ответ 2
Как я нашел в книге CLRS, есть некоторое значение с точки зрения производительности, поскольку, как правило, операторы сдвига работают очень быстро.
На большинстве компьютеров процедура LEFT может вычислять 2*i
в одной инструкции посредством просто смещение двоичного представления i, оставленного одной битовой позицией. Аналогичным образом, Правильная процедура может быстро вычислить 2*i+1
, сдвинув двоичное представлениеиз я оставлен одной битовой позицией, а затем добавляется в 1 как младший бит. Процедура PARENT может вычислять i/2
с помощью смещения я правой позиции в битах.
Итак, начало кучи в индексе 1, вероятно, сделает более быстрое вычисление родительских, левых и правых дочерних индексов.
Ответ 3
Как заметил AnonJ, речь идет скорее о вкусе, чем о технической необходимости. Одна хорошая вещь, начиная с 1, а не 0, состоит в том, что существует биекция между двоичными строками x и положительными целыми числами, которая отображает двоичную строку x в положительное целое число, записанное 1x в двоичном формате. Строка x указывает путь от корня к индексированному node, где 0 означает "взять левого ребенка", а 1 означает "взять правильный ребенок".
Другое соображение состоит в том, что в противном случае неиспользуемое "нулевое" местоположение может содержать дозорный элемент со значением минус бесконечность, который на архитектурах без предсказания ветвления может означать незначительное улучшение времени выполнения из-за того, что у него есть только один тест в просеивании вверх а не два.
Ответ 4
(Пока я искал, я придумал свой ответ, но я не знаю, правильно это или нет.)
Если для корня node используется индекс 0
, последующие вычисления его дочерних элементов не могут выполняться, потому что мы имеем indexOfLeftChild = indexOfParent * 2
и indexOfRightChild = indexOfParent * 2 + 1
. Однако 0 * 2 = 0
и 0 * 2 + 1 = 1
, которые не могут представлять отношения родитель-дети, которые мы хотим. Поэтому мы должны начинать с 1
, чтобы дерево, представленное массивом, соответствовало математическим свойствам, которые мы желаем.