Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Piotr Szawdyński
Biblioteki C++

Adapter kolejki priorytetowej (std::priority_queue)

[lekcja] 4. Adapter kolejki priorytetowej (std::priority_queue).

4.1. Co to jest kolejka priorytetowa?

Kolejka priorytetowa jest to struktura danych, w której dostęp do danych jest możliwy tylko do pierwszego elementu, a dane w niej są uporządkowane nierosnąco (lub niemalejąco). Kolejka priorytetowa jest określana mianem FIFO (First In First Out), czyli pierwszy wchodzący element jest pierwszym wychodzącym.

4.1.1. Co to znaczy uporządkowanie nierosnące?

Uporządkowanie nierosnące jest to taki porządek danych, w którym każda następna wartość jest mniejsza lub równa od swojego poprzednika.

Przykładowy porządek liczb nierosnący:
50,40,40,39,30,18,18,2,1,1,0

4.1.2. Co to znaczy uporządkowanie niemalejące?

Uporządkowanie niemalejące jest to taki porządek danych, w którym każda następna wartość jest większa lub równa od swojego poprzednika.

Przykładowy porządek liczb niemalejący:
1,1,2,3,4,33,77,87,88,88,99

4.2. Operacje na kolejce priorytetowej

Kolejka priorytetowa w STL'u posiada następujące operacje:

  • push - umieszczenie nowego elementu na końcu kolejki priorytetowej;
  • pop - usunięcie istniejącego elementu z początku kolejki priorytetowej;
  • empty - informacja czy kolejka priorytetowa jest pusta;
  • size - zwraca ilość elementów umieszczonych w kolejce priorytetowej;
  • top - zwraca wartość pierwszego elementu w kolejce priorytetowej.

4.3. Jak wykorzystywać adapter kolejki priorytetowej w C++

Aby móc skorzystać z kolejki priorytetowej w C++, należy dołączyć plik nagłówkowy:

C/C++
#include <queue>

Kolejnym krokiem, jaki musimy wykonać jest utworzenie kolejki priorytetowej. Zapis uogólniony wygląda tak:

C/C++
std::priority_queue < TYP_DANYCH > nazwa_dla_kolejki_priorytetowej;

Kolejka priorytetowa może przechowywać dowolny typ danych - mogą być to typy, które już znasz czyli:

C/C++
std::priority_queue < int > kolejka1; //kolejka priorytetowa przechowująca liczby
std::priority_queue < std::string > kolejka2; //kolejka priorytetowa przechowująca stringi (czyli tekst)

Kolejka priorytetowa umożliwia również przechowywanie własnych typów danych, takich jak struktury czy klasy. Nie jest to jednak już tak proste, jak w przypadku adaptera zwykłej kolejki czy też stosu. Aby mieć możliwość umieszczania własnych struktur danych w kolejce priorytetowej musimy poinformować adapter w jaki sposób dane powinny być porządkowane. Zagadnienie to zostanie omówione po przedstawieniu metod kontenera i sposobie korzystania z nich. Jedyne co warto teraz wiedzieć to fakt, że sposób korzystania z metod kolejki priorytetowej pozostanie taki sam i praktycznie wogóle nie będzie wymagana ingerencja w kod poza wierszem w którym tworzysz kontener.

4.4. Umieszczanie nowego elementu w kolejce priorytetowej (metoda: push)

Metoda push służy do umieszczenia nowego elementu na końcu kolejki priorytetowej.
void push( const TYP_DANYCH & wartosc );
Standardowa złożoność obliczeniowa metody: O(log(n)).

4.4.1. Przykład

C/C++
#include <queue>
int main()
{
    std::priority_queue < int > kolejkaLiczb;
    kolejkaLiczb.push( 123 );
    kolejkaLiczb.push( 12 );
    kolejkaLiczb.push( 55 );
    return 0;
}

4.5. Odczytywanie pierwszego elementu w kolejce priorytetowej (metoda: top)

Metoda top służy do odczytania wartości umieszczonej na początku kolejki priorytetowej. Modyfikacja wartości bezpośrednio nie jest możliwa.
const TYP_DANYCH & top();
Standardowo zwracana wartość: największa.
Standardowa złożoność obliczeniowa metody: O(1).

4.5.1. Przykład

C/C++
#include <queue>
#include <iostream>
#include <conio.h>

int main()
{
    std::priority_queue < int > kolejkaLiczb;
   
    std::cout << "Podaj liczbe: ";
    int liczba;
    std::cin >> liczba;
   
    kolejkaLiczb.push( liczba );
    kolejkaLiczb.push( 222 );
    kolejkaLiczb.push( 555 );
   
    std::cout << "Pierwsza liczba w kolejce priorytetowej to: " << kolejkaLiczb.top() << std::endl;
   
    kolejkaLiczb.push( kolejkaLiczb.top() + 55 );
    std::cout << "Dodalem nowy element do kolejki priorytetowej." << std::endl;
   
    std::cout << "Pierwsza liczba w kolejce priorytetowej to: " << kolejkaLiczb.top() << std::endl;
    getch();
    return 0;
}

4.6. Pobranie informacji o ilości elementów w kolejce priorytetowej (metoda: size)

Metoda size zwraca ilość elementów aktualnie znajdujących się w kolejce priorytetowej.
size_type size() const;
Standardowa złożoność obliczeniowa metody: O(1).

4.6.1. Przykład

C/C++
#include <queue>
#include <iostream>
#include <conio.h>

int main()
{
    std::priority_queue < int > kolejkaLiczb;
   
    int liczba = 0;
    do
    {
        std::cout << "Podaj liczbe (0 - konczy wprowadzanie liczb): ";
        liczba = 0;
        std::cin >> liczba;
        if( liczba != 0 ) kolejkaLiczb.push( liczba );
       
    } while( liczba != 0 );
   
    std::cout << "W kolejce priorytetowej znajduje sie " << kolejkaLiczb.size() << " liczb." << std::endl;
   
    getch();
    return 0;
}

4.7. Sprawdzanie czy kolejka priorytetowa jest pusta (metoda: empty)

Metoda empty sprawdza, czy kolejka priorytetowa jest pusta.
bool empty() const;
Standardowa złożoność obliczeniowa metody: O(1).

4.7.1. Przykład

C/C++
#include <queue>
#include <iostream>
#include <conio.h>

int main()
{
    std::priority_queue < int > kolejkaLiczb;
   
    if( kolejkaLiczb.empty() )
    {
        std::cout << "Kolejka jest pusta." << std::endl;
    } else
    {
        std::cout << "W kolejce znajduje sie przynajmniej jeden element." << std::endl;
    }
   
    kolejkaLiczb.push( 1234 );
    kolejkaLiczb.push( 124 );
    kolejkaLiczb.push( 34 );
    kolejkaLiczb.push( 4 );
    if( kolejkaLiczb.empty() )
    {
        std::cout << "Kolejka jest pusta." << std::endl;
    } else
    {
        std::cout << "W kolejce znajduje sie przynajmniej jeden element." << std::endl;
    }
   
    getch();
    return 0;
}

4.8. Usunięcie istniejącego elementu z początku kolejki priorytetowej (metoda: pop)

Metoda pop służy do usunięcia istniejącego elementu z początku kolejki priorytetowej.
void pop();
Standardowo usuwana wartość: największa.
Standardowa złożoność obliczeniowa metody: O(log(n)).

4.8.1. Przykład

C/C++
#include <queue>
#include <iostream>
#include <conio.h>

int main()
{
    std::queue < int > kolejkaLiczb;
   
    int liczba = 0;
    do
    {
        std::cout << "Podaj liczbe (0 - konczy wprowadzanie liczb): ";
        liczba = 0;
        std::cin >> liczba;
        if( liczba != 0 ) kolejkaLiczb.push( liczba );
       
    } while( liczba != 0 );
   
    std::cout << "Liczby wyjete z kolejki: ";
    while( kolejkaLiczb.empty() == false )
    {
        std::cout << kolejkaLiczb.front() << ", ";
        kolejkaLiczb.pop();
    }
   
    getch();
    return 0;
}

4.9. Ustawianie kierunku sortowania danych

Jeśli chcemy z góry określić kierunek sortowania dla danych lub poprostu go odwrócić możemy zrobić to w bardzo prosty sposób - wystarczy zmienić zapis tworzący kolejkę priorytetową na jeden z następujących:

C/C++
std::priority_queue < int, std::vector < int >, std::less < int > > k1; //malejąco - domyślnie
std::priority_queue < int, std::vector < int >, std::greater < int > > k2; //rosnąco

Ostatni parametr szablonu kolejki priorytetowej to funktor, który określa sposób porównywania dwóch elementów w kolejce priorytetowej. STL posiada jeszcze parę innych funktorów do porównywania danych, jednak w przypadku kolejki priorytetowej zastosowanie ich byłoby bezsensowne.

4.10. Wykorzystywanie własnych typów danych w kolejce priorytetowej

Korzystanie z kolejki priorytetowej dla standardowych typów danych dostępnych w C++ jest bardzo proste, jednak znacznie częściej będziemy potrzebowali własnego typu danych, czyli struktury lub klasy. Załóżmy więc, że mamy następującą strukturę danych:

C/C++
struct SOsoba
{
    std::string imie;
    std::string nazwisko;
    int kolejnosc;
    std::string inneDane;
};

Dla powyższej struktury danych utwórzmy teraz kontener na dane w oparciu o adaper kolejki priorytetowej:

C/C++
std::priority_queue < SOsoba > kolejkaPriorytetowa;

Pomimo, iż powyższy zapis wydaje się oczywisty na bazie tego, co się do tej pory nauczyłeś kompilator zwróci nam następujące błędy kompilacji:

Kompilator: Default compiler
Wykonywanie  g++.exe...
C:/Dev-Cpp/include/c++/3.4.2/bits/stl_function.h: In member function `bool std::less<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = SOsoba]':
C:/Dev-Cpp/include/c++/3.4.2/bits/stl_heap.h:279:   instantiated from `void std::__adjust_heap(_RandomAccessIterator, _Distance, _Distance, _Tp, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<SOsoba*, std::vector<SOsoba, std::allocator<SOsoba> > >, _Distance = int, _Tp = SOsoba, _Compare = std::less<SOsoba>]'
C:/Dev-Cpp/include/c++/3.4.2/bits/stl_heap.h:404:   instantiated from `void std::make_heap(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<SOsoba*, std::vector<SOsoba, std::allocator<SOsoba> > >, _Compare = std::less<SOsoba>]'

C:/Dev-Cpp/include/c++/3.4.2/bits/stl_queue.h:369:   instantiated from `std::priority_queue<_Tp, _Sequence, _Compare>::priority_queue(const _Compare&, const _Sequence&) [with _Tp = SOsoba, _Sequence = std::vector<SOsoba, std::allocator<SOsoba> >, _Compare = std::less<SOsoba>]'
D:/jakisPlik.cpp:12:   instantiated from here
C:/Dev-Cpp/include/c++/3.4.2/bits/stl_function.h:227: error: no match for 'operator<' in '__x < __y'

Wykonanie zakończone

Możesz w tym miejscu zacząć się zastanawiać po co się uczyć kontenera w którym nie można własnego typu danych używać. Jeśli tak myślisz, to jesteś w błędzie. STL został zaprojektowany tak, aby był on zarówno wydajny jak i elastyczny.

4.10.1. Analizujemy przyczynę wystąpienia błędu

Chcąc zostać prawdziwym programistą nie możemy skupiać się tylko i wyłącznie na szukaniu gotowych rozwiązań problemów. Napotykając problemy musisz nauczyć się je analizować i szukać przyczyny błędu. Przeprowadźmy więc krótką analizę tego co wiemy i postawmy tyle pytań ile się da oraz udzielmy na nie odpowiedzi, jeśli są one oczywiste:

  • Problemów z std:: queue nie było dla struktur
  • Problemy z std:: priority_queue są.

Dlaczego? Czym się one różnią te adaptery od siebie?

  • Adapter std:: queue dodaje dane zawsze na końcu
  • Adapter std:: priority_queue [b]porządkuje[/b] dane rosnąco lub malejąco

Dlaczego nie zaprojektowano tego adaptera, aby robił to za nas?
W tym momencie kończą się oczywiste wnioski dla osób, które nie mają dużego zasobu wiedzy z programowania i projektowania. Ważne jest jednak dokonywanie analizy każdego zagadnienia w którym napotykasz problemy - dzięki temu będziesz rozumiał lepiej to co robisz i w przyszłości będziesz miał większą wiedzę natrafiając na podobne problemy.

4.10.2. Kulisy powstawania STL'a

W czasach gdy STL powstawał projektanci STL'a nie byli w stanie stworzyć automatycznego mechanizmu, który umożliwiłby porządkowanie danych dla struktur o której nie mają praktycznie żadnych informacji. Co więcej do dnia dzisiejszego taki mechanizm prawdopodobnie nie istnieje,a przynajmniej nie jest mi znany i nie widzę możliwości wykonania takiego mechanizmu z dwóch względów:

  • W strukturze mogą być wskaźniki na dane, a co za tym idzie nie wiemy jak te dane wyglądają.
  • Nie wiemy jaki porządek chce mieć użytkownik - programista może chcieć posortować dane np. wg nazwiska rosnąco i wg imienia malejąco.

Zważywszy na powyższe punkty staje się oczywiste, że wykonanie automatycznego mechanizmu jest praktycznie rzecz biorąc niemożliwe. Projektanci STL'a jednak mając ogromną wiedzę z dziedziny programowania i projektowania powiedzieli sobie: skoro nie możemy utworzyć automatycznego mechanizmu sortowania, dajmy możliwość stworzenia go programiście w łatwy sposób. - i tak się stało.

4.10.3. Tworzymy własny funktor sortujący

Utworzenie funktora sortującego, z której STL będzie mógł korzystać jest bardzo proste. Zwróć jednak uwagę w jaki sposób zostało zaprogramowane sortowanie gdy mamy więcej niż jedną kolumnę. Jest to najprostszy sposób aby dane sortowały się prawidłowo w kilku kolumnach i polecam go dobrze zapamiętać.

C/C++
#include <string>
#include <queue>

struct SOsoba
{
    std::string imie;
    std::string nazwisko;
    int kolejnosc;
    std::string inneDane;
};

struct PorownajOsoby
{
    bool operator ()( const SOsoba & osoba1, const SOsoba & osoba2 )
    {
        //kolejność - rosnąco
        if( osoba1.kolejnosc > osoba2.kolejnosc ) return true;
       
        if( osoba1.kolejnosc < osoba2.kolejnosc ) return false;
       
        //nazwisko - rosnąco
        if( osoba1.nazwisko > osoba2.nazwisko ) return true;
       
        if( osoba1.nazwisko < osoba2.nazwisko ) return false;
       
        //imię - malejąco
        if( osoba1.nazwisko < osoba2.nazwisko ) return true;
       
        if( osoba1.nazwisko > osoba2.nazwisko ) return false;
       
        return false;
    }
};

int main()
{
    return 0;
}

Warto w tym miejscu również dodać, że kolumny których nie uwzględnimy w sortowaniu nie będą umieszczone w kolejności w której zostały dodane. Kolumny te będą na losowej pozycji. Warto tutaj też uściślić co to znaczy ta losowość - mianowicie dane zostaną posortowane wg. tych kolumn, które określiłeś, ale nie możesz zakładać, że jeśli wszystkie kolumny będą miały takie same dane po których sortujesz, to kolejność elementów w kolejce priorytetowej nie będzie zmieniana. Uruchomienie przykładu, znajdującego się w dalszej części tej lekcji powinno ułatwić Ci zrozumienie powyższej treści.

4.10.4. Tworzymy działający adapter kolejki priorytetowej dla własnej struktury

Aby utworzyć sprawny adapter musimy ustawić dodatkowo dwa parametry szablonu:

C/C++
std::priority_queue < /*własny typ danych*/, /*kontener przechowujący dane*/, /*funktor do porównywania danych*/ > nazwa_kolejki_priorytetowej;

Inicjacja adaptera kolejki priorytetowej może więc wyglądać tak:

C/C++
std::priority_queue < SOsoba, std::vector < SOsoba >, PorownajOsoby > kolejkaPriorytetowa;

4.10.5. Przykład

C/C++
#include <iostream>
#include <string>
#include <queue>
#include <conio.h>
struct SOsoba
{
    std::string imie;
    std::string nazwisko;
    int kolejnosc;
    std::string inneDane;
};

struct PorownajOsoby
{
    bool operator ()( const SOsoba & osoba1, const SOsoba & osoba2 )
    {
        //kolejność - rosnąco
        if( osoba1.kolejnosc > osoba2.kolejnosc ) return true;
       
        if( osoba1.kolejnosc < osoba2.kolejnosc ) return false;
       
        //nazwisko - rosnąco
        if( osoba1.nazwisko > osoba2.nazwisko ) return true;
       
        if( osoba1.nazwisko < osoba2.nazwisko ) return false;
       
        //imię - malejąco
        if( osoba1.nazwisko < osoba2.nazwisko ) return true;
       
        if( osoba1.nazwisko > osoba2.nazwisko ) return false;
       
        return false;
    }
};

typedef std::priority_queue < SOsoba, std::vector < SOsoba >, PorownajOsoby > TKolejkaPriorytetowaOsob;

void WstawOsobe( TKolejkaPriorytetowaOsob & kp, const char * imie, const char * nazwisko, int kolejnosc, const char * inneDane )
{
    SOsoba nowaOsoba;
    nowaOsoba.imie = imie;
    nowaOsoba.nazwisko = nazwisko;
    nowaOsoba.kolejnosc = kolejnosc;
    nowaOsoba.inneDane = inneDane;
    kp.push( nowaOsoba );
}

int main()
{
    TKolejkaPriorytetowaOsob kolejkaPriorytetowa;
    WstawOsobe( kolejkaPriorytetowa, "Imie", "Nazwisko", 1, "Pierwszy" );
    WstawOsobe( kolejkaPriorytetowa, "Imie", "Nazwisko", 1, "Drugi" );
    WstawOsobe( kolejkaPriorytetowa, "Imie", "Nazwisko", 1, "Trzeci" );
    WstawOsobe( kolejkaPriorytetowa, "Imie", "Ziomek", 1, "Czwarty" );
   
    while( kolejkaPriorytetowa.size() > 0 )
    {
        SOsoba odczytana = kolejkaPriorytetowa.top();
        kolejkaPriorytetowa.pop();
        std::cout << odczytana.imie << "; " << odczytana.nazwisko << "; " << odczytana.kolejnosc << "; " << odczytana.inneDane << "; " << std::endl;
    }
   
    getch();
    return 0;
}

4.11. Zaawansowane wykorzystywanie adaptera kolejki

Jak zapewne zauważyłeś do tej pory omówiliśmy pierwszy i trzeci parametr adaptera kolejki priorytetowej. W tym podrozdziale skoncentrujemy się na drugim. Zanim jednak do niego przejdziemy warto jeszcze wspomnieć o tym jak jest zdefiniowany adapter kolejki priorytetowej w STL'u.

C/C++
namespace std
{
    template < typename typZmiennej, typename kontenerDanych, typename funktorPorownania >
    class priority_queue
    {
        //...
    }; //class priority_queue
} //namespace std

Aby adapter kolejki priorytetowej był łatwy w użyciu dla stadrardowych typów danych, twórcy STL'a dopisali jeszcze linijkę:

C/C++
template < typename typZmiennej, typename kontenerDanych = vector < typZmiennej >, typename funktorPorownania = less < typZmiennej > >
class priority_queue;

Dzięki powyższemu zapisowi kompilator wie, że jeśli nie podamy drugiego i trzeciego parametru, to użyje on domyślnego kontenera i domyślnego funktora. Zgodnie z powyższym - poniższe dwa zapisy są sobie równoważne:

C/C++
std::priority_queue < int > mojaKolejka1;
std::priority_queue < int, std::vector < int >, std::less < int > > mojaKolejka2;

Rozdział ten jeszcze nie jest skończony. Tymczasowo pozostanie on w obecnym stanie.
Poprzedni dokument Następny dokument
Adapter kolejki (std::queue) Kontener tablicy (std::vector)