Tri des tas faibles


De tous les tas de zoo, cette structure est peut-être la plus inhabituelle. En même temps, l'élégante simplicité de l'algorithme est tout à fait à la hauteur de son excentricité étonnante.

Lors du tri à l'aide d'un segment faible, il y a toujours moins de comparaisons et d'échanges qu'à l'aide d'un segment standard. Alors oui, un tas faible est plus fort qu'un tas ordinaire.
Logiciel EDISON - développement web
Cet article a été écrit avec le soutien d'EDISON.

Nous sommes engagés dans la création de logiciels embarqués ainsi que dans le développement d'applications et de sites Web .

Nous aimons la théorie des algorithmes! ;-)

Pile faible


Un segment de mémoire normal est un arbre de tri dans lequel tout parent est supérieur (ou égal) à l'un de ses descendants. Dans un tas faible, cette exigence est affaiblie - tout parent est supérieur (ou égal à) tout descendant uniquement de son sous-arbre droit. Dans le sous-arbre gauche, les descendants peuvent être à la fois plus petits et plus grands que le parent, là tu as de la chance.


Cette approche peut réduire considérablement le coût de maintenance de l'ensemble de données dans un état de segment de mémoire. Après tout, il est nécessaire de garantir que le principe «un descendant n'est plus qu'un parent» ne s'applique pas à l'ensemble de la structure, mais seulement à la moitié de celle-ci. Dans le même temps, un tas faible, n'étant pas un arbre de tri à 100%, ne trie pas pire qu'un tas ordinaire, et à certains égards encore mieux. A fait la moitié de la bataille - marchez hardiment!

Puisque la racine du tas, même faible, a besoin exactement du plus grand élément, la racine du sous-arbre gauche n'en a pas besoin.

Minimiser le nombre de comparaisons



Ronald D. Dutton, un spécialiste des algorithmes et de la théorie des graphes, nous a présenté un groupe faible en 1993. Un tas conceptuellement faible est plus difficile à comprendre (mais cette difficulté n'est probablement pas dans la complexité, mais dans l'extravagance, vous devez briser vos schémas de conscience à travers le genou) qu'un tas ordinaire, donc il n'a pas reçu beaucoup de distribution pratique. Cependant, lorsque Dutton a inventé cette structure, non seulement il voulait pratiquer des abstractions abstraites, mais il poursuivait un objectif complètement pragmatique.

Il existe une limite inférieure théorique pour estimer le nombre minimal de comparaisons (dans les tri dans lesquels ces comparaisons sont largement utilisées):

log n ! = n log n - n / ln 2 + O (log n), où 1 / ln 2 = 1,4426

En triant par un tas faible, le nombre de comparaisons est minimisé et est suffisamment proche de la limite inférieure.

Cela peut être d'une importance pratique si vous devez organiser des objets dont la comparaison est coûteuse, par exemple, s'il s'agit de trier de longues chaînes.

Descendants jongleurs


«Gauche» et «droite» dans un tas faible est un phénomène situationnel. Un sous-arbre peut être un descendant gauche ou droit pour son nœud parent - en outre, cette relation «gauche / droite» pour le sous-arbre et le parent peut passer à plusieurs reprises d'une valeur à l'autre pendant le processus.

Indiquer au parent qui a son fils droit et qui est sa fille gauche n'est pas simple, mais très simple. Pour ce faire, vous avez besoin d'un bitmap supplémentaire (composé uniquement de valeurs 0/1) pour les nœuds ayant des descendants.

Rappelons comment l'indice i -ème élément parent, nous définissons les indices de ses descendants gauche et droit dans une pile conventionnelle (indices du tableau mesurés à partir de zéro):

Descendant gauche 2 × i + 1
Descendant droit 2 × i + 2

Dans un tas faible, nous avons une cerise sur le gâteau - la racine qui n'a que le sous-arbre droit, nous allons donc ajuster ces formules pour les indices descendants en ajoutant un décalage inverse à 1 position:

Descendant gauche: 2 × i
Descendant droit: 2 × i + 1

Et enfin , avait besoin d'un bitmap supplémentaire (appelez-le bIT ), où pour le i -ème élément noté, il y avait si l'échange se place entre ses sous-arbres gauche et droit. Si la valeur d'un élément est 0, il n'y a pas eu d'échange. Si la valeur est 1, les enfants gauche et droit vont dans l'ordre inverse. Et les formules dans ce cas sont les suivantes:

Descendant gauche: 2 × i + BIT [ i ]
Descendant droit: 2 × i + 1 - BIT [ i ]

Voilà à quoi ça ressemble. Les éléments dont les descendants sont situés «vice versa» sont surlignés en bleu. Les valeurs dans le tableau BIT pour eux sont 1.


Vous pouvez vérifier en substituant les valeurs parentes i et le 0/1 correspondant du tableau BIT dans les formules descendantes - les indices des descendants se révéleront au besoin.

Comme vous pouvez le voir, pour que tout parent permute les sous-arbres gauche et droit, dans le tableau lui-même, le groupe d'éléments n'a pas besoin d'être déplacé n'importe où. Seule la valeur 0/1 pour le parent dans le tableau BIT est commutée et c'est tout.

Ensuite - une session de magie avec son exposition ultérieure.

Construisez une pile faible


L'échange des descendants gauche et droit est le principal outil pour convertir en un tas faible un ensemble de données à partir d'un tableau.

Dans le processus de formation du tas faible principal, nous devons trier les éléments du tableau dans l'ordre inverse (à partir du dernier) et pour chacun d'eux trouver la branche du premier parent (à droite), pour laquelle ce sera le bon sous-arbre.

Si l'élément est le bon descendant de quelqu'un , vous n'avez pas besoin d'aller loin. Son parent immédiat est ce dont vous avez besoin:


Si l'élément est le descendant gauche de quelqu'un , vous devez remonter un certain nombre de niveaux avant de rencontrer le grand-parent souhaité, pour lequel l'élément se trouve dans le sous-arbre droit:



Ensuite, vous devez comparer le descendant et l'ancêtre trouvé quelque part ci-dessus. Et si le descendant est plus grand que le progéniteur, alors il faut faire ce qui suit:

  1. Si le descendant a ses descendants, échangez alors ses sous-arbres gauche et droit (c'est-à-dire basculez 0/1 dans le tableau BIT pour cet élément).
  2. Échangez les valeurs du nœud descendant et du nœud progéniteur.

Jetez un oeil à un exemple spécifique. Supposons que la situation suivante se soit produite:


Pour l'élément du tableau A [6] = 87, le progéniteur nécessaire A [1] = 76 a été trouvé.
L'ancêtre A [1] est plus petit que l'élément A [6] (76 <87).
L'élément A [6] a des sous-arbres gauche et droit (marqués en vert).
Vous devez échanger ces sous-arbres
(c'est-à-dire que pour l'élément A [6] dans le tableau BIT , changez la valeur de 0 à 1).
Il faut également échanger les valeurs des éléments A [6] et A [1].


Une fois les actions nécessaires terminées:


Pour l'élément A [6], les sous-arbres gauche et droit ont été échangés
(c'est-à-dire que dans le tableau BIT pour l'élément A [6], la valeur de 0 a été changée en 1).
Il y a également eu un échange de valeurs entre A [6] et A [1].


Si vous parcourez le tableau de la fin au début et effectuez cette procédure pour tous les éléments, vous vous retrouverez avec un groupe faible.

Pourquoi cet étrange mécanisme fonctionne est une explication plus proche de la fin de l'article.

Analyser une pile faible


Un tas est un tas si l'élément maximum est à la racine. En utilisant ce fait, tous les tris de tas fonctionnent de la même manière. La racine (où se trouve le maximum) échange des valeurs avec le dernier élément de la partie non triée du tableau - en conséquence, la partie non triée du tableau diminue et la partie triée du tableau augmente. Après cet échange, le tas cesse d'être un tas, car l'élément maximal actuel n'est plus à sa racine. Le tas doit être restauré, c'est-à-dire faire à nouveau de l'arbre résultant un tas - trouver un autre élément maximum et le déplacer vers la racine.

Comment récupérer un tas binaire régulier, nous le savons - à l'aide d'un tamis . Mais comment restaurer un tas faible? Pour ce faire, procédez comme suit.

De la racine, nous descendons les descendants de gauche (jusqu'au plus bas):


Ensuite, nous remontons les descendants de gauche et le long du chemin, chaque descendant de gauche est comparé à un élément de la racine de tas. Et si le prochain descendant gauche est plus grand que la racine, alors nous faisons la même chose qu'à l'étape précédente: au descendant gauche, nous échangeons les sous-arbres (s'il en a un) et modifions les valeurs du descendant gauche et de la racine.

En conséquence, nous restaurerons le tas faible - l'élément maximal qui se trouve dans l'arborescence restante apparaîtra à sa racine.

Et encore une fois, nous avons ce carrousel mystique avec des sous-arbres qui se remplacent. Quel est donc le secret du succès? Pourquoi, si lors de l'échange de nœuds avec des valeurs, les descendants gauche et droit du nœud inférieur sont échangés, aboutissons-nous à un tableau trié? Vous ne devinerez jamais, bien que la réponse soit simple dans son génie.

Tri par tas faible :: Tri par tas faible


Donc, l'algorithme final:

  • I. :
    • I.1. -.
    • I.2. «» .
    • I.3. .
    • I.4. , :
      • I.4.. ( ⇔ ) , .
      • I.4.. «» .
  • II. , :
    • II.1. .
    • II.2. . .
    • II.3. , . :
      • II.3.. .
      • II.3.. , .
      • II.3.. , , :
        • II.3.c.1. Nous échangeons (gauche ⇔ droite) les sous-arbres avec les descendants pour le nœud dans lequel se trouve le descendant gauche actuel.
        • II.3.c.2. Nous changeons la racine du tas et le nœud avec l'enfant gauche actuel.
    • II.4. À la racine du tas faible se trouve à nouveau l'élément maximum pour la partie non triée restante du tableau. Nous revenons au paragraphe II.1 et répétons le processus jusqu'à ce que tous les éléments soient triés.


Animation (les indices de tableau dans mes animations commencent par un):



Code C ++


En bas de la section «Liens», les personnes intéressées peuvent se familiariser avec l'implémentation de ce tri en C ++. Ici, je ne donne que la partie qui illustre l'algorithme lui-même.

#define GETFLAG(r, x) ((r[(x) >> 3] >> ((x) & 7)) & 1)
#define TOGGLEFLAG(r, x) (r[(x) >> 3] ^= 1 << ((x) & 7))

void WeakHeap::WeakHeapMerge(unsigned char *r, int i, int j) {
  if (wheap[i] < wheap[j]) {//""  ?
    //  ,   
    //( "",   "")
    TOGGLEFLAG(r, j);
    //  ""  
    swap(wheap[i], wheap[j]);
  }
}

void WeakHeap::WeakHeapSort() {
  int n = Size();
  if(n > 1) {
		
    int i, j, x, y, Gparent;
    int s = (n + 7) / 8;
    unsigned char * r = new unsigned char [s];
		
    //  ,    
    // "",   ""
    for(i = 0; i < n / 8; ++i) r[i] = 0;
		
    //   
    for(i = n - 1; i > 0; --i) {
      j = i;
      //    , 
      //   ""  
      while ((j & 1) == GETFLAG(r, j >> 1)) j >>= 1;
      //       ""  
      Gparent = j >> 1;
      //  ,   
      //   ""
      WeakHeapMerge(r, Gparent, i);
    }
		
    //      -->
    //  -->    
    for(i = n - 1; i >= 2; --i) {
      //      
      //       
      swap(wheap[0], wheap[i]);
      x = 1;
      //    "" 
      while((y = 2 * x + GETFLAG(r, x)) < i) x = y;
      //  ""     
      //        
      while(x > 0) {
        WeakHeapMerge(r, 0, x);
        x >>= 1;
      }
    }
    //  -   
    //    
    swap(wheap[0], wheap[1]);
    delete[] r;
  }
}

J'aime particulièrement la façon dont l'arbre binaire est traversé facilement et naturellement à l'aide d'opérations au niveau du bit.

Complexité de mémoire supplémentaire


Cela ressemble à O ( n ) - un tableau supplémentaire est requis, dans lequel pour les nœuds avec des descendants (il y a environ la moitié de ceux dans le tableau), l'ordre des sous-arbres gauche / droite est fixe.

Cependant, il y a une opinion que la complexité de tri ici est en fait O (1)! Pour un élément, nous n'avons besoin que d'un bit supplémentaire (zéro / un) pour indiquer l'ordre des descendants. Si nous trions, par exemple, des chaînes, il est tout à fait possible d'ajouter ce bit supplémentaire à l'élément lui-même.

Une autre façon de transformer O ( n ) en O (1) consiste à stocker des indicateurs dans un nombre entier. Expansion binaire des nombres - un ensemble de zéros et de uns responsables de l'ordre des sous-arbres de tous les éléments du tableau. Le i- ème élément du tableau correspond au i- ème bit du nombre.

Complexité temporelle


Par le temps, O ( n log n ) est identique à un tas normal. Lors du tri des lignes (en particulier les longues), un tas faible peut être plus rapide qu'un tas normal. Mais c'est si nous trions les longues lignes. Si nous trions les chiffres, alors, selon les rumeurs, un tas ordinaire est plus rapide à gérer.

Tamisage complet


Au stade de la formation du tas faible initial, par analogie avec le tas habituel, l'idée tout à fait évidente est d'élever de grands éléments le plus haut possible. Autrement dit, si nous échangions les valeurs du nœud inférieur et de son ancêtre, il serait logique de répéter immédiatement les étapes pour l'ancêtre - pour trouver son ancêtre droit le plus proche pour lui et comparer (et s'il est également nécessaire d'échanger des valeurs + échange de sous-arbres). Et, si possible, élevez un grand élément à la racine même. Voici à quoi cela ressemble à la première étape (les actions à la deuxième étape de l'algorithme sont inchangées):


Le score de complexité temporelle reste le même.

Pile binomiale


Tout ce qui était jusqu'ici, c'est une tromperie, une illusion. Bien sûr, nous effectuons formellement quelques manipulations avec l'arbre binaire, changeons les nœuds avec des valeurs, réorganisons les sous-arbres et tout ça. Cependant, l'algorithme a un double fond, sur lequel nous allons maintenant nous pencher.

Pour comprendre ce type, vous devez comprendre ce qu'est vraiment un tas faible.

Si nous prenons un tableau dans lequel le nombre d'éléments est une puissance de deux, alors le tas faible et le tas binomial construit sur sa base sont isomorphes.


Ne regardez pas le fait qu'un tas faible est binaire et que le binôme ne l'est pas. Dans un tas faible, les sous-arbres gauche et droit sont essentiellement différents. Le sous-arbre droit est un descendant au sens classique, mais le sous-arbre gauche est plutôt un «frère». Bien que non. Le sous-arbre gauche n'est même pas un «frère», mais un vecteur de «frères» avec moins de nœuds.

Cependant, le tas faible et le tas binomial ne sont pas identiques à 100%, bien qu'ils soient les plus proches parents. La différence est évidente, si vous prenez un tableau dont le nombre d'éléments n'est pas égal à 2 n . La décomposition binomiale d'un tel tableau donnera une liste connectée de plusieurs tas idéaux (le nombre de nœuds dans chacun d'eux est une certaine puissance de deux):


Un tas faible dans ce cas sera un arbre binaire imparfait:



La pile binomiale et la pile faible sont des frères jumeaux. L'ADN est le même, bien qu'en apparence on ne puisse pas le dire.

Algorithme secret


Étant donné qu'un tas faible est un tas cryptobinomial, le brassage des sous-arbres trouve soudain une explication simple.


Avec un tas faible, balayez le clinquant pseudobinaire et regardez les relations réelles entre les nœuds dans le style de tas binomial. Tout devient clair.

En réalité:

  1. Il n'y a pas de «faiblesse», c'est un arbre de tri à part entière (non binaire) dans lequel le principe «tout parent est supérieur à n'importe lequel de ses descendants» est atteint et maintenu.
  2. À toutes les étapes, nous comparons les descendants non pas avec leurs ancêtres, mais avec leurs parents immédiats.
  3. Ce qui ressemble à un échange de valeurs entre un descendant et un ancêtre + un échange de lieux de sous-arbres chez un descendant - il s'avère être l'échange du rapport lui-même (descendant / parent). Si le nœud parent est plus petit que le descendant par valeur, le parent lui-même devient le descendant et le descendant devient le parent.

Voici une visualisation honnête:



Dans la prochaine série


La prochaine pile dont je voudrais parler est mon préféré - l'arbre cartésien. Ce n'est pas seulement un tas, mais aussi un arbre de recherche binaire . Mais d'abord, dans l'article suivant, quelque chose d'intéressant doit être clarifié à propos des arbres BST. Et seulement alors, à travers l'article, et sur la conversation cartésienne.

Références


Faible Heap , Binomial Heap / Binomial

Heap C ++ faible mise en œuvre Heap

Ronald D. Dutton: page personnelle , UCF Site Web Profil

Faible Heaps et Amis: Développements récents

La structure de données faible-Heap: Des variantes et applications

sur les performances de Weak-HeapSort

Adaptive heapsort: Code source

Sergey Kopeliovich - Salle de conférence - Pile faible (de 48:32 à 1:16:06)

Articles de série:




Le tri d'aujourd'hui est ajouté à l'application AlgoLab par un groupe faible, qui l'utilise - mettez à jour le fichier Excel avec des macros.

Dans les commentaires de la cellule avec le nom du tri, vous pouvez spécifier certains paramètres. Si vous définissez siftup = 1, le tri utilisera le filtrage complet dans la première étape (par défaut siftup = 0).

Si vous prescrivez binôme = 1, alors l'arbre sera un «tas de binôme» (par défaut binôme = 0, c'est-à-dire juste un tas faible).

All Articles