Czy ktoś może wyjaśnić, w jaki sposób budowanie sterty może być złożonością O (n)?
Wstawianie elementu do sterty jest O(log n)
, a wstawianie jest powtarzane n / 2 razy (pozostałe są liśćmi i nie mogą naruszać właściwości sterty). To oznacza, że złożoność powinna być O(n log n)
, jak sądzę.
Innymi słowy, dla każdego elementu, który „stertyzujemy”, może on wymagać odfiltrowania raz dla każdego poziomu stosu do tej pory (czyli poziomów log n).
czego mi brakuje?
Odpowiedzi:
Myślę, że w tym temacie jest kilka pytań:
buildHeap
aby działało w czasie O (n) ?buildHeap
działa w czasie O (n), gdy jest poprawnie zaimplementowany?Jak wdrożyć,
buildHeap
aby działało w czasie O (n) ?Często odpowiedzi na te pytania koncentrują się na różnicy między
siftUp
isiftDown
. Właściwy wybór międzysiftUp
isiftDown
jest krytyczny dla uzyskania wydajności O (n)buildHeap
, ale nie robi nic, aby pomóc zrozumieć różnicę pomiędzybuildHeap
iheapSort
ogólnie. Rzeczywiście, odpowiednie implementacje obubuildHeap
iheapSort
będą korzystać tylkosiftDown
. TasiftUp
operacja jest potrzebna tylko do wstawiania do istniejącej sterty, więc może być wykorzystana do zaimplementowania kolejki priorytetowej przy użyciu na przykład sterty binarnej.Napisałem to, aby opisać, jak działa maksymalna sterty. Jest to typ sterty zwykle używany do sortowania sterty lub do kolejki priorytetowej, w której wyższe wartości wskazują wyższy priorytet. Przydatna jest również sterta min; na przykład podczas wyszukiwania elementów z kluczami całkowitymi w porządku rosnącym lub ciągami znaków w kolejności alfabetycznej. Zasady są dokładnie takie same; wystarczy zmienić kolejność sortowania.
Właściwość sterta określa, że każdy węzeł w stercie binarnym musi być co najmniej tak duży, jak oba jego elementy potomne. W szczególności oznacza to, że największy element na stosie znajduje się w katalogu głównym. Przesiewanie w dół i przesiewanie w górę to w zasadzie ta sama operacja w przeciwnych kierunkach: przesuń naruszający węzeł, aż spełni właściwości sterty:
siftDown
zamienia zbyt mały węzeł z jego największym dzieckiem (przesuwając go w dół), aż będzie co najmniej tak duży, jak oba węzły pod nim.siftUp
zamienia węzeł, który jest zbyt duży z rodzicem (tym samym przesuwając go w górę), aż nie będzie większy niż węzeł nad nim.Liczba operacji wymaganych
siftDown
isiftUp
jest proporcjonalna do odległości, jaką może pokonać węzeł. PonieważsiftDown
jest to odległość do dolnej części drzewa, więcsiftDown
jest kosztowna dla węzłów na szczycie drzewa. DziękisiftUp
, praca jest proporcjonalna do odległości do szczytu drzewa, więcsiftUp
jest kosztowna dla węzłów na dole drzewa. Chociaż obie operacje są w najgorszym przypadku O (log n) , w stercie tylko jeden węzeł znajduje się na górze, podczas gdy połowa węzłów leży na dolnej warstwie. Nie powinno więc dziwić, że gdybyśmy musieli zastosować operację do każdego węzła, wolelibyśmysiftDown
więcejsiftUp
.buildHeap
Funkcja przyjmuje tablicę nieposortowane przedmiotów i przenosi je do momentu spełnienia wszystkich właściwość stos, tworząc w ten sposób poprawny sterty. Istnieją dwa podejścia można by przyjąć zabuildHeap
wykorzystaniemsiftUp
isiftDown
operacje Opisaliśmy.Rozpocznij na górze stosu (na początku tablicy) i wywołaj
siftUp
każdy element. Na każdym etapie poprzednio przesiane elementy (elementy przed bieżącym elementem w tablicy) tworzą prawidłową stertę, a przesiewanie następnego elementu w górę umieszcza go w prawidłowej pozycji na stercie. Po przesianiu każdego węzła wszystkie elementy spełniają właściwość sterty.Lub idź w przeciwnym kierunku: zacznij od końca tablicy i cofnij się do przodu. Przy każdej iteracji przesiewasz przedmiot w dół, aż znajdzie się we właściwej lokalizacji.
Które wdrożenie
buildHeap
jest bardziej wydajne?Oba te rozwiązania wygenerują prawidłową stertę. Nic dziwnego, że bardziej wydajna jest druga operacja
siftDown
.Niech h = log n reprezentuje wysokość stosu. Praca wymagana do tego
siftDown
podejścia jest sumąKażdy termin w sumie ma maksymalną odległość, którą musi przesunąć węzeł na danej wysokości (zero dla dolnej warstwy, h dla korzenia) pomnożony przez liczbę węzłów na tej wysokości. Natomiast suma za wywołanie
siftUp
w każdym węźle wynosiPowinno być jasne, że druga suma jest większa. Sam pierwszy termin to hn / 2 = 1/2 n log n , więc to podejście ma w najlepszym razie złożoność O (n log n) .
Jak udowodnimy, że suma dla tego
siftDown
podejścia jest rzeczywiście O (n) ?Jedną z metod (istnieją inne analizy, które również działają) jest przekształcenie skończonej sumy w szereg nieskończony, a następnie użycie szeregu Taylora. Możemy zignorować pierwszy termin, który wynosi zero:
Jeśli nie masz pewności, dlaczego każdy z tych kroków działa, oto uzasadnienie tego procesu w słowach:
Ponieważ suma nieskończona wynosi dokładnie n , dochodzimy do wniosku, że suma skończona nie jest większa, a zatem wynosi O (n) .
Dlaczego sortowanie sterty wymaga czasu O (n log n) ?
Jeśli możliwe jest uruchomienie
buildHeap
w czasie liniowym, dlaczego sortowanie sterty wymaga czasu O (n log n) ? Sortowanie sterty składa się z dwóch etapów. Najpierw wywołujemybuildHeap
tablicę, która wymaga O (n) czasu, jeśli jest zaimplementowana optymalnie. Następnym etapem jest wielokrotne usuwanie największego elementu ze sterty i umieszczanie go na końcu tablicy. Ponieważ usuwamy element ze sterty, tuż za końcem sterty zawsze jest otwarte miejsce, w którym możemy przechowywać przedmiot. Tak więc sortowanie sterty osiąga uporządkowany porządek poprzez sukcesywne usuwanie następnego największego przedmiotu i umieszczanie go w tablicy, zaczynając od ostatniej pozycji i przesuwając się do przodu. To złożoność tej ostatniej części dominuje w rodzaju sterty. Pętla wygląda następująco:Oczywiście, pętla działa O (n) razy ( n - 1, by być dokładnym, ostatni element jest już na miejscu). Złożoność
deleteMax
stosu wynosi O (log n) . Zwykle jest to realizowane przez usunięcie korzenia (największego elementu pozostawionego na stercie) i zastąpienie go ostatnim elementem na stercie, który jest liściem, a zatem jednym z najmniejszych elementów. Ten nowy root prawie na pewno naruszy właściwość sterty, więc musisz zadzwonić,siftDown
dopóki nie przeniesiesz go z powrotem do akceptowalnej pozycji. To również powoduje przeniesienie następnego największego przedmiotu do korzenia. Zauważ, że w przeciwieństwie do tego,buildHeap
gdzie w przypadku większości węzłów wzywamysiftDown
od dołu drzewa, teraz wzywamysiftDown
od góry drzewa podczas każdej iteracji!Chociaż drzewo kurczy się, nie kurczy się wystarczająco szybko : wysokość drzewa pozostaje stała, dopóki nie usuniesz pierwszej połowy węzłów (po całkowitym usunięciu dolnej warstwy). Następnie przez następny kwartał wysokość wynosi h - 1 . Tak więc całkowita praca dla tego drugiego etapu wynosiZwróć uwagę na przełącznik: teraz zerowy przypadek roboczy odpowiada jednemu węzłowi, a przypadek roboczy h odpowiada połowie węzłów. Ta suma wynosi O (n log n), podobnie jak nieefektywna wersja
buildHeap
tego jest implementowana za pomocą siftUp. Ale w tym przypadku nie mamy wyboru, ponieważ próbujemy sortować i wymagamy następnego następnego największego elementu do usunięcia.Podsumowując, praca dla sortowania sterty jest sumą dwóch etapów: O (n) czas na buildHeap i O (n log n) do usunięcia każdego węzła w kolejności , więc złożoność wynosi O (n log n) . Możesz udowodnić (korzystając z niektórych pomysłów z teorii informacji), że dla sortowania opartego na porównaniu O (n log n) jest najlepszym, na co możesz liczyć, więc nie ma powodu, aby być rozczarowanym tym lub oczekiwać, że sortowanie sterty pozwoli osiągnąć O (n) czas, który
buildHeap
spełnia.źródło
siftUp
każdy element, albo zaczynasz od końca, cofając się i dzwoniącsiftDown
. Bez względu na to, które podejście wybierzesz, wybierasz następny element w nieposortowanej części tablicy i wykonujesz odpowiednią operację, aby przenieść go do prawidłowej pozycji w uporządkowanej części tablicy. Jedyną różnicą jest wydajność.Twoja analiza jest poprawna. Jednak nie jest ciasno.
Nie jest łatwo wyjaśnić, dlaczego budowanie sterty jest operacją liniową, lepiej ją przeczytaj.
Wielki analiza algorytmu można zobaczyć tutaj .
Główną ideą jest to, że w
build_heap
algorytmie rzeczywistyheapify
koszt nieO(log n)
dotyczy wszystkich elementów.Po
heapify
wywołaniu czas działania zależy od tego, jak daleko element może się przesunąć w dół drzewa przed zakończeniem procesu. Innymi słowy, zależy to od wysokości elementu w stercie. W najgorszym przypadku element może spaść aż do poziomu liścia.Policzmy pracę wykonaną poziom po poziomie.
Na najniższym poziomie są
2^(h)
węzły, ale nie wzywamyheapify
żadnego z nich, więc praca wynosi 0. Na następnym poziomie są2^(h − 1)
węzły i każdy może przejść w dół o 1 poziom. Na 3. poziomie od dołu znajdują się2^(h − 2)
węzły, z których każdy może przejść w dół o 2 poziomy.Jak widać, nie wszystkie operacje rozsypywania są
O(log n)
, dlatego otrzymujeszO(n)
.źródło
Heapify
jestO(n)
po zakończeniu,siftDown
aleO(n log n)
po zakończeniusiftUp
. Rzeczywiste sortowanie (wyciąganie przedmiotów ze stosu jeden po drugim) musi byćsiftUp
zatem wykonaneO(n log n)
.i
od dołu drzewa wysokości h muszą również dokonać2* log(h-i)
porównań i należy je również uwzględnić @ The111. Co myślisz?Intuicyjnie:
Nie do końca. Twoja logika nie tworzy ścisłej więzi - przesadza z oszacowaniem złożoności każdego heapify. W przypadku budowania od podstaw, wstawianie (heapify) może być znacznie mniejsze niż
O(log(n))
. Proces przebiega następująco:(Krok 1) Pierwsze
n/2
elementy trafiają do dolnego rzędu stosu.h=0
, więc heapify nie jest potrzebne.(Krok 2) Następne elementy idą w rzędzie 1 w górę od dołu. , heapify filtry o 1 poziom w dół.
n/22
h=1
(Krok i ) Następne elementy idą w rzędzie w górę od dołu. obniża poziomy filtrów .
n/2i
i
h=i
i
(Krok dziennika (n) ) Ostatni element idzie w rzędzie w górę od dołu. obniża poziomy filtrów .
n/2log2(n) = 1
log(n)
h=log(n)
log(n)
ZAUWAŻ: że po pierwszym kroku
1/2
elementy(n/2)
są już w stercie i nie musieliśmy nawet wywoływać jednorazowo wezwania. Zauważ też, że tylko jeden element, rdzeń, w rzeczywistości powoduje pełnąlog(n)
złożoność.Teoretycznie:
Całkowitą
N
liczbę kroków do zbudowania sterty wielkościn
można zapisać matematycznie.Na wysokości
i
pokazaliśmy (powyżej), że będą elementy, które będą musiały wywołać heapify, i wiemy, że heapify na wysokości to . To daje:n/2i+1
i
O(i)
Rozwiązanie ostatniego sumowania można znaleźć, biorąc pochodną obu stron znanego równania szeregów geometrycznych:
Wreszcie podłączenie
x = 1/2
do powyższego równania daje wynik2
. Podłączenie tego do pierwszego równania daje:Zatem łączna liczba kroków ma rozmiar
O(n)
źródło
Byłoby O (n log n), jeśli zbudowałeś stertę przez wielokrotne wstawianie elementów. Możesz jednak wydajniej utworzyć nową stertę, wstawiając elementy w dowolnej kolejności, a następnie stosując algorytm do „stertyfikowania” ich we właściwej kolejności (w zależności od rodzaju sterty, oczywiście).
Zobacz http://en.wikipedia.org/wiki/Binary_heap „Budowanie sterty” dla przykładu. W tym przypadku zasadniczo pracujesz od najniższego poziomu drzewa, zamieniając węzły nadrzędne i potomne, dopóki warunki sterty nie zostaną spełnione.
źródło
Jest już kilka świetnych odpowiedzi, ale chciałbym dodać trochę wizualnego wyjaśnienia
Teraz spójrz na obrazek, są
n/2^1
zielone węzły o wysokości 0 (tutaj 23/2 = 12)n/2^2
czerwone węzły o wysokości 1 (tutaj 23/4 = 6)n/2^3
niebieski węzeł o wysokości 2 (tutaj 23/8 = 3)n/2^4
fioletowe węzły o wysokości 3 (tutaj 23/16 = 2),więc są
n/2^(h+1)
węzły dla wysokości hAby znaleźć złożoność czasu, policzmy ilość pracy wykonanej lub max liczbę iteracji wykonanych przez każdy węzeł,
teraz można zauważyć, że każdy węzeł może wykonaj (atmost) iteracje == wysokość węzła
więc dla wszystkich węzłów o wysokości h maksymalna wykonana praca wynosi n / 2 ^ (h + 1) * h
Teraz wykonano całą pracę
teraz dla dowolnej wartości h , sekwencji
nigdy nie przekroczy 1
Tak więc złożoność czasowa nigdy nie przekroczy O (n) dla budowy stosu
źródło
Jak wiemy, wysokość stosu to log (n) , gdzie n to całkowita liczba elementów. Niech to reprezentuje jako h.
Kiedy wykonujemy operację heapify, elementy na ostatnim poziomie ( h ) nie poruszą się nawet krok.
Liczba elementów na drugim ostatnim poziomie ( h-1 ) wynosi 2 h-1 i mogą się one poruszać na maksymalnie 1 poziomie (podczas heapify).
Podobnie dla ı TH , poziom mamy 2 i elementów, które mogą się poruszać hi pozycji.
Dlatego łączna liczba ruchów = S = 2 h * 0 + 2 h-1 * 1 + 2 h-2 * 2 + ... 2 0 * h
S = 2 godz. {1/2 + 2/2 2 + 3/2 3 + ... h / 2 godz. } ----------------------- -------------------------- 1
to jest seria AGP , aby rozwiązać ten podział, podziel obie strony przez 2
S / 2 = 2 h {1/2 2 + 2/2 3 + ... h / 2 h + 1 } --------------------------------- ---------------- 2
odjęcie równania 2 od 1 daje
S / 2 = 2 h { 1/2 + 1/2 2 + 1/2 3 + ... + 1 / 2 godz. + Godz. / 2 godz. + 1 }
S = 2 h + 1 { 1/2 + 1/2 2 + 1/2 3 + ... +1/2 h + h / 2 h + 1 }
się 1/2 + 1/2 2 + 1/2 3 + ... + 1/2 h zmniejsza GP, którego suma jest mniejsza niż 1 (gdy h dąży do nieskończoności, suma dąży do 1). W dalszej analizie, weźmy górną granicę sumy, która wynosi 1.
To daje S = 2 h + 1 {1 + h / 2 h + 1 }
= 2 h + 1 + h
~ 2 h + h
as h = log (n) , 2 godz. = n
Dlatego S = n + log (n)
T (C) = O (n)
źródło
Budując stertę, powiedzmy, że stosujesz podejście oddolne.
źródło
W przypadku budowy hałdy zaczynamy od wysokości, logn -1 (gdzie logn jest wysokością drzewa n elementów). Dla każdego elementu obecnego na wysokości „h” przechodzimy do maksymalnej wysokości (logn-h) wysokości w dół.
źródło
Kolejne wstawki można opisać przez:
n! =~ O(n^(n + O(1)))
Dlatego przybliżając szpakT =~ O(nlog(n))
Mam nadzieję, że to pomaga, optymalnym sposobem
O(n)
jest użycie algorytmu budowania sterty dla danego zestawu (kolejność nie ma znaczenia).źródło
Zasadniczo praca jest wykonywana tylko na węzłach innych niż liście podczas budowania stosu ... a wykonana praca to ilość zamiany w dół, aby spełnić warunek stosu ... innymi słowy (w najgorszym przypadku) ilość jest proporcjonalna do wysokości węzła ... w sumie złożoność problemu jest proporcjonalna do sumy wysokości wszystkich nie-liściowych węzłów .. którym jest (2 ^ h + 1 - 1) -h-1 = nh-1 = Na)
źródło
@bcorso pokazał już dowód analizy złożoności. Ale ze względu na osoby wciąż uczące się analizy złożoności muszę dodać:
Podstawa twojego pierwotnego błędu wynika z błędnej interpretacji znaczenia stwierdzenia: „wstawienie do stosu zajmuje czas O (log n)”. Wstawianie do stosu jest rzeczywiście O (log n), ale musisz rozpoznać, że n jest rozmiarem stosu podczas wstawiania .
W kontekście wstawiania n obiektów do stosu złożoność i-tego wstawienia wynosi O (log n_i), gdzie n_i jest rozmiarem stosu jak przy wstawianiu i. Tylko ostatnie wstawienie ma złożoność O (log n).
źródło
Załóżmy, że masz N elementów na stosie. Wtedy jego wysokość wynosiłaby Log (N)
Teraz chcesz wstawić inny element, to złożoność byłoby: log (N) , musimy porównać całą drogę UP do korzenia.
Teraz masz N + 1 elementów i wysokość = Log (N + 1)
Za pomocą techniki indukcyjnej można udowodnić, że złożoność wstawienia byłaby ∑logi .
Teraz używa
Upraszcza to: ∑logi = log (n!)
czyli tak naprawdę O (NlogN)
Ale
robimy tutaj coś złego, ponieważ we wszystkich przypadkach nie sięgamy do szczytu. Dlatego podczas wykonywania większości przypadków możemy się przekonać, że nie idziemy nawet w połowie wysokości drzewa. Skąd ta granica może być zoptymalizowana, aby mieć jeszcze ściślejszą granicę, używając matematyki podanej w odpowiedziach powyżej.
Ta realizacja przyszła do mnie po szczegółach i eksperymentach na hałdach.
źródło
Naprawdę podoba mi się wyjaśnienie Jeremy'ego Westa .... inne podejście, które jest naprawdę łatwe do zrozumienia, znajduje się tutaj http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity
ponieważ budowanie zależy od użycia zależy od stosu i stosuje się podejście zmniejszające, które zależy od sumy wysokości wszystkich węzłów. Tak więc, aby znaleźć sumę wysokości węzłów, która jest dana przez S = sumowanie od i = 0 do i = h z (2 ^ i * (hi)), gdzie h = logn jest wysokością rozwiązania drzewa s, otrzymujemy s = 2 ^ (h + 1) - 1 - (h + 1), ponieważ n = 2 ^ (h + 1) - 1 s = n - h - 1 = n- logn - 1 s = O (n), i tak złożoność zestawienia kompilacji wynosi O (n).
źródło
„Liniową granicę czasową kompilacji Sterty można pokazać, obliczając sumę wysokości wszystkich węzłów w stercie, która jest maksymalną liczbą linii przerywanych. Dla idealnego drzewa binarnego wysokości h zawierającego N = 2 ^ ( h + 1) - 1 węzły, suma wysokości węzłów wynosi N - H - 1. Zatem jest to O (N). ”
źródło
Dowód O (n)
Dowód nie jest wyszukany i dość prosty, udowodniłem tylko, że jest to pełne drzewo binarne, wynik można uogólnić dla pełnego drzewa binarnego.
źródło
Czas wykonywania kompilacji sterty uzyskujemy, ustalając maksymalny ruch, jaki może podjąć każdy węzeł. Musimy więc wiedzieć, ile węzłów znajduje się w każdym rzędzie i jak daleko od nich może przejść każdy węzeł.
Zaczynając od węzła głównego, każdy następny rząd ma dwa razy więcej węzłów niż poprzedni, więc odpowiadając, jak często możemy podwoić liczbę węzłów, dopóki nie zostanie nam żaden węzeł, otrzymamy wysokość drzewa. Lub w kategoriach matematycznych wysokość drzewa wynosi log2 (n), gdzie n jest długością tablicy.
Aby obliczyć węzły w jednym rzędzie, zaczynamy od tyłu, wiemy, że n / 2 węzłów znajduje się na dole, więc dzieląc przez 2, otrzymujemy poprzedni wiersz i tak dalej.
Na tej podstawie otrzymujemy wzór na podejście Siftdown: (0 * n / 2) + (1 * n / 4) + (2 * n / 8) + ... + (log2 (n) * 1)
Termin w ostatnim nawiasie to wysokość drzewa pomnożona przez jeden węzeł znajdujący się u nasady, termin w pierwszym nawiasie to wszystkie węzły w dolnym rzędzie pomnożone przez długość, jaką mogą przebyć, 0. Ta sama formuła w smart:
Przywracając n mamy 2 * n, 2 można odrzucić, ponieważ jest to ciągłe i tada mamy najgorszy czas działania podejścia Siftdown: n.
źródło
myślę, że popełniasz błąd. Spójrz na to: http://golang.org/pkg/container/heap/ Budowanie sterty to nie O (n). Jednak wstawianie to O (lg (n). Zakładam, że inicjalizacja to O (n), jeśli ustawisz rozmiar sterty b / c, sterty muszą przydzielić miejsce i skonfigurować strukturę danych. Jeśli masz n elementów do umieszczenia do stosu, a następnie tak, każda wstawka jest lg (n) i jest n elementów, więc dostajesz n * lg (n), jak podano
źródło