algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10....

50
Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych Janusz Szwabiński Plan wykładu: Stos Kolejka Kolejka dwustronna Listy Źródła: większość ilustracji pochodzi z "Problem Solving with Algorithms and Data Structures using Python", http://interactivepython.org/runestone/static/pythonds/index.html (http://interactivepython.org/runestone/static/pythonds/index.html)

Upload: others

Post on 04-Aug-2021

16 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Algorytmy i struktury danych

Wykłady 2 i 3 ­ Abstrakcyjne struktury danych

Janusz Szwabiński

Plan wykładu:

StosKolejkaKolejka dwustronnaListy

Źródła: większość ilustracji pochodzi z "Problem Solving with Algorithms and Data Structures using Python",http://interactivepython.org/runestone/static/pythonds/index.html(http://interactivepython.org/runestone/static/pythonds/index.html)

Page 2: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

StosStos to liniowa struktura danych, w której dane dokładane są na wierzch stosu i z wierzchołka stosu sąpobierane. Czasami określa się go jako bufor typu LIFO (ang. Last In, First Out).

PopPush

Stosy można odnaleźć w wielu przykładach z życia codziennego. W kawiarniach i na stołówkach częstomamy do czynienia ze stosami talerzy lub tac. Na wielu biurkach znajdziemy stosy książek położonych jednana drugiej. Nowy egzemplarz kładzie się na wierzch stosu i z wierzchu stosu zdejmuje się kolejneegzemplarze. Elementy stosu poniżej wierzchołka można wprawdzie obejrzeć, aby je ściągnąć, trzebanajpierw po kolei ściągnąć to, co jest nad nimi.

Stos jest używany w systemach komputerowych na wszystkich poziomach funkcjonowania systemówinformatycznych. Stosowany jest przez procesory do chwilowego zapamiętywania rejestrów procesora, doprzechowywania zmiennych lokalnych, a także w programowaniu wysokopoziomowym.

Stos jako abstrakcyjna struktura danych został wymyślony w 1955 r (opatentowany w 1957) przez FriedrichaL. Baura. Jest bardzo użyteczny, ponieważ w naturalny sposób odwraca kolejność w sekwencji danych:

Page 3: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Definicja stosuAbstrakcyjny typ danych stos zdefiniowany jest jako uporządkowany zbiór elementów, w którym elementydodawane są na jego koniec (zwany "wierzchołkiem") i z końca są ściągane. Ten typ danych musi wspieraćnastępujące operacje:

Stack() ­ tworzenie nowego pustego stosupush(item) ­ wstawienie nowego elementu na wierzchołek stosupop() ­ pobranie (usunięcie z jednoczesnym zwróceniem wartości) elementu z wierzchołka stosupeek() ­ odczytuje wartość z wierzchołka stosu. Nie usuwa jej (stos pozostaje niezmieniony)isEmpty() ­ testuje, czy stos jest pustysize() ­ liczba elementów znajdujących się na stosie

Zakładając, że s jest nowym pustym stosem, w poniższej tabeli zestawione są przykładowe operacje na nimwraz z wynikami ich działań:

Operacja Zawartość stosu Zwracana wartość

s.isEmpty() [] True

s.push(4) [4]

s.push('dog') [4,'dog']

s.peek() [4,'dog'] 'dog'

s.push(True) [4,'dog',True]

s.size() [4,'dog',True] 3

Lista w Pythonie a stosLista to jeden z wbudowanych typów danych w Pythonie, używany do przechowywania uporządkowanychsekwencji elementów. Warto wspomnieć, że chociaż nazwy metod listy różnią się częściowo od tychzdefiniowanych powyżej, to listy oferują funkcjonalność stosu:

In [1]:

s = [] #pusta lista

Page 4: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [2]:

not s #True jeśli pusta

In [3]:

s.append(4)

In [4]:

s

In [5]:

s.append('dog')

In [6]:

s

In [7]:

s[-1] #wartość na wierzchołku

In [8]:

s.append(True)

In [9]:

s

In [10]:

len(s) #rozmiar

Out[2]:

True

Out[4]:

[4]

Out[6]:

[4, 'dog']

Out[7]:

'dog'

Out[9]:

[4, 'dog', True]

Out[10]:

3

Page 5: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [11]:

not s #czy pusta?

In [12]:

s.append(8.4)

In [13]:

s

In [14]:

s.pop()

In [15]:

s

In [16]:

s.pop()

In [17]:

s

In [18]:

len(s)

Out[11]:

False

Out[13]:

[4, 'dog', True, 8.4]

Out[14]:

8.4

Out[15]:

[4, 'dog', True]

Out[16]:

True

Out[17]:

[4, 'dog']

Out[18]:

2

Page 6: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Implementacja stosu w PythonieDysponując definicją stosu, możemy ją zaimplementować w wybranym języku programowania. W Pythonie,ze względu na jego obiektowość, najwygodniejszym sposobem będzie zdefiniowanie klasy Stack zoperacjami zaimplementowanymi w formie metod. Ze względu na podobieństwo między stosem a pythonowąlistą, wykorzystamy tą ostatnią jako punkt wyjścia do naszej implementacji.

In [19]:

class Stack: def __init__(self): self.items = []

def isEmpty(self): return self.items == []

def push(self, item): self.items.append(item)

def pop(self): return self.items.pop()

def peek(self): return self.items[len(self.items)-1]

def size(self): return len(self.items)

"Opakowanie" listy odpowiednimi operacjami nie nastręcza żadnych problemów. Podczas implementacjimusieliśmy podjąć tylko jedną ważną decyzję ­ który kraniec listy będziemy traktować jako bazę, a który jakowierzchołek. Ze względów wydajnościowych (  vs  )decydujemy się, aby wierzchołkiem byłzawsze ostatni element listy.

Sprawdźmy, jak działa nasz stos:

In [20]:

s=Stack()print(s.isEmpty())

In [21]:

s.push(4)s.push('dog')print(s.peek())

In [22]:

s.push(True)print(s.size())

O(1) O(n)

True

dog

3

Page 7: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [23]:

print(s.isEmpty())

In [24]:

s.push(8.4)

In [25]:

print(s.pop())

In [26]:

print(s.pop())

In [27]:

print(s.size())

Zaletą abstrakcyjnych typów danych jest to, że działają one niezależnie od ich fizycznej implementacji.

Przykład ­ sprawdzanie poprawności nawiasówW wyrażeniach arytmetycznych często używamy nawiasów dla określenia kolejności wykonywania działań,np.:

Pewne języki programowania, jak np. Lisp, wymagają używania nawiasów:

(defun square(n) (* n n))

(5 + 6) ∗ (7 + 8)/(4 + 3)

False

8.4

True

2

Page 8: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

W większości zastosowań liczba nawiasów musi być zbilansowana, tzn. powinno być tyle samo prawych colewych nawiasów. Ponadto, nawiasy muszą być poprawnie zagnieżdżone:

(()()()())

(((())))

(()((())()))

Dla porównania, poniżej znajduje się kilka przykładów niepoprawnych nawiasów:

((((((())

()))

(()()(()

Sprawdzanie poprawności nawiasów w różnych wyrażeniach to jedno z częstych i ważnych zadań, z którymmuszą uporać się programiści. Naszym celem będzie teraz uporanie się z tym problemem przy użyciu stosu.Zauważmy przede wszystkim że w sekwencji nawiasów pierwszy z nawiasów zamykających musi pasowaćdo ostatniego nawiasu otwierającego. Podobnie, pierwszy z nawiasów otwierających musi "czekać" doostatniego nawiasu zamykającego. Innymi słowy, nawiasy zamykające powinny pasować do otwierających wodwrotnym porządku.

Z tego właśnie powodu stos wydaje się być strukturą odpowiednią do rozwiązania tego zagadnienia. Powybraniu struktury reszta algorytmu jest prosta:

1. Rozpocznij z pustym stosem.2. Wczytaj ciąg znaków z nawiasami.3. Przetwarzaj ciąg idąc od lewej do prawej.4. Jeśli aktualny znak jest nawiasem otwierającym, wstaw go na stos.5. Jeśli aktualny znak jest nawiasem zamykającym, ściągnij nawias otwierający z wierzchołka stosu.6. Jeśli po dojściu do końca ciągu stos jest pusty i nie wystąpiły żadne błędy, nawiasy są poprawne.

Page 9: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [28]:

def parChecker(symbolString): s = Stack() #nowy stos balanced = True #zakładamy, że ciąg jest poprawny index = 0 while index < len(symbolString) and balanced: #do końca łańcucha lub do pierwszego błędu symbol = symbolString[index] #odczytaj kolejny znak if symbol == "(": #jeśli to "(", wstaw na stos s.push(symbol) else: if s.isEmpty(): #jeśli to ")" i stos jest pusty, nawiasy są niepoprawne balanced = False else: s.pop() #w przeciwnym razie ściągnij ze stosu

index = index + 1

if balanced and s.isEmpty(): return True else: return False

In [29]:

print(parChecker('((()))'))

In [30]:

print(parChecker('(()'))

True

False

Page 10: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Przykład ­ sprawdzanie poprawności różnych symboliW wielu językach programowania, również w Pythonie, mamy do czynienia z różnymi symbolamiotwierającymi i zamykającymi:

{ { ( [ ] [ ] ) } ( ) }

[ [ { { ( ( ) ) } } ] ]

[ ] [ ] [ ] ( ) { }

( [ ) ]

( ( ( ) ] ) )

[ { ( ) ]

Dlatego możemy spróbować uogólnić powyższy przykład na przypadek sprawdzania symboli różnego typu wjednym wyrażeniu.

In [31]:

def matches(op,cl): """A helper function to check if symbols match""" opens = "([{" closers = ")]}" return opens.index(op) == closers.index(cl)

In [32]:

def parChecker(symbolString): """Check if symbols are balanced""" s = Stack() balanced = True index = 0 while index < len(symbolString) and balanced: symbol = symbolString[index] if symbol in "([{": s.push(symbol) else: if s.isEmpty(): balanced = False else: top = s.pop() if not matches(top,symbol): balanced = False index = index + 1 if balanced and s.isEmpty(): return True else: return False

In [33]:

print(parChecker('{{([][])}()}'))

True

Page 11: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [34]:

print(parChecker('[{()]'))

Powyższe przykłady pokazują, że stosy są bardzo przydatne w parsowaniu źródeł programów. Praktyczniekażdy język programowania używa pewnych symboli, które muszą do siebie pasować w zbilansowanysposób.

Przykład ­ konwersja liczb z reprezentacji dziesiętnej do reprezentacjibinarnejUcząc się programowania prędzej czy później zostaniemy skonfrontowani z liczbami w postaci dwójkowej.Reprezentacja binarna jest bardzo ważna w informatyce, ponieważ wszystkie dane w pamięci komputerówsą przechowywane jako ciągi zer i jedynek.

Liczba   w reprezentacji dziesiętnej jest interpretowana jako:

Podobnie, jej binarny odpowiednik   możemy zinterpretować jako:

Do znalezienia reprezentacji binarnej liczby dziesiętnej algorytmu wykorzystującego dzielenie przez 2.Składa się on z prostej iteracji, w której dzielimy liczbę całkowicie przez 2, zapamiętujemy resztę z tegodzielenia, następnie dzielimy wynik przez 2, zapamiętujemy resztę itd. Przy tym pierwsza obliczona resztabędzie ostatnim bitem w reprezentacji liczby. Innymi słowy, aby otrzymać reprezentację dwójkową liczby,musimy odwrócić kolejność w wygenerowanym ciągu reszta. Wskazuje to, że stos będzienajprawdopodobniej odpowiednią strukturą do wykonania tego zadania.

233102 × + 3 × + 3 × = 200 + 30 + 3 = 233102 101 100

1110100121 × + 1 × + 1 × + 0 × + 1 × + 0 × + 0 × + 1 × = 128 + 64 + 32 +27 26 25 24 23 22 21 20

False

Page 12: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [35]:

def divideBy2(decNumber): remstack = Stack()

while decNumber > 0: rem = decNumber % 2 #reszta z dzielenia przez 2 remstack.push(rem) decNumber = decNumber // 2 #dzielenie całkowite

binString = "" while not remstack.isEmpty(): binString = binString + str(remstack.pop())

return binString

In [36]:

print(divideBy2(233))

In [37]:

print(divideBy2(8))

Przykład ­ konwersja do dowolnej reprezentacjiPowyższy przykład można uogólnić na przypadek dowolnej reprezentacji:

Algorytm jest bardzo podobny, jednak zamiast przez 2, dzielimy teraz po prostu przez bazę wybranejreprezentacji:

= 3 × + 5 × + 1 × =3518 82 81 80 23310E = 14 × + 9 × = 233916 161 160

11101001

1000

Page 13: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [38]:

def baseConverter(decNumber,base): digits = "0123456789ABCDEF"

remstack = Stack()

while decNumber > 0: rem = decNumber % base remstack.push(rem) decNumber = decNumber // base

newString = "" while not remstack.isEmpty(): newString = newString + digits[remstack.pop()]

return newString

In [39]:

print(baseConverter(25,2))

In [40]:

print(baseConverter(25,16))

Przykład ­ zapis infiksowy, prefiksowy i postfiksowyKolejnym przykładem zastosowania stosu będzie analiza wyrażeń arytmetycznych. Zapis   jest łatwy witerpretacji ­ zmienna   jest mnożona przez wartość  , ponieważ pomiędzy nimi pojawił się operatormnożenia  . O takim zapisie mówimy, że jest on infiksowy, ponieważ operator pojawia się pomiędzyoperandami. Jest to klasyczny zapis operacji arytmetycznych.

Oprócz symboli i argumentów operacji w zapisie infiksowym stosuje się nawiasy, aby ustalić inną niżdomyślna kolejność wykonywania operacji:

In [41]:

a = 10; b = 2; c = 3

B ∗ CB C

11001

19

Page 14: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [42]:

a+b*c

In [43]:

(a+b)*c

Out[42]:

16

Out[43]:

36

Page 15: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Oprócz klasycznego zapisu wyrażeń istnieją jeszcze notacja polska (zapis przedrostkowy lub prefiksowy)opracowana przez Jana Łukasiewicza w 1920 oraz odwrotna notacja polska (zapis postfiksowy)zaproponowana przez Charlesa Hamblina.

Klasyczne wyrażenie   będzie miało w notacji polskiej postać

natomiast w notacji odwróconej polskiej

Kilka przykładów zebranych jest w poniższej tabeli:

Infix Polska Odwrócona polska

A + B * C + D + + A * B C D A B C * + D +

(A + B) * (C + D) * + A B + C D A B + C D + *

A B + C D + A B C D A B C D +

A + B + C + D + + + A B C D A B + C + D +

Zaletą obu notacji jest to, że nie wymagają one nawiasów do ustalenia kolejności wykonywania działań,ponieważ z pozycji operatora w zapisie jednoznacznie wynika, do których operandów powinien zostaćzastosowany.

W pokazanych powyżej przykładach przekształcaliśmy wyrażenia z jednej notacji na drugą ręcznie. Teraznaszym celem będzie zautomatyzowanie tego procesu.

Rozważmy jeszcze raz wyrażenie infiksowe  , tym razem jednak w wersji ze wszystkiminawiasami, tzn.

W ten sposób jawnie zaznaczamy, że mnożenie wykonywane jest przed dodawaniem. Ponadto, każda paranawiasów określa parę operandów z odpowiednim operatorem między nimi. Przyjrzyjmy się np. prawemynawiasowi w wyrażeniu  . Gdybyśmy przesunęli symbol mnożenia w jego miejsce i usunęliodpowiadający mu lewy nawias, otrzymamy wyrażenie postfiksowe  . Podobnie, jeśli operatordodawania wstawimy w miejsce odpowiadającego mu prawego nawiasu i usuniemy lewy, to otrzymamy

czyli postfiksową wersję naszego wyrażenia. Przekształcenie zilustrowane jest na poniższym rysunku:

Analogicznie, jeżeli operatory będziemy wstawiać w miejsce odpowiadających im lewych nawiasów iusuniemy prawe, to otrzymamy prefiksowy zapisa wyrażenia.

(A + B) ∗ C∗ + ABC

AB + C∗

A + B ∗ C

(A + (B ∗ C))

(B ∗ C)BC∗

ABC ∗ +

+A ∗ BC

Page 16: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Aby zatem przekształcić wyrażenie w notacji infiksowej na notację polską lub odwróconą polską, musimynajpierw zapisać je w wersji ze wszystkimi nawiasami, następnie przesunąć operator do jednego z nawiasówdefiniujących obszar jego działania i usunąć drugi:

Zauważmy, że w procesie konwersji jedynie operatory zmieniają pozycje, natomiast operandy pozostają naswoich miejscach.

Wróćmy raz jeszcze to wyrażenia  . Analizując je od lewej do prawej, pierwszym operatorem, naktóry natrafiamy, jest operator dodawania  . Jednak ze względu na kolejność wykonywania działań,pierwsza powinna zostać wykonana operacja mnożenia  . Ponieważ w notacji postfiksowej   będzie stał naostatnim miejscu, będziemy musieli odwrócić kolejność operatorów w stosunku do wyrażenia wyjściowego.

Przeanalizujmy teraz wyrażenie  AB+C*+$. Jednak teraz, ze względu na

nawiasy, będzie on miał pierwszeństwo przed operatorem mnożenia.

Na podstawie tych przykładów możemy już wywnioskować, jak powinien działać algorytm konwersji.Natrafiając na lewy nawias, zapisujemy go w celu zaznaczenia faktu, że za chwilę natrafimy na operator owyższym prorytecie. Operator ten "czeka" na stosie do momentu, aż pojawi się prawy nawias pasujący dozapisanego wcześniej lewego. Wówczas można zdjąć operator ze stosu.

Załóżmy zatem, że wyrażenie w postaci infiksowej to łańcuch tokenów ograniczony spacjami. Symboleoperatorów to *,/,+,- oraz nawiasy ( i ). Chcemy zaimplementować następującą procedurę:

1. Utwórz pusty stos o nazwie opstack. Utwórz pustą listę do zapisania wyniku.2. Przekształć łańcuch znaków z wyrażeniem infiksowym do listy.3. Przetwarzaj listę element po elemencie od lewej do prawej:

jeśli znak jest operandem, dodaj go na koniec listy wynikowej,jeśli znak jest lewym nawiasem, wstaw go na opstack,jeśli znak jest prawym nawiasem, ściągaj wartości z wierzchołka stosu do momentuznalezienia lewego nawiasu. Ściągnięty operator wstaw na koniec listy wynikowej.jeśli znak jest operatorem, wstaw go na opstack. Wcześniej jednak usuń ze stosuwszystkie operatory, które mają wyższy lub taki sam priorytet i dodaj je na koniec listywynikowej.

4. Po dojściu do ostatniego znaku wyrażenia wejściowego sprawdź zawartość opstack. Jeśli zostałyna nim jakieś operatory, wstaw je na koniec listy wynikowej.

Poniższy rysunek ilustruje działanie tego algorytmu dla wyrażenia  . Zauważmy, żepierwszy operator   jest usunięty ze stosu po natrafieniu na  . Ponadto,   pozostaje na stosie po dojściudo drugiego operatora  , ponieważ mnożenie ma wyższy priorytet.

A + B ∗ C+

∗ +

(A + B) ∗ C, któremuodpowiadazapispostfiksowy

. IdÅcodlewejdoprawej, znowupierwszyoperatorembÙdzie

A ∗ B + C ∗ D∗ + +

Page 17: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [44]:

def infixToPostfix(infixexpr): prec = {} prec["*"] = 3 #im wyższa liczba, tym większy priorytet prec["/"] = 3 prec["+"] = 2 prec["-"] = 2 prec["("] = 1 opStack = Stack() #nowy stos na operatory postfixList = [] #lista wyjściowa tokenList = infixexpr.split()

for token in tokenList: if token in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" or token in "0123456789": postfixList.append(token) #liczbę lub cyfrę dodajemy bezpośrednio elif token == '(': opStack.push(token) #lewy nawias na stos elif token == ')': topToken = opStack.pop() while topToken != '(': #ściągamy ze stosu aż do natrafienia na lewy nawias postfixList.append(topToken) topToken = opStack.pop() else: #natrafiliśmy na operator while (not opStack.isEmpty()) and (prec[opStack.peek()] >= prec[token]): postfixList.append(opStack.pop()) #ściągamy ze stosu operatory o takim samym lub wyższym prior. opStack.push(token) #wstawiamy operator na stos

while not opStack.isEmpty(): postfixList.append(opStack.pop()) #dopisujemy pozostałe operatory return " ".join(postfixList)

In [45]:

print(infixToPostfix("A * B + C * D"))print(infixToPostfix("( A + B ) * C - ( D - E ) * ( F + G )"))

A B * C D * +A B + C * D E - F G + * -

Page 18: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [46]:

infixToPostfix("( A + B ) * ( C + D )")

In [47]:

infixToPostfix("( A + B ) * C")

In [48]:

infixToPostfix("A + B * C")

Na zakończenie tej części wykładu zastanówmy się, jak wykonać wyrażenie zapisane w postaci postfiksowej.Weźmy np. wyrażenie

Idąc od lewej do prawej, najpierw natrafiamy na liczby 4 i 5. W tym momencie nie wiemy jeszcze, co z nimizrobić, dlatego wstawiamy je na stos i idziemy dalej. Trafiamy na kolejny operand, więc również wstawiamygo na stos. Kolejny znak to operator. Ściągając dwa operandy ze stosu, możemy wykonać odpowiadającemu działanie. Wynik wstawiamy na stos i przechodzimy do kolejnego znaku. Na samym końcu na stosiezostanie jedna wartość ­ wynik całego wyrażenia.

Algorytm wykonania wyrażenia postfiksowego składał się będzie z następujących kroków:

1. Utwórz pusty stos, np. o nazwie operandStack.2. Przekształć wyrażenie wejściowe do listy znaków.3. Przetwarzaj listę element po elemencie od lewej do prawej:

jeśli znak jest operandem, zamień go na liczbę naturalną i wstaw na stos,jeśli znak jest operatorem, ściągnij ze stosu dwie wartości, wykonaj na nich operację, a jejwynik wstaw z powrotem na stos.

4. Po przetworzeniu ostatniego znaku na stosie znajduje się wynik wyrażenia.

456 ∗ +

Out[46]:

'A B + C D + *'

Out[47]:

'A B + C *'

Out[48]:

'A B C * +'

Page 19: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [49]:

def doMath(op, op1, op2): if op == "*": return op1 * op2 elif op == "/": return op1 / op2 elif op == "+": return op1 + op2 else: return op1 - op2

In [50]:

def postfixEval(postfixExpr): operandStack = Stack() tokenList = postfixExpr.split()

for token in tokenList: if token in "0123456789": operandStack.push(int(token)) else: operand2 = operandStack.pop() operand1 = operandStack.pop() result = doMath(token,operand1,operand2) operandStack.push(result) return operandStack.pop()

In [51]:

print(postfixEval('7 8 + 3 2 + /'))

Przedstawione powyżej programy działają oczywiście przy założeniu, że wszystkie wyrażenia wejściowe sąpoprawne.

3.0

Page 20: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

KolejkaKolejka (ang. queue) to uporządkowana kolekcja danych, w której nowe dane dopisywane są na końcu,natomiast z początku pobiera się dane do dalszego przetwarzania.

Nowy element zostaje wstawiony na koniec kolejki i przesuwa się sukcesywnie do przodu w trakcieprzetwarzania wcześniej dodanych elementów. Na początku kolejki znajduje się zawsze element, który wdanej chwili przebywa w niej najdłużej. Innymi słowy jest to bufor typu FIFO (ang. First In, First Out, pierwszyna wejściu, pierwszy na wyjściu).

Zwróćmy uwagę, że kolejka jako abstrakcyjny typ danych imituje zachowanie kolejek znanych choćby zcodziennych zakupów, wyjść do kina itp.

W szeroko pojętej informatyce kolejki stosowane są bardzo często. Bardzo częstą praktyką na uczelniach i wkorporacjach jest np. zakup jednej drukarki sieciowej, z której jednocześnie korzystać mogą wszyscypracownicy znajdujący się w tej samej podsieci. Zadania wydruku napływające do drukarki wstawiane sąwłaśnie do kolejki i przetwarzane sukcesywnie jeden po drugim. Systemy operacyjne używają kolejek dokontrolowania procesów uruchamianych podczas działania komputera. Kolejki przydają się równieżprogramistom do wydajnego rozwiązywania pewnej klasy problemów, których przykłady omówimy poniżej.

Zwróćmy uwagę, że w przeciwieństwie do stosu, kolejka nie odwraca kolejności elementów w sekwencji:

Page 21: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Definicja kolejkiKolejka jako struktura danych jest zedfiniowana jako uporządkowany typ danych, w którym dane dodawanesą na koniec a pobierane z początku. Typ ten powinien wsperać następujące operacje:

Queue() ­ tworzy nową kolejkę,enqueue(item) ­ dodaje element na końcu kolejki (nic nie zwraca),dequeue() ­ usuwa z kolejki element znajdujący się na jej początku i zwraca go,isEmpty() ­ testuje, czy kolejka jest pusta,size() ­ zwraca liczbę elementów w kolejce.

Zakładając, że q jest nową pustą kolejką, w poniższej tabeli przedstawione są przykładowe operacjewykonane na niej wraz z wynikami:

Operacja Zawartość kolejki Zwracana wartość

q.isEmpty() [] True

q.enqueue(4) [4]

q.enqueue('dog')['dog',4]

q.enqueue(True) [True,'dog',4]

q.size() [True,'dog',4] 3

q.isEmpty() [True,'dog',4] False

q.enqueue(8.4) [8.4,True,'dog',4]

q.dequeue() [8.4,True,'dog'] 4

q.dequeue() [8.4,True] 'dog'

q.size() [8.4,True] 2

Kolejka a lista w PythoniePodobnie, jak to miało miejsce w przypadku stosu, również teraz stosując pythonowe listy możemy otrzymaćfunkcjonalność zbliżoną do kolejki:

Page 22: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [52]:

q = [] #nowa "kolejka"

In [53]:

not q #True jeśli pusta

In [54]:

q.append(4)

In [55]:

q.append('dog')

In [56]:

q.append(True)

In [57]:

q

In [58]:

len(q) #rozmiar

In [59]:

not q

In [60]:

q.append(8.4)

In [61]:

q

Out[53]:

True

Out[57]:

[4, 'dog', True]

Out[58]:

3

Out[59]:

False

Out[61]:

[4, 'dog', True, 8.4]

Page 23: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [62]:

q.pop(0) #w tym miejscu różni się od stosu

In [63]:

q.pop(0)

In [64]:

len(q)

In [65]:

q

Implementacja kolejki w PythoniePodobnie, jak w przypadku stosu, również teraz stworzymy własną klasę Queue. I również wykorzystamywbudowane w Pythona listy jako punkt wyjścia. Dla ustalenia uwagi przyjmiemy, że koniec kolejki znajdowaćsię będzie na początku znajdującej się "pod spodem" listy. Pozwoli to nam skorzystać z metody insertzdefiniowanej dla list do wstawiania nowych elementów na koniec kolejki oraz z metody pop() do usuwaniaelementów z początku kolejki. Taki wybór oznacza, że operacja wstawiania jest rzędu  , natomiastusuwanie ­ rzędu  . Możliwa jest również implementacja z początkiem listy odpowiadającym początkowikolejki. Wówczas jednak usuwanie będzie rzędu  , a wstawianie ­ rzędu  .

In [66]:

class Queue: def __init__(self): self.items = []

def isEmpty(self): return self.items == []

def enqueue(self, item): self.items.insert(0,item)

def dequeue(self): return self.items.pop()

def size(self): return len(self.items)

O(n)O(1)

O(n) O(1)

Out[62]:

4

Out[63]:

'dog'

Out[64]:

2

Out[65]:

[True, 8.4]

Page 24: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [67]:

q=Queue()q.enqueue(4)q.enqueue('dog')q.enqueue(True)print(q.size())

In [68]:

q.isEmpty()

In [69]:

q.enqueue(8.4)

In [70]:

q.dequeue()

In [71]:

q.dequeue()

In [72]:

q.size()

3

Out[68]:

False

Out[70]:

4

Out[71]:

'dog'

Out[72]:

2

Page 25: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Przykład ­ gra w gorącego ziemniakaTypowe zastosowania kolejek to symulacje rzeczywistych sytuacji, w których dane muszą być przetwarzanena sposób FIFO. Przykładem może być towarzyska gra w gorącego ziemniaka. Ustawieni w kręgu uczestnicyprzekazują sobie nawzajem (do najbliższego sąsiada) pewien element (ziemniak). W pewnym momencieakcja zostaje przerwana. Uczestnik, który jest akurat w posiadaniu elementu, odpada z gry. Akcja jestkontynuowana aż do pozostania jednego uczestnika.

Zabawa ta to w zasadzie nowoczesna wersja problemu Józefa Flawiusza(https://pl.wikipedia.org/wiki/Problem_J%C3%B3zefa_Flawiusza(https://pl.wikipedia.org/wiki/Problem_J%C3%B3zefa_Flawiusza)).

Naszym celem jest stworzenie symulacji tej gry. Wykorzystamy przy tym kolejkę, aby zaimplementowaćustawienie uczestników w kręgu. Założymy mianowicie, że ziemniaka ma zawsze uczestnik znajdujący się napoczątku kolejki. Przekazanie ziemniaka sąsiadowi będzie odpowiadało usunięciu tego uczestnika zpoczątku kolejki i wstawienie go natychmiast na koniec. Co pewien czas (po określonej liczbie operacjiusuwania/wstawiania) będziemy usuwać uczestnika znajdującego się na początku kolejki na stałe. Symulacjatoczy się do momentu, aż w kolejce zostanie tylko jeden uczestnik.

Page 26: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [73]:

def hotPotato(namelist, num): '''Simulation of hot potato game using queues. Input: namelist - list of participants num - number of operations after which a participant is removed ''' simqueue = Queue() #nowa kolejka for name in namelist: #wstaw uczestników do kolejki simqueue.enqueue(name)

while simqueue.size() > 1: #kontynuuj do dwóch pozostałych uczestników for i in range(num): simqueue.enqueue(simqueue.dequeue()) #usuń i wstaw

simqueue.dequeue() #usuń przedostatniego

return simqueue.dequeue()

In [74]:

print(hotPotato(["Bill","David","Susan","Jane","Kent","Brad"],7))

In [75]:

print(hotPotato(["Susan","David","Bill","Jane","Kent","Brad"],7))

In [76]:

print(hotPotato(["Bill","David","Susan","Jane","Kent","Brad"],3))

Susan

Bill

Kent

Page 27: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Przykład ­ symulacja kolejki wydrukuInną ciekawym zagadnieniem z wykorzystaniem kolejek może być symulacja kolejki wydruku na drukarcesieciowej. Komputery w sieci lokalnej wysyłają dokumenty do wydruku, które wstawiane są do kolejki anastępnie przetwarzane w porządku FIFO. Symulacja takiej kolejki może być interesująca, ponieważ dziękiniej możemy zbadać przepustowość drukarki czy średnie czasy oczekiwania na wydruk.

Załóżmy, że mamy pracownię komputerową, w której średnio pracuje 10 studentów w ciągu każdej godziny.Każdy z tych studentów drukuje średnio 2 razy w tym czasie, a liczba wydrukowanych stron waha sie od 1 do20. Drukarka sieciowa w pracowni nie jest najmłodsza i może wydrukować 10 stron/minutę w trybie szybkim.Może ona zostać przełączona na tryb wysokiej jakości, ale wówczas drukowałaby tylko 5 stron/minutę.Chcemy odpowiedzieć na pytanie, w którym trybie powinna pracować drukarka, aby czasy oczekiwania nawydruk nie były zbyt długie.

Symulacja wymaga od nas stworzenia reprezentacji studentów, zadań wydruku i drukarki:

zadania do drukowania generowane przez studentów trafiają do kolejkipo zakończeniu wydruku aktualnego dokumentu drukarka pobiera kolejne zadanie z początkukolejkijeśli każda długość dokumentu jest jednakowo prawdopodobna, możemy zasymulować liczby stronprzy pomocy liczb losowych z zakresu od 1 do 20.jeśli w pracowni jest 10 studentów i każdy drukuje 2 razy, to mamy średnio 20 zadań wydruku nagodzinę20 zadań na godzinę to 1 zadanie na 180 sekund:

możemy symulować prawdopodobieństwo pojawienia się zadania w każdej sekundzie losując liczbęlosową z zakresu od 1 do 180 (włącznie). Wylosowanie 180 będzie oznaczało nowe zadanie.

Zatem główne kroki symulacji będą wyglądały następująco:

× × =20 tasks

1 hour

1 hour

60 minutes

1 minute

60 seconds

1 task

180 seconds

Page 28: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

1. Utwórz pustą kolekę zadań wydruku. Każde nowe zadanie otrzyma znacznik czasu odpowiadającymomentowi wstawienia go do kolejki.

2. W każdej sekundzie (currentSecond):

Czy pojawiło się nowe zadanie? Jeśli tak, wstaw do kolejki ze znacznikiem currentSecond.Jeśli drukarka nie jest zajęta i są zadania w kolejce:

Pobierz zadanie z kolejki.Oblicz czas oczekiwania odejmując znacznik czasu od currentSecond.Wstaw czas oczekiwania do listy w celu późniejszego przetwarzania.Oszacuj czas wydruku na podstawie liczby stron.

Drukarka drukuje przez 1 sekundę. Odejmuje również sekundę od czasu potrzebnego nawydruk.Jeśli zadanie wydruku zostało zakończone, drukarka przechodzi w stan oczekiwania.

3. Oblicz średni czas oczekiwania.

Na potrzeby symulacji zdefiniujemy dwie nowe klasy: Printer i Task. Klasa Printer musi m.in. śledzićstopień zaawansowania wydruku oraz pozwolić na wyliczenie czasu wydruku:

In [163]:

class Printer: def __init__(self, ppm): self.pagerate = ppm self.currentTask = None self.timeRemaining = 0

def tick(self): if self.currentTask != None: self.timeRemaining = self.timeRemaining - 1 if self.timeRemaining <= 0: self.currentTask = None

def busy(self): if self.currentTask != None: return True else: return False

def startNext(self,newtask): self.currentTask = newtask self.timeRemaining = newtask.getPages() * 60/self.pagerate

Klasa Task reprezentuje zadanie wydruku. Podczas tworzenia jej instancji długość dokumentu będziegenerowana losowo z zakresu od 1 do 20:

Page 29: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [164]:

import random

class Task: def __init__(self,time): self.timestamp = time self.pages = random.randrange(1,21)

def getStamp(self): return self.timestamp

def getPages(self): return self.pages

def waitTime(self, currenttime): return currenttime - self.timestamp

Możemy już przejść do właściwej symulacji:

In [165]:

def newPrintTask(): num = random.randrange(1,181) if num == 180: return True else: return False

Page 30: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [166]:

def simulation(numSeconds, pagesPerMinute):

labprinter = Printer(pagesPerMinute) printQueue = Queue() waitingtimes = []

for currentSecond in range(numSeconds):

if newPrintTask(): task = Task(currentSecond) printQueue.enqueue(task)

if (not labprinter.busy()) and (not printQueue.isEmpty()): nexttask = printQueue.dequeue() waitingtimes.append( nexttask.waitTime(currentSecond)) labprinter.startNext(nexttask)

labprinter.tick()

averageWait=sum(waitingtimes)/len(waitingtimes) print("Average Wait %6.2f secs %3d tasks remaining."%(averageWait,printQueue.size()))

Page 31: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [168]:

for i in range(10): simulation(3600,10)

Warto wiedziećW bibliotece standardowej Pythona znajduje się moduł queue, w którym można znaleźć m.in. implementacjęomawianej tu kolejki. Więcej na ten temat pod adresem https://docs.python.org/3/library/queue.html(https://docs.python.org/3/library/queue.html):

In [82]:

import queue

In [83]:

q1 = queue.Queue()

In [84]:

q1.put(2)

In [85]:

q1.empty()

In [86]:

q1.put('dog')

Average Wait 18.05 secs 0 tasks remaining.Average Wait 7.80 secs 0 tasks remaining.Average Wait 14.00 secs 0 tasks remaining.Average Wait 10.24 secs 0 tasks remaining.Average Wait 14.65 secs 0 tasks remaining.Average Wait 4.94 secs 0 tasks remaining.Average Wait 22.67 secs 1 tasks remaining.Average Wait 14.64 secs 0 tasks remaining.Average Wait 34.06 secs 0 tasks remaining.Average Wait 22.95 secs 1 tasks remaining.

Out[85]:

False

Page 32: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [87]:

q1.qsize()

In [88]:

q1.get()

In [89]:

q1.get()

In [90]:

q1.empty()

Out[87]:

2

Out[88]:

2

Out[89]:

'dog'

Out[90]:

True

Page 33: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Kolejka dwustronnaKolejka dwustronna (ang. double­ended queue, w skrócie deque) to uporządkowana kolekcja elementówrozszerzająca funkcjonalność kolejki. W przypadku kolejki dwustronnej dodawanie i ściąganie elementówmożliwe jest na obu jej końcach, zarówno z przodu jak i z tyłu.

Mimo, że kolejka dwustronna posiada wiele charakterystyk stosu i kolejki, nie wymaga ona buforowania FIFOlub LIFO. Kolejność dodawania i ściągania elementów w jej przypadku pozostaje w gestii programisty.

Definicja kolejki dwustronnejKolejka dwustronna jest uporządkowaną kolekcją danych o następujących operacjach:

Deque() ­ tworzy nową pustą kolejkę dwustronną.addFront(item) ­ dodaje element na początku kolejki.addRear(item) ­ dodaje element na końcu kolejki.removeFront() ­ ściąga element z przodu kolejki.removeRear() ­ ściąga element z końca kolejki.isEmpty() ­ testuje, czy kolejka jest pusta.size() ­ podaje liczbę elementów w kolejce.

Zakładając, że d jest nową pustą kolejką, kilka operacji na niej wraz z wynikami przedstawionych jest wponiższej tabeli. Operacje wykonywane były przy założeniu, że początek kolejki znajduje się po prawejstronie.

Operacja Zawartość kolejki Zwracana wartość

d.isEmpty() [] True

d.addRear(4) [4]

d.addRear('dog') ['dog',4,]

d.addFront('cat')['dog',4,'cat']

d.addFront(True) ['dog',4,'cat',True]

d.size() ['dog',4,'cat',True] 4

d.isEmpty() ['dog',4,'cat',True] False

d.addRear(8.4) [8.4,'dog',4,'cat',True]

d.removeRear() ['dog',4,'cat',True] 8.4

d.removeFront() ['dog',4,'cat'] True

Lista w Pythonie a kolejka dwustronna

Page 34: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Warto zauważyć, że lista w Pythonie ma wszystkie charakterystyki abstrakcyjnego typu danych, jakim jestkolejka dwustronna.

In [91]:

d = [] #tworzenie

In [92]:

not d #True jeśli puste

In [93]:

d.insert(0,4) #zakładamy, że tył kolejki jest na początku listy

In [94]:

d.insert(0,'dog')

In [95]:

d.append('cat') #append wstawia na przód kolejki

In [96]:

d.append(True)

In [97]:

d

In [98]:

len(d) #liczba elementów

In [99]:

not d

In [100]:

d.insert(0,8.4)

Out[92]:

True

Out[97]:

['dog', 4, 'cat', True]

Out[98]:

4

Out[99]:

False

Page 35: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [101]:

d

In [102]:

d.pop(0) #ściąganie elementu z końca

In [103]:

d.pop() #i z początku

Implementacja kolejki dwustronnej w PythonieW najprostszej implementacji kolejki dwustronnej możemy skorzystać z możliwości pythonowej listy i poprostu stworzyć klasę, która tę listę opakuje, tworząc interfejs charakterystyczny dla kolejki dwustronnej.

In [104]:

class Deque: def __init__(self): self.items = []

def isEmpty(self): return self.items == []

def addFront(self, item): self.items.append(item)

def addRear(self, item): self.items.insert(0,item)

def removeFront(self): return self.items.pop()

def removeRear(self): return self.items.pop(0)

def size(self): return len(self.items)

Podobnie jak wcześniej, również tutaj założyliśmy, że przód kolejki to element listy o najwyższym indeksie(czyli ten po prawej). Pamiętajmy również, że ze względu na wydajność operacji na listach, dodawanie iściąganie elementu z początku kolejki w naszej implementacji jest klasy  , a z tyłu kolejki ­ klasy  .O(1) O(n)

Out[101]:

[8.4, 'dog', 4, 'cat', True]

Out[102]:

8.4

Out[103]:

True

Page 36: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [105]:

d=Deque()

In [106]:

d.isEmpty()

In [107]:

d.addRear(4)d.addRear('dog')d.addFront('cat')d.addFront(True)

In [108]:

d.size()

In [109]:

d.isEmpty()

In [110]:

d.addRear(8.4)

In [111]:

d.removeRear()

In [112]:

d.removeFront()

Out[106]:

True

Out[108]:

4

Out[109]:

False

Out[111]:

8.4

Out[112]:

True

Page 37: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Przykład ­ palindromyInteresującym przykładem zastosowania kolejki dwustronnej jest sprawdzanie, czy danych łańcuch znakówjest palindromem. Palindrom (gr. palindromeo – biec z powrotem) to wyrażenie brzmiące tak samo czytaneod lewej do prawej i od prawej do lewej strony.

Przykłady:

Kobyła ma mały bok (po usunięciu spacji)Ada raportuje, że jutro parada (Edmund John)Ale uda szałowe me! – Woła z sadu Ela. (j.w.)A to idiotakajak, radar

Pomysł wykorzystania kolejki dwustronnej do sprawdzania palindromów przedstawiony jest na poniższymrysunku.

Dane wyrażenie przetwarzamy od lewej do prawej. Każdy nowy znak dodajemy na koniec kolejki. Następnieściągamy jednocześnie po jednym znaku z początku i końca kolejki i porównujemy je ze sobą. Jeśli są takiesame, kontynuujemy przetwarzanie i ściągamy następną parę. Jeżeli wyrażenie jest palindromem, to albousuniemy z kolejki wszystkie znaki, albo zostanie w niej tylko jeden, w zależności od tego, czy liczba znakóww wyrażeniu była parzysta czy nieparzysta.

Page 38: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [113]:

def palchecker(aString): chardeque = Deque()

for ch in aString: chardeque.addRear(ch)

stillEqual = True

while chardeque.size() > 1 and stillEqual: first = chardeque.removeFront() last = chardeque.removeRear() if first != last: stillEqual = False

return stillEqual

In [114]:

print(palchecker("lsdkjfskf"))print(palchecker("radar"))

Warto wiedziećW module collections z biblioteki standardowej Pythona znajduje się gotowa implementacja kolejkidwustronnej ( https://docs.python.org/3/library/collections.html#collections.deque(https://docs.python.org/3/library/collections.html#collections.deque)).

In [115]:

from collections import deque

In [116]:

d = deque('ghi')

In [117]:

d

FalseTrue

Out[117]:

deque(['g', 'h', 'i'])

Page 39: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [118]:

d.append('j')

In [119]:

d.appendleft('f')

In [120]:

d

In [121]:

d.pop()

In [122]:

d.popleft()

In [123]:

d.extend('jkl')

In [124]:

d

In [125]:

d.rotate(1)

In [126]:

d

In [127]:

d.clear()

Out[120]:

deque(['f', 'g', 'h', 'i', 'j'])

Out[121]:

'j'

Out[122]:

'f'

Out[124]:

deque(['g', 'h', 'i', 'j', 'k', 'l'])

Out[126]:

deque(['l', 'g', 'h', 'i', 'j', 'k'])

Page 40: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [128]:

d.pop()

ListyDo tej pory używaliśmy pythonowych list do zaimplementowania abstrakcyjnych struktur danych: stosu,kolejki i kolejki dwustronnej. Lista to bardzo uniwersalny typ danych, w którym elementy ułożone są wliniowym porządku, określonym poprzez związane z nimi wskaźniki.

Nie wszystkie języki programowania oferują programiście typ danych odpowiadający liście. W innychimplementacje list mogą nie być optymalne. W takich przypadkach zadaniem programisty jestzaimplementowanie abstrakcyjnej struktury danych o charakterystyce listy.

ListyTo po prostu kolekcja elementów, z których każdy pamięta swoją pozycję względem pozostałych elementów.. Lista z Pythona stanowi przykład implementacji listy. Podstawowe operacje, jakie powinny być możliwe dowykonania na liście, są następujące:

List() ­ tworzy nową listę.add(item) ­ dodaje element do listy.remove(item) ­ usuwa element z listy.search(item) ­ szuka elementu w liście.isEmpty() ­ sprawdza, czy lista jest pusta.size() ­ liczba elementów w liście.append(item) ­ dodaje element na koniec listy.index(item) ­ pozycja elementu.insert(pos,item) ­ wstawia element na pozycji podanej jako pierwszy argument.pop() ­ usuwa ostatni element i zwraca jego wartość.pop(pos) ­ usuwa element z pozycji pos i zwraca jego wartość.

Listy w PythonieO przydatności list wbudowanych w Pythonie mogliśmy się już wielokrotnie przekonać. Ich metody oferująm.in. wspomnianą powyżej funkcjonalność

In [135]:

l = [] #nowa lista

---------------------------------------------------------------------------IndexError Traceback (most recent call last)<ipython-input-128-c3f74e487764> in <module>()----> 1 d.pop()

IndexError: pop from an empty deque

Page 41: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [136]:

l.append(4) #dodajemy coś na koniecl.append('dog')l.append(8.4)

In [137]:

l

In [138]:

len(l) #rozmiar

In [139]:

l.insert(0,True) #wstawiamy na początek

In [140]:

l

In [141]:

not l #czy pusta?

In [142]:

l.pop()

In [143]:

l.pop(0)

Out[137]:

[4, 'dog', 8.4]

Out[138]:

3

Out[140]:

[True, 4, 'dog', 8.4]

Out[141]:

False

Out[142]:

8.4

Out[143]:

True

Page 42: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [146]:

l.index('dog')

In [147]:

l.index('10')

In [148]:

10 in l

In [149]:

4 in l

In [150]:

l.remove(4)

In [151]:

l

Out[146]:

1

---------------------------------------------------------------------------ValueError Traceback (most recent call last)<ipython-input-147-01456c578c02> in <module>()----> 1 l.index('10')

ValueError: '10' is not in list

Out[148]:

False

Out[149]:

True

Out[151]:

['dog']

Page 43: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Implementacja listyZaimplementujemy naszą listę w postaci tzw. listy jednokierunkowej. To prosta i elastyczna struktura, służącado reprezentowania zbiorów dynamicznych (zmieniających się w czasie).

Definicja listy wymaga, abyśmy pamiętali pozycje poszczególnych elementów względem siebie. Nie mówinatomiast nic na temat tego, jak wskaźniki określające te pozycje mają być przechowywane w pamięcikomputera.

Rozważmy dla przykładu kolekcję elementów przedstawionych na następującym rysunku:

Wydaje się, że wartości te zostały rozmieszczone na płaszczyźnie w sposób losowy. Gdyby jednak każdy zelementów zawierał dodatkową informację na temat pozycji następnego elementu, otrzymalibyśmyuporządkowaną sekwencję:

Ważne jest, aby przy takim sposobie prezentowania listy określić położenie pierwszego elementu jawnie.Jeżeli wiemy, gdzie jest pierwszy element, informacja w nim zawarta wskaże nam drugi, potem trzeci itd.Podobnie ostatni element na liście powinien "wiedzieć", że nie ma już kolejnego.

W pierwszym kroku zdefiniujemy sobie klasę Node, która będzie reprezentowała pojedynczy element.Oprócz jego wartości klasa musi przechowywać wskaźnik do kolejnego elementu:

Page 44: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [152]:

class Node: def __init__(self,initdata): self.data = initdata self.next = None

def getData(self): return self.data

def getNext(self): return self.next

def setData(self,newdata): self.data = newdata

def setNext(self,newnext): self.next = newnext

Sprawdźmy, jak działa ta klasa:

In [153]:

temp = Node(93)

In [154]:

temp.getData()

In [156]:

print(temp.getNext())

Out[154]:

93

None

Page 45: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Referencja do wartości specjalnej None oznacza, że węzeł nie wskazuje na żaden inny obiekt. Ustawianie None automatycznie w konstruktorze klasy nazywane jest czasami "uziemianiem obiektu", dlatego nadiagramach często używa się symbolu uziemienia:

Page 46: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

Listę możemy potraktować jako kolekcję zdefiniowanych powyżej węzłów:

Wspomnieliśmy już wcześniej, że implementacja listy powinna wskazywać na pierwszy element. Jednak przytworzeniu pustej listy chcemy, aby była ona "uziemiona", tzn.

Dzięki temu m.in. bardzo prosto będzie sprawdzić, czy jest ona pusta (referencja do pierwszego elementuma wartość None).

Aby zaimplementować dodawanie elementów do listy, musimy zdecydować, w którym miejscu ma zostaćwstawiony nowy element. Założymy tutaj, że będzie on wstawiany na początek listy:

Operacja ta wymaga dwóch kroków. W pierwszym musimy zmienić wskaźnik dodawanego elementu na tenznajdujący się na początku starej listy. W drugim zmieniamy oznaczenie początku listy.

Do implementacji metod size, search i remove możemy skorzystać z tzw. techniki przechodzenia.Startując od elementu znajdującego się na początku listy przechodzimy do następnego zgodnie zewskaźnikiem, sprawdzamy jego wartość, ewentualnie przechodzimy do następnego itd. Przechodzenierealizuje się często przy pomocy zewnętrznego wskaźnika odnoszącego się do akurat odwiedzanego węzła iaktualizowanego przy każdym przeskoku.

Usunięcie elementu z listy będzie wymagało zmodyfikowania wskaźnika w jego poprzedniku. Ponieważjednak ten typ listy nie wspiera cofania się do poprzednich elementów, rozwiążemy ten problem przy pomocydwóch zewnętrznych wskaźników.

Page 47: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych
Page 48: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [157]:

class UnorderedList:

def __init__(self): self.head = None

def isEmpty(self): return self.head == None

def add(self,item): temp = Node(item) temp.setNext(self.head) self.head = temp

def size(self): current = self.head count = 0 while current != None: count = count + 1 current = current.getNext()

return count

def search(self,item): current = self.head found = False while current != None and not found: if current.getData() == item: found = True else: current = current.getNext()

return found

def remove(self,item): current = self.head previous = None found = False while not found: if current.getData() == item: found = True else: previous = current current = current.getNext()

if previous == None: #jeśli usuwamy pierwszy element self.head = current.getNext() else: previous.setNext(current.getNext())

Page 49: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [158]:

mylist = UnorderedList()

In [159]:

mylist.add(31)mylist.add(77)mylist.add(17)mylist.add(93)mylist.add(26)mylist.add(54)

In [160]:

print(mylist.size())print(mylist.search(93))print(mylist.search(100))

In [161]:

mylist.add(100)print(mylist.search(100))print(mylist.size())

6TrueFalse

True7

Page 50: Algorytmy i struktury danychprac.im.pwr.wroc.pl/~szwabin/assets/algo/lectures/2.pdf · 2016. 10. 12. · Algorytmy i struktury danych Wykłady 2 i 3 Abstrakcyjne struktury danych

In [162]:

mylist.remove(54)print(mylist.size())mylist.remove(93)print(mylist.size())mylist.remove(31)print(mylist.size())print(mylist.search(93))

Kilka słów o wydajności

Operacja Złożoność

isEmpty()

size()

search()

remove()

add() $O(1)

O(1)

O(n)

O(n)

O(n)

654False