Maintenir une commande efficace où vous pouvez insérer des éléments «entre» deux autres éléments dans la commande?

8

Imaginez que j'ai une commande sur un tas d'éléments comme ça:

entrez la description de l'image ici

Où une flèche signifie . Il est également transitif: .XYX<Y(X<Y)(Y<Z)(X<Z)

Pour répondre efficacement aux requêtes comme , une sorte d'étiquetage ou de structure de données est nécessaire. Par exemple, vous pouvez numéroter les nœuds de gauche à droite, et donc vous pouvez simplement faire entier comparaison pour répondre à la requête: . Cela ressemblerait à quelque chose comme ceci:A<?DA<?D1<4T

entrez la description de l'image ici

Où le numéro est la commande et la lettre n'est qu'un nom.

Mais que se passe-t-il si vous devez insérer des éléments "entre" deux autres éléments dans l'ordre, comme ceci:

entrez la description de l'image ici

entrez la description de l'image ici

entrez la description de l'image ici

Comment pouvez-vous maintenir une telle commande? Avec une numérotation simple, vous rencontrez le problème qu'il n'y a pas d'entiers "entre" à utiliser.2,3

Realz Slaw
la source

Réponses:

7

C'est ce qu'on appelle le problème de "maintenance des commandes" . Il existe une solution relativement simple utilisant le temps amorti pour les requêtes et les insertions. Maintenant, par "relativement simple", je veux dire que vous devez comprendre certains blocs de construction, mais qu'une fois que vous les avez, le reste n'est pas difficile à voir.O(1)

http://courses.csail.mit.edu/6.851/spring12/lectures/L08.html

L'idée de base est une structure de données à deux niveaux. Le niveau supérieur est comme la solution d'arbre AVL de Realz Slaw, mais

  • Les nœuds sont directement étiquetés avec des chaînes de bits de longueur avec un ordre qui correspond à leur ordre dans l'arborescence. La comparaison prend donc un temps constantO(lgn)

  • Un arbre avec moins de rotations qu'un arbre AVL est utilisé, comme un bouc émissaire ou un arbre à poids équilibré, de sorte que le réétiquetage se produit moins fréquemment.

Le niveau inférieur est les feuilles de l'arbre. Ce niveau utilise la même longueur d'étiquettes, , mais ne contient que des éléments dans chaque feuille dans une simple liste chaînée. Cela vous donne suffisamment de bits supplémentaires pour réétiqueter agressivement.O(lgn)O(lgn)

Les feuilles deviennent trop grandes ou trop petites à chaque insertion de , induisant un changement dans le niveau supérieur, ce qui prend temps amorti ( pire des cas). Amorti, ce n'est que .O(lgn)O(lgn)Ω(n)O(1)

Des structures beaucoup plus complexes existent pour effectuer des mises à jour dans le pire des cas .O(1)

jbapple
la source
7

Au lieu d'une simple numérotation, vous pouvez répartir les nombres sur une large plage (de taille constante), comme le minimum et le maximum entiers d'un entier CPU. Ensuite, vous pouvez continuer à mettre des nombres "entre" en faisant la moyenne des deux nombres environnants. Si les nombres deviennent trop encombrés (par exemple, vous vous retrouvez avec deux entiers adjacents et qu'il n'y a pas de nombre entre les deux), vous pouvez effectuer une renumérotation unique de l'ensemble de la commande, en redistribuant les nombres de manière égale sur toute la plage.

Bien sûr, vous pouvez rencontrer la limitation selon laquelle tous les nombres compris dans la plage de la grande constante sont utilisés. Tout d'abord, ce n'est généralement pas un problème, car la taille entière sur une machine est suffisamment grande pour que si vous aviez plus d'éléments, elle ne rentrerait probablement pas dans la mémoire de toute façon. Mais s'il s'agit d'un problème, vous pouvez simplement les renuméroter avec une plage entière plus grande.

Si l'ordre d'entrée n'est pas pathologique, cette méthode peut amortir les renumérotations.

Répondre aux requêtes

Une simple comparaison d'entiers peut répondre à la requête (X<?Y).

Le temps de requête serait très rapide ( O(1)) si vous utilisez des entiers machine, car il s'agit d'une simple comparaison d'entiers. L'utilisation d'une plus grande plage nécessiterait de plus grands nombres entiers, et la comparaison prendraitO(log|integer|).

Insertion

Premièrement, vous maintiendrez la liste chaînée de la commande, illustrée dans la question. L'insertion ici, étant donné les nœuds pour placer le nouvel élément entre les deux, seraitO(1).

L'étiquetage du nouvel élément serait généralement rapide O(1)parce que vous calculeriez facilement le nouveau nombre en faisant la moyenne des nombres environnants. Parfois, vous pourriez manquer de nombres "entre les deux", ce qui déclencherait laO(n) procédure de renumérotation horaire.

Éviter la renumérotation

Vous pouvez utiliser des flottants au lieu d'entiers, donc lorsque vous obtenez deux entiers "adjacents", ils peuvent être moyennés. Ainsi, vous pouvez éviter de renuméroter face à deux flottants entiers: il suffit de les diviser en deux. Cependant, le type à virgule flottante finira par manquer de précision et deux flotteurs "adjacents" ne pourront pas être moyennés (la moyenne des nombres environnants sera probablement égale à l'un des nombres environnants).

Vous pouvez également utiliser un entier "décimal", où vous conservez deux entiers pour un élément; un pour le nombre et un pour la décimale. De cette façon, vous pouvez éviter de renuméroter. Cependant, l'entier décimal débordera éventuellement.

L'utilisation d'une liste d'entiers ou de bits pour chaque étiquette peut entièrement éviter la renumérotation; cela revient à utiliser des nombres décimaux de longueur illimitée. La comparaison se ferait lexicographiquement et les temps de comparaison augmenteront jusqu'à la longueur des listes concernées. Cependant, cela peut déséquilibrer l'étiquetage; certaines étiquettes peuvent nécessiter un seul entier (pas de décimales), d'autres peuvent avoir une liste de longues longueurs (longues décimales). C'est un problème, et la renumérotation peut aussi aider ici, en redistribuant la numérotation (ici des listes de numéros) uniformément sur une plage choisie (plage ici signifiant peut-être la longueur des listes) de sorte qu'après une telle renumérotation, les listes soient toutes de la même longueur .


Cette méthode est effectivement utilisée dans cet algorithme ( implémentation , structure de données pertinente ); au cours de l'algorithme, un ordre arbitraire doit être conservé et l'auteur utilise des entiers et une renumérotation pour y parvenir.


Essayer de s'en tenir aux chiffres rend votre espace clé quelque peu limité. On pourrait utiliser des chaînes de longueur variable à la place, en utilisant la logique de comparaison "a" <"ab" <"b". Il reste encore deux problèmes à résoudre A. Les clés pourraient devenir arbitrairement longues B. La comparaison des clés longues pourrait devenir coûteuse

Realz Slaw
la source
3

Vous pouvez conserver une arborescence AVL sans clé ou similaire.

Cela fonctionnerait comme suit: L'arbre maintient un ordre sur les nœuds comme le fait normalement un arbre AVL, mais au lieu de la clé déterminant où le nœud "devrait" se trouver, il n'y a pas de clés, et vous devez insérer explicitement les nœuds "après "un autre nœud (ou en d'autres termes" entre "deux nœuds), où" après "signifie qu'il vient après lui dans une traversée ordonnée de l'arbre. L'arbre maintiendra ainsi naturellement l'ordre pour vous, et il s'équilibrerait également, en raison des rotations intégrées de l'AVL. Cela gardera tout uniformément distribué automatiquement.

Insertion

En plus de l'insertion régulière dans la liste, comme illustré dans la question, vous devez conserver une arborescence AVL distincte. L'insertion dans la liste elle-même estO(1), car vous avez les nœuds "avant" et "après".

Le temps d'insertion dans l'arbre est O(logn), identique à l'insertion dans une arborescence AVL. L'insertion implique d'avoir une référence au nœud que vous souhaitez insérer après, et vous insérez simplement le nouveau nœud dans la gauche du nœud le plus à gauche de l'enfant droit; cet emplacement est "suivant" dans l'ordre de l'arborescence (il est suivant dans la traversée dans l'ordre). Effectuez ensuite les rotations AVL typiques pour rééquilibrer l'arborescence. Vous pouvez effectuer une opération similaire pour "insérer avant"; cela est utile lorsque vous devez insérer quelque chose au début de la liste et qu'il n'y a pas de nœud "avant".

Répondre aux requêtes

Pour répondre aux questions de (X<?Y), vous retrouvez simplement tous les ancêtres de X et Ydans l'arbre, et vous analysez l'emplacement de l'endroit où dans l'arbre les ancêtres divergent; celui qui diverge vers la "gauche" est le moindre des deux.

Cette procédure prend O(logn)le temps de grimper à la racine et d'obtenir les listes d'ancêtres. S'il est vrai que cela semble plus lent que la comparaison entière, la vérité est que c'est la même chose; seule cette comparaison entière sur un CPU est limitée par une grande constante pour le rendreO(1); si vous dépassez cette constante, vous devez conserver plusieurs entiers (O(logn) entiers en fait) et faire de même O(logn)comparaisons. Alternativement, vous pouvez "délimiter" la hauteur de l'arbre d'une quantité constante et "tricher" de la même manière que la machine avec des entiers: maintenant les requêtes semblerontO(1).

Démonstration de l'opération d'insertion

Pour démontrer, vous pouvez insérer certains éléments avec leur ordre dans la liste de la question:

Étape 1

Commencer avec D

Liste:

liste étape 1

Arbre:

arbre étape 1

Étape 2

Insérer C, <C<D.

Liste:

liste étape 2

Arbre:

arbre étape 2

Remarque, vous mettez explicitement C "avant" D, non pas parce que la lettre C est avant D, mais parce que C<D dans la liste.

Étape 3

Insérer A, <A<C.

Liste:

liste étape 3

Arbre:

arbre étape 3 avant rotation

Rotation AVL:

arbre étape 3 après rotation

Étape 4

Insérer B, A<B<C.

Liste:

liste étape 4

Arbre:

arbre étape 4

Aucune rotation nécessaire.

Étape 5

Insérer E, D<E<

Liste:

liste étape 5

Arbre:

étape 5 de l'arbre

Étape 6

Insérer F, B<F<C

Nous l'avons juste mis "après" B dans l'arbre, dans ce cas en le fixant simplement à Ba raison; DoncF est maintenant juste après B dans la traversée ordonnée de l'arbre.

Liste:

liste étape 6

Arbre:

arbre étape 6 avant rotation

Rotation AVL:

arbre étape 6 après rotation

Démonstration de l'opération de comparaison

A<?F

ancestors(A) = [C,B]
ancestors(F) = [C,B]
last_common_ancestor = B
B.left = A
B.right = F
... A < F #left is less than right

D<?F

ancestors(D) = [C]
ancestors(F) = [C,B]
last_common_ancestor = C
C.left = D
C.right = B #next ancestor for F is to the right
... D < F #left is less than right

B<?A

ancestors(B) = [C]
ancestors(A) = [B,C]
last_common_ancestor = B
B.left = A
... A < B #left is always less than parent

Sources graphiques

Realz Slaw
la source
@saadtaame corrigé et ajouté des sources de fichiers dot en bas. Merci de l'avoir signalé.
Realz Slaw