web-dev-qa-db-fra.com

Comment peut-on construire un tas O(n) la complexité du temps?

Quelqu'un peut-il aider à expliquer comment la construction d'un tas peut être complexe O(n)? 

L'insertion d'un élément dans un tas correspond à 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 tas). Donc, cela signifie que la complexité devrait être O(n log n), je pense.

En d'autres termes, pour chaque élément que nous "clous", il peut potentiellement être filtré une fois pour chaque niveau du tas jusqu'à présent (c'est-à-dire log n level).

Qu'est-ce que je rate?

351
GBa

Je pense qu'il y a plusieurs questions cachées dans ce sujet:

  • Comment implémentez-vous buildHeap pour qu'il s'exécute dans le temps O (n)?
  • Comment montrez-vous que buildHeap s'exécute dans le temps O (n) lorsqu'il est correctement implémenté?
  • Pourquoi cette même logique ne fonctionne-t-elle pas pour faire en sorte que le tri de tas s'exécute dans le temps O (n) plutôt que O (n log n)?

Comment implémentez-vous buildHeap pour qu'il s'exécute dans le temps O (n)?

Souvent, les réponses à ces questions portent sur la différence entre siftUp et siftDown. Faire le bon choix entre siftUp et siftDown est essentiel pour obtenir O (n) performances 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 de heapSort utiliseront seulement et siftDown. L'opération siftUp n'est nécessaire que pour effectuer des insertions dans un segment de mémoire existant. Elle serait donc utilisée pour implémenter une file d'attente prioritaire à l'aide d'un segment de mémoire binaire, par exemple.

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 de priorités où les valeurs les 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 tas 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 l'élément le plus volumineux du tas se trouve à la racine. Les analyses vers le bas et vers le haut 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é de segment de mémoire:

  • siftDown échange un noeud 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 noeuds en dessous.
  • siftUp échange un noeud trop volumineux avec son parent (le déplaçant ainsi vers le haut) jusqu'à ce qu'il ne dépasse pas le noeud situé au-dessus de lui.

Le nombre d'opérations requises pour siftDown et siftUp est proportionnel à la distance que le nœud peut avoir à parcourir. Pour siftDown, il s’agit de la distance au bas de l’arbre, donc siftDown est coûteux pour les nœuds situés en haut de l’arbre. Avec siftUp, le travail est proportionnel à la distance qui le sépare du sommet de l’arbre. Ainsi, siftUp coûte cher pour les nœuds situés au bas de l’arbre. Bien que les deux opérations soient O (log n) dans le pire des cas, dans un segment de mémoire, un seul nœud se trouve 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 devions appliquer une opération à chaque nœud, nous préférerions siftDown à 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 existe deux approches possibles pour buildHeap à l'aide des opérations siftUp et siftDown que nous avons décrites.

  1. 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 criblés (les éléments précédant l'élément en cours dans le tableau) forment un segment valide et le fait de tamiser le prochain élément le place dans une position valide dans le segment. Après avoir passé au crible chaque nœud, tous les éléments satisfont à la propriété heap.

  2. Ou bien, allez dans le sens opposé: commencez à la fin du tableau et reculez vers l’avant. À chaque itération, vous filtrez un élément jusqu'à ce qu'il se trouve au bon emplacement.

Quelle implémentation pour buildHeap est 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 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 correspond à 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, de sorte que cette approche a au mieux une complexité O (n log n).

Comment pouvons-nous prouver que la somme pour la méthode siftDown 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. On peut ignorer le premier terme, qui est zéro:

Taylor series for buildHeap complexity

Si vous ne savez pas pourquoi chacune de ces étapes fonctionne, voici une justification en quelques mots:

  • Les termes étant tous positifs, la somme finie doit être inférieure à la somme infinie.
  • La série est égale à une série de puissance évaluée à x = 1/2.
  • Cette série de puissances est égale à (une fois constante) la dérivée de la série de Taylor pour f (x) = 1/(1-x).
  • x = 1/2 se situe dans l'intervalle de convergence de cette série de Taylor.
  • Par conséquent, nous pouvons remplacer la série de Taylor par 1/(1-x), différencier et évaluer pour trouver la valeur de la série infinie.

Puisque la somme infinie est exactement n, nous concluons que la somme finie n’est pas plus grande et qu’elle est donc O (n).

Pourquoi le tri de tas nécessite-t-il le temps O (n log n)?

S'il est possible d'exécuter buildHeap en temps linéaire, pourquoi le tri par tas nécessite-t-il O (n log n) temps? Eh bien, le tri en tas consiste en deux étapes. Premièrement, nous appelons buildHeap sur le tableau, ce qui nécessite O (n) temps s'il est mis en œuvre 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. Comme nous supprimons un élément du tas, il y a toujours un espace libre juste après la fin du tas où nous pouvons stocker l'élément. Ainsi, le tri par tas réalise un ordre trié en supprimant successivement le prochain élément le plus grand et en le plaçant dans le tableau en commençant par la dernière position et en se dirigeant vers l'avant. C'est la complexité de cette dernière partie qui domine dans le tri en tas. La boucle ressemble à ceci:

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

Clairement, la boucle 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 segment de mémoire 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 la remplaçant par le dernier élément du tas, qui est une feuille, et par conséquent 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 à la racine. Notez que contrairement à buildHeap où, pour la plupart des nœuds, nous appelons siftDown du bas de l'arbre, nous appelons maintenant siftDown du haut de l'arbre à chaque itération. ! Bien que l'arbre soit en train de rétrécir, il ne le fait 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 le calque du bas). Ensuite, pour le prochain trimestre, la hauteur est de h - 1. Donc, le travail total pour cette deuxième étape est

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

Remarquez le commutateur: le cas de travail zéro correspond maintenant à un seul noeud et le cas de travail h correspond à la moitié des noeuds. Cette somme est O (n log n) tout comme la version inefficace de buildHeap mise en œuvre avec siftUp. Mais dans ce cas, nous n’avons pas le choix, car nous essayons de trier et nous demandons que l’élément le plus volumineux suivant soit supprimé.

En résumé, le travail de tri de tas correspond à la somme des deux étapes: O (n) fois pour buildHeap et O (n log n) pour supprimer chaque nœud dans l’ordre , la complexité est donc O (n log n). Vous pouvez prouver (en utilisant certaines idées de la théorie de l'information) que pour un tri fondé sur la comparaison, O (n log n) est le meilleur que vous puissiez espérer de toute façon, il n'y a donc aucune raison d'être déçu par cela. ou attendez-vous que le tri par tas atteigne le O(n) délai que buildHeap accomplit.

321
Jeremy West

Votre analyse est correcte. Cependant, ce n'est pas serré. 

Il n’est pas facile d’expliquer pourquoi la construction d’un tas est une opération linéaire, vous devriez mieux le lire.

Une grande analyse de l'algorithme peut être vue ici .


L'idée principale est que, dans l'algorithme build_heap, le coût réel de heapify n'est pas O(log n) pour tous les éléments.

Lorsque heapify est appelé, la durée d'exécution dépend de la distance qu'un élément peut atteindre 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. 

Laissez-nous compter le travail effectué niveau par niveau.

Au niveau le plus bas, il y a 2^(h)node, mais nous n'appelons pas heapify, le travail est donc égal à 0. Au niveau immédiatement supérieur, il existe des nœuds 2^(h − 1) et chacun peut descendre d'un niveau. Au 3ème niveau à partir du bas, il y a des nœuds 2^(h − 2), chacun pouvant descendre de 2 niveaux.

Comme vous pouvez le constater, toutes les opérations heapify sont des O(log n), c’est pourquoi vous obtenez O(n).

286
emre nevayeshirazi

Intuitivement:

"La complexité devrait être O (nLog n) ... pour chaque élément que nous" heapifions ", il peut potentiellement être filtré une fois pour chaque niveau du segment de mémoire jusqu'à présent (c'est-à-dire les niveaux de journalisation)."

Pas assez. Votre logique ne produit pas de limite étroite - elle surestime la complexité de chaque heapify. Si construit de bas en haut, l'insertion (heapify) peut être beaucoup moins que O(log(n)). Le processus est le suivant:

(Étape 1) Les premiers éléments n/2 vont sur la dernière ligne du tas. h=0, donc Heapify n'est pas nécessaire.

(Étape 2) Les éléments n/22 suivants apparaissent sur la ligne 1 en partant du bas. h=1, filtre heapify 1 niveau vers le bas.

(Étape i) Les éléments n/2i suivants apparaissent dans la ligne i à partir du bas. h=i, heapify filter i level down.

(Étape log (n)) Le dernier élément n/2log2(n) = 1 apparaît dans la ligne log(n) à partir du bas. h=log(n), filtre heapify log(n)

AVIS: qu'après la première étape, 1/2 des éléments (n/2) sont déjà dans le tas, et nous n'avons même pas eu besoin d'appeler heapify une fois. En outre, notez que seul un élément, la racine, implique la complexité complète de log(n).


Théoriquement:

Le nombre total d'étapes N pour créer un segment de taille n peut être écrit mathématiquement. 

À 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 à hauteur i est O(i). Cela donne:

enter image description here

La solution à la dernière somme peut être trouvée en prenant la dérivée des deux côtés de l’équation bien connue des séries géométriques:

enter image description here

Finalement, en branchant x = 1/2 dans l'équation ci-dessus, on obtient 2. Brancher ceci dans la première équation donne:

enter image description here

Ainsi, le nombre total d'étapes est de taille O(n)

83
bcorso

Ce serait O (n log n) si vous construisiez le tas en insérant plusieurs fois des éléments. Toutefois, vous pouvez créer un nouveau segment de mémoire plus efficacement en insérant les éléments dans un ordre arbitraire, puis en appliquant un algorithme pour les "hiérarchiser" dans le bon ordre (en fonction du type de segment de mémoire, 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 bas de l'arborescence, en échangeant les nœuds parent et enfant jusqu'à ce que les conditions de tas soient remplies. 

32
mike__t

Comme nous le savons, la hauteur d'un tas est log (n), où n est le nombre total d'éléments. Représentons-le sous la forme h
Lorsque nous effectuons une opération heapify, les éléments du dernier niveau (h) ne bougent même pas.
Le nombre d'éléments à l'avant dernier niveau (h-1) est 2h-1 et ils peuvent se déplacer au maximum 1 niveau (pendant heapify). 
De même, pour le ith, niveau nous avons 2je éléments pouvant déplacer h-i positions.

Donc 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
ceci est AGP série, pour résoudre cette division des 2 côtés par 2
S/2 = 2h {1/22 + 2/23+ ... h/2h + 1} --------------------------------------------- ---- 2
soustrayant l’équation 2 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 décroissante GP dont la somme est inférieure à 1 (lorsque h tend vers l'infini, la somme tend vers 1). Dans une analyse plus poussée, 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

Par conséquent, S = n + log (n)
T (C) = O (n)

7
Tanuj Yadav

Tout en construisant un tas, disons que vous prenez une approche ascendante.

  1. Vous prenez chaque élément et comparez-le avec ses enfants pour vérifier si la paire est conforme aux règles du tas. Les feuilles sont donc incluses gratuitement dans le tas. C'est parce qu'ils n'ont pas d'enfants. 
  2. En remontant, le pire scénario pour le nœud juste au-dessus des feuilles serait d'une comparaison (au maximum, ils seraient comparés à une seule génération d'enfants).
  3. En allant plus loin, leurs parents immédiats peuvent au maximum être comparés à deux générations d’enfants. 
  4. 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, etc.
  5. 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 autre que O (n). 
5
Jones

Il y a déjà de bonnes réponses mais j'aimerais ajouter une petite explication visuelle

enter image description here

Maintenant, regardez l'image, il y a
n/2^1 nœuds verts avec hauteur 0 (ici 23/2 = 12)
n/2^2 nœuds rouges avec hauteur 1 (ici 23/4 = 6)
n/2^3 noeud bleu avec hauteur 2 (ici 23/8 = 3)
n/2^4 nœuds violets avec hauteur 3 (ici 23/16 = 2)
donc il y a n/2^(h+1) noeuds pour la hauteur h
Pour trouver la complexité temporelle, comptons la quantité de travail effectué ou nombre maximum d'itérations effectuées par chaque nœud
Maintenant, on peut remarquer que chaque nœud peut effectuer (au maximum) itérations == hauteur du nœud

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  

ainsi, pour tout nœud de hauteur h , le travail maximum est de n/2 ^ (h + 1) * h

Maintenant, le travail total est

->(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) ) 

maintenant pour toute valeur de h , la séquence

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

ne dépassera jamais 1
Ainsi, la complexité temporelle ne dépassera jamais O(n) pour la construction de tas

3
Julkar9

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 à la hauteur maximale (logn -h) inférieure.

    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)
2
Kartik Goyal

Les insertions successives peuvent être décrites par: 

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

Par approximation de Starling, n! =~ O(n^(n + O(1))), donc T =~ O(nlog(n))

J'espère que cela vous aidera, de manière optimale O(n) utilise l'algorithme de construction de tas pour un ensemble donné (l'ordre n'a pas d'importance).

1
Tomer Shalev

Preuve de O(n)

La preuve n'est pas sophistiquée, et assez simple, je n'ai démontré que le cas d'un arbre binaire complet, le résultat peut être généralisé pour un arbre binaire complet.

1
Yi Y

@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 interprétation erronée du sens de la déclaration, "l'insertion dans un segment de mémoire prend O (log n) fois". L'insertion dans un tas est bien O (log n), mais vous devez reconnaître que n est la taille du tas pendant l'insertion .

Dans le contexte de l'insertion de n objets dans un segment de mémoire, la complexité de la ième insertion est O (log n_i), où n_i est la taille du segment de mémoire comme à l'insertion i. Seule la dernière insertion a une complexité de O (log n).

1
N.Vegeta

Supposons que vous ayez des éléments N dans un tas. Alors sa hauteur serait Log (N)

Maintenant, vous voulez insérer un autre élément, alors la complexité serait: Log (N), nous devons comparer tout le chemin JUSQU'À à la racine.

Vous avez maintenant N + 1 éléments et hauteur = Log (N + 1)

En utilisant la technique induction , il peut être prouvé que la complexité de l'insertion serait ∑logi

Maintenant en utilisant 

log a + log b = log ab

Ceci 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 à la moitié de l'arbre. Par conséquent, cette limite peut être optimisée pour avoir une autre limite plus étroite 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.

0
Fooo

J'aime beaucoup les explications de Jeremy West .... une autre approche très facile à comprendre est donnée ici http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity

depuis, buildheap dépend de dépend de heapify et l’approche par décalage est utilisée, laquelle 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 de (2 ^ i * (h-i)), où h = logn est la hauteur de l'arbre en résolvant s, on obtient s = 2 ^ (h + 1) - 1 - (h + 1) puisque n = 2 ^ (h + 1) - 1 s = n - h - 1 = n - logn - 1 s = O (n), et la complexité de buildheap est donc O (n).

0
Nitish Jain

"La limite de temps linéaire de la construction Heap peut être montrée en calculant la somme des hauteurs de tous les nœuds du tas, ce qui correspond au nombre maximum de lignes en pointillés . 2 ^ (h + 1) - 1 nœuds, la somme des hauteurs des nœuds est N - H - 1 . Ainsi, il s’agit de O (N). "

0
sec3