• Nie Znaleziono Wyników

Górniczo-Hutnicza w Krakowie

N/A
N/A
Protected

Academic year: 2021

Share "Górniczo-Hutnicza w Krakowie"

Copied!
44
0
0

Pełen tekst

(1)

Akademia

Górniczo-Hutnicza w Krakowie

Adrian Horzyk horzyk@agh.edu.pl

(2)

PROGRAMISTA vs. INFORMATYK

Czym różni się informatyk od programisty?

• Programista zajmuje się wyszukiwaniem odpowiednich algorytmów

oraz ich zestawianiem i translacją na postać programów komputerowych w konkretnym języku programowania, starając się ich działanie możliwie zoptymalizować na poziomie języka programowania poprzez dobór

odpowiednich metod i ew. również struktur danych.

• Informatyk powinien być programistą, który potrafi na bazie zdobytej wiedzy umieć rozwiązywać różne problemy poprzez tworzenie nowych algorytmów, które mogą być podobne do istniejących lub

wykorzystywać istniejące algorytmy i struktury danych, lecz w nowy innowacyjny sposób.

Ponadto informatyk powinien umieć dostosować i zoptymalizować

istniejące algorytmy do rozwiązywanego zadania poprzez uproszczenie, rozbudowanie lub modyfikację zwykle stosowanych struktur danych oraz uproszczenie lub modyfikacje pewnych części algorytmu

dopasowując go do wymogów rozwiązywanego zadania.

(3)

ALGORYTMIKA

Algorytmika – to dział informatyki zajmujący się teorią związaną z tworzeniem i badaniem algorytmów, poprawnością, złożonością oraz algorytmizowaniem procesów i zjawisk zachodzących w otaczającym nas świecie.

Tworzenie algorytmu to proces modelowania pewnego zjawiska lub procesu dla pewnego rodzaju danych wejściowych, które mają zostać przetworzone w celu osiągnięcia pewnego wyniku w skończonej ilości kroków.

Algorytmizacja jest więc procesem budowy algorytmu dla konkretnego zjawiska lub procesu, który chcemy zamodelować i zautomatyzować, celem jego implementacji i wykonania na maszynie obliczeniowej.

Jednym z podstawowych celów algorytmiki jest analiza algorytmów pod względem efektywności, dokładności i poprawności ich działania oraz poszukiwanie algorytmów o większej efektywności i dokładności.

Algorytmika bada również kwestie złożoności czasowej i pamięciowej algorytmów w celu weryfikacji ich możliwości wykonania na dostępnych zasobach i w pożądanym czasie.

Część problemów wymaga rozwiązania w czasie rzeczywistym (real time).

(4)

SCHEMAT BLOKOWY

Schemat blokowy jest jednym ze sposobów zapisu algorytmu prezentujący kolejne kroki (instrukcje), jakie trzeba wykonać w celu osiągnięcia postawionego celu.

Schemat blokowy wykorzystuje pewien zbiór figur geometrycznych

reprezentujących pewne kategorie operacji na danych oraz połączenia, które wskazują kierunek ich przetwarzania i możliwie alternatywne przejścia:

Blok graniczny wskazujący początek, koniec, przerwanie lub wstrzymanie wykonywania algorytmu.

Blok wejścia-wyjścia związany jest z wprowadzanie danych do przetworzenia oraz wyprowadzaniem wyników.

Blok operacyjny służy do wykonywania operacji na danych przechowywanych w postaci zmiennych i stałych różnego typu.

Blok decyzyjny (warunkowy) umożliwia dokonanie wyboru na podstawie wyniku operacji logicznej zapisanej w postaci pewnego warunku.

Blok podprogramu umożliwia przejście do wydzielonego fragmentu algorytmu i wywołania go dla pewnych parametrów, ew. zwrot obliczonych wartości.

Blok fragmentu przedstawiający wyodrębniony fragment kodu.

Blok komentarza pozwala wstawić objaśnienia ułatwiające zrozumienie algorytmu.

Łącznik wewnętrzny służy do łączenia odrębnych części schematu na tej samej stronie opatrzony etykietą przeniesienia.

Łącznik zewnętrzny łączy schematy na różnych stronach,

więc powinien zawierać numer strony i etykietę przeniesienia.

TAK NIE

1 1

4.2 4.2

(5)

SCHEMAT BLOKOWY

Algorytm możemy opisać słownie, przy pomocy schematu blokowego lub kodu w języku programowania, np. wyznaczający NWD i NWW algorytmem Euklidesa:

START

Wczytaj a, b

r>0

TAK

NIE

Opis słowny (pseudokod)

1. Wczytaj dwie liczby naturalne a i b.

2. Jeśli a > b,

to przyporządkuj w = a oraz m = b,

a w przeciwnym przypadku przyporządkuj

w = b oraz m = a.

3. Następnie dopóki r jest większe od zera wyznacz resztę z

dzielenia w przez m i zapisz ją w zmiennej r oraz dokonaj przyporządkowania w = m, m = r.

4. Wypisz wartość w NWD oraz a*b/w jako NWW

Schemat blokowy Program w Pythonie

print "Podaj dwie liczby naturalne:”

a = input("Pierwsza liczba:") b = input("Druga liczba:") if a > b:

w = a; m = b else:

w = b; m = a while r:

r = w % m w = m m = r

print "NWD liczb %i i %i wynosi %i, a ich NWW

wynosi %i" % (a, b, w, a*b/w) Wypisz w

oraz a*b/w r = w % m

w = m m = r

w = a m = b a > b TAK

NIE

w = b m = a

(6)

SCHEMAT BLOKOWY – PRZYKŁADY

SORTOWANIE PRZEZ WYBIERANIE SORTOWANIE PRZEZ WSTAWIANIE

(7)

STRATEGIA: DZIEL I ZWYCIĘ ŻAJ

Strategia „dziel i zwyciężaj” (a divide and conquer strategy) polega

na rekurencyjnym dzieleniu zadania na mniejsze i stosowaniu tej strategii do zadań składowych tak długo, dopóki nie uzyska się rozwiązania całego zadania.

Następnie (w zależności od postawionego zadania) rozwiązanie końcowe składa się z rozwiązań cząstkowych uzyskanych dla podzielonych podzadań, jedynie że rozwiązanie cząstkowe jest zarazem rozwiązaniem końcowym.

Dzięki takiej strategii, ilość porównań (lub innych operacji) znacznie maleje (zwykle logarytmicznie).

Strategię tą stosujemy w wielu algorytmach, np.:

• wyszukiwanie połówkowe (binary search),

• wyszukiwanie interpolowane (interpolation search),

• sortowaniu szybkim (quick sort),

• sortowaniu przez scalanie (merge sort)

w celu osiągnięcia wysokiej efektywności ich działania.

(8)

WYSZUKIWANIE

Wyszukiwanie – to najczęstsze operacje wykonywane w informatyce!

Jego efektywna implementacja decyduje zwykle o szybkości całej aplikacji.

Nieuporządkowane struktury sekwencyjne musimy przeszukiwać sekwencyjnie element po elemencie, czyli tzw. wyszukiwanie liniowe (sequential search), co w pesymistycznym

przypadku zmusza nas do przeglądnięcia wszystkich N elementów, więc kosztuje N porównań!

PRZYKŁAD: N = 1.000.000.000 →Max ilość porównań: 1.000.000.000

W przypadku przeszukiwania uporządkowanej sekwencyjnej struktury danych, możemy zastosować algorytm wyszukiwania połówkowego (binary search), który wymaga maksymalnie log2 Noperacji porównywania.

PRZYKŁAD: N = 1.000.000.000 →Max ilość porównań: 30

W przypadku równomiernego rozkładu liczb w uporządkowanej sekwencji

możemy zastosować algorytm wyszukiwania interpolowanego (interpolation search), który próbuje „odgadnąć” pozycję (obliczyć indeks) poszukiwanej wartości,

co wiąże się z ilością operacji porównywania równą log2 log2N

PRZYKŁAD: N = 1.000.000.000 →Przewidywana ilość porównań ok. 5

Istnieje jeszcze możliwość wykorzystania tablicy haszującej do bardzo szybkiego wyszukiwania elementów w czasie stałym, o ile jesteśmy w stanie określić funkcję haszującą

dla przeszukiwanej sekwencji danych, od których zwykle również wymaga się pewnego

równomiernego rozkładu. Funkcja haszująca określa miejsce w tablicy wskaźnika krótkiej listy elementów, w której znajduje się poszukiwany element, stąd tak ważny jest odpowiedni

rozkład danych oraz możliwość szybkiego O(1) przejrzenia takiej listy.

Zwykle i tak wykonujemy pewną stałą ilość porównań, więc ich ilość jest zwykle porównywalna z wyszukiwaniem interpolowanym.

(9)

WYSZUKIWANIE SEKWENCYJNE

Przeszukiwanie sekwencyjne – stosowane jest do nieuporządkowanych liniowych struktur danych, przeglądając poszczególne elementy jeden po drugim aż do napotkania poszukiwanego elementu:

Pierwszy algorytm

przeszukuje listę od tyłu w celu eliminacji obliczania wartości indeksu w przypadku nieodnalezienia elementu.

Drugi algorytm stosuje

wartownika dodanego

na końcu listy w celu

uproszczenia warunku

sprawdzania zakończenia

pętli, co zwiększa szybkość

jego działania w stosunku

do poprzedniego.

(10)

WYSZUKIWANIE POŁÓWKOWE

Przeszukiwanie połówkowe – dzieli przeszukiwaną uporządkowaną strukturę liniową (np. listę, tablicę) na 2 części, wyznaczając indeks środkowego elementu i sprawdzając, czy jest on równy poszukiwanemu.

Jeśli to nie jest, wtedy powtarza tą samą procedurę rekurencyjnie na tej części struktury, która może zawierać poszukiwany element:

Algorytm wykonuje znacznie mniejszą ilość porównań niż te poprzednie!

(11)

WYSZUKIWANIE INTERPOLOWANE

Przeszukiwanie interpolowane – umożliwia bardzo szybkie wyszukiwanie elementów w posortowanej liniowej strukturze danych o mniej więcej równomiernym rozkładzie wartości w przeszukiwanym przedziale:

Przy tych założeniach algorytm wykonuje jeszcze mniejszą ilość porównań niż te poprzednie, gdyż próbuje obliczyć (zgadnąć) indeks poszukiwanego elementu na podstawie jego wartości oraz wartości pierwszego i ostatniego elementu w przeszukiwanym przedziale. Jeśli mu się to nie uda, wtedy

zawęża obszar poszukiwać na przedziału, który zawiera poszukiwany element.

(12)

TABLICE I FUNKCJE HASHUJĄCE

Wyszukiwanie z wykorzystaniem tablicy haszującej (hash table) umożliwia teoretycznie najszybsze wyszukiwanie (w czasie stałym) pod warunkiem określenia takiej funkcji haszującej, iż możliwe jest dokładne obliczenie w tablicy indeksu początku odpowiedniej krótkiej listy, w której znajduje się poszukiwany element.

Podobnie więc jak w wyszukiwaniu interpolowanym równomierny rozkład wartości danych jest zaletą.

Ponadto stworzenie funkcji haszującej wymaga zwykle znajomości wartości minimalnej i maksymalnej, co kosztuje czas liniowy O(n) i wymaga dodatkowego miejsca w pamięci zależnego liniowo od n, a więc również O(n).

Funkcja haszująca h(x) przekształca klucz w indeks w tablicy haszującej, której wielkość może być mniejsza niż ilość kluczy, lecz wtedy pod tym samym indeksem mieści się kilka kluczy, które zwykle organizowane są w postaci listy (nieposortowanej lub posortowanej przez proste wstawianie).

Funkcja haszująca modularna h(k) uniemożliwia przekroczenie przewidywanego zakresu kluczy:

h(k) = k % m, gdzie m to rozmiar tablicy mieszającej (haszującej).

Tablice haszujące nazywane są też tablicami mieszającymi lub tablicami z haszowaniem.

Niektóretablice haszujące można również wykorzystać do sortowania danych.

Najpierw tworzymy taką tablicę krótkich list, a następnie łączymy je ze sobą tworząc listę wynikową lub przepisujemy kolejno wyniki do tablicy wynikowej, co jednak kosztuje miejsce w pamięci zależne od ilości danych (not in place).

Słownikiw Pythonie są wewnętrznie implementowane jako tablice haszujące.

(13)

PRZYKŁAD TABLICY HASZUJ ĄCEJ

Typowa tablica haszująca to tablica krótkich posortowanych list, w których indeks początku listy w tablicy wyznaczamy przy pomocy funkcji haszującej, którą dobiera się do konkretnego rodzaju i rozkładu kluczy w przestrzeni.

Idealnie jest, gdy uda się znaleźć przyporządkowanie zwarte, które N kluczom przydziela unikalne N wartości z zakresu od 0 do N-1.

Listy najprościej i najszybciej O(1) uzupełnia się od przodu w miejscu głowy listy,

ew. od tyłu, dodając nowy obiekt na końcu listy (append), uzyskując nieposortowane listy obiektów, które potem są przeglądane liniowo w celu odnalezienie klucza.

Biorąc pod uwagę to, iż elementów na liście jest niewiele, przyjmuje się,

iż zabiera to stałą ilość czasu.

(14)

SORTOWANIE KUBEŁKOWE

Odpowiednio dobrana funkcja haszująca wespół z odpowiednim rozkładem danych

w przestrzeni pozwala wykorzystać funkcję haszującą do szybkiego O(n) sortowania danych:

Do krótkich list można przez proste wstawianie dodawać nowe elementy w kolejności rosnącej lub malejącej uzyskują tablicę posortowanych list. Takie sortowanie jest również wykonywane w czasie stałym przy założeniu niewielkich list.

W przypadku konieczności wyszukania kluczy elementów na poszczególnych podlistach możliwe jest wykorzystanie szybszych algorytmów, np. wyszukiwanie połówkowe.

Ponadto można takie posortowane listy wykorzystać do budowy posortowanej listy

wynikowej wszystkich obiektów, co odbywa się teoretycznie średnio w czasie liniowym

O(n) i nazywa się sortowaniem kubełkowym (bucket sort).

(15)

SORTOWANIE SZYBKIE

Sortowanie szybkie (quick sort) – jest jednym z najbardziej efektywnych algorytmów sortowania ogólnego przeznaczenia wykorzystującym strategię

„dziel i zwyciężaj”, lecz niestety nie jest sortowaniem stabilnym, co wyklucza go z pewnych zastosowań, gdzie stabilność jest wymagana!

Sortowanie stabilne to takie, które zachowuje względną kolejność kluczy o tych samych wartościach w ciągu posortowanym.

Przedstawiony algorytm jest algorytmem rekurencyjnym, gdyż wywołuje sam siebie i posiada warunek stopu, tj. dla first >= j oraz i >= last nie dojdzie do ponownego wywołania rekurencyjnego.

To typowa dla Pythona forma skrótowa, ale niezbyt efektywna.

(16)

REKURENCJA

Algorytm rekurencyjny to taki, który wywołuje sam siebie i posiada warunek stopu, który określa moment, w którym do dalszych wywołań nie dojdzie.

Warunek stopu umożliwia zatrzymanie kolejnych wywołań i wyjście z procedury rekurencyjnej.

Parametry wywołania funkcji rekurencyjnej mogą się zmieniać.

Należy pamiętać o tym, iż każde wywołanie funkcji pociąga za sobą odłożenia

pewnych danych na stosie systemowym, co dodatkowo wpływa na zmniejszenie efektywności wykonania programu!

Algorytm sortowania szybkiego można przedstawić w formie rekurencyjnej.

Algorytmy rekurencyjne można zawsze

przekształcić na algorytmy iteracyjne.

(17)

PRZYKŁAD REKURENCJ I

Skoczek szachowy ma za zadanie obskoczyć wszystkie pola szachownicy NxN dokładnie jeden raz. Można do tego wykorzystać algorytm rekurencyjny, który próbuje wykonać jeden z dostępnych ruchów, a jeśli dana sekwencja skoków nie prowadzi do sukcesu, wtedy się wycofać próbować ponownie inną:

algorytm w pseudokodzie

(18)

EFEKTYWNOŚ Ć

Efektywność wykonywania różnych operacji na danych w informatyce jest najczęściej związana z zastosowaniem odpowiednich struktur danych,

na których dodatkowo można wykonywać operacje zdecydowanie szybciej, gdy przechowywane dane w nich są uporządkowane.

Wobec tego duże znaczenie ma stosowanie odpowiednio szybkich

algorytmów sortowania dopasowanych do rodzaju i rozmiaru sortowanych danych.

Algorytmy sortowania różnią się kilkoma właściwościami:

• mogą sortować dane w miejscu lub nie w miejscu

• mogą być stabilne lub niestabilne

• mogą wykonywać sortowanie z różną złożonością obliczeniową

Sortowanie w miejscu nie wymaga wykorzystania dodatkowej pamięci zależnej od ilości sortowanych danych, lecz tylko pewną stałą jej ilość.

Sortowanie stabilne zapewnia, iż względna kolejność sortowanych kluczy

o tych samych wartościach będzie zachowana w ciągu posortowanym,

zaś w przypadku sortowania niestabilnego, takiej gwarancji nie ma.

(19)

ZŁOŻONOŚ Ć OBLICZENIOWA

Złożoność obliczeniowa i poprawność semantyczna algorytmów badana jest przez algorytmikę – dział informatyki zajmujący się analizą

algorytmów .

W trakcie analizy algorytmów próbujemy udzielić odpowiedzi na pytania:

Czy możliwe jest rozwiązanie zadania w dostępnym czasie i pamięci?

Który ze znanych algorytmów należy zastosować w danych okolicznościach?

Czy istnieje lepszy, szybszy lub dokładniejszy algorytm od rozważanego?

Jak udowodnić, że zastosowany algorytm poprawnie rozwiąże zadanie?

Czy możliwe jest zrównoleglenie algorytmu i wykorzystanie większej ilości rdzeni obliczeniowych?

Złożoność obliczeniowa badana jest w dwóch podstawowych aspektach:

• czasu – złożoność czasowa

• pamięci – złożoność pamięciowa

Celem analizy algorytmów jest opracowanie metod formalnych i analiz

umożliwiających zbudowanie optymalnych, efektywnych i poprawnych

semantycznie algorytmów wykonalnych na dostępnych zasobach.

(20)

RODZAJE ZŁOŻONOŚ CI OBLICZENIOWEJ

Złożoność czasowa to ilość czasu potrzebnego na wykonanie zadania, wyrażona jako funkcja ilości danych, mierzona ilością elementarnych kroków, tj. instrukcji warunkowych, przypisania i operacji arytmetycznych.

Złożoność pamięciowa to ilość dodatkowej pamięci potrzebnej do wykonania zadania, wyrażona jako funkcja ilości danych.

Rozróżniamy kilka rodzajów złożoności:

Pesymistyczna – określa ilość zasobów (czasu lub dodatkowej pamięci) potrzebnych do wykonania algorytmu przy założeniu wystąpienia „złośliwych” lub najgorszych danych. Oznaczamy ją O(f(n)) , gdzie f(n) jest funkcją ilości danych n.

Praktyczna – określa dokładną liczbę elementarnych kroków potrzebnych do wykonania algorytmu rozwiązującego określone zadanie.

Oczekiwana– określa ilość zasobów potrzebnych do wykonania algorytmu przy założeniu wystąpienia „typowych” lub oczekiwanych danych.

Optymistyczna Ω(f(n)) – określa ilość zasobów potrzebnych do wykonania algorytmu przy założeniu wystąpienia „najlepszych” danych.

Złożoność wykonywania różnych algorytmów możemy szacować:

od dołu stwierdzając, iż złożoność obliczeniowa jest nie mniejsza niż pewna klasa funkcji Ω(f(n))

asymptotycznie, szacując dokładnie złożoność obliczeniową poprzez pewną klasę funkcji Θ(f(n))

od góry ograniczając złożoność obliczeniową poprzez pewną klasę funkcji O(f(n))

(21)

KLASY ZŁOŻONOŚ CI OBLICZENIOWEJ

Klasy złożoności obliczeniowej określają pewne typowe funkcje zależne od ilości danych, które stosowane są wyznaczenia złożoności obliczeniowej danego zadania. Do najbardziej typowych zaliczamy złożoność:

---PROBLEMY ŁATWE--- Stałą Θ(1) – gdy czas wykonania algorytmu jest niezależny od rozmiaru danych

Logarytmiczną Θ(log n) – gdy czas wykonania zależny jest od logarytmu rozmiaru danych Liniową Θ(n) – gdy czas wykonania zależny jest liniowo od rozmiaru danych

Logarytmiczno-liniową Θ(n log n) – gdy czas wykonania algorytmu jest logarytmiczno-linowy Kwadratową Θ(n2) – gdy czas wykonania zależny jest od kwadratu rozmiaru danych

Sześcienną Θ(n3) – gdy czas wykonania zależny jest od sześcianu rozmiaru danych

Wielomianową Θ(nk+ nk-1 + … + n) – gdy czas wykonania zależny jest od wielomianu rozmiaru danych

--- PROBLEMY TRUDNE --- Wykładniczą Θ(2n) – gdy czas wykonania rośnie wykładniczo z rozmiarem danych

Silnia Θ(n!) – gdy czas wykonania wyrażona jest za pomocą silni rozmiaru danych, czyli związana jest z przeszukiwaniem wszystkich (kombinacji, wariacji czy permutacji danych)

(22)

PRZYKŁADY KLAS ZŁOŻONOŚ CI OBLICZENIOWEJ

---PROBLEMY ŁATWE--- Stała Θ(1): dowolna elementarna instrukcja przypisania, pojedynczej operacji arytmetycznej lub logicznej, porównania, np. x = 2, x += 1, 2+3, if x<y, z and w

Stała O(1): wyszukiwanie w tablicy haszującej, jeśli możliwe jest określenie funkcji haszującej w taki sposób, aby dostęp do poszczególnych elementów był stały.

Logarytmiczno-logarytmiczna Θ(log log n): algorytm wyszukiwania interpolowanego przy spełnieniu warunków jego zastosowania

Logarytmiczna Θ(log n): algorytm wyszukiwania połówkowego, bisekcji, przechodzenia od korzenia do liścia lub w drugą stronę w drzewie wyważonym zawierającym n węzłów, np. w drzewie BST, kopcu zupełnym, B-drzewach.

Liniowa Θ(n):wyszukiwanie liniowe w n-elementowych ciągach nieposortowanych, sortowanie

przez zliczanie i pozycyjne, wyszukiwanie wzorców metodą Knutha-Morrisa-Pratta w ciągach tekstów.

Logarytmiczno-liniowa Θ(n log n):najefektywniejsze metody sortowania elementów polegające na porównaniach (kopcowe, przez łączenie, szybkie) ogólnego przeznaczenia.

Kwadratowa Θ(n2):proste algorytmy sortowania przez wstawianie, wybieranie, bąbelkowe, szukanie najkrótszej ścieżki w grafie metodą ….

--- PROBLEMY TRUDNE ---

Wykładnicza Θ(2n) – gdy problem dzieli się w każdym kroku na dwa lub więcej składowych operujących na całym zbiorze danych, np. niektóre algorytmy wykorzystujące rekurencję, np. ciąg Fibonacciego.

Silnia Θ(n!) – różne problemy kombinatoryczne polegające na przeszukiwaniu wszystkich kombinacji, wariacji czy permutacji n danych, np. niektóre algorytmy operujące na grafach.

(23)

DZIAŁANIA NA KLASACH ZŁOŻONOŚ CI OBLICZ.

Próbując określić klasę złożoności obliczeniowej dla całego algorytmu lub programu analizujemy jego części wyznaczając ich klasy złożoności obliczeniowej, a następnie korzystając z poniższych operacji wyznaczamy ogólną złożoność:

Θ(c) = Θ(1) – dowolna liczba operacji c niezależna od n to klasa stałej złożoności obliczeniowej.

c · Θ(f(n)) = Θ(f(n)) – gdy c jest stałą niezależną od n.

Taki przypadek występuje np. w sytuacji, gdy wykonujemy pętlę obliczeniową pewną stałą ilość razy niezależną od wymiaru danych n, wtedy taka pętla nie zwiększa złożoności

obliczeniowej. Ważne jest ustalenie niezależności c od n!

Θ(f(n)) + Θ(g(n)) = max( Θ(f(n)), Θ(g(n)) ) – a więc sumę złożoności obliczeniowych charakteryzuje ta o większej klasie złożoności obliczeniowej.

To najczęstszy przypadek, gdy obliczamy złożoność na podstawie kolejnych fragmentów kodu następujących po sobie w programie lub algorytmie. O złożoności decyduje fragment kodu o największej złożoności obliczeniowej.

Θ(f(n)) · Θ(g(n)) = Θ( f(n) · g(n)) – w przypadku iloczynu złożoności obliczeniowych klasa będzie określona poprzez klasę iloczynu funkcji określających poszczególne klasy.

Mamy z tym do czynienia np. w przypadku pętli zagnieżdżonych lub podczas wywoływania funkcji / procedur / metod. Jeśli np. funkcja ma złożoność g(n) i jest wywoływana

w programie głównym w pętli o złożoności f(n), wtedy złożoność tego fragmentu kodu, tj. pętli zawierającej wywołanie tej funkcji będzie miało złożoność Θ( f(n) · g(n)).

(24)

DLACZEGO ZŁOŻONOŚ Ć J EST WAŻNA?

Problemy złożoności obliczeniowej bezpośrednio przekładają się na możliwość wykonania algorytmu na współczesnych maszynach obliczeniowych opartych na modelu maszyny Turinga, a ponadto istotnie wpływają na czas obliczeń:

gdzie wiek naszego wszechświata szacuje się na 13,7 mld lat.

Rozróżniamy więc dwie podstawowe i jedną dodatkową klasę algorytmów pod względem złożoności obliczeniowej:

Łatwe (P – polynomial) – czyli rozwiązywalne co najwyżej w czasie wielomianowym

Trudne (NP – non-polynomial) – wymagające czasu dłuższego niż wielomianowy, czyli np. wykładniczy lub silni

NP zupełne (NPC – NP complete) – nie udowodniono, iż nie posiadają rozwiązania wielomianowego.

(25)

PRZYKŁAD SORTOWANIA OBIEKTÓW

Najpierw definiujemy klasę zawierającą obiekty składające się z pewnej liczby atrybutów oraz ich nazwy, wczytując je z pliku CSV i zapisując w postaci listy list:

Następnie przeprowadzimy pomiar szybkości działania różnych algorytmów

sortowania, porządkujących te obiekty po kolei według wszystkich ich atrybutów.

(26)

ZBIÓR OBIEKTÓW DO POSORTOWANIA

ATRYBUTY OBIEKTÓW

OBIEKT

(27)

SPOSÓB DZIAŁANIA SORTOWANIA SZYBKIEGO

Sortowanie szybkie (quick sort) w podanym przedziale wyszukuje

niemniejszy klucz z lewej i niewiększy klucz z prawej i dokonuje

ich zamiany miejscami dopóki indeksy i, j się nie miną:

(28)

SPOSÓB DZIAŁANIA SORTOWANIA SZYBKIEGO

1

2 3

4 5

6

7

8

9

(29)

ANALIZA ZŁOŻONOŚ CI OBLICZENIOWEJ Sortowanie

szybkie

(quicksort):

Ile operacji

elementarnych?

Ile porównań?

Ile przypisań?

Niestabilne,

w miejscu.

(30)

PRZYKŁAD ANALIZY ZŁOŻONOŚ CI OBLICZENIOWEJ Sortowanie szybkie n obiektów względem wszystkich k atrybutów:

Próbując wyznaczyć złożoność obliczeniową całego programu,

wyznaczamy maksimum z następujących po sobie operacji na tym samym poziomie, wyznaczając złożoność operacji złożonych poprzez przemnożenie przez złożoność wnętrza pętli.

Θ(k) · Θ(wnętrze pętli) Θ(1)

Θ(1) Θ(1) Θ(n · k) Θ(1) Θ(1) Θ(1)

Θ(1)

Θ(QuickSort) Θ(wnętrze pętli)

Θ(1) · Θ(k) Θ(1)

Θ(1) Θ(1) Θ(1)

MAX

(31)

PRZYKŁAD ANALIZY ZŁOŻONOŚ CI OBLICZENIOWEJ

Sortowanie szybkie dla wybranego klucza (atrybutu):

Θ(1)

Θ(1) Θ(1) Θ(1)

Θ(m) · Θ(wnętrze pętli) Θ(1) · Θ(wnętrze pętli)

Θ(1)

Θ(1) · Θ(wnętrze pętli) Θ(1)

Θ(1)

Θ(Swap) Θ(1) Θ(1) Θ(1)

Θ(QuickSort dla średnio m/2 obiektów) Wywołany dla m sortowanych obiektów:

Θ(1)

Θ(QuickSort dla średnio m/2 obiektów)

Tutaj pętle wewnętrzne są sprzężone z pętlą nadrzędną, tzn. nie iterują po innym atrybucie zależnym od m, lecz po tym samym co pętla główna, stąd nie przemnażamy złożoności obliczeniowych Θ(m) · Θ(m)

Ilość sortowanych obiektów m w każdym rekurencyjnym wywołaniu maleje o 2,

więc otrzymujemy: Θ(m) + 2 · Θ(m/2) = Θ(m), a oczekiwana ilość takich rekurencyjnych wywołań jest: Θ(log n) dla n obiektów, co daje złożoność sortowaniaΘ(n log n).

(32)

DRZEWO

Drzewo to spójna struktura nieliniowa składająca się z węzłów (wierzchołków) i krawędzi, w której istnieje dokładnie jedna ścieżka pomiędzy parą dowolnych dwóch węzłów.

Rodzic (poprzednik, przodek) to węzeł w drzewie, posiadający przynajmniej jedno dziecko

(następnika, potomka) będący o jedną krawędź bliżej korzenia w stosunku do węzła będącego jego dzieckiem (następnikiem, potomkiem).

Korzeń to węzeł główny w drzewie nie mający rodzica (poprzednika, przodka).

Liście to takie węzły w drzewie, które nie mają dzieci (następników, potomków).

Węzły wewnętrzne to węzły posiadające rodzica i przynajmniej jedno dziecko.

Ścieżka w drzewie to droga wyznaczona przez ciąg krawędzi pomiędzy połączonymi węzłami prowadząca pomiędzy dwoma dowolnymi węzłami.

Istnieje zawsze tylko dokładnie jedna ścieżka

w drzewie pomiędzy dowolnymi dwoma węzłami.

Długością ścieżki nazywamy ilość krawędzi tworzących ścieżkę w drzewie.

Poziom węzła w drzewie wyznaczony jest

równy długości ścieżki łączącej go z korzeniem.

Wysokość drzewa wyznaczona jest przez maksymalny poziom wszystkich jego węzłów.

Drzewo binarne to takie drzewo, którego każdy węzeł (rodzic) ma co najwyżej dwójkę dzieci.

Drzewo regularne to drzewo o określonej maksymalnej ilości dzieci dla każdego węzła.

Drzewo jest zupełne, gdy ma wszystkie poziomy z wyjątkiem ostatniego całkowicie zapełnione, a ostatni jest spójnie zapełniony od strony lewej.

KORZEŃ

LIŚCIE WĘZŁY WEWNĘTRZNE

wysokość

poziom

(33)

IMPLEMENTACJA DRZEWA W PYTHONIE

(34)

KOPIEC

Kopiec to drzewo binarne, w którego węzłach znajdują się elementy reprezentowanego multizbioru, które spełniają warunek kopca.

Warunek kopca mówi, iż jeśli węzeł y jest następnikiem węzła x, to element w węźle y jest nie większy niż element w węźle x.

Drzewo ma uporządkowanie kopcowe, gdy wszystkie jego elementy zachowują porządek kopcowy, tzn. elementy na ścieżkach w drzewie od korzenia do liścia uporządkowane są w sposób nierosnący.

Kopiec zupełny to zupełne drzewo binarne o uporządkowaniu kopcowym, czyli takie, którego wszystkie poziomy są wypełnione całkowicie za wyjątkiem co najwyżej ostatniego, który jest spójnie wypełniony od strony lewej.

Każde drzewo binarne można w łatwy sposób reprezentować w tablicy, a indeksy określone są przy pomocy

następującej zależności:

(35)

SPOSÓB DZIAŁANIA SORTOWANIA STOGOWEGO

Sortowanie stogowe (heap sort) polega na zbudowaniu kopca zupełnego, który jest drzewem binarnym spełniającym warunek kopca, czyli rodzic jest nie mniejszy niż dziecko, zaś zupełność oznacza,

iż wszystkie poziomy drzewa poza

ostatnim są w pełni wypełnione,

a ostatni spójnie od strony lewej:

(36)

SPOSÓB DZIAŁANIA SORTOWANIA STOGOWEGO

Sortowanie stogowe (heap sort):

(37)

ANALIZA ZŁOŻONOŚ CI OBLICZENIOWEJ

Sortowanie stogowe (heap sort):

Ile operacji elementarnych?

Jak wyznaczyć złożoność czasową?

Które części programu wykonywane są zawsze, które jednorazowo,

a które czasami?

Stabilne, w miejscu.

(38)

PRZYKŁAD ANALIZY ZŁOŻONOŚ CI OBLICZENIOWEJ Sortowanie stogowe (heap sort):

Θ(1)

Θ(1) Θ(1) Θ(1) Θ(1)

Θ(1) Θ(1)

Θ(1) Θ(1)

Θ(Swap) Θ(1)

Θ(Heapify)

Wywołany dla n sortowanych obiektów rekurencyjnie Θ(log n) razy.

Θ(1) Θ(1) Θ(n)

Θ(Heapify)

Θ(Heapify) Θ(Swap) Θ(n)

Złożoność stała razy ilość rekurencyjnych wywołań: Θ(log n).

(39)

SPOSÓB DZIAŁANIA SORTOWANIA PRZEZ SCALANIE

Sortowanie przez scalanie (merge sort) na koniec listy wynikowej merged

dołącza obiekty o rosnących wartościach kluczy uzyskanych z podzielonych

posortowanych list we wcześniejszych rekurencyjnych etapach sortowania:

(40)

ANALIZA ZŁOŻONOŚ CI OBLICZENIOWEJ

Sortowanie przez scalanie (merge sort):

Na ile kolejność wykonywania porównań może zmniejszyć ilość wykonywanych operacji elementarnych?

Stabilne, nie w miejscu.

(41)

PRZYKŁAD ANALIZY ZŁOŻONOŚ CI OBLICZENIOWEJ Sortowanie przez scalanie (merge sort)

Θ(1) Θ(1) Θ(1) Θ(2)

Θ(MergeSort m/2 obiektach) Θ(MergeSort m/2 obiektach) Θ(5) · Θ(wnętrze pętli)

Θ(2)

Θ(2)

Θ(2) · Θ(wnętrze pętli)

Θ(2) · Θ(wnętrze pętli) Θ(2)

Θ(2)

Θ(1)

Θ(2) Θ(2) Θ(3)

Sortowanie przez scalanie na koniec listy wynikowej merged dołącza obiekty o rosnących wartościach kluczy uzyskanych

a podzielonych posortowanych list we wcześniejszych rekurencyjnych etapach sortowania.

Funkcja MergeSort wykonywana jest w czasie stałym,

lecz jest rekurencyjnie wywoływana Θ(log n) razy.

(42)

ZŁOŻONOŚ Ć PAMIĘ CIOWA

Złożoność pamięciowa określa nam, ile dodatkowych jednostek pamięci potrzeba do wykonania algorytmu (rozwiązania zadania).

Złożoność pamięciową również określamy jako funkcję

względem rozmiaru danych.

Jeśli w algorytmie sortowania przez scalanie korzystamy z dodatkowej listy merged[]

do scalania posortowanych list, której wielkość jest zależna

od ilości sortowanych danych, wtedy złożoność pamięciowa rośnie i mówimy, że dane sortowanie nie odbywa się w miejscu, lecz

nie w miejscu (not in place).

(43)

WBUDOWANE SORTOWANIE W PYTHONIE

Sortowanie jest jedną ze standardowych operacji wykonywanych

na danych, więc w Pythonie istnieją funkcje sortujące dane w miejscu:

list.sort() sorted(list)

sorted(dictionary) sorted(tuple)

PRZYKŁADY:

list = [['4'], ['5'], ['1'], ['9'], ['6'], ['3'], ['6'], ['8'], ['3'], ['1'], ['2'], ['5'], ['7']]

list.sort() print(list)

zwróci: [['1'], ['1'], ['2'], ['3'], ['3'], ['4'], ['5'], ['5'], ['6'], ['6'], ['7'], ['8'], ['9']]

print sorted("Ten wykład odkrywa przed nami tajemnice Pythona, algorytmiki oraz programowania".split(), key=str.lower)

zwróci: ['algorytmiki', 'nami', 'odkrywa', 'oraz', 'programowania', 'przed', 'Pythona,', 'tajemnice', 'Ten', 'wyk\xc5\x82ad']

gdzie key=str.lower sprawia, iż małe i duże litery są traktowane jednakowo w trakcie sortowania.

(44)

Cytaty

Powiązane dokumenty

Jaka jest masa cząsteczkowa estru będącego produktem reakcji równomolowej mieszaniny nasyconego alifatycznego kwasu jednokarboksylowego i nasyconego alifatycznego

Możliwe jest opracowanie bardziej wydajnego – od obecnie stosowanych – algorytmu wyboru trybu pracy stacji abonenckiej w bezprzewodowej sieci lokalnej WLAN standardu IEEE

Odważnie stwierdza, że wskutek nasilenia się procesu napływu imigrantów może dojść do swego rodzaju kryzysu demokracji, która nie poradzi sobie z takim poziomem

Funkcja zespolona f określona w otwartym podzbiorze Ω płaszczyzny ma pier- wotną, wtedy i tylko wtedy gdy jej całka nie zależy od

Najprostszy i chyba najbardziej oczywisty sposób połączenia pomiędzy klientem i serwerem, to taki w którym klient uruchamia u siebie program do transmisji danych (ftp) i za jego

Podaj nazwę kategorii znaczeniowej rzeczowników pochodnych, do której należy rzeczownik czytelniczka i podkreśl jego formant, a następnie za pomocą tego samego formantu

4) elektorzy spośród studentów i doktorantów, którzy stanowią nie mniej niż 20% składu kolegium; liczbę studentów i doktorantów ustala się proporcjonalnie do liczebności obu

Aleksandra Kusińska, Mieszkanka DPS Dyrektor Domu Pomocy Społecznej w Strzelcach Opolskich oraz kierownik i mieszkańcy strzelec- kiego Domu składają serdeczne podziękowania