Quel est l'intérêt de l'implémentation d'une pile utilisant deux files d'attente?

34

J'ai la question de devoirs suivante:

Implémentez les méthodes de pile push (x) et pop () à l'aide de deux files d'attente.

Cela me semble étrange parce que:

  • Une pile est une file d'attente (LIFO)
  • Je ne vois pas pourquoi vous auriez besoin de deux files d'attente pour le mettre en œuvre

J'ai cherché autour de:

et trouvé quelques solutions. C'est ce que j'ai fini avec:

public class Stack<T> {
    LinkedList<T> q1 = new LinkedList<T>();
    LinkedList<T> q2 = new LinkedList<T>();

    public void push(T t) {
        q1.addFirst(t);
    }

    public T pop() {
        if (q1.isEmpty()) {
            throw new RuntimeException(
                "Can't pop from an empty stack!");
        }

        while(q1.size() > 1) {
            q2.addFirst( q1.removeLast() );
        }

        T popped = q1.pop();

        LinkedList<T> tempQ = q1;
        q1 = q2;
        q2 = tempQ;

        return popped;
    }
}

Mais je ne comprends pas l’avantage de l’utilisation d’une seule file; la version à deux files d'attente semble inutilement compliquée.

Supposons que nous choisissions les poussées comme étant les plus efficaces des 2 (comme je l’ai fait ci-dessus), pushresterions les mêmes et popexigeraient simplement une itération au dernier élément, puis le retourner. Dans les deux cas, le pushserait O(1)et le popserait O(n); mais la version à file d'attente unique serait considérablement plus simple. Cela ne devrait nécessiter qu'une seule boucle for.

Est-ce que je manque quelque chose? Toute idée ici serait appréciée.

Carcigenicate
la source
17
Une file d'attente fait généralement référence à une structure FIFO alors que la pile est une structure LIFO. L’interface de LinkedList en Java est celle d’une deque (file d’attente à deux extrémités) qui permet l’accès FIFO et LIFO. Essayez de changer la programmation pour l' interface Queue plutôt que pour l'implémentation LinkedList.
12
Le problème le plus courant consiste à implémenter une file d'attente à l'aide de deux piles. Vous trouverez peut-être intéressant le livre de Chris Okasaki sur les structures de données purement fonctionnelles.
Eric Lippert
2
En vous appuyant sur ce que Eric a dit, vous pouvez parfois vous retrouver dans un langage basé sur des piles (tel que dc ou un automate à pile avec deux piles (équivalent à une machine de turing car vous pouvez en faire plus)) où vous pouvez vous retrouver plusieurs piles, mais pas de file d'attente.
1
@MichaelT: Ou vous pouvez aussi vous retrouver sur un processeur basé sur une pile
slebetman
11
"Une pile est une file d'attente (LIFO)" ... euh, une file d'attente est une file d'attente. Comme la ligne d'utilisation des toilettes publiques. Est-ce que les lignes que vous attendez se comportent toujours de manière LIFO? Arrêtez d'utiliser le terme "file d'attente LIFO", c'est absurde.
Mehrdad

Réponses:

44

Il n'y a aucun avantage: c'est un exercice purement académique.

Il y a très longtemps, lorsque j'étais étudiant de première année à l'université, j'avais un exercice similaire 1 . L'objectif était d'enseigner aux étudiants comment utiliser la programmation orientée objet pour mettre en œuvre des algorithmes au lieu d'écrire des solutions itératives à l'aide de forboucles avec des compteurs de boucles. Combinez et réutilisez les structures de données existantes pour atteindre vos objectifs.

Vous n'utiliserez jamais ce code dans Real World TM . Ce que vous devez retenir de cet exercice, c'est comment "sortir des sentiers battus" et réutiliser le code.


Veuillez noter que vous devriez utiliser l' interface java.util.Queue dans votre code au lieu d'utiliser directement l'implémentation:

Queue<T> q1 = new LinkedList<T>();
Queue<T> q2 = new LinkedList<T>();

Cela vous permet d’utiliser d’autres Queueimplémentations si vous le souhaitez, ainsi que de masquer 2 méthodes LinkedListqui peuvent contourner l’esprit de l’ Queueinterface. Cela inclut get(int)et pop()(pendant que votre code est compilé, il y a une erreur de logique là-dedans étant donné les contraintes de votre affectation. Déclarer vos variables comme Queueau lieu de LinkedListle révélera). Lecture connexe: Comprendre «programmer pour une interface» et Pourquoi les interfaces sont-elles utiles?

1 Je me souviens encore: l’exercice consistait à inverser une pile en utilisant uniquement des méthodes de l’interface de pile et aucune méthode d’utilité ni aucune java.util.Collectionsautre classe d’utilitaire "statique uniquement". La solution correcte consiste à utiliser d'autres structures de données en tant qu'objets de stockage temporaires: vous devez connaître les différentes structures de données, leurs propriétés et comment les combiner pour le faire. Stumped la plupart de ma classe CS101 qui n'avait jamais programmé auparavant.

2 Les méthodes sont toujours là, mais vous ne pouvez pas y accéder sans transformations de caractères ou réflexions. Il n'est donc pas facile d'utiliser ces méthodes sans file d'attente.

Communauté
la source
1
Merci. Je suppose que cela a du sens. Je me suis aussi rendu compte que j'utilise des opérations "illégales" dans le code ci-dessus (poussée au début d'une FIFO), mais je ne pense pas que cela change quoi que ce soit. J'ai inversé toutes les opérations et cela fonctionne toujours comme prévu. Je vais attendre un peu avant d'accepter car je ne veux décourager aucune autre personne de donner son avis. Merci quand même.
Carcigenicate
19

Il n'y a pas d'avantage. Vous avez correctement compris que l’utilisation de files d’attente pour implémenter une pile entraîne une complexité horrible en termes de temps. Aucun programmeur (compétent) ne ferait jamais quelque chose comme ça dans la "vraie vie".

Mais c'est possible. Vous pouvez utiliser une abstraction pour en implémenter une autre, et inversement. Une pile peut être implémentée en termes de deux files d'attente. Vous pouvez également implémenter une file d'attente en termes de deux piles. L'avantage de cet exercice est:

  • vous récapitulez des piles
  • vous récapitulez les files d'attente
  • vous vous habituez à la pensée algorithmique
  • vous apprendrez des algorithmes liés à la pile
  • vous devez penser à des compromis dans les algorithmes
  • en réalisant l'équivalence des files d'attente et des piles, vous connectez divers sujets de votre cours
  • vous acquérez de l'expérience en programmation

En fait, c’est un excellent exercice. Je devrais le faire moi-même maintenant :)

Amon
la source
3
@JohnKugelman Merci pour votre modification, mais je voulais vraiment dire «une complexité de temps horrible». Pour une pile basée sur une liste chaînée push, peeket les popopérations sont en O (1). Il en va de même pour une pile redimensionnable basée sur un tableau, à l'exception du fait qu'elle pushest dans O (1) et qu'elle est amortie, avec le cas le plus défavorable de O (n). Comparé à cela, une pile en file d'attente est très inférieure avec O (n) push, O (1) pop et peek, ou bien O (1) push, O (n) pop et peek.
amon
1
"horrible complexité temporelle" et "largement inférieur" n'est pas tout à fait correct. La complexité amortie est toujours O (1) pour push et pop. Il y a une question amusante dans TAOCP (vol1?) À ce sujet (en gros, vous devez montrer que le nombre de fois qu'un élément peut basculer d'une pile à une autre est constant). Les performances dans le pire des cas pour une opération sont différentes, mais j'entends rarement quelqu'un parler de la performance O (N) pour l'insertion dans ArrayLists - pas le nombre généralement intéressant.
Voo
5

Il est vraiment utile de créer une file d’attente sur deux piles. Si vous utilisez des structures de données immuables à partir d'un langage fonctionnel, vous pouvez insérer une pile d'éléments pouvant être déplacés et extraire une liste d'éléments pouvant être masqués. Les éléments amovibles sont créés lorsque tous les éléments ont été extraits, et la nouvelle pile extensible est l'inverse de la pile extensible, où la nouvelle pile extensible est maintenant vide. C'est efficace.

En ce qui concerne une pile composée de deux files d'attente? Cela peut sembler logique dans un contexte où vous avez un grand nombre de files d’attente rapides et volumineuses. C'est définitivement inutile en tant qu'exercice sur Java. Mais cela peut avoir un sens s'il s'agit de canaux ou de files d'attente de messagerie. (ie: N messages mis en file d'attente, avec une opération O (1) pour déplacer (N-1) les éléments à l'avant dans une nouvelle file d'attente.)

Rob
la source
Hmm .. cela m'a fait penser à l'utilisation des registres à décalage comme base de l'informatique et à l'architecture de la courroie / du broyeur
slebetman
wow, le processeur de Mill est en effet intéressant. Un "Queue Machine" à coup sûr.
Rob
2

L'exercice est inutilement conçu d'un point de vue pratique. Le but est de vous obliger à utiliser l'interface de la file d'attente de manière intelligente pour implémenter la pile. Par exemple, votre solution "Une file d'attente" nécessite que vous effectuiez une itération sur la file d'attente pour obtenir la dernière valeur d'entrée pour l'opération "pop" de pile. Cependant, une structure de données de file d'attente ne permet pas de parcourir les valeurs, vous êtes limité à y accéder dans le premier entré, premier sorti (FIFO).

Destruktor
la source
2

Comme d'autres l'ont déjà fait remarquer: il n'y a pas d'avantage réel.

Quoi qu'il en soit, une réponse à la deuxième partie de votre question, pourquoi ne pas simplement utiliser une file d'attente, est au-delà de Java.

En Java même le Queue interface a une size()méthode et toutes les implémentations standard de cette méthode sont O (1).

Ce n'est pas nécessairement vrai pour la liste chaînée naïve / canonique comme le ferait un programmeur C / C ++, qui garderait simplement les pointeurs sur le premier et le dernier élément et chaque élément un pointeur sur l'élément suivant.

Dans ce cas, size()c’est O (n) et devrait être évité en boucle. Ou la mise en œuvre est opaque et ne fournit que le strict minimum add()etremove() .

Avec une telle implémentation, vous devez d’abord compter le nombre d’éléments en les transférant dans la deuxième file, transférer n-1 à la première file d'attente et renvoyer l'élément restant.

Cela dit, cela ne constituerait probablement pas une telle chose si vous vivez à Java-land.

Thraidh
la source
Bon point à propos de la size()méthode. Cependant, en l'absence d'une size()méthode O (1) , il est trivial pour la pile de garder une trace de sa taille actuelle. Rien ne peut arrêter une implémentation à une file d'attente.
cmaster
Tout dépend de la mise en œuvre. Si vous avez une file d'attente, implémentée avec uniquement des pointeurs de transfert et des pointeurs vers le premier et le dernier élément, vous pouvez toujours écrire et un algorithme qui supprime un élément, l'enregistre dans une variable locale, ajoute l'élément précédemment contenu dans cette variable à la même file d'attente jusqu'à ce que le premier élément est vu à nouveau. Cela ne fonctionne que si vous pouvez identifier de manière unique un élément (par exemple, via un pointeur) et pas simplement quelque chose avec la même valeur. O (n) et utilise uniquement add () et remove (). Quoi qu’il en soit, il est plus facile d’optimiser que de trouver une raison de le faire, sauf en ce qui concerne les algorithmes.
Thraidh
0

Il est difficile d'imaginer une utilisation pour une telle implémentation, c'est vrai. Mais l' essentiel est de prouver que cela peut être fait .

En termes d'utilisations réelles de ces choses, cependant, je peux penser à deux. Une des utilisations possibles consiste à implémenter des systèmes dans des environnements soumis à des contraintes qui ne sont pas conçus : par exemple, les blocs redstone de Minecraft s’avèrent être un système complet de Turing, permettant aux utilisateurs d’implémenter des circuits logiques et même des processeurs entiers. Au tout début du jeu basé sur un script, beaucoup des premiers robots de jeu étaient également implémentés de cette façon.

Mais vous pouvez également appliquer ce principe à l’inverse, en veillant à ce que quelque chose ne soit pas possible dans un système alors que vous ne le souhaitez pas . Cela peut se produire dans des contextes de sécurité: par exemple, des systèmes de configuration puissants peuvent être un atout, mais il existe toujours des degrés de puissance que vous préféreriez peut-être ne pas donner aux utilisateurs. Cela limite ce que vous pouvez permettre au langage de configuration de faire, de peur qu'il ne soit renversé par un attaquant, mais dans ce cas, c'est ce que vous voulez.

Le Spooniest
la source