Как можно построить кучу O (n) сложности времени?

Может кто-нибудь объяснить, как построить кучу O (n) сложности?

Вставка элемента в кучу O(log n), а вставка повторяется n/2 раза (остальные - листья и не могут нарушать свойство кучи). Таким образом, это означает, что сложность должна быть O(n log n), я бы подумал.

Другими словами, для каждого элемента, который мы "heapify", у него есть возможность отфильтровать один раз для каждого уровня для кучи до сих пор (это уровни log n).

Что мне не хватает?

Ответ 1

Я думаю, что в этой теме похоронено несколько вопросов:

  • Как вы реализуете buildHeap чтобы он buildHeap за O (n) раз?
  • Как вы показываете, что buildHeap работает в O (N) времени, когда реализовано правильно?
  • Почему та же самая логика не работает для того, чтобы сортировка кучи выполнялась за O (n), а не за O (n log n)?

Как вы реализуете buildHeap чтобы он buildHeap за O (n) раз?

Часто ответы на эти вопросы сосредоточены на разнице между siftUp и siftDown. Правильный выбор между siftUp и siftDown имеет решающее значение для получения производительности O (n) для buildHeap, но не помогает понять разницу между buildHeap и heapSort в целом. Действительно, правильные реализации и buildHeap и heapSort будут использовать только siftDown. Операция siftUp необходима только для вставки в существующую кучу, поэтому она будет использоваться для реализации очереди с приоритетами, например, с использованием двоичной кучи.

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

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

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

Количество операций, необходимых для siftDown и siftUp, пропорционально расстоянию, которое может пройти узел. Для siftDown это расстояние до нижней части дерева, поэтому siftDown дорог для узлов в верхней части дерева. С siftUp работа пропорциональна расстоянию до вершины дерева, поэтому siftUp стоит дорого для узлов в нижней части дерева. Хотя в худшем случае обе операции равны O (log n), в куче только один узел находится вверху, тогда как половина узлов лежит на нижнем уровне. Поэтому не должно быть слишком удивительно, что если нам нужно применить операцию к каждому узлу, мы бы предпочли siftDown siftUp.

Функция buildHeap принимает массив несортированных элементов и перемещает их до тех пор, пока все они не удовлетворят свойству кучи, создавая тем самым допустимую кучу. Существует два подхода, которые можно использовать для buildHeap используя siftUp siftDown операции siftUp и siftDown.

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

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

Какая реализация для buildHeap более эффективна?

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

Пусть h = log n представляет высоту кучи. Работа, требуемая для подхода siftDown определяется суммой

(0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).

Каждый член в сумме имеет максимальное расстояние, которое должен пройти узел на данной высоте (ноль для нижнего слоя, h для корня), умноженное на количество узлов на этой высоте. Напротив, сумма для вызова siftUp на каждом узле

(h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).

Должно быть понятно, что вторая сумма больше. Один первый член равен hn/2 = 1/2 n log n, поэтому такой подход в лучшем случае имеет сложность O (n log n).

Как мы можем доказать, что сумма для подхода siftDown действительно равна O (n)?

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

Taylor series for buildHeap complexity

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

  • Все члены являются положительными, поэтому конечная сумма должна быть меньше бесконечной суммы.
  • Ряд равен степенному ряду, оцененному в x = 1/2.
  • Этот степенной ряд равен (постоянному времени) производной ряда Тейлора при f (x) = 1/(1-x).
  • х = 1/2 находится в интервале сходимости этого ряда Тейлора.
  • Поэтому мы можем заменить ряд Тейлора на 1/(1-x), дифференцировать и оценить, чтобы найти значение бесконечного ряда.

Поскольку бесконечная сумма равна ровно n, мы заключаем, что конечная сумма не больше, и, следовательно, O (n).

Почему сортировка кучи требует времени O (n log n)?

Если можно запустить buildHeap за линейное время, почему для сортировки кучи требуется время O (n log n)? Ну, куча сортировки состоит из двух этапов. Сначала мы вызываем buildHeap для массива, который требует оптимального времени O (n). Следующим этапом является повторное удаление самого большого элемента в куче и помещение его в конец массива. Поскольку мы удаляем элемент из кучи, сразу после окончания кучи всегда есть открытое место, где мы можем сохранить элемент. Таким образом, сортировка кучи достигает отсортированного порядка, последовательно удаляя следующий по величине элемент и помещая его в массив, начиная с последней позиции и двигаясь вперед. Это сложность этой последней части, которая доминирует в куче. Цикл выглядит так:

for (i = n - 1; i > 0; i--) {
    arr[i] = deleteMax();
}

Ясно, что цикл выполняется O (n) раз (точнее, n - 1, последний элемент уже на месте). Сложность deleteMax для кучи - O (log n). Обычно это выполняется путем удаления корня (самый большой элемент, оставшийся в куче) и замены его последним элементом в куче, который является листом и, следовательно, одним из самых маленьких элементов. Этот новый корень почти наверняка нарушит свойство кучи, поэтому вы должны вызывать siftDown пока не siftDown его обратно в приемлемое положение. Это также приводит к перемещению следующего по величине элемента до корня. Обратите внимание, что в отличие от buildHeap где для большинства узлов мы вызываем siftDown из нижней части дерева, мы теперь вызываем siftDown из верхней части дерева на каждой итерации! Хотя дерево сжимается, оно не сжимается достаточно быстро: высота дерева остается постоянной, пока вы не удалите первую половину узлов (когда вы полностью очистите нижний слой). Затем в следующем квартале высота h - 1. Таким образом, общая работа для этого второго этапа

h*n/2 + (h-1)*n/4 + ... + 0 * 1.

Обратите внимание на переключение: теперь нулевой рабочий случай соответствует одному узлу, а h рабочий случай соответствует половине узлов. Эта сумма равна O (n log n), как и неэффективная версия buildHeap которая реализована с использованием siftUp. Но в этом случае у нас нет выбора, так как мы пытаемся отсортировать, и мы требуем, чтобы следующий самый большой элемент был удален следующим.

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

Ответ 2

Ваш анализ верен. Однако он не является жестким.

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

Большой анализ алгоритма можно увидеть здесь.


Основная идея заключается в том, что в алгоритме build_heap фактическая стоимость heapify не равна O(log n) для всех элементов.

Когда вызывается heapify, время выполнения зависит от того, насколько далеко элемент может двигаться вниз в дереве до завершения процесса. Другими словами, это зависит от высоты элемента в куче. В худшем случае элемент может опускаться до уровня листа.

Давайте посчитаем выполненную работу по уровню.

На самом нижнем уровне есть узлы 2^(h), но мы никоим образом не называем heapify, поэтому работа равна 0. На соседнем уровне есть узлы 2^(h − 1), и каждый из них может перемещаться вниз на 1 уровень. На 3-м уровне снизу есть узлы 2^(h − 2), и каждый из них может опуститься на 2 уровня.

Как вы видите, не все операции heapify O(log n), поэтому вы получаете O(n).

Ответ 3

Наглядно:

"Сложность должна быть O (nLog n)... для каждого элемента, который мы" heapify ", у него есть возможность отфильтровать один раз для каждого уровня для кучи до сих пор (это уровни log n)."

Не совсем. Ваша логика не создает жесткой привязки - она ​​оценивает сложность каждой из них. Если построено снизу вверх, вставка (heapify) может быть намного меньше O(log(n)). Процесс выглядит следующим образом:

(Шаг 1) Первые элементы n/2 входят в нижнюю строку кучи. h=0, поэтому heapify не требуется.

(Шаг 2) Следующие элементы n/22 идут по строке 1 вверх. h=1, heapify фильтры 1 уровня вниз.

(шаг i) Следующие n/2i элементы идут в строке i вверху снизу. h=i, heapify фильтры i вниз.

(Step log (n)) Последний элемент n/2log2(n) = 1 находится в строке log(n) вверху снизу. h=log(n), удалите фильтры log(n) вниз.

УВЕДОМЛЕНИЕ:, что после первого шага, 1/2 элементов (n/2) уже находятся в куче, и нам даже не нужно было вызывать heapify один раз. Также обратите внимание, что только один элемент, корень, фактически несет полную сложность log(n).


Теоретически:

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

На высоте i мы показали (выше), что будут элементы n/2i+1, которые должны вызывать heapify, и мы знаем heapify at height i is O(i). Это дает:

enter image description here

Решение последнего суммирования можно найти, взяв производную от обеих сторон известного уравнения геометрических рядов:

enter image description here

Наконец, включение x = 1/2 в приведенное выше уравнение дает 2. Включение этого в первое уравнение дает:

enter image description here

Таким образом, общее количество шагов имеет размер O(n)

Ответ 4

Это будет O (n log n), если вы построили кучу, многократно вставляя элементы. Однако вы можете создать новую кучу более эффективно, вставив элементы в произвольный порядок, а затем применив алгоритм для "heapify" их в правильный порядок (в зависимости от типа кучи, конечно).

См. http://en.wikipedia.org/wiki/Binary_heap, "Создание кучи" для примера. В этом случае вы по существу работаете с нижним уровнем дерева, обменивая родительский и дочерний узлы до тех пор, пока условия кучи не будут удовлетворены.

Ответ 5

Как мы знаем, высота кучи log (n), где n - общее количество элементов. Обозначает ее как h
    Когда мы выполняем операцию heapify, элементы на последнем уровне (h) не будут перемещаться даже на один шаг.   Число элементов на втором последнем уровне ( h-1) 2 h-1 и они могут перемещаться на уровне max 1 (во время heapify).   Аналогично, для уровня i th, мы имеем 2 i которые могут перемещать позиции hi.

Поэтому общее число ходов = S= 2 h * 0 + 2 ч-1 * 1 + 2 ч-2 * 2 +... 2 0 * ч

                                                S = 2 h {1/2 + 2/2 2 + 3/2 3 +... h/2 h} ------------------------------------------ ------- 1
это AGP серия, чтобы решить эту проблему с обеих сторон на 2
                                                S/2= 2 h {1/2 2 + 2/2 3 +... h/2 h + 1} ------------------------------- ------------------ 2
вычитая уравнение 2 от 1 дает                                                 S/2 = 2 h {1/2 + 1/2 2 + 1/2 3 +... + 1/2 h + h/2 h + 1}
                                                S = 2 h + 1 {1/2 + 1/2 2 + 1/2 3 +... + 1/2 h + h/2 h + 1}
теперь 1/2 + 1/2 2 + 1/2 3 +... + 1/2 h уменьшается GP, чья сумма меньше чем 1 (когда h стремится к бесконечности, сумма стремится к 1). В дальнейшем анализе возьмем верхнюю оценку суммы, которая равна 1.
Это дает S = 2 h + 1 {1 + h/2 h + 1}
                    = 2 Н + 1 <сильные > + Н
                    ~ 2 h + h
как h = log (n), 2 ч= п
Следовательно, S = n + log (n)
T (C) = O (n)

Ответ 6

Создавая кучу, давайте предположим, что вы делаете снизу вверх.

  • Вы берете каждый элемент и сравниваете его со своими дочерними элементами, чтобы проверить, соответствует ли пара правилам кучи. Таким образом, листья попадают в кучу бесплатно. Это потому, что у них нет детей.
  • Перемещение вверх, наихудший сценарий для node прямо над листьями будет 1 сравнение (по максимуму они будут сравниваться только с одним поколением детей).
  • Двигаясь дальше, их ближайшие родители могут сравниться с двумя поколениями детей.
  • Продолжая в том же направлении, вы будете иметь log (n) сравнения для корня в худшем случае. и log (n) -1 для своих непосредственных детей, log (n) -2 для их непосредственных детей и т.д.
  • Итак, суммируя все это, вы приходите к чему-то вроде log (n) + {log (n) -1} * 2 + {log (n) -2} * 4 +..... + 1 * 2 ^ {(logn) -1}, что не что иное, как O (n).

Ответ 7

Уже есть несколько отличных ответов, но я хотел бы добавить небольшое визуальное объяснение

enter image description here

Теперь взгляните на изображение, есть
n/2^1 зеленых узлов с высотой 0 (здесь 23/2 = 12)
n/2^2 красных узла с высотой 1 (здесь 23/4 = 6)
n/2^3 синий узел с высотой 2 (здесь 23/8 = 3)
n/2^4 фиолетовых узла с высотой 3 (здесь 23/16 = 2)
так что есть n/2^(h+1) узлов для высоты ч
Чтобы определить сложность времени, давайте посчитаем объем выполненной работы или максимальное количество итераций, выполненных каждым узлом.
теперь можно заметить, что каждый узел может выполнять (максимум) итерации == высота узла

Green = n/2^1 * 0 (no iterations since no children)  
red   = n/2^2 * 1 (*heapify* will perform atmost one swap for each red node)  
blue  = n/2^3 * 2 (*heapify* will perform atmost two swaps for each blue node)  
purple = n/4^3 * 3  

поэтому для любых узлов с высотой h максимальная работа равна n/2 ^ (h + 1) * h

Теперь общая работа сделана

->(n/2^1 * 0) + (n/2^2 * 1)+ (n/2^3 * 2) + (n/2^4 * 3) +...+ (n/2^(h+1) * h)  
-> n * ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

Теперь для любого значения ч, последовательность

-> ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

никогда не превысит 1
Таким образом, сложность времени никогда не будет превышать O (n) для построения кучи

Ответ 8

В случае создания кучи, мы начинаем с высоты,    logn -1 (где logn - высота дерева из n элементов). Для каждого элемента, присутствующего на высоте "h", мы идем с максимальной высотой до (logn -h) вниз.

    So total number of traversal would be:-
    T(n) = sigma((2^(logn-h))*h) where h varies from 1 to logn
    T(n) = n((1/2)+(2/4)+(3/8)+.....+(logn/(2^logn)))
    T(n) = n*(sigma(x/(2^x))) where x varies from 1 to logn
     and according to the [sources][1]
    function in the bracket approaches to 2 at infinity.
    Hence T(n) ~ O(n)

Ответ 9

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

T = O(log(1) + log(2) + .. + log(n)) = O(log(n!))

По скрипичному приближению n! =~ O(n^(n + O(1))), поэтому T =~ O(nlog(n))

Надеемся, что это поможет, оптимальный способ O(n) использует алгоритм кучи построения для данного набора (порядок не имеет значения).

Ответ 10

В основном, работа выполняется только на нелистовых узлах при построении кучи... и выполненная работа - это сумма замены для удовлетворения состояния кучи... другими словами (в худшем случае) сумма пропорциональна на высоту node... вся сложность задачи пропорциональна сумме высот всех нелистовых узлов, которая равна (2 ^ h + 1 - 1) -h-1 = nh-1 = O (n)

Ответ 11

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

Основа вашей первоначальной ошибки связана с неверным истолкованием значения утверждения: "Вставка в кучу принимает O (log n) time". Вставка в кучу - это действительно O (log n), но вы должны признать, что n - это размер кучи во время вставки.

В контексте вставки n объектов в кучу сложность i-й вставки - O (log n_i), где n_i - размер кучи, как при вставке i. Только последняя вставка имеет сложность O (log n).

Ответ 12

Доказательство O (n)

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

Ответ 13

Мне очень нравится объяснение Джереми на западе... здесь дается другой подход, который очень прост для понимания. http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity

поскольку, buildheap зависит от использования зависит от heapify и используется подход сдвига, который зависит от суммы высот всех узлов. Итак, чтобы найти сумму высоты узлов, которая задается формулой         S = суммирование от я = 0 до я = h of (2 ^ я * (h-i)), где h = logn - высота дерева       решение s, получим s = 2 ^ (h + 1) - 1 - (h + 1)       так как n = 2 ^ (h + 1) - 1       s = n - h - 1 = n-logn - 1       s = O (n), и поэтому сложность построения является O (n).

Ответ 14

"Линейная временная граница построения кучи, может быть показана путем вычисления суммы высот всех узлов в куче, которая является максимальным числом штриховых линий. Для идеального бинарного дерева с высотой h, содержащего N = 2 ^ (h + 1) - 1 узлов, сумма высот узлов равна N - H - 1. Таким образом, это O (N). "

Ответ 15

Предположим, у вас есть N элементов в куче. Тогда его высота будет Log (N)

Теперь вы хотите, чтобы вставить еще один элемент, то сложность будет: Log (N), мы должны сравнить весь путь до корня.

Теперь у вас есть N + 1 элементов и высота = Log (N + 1)

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

Сейчас использую

log a + log b = log ab

Это упрощает: ∑logi = log (n!)

что на самом деле O (NlogN)

Но

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

Это осознание пришло ко мне после подробностей и экспериментов на кучах.

Ответ 16

Думаю, вы ошибаетесь. Взгляните на это: http://golang.org/pkg/container/heap/ Построение кучи isn'y O (n). Тем не менее, вставка - это O (lg (n)). Я предполагаю, что инициализация - это O (n), если вы задаете размер кучи b ​​/c, куча должна выделить пространство и настроить структуру данных. Если у вас есть n элементов, которые нужно положить в кучу, тогда да, каждая вставка будет lg (n) и есть n элементов, поэтому вы получите n * lg (n), как указано u