Je suis tombé sur cette question: Mettre en place une file d'attente dans laquelle Push_rear (), pop_front () et get_min () sont tous des opérations à temps constant.
J'ai d'abord pensé à utiliser une structure de données min-tas qui a une complexité O(1) pour un get_min (). Mais Push_rear () et pop_front () seraient O (log (n)).
Quelqu'un sait-il quel serait le meilleur moyen d'implémenter une telle file d'attente comportant O(1) Push (), pop () et min ()?
J'ai googlé à ce sujet et je voulais signaler ce fil de discussion { Algorithm Geeks }. Mais il semble qu'aucune des solutions ne suivent la règle du temps constant pour les 3 méthodes: Push (), pop () et min ().
Merci pour toutes les suggestions.
Vous pouvez implémenter une pile avec O(1) pop (), Push () et get_min (): stockez simplement le minimum actuel avec chaque élément. Ainsi, par exemple, la pile [4,2,5,1]
(1 en haut) devient [(4,4), (2,2), (5,2), (1,1)]
.
Ensuite, vous pouvez utiliser deux piles pour implémenter la file d'attente . Poussez sur une pile, sautez d'une autre pile; si la deuxième pile est vide lors du pop-up, déplacez tous les éléments de la première pile à la seconde.
Par exemple, pour une requête pop
, en déplaçant tous les éléments de la première pile [(4,4), (2,2), (5,2), (1,1)]
, la deuxième pile serait [(1,1), (5,1), (2,1), (4,1)]
. et retourne maintenant l’élément supérieur de la deuxième pile.
Pour trouver l'élément minimum de la file d'attente, examinez les deux plus petits éléments des min-piles individuelles, puis prenez le minimum de ces deux valeurs. (Bien sûr, il y a une certaine logique supplémentaire dans le cas où l'une des piles est vide, mais ce n'est pas trop dur à contourner).
Il aura O(1) get_min()
et Push()
et amorti O(1) pop()
.
OK - Je pense avoir une réponse qui vous donne toutes ces opérations dans amortised O (1), ce qui signifie que toute opération peut prendre jusqu'à O (n), mais que toute séquence de n opérations prend O(1) temps par opération.
L'idée est de stocker vos données sous la forme d'un arbre cartésien . Il s'agit d'un arbre binaire obéissant à la propriété min-heap (chaque nœud n'est pas plus gros que ses enfants) et est ordonné de manière à ce qu'une traversée en ordre des nœuds vous rende les nœuds dans le même ordre dans lequel ils ont été ajoutés. Par exemple, voici un arbre cartésien pour la séquence 2 1 4 3 5
:
1
/ \
2 3
/ \
4 5
Il est possible d'insérer un élément dans un arbre cartésien dans O(1) temps amorti à l'aide de la procédure suivante. Regardez l'épine droite de l'arbre (le chemin de la racine à la feuille la plus à droite formée en marchant toujours vers la droite). En commençant au nœud le plus à droite, balayez vers le haut le long de ce chemin jusqu'à ce que vous trouviez le premier nœud plus petit que le nœud que vous insérez.
Modifiez ce noeud de sorte que son enfant de droite soit ce nouveau noeud, puis faites de l’enfant de droite précédent de ce noeud l’enfant de gauche du noeud que vous venez d’ajouter. Par exemple, supposons que nous voulions insérer une autre copie de 2 dans l’arborescence ci-dessus. Nous remontons l’épine droite après les 5 et 3, mais nous nous arrêtons sous le 1 parce que 1 <2. Nous changeons alors l’arbre pour ressembler à ceci:
1
/ \
2 2
/
3
/ \
4 5
Notez qu'une traversée en ordre donne 2 1 4 3 5 2, qui est la séquence dans laquelle nous avons ajouté les valeurs.
Ceci est amorti O(1) car nous pouvons créer une fonction potentielle égale au nombre de nœuds dans l’épine droite de l’arbre. Le temps réel nécessaire pour insérer un noeud est égal à 1 plus le nombre de noeuds de la colonne que nous considérons (appelons cela k). Une fois que nous avons trouvé l'endroit où insérer le nœud, la taille de l'épine dorsale est réduite de k-1, puisque chacun des k nœuds visités ne se trouve plus sur l'épine droite et que le nouveau nœud est à sa place. Cela donne un coût amorti de 1 + k + (1 - k) = 2 = O (1), pour l'amorti O(1) insert. Une autre façon de penser à cela est qu’une fois qu’un nœud a été retiré de la colonne vertébrale droite, il ne fait plus jamais partie de la colonne vertébrale droite et nous n’aurons donc plus besoin de le déplacer. Puisque chacun des n nœuds peut être déplacé au maximum une fois, cela signifie que n insertions peuvent effectuer au plus n déplacements. Le temps d’exécution total est donc au maximum O(n) pour un amortissement O(1) par élément.
Pour effectuer une étape de retrait de la file d'attente, nous supprimons simplement le nœud le plus à gauche de l'arbre cartésien. Si ce nœud est une feuille, nous avons terminé. Sinon, le nœud ne peut avoir qu'un seul enfant (le bon enfant) et nous le remplaçons donc par son bon enfant. À condition que nous gardions une trace de l'emplacement du nœud le plus à gauche, cette étape prend O(1) time. Cependant, après avoir retiré le noeud le plus à gauche et l'avoir remplacé par son enfant de droite, il est possible que nous ne sachions pas où se trouve le nouveau noeud. Pour résoudre ce problème, nous descendons simplement l’épine gauche de l’arbre en commençant par le nouveau nœud que nous venons de déplacer vers l’enfant le plus à gauche. Je prétends que cela fonctionne toujours dans O(1) temps amorti. Pour voir cela, je prétends qu'un nœud est visité au plus une fois au cours de l'une de ces passes pour trouver le nœud le plus à gauche. Pour voir cela, notez qu'une fois qu'un nœud a été visité de cette manière, la seule façon dont nous pourrions avoir besoin de le revoir serait s'il était déplacé d'un enfant du nœud le plus à gauche au nœud le plus à gauche. Mais tous les nœuds visités sont les parents du nœud le plus à gauche, cela ne peut donc pas arriver. Par conséquent, chaque nœud est visité au maximum une fois au cours de ce processus et la commande Pop s'exécute en O (1).
Nous pouvons faire find-min dans O(1) car l’arbre cartésien nous permet d’accéder gratuitement au plus petit élément de l’arbre; c'est la racine de l'arbre.
Enfin, pour vérifier que les nœuds reviennent dans le même ordre dans lequel ils ont été insérés, notez qu'un arbre cartésien stocke toujours ses éléments, de sorte qu'un parcours en ordre les visite dans un ordre trié. Comme nous supprimons toujours le nœud le plus à gauche à chaque étape et qu'il s'agit du premier élément de la traversée en ordre, nous récupérons toujours les nœuds dans l'ordre dans lequel ils ont été insérés.
En bref, nous obtenons O(1) Push and Pop amorti, et O(1) dans le pire des cas, find-min.
Si je peux arriver à une pire mise en œuvre O(1) - mise en œuvre, je la posterai définitivement. C'était un gros problème. merci de l'avoir posté!
Ok, voici une solution.
Premièrement, nous avons besoin de quelques éléments qui fournissent Push_back (), Push_front (), pop_back () et pop_front () dans 0 (1). C'est facile à implémenter avec array et 2 itérateurs. Le premier itérateur pointera vers l'avant, le second vers l'arrière. Appelons ces choses deque.
Voici le pseudo-code:
class MyQueue//Our data structure
{
deque D;//We need 2 deque objects
deque Min;
Push(element)//pushing element to MyQueue
{
D.Push_back(element);
while(Min.is_not_empty() and Min.back()>element)
Min.pop_back();
Min.Push_back(element);
}
pop()//poping MyQueue
{
if(Min.front()==D.front() )
Min.pop_front();
D.pop_front();
}
min()
{
return Min.front();
}
}
Explication:
Exemple poussons les nombres [12,5,10,7,11,19] et vers notre file d'attente MyQueue
1) en poussant 12
D [12]
Min[12]
2) en poussant 5
D[12,5]
Min[5] //5>12 so 12 removed
3) en poussant 10
D[12,5,10]
Min[5,10]
4) en poussant 7
D[12,5,10,7]
Min[5,7]
6) en poussant 11
D[12,5,10,7,11]
Min[5,7,11]
7) en poussant 19
D[12,5,10,7,11,19]
Min[5,7,11,19]
Appelons maintenant pop_front ()
nous avons
D[5,10,7,11,19]
Min[5,7,11,19]
Le minimum est 5
Appelons à nouveau pop_front ()
Explication: pop_front supprimera 5 de D, mais il fera aussi apparaître l'élément avant de Min, car il est égal à l'élément avant de D (5).
D[10,7,11,19]
Min[7,11,19]
Et minimum est 7. :)
Utilisez un deque (A) pour stocker les éléments et un autre deque (B) pour stocker les minimums.
Lorsque x est mis en file d'attente, envoyez-le Push_back à A et maintenez le pop_backing B jusqu'à ce que le dos de B soit inférieur à x, puis Push_back x à B.
lors de la mise en file d'attente de A, pop_front A est la valeur de retour, et si elle est égale à l'avant de B, pop_front B également.
pour obtenir le minimum de A, utilisez l'avant de B comme valeur de retour.
dequeue et getmin sont évidemment O (1). Pour l'opération de mise en file d'attente, considérons le Push_back de n éléments. Il y a n Push_back to A, n Push_back to B et au plus n pop_back de B car chaque élément restera soit dans B, soit sortira une fois de B. Au-dessus de tout, il y a O(3n) opérations et donc le Le coût amorti est également de O(1) pour la mise en file d'attente.
Enfin, cet algorithme fonctionne parce que, lorsque vous mettez en file d'attente x sur A, si certains éléments de B sont plus grands que x, ils ne seront jamais des minimums car x restera dans la file d'attente A plus longtemps que tous les éléments de B (une file d'attente). est FIFO). Par conséquent, nous devons extraire des éléments de B (de l’arrière) plus grands que x avant de pousser x dans B.
from collections import deque
class MinQueue(deque):
def __init__(self):
deque.__init__(self)
self.minq = deque()
def Push_rear(self, x):
self.append(x)
while len(self.minq) > 0 and self.minq[-1] > x:
self.minq.pop()
self.minq.append(x)
def pop_front(self):
x = self.popleft()
if self.minq[0] == x:
self.minq.popleft()
return(x)
def get_min(self):
return(self.minq[0])
Si cela ne vous dérange pas de stocker un peu de données supplémentaires, il devrait être facile de stocker la valeur minimale. Push and Pop peut mettre à jour la valeur si l'élément nouveau ou supprimé est le minimum, et renvoyer la valeur minimale est aussi simple que d'obtenir la valeur de la variable.
Ceci suppose que get_min () ne modifie pas les données; si vous préférez quelque chose comme pop_min () (c’est-à-dire supprimer l’élément minimum), vous pouvez simplement stocker un pointeur sur l’élément réel et sur l’élément le précédant (le cas échéant), et le mettre à jour en conséquence avec Push_rear () et pop_front () ainsi que.
Editer après les commentaires:
Cela conduit évidemment à O(n) Push and Pop dans le cas où le minimum change sur ces opérations, et ne satisfait donc pas strictement aux exigences.
Vous pouvez réellement utiliser une liste liée pour gérer la file d'attente.
Chaque élément dans LinkedList sera de type
class LinkedListElement
{
LinkedListElement next;
int currentMin;
}
Vous pouvez avoir deux pointeurs: un point vers le début et l’autre point vers la fin.
Si vous ajoutez un élément au début de la file d'attente. Examinez le pointeur de départ et le noeud à insérer. Si le noeud à insérer, currentmin est inférieur à start, le noeud à insérer est le minimum. Sinon, mettez à jour le currentmin avec start currentmin.
Répétez la même chose pour enque.
Les solutions à cette question, y compris le code, sont disponibles à l’adresse suivante: http://discuss.joelonsoftware.com/default.asp?interview.11.742223.32
Nous savons que Push et Pop sont des opérations à temps constant [O (1) pour être précis].
Mais lorsque nous pensons à get_min () [c.-à-d. Pour trouver le nombre minimal actuel dans la file], la première chose qui nous vient à l’esprit est de chercher dans toute la file chaque fois que la demande de l’élément minimum est faite. Mais cela ne donnera jamais l'opération à temps constant, qui est l'objectif principal du problème.
Ceci est généralement demandé très fréquemment dans les interviews, vous devez donc connaître le truc
Pour ce faire, nous devons utiliser deux autres files d'attente qui garderont la trace de l'élément minimum et nous devrons continuer à modifier ces 2 files d'attente tout comme nous le ferons pour les opérations Push et Pop sur la file d'attente de manière à obtenir cet élément minimum dans O(1) temps.
Voici le code Sudo auto-descriptif basé sur l'approche mentionnée ci-dessus.
Queue q, minq1, minq2;
isMinq1Current=true;
void Push(int a)
{
q.Push(a);
if(isMinq1Current)
{
if(minq1.empty) minq1.Push(a);
else
{
while(!minq1.empty && minq1.top < =a) minq2.Push(minq1.pop());
minq2.Push(a);
while(!minq1.empty) minq1.pop();
isMinq1Current=false;
}
}
else
{
//mirror if(isMinq1Current) branch.
}
}
int pop()
{
int a = q.pop();
if(isMinq1Current)
{
if(a==minq1.top) minq1.pop();
}
else
{
//mirror if(isMinq1Current) branch.
}
return a;
}
#include <iostream>
#include <queue>
#include <deque>
using namespace std;
queue<int> main_queue;
deque<int> min_queue;
void clearQueue(deque<int> &q)
{
while(q.empty() == false) q.pop_front();
}
void PushRear(int elem)
{
main_queue.Push(elem);
if(min_queue.empty() == false && elem < min_queue.front())
{
clearQueue(min_queue);
}
while(min_queue.empty() == false && elem < min_queue.back())
{
min_queue.pop_back();
}
min_queue.Push_back(elem);
}
void PopFront()
{
int elem = main_queue.front();
main_queue.pop();
if (elem == min_queue.front())
{
min_queue.pop_front();
}
}
int GetMin()
{
return min_queue.front();
}
int main()
{
PushRear(1);
PushRear(-1);
PushRear(2);
cout<<GetMin()<<endl;
PopFront();
PopFront();
cout<<GetMin()<<endl;
return 0;
}
Cette solution contient 2 files d'attente:
1. main_q - stocke les nombres saisis.
2. min_q - stocke les nombres minimaux selon certaines règles que nous avons décrites (apparaissent dans les fonctions MainQ.enqueue (x), MainQ.dequeue (), MainQ.get_min ()).
Voici le code en Python. La file d'attente est implémentée à l'aide d'une liste.
L’idée principale réside dans les fonctions MainQ.enqueue (x), MainQ.dequeue (), MainQ.get_min ().
Une hypothèse clé est que vider une file d'attente prend 0 (0).
Un test est fourni à la fin.
import numbers
class EmptyQueueException(Exception):
pass
class BaseQ():
def __init__(self):
self.l = list()
def enqueue(self, x):
assert isinstance(x, numbers.Number)
self.l.append(x)
def dequeue(self):
return self.l.pop(0)
def peek_first(self):
return self.l[0]
def peek_last(self):
return self.l[len(self.l)-1]
def empty(self):
return self.l==None or len(self.l)==0
def clear(self):
self.l=[]
class MainQ(BaseQ):
def __init__(self, min_q):
super().__init__()
self.min_q = min_q
def enqueue(self, x):
super().enqueue(x)
if self.min_q.empty():
self.min_q.enqueue(x)
Elif x > self.min_q.peek_last():
self.min_q.enqueue(x)
else: # x <= self.min_q.peek_last():
self.min_q.clear()
self.min_q.enqueue(x)
def dequeue(self):
if self.empty():
raise EmptyQueueException("Queue is empty")
x = super().dequeue()
if x == self.min_q.peek_first():
self.min_q.dequeue()
return x
def get_min(self):
if self.empty():
raise EmptyQueueException("Queue is empty, NO minimum")
return self.min_q.peek_first()
INPUT_NUMS = (("+", 5), ("+", 10), ("+", 3), ("+", 6), ("+", 1), ("+", 2), ("+", 4), ("+", -4), ("+", 100), ("+", -40),
("-",None), ("-",None), ("-",None), ("+",-400), ("+",90), ("-",None),
("-",None), ("-",None), ("-",None), ("-",None), ("-",None), ("-",None), ("-",None), ("-",None))
if __== '__main__':
min_q = BaseQ()
main_q = MainQ(min_q)
try:
for operator, i in INPUT_NUMS:
if operator=="+":
main_q.enqueue(i)
print("Added {} ; Min is: {}".format(i,main_q.get_min()))
print("main_q = {}".format(main_q.l))
print("min_q = {}".format(main_q.min_q.l))
print("==========")
else:
x = main_q.dequeue()
print("Removed {} ; Min is: {}".format(x,main_q.get_min()))
print("main_q = {}".format(main_q.l))
print("min_q = {}".format(main_q.min_q.l))
print("==========")
except Exception as e:
print("exception: {}".format(e))
Le résultat du test ci-dessus est:
"C:\Program Files\Python35\python.exe" C:/dev/python/py3_pocs/proj1/priority_queue.py
Added 5 ; Min is: 5
main_q = [5]
min_q = [5]
==========
Added 10 ; Min is: 5
main_q = [5, 10]
min_q = [5, 10]
==========
Added 3 ; Min is: 3
main_q = [5, 10, 3]
min_q = [3]
==========
Added 6 ; Min is: 3
main_q = [5, 10, 3, 6]
min_q = [3, 6]
==========
Added 1 ; Min is: 1
main_q = [5, 10, 3, 6, 1]
min_q = [1]
==========
Added 2 ; Min is: 1
main_q = [5, 10, 3, 6, 1, 2]
min_q = [1, 2]
==========
Added 4 ; Min is: 1
main_q = [5, 10, 3, 6, 1, 2, 4]
min_q = [1, 2, 4]
==========
Added -4 ; Min is: -4
main_q = [5, 10, 3, 6, 1, 2, 4, -4]
min_q = [-4]
==========
Added 100 ; Min is: -4
main_q = [5, 10, 3, 6, 1, 2, 4, -4, 100]
min_q = [-4, 100]
==========
Added -40 ; Min is: -40
main_q = [5, 10, 3, 6, 1, 2, 4, -4, 100, -40]
min_q = [-40]
==========
Removed 5 ; Min is: -40
main_q = [10, 3, 6, 1, 2, 4, -4, 100, -40]
min_q = [-40]
==========
Removed 10 ; Min is: -40
main_q = [3, 6, 1, 2, 4, -4, 100, -40]
min_q = [-40]
==========
Removed 3 ; Min is: -40
main_q = [6, 1, 2, 4, -4, 100, -40]
min_q = [-40]
==========
Added -400 ; Min is: -400
main_q = [6, 1, 2, 4, -4, 100, -40, -400]
min_q = [-400]
==========
Added 90 ; Min is: -400
main_q = [6, 1, 2, 4, -4, 100, -40, -400, 90]
min_q = [-400, 90]
==========
Removed 6 ; Min is: -400
main_q = [1, 2, 4, -4, 100, -40, -400, 90]
min_q = [-400, 90]
==========
Removed 1 ; Min is: -400
main_q = [2, 4, -4, 100, -40, -400, 90]
min_q = [-400, 90]
==========
Removed 2 ; Min is: -400
main_q = [4, -4, 100, -40, -400, 90]
min_q = [-400, 90]
==========
Removed 4 ; Min is: -400
main_q = [-4, 100, -40, -400, 90]
min_q = [-400, 90]
==========
Removed -4 ; Min is: -400
main_q = [100, -40, -400, 90]
min_q = [-400, 90]
==========
Removed 100 ; Min is: -400
main_q = [-40, -400, 90]
min_q = [-400, 90]
==========
Removed -40 ; Min is: -400
main_q = [-400, 90]
min_q = [-400, 90]
==========
Removed -400 ; Min is: 90
main_q = [90]
min_q = [90]
==========
exception: Queue is empty, NO minimum
Process finished with exit code 0
Implémentation Java
import Java.io.*;
import Java.util.*;
public class queueMin {
static class stack {
private Node<Integer> head;
public void Push(int data) {
Node<Integer> newNode = new Node<Integer>(data);
if(null == head) {
head = newNode;
} else {
Node<Integer> prev = head;
head = newNode;
head.setNext(prev);
}
}
public int pop() {
int data = -1;
if(null == head){
System.out.println("Error Nothing to pop");
} else {
data = head.getData();
head = head.getNext();
}
return data;
}
public int peek(){
if(null == head){
System.out.println("Error Nothing to pop");
return -1;
} else {
return head.getData();
}
}
public boolean isEmpty(){
return null == head;
}
}
static class stackMin extends stack {
private stack s2;
public stackMin(){
s2 = new stack();
}
public void Push(int data){
if(data <= getMin()){
s2.Push(data);
}
super.Push(data);
}
public int pop(){
int value = super.pop();
if(value == getMin()) {
s2.pop();
}
return value;
}
public int getMin(){
if(s2.isEmpty()) {
return Integer.MAX_VALUE;
}
return s2.peek();
}
}
static class Queue {
private stackMin s1, s2;
public Queue(){
s1 = new stackMin();
s2 = new stackMin();
}
public void enQueue(int data) {
s1.Push(data);
}
public int deQueue() {
if(s2.isEmpty()) {
while(!s1.isEmpty()) {
s2.Push(s1.pop());
}
}
return s2.pop();
}
public int getMin(){
return Math.min(s1.isEmpty() ? Integer.MAX_VALUE : s1.getMin(), s2.isEmpty() ? Integer.MAX_VALUE : s2.getMin());
}
}
static class Node<T> {
private T data;
private T min;
private Node<T> next;
public Node(T data){
this.data = data;
this.next = null;
}
public void setNext(Node<T> next){
this.next = next;
}
public T getData(){
return this.data;
}
public Node<T> getNext(){
return this.next;
}
public void setMin(T min){
this.min = min;
}
public T getMin(){
return this.min;
}
}
public static void main(String args[]){
try {
FastScanner in = newInput();
PrintWriter out = newOutput();
// System.out.println(out);
Queue q = new Queue();
int t = in.nextInt();
while(t-- > 0) {
String[] inp = in.nextLine().split(" ");
switch (inp[0]) {
case "+":
q.enQueue(Integer.parseInt(inp[1]));
break;
case "-":
q.deQueue();
break;
case "?":
out.println(q.getMin());
default:
break;
}
}
out.flush();
out.close();
} catch(IOException e){
e.printStackTrace();
}
}
static class FastScanner {
static BufferedReader br;
static StringTokenizer st;
FastScanner(File f) {
try {
br = new BufferedReader(new FileReader(f));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public FastScanner(InputStream f) {
br = new BufferedReader(new InputStreamReader(f));
}
String next() {
while (st == null || !st.hasMoreTokens()) {
try {
st = new StringTokenizer(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
return st.nextToken();
}
String nextLine(){
String str = "";
try {
str = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return str;
}
int nextInt() {
return Integer.parseInt(next());
}
long nextLong() {
return Long.parseLong(next());
}
double nextDoulbe() {
return Double.parseDouble(next());
}
}
static FastScanner newInput() throws IOException {
if (System.getProperty("JUDGE") != null) {
return new FastScanner(new File("input.txt"));
} else {
return new FastScanner(System.in);
}
}
static PrintWriter newOutput() throws IOException {
if (System.getProperty("JUDGE") != null) {
return new PrintWriter("output.txt");
} else {
return new PrintWriter(System.out);
}
}
}
Implémentation de JavaScript
(Merci à la solution d'adamax pour l'idée; I vaguement a basé une implémentation sur celle-ci. Passez au bas de la page pour voir le code entièrement commenté ou lisez les étapes générales ci-dessous. Notez que ceci trouve la valeur maximale dans O(1) temps constant plutôt que la valeur minimale - facile à modifier):
L'idée générale est de créer deux piles lors de la construction de
MaxQueue
(j'ai utilisé une liste chaînée en tant que structure de donnéesStack
sous-jacente - non incluse dans le code; toutefois, touteStack
fera l'affaire tant qu'elle est implémentée avec O(1) insertion/suppression). Une nous allons principalementpop
de (dqStack
) et une autrePush
à (eqStack
).
Pour
enqueue
, siMaxQueue
est vide, nous allonsPush
la valeur àdqStack
ainsi que la valeur maximale actuelle dans un Tuple (la même valeur puisqu'il s'agit de la seule valeur dansMaxQueue
); par exemple.:
const m = new MaxQueue();
m.enqueue(6);
/*
the dqStack now looks like:
[6, 6] - [value, max]
*/
Si
MaxQueue
n'est pas vide, nousPush
uniquement le valeur àeqStack
;
m.enqueue(7);
m.enqueue(8);
/*
dqStack: eqStack: 8
[6, 6] 7 - just the value
*/
puis, mettez à jour la valeur maximale dans le tuple.
/*
dqStack: eqStack: 8
[6, 8] 7
*/
Pour
dequeue
, nous allonspop
à partir dedqStack
et renverrons la valeur à partir du tuple.
m.dequeue();
> 6
// equivalent to:
/*
const Tuple = m.dqStack.pop() // [6, 8]
Tuple[0];
> 6
*/
Ensuite, si
dqStack
est vide, déplacez toutes les valeurs deeqStack
versdqStack
, par exemple:
// if we build a MaxQueue
const maxQ = new MaxQueue(3, 5, 2, 4, 1);
/*
the stacks will look like:
dqStack: eqStack: 1
4
2
[3, 5] 5
*/
Au fur et à mesure que chaque valeur est déplacée, nous allons vérifier si elle est supérieure au max jusqu'ici et la stocker dans chaque tuple
maxQ.dequeue(); // pops from dqStack (now empty), so move all from eqStack to dqStack
> 3
// as dequeue moves one value over, it checks if it's greater than the ***previous max*** and stores the max at Tuple[1], i.e., [data, max]:
/*
dqStack: [5, 5] => 5 > 4 - update eqStack:
[2, 4] => 2 < 4 - no update
[4, 4] => 4 > 1 - update
[1, 1] => 1st value moved over so max is itself empty
*/
Étant donné que chaque valeur est déplacée vers
dqStack
au plus une fois, nous pouvons dire quedequeue
a O(1) complexité du temps amorti.
Ensuite, à tout moment, nous pouvons appeler
getMax
pour récupérer la valeur maximale actuelle dans O(1) temps constant. Tant queMaxQueue
n'est pas vide, la valeur maximale est facilement extraite du prochain tuple dansdqStack
.
maxQ.getMax();
> 5
// equivalent to calling peek on the dqStack and pulling out the maximum value:
/*
const peekedTuple = maxQ.dqStack.peek(); // [5, 5]
peekedTuple[1];
> 5
*/
class MaxQueue {
constructor(...data) {
// create a dequeue Stack from which we'll pop
this.dqStack = new Stack();
// create an enqueue Stack to which we'll Push
this.eqStack = new Stack();
// if enqueueing data at construction, iterate through data and enqueue each
if (data.length) for (const datum of data) this.enqueue(datum);
}
enqueue(data) { // O(1) constant insertion time
// if the MaxQueue is empty,
if (!this.peek()) {
// Push data to the dequeue Stack and indicate it's the max;
this.dqStack.Push([data, data]); // e.g., enqueue(8) ==> [data: 8, max: 8]
} else {
// otherwise, the MaxQueue is not empty; Push data to enqueue Stack
this.eqStack.Push(data);
// save a reference to the Tuple that's next in line to be dequeued
const next = this.dqStack.peek();
// if the enqueueing data is > the max in that Tuple, update it
if (data > next[1]) next[1] = data;
}
}
moveAllFromEqToDq() { // O(1) amortized as each value will move at most once
// start max at -Infinity for comparison with the first value
let max = -Infinity;
// until enqueue Stack is empty,
while (this.eqStack.peek()) {
// pop from enqueue Stack and save its data
const data = this.eqStack.pop();
// if data is > max, set max to data
if (data > max) max = data;
// Push to dequeue Stack and indicate the current max; e.g., [data: 7: max: 8]
this.dqStack.Push([data, max]);
}
}
dequeue() { // O(1) amortized deletion due to calling moveAllFromEqToDq from time-to-time
// if the MaxQueue is empty, return undefined
if (!this.peek()) return;
// pop from the dequeue Stack and save it's data
const [data] = this.dqStack.pop();
// if there's no data left in dequeue Stack, move all data from enqueue Stack
if (!this.dqStack.peek()) this.moveAllFromEqToDq();
// return the data
return data;
}
peek() { // O(1) constant peek time
// if the MaxQueue is empty, return undefined
if (!this.dqStack.peek()) return;
// peek at dequeue Stack and return its data
return this.dqStack.peek()[0];
}
getMax() { // O(1) constant time to find maximum value
// if the MaxQueue is empty, return undefined
if (!this.peek()) return;
// peek at dequeue Stack and return the current max
return this.dqStack.peek()[1];
}
}