Comment la construction d'un tas peut-elle être une complexité temporelle O(n)?
Quelqu'un peut-il aider à expliquer comment la construction d'un tas peut-elle être une complexité O(n)?
L'insertion d'un élément dans un tas est O(log n)
, et l'insertion est répétée n / 2 fois (le reste sont des feuilles et ne peut pas violer la propriété heap). 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 manque?
14 réponses
Je pense qu'il y a plusieurs questions enfouies dans ce sujet:
- Comment implémentez-vous buildHeap pour qu'il s'exécute dans O(N) Temps?
- Comment montrez-vous que buildHeap s'exécute dans O (N) Temps lorsqu'il est implémenté correctement?
- pourquoi cette même logique ne fonctionne-t-elle pas pour que le tri de tas s'exécute dans O(N) time plutôt que O(N log n)?
Souvent, les réponses à ces questions se concentrent sur la différence entre siftUp
et siftDown
. Faire de la bon choix entre les siftUp
et siftDown
est essentiel pour obtenir O(n) performance pour buildHeap
, mais ne fait rien pour aider à comprendre la différence entre buildHeap
et heapSort
en général. En effet, les implémentations appropriées de buildHeap
et heapSort
n'utiliseront que siftDown
. L'opération siftUp
n'est nécessaire que pour effectuer des insertions dans un tas existant, elle serait donc utilisée pour implémenter une file d'attente prioritaire en utilisant un tas binaire, par exemple.
J'ai écrit ceci pour décrire comment un tas max travail. C'est le type de tas généralement utilisé pour le tri de tas 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; il suffit de changer l'ordre de tri.
La propriété heap spécifie que chaque nœud d'un tas binaire doit être au moins aussi grand que ses deux enfants. En particulier, cela implique que le plus grand élément dans le tas est à la racine. Le criblage et le criblage sont essentiellement la même opération dans des directions opposées: déplacez un nœud incriminé jusqu'à ce qu'il satisfasse la propriété heap:
-
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.
Le nombre d'opérations requises pour siftDown
et siftUp
est proportionnel à la distance que le nœud peut avoir à déplacer. Pour siftDown
, c'est la distance du bas de l'arbre, donc siftDown
est coûteux pour les nœuds en haut de l'arbre. Avec siftUp
, le travail est proportionnel à la distance du haut de l'arbre, donc siftUp
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 alors 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 pour chaque nœud, nous préférons siftDown
sur siftUp
.
La fonction buildHeap
prend un tableau d'éléments non triés et les déplace jusqu'à ce qu'ils satisfassent tous la propriété heap, produisant ainsi un tas valide. Il y a deux approches que l'on peut prendre pour buildHeap
en utilisant les opérations siftUp
et siftDown
que nous avons décrites.
Commencez en haut du tas (le début du tableau) et appelez
siftUp
sur chaque élément. À chaque étape, les éléments précédemment tamisés (les éléments avant l'élément en cours dans le tableau) forment un tas valide, et le tamisage de l'élément suivant le place dans une position valide dans le tas. Après avoir tamisé chaque nœud, tous les éléments satisfont la propriété heap.Ou, allez dans la direction opposée: commencez à la fin du tableau et reculez vers l'avant. À chaque itération, vous tamisez un élément jusqu'à ce qu'il soit dans le emplacement correct.
Ces deux solutions produiront un tas valide. La question Est: quelle implémentation pour buildHeap
est la plus efficace? Sans surprise, c'est la deuxième opération qui utilise siftDown
.
Soit h = log n représente la hauteur du tas. Le travail requis pour l'approche siftDown
est donné par la somme
(0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).
Chaque terme de la somme a la distance maximale qu'un nœud à la hauteur donnée devra déplacer (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 est
(h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).
, Il devrait être clair que la deuxième somme est plus grande. Le premier terme seul est hn / 2 = 1/2 N log n , donc cette approche a au mieux de la complexité O (N log n) . Mais comment prouver que la somme pour l'approche siftDown
est bien O (N) ? Une méthode (il y a d'autres analyses qui fonctionnent également) consiste à transformer la somme finie en une série infinie et puis utilisez la série Taylor. Nous pouvons ignorer le premier terme, qui est zéro:
Si vous n'êtes pas sûr de savoir pourquoi chacune de ces étapes fonctionne, voici une justification pour le processus en mots:
- les termes sont tous positifs, donc la somme finie doit être plus petite que la somme infinie.
- la série est égale à une série de puissance évaluée à x = 1/2 .
- cette série de puissance est égale à (un temps constant) la dérivée du Taylor série de f(x)=1/(1-x).
- x = 1/2 est dans l'intervalle de convergence de cette série de Taylor.
- par conséquent, nous pouvons remplacer la série Taylor par 1/(1-x) , différencier et évaluer pour trouver la valeur de la série infinie.
Comme la somme infinie est exactement n, nous concluons que les limites de la somme n'est pas plus grande, et est donc, O(n).
La question suivante est: s'il est possible d'exécuter buildHeap
en temps linéaire, pourquoi le tri de tas nécessite-t-il O(N log n) time? Eh bien, tas tri se compose de deux étapes. Tout d'abord, nous appelons buildHeap
sur le tableau, ce qui nécessite O(N) Temps s'il est implémenté de manière optimale. L'étape suivante consiste à supprimer à plusieurs reprises le plus grand é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. Donc tas sort réalise un ordre trié par successivement enlever le prochain plus grand élément et le mettre dans le tableau à partir de la dernière position et se déplaçant vers l'avant. C'est la complexité de cette dernière partie qui domine dans le tri de tas. La boucle ressemble à ceci:
for (i = n - 1; i > 0; i--) {
arr[i] = deleteMax();
}
Clairement, la boucle s'exécute en O(n) fois (n - 1, pour être précis, le dernier élément est déjà en place). La complexité de deleteMax
pour un segment est O(log n). Il est généralement implémenté en supprimant la racine (le plus grand élément laissé dans le tas) et le remplacer 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é heap, vous devez donc appeler siftDown
jusqu'à ce que vous la replaciez dans une position acceptable. Cela a également pour effet de déplacer le prochain élément le plus important jusqu'à la racine. Notez que, contrairement à buildHeap
où pour la plupart des nœuds nous appelons {[5] } depuis le bas de l'arbre, nous appelons maintenant siftDown
depuis le haut de l'arbre à chaque itération! bien que l'arbre rétrécit, il ne rétrécit pas assez vite: La hauteur de l'arbre reste constante jusqu'à ce que vous ayez supprimé la première moitié des nœuds (lorsque vous effacez 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 est
h*n/2 + (h-1)*n/4 + ... + 0 * 1.
Notez 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
qui est implémentée en utilisant siftUp. Mais dans ce cas, nous n'avons pas le choix puisque nous essayons de trier et nous exigeons que le prochain élément le plus important soit retiré ensuite.
En résumé, le travail pour le tri de tas est la somme des deux étapes: O (N) temps 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 quelques idées de la théorie de l'information) que pour une comparaison basée sur sort, O (N log n) est le meilleur que vous pourriez espérer de toute façon, donc il n'y a aucune raison d'être déçu par cela ou d'attendre que heap sort atteigne le délai O(N) que buildHeap
fait.
Votre analyse est correcte. Cependant, il n'est pas serré.
Il n'est pas vraiment facile d'expliquer pourquoi construire un tas est une opération linéaire, vous devriez mieux le lire.
Un excellente analyse de l'algorithme peut être vu ici.
L'idée principale est que dans le build_heap
algorithme de la réelle heapify
coût n'est pas O(log n)
pour tous les éléments.
Lorsque heapify
est appelé, le temps d'exécution dépend de la distance d'un élément pouvant descendre dans l'arborescence avant le processus se termine. En d'autres termes, il dépend de la hauteur de l'élément dans le tas. Dans le pire des cas, l'élément peut aller jusqu'au niveau de la feuille.
Comptons le travail effectué niveau par niveau.
Au niveau le plus bas, il y a des nœuds 2^(h)
, mais nous n'appelons pas heapify
sur aucun d'entre eux, donc le travail est 0. Au niveau suivant, il y a des nœuds 2^(h − 1)
, et chacun peut descendre de 1 niveau. Au 3ème niveau à partir du bas, il y a des nœuds 2^(h − 2)
, et chacun peut descendre de 2 niveaux.
Comme vous pouvez le voir, pas toutes les heapify opérations O(log n)
, c'est pourquoi vous obtenez O(n)
.
Intuitivement:
"la complexité devrait être O (nLog n)... 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)."
Pas tout à fait. Votre logique ne produit pas une limite étroite-elle surestime la complexité de chaque heapify. 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 ) L' les premiers éléments n/2
vont sur la rangée du bas du tas. h=0
, donc heapify n'est pas nécessaire.
( Étape 2 ) les éléments n/22
suivants vont sur la rangée 1 à partir du bas. h=1
, heapify filtres 1 niveau vers le bas.
( Étape , je )
les éléments n/2i
suivants vont dans la rangée i
en haut à partir du bas. h=i
, heapify filtres i
niveaux vers le bas.
( Étape log(n) ) La dernière n/2log2(n) = 1
élément va à la ligne log(n)
par le bas. h=log(n)
, heapify filtres log(n)
niveaux vers le bas.
Notez: {[35] } qu'après la première étape, {[13] } des éléments (n/2)
sont déjà dans le tas, et nous n'avons même pas besoin d'appeler heapify une fois. Notez également que seul un seul élément, la racine, encourt la complexité log(n)
complète.
Théoriquement:
Les étapes totales N
pour construire un tas de taille n
, peuvent être écrites mathématiquement.
En hauteur i
, nous avons montré (ci-dessus) qu'il y aura des éléments n/2i+1
qui doivent appeler heapify, et nous savons que heapify à la hauteur i
est O(i)
. Cela donne:
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 la série géométrique bien connue:
Enfin, brancher x = 1/2
dans l'équation ci-dessus donne 2
. Brancher ceci dans la première équation donne:
Ainsi, le nombre total d'étapes est de taille O(n)
Ce serait O (N log n) Si vous construisiez le tas en insérant plusieurs é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 "heapify" 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'arbre, en échangeant parent et enfant nœuds jusqu'à ce que les conditions de tas soient remplies.
Lors de la construction d'un tas, disons que vous adoptez une approche ascendante.
- vous prenez chaque élément et le comparez avec ses enfants pour vérifier si la paire est conforme aux règles de tas. Si, par conséquent, les feuilles incluses dans le tas gratuitement. C'est parce qu'ils n'ont pas d'enfants.
- en se déplaçant vers le haut, le pire des cas pour le nœud juste au-dessus des feuilles serait 1 comparaison (au maximum, ils seraient comparés à une seule génération d'enfants)
- aller plus loin en haut, leurs parents immédiats peuvent au maximum être comparés à deux générations d'enfants.
- en continuant dans la même direction, vous aurez des comparaisons log(n) Pour la racine dans le pire des cas. et log(n)-1 pour ses enfants immédiats, log(n)-2 pour leurs enfants immédiats et ainsi de suite.
- Donc, en résumé, vous arrivez sur quelque chose comme log(n) + {log(n)-1}*2 + {log(n)-2}*4 + ..... + 1 * 2^{(logn) -1} qui n'est rien D'autre que O (n).
Comme nous le savons, la hauteur d'un segment est log(n), où n est le nombre total d'éléments.Permet de la représenter comme h
Lorsque nous effectuons une opération heapify, les éléments du dernier niveau ( h) ne bougeront même pas d'une seule étape.
, Le nombre d'éléments à l'avant-dernier niveau(h-1) est 2h-1 et ils peuvent se déplacer à max 1 niveau(cours de heapify).
de Même, pour les jeth, niveau que nous avons 2j' les éléments qui peuvent se déplacer d' h-i positions.
, par conséquent, nombre total de coups=S= 2h*0+2h-1*1+2h-2*2+...20*h
S=2h{1/2 + 2/22+ 3/23+ ... h/2h} -------------------------------------------------1
c'est la série AGP, pour résoudre cette division des deux côtés par 2
S/2=2h{1/22+ 2/23+ ... h/2h+1} -------------------------------------------------2
en soustrayant l'équation 2 à partir de 1 donne
S/2=2h{1/2+1/22+ 1/23+ ...+1/2h+ h/2h+1}
S=2h+1{1/2+1/22+ 1/23+ ...+1/2h+ h/2h+1}
maintenant 1/2+1/22+ 1/23+ ...+1/2h est la diminution de GP dont la somme est inférieure à 1 (lorsque 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=2h+1{1+h/2h+1}
=2h+1+h
~2h+h
comme h=log(n), 2h=n
Donc S=n+log(n)
T(C)=O(n)
En cas de construction du tas, nous partons 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 allons à max jusqu'à (logn-h) Hauteur vers le bas.
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)
Les insertions successives peuvent être décrites par:
T = O(log(1) + log(2) + .. + log(n)) = O(log(n!))
Par approximation d'étourneau, n! =~ O(n^(n + O(1)))
, donc T =~ O(nlog(n))
J'espère que cela aide, la manière optimale O(n)
utilise l'algorithme de tas de construction pour un ensemble donné (l'ordre n'a pas d'importance).
@bcorso a déjà démontré la preuve de l'analyse de complexité. Mais pour ceux qui apprennent encore l'analyse de la complexité, j'ai ceci à ajouter:
La base de votre erreur initiale est due à une mauvaise interprétation de la signification de l'instruction, "l'insertion dans un tas prend du temps O(log n)". L'Insertion dans un tas est en effet 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).
J'aime beaucoup les explications 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 depends en utilisant dépend de heapify et shiftdown approche 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 = somme de i = 0 à i = h (2^i*(h-i)), où h = logn est la hauteur de la arbre la résolution de s, on obtient s = 2^(h+1) - 1 - (h+1) depuis, n = 2^(h+1) - 1 s = n - h - 1 = n - logn - 1 s = O(n), et donc la complexité de buildheap est O(n).
" la limite temporelle linéaire du tas de construction, peut être affichée en calculant la somme des hauteurs de tous les nœuds du tas, qui est le nombre maximum de lignes pointillées. Pour le parfait arbre binaire de hauteur h contenant N = 2^(h+1) – 1 nœuds, la somme des hauteurs des nœuds est N – H – 1. Il est donc O(N)."
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é d'échange vers le bas pour satisfaire tas condition...in d'autres termes (dans le pire des cas), la quantité est proportionnelle à la hauteur du nœud...dans l'ensemble de la complexité du problème est proportionnelle à la somme des hauteurs de tous les non-nœuds feuilles..qui est (2^h + 1-1)-h-1=n-h-1= O(n)
La preuve n'est pas fantaisiste, et assez simple, j'ai seulement prouvé le cas pour un arbre binaire complet, le résultat peut être généralisé pour un arbre binaire complet.
Pensez que vous faites une erreur. Jetez un oeil à 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 tas b / c, le tas doit allouer de l'espace et configurer la structure de données. Si vous avez n éléments à mettre 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é