POLITECHNIKA WARSZAWSKA Instytut Automatyki i Robotyki ZASADY PROGRAMOWANIA KOMPUTERÓW ZAP zima 2015 Język programowania: Środowisko programistyczne: C/C++ Qt Wykład 5 : Algorytmy sortowania. Struktury i rekordy. Uwagi przed sprawdzianem nr 1.
SKS System Komunikacji ze Studentami 2 Wszyscy już powinni mieć dostęp do SKS (login i hasło otrzymane w mailu od administratora systemu) tu jest dostępny online podręcznik multimedialny Programowanie C++, prezentacje do wykładów, tematy zadań dodatkowych i sprawdzianów, aktualne ogłoszenia i inne pliki. Administratorem SKS jest dr inż. Krzysztof Kukiełka (pok. 340). Należy się do niego zwrócić mailowo: k.kukielka@mchtr.pw.edu.pl podając swój adres i nr albumu w sytuacji, gdy do tej pory nie otrzymaliśmy loginu i hasła do SKS. Osoby powtarzające przedmiot koniecznie powinny się zwrócić mailowo do dr Kukiełki z prośbą o dopisanie do aktualnej grupy, w której powtarzają ćwiczenia (podając informację, w której grupie byli w ub. roku) inaczej nie będą mieli dostępu do tegorocznej witryny SKS, nowych plików, ogłoszeń i wyników ze sprawdzianów. Od następnego tygodnia wszystkie prezentacje do wykładów i inne pliki będą umieszczane wyłącznie na witrynie SKS.
Algorytmy prostego sortowania 3 7 3 4 6 9 5 0 1... N-1 Algorytm 1 sortowanie przez wybieranie ( selection sort ) Wybieramy najmniejszy element z nieposortowanych i zamieniamy z pierwszym nieposortowanym aż do posortowania całości. Algorytm 2 sortowanie przez wstawianie ( insertion sort ) Bierzemy pierwszy element z nieposortowanych i wstawiamy go w odpowiednie miejsce w części posortowanej aż do posortowania całości. Algorytm 3 sortowanie przez zamianę ( bubble sort ) Zamieniamy ze sobą, jeśli trzeba, dwóch sąsiadów najpierw w całej tablicy, a potem za każdym razem w części mniejszej o 1 element.
Algorytm 1 sortowanie przez wybieranie ( selection sort ) 4 for (i = 0; i< N-1; i++) { i_min = i; for (j = i+1; j < N; j++) if (a[ j ] < a[ i_min ]) i_min = j; temp = a[ i ]; a[ i ] = a[ i_min ]; a[ i_min ] = temp; } // 0..i-1 posortowane // i..n-1 - nieposortowane Algorytm: z nieposortowanych elementów wybieramy element najmniejszy (zapamiętujemy jego położenie) zamieniamy go z pierwszym elementem z ciągu nieposortowanych elementów - odtąd będzie należał do posortowanych Zatem: W pierwszym kroku element najmniejszy w tablicy zamieniamy z pierwszym Ostatni krok to ewentualna zamiana ostatniego elementu z przedostatnim -3-1 0 2 4 5 9 8 10 6 7 0 1 2...... N-1-3 -1 0 32 4 5 6 8 10 9 7 posortowane nieposortowane
Algorytm 2 sortowanie przez wstawianie ( insertion sort ) 5 for (i = 0; i<n; i++ ) { j = i; temp = a[j]; while (j>0 && temp < a[j-1]) { a[ j ] = a[ j-1 ] ; j=j-1 ; } a[ j ] = temp; } // 0..i-1 - posortowane // i..n-1 - nieposortowane bierzemy pierwszy element z części nieposortowanej porównujemy go kolejno z elementami posortowanymi, idąc na lewo dopóki jest mniejszy od kolejnego elementu z grupy posortowanych, przesuwamy ten element w prawo jeśli nie jest już mniejszy - wstawiamy go na zwolnione miejsce (albo na początek, jeśli inaczej się nie da) 0 3 4 6 9 12 5 8 10 2 7 0 1 2...... N-1 0 3 4 5 6 9 12 8 10 2 7 posortowane nieposortowane
Algorytm 3 sortowanie przez zamianę (tzw. bąbelkowe) 6 for (i = N-1; i>0; i--) { } Pierwszy przebieg : zamieniamy miejscami dwa pierwsze elementy, jeśli pierwszy z nich jest większy od drugiego taką samą zamianę sąsiednich elementów wykonujemy idąc po kolei wzdłuż całej tablicy, od 0 do N-1 (for j) w efekcie tego największy element znajdzie się na końcu tablicy - jako a[n-1] Kolejne przebiegi: for (j = 0; j < i; j++) if (a[ j ] > a[ j+1 ]) { } temp = a[ j ]; a[ j ] = a[ j+1]; a[ j+1] = temp; 5 8 10 2 7 5 8 2 7 10 takie same zamiany sąsiadów wykonujemy w obszarze od 0 do i, gdzie i maleje od N-1 aż do 1 (for i) - kolejny największy element znajdzie się w a[i] bool byla_zamiana=true; i=n-1; while (byla_zamiana) { } była_zamiana=false; for (j = 0; j< i; j++) if (a[ j ] > a[ j+1 ]) { } i-- ; temp = a[ j ]; a[ j ] = a[ j+1]; a[ j+1] = temp; była_zamiana=true; Wersja bardziej efektywna - przejście przez tablicę i zamianę sąsiadów powtarzamy tylko wtedy, jeśli w poprzednim kroku była choć jedna taka zamiana.
Porównanie algorytmów prostego sortowania 7 Porównując metody sortowania analizuje się liczbę porównań elementów i liczbę ich przesunięć w tablicy. Należy też uwzględnić przypadki najlepsze (tablica posortowana lub prawie posortowana) i najgorsze (posortowana w kolejności odwrotnej). Sortowanie przez zamianę (nawet w wersji ulepszonej) jest gorsze zarówno od metody sortowania przez wstawianie, jak i od metody sortowania przez wybieranie. Algorytm sortowania przez zamianę jest bardzo wrażliwy na konfiguracje danych i tylko w przypadku elementów prawie posortowanych ma przewagę nad pozostałymi metodami. Algorytm prostego wybierania jest przeważnie lepszy od prostego wstawiania (które wymaga przesuwania elementów w tablicy) i tylko w przypadku elementów prawie posortowanych proste wstawianie jest od niego nieco szybsze. Jednakże wszystkie zaprezentowane algorytmy prostego sortowania mają złożoność obliczeniową O(N 2 ), co można czytać jako proporcjonalne do N 2, czyli w przypadku b. dużych wartości N algorytmy te są mało efektywne. UWAGA: zarówno 10N 2 +100N, jak i N 2 /30 są rzędu O(N 2 ). Metody szybkie, o mniejszej złożoności (zwykle N lg 2 N), konieczne do zastosowania w przypadku wielkich wartości N, wymagają zaawansowanych algorytmów (np. quicksort) lub zaawansowanych struktur danych (np. drzew binarnych). Dla małych i średnich wartości N algorytmy prostego sortowania są natomiast całkowicie wystarczające, a wybierając któryś z nich należy przede wszystkim kierować się czytelnością kodu i łatwością jego użycia.
Tablice i rekordy 8 Są to złożone struktury danych, które pozwalają za pomocą jednej nazwy zapamiętać wiele wartości (elementów). w tablicy (zmiennej typu tablicowego) - wszystkie elementy muszą być tego samego typu, rozróżniane są za pomocą indeksów. w rekordzie (zmiennej typu struktura) - elementy (pola) rekordu mogą być różnych typów, rozróżniane są za pomocą nazw. a 0... i... N-1 2 7 1-3 12 a[0]... a[i]... a[n-1] indeksy elementów nazwy pól typy pól tak odwołujemy się do elementów tablicy o nazwie a...a tak odwołujemy się do pól rekordu o nazwie osoba1 osoba1.imie osoba1.nazwisko...... osoba1.zarobki... imie string nazwisko string plec char dzien int miesiac int rok int zarobki double jest_w_pracy bool
Struktury i rekordy 9 REKORDY pozwalają za pomocą jednej nazwy przechowywać wiele elementów różnych typów. Żeby zdefiniować pojedyncze rekordy, trzeba najpierw zdefiniować ich strukturę, czy nowy typ danych. Definicja struktury - czyli typu strukturalnego (inaczej: rekordowego) struct nazwa struktury { typ pola nazwa pola,..., nazwa pola; // nazwy pól i ich typ... typ pola nazwa pola,..., nazwa pola; } ; // uwaga - średnik na końcu!!! Definicja pojedynczych rekordów - czyli zmiennych typu strukturalnego nazwa struktury nazwa rekordu,..., nazwa rekordu; typ danych nazwy zmiennych tego typu Desygnator ( oznacznik ) pola rekordu nazwa rekordu. nazwa pola lub tu są kropki nazwa rekordu. nazwa pola. nazwa pola itd.
Nazwy typów a nazwy zmiennych 10 Definiując struktury, czyli nowe typy danych, trzeba bardzo uważać, by nie pomylić nazwy struktury (czyli nowo zdefiniowanego typu) z nazwą zmiennej tego typu. Aby uniknąć pomyłek, warto nazwy typów strukturalnych rozpoczynać od wielkiej litery S (jak Struktura) albo T (jak Typ) albo od innej wielkiej litery. zalecany styl using namespace std; struct SOsoba { definicje struktur najlepiej umieszczać string imie, nazwisko; poza funkcją main (jako def. zewnętrzne) char plec; zdefiniowaliśmy nowy typ danych - int dzien, miesiac, rok; nową strukturę o nazwie SOsoba; double zarobki; // pole zarobki jest typu double każde z pól struktury ma podany typ bool jest_w_pracy; }; int main( ) { SOsoba osoba1, osoba2, szef; // tak definiujemy rekordy - zmienne typu SOsoba cin >> szef.zarobki; a tak odwołujemy się do pól tych rekordów szef.imie = "Jan"; }... cin >> SOsoba.zarobki; SOsoba.plec= M ; Tak nie wolno! SOsoba to nie zmienna, tylko typ danych.
Przykład struktury zagnieżdżonej 11 nazwa typu danych nazwy pól // najpierw trzeba zdefiniować strukturę SData: struct SData { int dzien, miesiac, rok; }; // a dopiero teraz można zdefiniować strukturę SOsoba: struct SOsoba { string imie, nazwisko; char plec; SData data; // tu jest pole o nazwie data, typu SData double zarobki; bool jest_w_pracy; }; SOsoba pracownik; pracownik.data.rok = 2014; // pracownik jest zmienną typu SOsoba // tak odwołujemy się do pól struktur zagnieżdżonych
Operacje wykonywane na rekordach (i tablicach) 12 REKORDY należy wczytywać i drukować odwołując się do kolejnych pól rekordu, nie wolno tych operacji wykonywać na całych rekordach: cout << szef.nazwisko << szef.imie << szef.zarobki // drukujemy różne dane szefa cout << szef; // nie wolno drukować szefa w całości! Przypomnienie: ta sama zasada odnosi się do TABLIC: wczytujemy i drukujemy kolejne elementy tablic, a nie tablice w całości: const N=20; int i, a[n], b[n]; for (i=0; i<n; i++) cin >> a[i]; tablice wczytujemy element po elemencie cin >> b; b=a; // nie wolno wczytywać tablic w całości // i nie wolno kopiować całych tablic (podstawiać jednych pod drugie) UWAGA: inicjowanie rekordów w sposób analogiczny do inicjowania tablic jest niewskazane, gdyż może być różnie interpretowane przez kompilatory.
Tablice rekordów 13 REKORDY bardzo często są elementami tablic: using namespace std; struct Tpunkt { double x,y; // definicja struktury o nazwie Tpunkt }; int main ( ) { const N=10; Tpunkt p[n], q[n]; // definicja tablic p i q zawierających N elementów typu Tpunkt for (int i=0; i<n; i++) cin >> p[i].x >> p[i].y; tak wczytujemy dane do tablicy p for (int i=0; i<n; i++) cin >> p[i]; for (int i=0; i<n; i++) q[i] = p[i]; Tpunkt schowek = p[0]; p[0] = p[n-1]; p[n-1] = schowek; tak nie wolno - nie da się wczytać rekordów w całości ale można wykonywać przypisania całych rekordów, o ile ich polami nie są tablice - na to należy uważać! Taka możliwość przypisania nie jest więc zbyt bezpieczna.
Struktury rekordów a klasy obiektów 14 Programowanie strukturalne: struktura struktura - typ danych (inaczej: typ strukturalny, typ rekordowy, wzorzec struktury, szablon) rekord 1 rekord 2... rekord n zmienne danego typu (zmienne strukturalne, bywają w uproszczeniu, niezbyt poprawnie, nazywane strukturami) Programowanie obiektowe: klasa obiekt 1 obiekt 2... obiekt n klasa - typ danych (inaczej: typ obiektowy, szablon obiektów) zmienne danego typu (zmienne obiektowe, instancje klasy, egzemplarze klasy)
Kilka uwag przed sprawdzianem nr 1 15 Założenia podstawowe Należy bardzo dokładnie czytać treść zadań. Zadania rozwiązujemy za pomocą konstrukcji, które były już przedmiotem zajęć laboratoryjnych, innych nie używamy. Ocena zadania nie zależy od efektywności jego rozwiązania, wystarczy, że jest ono poprawne (wyjątkiem jest nieumiejętność poruszania się po przekątnych w tablicy w jednej pętli). Nie robić tego, co nie podano w zadaniu, np. nie drukować tablic, jeśli nie było takiego polecenia. Jeśli w zadaniu nie jest podane, jakiego typu są wczytywane dane liczbowe, to można przyjąć dowolny typ danych (double lub int). Wszelkie inne założenia konieczne do rozwiązania zadania, a nie podane w treści, mogą być przyjęte dowolnie (należy wtedy dopisać komentarz na ten temat). UWAGA: Prace muszą być czytelne!!!
Kilka uwag Uwagi edycyjne 16 Uwagi edycyjne (przypomnienie): Do porównywania wyrażeń używać operatora ==, nie zaś = Nie wstawiać z rozpędu średnika tuż za nawiasem zamykającym warunek w instrukcji if ( ) lub za nawiasem kończącym definicję instrukcji for ( ), bo to oznacza, że ma się wykonywać instrukcja pusta. Uwagi edycyjne (nowe!): Nie zapominać o średniku za klamrą zamykającą definicję struktury. Nie kończyć komentarza (rozpoczynającego się od //) znakiem backslash \, bo to oznacza, że następna linia będzie doklejona do tego komentarza i też traktowana jako komentarz zaś edytor wcale nie pokaże, że jest to linia nieaktywna - i to jest najbardziej niebezpieczne!
Kilka uwag Pętle for i while 17 W pętlach for, while i do-while wykonuje się zawsze tylko jedna instrukcja, więc jeśli ma się wykonywać ich kilka, trzeba je ująć w klamry { }. To samo dotyczy instrukcji if. Na ogół warunek w pętli while polega na sprawdzeniu jakiejś zmiennej, i ta zmienna musi być wczytana jednorazowo przed pętlą while (lub inaczej przed wejściem do pętli określona). Nie wolno więc napisać np. while (x>0) {...}, jeśli wcześniej nie poznaliśmy wartości zmiennej x. Jeśli wczytywany ciąg ma się kończyć jakimiś dwiema kolejnymi liczbami etc., to nie wolno danych wczytywać w pętli parami i porównywać ich z tymi dwiema końcowymi; zmienne należy wczytywać w pętli po jednej, uprzednio zapamiętując wartość poprzednią, i w każdym kroku sprawdzać warunek dotyczący dwu kolejnych wartości. Jeśli w pętli mamy sprawdzać wartość bieżącą i poprzednią, to zanim wczytamy nową wartość bieżącą, trzeba uaktualnić wartość poprzednią. Odwrotna kolejność jest zupełnie bez sensu i świadczy o próbie nauczenia się programowania na pamięć (co nie jest wykonalne!).
Kilka uwag Maksima-minima i zadania z tablicami 18 Wartość początkowa maksimum powinna należeć do zbioru wartości, wśród których szukamy maksimum - albo być od tych wartości na pewno mniejsza. Podstawienie max=0 jest więc dozwolone tylko w bardzo szczególnych przypadkach (jeśli w zbiorze przeszukiwanym jest co najmniej jedna liczba >=0). Analogiczna zasada dotyczy szukania minimum. Jeśli chcemy zlokalizować wartość maksymalną/minimalną w tablicy, trzeba nie tylko zapamiętywać w pętli indeksy aktualnego maksimum/minimum, ale także przed petlą ustawić wartości początkowe tych indeksów. Tablice indeksowane są od 0, zatem ostatni element tablicy N-elementowej ma indeks N-1. Po przekątnych tablicy i po pojedynczych wierszach i kolumnach poruszamy się z użyciem pętli pojedynczej. Do poruszania się w pozostałych przypadkach - używamy pętli podwójnej (pętli w pętli) Ustawianie wartości początkowych sum, maksimów, indeksów etc. w tablicy należy wykonywać zależnie od zadania - często dla każdego wiersza/kolumny od nowa (a więc wewnątrz pętli zewnętrznej).