Queues Page 1 Μάθημα 21: Ουρές (Queues) Η ουρά (queue) είναι μια δομή δεδομένων. Η βασική λειτουργικότητα είναι η εισαγωγή στοιχείων στην πίσω θέση και η εξαγωγή-διαγραφή στοιχείων από την μπροστινή θέση. Με αυτόν τον τρόπο, η ουρά είναι μια FIFO (First-In-First-Out, Πρώτο-Μέσα-Πρώτο-Έξω) δομή δεδομένων. Σε μια FIFO δομή δεδομένων, το πρώτο στοιχείο που εισάγεται στην ουρά θα είναι το πρώτο που θα αφαιρεθεί-εξυπηρετηθεί. Ουρές δεδομένων μπορούν να δημιουργηθούν με την χρήση του queue της πρότυπης βιβλιοθήκης (STL) της C++. Ορίζεται στο αρχείο επικεφαλίδας <queue> και η βασική λειτουργικότητά του είναι η δυνατότητα εισαγωγής στοιχείων στο πίσω μέρος της δομής και εξαγωγή στοιχείων από την αρχή του. Η βιβλιοθήκη <queue> παρέχει δύο είδη ουρών: Απλή ουρά δεδομένων όπου όλα τα στοιχεία εξυπηρετούνται με την σειρά που μπήκαν. Ουρά δεδομένων με δυνατότητα επιλογής προτεραιότητας (priority queue). Στην ουρά αυτή τα νέα στοιχεία που εισάγονται με προτεραιότητα τοποθετούνται μπροστά από όλα τα στοιχεία μικρότερης προτεραιότητας. Μέθοδοι Συνήθεις μέθοδοι από την βιβλιοθήκη queue για απλές ουρές και ουρές προτεραιότητας είναι οι: push(item) Εισαγωγή στοιχείου στην ουρά. Συγκεκριμένα εισάγει το αντικείμενο item στο πίσω μέρος της ουράς. Στην ουρά προτεραιότητας το στοιχείο τοποθετείται μέσα στην ουρά με βάση την προτεραιότητα που έχει. front() pop() back() top() empty() size() Επιστρέφει το πρώτο στοιχείο της ουράς χωρίς να το αφαιρέσει. Διαγράφει/αφαιρεί το στοιχείο που βρίσκεται στην αρχική/μπροστινή θέση μιας μη άδειας ουράς. Για την επιστροφή του στοιχείου αυτού θα πρέπει νωρίτερα να χρησιμοποιηθεί η front(). Επιστρέφει το τελευταίο στοιχείο της ουράς χωρίς να το αφαιρέσει. Επιστρέφει το στοιχείο με την μεγαλύτερη προτεραιότητα (για ουρές προτεραιότητας) χωρίς να το αφαιρέσει. Επιστρέφει True (αληθές) αν η ουρά είναι άδεια και False διαφορετικά. Επιστρέφει το συνολικό πλήθος των στοιχείων της ουράς.
Queues Page 2 Παράδειγμα 1 Ποιο θα είναι το αποτέλεσμα μετά την εκτέλεση του πιο κάτω κώδικα; #include <queue> #include <iostream> using namespace std; int main(){ queue< char > Q; Q.push('a'); Q.push('b'); Q.push('c'); Q.push('A'); Q.push('B'); Q.push('C'); cout << Q.front() << endl; cout << Q.back() << endl; Q.pop(); Q.pop(); while(!q.empty()) { cout << Q.size() << endl; cout << Q.front() << endl; Q.pop(); return 0; Ουρά προτεραιοτήτων (Priority Queue) Στην ουρά προτεραιότητας κατά την λειτουργία top()-pop() για να επιλεχθεί το στοιχείο με την προτεραιότητα συγκρίνονται όλα τα στοιχεία της ουράς με το τελεστή μικρότερο < και εξάγεται το μεγαλύτερο στοιχείο (μεγαλύτερη προτεραιότητα). Μπορούμε επίσης να ορίσουμε τη δική μας συνάρτηση ελέγχου προτεραιότητας. Παράδειγμα 2 Ποιο θα είναι το αποτέλεσμα μετά την εκτέλεση του πιο κάτω κώδικα; #include <queue> #include <iostream> using namespace std; int main() { priority_queue<int> pq; pq.push(120); pq.push(67);
Queues Page 3 pq.push(112); pq.push(23); pq.push(56); pq.push(94); pq.push(256); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); return 0; Παράδειγμα 3 Ποιο θα είναι το αποτέλεσμα μετά την εκτέλεση του πιο κάτω κώδικα; #include <iostream> #include <queue> using namespace std; int main(){ priority_queue <int, vector<int>, greater<int> > pq; pq.push(2); pq.push(5); pq.push(3); pq.push(1); pq.push(4); while (!pq.empty()) { cout << pq.top() << endl; pq.pop(); return 0; deque Το deque είναι ένα container όμοιο σε δυνατότητες με ένα vector. Η διαφορά τους είναι ότι, σε αντίθεση με το vector που επιτρέπει την εισαγωγή και διαγραφή στοιχείων μόνο στο τέλος (push_back(), pop_back()) το deque παρέχει αυτή τη δυνατότητα και στα δύο άκρα (push_back(), push_front(), pop_back(), pop_front()). Χρησιμοποιεί όλες τις υπόλοιπες συναρτήσεις που παρέχει το vector.
Queues Page 4 Παράδειγμα 4 #include <deque> #include <iostream> using namespace std; int main() { deque<int> d; for (int i = 0; i<10; i++) d.push_front(i); for (int i = 10; i < 20; i++) d.push_back(i); int f = d.front(); int b = d.back(); cout << "front element is " << f << endl; cout << "back element is " << b << endl; for (deque<int>::iterator it=d.begin(); it!=d.end(); ++it) cout << *it << " "; return 0; Αλγόριθμος BFS Ο Αλγόριθμος BFS υλοποιείται πολύ εύκολα με την STL χρησιμοποιώντας τα queue και map. Το queue αποθηκεύει τη σειρά με την οποία επισκεφτήκαμε τους κόμβους ενώ το map αν έχουμε επισκεφτέι ένα κόμβο καθώς και την απόσταση από τον κόμβο-αφετηρία. Αρχικά δηλώνουμε: #define TRvii(c, it) \ for (vii::iterator it = c.begin(); it!= c.end(); it++) typedef pair<int, int> ii; typedef vector<ii> vii; vector<vii> AdjList;
Queues Page 5 //BFS algorithm queue<int> q; map<int, int> dist; q.push(s); dist[s] = 0; while (!q.empty()){ int u = q.front(); q.pop(); TRvii (AdjList[u], it) // traverse each neighbour of u if (!dist.count(it->first)) { dist[it->first] = dist[u] + 1; q.push(it->first); Input: 13 16 10 15 15 20 20 25 10 30 30 47 47 50 25 45 45 65 15 35 35 55 20 40 50 55 35 40 55 60 40 60 60 65 Output: Distance: 0, Visited: 35 Distance: 1, Visited: 15 55 40 Distance: 2, Visited: 10 20 50 60 Distance: 3, Visited: 30 25 47 65 Distance: 4, Visited: 45 Path: 35 15 10 30 Αλγόριθμος του Dijkstra Με τη χρήση του priority queue μπορούμε να υλοποιήσουμε τον αλγόριθμο του Dijkstra. vector<int> dist(v, INF); dist[s] = 0; priority_queue<ii, vector<ii>, greater<ii> > pq; pq.push(ii(0, s)); while (!pq.empty()){ ii top = pq.top(); pq.pop(); int d = top.first, u = top.second; if (d == dist[u]) TRvii (AdjList[u], it){ int v = it->first, weight = it->second; if (dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; pq.push(ii(dist[v], v));
Queues Page 6 Input: 5 7 2 1 2 2 3 7 2 5 6 1 3 3 1 4 6 3 4 5 5 4 1 Output: Shortest Path(2,1)= 2 Shortest Path(2,2)= 0 Shortest Path(2,3)= 5 Shortest Path(2,4)= 7 Shortest Path(2,5)= 6 Επεξήγηση: 1. Στην αρχή έχουμε: dist[source] = dist[2] = 0, το pq είναι η ακμή {(0,2). 2. Από τον κόμβο 2, ελέγχουμε τους κόμβους {1, 3, 5. Τώρα dist[1] = 2, dist[3] = 7, και dist[5] = 6. Το περιεχόμενο του pq είναι οι {(2,1), (6,5), (7,3). 3. Ανάμεσα στους {1, 5, 3 μέσα στο pq, ο κόμβος 1 έχει το μικρότερο dist[1] = 2 και είναι στην κορυφή του pq. Αφαιρούμε το (2,1) και ελέγχουμε τους γείτονες: {3, 4 έτσι ώστε το dist[3] = min(dist[3], dist[1]+weight(1,3)) = min(7, 2+3) = 5 και dist[4] = 8. Τώρα το pq περιέχει τα {(5,3), (6,5), (7,3), (8,4). Βλέπουμε ότι υπάρχει δύο φορές ο κόμβος 3. Δεν μας επηρεάζει καθώς ο Dijkstra s θα επιλέξει το ζεύγος με το μικρότερο βάρος. 4. Αφαιρούμε το (5,3) και δοκιμάζουμε το (3,4), αλλά 5+5 = 10, ενώ dist[4] = 8 (από το μονοπάτι (2-1-4). Έτσι το dist[4] δεν αλλάζει. Το pq περιέχει τα {(6,5), (7,3), (8,4). 5. Αφαιρούμε το (6,5) και δοκιμάζουμε το (5, 4), αλλάζοντας το dist[4] = 7 (το συντομότερο μονοπάτι από το 2 στο 4 είναι τώρα το 2-5-4 αντί το 2-1-4). Το pq περιέχει τα {(7,3), (7,4), (8,4).
Queues Page 7 6. Ακολούθως, το (7,3) μπορεί να αγνοηθεί καθώς γνωρίζουμε ότι d > dist[3] (7 > 5). Μετά ελέγχουμε το (7,4) όπως προηγουμένως. Τέλος, η το (8,4) θα αγνοηθεί καθώς το d > dist[4] (8 > 7). Ο Dijkstra s σταματά καθώς το priority queue έχει αδειάσει. Αλγόριθμος Bellman Ford s Αν ο γράφος έχει αρνητικά βάρη ο αλγόριθμος του Dikstra πιθανόν να αποτύχει. Για παράδειγμα στον πιο κάτω γράφο ο Dijkstra αποτυγχάνει. Γιατί; Για γράφους με αρνητικά βάρη χρησιμοποιούμε τον αλγόριθμο Bellman Ford. Δουλεύει ακόμα και με αρνητικά βάρη ακμών Επισκέπτεται όλες τις ακμές V-1 φορές Η επανάληψη i βρίσκει όλα τα συντομότερα μονοπάτια που χρησιμοποιούν i ακμές Αν ο γράφος μας δεν είναι άκυκλος πρέπει να ελέγχουμε για αρνητικούς κύκλους (negative cycles). vector<int> dist(v, INF); dist[s] = 0; REP (i, 0, V - 1) REP (u, 0, V - 1) TRvii (AdjList[u], it) dist[it->first] = min(dist[it->first], dist[u] + it->second); bool negative_cycle_exist = false;
Queues Page 8 REP (u, 0, V - 1) TRvii (AdjList[u], it) if (dist[it->first] > dist[u] + it->second) negative_cycle_exist = true; if (!negative_cycle_exist) REP (i, 0, V - 1) cout << s << "->" << i<< "=" << dist[i] << endl;