Quelqu'un peut-il aider à expliquer comment la construction d'un tas peut être une complexité O (n)?
L'insertion d'un élément dans un segment de mémoire est O(log n)
, et l'insertion est répétée n / 2 fois (les autres sont des feuilles et ne peuvent pas violer la propriété du segment de mémoire). Donc, cela signifie que la complexité devrait être O(n log n)
, je pense.
En d'autres termes, pour chaque élément que nous "heapify", il a le potentiel d'avoir à filtrer une fois pour chaque niveau pour le tas jusqu'à présent (qui est log n niveaux).
Qu'est-ce que je rate?
Réponses:
Je pense qu'il y a plusieurs questions enfouies dans ce sujet:
buildHeap
pour qu'il fonctionne en temps O (n) ?buildHeap
s'exécute en temps O (n) lorsqu'il est correctement implémenté?Comment l'implémentez-vous
buildHeap
pour qu'il fonctionne en temps O (n) ?Souvent, les réponses à ces questions se concentrent sur la différence entre
siftUp
etsiftDown
. Faire le bon choix entresiftUp
etsiftDown
est essentiel pour obtenir des performances O (n)buildHeap
, mais ne fait rien pour aider à comprendre la différence entrebuildHeap
etheapSort
en général. En effet, les implémentations appropriées des deuxbuildHeap
et neheapSort
seront utilisées . L' opération n'est nécessaire que pour effectuer des insertions dans un segment existant, elle serait donc utilisée pour implémenter une file d'attente prioritaire à l'aide d'un segment binaire, par exemple.siftDown
siftUp
J'ai écrit ceci pour décrire le fonctionnement d'un tas max. Il s'agit du type de segment de mémoire généralement utilisé pour le tri de segment de mémoire ou pour une file d'attente prioritaire où des valeurs plus élevées indiquent une priorité plus élevée. Un tas min est également utile; par exemple, lors de la récupération d'éléments avec des clés entières dans l'ordre croissant ou des chaînes dans l'ordre alphabétique. Les principes sont exactement les mêmes; changez simplement l'ordre de tri.
La propriété de segment de mémoire spécifie que chaque nœud d'un segment de mémoire binaire doit être au moins aussi grand que ses deux enfants. En particulier, cela implique que le plus gros élément du tas est à la racine. Le filtrage et le filtrage sont essentiellement la même opération dans des directions opposées: déplacer un nœud incriminé jusqu'à ce qu'il satisfasse la propriété du tas:
siftDown
échange un nœud trop petit avec son plus grand enfant (le déplaçant ainsi vers le bas) jusqu'à ce qu'il soit au moins aussi grand que les deux nœuds en dessous.siftUp
échange un nœud trop grand avec son parent (le déplaçant ainsi vers le haut) jusqu'à ce qu'il ne soit pas plus grand que le nœud au-dessus de lui.Le nombre d'opérations nécessaires
siftDown
etsiftUp
proportionnel à la distance que le nœud peut avoir à parcourir. CarsiftDown
, c'est la distance jusqu'au bas de l'arbre, doncsiftDown
c'est cher pour les nœuds en haut de l'arbre. AvecsiftUp
, le travail est proportionnel à la distance jusqu'au sommet de l'arbre, doncsiftUp
est coûteux pour les nœuds au bas de l'arbre. Bien que les deux opérations soient O (log n) dans le pire des cas, dans un tas, un seul nœud est en haut tandis que la moitié des nœuds se trouvent dans la couche inférieure. Donc , il ne devrait pas être trop surprenant que si nous devons appliquer une opération à chaque nœud, nous préférerionssiftDown
plussiftUp
.La
buildHeap
fonction prend un tableau d'éléments non triés et les déplace jusqu'à ce qu'ils satisfassent tous à la propriété du tas, produisant ainsi un tas valide. Il existe deux approches pourbuildHeap
utiliser les opérationssiftUp
et quesiftDown
nous avons décrites.Commencez par le haut du tas (le début du tableau) et appelez
siftUp
chaque élément. À chaque étape, les éléments précédemment triés (les éléments précédant l'élément actuel dans le tableau) forment un segment de mémoire valide, et le tri de l'élément suivant le place dans une position valide dans le segment de mémoire. Après avoir trié chaque nœud, tous les éléments satisfont la propriété du tas.Ou, allez dans la direction opposée: commencez à la fin du tableau et reculez vers l'avant. À chaque itération, vous tamisez un article jusqu'à ce qu'il soit au bon endroit.
Quelle implémentation
buildHeap
est la plus efficace?Ces deux solutions produiront un tas valide. Sans surprise, la plus efficace est la deuxième opération qui utilise
siftDown
.Soit h = log n la hauteur du tas. Le travail requis pour la
siftDown
démarche est donné par la sommeChaque terme de la somme a la distance maximale qu'un nœud à la hauteur donnée devra parcourir (zéro pour la couche inférieure, h pour la racine) multiplié par le nombre de nœuds à cette hauteur. En revanche, la somme pour appeler
siftUp
sur chaque nœud estIl doit être clair que la deuxième somme est plus importante. Le premier terme seul est hn / 2 = 1/2 n log n , donc cette approche a au mieux une complexité O (n log n) .
Comment prouver que la somme de l'
siftDown
approche est bien O (n) ?Une méthode (il existe d'autres analyses qui fonctionnent également) consiste à transformer la somme finie en une série infinie, puis à utiliser la série de Taylor. Nous pouvons ignorer le premier terme, qui est zéro:
Si vous ne savez pas pourquoi chacune de ces étapes fonctionne, voici une justification du processus en mots:
Puisque la somme infinie est exactement n , nous concluons que la somme finie n'est pas plus grande, et est donc O (n) .
Pourquoi le tri en tas nécessite-t-il O (n log n) ?
S'il est possible de s'exécuter
buildHeap
en temps linéaire, pourquoi le tri en tas requiert-il un temps O (n log n) ? Eh bien, le tri en tas se compose de deux étapes. Tout d'abord, nous faisons appelbuildHeap
au tableau, qui nécessite un temps O (n) s'il est implémenté de manière optimale. L'étape suivante consiste à supprimer à plusieurs reprises le plus gros élément du tas et à le placer à la fin du tableau. Parce que nous supprimons un élément du tas, il y a toujours un endroit ouvert juste après la fin du tas où nous pouvons stocker l'élément. Ainsi, le tri en tas atteint un ordre trié en supprimant successivement le prochain élément le plus grand et en le plaçant dans le tableau en commençant à la dernière position et en se déplaçant vers l'avant. C'est la complexité de cette dernière partie qui domine en tri par tas. La boucle ressemble à ceci:De toute évidence, la boucle s'exécute O (n) fois ( n - 1 pour être précis, le dernier élément est déjà en place). La complexité de
deleteMax
pour un tas est O (log n) . Il est généralement implémenté en supprimant la racine (le plus grand élément restant dans le tas) et en le remplaçant par le dernier élément du tas, qui est une feuille, et donc l'un des plus petits éléments. Cette nouvelle racine violera presque certainement la propriété du tas, vous devez donc appelersiftDown
jusqu'à ce que vous la remettiez dans une position acceptable. Cela a également pour effet de déplacer l'élément suivant le plus grand jusqu'à la racine. Notez que, contrairement à l'buildHeap
endroit où pour la plupart des nœuds que nous appelonssiftDown
depuis le bas de l'arbre, nous appelons maintenantsiftDown
depuis le haut de l'arbre à chaque itération!Bien que l'arbre rétrécisse, il ne rétrécit pas assez rapidement : la hauteur de l'arbre reste constante jusqu'à ce que vous ayez supprimé la première moitié des nœuds (lorsque vous supprimez complètement la couche inférieure). Ensuite, pour le trimestre suivant, la hauteur est h - 1 . Donc, le travail total pour cette deuxième étape estRemarquez le commutateur: maintenant le cas de travail zéro correspond à un seul nœud et le cas de travail h correspond à la moitié des nœuds. Cette somme est O (n log n) tout comme la version inefficace de
buildHeap
celle-ci est implémentée à l'aide de siftUp. Mais dans ce cas, nous n'avons pas le choix, car nous essayons de trier et nous exigeons que le prochain élément le plus grand soit supprimé.En résumé, le travail pour le tri de tas est la somme des deux étapes: temps O (n) pour buildHeap et O (n log n) pour supprimer chaque nœud dans l'ordre , donc la complexité est O (n log n) . Vous pouvez prouver (en utilisant certaines idées de la théorie de l'information) que pour un tri basé sur la comparaison, O (n log n) est le meilleur que vous puissiez espérer de toute façon, donc il n'y a aucune raison d'être déçu par cela ou de s'attendre à ce que le tri en tas atteigne le O (n) limité dans le temps
buildHeap
.la source
siftUp
chaque élément, soit en commençant à la fin en reculant et en appelantsiftDown
. Quelle que soit l'approche choisie, vous sélectionnez l'élément suivant dans la partie non triée du tableau et effectuez l'opération appropriée pour le déplacer dans une position valide dans la partie ordonnée du tableau. La seule différence est la performance.Votre analyse est correcte. Cependant, ce n'est pas serré.
Il n'est pas vraiment facile d'expliquer pourquoi la construction d'un tas est une opération linéaire, il vaut mieux la lire.
Une grande analyse de l'algorithme peut être vue ici .
L'idée principale est que dans l'
build_heap
algorithme, leheapify
coût réel n'est pasO(log n)
pour tous les éléments.Lorsque
heapify
est appelé, le temps d'exécution dépend de la distance à laquelle un élément peut descendre dans l'arborescence avant la fin du processus. En d'autres termes, cela dépend de la hauteur de l'élément dans le tas. Dans le pire des cas, l'élément peut descendre jusqu'au niveau de la feuille.Comptons le travail effectué niveau par niveau.
Au niveau le plus bas, il y a des
2^(h)
nœuds, mais nous n'en appelonsheapify
aucun, donc le travail est 0. Au niveau suivant, il y a des2^(h − 1)
nœuds, et chacun peut descendre d'un niveau. Au 3e niveau à partir du bas, il y a des2^(h − 2)
nœuds, et chacun peut descendre de 2 niveaux.Comme vous pouvez le voir, toutes les opérations de tas ne le sont pas
O(log n)
, c'est pourquoi vous obtenezO(n)
.la source
Heapify
est uneO(n)
fois terminé,siftDown
maisO(n log n)
terminésiftUp
. Le tri réel (extraire les éléments du tas un par un) doit donc être fait de la mêmesiftUp
façonO(n log n)
.i
partir du bas d'un arbre de hauteur h doivent également faire des2* log(h-i)
comparaisons et doivent également être prises en compte @ The111. Qu'est-ce que tu penses?Intuitivement:
Pas assez. Votre logique ne produit pas de limite stricte - elle surestime la complexité de chaque tas. Si elle est construite de bas en haut, l'insertion (heapify) peut être bien inférieure à
O(log(n))
. Le processus est le suivant:(Étape 1) Les premiers
n/2
éléments vont sur la ligne inférieure du tas.h=0
, donc heapify n'est pas nécessaire.(Étape 2) Les éléments suivants vont sur la ligne 1 en partant du bas. , tassez les filtres 1 niveau plus bas.
n/22
h=1
(Étape i ) Les éléments suivants vont en ligne à partir du bas. , heapify filtre les niveaux vers le bas.
n/2i
i
h=i
i
(Étape log (n) ) Le dernier élément monte en ligne à partir du bas. , heapify filtre les niveaux vers le bas.
n/2log2(n) = 1
log(n)
h=log(n)
log(n)
AVIS: après la première étape,
1/2
les éléments(n/2)
sont déjà dans le tas, et nous n'avons même pas eu besoin d'appeler heapify une fois. Notez également qu'un seul élément, la racine, engendre en fait toute lalog(n)
complexité.Théoriquement:
Les étapes totales
N
pour créer un tas de taillen
peuvent être écrites mathématiquement.En hauteur
i
, nous avons montré (ci-dessus) qu'il y aura des éléments qui devront appeler heapify, et nous savons que heapify en hauteur l' est . Cela donne:n/2i+1
i
O(i)
La solution à la dernière sommation peut être trouvée en prenant la dérivée des deux côtés de l'équation de série géométrique bien connue:
Enfin, la connexion à
x = 1/2
l'équation ci-dessus donne2
. Brancher ceci dans la première équation donne:Ainsi, le nombre total d'étapes est de taille
O(n)
la source
Ce serait O (n log n) si vous construisiez le tas en insérant à plusieurs reprises des éléments. Cependant, vous pouvez créer un nouveau tas plus efficacement en insérant les éléments dans un ordre arbitraire puis en appliquant un algorithme pour les "tas" dans le bon ordre (en fonction du type de tas bien sûr).
Voir http://en.wikipedia.org/wiki/Binary_heap , "Construire un tas" pour un exemple. Dans ce cas, vous travaillez essentiellement à partir du niveau inférieur de l'arborescence, en échangeant les nœuds parent et enfant jusqu'à ce que les conditions du segment soient remplies.
la source
Il y a déjà de bonnes réponses mais j'aimerais ajouter une petite explication visuelle
Maintenant, regardez l'image, il y a
n/2^1
des nœuds verts avec une hauteur 0 (ici 23/2 = 12)n/2^2
des nœuds rouges avec une hauteur 1 (ici 23/4 = 6)n/2^3
un nœud bleu avec une hauteur 2 (ici 23/8 = 3)n/2^4
nœuds violets de hauteur 3 (ici 23/16 = 2)donc il y a des
n/2^(h+1)
nœuds pour la hauteur hPour trouver la complexité temporelle, nous pouvons compter la quantité de travail effectuée ou le nombre maximum d'itérations effectuées par chaque nœud
maintenant, on peut remarquer que chaque nœud peut effectuer (au plus) des itérations == hauteur du nœud
donc pour tous les nœuds de hauteur h, le travail maximum effectué est n / 2 ^ (h + 1) * h
Maintenant, le travail total est
maintenant pour toute valeur de h , la séquence
ne dépassera jamais 1
Ainsi, la complexité temporelle ne dépassera jamais O (n) pour la construction du tas
la source
Comme nous savons que la hauteur d'un tas est log (n) , où n est le nombre total d'éléments. Représentons-le comme h
Lorsque nous effectuons une opération de tas, les éléments du dernier niveau ( h ) ne bougeront même pas un seul étape.
Le nombre d'éléments à l'avant-dernier niveau ( h-1 ) est de 2 h-1 et ils peuvent se déplacer à max 1 niveau (pendant le tas).
De même, pour le i e , niveau nous avons 2 i éléments qui peuvent se déplacer salut positions.
Donc nombre total de mouvements = S = 2 h * 0 + 2 h-1 * 1 + 2 h-2 * 2 + ... 2 0 * h
S = 2 h {1/2 + 2/2 2 + 3/2 3 + ... h / 2 h } ----------------------- -------------------------- 1
il s'agit de la série AGP , pour résoudre cette division des deux côtés par 2
S / 2 = 2 h {1/2 2 + 2/2 3 + ... h / 2 h + 1 } --------------------------------- ---------------- 2 en
soustrayant l'équation 2 de 1 donne
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 }
maintenant 1/2 / 1 + 2 2 + 1/2 3 + ... + 1/2 h diminue GP dont la somme est inférieure à 1 (quand h tend vers l'infini, la somme tend vers 1). Dans une analyse plus approfondie, prenons une limite supérieure sur la somme qui est 1.
Cela donne S = 2 h + 1 {1 + h / 2 h + 1 }
= 2 h + 1 + h
~ 2 h + h
comme h = log (n) , 2 h = n
Donc S = n + log (n)
T (C) = O (n)
la source
Lors de la construction d'un tas, disons que vous adoptez une approche ascendante.
la source
En cas de construction du tas, on part de la hauteur, logn -1 (où logn est la hauteur de l'arbre de n éléments). Pour chaque élément présent à la hauteur «h», nous descendons jusqu'à la hauteur maximale (logn -h).
la source
Les insertions successives peuvent être décrites par:
Par approximation étourneau
n! =~ O(n^(n + O(1)))
, doncT =~ O(nlog(n))
J'espère que cela vous aidera, la façon optimale
O(n)
est d'utiliser l'algorithme de tas de construction pour un ensemble donné (l'ordre n'a pas d'importance).la source
Fondamentalement, le travail est effectué uniquement sur les nœuds non-feuilles lors de la construction d'un tas ... et le travail effectué est la quantité de swaps vers le bas pour satisfaire la condition de tas ... en d'autres termes (dans le pire des cas), la quantité est proportionnelle à la hauteur du nœud ... dans l'ensemble la complexité du problème est proportionnelle à la somme des hauteurs de tous les nœuds non-feuilles..qui est (2 ^ h + 1 - 1) -h-1 = nh-1 = Sur)
la source
@bcorso a déjà démontré la preuve de l'analyse de complexité. Mais pour le bien de ceux qui apprennent encore l'analyse de complexité, j'ai ceci à ajouter:
La base de votre erreur d'origine est due à une mauvaise interprétation de la signification de la déclaration, "l'insertion dans un segment de mémoire prend du temps O (log n)". L'insertion dans un tas est bien O (log n), mais vous devez reconnaître que n est la taille du tas lors de l'insertion .
Dans le contexte de l'insertion de n objets dans un tas, la complexité de la ième insertion est O (log n_i) où n_i est la taille du tas comme à l'insertion i. Seule la dernière insertion a une complexité de O (log n).
la source
Supposons que vous ayez N éléments dans un tas. Alors sa hauteur serait Log (N)
Maintenant , vous voulez insérer un autre élément, la complexité serait la suivante: Log (N) , nous devons comparer tout le chemin UP à la racine.
Vous avez maintenant N + 1 éléments et height = Log (N + 1)
En utilisant la technique d' induction , il peut être prouvé que la complexité de l'insertion serait ∑logi .
Maintenant en utilisant
Cela simplifie: ∑logi = log (n!)
qui est en fait O (NlogN)
Mais
nous faisons quelque chose de mal ici, car dans tous les cas, nous n'atteignons pas le sommet. Par conséquent, lors de l'exécution la plupart du temps, nous pouvons constater que nous n'allons même pas à mi-chemin dans l'arbre. D'où, cette limite peut être optimisée pour avoir une autre limite plus stricte en utilisant les mathématiques données dans les réponses ci-dessus.
Cette réalisation m'est venue après un détail et une expérimentation sur Heaps.
la source
J'aime vraiment l'explication de Jeremy West .... une autre approche qui est vraiment facile à comprendre est donnée ici http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity
depuis, buildheap dépend de l'utilisation dépend de heapify et l'approche de rétrogradation est utilisée qui dépend de la somme des hauteurs de tous les nœuds. Donc, pour trouver la somme de la hauteur des nœuds qui est donnée par S = sommation de i = 0 à i = h de (2 ^ i * (hi)), où h = logn est la hauteur de l'arbre à résoudre s, nous obtenons s = 2 ^ (h + 1) - 1 - (h + 1) puisque, n = 2 ^ (h + 1) - 1 s = n - h - 1 = n- logn - 1 s = O (n), et donc la complexité du buildheap est O (n).
la source
"La limite de temps linéaire de la génération de tas, peut être affichée en calculant la somme des hauteurs de tous les nœuds du tas, qui est le nombre maximal de lignes en pointillés. Pour l'arbre binaire parfait de hauteur h contenant N = 2 ^ ( h + 1) - 1 nœuds, la somme des hauteurs des nœuds est N - H - 1. Ainsi, c'est O (N). "
la source
Preuve d'O (n)
La preuve n'est pas sophistiquée, et assez simple, je n'ai prouvé que le cas pour un arbre binaire complet, le résultat peut être généralisé pour un arbre binaire complet.
la source
Nous obtenons le temps d'exécution pour la construction du tas en déterminant le déplacement maximum que chaque nœud peut prendre. Nous devons donc savoir combien de nœuds sont dans chaque ligne et jusqu'où peut aller chaque nœud.
En partant du nœud racine, chaque ligne suivante a le double des nœuds que la ligne précédente, donc en répondant à quelle fréquence pouvons-nous doubler le nombre de nœuds jusqu'à ce qu'il ne reste plus de nœuds, nous obtenons la hauteur de l'arbre. Ou en termes mathématiques, la hauteur de l'arbre est log2 (n), n étant la longueur du tableau.
Pour calculer les nœuds dans une ligne, nous partons de l'arrière, nous savons que n / 2 nœuds sont en bas, donc en divisant par 2 nous obtenons la ligne précédente et ainsi de suite.
Sur cette base, nous obtenons cette formule pour l'approche Siftdown: (0 * n / 2) + (1 * n / 4) + (2 * n / 8) + ... + (log2 (n) * 1)
Le terme dans la dernière paranthèse est la hauteur de l'arbre multipliée par le nœud qui se trouve à la racine, le terme dans la première paranthèse correspond à tous les nœuds de la rangée du bas multipliés par la longueur qu'ils peuvent parcourir, 0. Même formule en smart:
En ramenant le n, nous avons 2 * n, 2 peut être écarté car c'est une constante et nous avons le pire cas d'exécution de l'approche Siftdown: n.
la source
pensez que vous faites une erreur. Jetez un œil à ceci: http://golang.org/pkg/container/heap/ Construire un tas n'est pas O (n). Cependant, l'insertion est O (lg (n). Je suppose que l'initialisation est O (n) si vous définissez une taille de segment b / c, le segment doit allouer de l'espace et configurer la structure de données. Si vous avez n éléments à placer dans le tas alors oui, chaque insert est lg (n) et il y a n éléments, donc vous obtenez n * lg (n) comme indiqué u
la source