Vorlesung Informatik 2 - Teil B: Theorie

3.2 Queue

Wird auch als FIFO bezeichnet: First In First Out.

Eine Queue ist eine Warteschlange, sie hat genau zwei Operationen:

Queue<T>:
   offfer(T x): void    fügt x in die Warteschlange ein
   poll():T                   holt das nächste Element aus der Warteschlange.

Wie bei dem Stack haben diese Operationen die Komplexität O(1), sind also unabhängig von der Größe der Queue.

In der Praxis hat eine Queue häufig noch weitere Methoden wie isEmpty, peek, size.

Anwendungen für Queues:

  • Warteschlange der rechenwilligen Prozesse im Betriebssystem. Wenn mehr Prozesse gerade eine CPU brauchen als die Zahl der logischen Prozessoren, werden die Prozesse in einer Queue angeordnet, die vom Scheduler verwaltet wird.
  • Druckerwarteschlange: gemeinsam genutzte Drucker im Netzwerk haben eine Warteschlange.
  • Internet: die Switche und Router im Netz haben Warteschlangen für die Pakete, die sie weiterschicken.
  • In verteilten Systemen spielen Warteschlangen eine entscheidende Rolle,
  • Mail: E-Mail MTA's (Mail Transfer Agent) nehmen E-MAils entgegen und senden sie weiter. Sie verwalten die Mails in Warteschlangen.
  • Spiele: Tetris, Snake,  Multiplayer-Spiele (Warten auf einen freien Raum in Overwatch u.s.w.), 
  • die Warteschlange beim Anruf einer Hotline
  • Verteilung des Covid-19 Impfstoffes,
  • Wareschlangen beim Kauf von begrenzten Artikeln, Versteigerungen, ...

Implementierung mit einem Array (Ringpuffer):

Zu Beginn wächst der Inhalt des Arrays um neue Elemente, d.h. die Queue wird von lins nach rechts aufgefüllt. Eine Queue mit den Elementen ABCDEFG steht also zunächst so in  einem Array der Länge 10:

 A B C D E F G H _ _  

Entfernt man die Element A, B und C und sieht das Array so aus

_ _ _ D E F G H _ _  
     ^       ^
    kopf     ende

d.h. der Anfang der Queue steht am Index 3 und das Ende am Index 7.  

Entfernt man D und E und fügt  I J K und L hinzu hat man folgende Situation:

K L _ _ _ F G H I J 
   ^       ^
 ende   kopf

Beim Einfügen eines neuen Elements und einer array-Länge n berechnet sich Ende zu 

     ende = (ende+1)%n

und das gleiche gilt beim Entfernen eines Elements für den Kopf:

     kopf = (kopf+1)%n

Diese Implementierung ist ideal für Warteschlangen die nur eine maximale Anzahl von Elementen aufnehmen kann (Blocking Queue). Muss das  Array vergrößert werden, hat die Umsortierung die Komplexität O(N). 


Implementierung mit zirkulär verketteter Liste:

Hier schreiben wir uns wieder eine innere Hilfsklasse wie beim Stack:

 private class Eintrag<T>{   // innere Klasse-------------------
         T inhalt;
         Eintrag next;      // Zeiger auf den nächsten Eintrag der verketten Liste
    }// Ende der inneren Klasse Eintrag ---------------------------

Die Queue Klasse selbst hat nur eine einzige Eigenschaft:    -ende: Eintrag

Ende                 Kopf
-------------------------
| F | E | D | C | B | A |
-------------------------

die Queue wird so abgebildet:

            --------------------------------------------------------------------
            |                                                                  |
            V                                                                  
          -------     -------     -------     -------     -------     -------   |
         | A | +---->| B | +---->| C | +---->| D | +---->| E | +---->| F | +----  -------- ------- ------- ------- ------- -------

d.h. die Variable ende zeigt auf den letzten Eintrag, während ende.next auf den Kopf der Warteschlange zeigt.

Ein neuer Eintrag wird jetzt so hinzugefügt ( offer(x) ):

       ende.next = new Eintrag(x, ende.next)
       ende = ende.next

Und das Entfernen des letzten Eintrags geht so (poll() ):

       ende.next = ende.next.next

In beiden Fällen müssen noch die Spezialfälle bedacht werden, dass die Queue leer (ende==null) ist bzw. nur einen Eintrag hat       (ende=ende.next). 

Lehrvideo  (YouTube)