Algorytmy i struktury danych Wykład 3: Stosy, kolejki i listy Dr inż. Paweł Kasprowski pawel@kasprowski.pl Kolejki FIFO First In First Out (kolejka) LIFO Last In First Out (stos) Stos (stack) Dostęp jedynie do wierzchołka stosu (elementu, który leży na szczycie) Metody: push() wstaw na stos pop() pobierz ze stosu Ograniczenie dostępu do elementów ułatwia użytkowanie stosu Stos może być przechowywany w dowolnej strukturze (na razie przykład z tablicą) 1
Wstawianie elementu public void push(int wartosc) { tablica[nelem]=wartosc; nelem++; Kod identyczny jak metody wstaw() W rzeczywistości stos nie musi być implementowany jako tablica! Pobieranie elementu public int pop() { nelem--; return tablica[nelem]; Zawsze pobiera ostatni element (ostatnio włożony na stos) i "zwalnia" miejsce Wykorzystanie stosu Obracanie słów Sprawdzanie nawiasów Obliczanie wyrażeń algebraicznych 2
Klasa Stack class Stack { char[ ] tablica; int nelem; public void push(char h wartosc) { tablica[nelem]=wartosc; nelem++; public char pop() { nelem--; return tablica[nelem]; Klasa StackMain static public void main(string[] args) { System.out.println("Podaj tekst:"); String txt = getstring(); String txt2 = reverse(txt); System.out.println("Przed obrotem: "+txt); System.out.println("Po obrocie: "+txt2); Funkcja getstring public static String getstring() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readline(); return s; Pobiera tekst ze standardowego wejścia Charakterystyczna dla Javy nie będziemy jej analizować, tylko używać 3
Funkcja reverse public static String reverse(string txt) { Stack stos = new Stack(txt.length()); // stworzenie stosu for(int i=0;i<txt.length();i++) stos.push(txt.charat(i)); t h // wrzucenie kolejnych liter String txt2 = ""; // zmienna pomocnicza while(stos.size()>0) txt2+=stos.pop();// ściągnięcie liter (kolejność odwrotna) return txt2; StackMain.java Sprawdzanie nawiasów Trzy rodzaje nawiasów: okrągłe ( ) klamrowe { kwadratowe [ ] Wprowadzony tekst powinien zawierać znaki otwarcia i zamknięcia wszystkich nawiasów Nawiasy są zawsze zagnieżdżone ( { ( ) ) OK ( { ) źle Oprócz nawiasów mogą występować dowolne inne znaki Algorytm Przeglądaj wprowadzony tekst Jeśli nawias otwierający wrzuć na stos Jeśli nawias zamykający ściągnij nawias otwierający ze stosu jeśli nawiasy nie odpowiadają sobie lub stos pusty zakończ zgłaszając błąd Jeśli inny znak pomiń Jeśli koniec ciągu zakończ zgłaszając poprawność 4
Implementacja for(int i=0;i<txt.length();i++) { // dla kolejnych znaków char znak = txt.charat(i); if(znak=='(' znak=='[' znak=='{') // jeśli nawias otwierający stos.push(znak); // wrzuć na stos if(znak==')' znak==']' znak=='') // jeśli nawias zamykający if(stos.size()==0) size()==0) // stos pusty - błąd System.out.println("Błąd na pozycji "+i); else { char x = stos.pop(); if( (x=='{' && znak!='') (x=='[' && znak!=']') (x=='(' && znak!=')') ) // na stosie inny nawias - błąd System.out.println("Błąd na pozycji "+i); Funkcja check public static void check(string txt) { Stack stos = new Stack(txt.length()); for(int i=0;i<txt.length();i++) {...treść z poprzedniego slajdu... if(stos.size()>0) System.out.println("Błąd: Brak nawiasu zamykającego!"); Kolejka FIFO First In First Out Kolejka ma zawsze początek i koniec Wstawiamy elementy na koniec P bi l k Pobieramy elementy z początku Zawsze przechowywane wskaźnik na początek (pierwszy element w kolejce) wskaźnik na koniec (ostatnio dodany element) liczba elementów 5
Definicja klasy Kolejka (Queue) class Queue { int[ ] tablica; int nelem; int front; // początek kolejki int rear; // koniec kolejki public Queue(int wielkosc) { tablica = new int[wielkosc]; nelem = 0; front = 0; rear = -1; public void insert(int wartosc); public int remove(); front rear Obsługa kolejki public void insert(int wartosc) { rear++; // przesuń wskaźnik końca tablica[rear]=wartosc; // wstaw nowy element nelem++; // zwiększ długość kolejki public int remove() { int wartosc = tablica[front]; // zapamiętaj wartość z początku front++; // przesuń wskaźnik początku nelem--; // zmniejsz długość kolejki return wartosc; Dla kolejki cyklicznej jest to trochę trudniejsze Funkcja insert (wstaw) public void insert(int wartosc) { rear++; // przesuń wskaźnik if(rear == tablica.length) rear=0; // jeśli wskaźnik na końcu tablicy to przewiń tablica[rear]=wartosc; // wstaw nowy element nelem++; // zwiększ długość kolejki 6
Funkcja remove (usuń) public int remove() { int wartosc = tablica[front]; front++; // przesuń wskaźnik początku if(front == tablica.length) // jeśli na końcu to przewiń front = 0; nelem--; // zmniejsz długość kolejki return wartosc; Kolejka priorytetowa Elementy w kolejce są posortowane Funkcja remove() zawsze pobiera największy element (ten o najwyższym priorytecie) Funkcja insert() wstawia element w odpowiednie miejsce w kolejce (tak jak funkcja wstaw dla tablicy uporządkowanej) Zwykle implementuje się je strukturach innych niż tablice (np. w kopcach) Obliczanie wartości wyrażenia Dwa etapy: Konwersja wyrażenia arytmetycznego na notację ONP Obliczenie wartości wyrażenia w notacji ONP Obliczenie wartości wyrażenia w notacji ONP W obu przypadkach przyda się stos 7
Notacja ONP Odwrotna Notacja Polska (Reverse Polish Notation) Polski wkład w informatykę (Łukasiewicz) Notacja pozwala na zapis wyrażeń arytmetycznych bez użycia nawiasów Budowa notacji nawiasowej <argument> <operator> <argument> (np. a+b) Budowa notacji ONP <argument> <argument> <operator> (np. ab+ ) Konwersja wyrażeń na ONP a+b > ab+ a*b+c > ab*c+ a*(b+c) > abc+* a+b*c+d > abc*+d+ (a+b) * (c+d) > ab+cd+* a+b-c+d > ab+c-d+ a+(b-c)+d > abc-+d+ Co nam daje ONP? Uproszczenie zapisu (brak nawiasów) Możliwość analizy i obliczania wartości wyrażeń po kolei od lewej do prawej Algorytm obliczania wyrażenia: pobierz znak jeśli argument odłóż na stosie jeśli operator pobierz ze stosu dwa argumenty wykonaj działanie wstaw wynik na stos jeśli koniec wyrażenia zdejmij ze stosu wynik 8
Implementacja obliczenia for(int i=0;i<txt.length();i++) { char znak = txt.charat(i); if(znak>='0' && znak<='9') stos.push( (int)(znak-'0') ); // argumenty na stos else { // operator przelicz dwa ostatnie argumenty int arg2 = stos.pop(); // pobierz argumenty ze stosu int arg1 = stos.pop(); int wynik = 0; if(znak=='+') wynik = arg1 + arg2; // wykonaj działanie if(znak=='-') wynik = arg1 - arg2; if(znak=='*') wynik = arg1 * arg2; if(znak=='/') wynik = arg1 / arg2; stos.push(wynik); return stos.pop(); // na końcu na stosie jeden element - wynik Przykład 1 Notacja nawiasowa: 4*(8-2)/3+2 = 10 Notacja ONP: 482-*3/2+ Obliczenia: 482 4,8,2, 4,6, * 24,3,/ 8,2,+ 10 Uwaga: jeśli wyniki cząstkowe nie są całkowite będą zaokrąglane! Przykład 2 Notacja nawiasowa: (2+3)*(8+2)/5+7 = 17 Notacja ONP: 23+82+*5/7+ Obliczenia: 23 2,3,+ 5,8,2,+ 5,10,* 50,5,/ 10,7,+ 17 9
Zamiana wyrażenia na ONP Znowu używamy stosu! Tym razem wrzucamy do niego operatory a nie argumenty Algorytm: pobierz znak jeśli argument wypisz na wyjście jeśli nawias otwierający na stos jeśli nawias zamykający zdejmij ze stosu wszystko do nawiasu otwierającego i wypisz na wyjście jeśli operator zdejmij ze stosu i wypisz operatory o wyższym priorytecie aż do nawiasu otwierającego, umieść operator na stosie jeśli koniec wyrażenia wypisz zawartość stosu na wyjście Listy Dr inż. Paweł Kasprowski pawel@kasprowski.pl Listy a tablice Zalety: Nie trzeba z góry definiować wielkości listy Szybsze wstawianie w środek listy S b i Szybsze usuwanie Wady Wolniejsze wyszukiwanie Więcej miejsca w pamięci 10
Zasada działania Każdy element listy zawiera wskaźnik na kolejny Wystarczy, ze znamy wskaźnik na pierwszy element dane next dane next dane next Element listy class Element { int dana;... Element next; //adres następnego elementu Element listy class Element { public int dana;... public Element next; //adres następnego elementu public Element(int dana) { this.dana = dana; this.next = null; 11
Klasa Lista class Lista { Element head; public Lista() { head = null; public void wstaw(int klucz); public void usun(int klucz); public boolean szukaj(int klucz);... Klasa ArrayMain class ArrayMain { public static void main() { Array tab = new Array(20); for(int i=0; i<10; i++) { tab.wstaw(100*i + 2); tab.pokaztablice(); tab.wstaw(250); if(tab.szukaj(120)) System.out.println("Znaleziono "); Klasa ListaMain class ListaMain { public static void main() { Lista lista = new Lista(); for(int i=0; i<10; i++) { lista.wstaw(100*i + 2); lista.pokazliste(); lista.wstaw(250); if(lista.szukaj(120)) System.out.println("Znaleziono "); 12
Metoda wstawpierwszy() public void wstawpierwszy(int klucz) { Element x = new Element(klucz); x.next = head; head = x; Metoda pokazliste() public void pokazliste() { x = head; while(x!=null) { System.out.println("Element: "+x.dana); x = x.next; ListaMain.java Metoda wstaw() public void wstaw (int klucz) { Element elem = new Element(klucz); x = head; hil (! ll) { // jś i d i while(x.next!= null) { // przejście do ostatniego x = x.next; // tutaj x wskazuje na ostatni element x.next = elem; 13
Metoda usunpierwszy() public void usunpierwszy() { head = head.next; Uwaga1: W Javie nie trzeba zwalniać pamięci! Uwaga2: Jeśli nie ma na liście elementów to head=null i metoda wygeneruje wyjątek NullPointerException! Metoda usunpierwszy() public void usunpierwszy() { if(head!= null) head = head.next; Zwracanie usuniętego elementu public Element usunpierwszy() { Element x = head; if(head!= null) h d h d head = head.next; return x; 14
Zwracanie wartości usuniętego elementu public int usunpierwszy() { int x = head.dana; if(head!= null) head = head.next; return x; Uwaga! Znowu może być NullPointerException Zwracanie wartości usuniętego elementu public int usunpierwszy() { int x = -1; if(head!= null) { x = head.dana; d head = head.next; return x; Metoda usun() dla tablicy public void usun(int klucz) { int i=0; while(i<nelem && tablica[i]!= klucz) // wyszukanie i i 1 i = i + 1; while(i<=nelem-1) { // przesunięcie tablica[i] = tablica[i+1]; i++; 15
Metoda usun() dla listy public void usun(int klucz) { Element x = head; while(x!=null && x.dana!= klucz) // wyszukanie x = x.next;... Metoda usun() dla listy public void usun(int klucz) { Element x = head; Element xprev = null; while(x!=null && x.dana!= klucz) { // wyszukanie xprev = x; x = x.next; // tutaj: // x - element do usunięcia, // xprev - element poprzedni Metoda usun() dla listy public void usun(int klucz) { Element x = head; Element xprev = null; while(x!=null && x.dana!= klucz) { // wyszukanie xprev = x; x = x.next; if(x!=null) { // usunięcie elementu z łańcucha xprev.next = x.next; 16
Metoda usun() dla listy public void usun(int klucz) { Element x = head; Element xprev = null; while(x!=null && x.dana!= klucz) { // wyszukanie xprev = x; x = x.next; if(x!=null) { // usunięcie elementu z łańcucha xprev.next = x.next; Problem: co jeśli usuwamy pierwszy element? Metoda usun() dla listy public void usun(int klucz) { Element x = head; Element xprev = null; while(x!=null && x.dana!= klucz) { // wyszukanie xprev = x; x = x.next; if(x!=null) { // usunięcie elementu z łańcucha if(xprev!= null) xprev.next = x.next; else // usuwamy pierwszy element head = x.next; Zwracanie usuwanego elementu public Element usun2(int klucz) { Element x = head; Element xprev = null; while(x!=null && x.dana!= klucz) { // wyszukanie xprev = x; x = x.next; if(x!=null) { // usunięcie elementu z łańcucha if(xprev!= null) xprev.next = x.next; else head = x.next; return x; 17
Zwracanie wartości usuwanego elementu public int usun2(int klucz) { Element x = head; Element xprev = null; while(x!=null && x.dana!= klucz) { // wyszukanie xprev = x; x = x.next; if(x!=null) { // usunięcie elementu z łańcucha if(xprev!= null) xprev.next = x.next; else head = x.next; return (x!=null)?x.dana:-1; Implementacja stosu Metoda push() wstawienie nowego elementu na szczyt stosu po prostu metoda wstawpierwszy() Metoda pop() usunięcie elementu ze szczytu stosu po prostu metoda usunpierwszy() Z punktu widzenia użytkownika stosu sposób przechowywania danych (tablica czy lista) jest obojętny on używa tylko metod push() i pop() Implementacja kolejki Metoda insert() wstawia element na koniec kolejki po prostu metoda wstaw() Metoda remove() pobiera pierwszy element z kolejki po prostu metoda usunpierwszy() Lista jest tworzona dynamicznie nie ma tu potrzeby "zawijania" jak przy użyciu tablicy 18
Kolejka tablica czy lista? Tablica konieczność określenia maksymalnej wielkości stosu/kolejki czas usuwania rzędu O(n) czas wstawiania rzędu O(1) gdy na koniec! Lista większy rozmiar danych (przechowywanie linków) czas usuwania rzędu O(1) czas wstawiania rzędu O(n) konieczność przejścia na koniec listy Ale to można poprawić! Przechowanie końca listy class Lista { Element head; Element tail; public Lista() { head = null; tail = null;... Szybsze wstaw() public void wstaw (int klucz) { Element elem = new Element(klucz); tail.next = elem; tail = elem; 19
Poprawka w usunpierwszy() public int usunpierwszy() { int x = -1; if(head!= null) { x = head.dana; d head = head.next; if(head==null) tail=null; return x; Listy dwukierunkowe Oprócz next, także prev Możliwość przechodzenia w obie strony listy Usuwanie i wstawianie robi się bardziej skomplikowane Wyszukiwanie może być łatwiejsze Dziękuję za uwagę Do zobaczenia... materiały dostępne pod adresem: www.kasprowski.pl 20