Contexte
Il y a plusieurs années, lorsque j'étais étudiant de premier cycle, on nous a confié un devoir d'analyse en amortissement. J'ai été incapable de résoudre l'un des problèmes. Je l’avais demandé en théorie , mais aucun résultat satisfaisant n’a été obtenu. Je me souviens du cours TA a insisté sur quelque chose qu'il ne pouvait pas prouver, et a dit qu'il avait oublié la preuve, et ... [vous savez quoi].
Aujourd'hui, j'ai rappelé le problème. J'avais encore hâte de savoir, alors la voici ...
La question
Est-il possible d'implémenter une pile utilisant deux files d'attente , de sorte que les opérations PUSH et POP s'exécutent dans le temps amorti O (1) ? Si oui, pourriez-vous me dire comment?
Note: La situation est assez facile si nous voulons implémenter une file d’ attente avec deux piles (avec les opérations correspondantes ENQUEUE et DEQUEUE ). S'il vous plaît observer la différence.
PS: Le problème ci-dessus n’est pas le devoir à la maison. Les devoirs ne nécessitaient aucune limite inférieure; juste une implémentation et l'analyse du temps d'exécution.
Réponses:
Je n'ai pas de réponse réelle, mais voici quelques preuves que le problème est ouvert:
Ce n'est pas mentionné dans Ming Li, Luc Longpré et Paul MB Vitányi, "Le pouvoir de la file d'attente", Structures 1986, qui considère plusieurs autres simulations étroitement liées
Martin Hühne n’a pas mentionné "Sur le pouvoir de plusieurs files", Theor. Comp. Sci. 1993, un article de suivi.
Ce n'est pas mentionné dans Holger Petersen, "Stacks versus Deques", COCOON 2001.
Burton Rosenberg, "Reconnaissance rapide non déterministe de langages sans contexte utilisant deux files", Inform. Proc. Lett. 1998, donne un algorithme O (n log n) à deux files d’attente permettant de reconnaître toute CFL utilisant deux files d'attente. Mais un automate à pression non déterministe peut reconnaître les LFC en temps linéaire. Donc, s'il y avait une simulation d'une pile avec deux files d'attente plus rapidement que O (log n) par opération, Rosenberg et ses arbitres auraient dû être au courant.
la source
La réponse ci-dessous est "tricher", en ce sens qu'elle n'utilise pas d'espace entre les opérations mais les opérations elles-mêmes peuvent utiliser plus que l' espace . Voir ailleurs dans ce fil pour une réponse qui n'a pas ce problème.O(1)
Bien que je n’aie pas de réponse à votre question exacte, j’ai trouvé un algorithme qui fonctionne en temps au lieu de . Je crois que c'est serré, bien que je n'ai pas de preuve. Si quelque chose se passe, l'algorithme montre qu'essayer de prouver une borne inférieure de est futile, cela pourrait donc aider à répondre à votre question.O(n)O(n)O(n−−√) O(n) O(n)
Je présente deux algorithmes, le premier étant un algorithme simple avec un temps d'exécution pour Pop et le second avec un temps d'exécution pour Pop. Je décris le premier principalement en raison de sa simplicité, de sorte que le second est plus facile à comprendre.O ( √O(n) O(n−−√)
Pour être plus précis: le premier n’utilise pas d’espace supplémentaire, a un push pire (et amorti) et un pop (et amorti), mais le pire comportement n’est pas toujours déclenché. Dans la mesure où elle n'utilise pas d'espace supplémentaire au-delà des deux files d'attente, elle est légèrement "meilleure" que la solution proposée par Ross Snider.O ( n )O(1) O(n)
La seconde utilise un seul champ entier (donc espace supplémentaire), a un cas le plus défavorable (et amorti) et un amorti Pop. Son temps de fonctionnement est donc nettement meilleur que celui de l'approche «simple», mais il utilise encore un peu d'espace supplémentaire.O ( 1 ) O ( √O(1) O(1) O(n−−√)
Le premier algorithme
Nous avons deux files d'attente: file d'attente en et file d'attente . sera notre "file d'attente", le sera la file déjà en ordre de pile.s e c o n d f i r s t s e c o n dfirst second first second
Code C # pour le premier algorithme
Cela pourrait être assez lisible, même si vous n’avez jamais vu C # auparavant. Si vous ne savez pas ce que sont les génériques, remplacez toutes les occurrences de «T» par «chaîne» dans votre esprit, pour une pile de chaînes.
Une analyse
De toute évidence, Push fonctionne dans le temps . Pop peut toucher tout ce qui est à l'intérieur en et en un nombre de fois constant, nous avons donc dans le pire des cas. L’algorithme présente ce comportement (par exemple) si l’on place éléments sur la pile, puis exécute de manière répétée une opération Push unique et une seule opération Pop successivement.f i r s t s e c o n d O ( n ) nO(1) first second O(n) n
Le deuxième algorithme
Nous avons deux files d'attente: file d'attente en et file d'attente . sera notre "file d'attente", le sera la file déjà en ordre de pile.first second first second
Il s'agit d'une version adaptée du premier algorithme, dans laquelle nous ne «brassons» pas immédiatement le contenu de à . Au lieu de cela, si contient un nombre suffisamment petit d’éléments par rapport à (à savoir la racine carrée du nombre d’éléments en ), nous ne le réorganisons qu’en dans l’ordre de pile et ne le fusionnons pas avec .first second first second second first second
Code C # pour le premier algorithme
Cela pourrait être assez lisible, même si vous n’avez jamais vu C # auparavant. Si vous ne savez pas ce que sont les génériques, remplacez toutes les occurrences de «T» par «chaîne» dans votre esprit, pour une pile de chaînes.
Une analyse
De toute évidence, Push fonctionne dans le temps .O(1)
Pop fonctionne en temps amorti . Il y a deux cas: if , puis nous mélangeons d' dans l'ordre des piles dans le temps . Si , nous devons avoir reçu au moins appels Push. Par conséquent, nous ne pouvons frapper ce cas que tous les appels à Push et Pop. Le temps d'exécution réel pour ce cas est , le temps amorti est donc .O(n−−√) |first|<|second|−−−−−−−√ first O(|first|)=O(n−−√) |first|≥|second|−−−−−−−√ n−−√ n−−√ O(n) O(nn√)=O(n−−√)
Note finale
Il est possible d’éliminer la variable supplémentaire au prix de la transformation Pop en une opération , en ayant Pop réorganisé d’ à chaque appel au lieu de laisser Push effectuer tout le travail.O(n−−√) first
la source
Après quelques commentaires sur ma réponse précédente, il m'est apparu clairement que je trichais plus ou moins: j'ai utilisé un espace supplémentaire ( espace supplémentaire dans le deuxième algorithme) lors de l'exécution de ma méthode Pop.O(n−−√)
L'algorithme suivant n'utilise aucun espace supplémentaire entre les méthodes et uniquement un espace supplémentaire lors de l'exécution de Push et Pop. Push a un temps d'exécution amorti et Pop a un temps d'exécution cas le plus défavorable (et amorti).O(1) O(n−−√) O(1)
Note aux modérateurs: Je ne suis pas tout à fait sûr si ma décision d’en faire une réponse distincte est correcte. J'ai pensé que je ne devrais pas effacer ma réponse originale, car elle pourrait encore être pertinente pour la question.
L'algorithme
Nous avons deux files d'attente: file d'attente en et file d'attente . sera notre "cache", le sera notre "stockage" principal. Les deux files d'attente seront toujours dans "l'ordre des piles". contiendra les éléments au sommet de la pile et le contiendra les éléments au bas de la pile. La taille du sera toujours au plus la racine carrée du .first second first second first second first second
Code C # pour le premier algorithme
Ce code devrait être assez lisible, même si vous n'avez jamais vu C # auparavant. Si vous ne savez pas ce que sont les génériques, remplacez toutes les occurrences de «T» par «chaîne» dans votre esprit, pour une pile de chaînes.
Une analyse
De toute évidence, Pop fonctionne en temps dans le pire des cas.O(1)
Push fonctionne en temps amorti . Il y a deux cas: if alors Push prend un temps . Si puis appuyez sur prend temps, mais après cette opération sera vide. Il faudra un temps pour obtenir ce cas à nouveau. Le temps amorti est donc .O(n−−√) |first|<|second|−−−−−−−√ O(n−−√) |first|≥|second|−−−−−−−√ O(n) first O(n−−√) O(nn√)=O(n−−√)
la source
first.Enqueue(first.Dequeue())
. Avez-vous mal tapé quelque chose?Je prétends que nous avons un coût amorti par opération . L'algorithme d'Alex donne la limite supérieure. Pour prouver la limite inférieure, je donne une séquence dans le pire des cas de mouvements PUSH et POP.Θ(N−−√)
La séquence la plus défavorable consiste en opérations PUSH, suivies des opérations PUSH et des opérations POP, suivies à nouveau des opérations PUSH et des opérations POP, etc. est:N N−−√ N−−√ N−−√ N−−√
Considérez la situation après les opérations initiales de PUSH. Quel que soit le fonctionnement de l’algorithme, au moins une des files d’attente doit contenir au moins entrées.N N/2
Examinons maintenant la tâche consistant à traiter avec le (premier ensemble de) opérations PUSH et POP. Toute tactique algorithmique, quelle qu'elle soit, doit tomber dans l'un des deux cas suivants:N−−√
Dans le premier cas, l'algorithme utilisera les deux files d'attente. La plus grande de ces files d'attente contient au moins entrées; nous devons donc encourir un coût d'au moins opérations de file d'attente afin de récupérer même un seul élément que nous ENQUEUONS et que nous devrons extraire ultérieurement de cette file d'attente plus longue.N/2 N/2
Dans le second cas, l'algorithme n'utilise pas les deux files d'attente. Cela réduit le problème à la simulation d'une pile avec une seule file d'attente. Même si cette file d’attente est initialement vide, nous ne pouvons pas faire mieux que l’utiliser comme une liste circulaire avec accès séquentiel, et il semble évident que nous devons utiliser au moins opérations de file d’attente en moyenne pour chacun des éléments suivants: les opérations de pile.N−−√/2 2N−−√
Dans les deux cas, nous avons eu besoin d'au moins fois (opérations de file d'attente) pour pouvoir gérer opérations de pile . Comme nous pouvons répéter ce processus fois, nous avons besoin de fois pour traiter au total les opérations de pile , ce qui donne une limite inférieure de temps amorti par opération. .N/2 2N−−√ N−−√ NN−−√/2 3N Ω(N−−√)
la source
Vous pouvez obtenir un ralentissement (amorti) si, après plusieurs et aucun , lorsque vous voyez un vous effectuez une séquence de mélanges parfaits à l'aide des deux files d'attente. Il a été prouvé par Diaconis, Graham et Cantor dans "The Mathematics of Perfect Shuffles" en 1983 que, avec un mélange parfait pouvait être réorganisé dans n'importe quel ordre. Par conséquent, vous pouvez conserver une file d'attente en tant que "file d'attente d'entrée" et une file d'attente en tant que "file d'attente de sortie" (similaire au cas des deux piles), puis lorsqu'un - est demandé et que la file d'attente de sortie est vide, vous effectuez une séquence de shuffle pour inverser la file d’entrée et la stocker dans la file d’attente.O(lgn) push pop pop O(lgn) pop
La seule question qui reste est de savoir si le schéma particulier de mélange parfait requis est suffisamment régulier pour ne pas nécessiter plus que mémoire.O(1)
Pour autant que je sache, c'est une nouvelle idée ...
la source
Sans utiliser d'espace supplémentaire, peut-être en utilisant une file d'attente priorisée et en forçant chaque nouvelle poussée à lui donner une priorité plus grande que la précédente? Ne serait toujours pas O (1) cependant.
la source
Je ne peux pas obtenir les files d'attente pour implémenter une pile en temps constant amorti. Cependant, je peux penser à un moyen de faire en sorte que deux files d'attente implémentent une pile au pire moment linéaire.
Bien sûr, nous pouvons ajouter un autre bit d'état externe qui nous dit si la dernière opération a été une poussée ou un pop. Nous pouvons retarder le transfert de tout d'une file d'attente à l'autre jusqu'à ce que nous ayons deux opérations push consécutives. Cela rend également l'opération pop légèrement plus compliquée. Cela nous donne une complexité amortie de O (1) pour l'opération pop. Malheureusement, la poussée reste linéaire.
Tout cela fonctionne car chaque fois qu'une opération push est effectuée, le nouvel élément est placé en tête d'une file vide et la file d'attente complète est ajoutée à la fin de celui-ci, inversant ainsi les éléments.
Si vous souhaitez obtenir des opérations amorties à temps constant, vous devrez probablement faire quelque chose de plus intelligent.
la source
Il existe une solution triviale, si votre file d'attente autorise le chargement en amont, qui ne nécessite qu'une seule file d'attente (ou, plus précisément, deque.) Peut-être est-ce le type de file d'attente que le cours du TA avait à l'esprit dans la question initiale?
Sans permettre le chargement frontal, voici une autre solution:
Cet algorithme nécessite deux files d'attente et deux pointeurs. Nous les appellerons respectivement Q1, Q2, primaire et secondaire. Lors de l'initialisation, Q1 et Q2 sont vides, les points principaux sont Q1 et les points secondaires sont Q2.
L'opération PUSH est triviale, elle consiste simplement:
L’opération POP est légèrement plus impliquée; il faut spouler tout le dernier élément de la file primaire, sauf le dernier, sur la deuxième file, échanger les pointeurs et renvoyer le dernier élément restant de la file initiale:
Aucune vérification des limites n'est effectuée et ce n'est pas O (1).
Pendant que je tape ceci, je vois que cela pourrait être fait avec une seule file d'attente utilisant une boucle for à la place d'une boucle while, comme Alex l'a fait. Dans les deux cas, l’opération PUSH est O (1) et l’opération POP devient O (n).
Voici une autre solution utilisant deux files d'attente et un pointeur, appelés respectivement Q1, Q2 et queue_p:
Lors de l'initialisation, Q1 et Q2 sont vides et queue_p pointe sur Q1.
Là encore, l'opération PUSH est simple, mais nécessite une étape supplémentaire consistant à pointer queue_p sur l'autre file d'attente:
L’opération POP est similaire à celle d’avant, mais il faut maintenant faire pivoter n / 2 éléments dans la file d’attente:
L'opération PUSH est toujours O (1), mais maintenant l'opération POP est O (n / 2).
Personnellement, pour ce problème, je préfère l’idée de mettre en oeuvre une seule file d’attente à deux extrémités (deque) et de l’appeler pile lorsque nous le voulons.
la source
Dans une direction (c. -à- limite supérieure), la ème file d' attente aura taille , et en même temps que les files d' attente inférieures de numéro, aura la plus récente éléments par pile (sauf si une pile a moins d'éléments; les éléments sont uniquement déplacés et non copiés). Nous maintenons cette contrainte en déplaçant des éléments entre les files d'attente, en effectuant les mouvements en bloc de manière à obtenir un temps par élément déplacé vers la file d'attente et vers la file d'attente . Chaque article est annoté par l'identité de sa pile et sa hauteur dans la pile; ceci n'est pas nécessaire si nous permettons à push d'être (ou si nous avons juste une pile et permettons un temps de saut amorti).i Θ(ni/k) Θ(ni/k) O(1) i+1 O(n1/k) i−1 Θ(n1/k)
Dans l’autre direction (c’est-à-dire la limite inférieure), nous pouvons continuer à ajouter des éléments jusqu’à ce que, pour certains , le ième élément le plus récent soit à la fin de chaque file le contenant, puis nous le demandons et répétons. Supposons que cela n'arrive pas assez. Ensuite, un nouvel élément doit généralement être ajouté à une file d'attente de taille . Pour conserver cette taille de file d'attente, les éléments doivent être déplacés avec la fréquence vers une autre file d'attente, dont la taille doit généralement être pour permettre une extraction suffisamment rapide des éléments après le déplacement. . En répétant cet argument, nous obtenons files d'attente d'une taille totale de (et en croissance), selon les besoins.m Ω ( m n 1 / k ) o ( n 1 / k ) Ω ( n 1 / k ) o ( n 2 / k ) k o ( n )m m Ω(mn1/k) o(n1/k) Ω(n1/k) o(n2/k) k o(n)
De plus, si une pile doit être vidée en une seule fois (avant de commencer à ajouter des éléments à nouveau), la performance optimale après amortissement est de (une pile utilisant deux files d'attente ou plus); cette performance peut être obtenue en utilisant (essentiellement) le tri par fusion.Θ(logn)
la source
Une pile peut être mise en œuvre en utilisant deux files d'attente en utilisant la seconde file d'attente comme exemple. Lorsque des éléments sont placés dans la pile, ils sont ajoutés à la fin de la file d'attente. Chaque fois qu'un élément est affiché, les n - 1 éléments de la première file d'attente doivent être déplacés vers le second tandis que l'élément restant est renvoyé. classe publique QueueStack implemen ts IStack {IQueue privée q1 = new Queue (); IQueue privée q2 = nouvelle file d'attente (); public vide push (E e) {q1.enqueue (e) // O (1)} public E pop (E e) {while (1 <q1.size ()) // O (n) {q2.enqueue ( q1.dequeue ()); } sw apQueues (); return q2.dequeue (); } p rivate void swapQueues () {IQueue Q = q2; q2 = q1; q1 = Q; }}
la source