Может кто-нибудь помочь объяснить, как сборка кучи может быть O (n) сложность?
Вставка элемента в кучу происходит O(log n)
, и вставка повторяется n / 2 раза (остальные - листья и не могут нарушать свойство кучи). Таким образом, это означает, что сложность должна быть O(n log n)
, я думаю.
Другими словами, для каждого элемента, который мы «heapify», он может фильтровать один раз для каждого уровня кучи (то есть log n уровней).
Что мне не хватает?
Ответы:
Я думаю, что в этой теме похоронено несколько вопросов:
buildHeap
чтобы он работал в O (N) времени?buildHeap
работает в O (N) времени при правильной реализации?Как вы реализуете,
buildHeap
чтобы он работал в O (N) времени?Часто ответы на эти вопросы сосредоточены на разнице между
siftUp
иsiftDown
. Делая правильный выбор междуsiftUp
иsiftDown
имеет решающее значение для получения (п) O производительность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
каждый элемент. На каждом шаге ранее просеянные элементы (элементы перед текущим элементом в массиве) образуют правильную кучу, а отсеивание следующего элемента помещает его в правильную позицию в куче. После просеивания каждого узла все элементы удовлетворяют свойству кучи.Или идите в противоположном направлении: начните с конца массива и двигайтесь назад вперед. На каждой итерации вы просеиваете предмет, пока он не окажется в правильном месте.
Какая реализация для
buildHeap
более эффективна?Оба эти решения будут создавать допустимую кучу. Неудивительно, что более эффективной является вторая операция, которая использует
siftDown
.Пусть h = log n представляет высоту кучи. Требуемая для
siftDown
подхода работа определяется суммойКаждый член в сумме имеет максимальное расстояние, которое должен пройти узел на данной высоте (ноль для нижнего слоя, h для корня), умноженное на количество узлов на этой высоте. Напротив, сумма для вызова
siftUp
на каждом узлеДолжно быть понятно, что вторая сумма больше. Одно только первое слагаемое имеет вид hn / 2 = 1/2 n log n , поэтому такой подход в лучшем случае имеет сложность O (n log n) .
Как мы докажем, что сумма для
siftDown
подхода действительно O (n) ?Один из методов (есть и другие анализы, которые также работают) - превратить конечную сумму в бесконечный ряд, а затем использовать ряд Тейлора. Мы можем игнорировать первый член, который равен нулю:
Если вы не уверены, почему каждый из этих шагов работает, вот оправдание для процесса в словах:
Поскольку бесконечная сумма равна ровно n , мы заключаем, что конечная сумма не больше, и, следовательно, O (n) .
Почему сортировка кучи требует времени O (n log n) ?
Если можно работать
buildHeap
за линейное время, почему сортировка кучи требует времени O (n log n) ? Ну, куча сортировки состоит из двух этапов. Сначала мы обращаемсяbuildHeap
к массиву, который требует оптимального времени O (n) . Следующим этапом является повторное удаление самого большого элемента в куче и помещение его в конец массива. Поскольку мы удаляем элемент из кучи, сразу после окончания кучи всегда есть открытое место, где мы можем сохранить элемент. Таким образом, сортировка кучи достигает отсортированного порядка, последовательно удаляя следующий по величине элемент и помещая его в массив, начиная с последней позиции и двигаясь вперед. Это сложность этой последней части, которая доминирует в куче. Цикл выглядит так:Ясно, что цикл выполняется O (n) раз ( точнее, n - 1 , последний элемент уже на месте). Сложность
deleteMax
для кучи составляет O (log n) . Обычно это выполняется путем удаления корня (самый большой элемент, оставшийся в куче) и замены его последним элементом в куче, который является листом и, следовательно, одним из самых маленьких элементов. Этот новый корень почти наверняка нарушит свойство кучи, поэтому вы должны вызывать его,siftDown
пока не вернете его обратно в приемлемое положение. Это также приводит к перемещению следующего по величине элемента до корня. Обратите внимание, что в отличие от того,buildHeap
где для большинства узлов мы вызываемsiftDown
из нижней части дерева, мы теперь вызываемsiftDown
из верхней части дерева на каждой итерации!Хотя дерево сжимается, оно не сжимается достаточно быстро : высота дерева остается постоянной, пока вы не удалите первую половину узлов (когда вы полностью очистите нижний слой). Тогда для следующей четверти высота h - 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
делает.источник
siftUp
каждый предмет, либо начинаете в конце, двигаясь назад и звонитеsiftDown
. Независимо от того, какой подход вы выберете, вы выбираете следующий элемент в несортированной части массива и выполняете соответствующую операцию, чтобы переместить его в правильную позицию в упорядоченной части массива. Разница лишь в производительности.Ваш анализ верен. Тем не менее, это не тесно.
Нелегко объяснить, почему построение кучи является линейной операцией, вам лучше ее прочитать.
Большой анализ алгоритма можно увидеть здесь .
Основная идея заключается в том, что в
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)
.источник
Heapify
этоO(n)
когда сделано сsiftDown
ноO(n log n)
когда сделано сsiftUp
. Фактическая сортировка (извлечение элементов из кучи один за другим) должна выполнятьсяsiftUp
таким образомO(n log n)
.i
от нижней части дерева с высотой h, должны также делать2* log(h-i)
сравнения и должны учитываться также @ The111. Что вы думаете?Наглядно:
Не совсем. Ваша логика не дает жесткой границы - она переоценивает сложность каждой кучи. Если построено снизу вверх, вставка (heapify) может быть намного меньше, чем
O(log(n))
. Процесс выглядит следующим образом:(Шаг 1) Первые
n/2
элементы идут в нижнем ряду кучи.h=0
, так что heapify не нужен.(Шаг 2) Следующие элементы идут в строке 1 снизу вверх. Фильтры на 1 уровень вниз.
n/22
h=1
(Шаг i ) Следующие элементы идут в ряд снизу вверх. Отфильтруйте фильтры уровнями вниз.
n/2i
i
h=i
i
( Журнал шагов (n) ) Последний элемент идет в ряд снизу вверх. Отфильтруйте фильтры уровнями вниз.
n/2log2(n) = 1
log(n)
h=log(n)
log(n)
ЗАМЕЧАНИЕ: после первого шага
1/2
элементы(n/2)
уже находятся в куче, и нам даже не нужно было вызывать heapify один раз. Кроме того, обратите внимание, что только один элемент, корень, фактически несет полнуюlog(n)
сложность.Теоретически:
Всего шагов
N
для построения кучи размераn
, можно записать математически.На высоте
i
мы показали (выше), что будут элементы, которые нужно вызвать heapify, и мы знаем, что heapify на высоте is . Это дает:n/2i+1
i
O(i)
Решение последней суммы можно найти, взяв производную обеих сторон хорошо известного уравнения геометрической серии:
Наконец, подключение
x = 1/2
к приведенному выше уравнению дает2
. Включение этого в первое уравнение дает:Таким образом, общее количество шагов имеет размер
O(n)
источник
Было бы O (n log n), если бы вы строили кучу, многократно вставляя элементы. Тем не менее, вы можете создать новую кучу более эффективно, вставляя элементы в произвольном порядке, а затем применяя алгоритм для «кучи» их в правильном порядке (конечно, в зависимости от типа кучи).
См. Http://en.wikipedia.org/wiki/Binary_heap , «Создание кучи» для примера. В этом случае вы, по сути, работаете с нижнего уровня дерева, меняя местами родительский и дочерний узлы, пока не будут выполнены условия кучи.
источник
Уже есть несколько отличных ответов, но я хотел бы добавить небольшое визуальное объяснение
Теперь взгляните на изображение: есть
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)
узлы для высоты h.Чтобы определить сложность времени, давайте посчитаем количество выполненной работы или максимальное количество итераций, выполненных каждым узлом,
теперь можно заметить, что каждый узел может выполнить (максимум) итераций == высота узла
поэтому для любых узлов с высотой h максимальная работа равна n / 2 ^ (h + 1) * h
В настоящее время общая работа выполнена
Теперь для любого значения ч , последовательность
никогда не будет превышать 1
Таким образом, сложность времени никогда не будет превышать O (n) для построения кучи
источник
Как мы знаем, высота кучи равна log (n) , где n - общее количество элементов. Представим его как h.
Когда мы выполняем операцию heapify, элементы на последнем уровне ( h ) не будут перемещаться даже на единицу. шаг.
Количество элементов на втором последнем уровне ( h-1 ) равно 2 h-1, и они могут двигаться на уровне максимум 1 (во время heapify).
Аналогичным образом , для я - го , уровня мы имеем 2 я элементы , которые могут перемещаться привет позиции.
Поэтому общее количество ходов = S = 2 ч * 0 + 2 ч-1 * 1 + 2 ч-2 * 2 + ... 2 0 * ч
S = 2 ч {1/2 + 2/2 2 + 3/2 3 + ... ч / 2 ч } ----------------------- -------------------------- 1
это серия AGP , для решения этого делим обе стороны на 2
S / 2 = 2 ч {1/2 2 + 2/2 3 + ... ч / 2 ч + 1 } --------------------------------- ---------------- 2
вычитая уравнение 2 из 1, получаем
S / 2 = 2 ч { 1/2 + 1/2 2 + 1/2 3 + ... + 1 / 2 ч + ч / 2 ч + 1 }
S = 2 ч + 1 {1/2 + 1/2 2 + 1/2 3 + ... 1/2 ч + ч / 2 ч + 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 h + 1 + h
~ 2 h + h
как h = log (n) , 2 h = n
Следовательно, S = n + log (n)
T (C) = O (n)
источник
При построении кучи, допустим, вы подходите снизу вверх.
источник
В случае построения кучи, мы начинаем с высоты, logn -1 (где logn - высота дерева из n элементов). Для каждого элемента, присутствующего на высоте 'h', мы идем на максимум вверх (logn -h) вниз.
источник
Последовательные вставки могут быть описаны:
n! =~ O(n^(n + O(1)))
Следовательно, по скворецу приближенияT =~ O(nlog(n))
Надеюсь, это поможет, оптимальный способ
O(n)
- использовать алгоритм кучи сборки для заданного набора (порядок не имеет значения).источник
В основном, работа выполняется только на неконечных узлах при построении кучи ... и проделанная работа - это объем обмена, чтобы удовлетворить условию кучи ... другими словами (в худшем случае) это количество пропорционально высоте узла ... в целом сложность задачи пропорциональна сумме высот всех неконечных узлов .. которая равна (2 ^ h + 1 - 1) -h-1 = nh-1 = На)
источник
@bcorso уже продемонстрировал доказательство анализа сложности. Но ради тех, кто все еще изучает анализ сложности, я хочу добавить следующее:
Основой вашей первоначальной ошибки является неправильное толкование значения выражения «вставка в кучу занимает O (log n) времени». Вставка в кучу - это действительно O (log n), но вы должны понимать, что n - это размер кучи во время вставки .
В контексте вставки n объектов в кучу сложность i-й вставки составляет O (log n_i), где n_i - размер кучи, как при вставке i. Только последняя вставка имеет сложность O (log n).
источник
Предположим, у вас есть N элементов в куче. Тогда его высота будет Log (N)
Теперь вы хотите , чтобы вставить еще один элемент, то сложность будет: Log (N) , мы должны сравнить весь путь UP к корню.
Теперь у вас есть N + 1 элементов и высота = Log (N + 1)
Используя технику индукции , можно доказать, что сложность введения была бы логической .
Сейчас использую
Это упрощает: ∑logi = log (n!)
что на самом деле O (NlogN)
Но
мы делаем что-то не так, как и во всех случаях, когда мы не достигаем вершины. Следовательно, выполняя большую часть времени, мы можем обнаружить, что мы не идем даже на полпути вверх по дереву. Таким образом, эта граница может быть оптимизирована, чтобы иметь другую более жесткую границу с помощью математики, приведенной в ответах выше.
Это осознание пришло ко мне после подробного изучения экспериментов на кучах.
источник
Мне очень нравится объяснение Джереми Уэста .... другой подход, который действительно легко понять, дан здесь http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity
поскольку buildheap зависит от использования, зависит от heapify и используется подход смещения, который зависит от суммы высот всех узлов. Итак, чтобы найти сумму высоты узлов, которая определяется как S = суммирование от i = 0 до i = h (2 ^ i * (hi)), где 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), и поэтому сложность buildheap составляет O (n).
источник
«Линейная временная граница сборки Heap, может быть показана путем вычисления суммы высот всех узлов в куче, которая является максимальным числом пунктирных линий. Для идеального двоичного дерева высоты h, содержащего N = 2 ^ ( h + 1) - 1 узлов, сумма высот узлов равна N - H - 1. Таким образом, это O (N). "
источник
Доказательство O (n)
Доказательство не фантастическое, и довольно простое, я только доказал случай для полного двоичного дерева, результат может быть обобщен для полного двоичного дерева.
источник
Мы получаем время выполнения для сборки кучи, вычисляя максимальное движение, которое может сделать каждый узел. Поэтому нам нужно знать, сколько узлов находится в каждой строке и как далеко от них может пройти каждый узел.
Начиная с корневого узла, каждая следующая строка имеет двойные узлы, чем предыдущая, поэтому, отвечая на то, как часто мы можем удваивать количество узлов, пока у нас не останется ни одного узла, мы получим высоту дерева. Или в математических терминах высота дерева равна log2 (n), где n - длина массива.
Чтобы вычислить узлы в одном ряду, мы начинаем сзади, мы знаем, что n / 2 узлов находятся внизу, поэтому, разделив на 2, мы получим предыдущий ряд и так далее.
Исходя из этого, мы получаем следующую формулу для подхода Siftdown: (0 * n / 2) + (1 * n / 4) + (2 * n / 8) + ... + (log2 (n) * 1)
Термин в последнем парантезе представляет собой высоту дерева, умноженную на один узел, находящийся в корне, термином в первом парантезе являются все узлы в нижнем ряду, умноженные на длину, которую они могут пройти, 0. Та же формула в смарте:
Возвращая n обратно, мы имеем 2 * n, 2 можно отбросить, потому что его константа и тада, у нас наихудший случай выполнения подхода Siftdown: n.
источник
Я думаю, что вы делаете ошибку. Взгляните на это: http://golang.org/pkg/container/heap/ Сборка кучи отсутствует O (n). Тем не менее, вставка - это O (lg (n). Я предполагаю, что инициализация - это O (n), если вы устанавливаете размер кучи b / c, куча должна выделять пространство и настраивать структуру данных. Если у вас есть n элементов для размещения тогда в кучу да, каждая вставка - это lg (n), и есть n элементов, так что вы получите n * lg (n), как указано выше
источник