Podstawy Informatyki Wskaźniki w języku C i C++ dr inż. Piotr Kaczmarek Piotr.Kaczmarek@put.poznan.pl http://pk.cie.put.poznan.pl/wyklady.php
Organizacja pamięci Pamięć ma organizację bajtową, liniową każdy bajt posiada swój indywidualny adres, który jest liczbą całkowitą długość adresu wynosi obecnie 4 bajty (32bit) co pozwala zaadresować do 4Gb pamięci zmienne przechowują dane w kolejnych komórkach np. zmienna unsigned int a=0xffeeddcc char z='0'; zmienna a z komórka pamięci 0xcc 0xdd 0xee 0xff 0x32... adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04
Typy wskaźnikowe zmienna typu wskaźnikowego, służy do przechowywania adresów innych zmiennych zmienna typu wskaźnikowego, przechowuje wartość całkowitą, (4b), która jest adresem początku obszaru pamięci w którym zlokalizowano dane Przy deklaracji wskaźnika, zawsze trzeba określić zmiennej jakiego typu będzie on dotyczył (typ* nazwa;) np.: int* p1; float* p2; char* p2; zmienna a z p1 p2 komórka pamięci 0xcc 0xdd 0xee 0xff 0x32... adres zmienn. int Adres zm. float... adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04 FF05 FF06
Operacje na wskaźnikach Przypisanie adresu zmiennej do wskaźnika unsigned int a=0xffeeddcc; int* p1; p1= &a; //adres początku Zmiana wartości pod adresem wskazywanym unsigned int a=0xffeeddcc; int* p1; p1= &a; *p1=2; //zmienna a=2 Uwagi: &zmianna odwołanie się do adresu zmiennej *wskaźnik odwołanie się do wartości przechowywanej pod wskazanym adresem zmienna *p1 a z p1 p2 komórka pamięci 0xcc 0xdd 0xee 0xff 0x32... adres zmienn. int 1002 Adres zm. float... adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04 FF05 FF06 &a p1=&a
Operacje na wskaźnikach cd. Przypisanie adresu zmiennej do wskaźnika char z='0'; char* p2; p2= &z; *p2='a'; ile wynosi wartość: p2, &z, z,*p2 z 'a' *p2 'a' wartości p2 i &z są takie same (1009) Uwagi: &zmianna odwołanie się do adresu zmiennej *wskaźnik odwołanie się do wartości przechowywanej pod wskazanym adresem zmienna *p2 a z p1 p2 komórka pamięci 0xcc 0xdd 0xee 0xff 0x32... adres zmienn. int 1002 1009... adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04 FF05 FF06 &z p2=&z
Operacje na wskaźnikach cd.. inkrementacja char z='0'; char* p2; p2= &z; *p2++; //inkrem. wartości ile wynosi wartość: p2, &z, z,*p2 z '1', *p2 '1' wartości p2 i &z są takie same (1009) p2++; inkrementacja adresu p1++; ile wynosi wartość: p2, &z, z,*p2, p1 z '1', *p2 '1', &z bez zmian (1009) p2 100A (kolejny element char) p1 1006 (kolejny elemnet int) zmienna *p2 a z p1 p2 komórka pamięci 0xcc 0xdd 0xee 0xff 0x32 ('0')... adres zmienn. int 1002 1009... adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04 FF05 FF06 p1++; p2++;
Operacje arytmetyczne na wskaźnikach Operacje na wskaźnikach mają inny przebieg niż w zwykłej arytmetyce. Operacja dodawania lub odejmowania wsk+k wsk-k oznacza: przejdź o k elementów, stąd adres wynikowy zależy od rozmiaru wskazywanej zmiennej: int* p1=1002; char* p2=1009; p2=p2+1;// adres wynosi 100A (char - 1b) p1=p1+1;//adres wynosi 1006 (int - 4b) Uwaga: To o ile zmienia się wartość adresu determinowane jest przez typ wskaźnika. a z p1 p2 komórka pamięci 0xcc 0xdd 0xee 0xff 0x32 ('0')... adres zmienn. int 1002 adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04 FF05 1009 FF06... p1++; p2++;
Referencje Zmienne tworzona jako referencja przechowuje wartość w tym samym obszarze pamięci int a=1; int&b=a; // b jest referencją a int* p1, p2; p1=&a; p2=&b; ile wynoszą wartości: *p1,*p2, &a, &b, p1,p2,a,b p1,p2,&a,&b mają tą samą wartość (1002) p1,p2,a,b mają wartość 1; *p1, *p2 a b z p1 p2 komórka pamięci 0x01 0x00 0x00 0x00 0x32 ('0')... adres zmienn. int 1002 1002... adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04 FF05 FF06
Wykorzystanie wskaźników int a=2, b=3; int* p1, p2; int& c=a; a c 2 p1 p2 p1=&a; p2=&b; b 3 *p1=*p2; b++; p2=p1; *p2++; c++; ile wynosi wartość: a,b, c, *p1, *p2 p1, p2, &a, &b, &c
Wykorzystanie wskaźników int a=2, b=3; int* p1, p2; int& c=a; a c 3 *p1 p1 p2 p1=&a; p2=&b; b 3 *p2 *p1=*p2; b++; p2=p1; *p2++; c++; odwołanie się do wartości we wskazywanej komórce ile wynosi wartość: a,b, c, *p1, *p2 p1, p2, &a, &b, &c
Wykorzystanie wskaźników int a=2, b=3; int* p1, p2; int& c=a; a c 3 *p1 p1 p2 p1=&a; p2=&b; b 4 *p2 *p1=*p2; b++; p2=p1; *p2++; c++; ile wynosi wartość: a,b, c, *p1, *p2 p1, p2, &a, &b, &c
Wykorzystanie wskaźników int a=2, b=3; int* p1, p2; int& c=a; a c 3 *p1 p1 p2 p1=&a; p2=&b; b 4 *p2 *p1=*p2; b++; p2=p1; *p2++; c++; przypisanie do p1 adresu p2 ile wynosi wartość: a,b, c, *p1, *p2 p1, p2, &a, &b, &c
Wykorzystanie wskaźników int a=2, b=3; int* p1, p2; int& c=a; a c 5 *p1 p1 p2 p1=&a; p2=&b; b 4 *p2 *p1=*p2; b++; p2=p1; *p2++; c++; ile wynosi wartość: a,b, c, *p1, *p2 p1, p2, &a, &b, &c
Wykorzystanie wskaźników int a=2, b=3; int* p1, p2; int& c=a; a c 3 *p1 p1 p2 p1=&a; p2=&b; b 4 *p2 *p1=*p2; b++; p2=p1; *p2++; c++; przypisanie do p1 adresu p2
jest tożsama operacji: *(tab+i) = 0.5*i; Tablice i wskaźniki tablica jest ciągłym obszarem pamięci, w którym kolejno umieszczone są wartości elementów zmienna tab jest wskaźnikiem (typ float*) do pierwszego elementu tablicy stąd *tab, odwołuje się do wartości elementu 0 natomiast tab+i, jest adresem i-tego elementu tablicy, a *(tab+i) odwołuje się do wartości i-tego elementu tablicy. stąd operacja : tab[i] = 0.5*i; nr elementu tab *tab *(tab+1) 0 1 2 0.0 0.5 1.0 tab+4 float tab[5]; for(i=0;i<5;i++) tab[i]=0.5*i; 3 4 1.5 2.0 adres k k+6 k+12 k+18 k+24
Wskaźniki jako argument funkcji Zastosowanie wskaźników pozwala na tworzenie funkcji przekazujących na zewnątrz więcej niż jedną wartość Przekazanie argumentów przez wartość void Zamien(int a,int b) { int t=a; a=b; b=t; } Przekazanie argumentów przez wskaźnik void Zamien(int *a,int *b) { int t=*a; *a=*b; *b=t; } Przekazanie argumentów przez referencję void Zamien(int& a,int& b) { int t=a; a=b; b=t; } int main { int x=2,y=3; Zamien(x,y); } int main { int x=2,y=3; Zamien(&x,&y); } int main { int x=2,y=3; Zamien(x,y); } x,y nie zmieniają wartości x,y zmieniają swoje wartości
Wskaźniki jako argument funkcji Przekazując argument przez wartość (void Zamien(int a, int b)) przy każdym wywołaniu funkcji tworzone są lokalne zmienne a,b i przypisywana jest im wartość argumentów Czas wywołania funkcji oraz ilość pamięci rezerwowanej dla argumentów jest zależny od typu argumentu (musi zostać skopiowana pewna ilość bajtów z zmiennej przekazanej jako argument do zmiennej lokalnej) Przekazując argument jako wskaźnik (void Zamien(int *a, int *b)) przy każdym wywołaniu funkcji tworzone są lokalnie zmienne wskaźnikowe a i b i przypisywana jest im wartość adresów argumentów wejściowych (po 4 bajty na adres) Czas wywołania i ilość pamięci zajmowane przez argumenty funkcji jest więc niezależny od typu argumentów Wszystkie operacje modyfikujące wartość wskazywaną przez a lub b będą miały swoje odzwierciedlenie na zewnątrz funkcji Przekazanie argumentu przez referencję, działa tak jak przekazanie argumentu przez wskaźnik, z tym że nie ma etapu kopiowania adresu (metoda ta jest najszybsza)
Ochrona argumentów przed zmianami Przekazanie argumentów przez wskaźnik lub referencję pozwala na zwiększenie szybkości wywoływania funkcji (szczególnie w przypadku argumentów o dużym rozmiarze) Może to jednak powodować że nastąpi zmiana argumentów przekazanych do funkcji (co nie zawsze jest pożądane) Zapewnić że argumenty nie zostaną zmodyfikowane przekazując je jako referencję do stałej void SzybkaFunkcja(const int& X) { int a=x; //ok X=1;//błąd zmienna tylko do odczytu } void SzybkaFunkcja(const int* X) { int a=*x; //ok *X=1;//błąd zmienna tylko do odczytu }
Dynamiczny przydział pamięci Pamięć dla zmiennych i tablic deklarowanych w sposób pokazywany poprzednio jest przydzielona w chwili gdy program napotyka deklarację zmiennej: { float a; int tab[100]; char znak;... } Rozmiar zmiennej (np. tablicy) musi być znany już w chwili uruchamiania programu. Konieczne jest więc deklarowanie tablic większych niże maksymalny rozmiar wprowadzony przez użytkownika. Zmienne automatyczne istnieją od momentu deklaracji do końca zasięgu ich widoczności. Program sam przydziela i zwalnia pamięć
Dynamiczny przydział pamięci cd.. Pamięć może być również przydzielana ręcznie przez użytkownika: przez zastosowanie operatora new. Składnia ma postać: typ* wsk = new typ; float *wsk; wsk=new float; *wsk = 6.0; wsk=new float; przydzielony blok nie ma stowarzyszonej żadnej zmiennej automatycznej i istnieje do czasu aż użytkownik uwolni przydzieloną pamięć. Składnia ma postać: delete wskaźnik; delete wsk; wsk; wsk komórka pamięci... adres zmienn. int 1002... adres 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 100A 100B FF01 FF02 FF03 FF04 FF05 FF06
Przydział pamięci dla tablic w programie można uzależnić ilość przydzielanej pamięci od aktualnego zapotrzebowania. rezerwacja pamięci dla tablicy o N elementach ma postać: typ* wsk=new typ[n]; float *ptab; int N; cout<< Ile elementów chcesz wprowadzić? ; cin>>n; ptab=new float[n]; for(int i=0;i<n;i++) cin>>ptab[i]; //lub cin>>*(ptab+i); Zwolnienie pamięci dla tablicy odbywa się za pomocą operatora delete[] delete[] ptab; Uwaga: delete ptab; //zwolnienie pamięci elementu 0 tablicy!!!
Weryfikacji prawidłowego przydziału pamięci Sprawdzenie poprawności przydzielenia pamięci jest istotne szczególnie przy tworzeniu tablic o dużych rozmiarach. przy stosowaniu operatora new, jeśli można zarezerwować żądany przez użytkownika blok pamięci zwracany jest jego adres, w innym przypadku, operator new nie zwraca żadnej wartości Stąd można zastosować następującą procedurę sprawdzania: int N; float *ptab = null; cin>>n; ptab=new float[n]; if(ptab==null) { cout<< blad przydzialu ; exit(-1); }... // pamięć przydzielona OK
Przydział pamięci cd. Można stworzyć funkcje dedykowaną do przydziału pamięci, której zadaniem będzie sprawdzenie poprawności przydziału float* PrzydzielPamiec(int N) { float *ptab = null; cin>>n; ptab=new float[n]; if(ptab==null) { cout<< blad przydzialu ; exit(-1); } return ptab; } Użycie (wywołanie) będzie miało postać: int n; cin>>n; float* tab=przydzielpamiec(n);...
Tablice 2D W języku C można zdefiniować tablicę dwuwymiarową: int n=2,m=4; //Deklaracja tablicy 2x4 int tab[2][4]; //zapis wartości do pierwszego elementu Tab[0][0]=1; //wypelnenie wszystkich elementów zerami for(int i=0;i<n;i++) for(int j=0;j<m;j++) tab[i][j]=0;
Wskaźniki do funkcji Dla funkcji: typ nazwa(argumenty){} np.: int funkcja(float wartosc){} Można stworzyć wskaźnik: typ (*nazwa)(argumenty) np.: int (*wsk_funkcji)(float); Taki że wsk_funkcji=&funkcja; Wywołąnie funkcji może mieć postać: (*wsk_funkcji)(2.0);
Dynamiczne tablice 2D W języku C nie ma komendy automatycznie przydzielającej pamięć dla tablic 2D, możemy natomiast można stworzyć wiele tablic 1D, a wskaźniki do nich umieścić w tablicy, której elementy są typu typ* Int** wsk_2d //tworzenie 2D tablicy NxM typu int Int** tab=new int*[n]; Int* wsk1 Int* wsk2. Int* wskn int.... int int.... int. int.... int for(int i=0;i<n;i++) tab[i]=new int[m]; //odczyt/zapis danych // z dynamicznej tablicy 2D Tab[0][0]=0; //usuwanie tablicy 2D NxM typu int for(int i=0;i<n;i++) delete[] tab[i];//usuwa wiersze delete[] tab; //usuwa kolumny
Grupowanie zmiennych Często pewien obiekt jest opisywany nie przez jedną zmienną, ale przez pewien zestaw w celu przechowania danych osoby w programie należy zdefiniować kilka zmiennych: char imie_nazwisko[25]; int wiek; char plec; aby dodaś kolejną osobę należy stworzyć kolejne zmienne char imie_nazwisko1[25]; int wiek1; char plec1;
Struktury Struktury są typami umożliwiającymi grupowanie zmiennych. struct snazwa { typ pole1; typ pole 2; typ pole 3; }; Struktura służąca do przechowywania danych osoby: struct sosoba { char Imie_Nazwisko[25]; int wiek; char plec; }; Uwaga: definicja typu strukturalnego powinna znajdować się w pliku nagłówkowym
Struktury - wykorzystanie Można zadeklarować zmienną typu strukturalnego sosoba student; Do odwołania się do konkretnego pola struktury stosuje się operator '.' cout<< Wprowadz imię i nazwisko ; cin.getline(student.imie_nazwisko,25); cout<< Podaj wiek ; cin>>student.wiek; cout<< Podaj plec ; cin>>student.plec;
Tablice struktur - wykorzystanie W celu przechowania wielu osób można zadeklarować tablicę: sosoba studenci[50]; Do odwołania się do konkretnego pola struktury stosuje się operator '.' for(int i=0; i<50; i++) { cout<< Wprowadz imię i nazwisko ; cin.getline(studenci[i].imie_nazwisko,25); cout<< Podaj wiek ; cin>>studenci[i].wiek; cout<< Podaj plec ; cin>>studenci[i].plec; }
Wskaźniki do struktur Wskaźników do struktur używa się tak jak innych wskaźników do typów wbudowanych sosoba student1,student2; sosoba* wsk; wsk = &student1; Odwołanie do pól struktury (selektor '.') *wsk.wiek=16; cin.getline(*wsk.imie_nazwisko,25); Student1 Student2 Imie_Nazwisko wiek plec Imie_Nazwisko wiek plec wsk Bardziej poprawne odwołanie do pól struktury, dla wskaźników (selektror ->) wsk->wiek=16; cin.getline(wsk->imie_nazwisko,25);
Funkcje operujące na strukturach Zmienna typu strukturalnego może być przekazywana do funkcji tak jak zmienna wbudowana void Wyswietl(sOsoba* O) { cout<<o->imie_nazwisko<<endl << wiek: <<O->Wiek<<endl << plec: <<O->plec<<endl; } Przekazanie adresu zmiennej pozwala przyspieszyć czas wykonywania funkcji (pola nie są kopiowane) sosoba student;... Wyswietl(&student);
Przydział pamięci do struktur Przydział pamięci dla pojedynczej zmiennej strukturalnej, wygląda analogicznie jak dla typów wbudowanych sosoba* wsk; wsk = new sosoba;... delete wsk; Przydział pamięci dla tablic struktur sosoba* posoby; int N; cout<< ile osób chcesz wprowadzic ; cin>>n; posoby = new sosoba[n];... delete[] posoby;
Wady stosowania tablic struktur Dodawanie/usuwanie elementów stworzyć nową tablicę o właściwym rozmiarze, przekopiować wszystkie elementy ze starej tablicy do nowej (dodając/usuwając pewne elementy) usunąć starą tablicę Aby przestawić 2 elementy (np. przy sortowaniu) w tablicy należy zapamiętać pierwszy element w elemencie tymczasowym (skopiować wszystkie jego pola) przypisać wartości pół 2 elementu do 1 elementu przypisać wartość elementu tymczasowego do 2 elementu kopiowanie 2 elementów: sosoba o1,o2;... strcpy(o1.imienazwisko, o2.imienazwisko); o1.wiek=o2.wiek; o1.plec=o2.plec; Uwaga: struktury mogą zajmować dużo miejsca w pamięci, stąd operacja kopiowania elementów jest czasochłonna i może pochłaniać wiele zasobów
Dynamiczne struktury danych STOS wstawianie, usuwanie i dostęp do składników są możliwe tylko w jednym końcu zwanym wierzchołkiem stosu KOLEJKA dołączanie składników jest możliwe tylko w jednym końcu, a usuwanie tylko w drugim końcu LISTA dla każdego składnika (poza pierwszym i ostatnim) jest określony jeden składnik poprzedni i jeden składnik następny lub tylko składnik poprzedni lub następny, w dowolnym miejscu można dołączać lub usuwać składnik DRZEWO dla każdego składnika (poza pierwszym) jest określony jeden składnik poprzedni i dla każdego składnika (poza ostatnim) jest określonych n (n 2) składników następnych GRAF struktury definiowane przez dwa zbiory: zbiór wierzchołków i zbiór krawędzi określający powiązania pomiędzy poszczególnymi wierzchołkami.
Stos (LIFO) Początek -wstawianie -usuwanie -odczyt Element osoba P Element osoba P Element osoba P
Kolejka (FIFO) Początek -wstawianie Element osoba P Element osoba P Element osoba P Koniec -usuwanie -odczyt
Listy dynamiczne dwukierunkowe Tablice wymagały przydzielenia ciągłego obszaru pamięci, stąd każda modyfikacja wiązała się z ingerencją w zawartość całej tablicy Zastosowanie struktur dynamicznych tj. Listy pozwala ominąć ten problem. Rozpatrzmy strukturę umożliwiającą zaimplementowanie wytworzenie listy dwukierunkowej: struct sosoba { char Imie_Nazwisko[25]; int wiek; char plec; sosoba* N;//nastepny element sosoba* P;//poprzedni element }; Pola N i P przechowują adresy następnego i poprzedniego elementu listy. Wartość NULL tego pola oznacza, że nie istnieje następny (aktualny element jest ostatni) lub poprzedni element (aktualny element jest pierwszy.
Dodawanie elementów do listy Załóżmy że zmienna sosoba *root przechowuje adres pierwszego elementu listy sosoba *root; root = new sosoba; root->n=null;//ostatni element root->p=null; //pierwszy element root null osoba N P null dodawanie kolejnego elementu d listy sosoba* nowy=new sosoba nowy->p=root; root->n=nowy; nowy->n=null; //ostatni element root null osoba N P osoba nowy N P null
Poszukiwanie ostatniego elementu na liście Ostatnim elementem na liście jest ten, którego pole N ma wartość null. Poniższa funkcja poszukuje ostatniego elementu na liście i zwraca jego adres. sosoba* Ostatni(sOsoba* root) { sosoba* aktualny=root; while(aktualny->n!= null) { aktualny = aktualny->n; } return aktualny; } root null osoba N P osoba N P osoba N P osoba N P null
Dodawanie elementu na końcu listy Aby dodać element na końcu listy należy odszukać element ostatni przydzielić pamięć dla nowego elementu zaktualizować pola N i P nowego i ostatniego elementu //dodawanie elementu sosoba *koniec=ostatni(root); sosoba *nowy=new sosoba; koniec->n=nowy; nowy->p=koniec; nowy->n=null; root null osoba N P osoba N P osoba N P koniec osoba N P null osoba nowy N P null
Przeszukiwanie listy Wyświetlić osoby, których wiek wynosi 28 lat; sosoba* aktualny=root; int wiek =28; while(aktualny!= null) // przechodzi do pierwszego do ostatniego elementu { if(aktualny->wiek==wiek) Wyswietl(aktualny)l aktualny = aktualny->n; } root null osoba N P osoba N P osoba N P osoba N P osoba N P null
Przestawianie 2 elementów Aby przestawić 2 sąsiednie elementy należy określić adresy tych elementów zmienić wartości pól elementów poprzedzających i następujących sosoba* E1;E2,Po,Na... E2=E1->N; Po=E1->P; Na=E2->N; osoba E2 N P Po->N=E2; E2->P=Po; Na->P=E1; E1->N=Na root null osoba Po N P osoba E1 N P E1->P=E2; E2->N=E1; osoba Na N P
Usuwanie elementów z listy Aby usunąć element o adresie E1 Określić adresy elementów poprzedniego i następnego Zaktualizować pole N poprzednika tak by wskazywało adres elementu następnego Zaktualizować pole P el. następnego tak by wskazywało adres elementu poprzedniego uwolnić pamięć dla obiektu E1 sosoba* E1;Po,Na... Po=E1->P; Na=E1->N; Po->N=Na; Na->P=Po; delete E1; root null osoba N P osoba Po N P osoba E1 N P osoba Na N P osoba N P null
Listy jednokierunkowe Element strukturalny struct sosoba { char Imie_Nazwisko[25]; int wiek; char plec; sosoba* N;//nastepny element }; osoba N osoba nowy N null Gdzie wskaźnik N przechowuje adresy następnego i poprzedniego elementu listy. Wartość NULL tego pola oznacza, że nie istnieje następny (aktualny element jest ostatni) lub poprzedni element (aktualny element jest pierwszy. Operacje przeszukiwania listy, oraz dodawanie elementu do końca listy przebiegają tak jak dla list dwukierunkowych. Natomiast operacja usuwania elementu z listy czy też przestawiania są trudniejsze.
Usuwanie elementów z listy jednokierunkowej Aby usunąć element o adresie E1 Określić adresy elementu poprzedzającego element do usunięcia (spełniającego warunek E->N==E1 Dokonać przepisania E->N=E1->N Usunąć element E1 (delete E1;) sosoba* E1;Po,Na... //znajdź E taki że E->N==E1 E->N=E1->N; Delete E1; root osoba N osoba Po N osoba E1 N osoba Na N osoba N null
Złożoność obliczeniowa Typy złożoności: Złożoność algorytmów (ilość operacji) Złożoność czasowa Złożoność pamięciowa Sposób reprezentacji: zazwyczaj złożoność określa się jako funkcję rozmiaru problemu (np.. ilości danych, wielkości zbioru).
Złożoność obliczeniowa - funkcja Typy złożoności: O(n) złożoność liniowa np. T(n)=a 1 n+a 0 złożoność będzie funkcją liniową ilości danych O(n2 ) złożoność kwadratowa np. T(n)=a 2 n 2 +a 1 n+a 0 O(log(n)) złożoność logarytmiczna NP - (non deterministic polynomial time problems)
Złożoność dla sortowania stabilnego sortowanie bąbelkowe (ang. bubblesort) O(n2 ) sortowanie przez wstawianie (ang. insertion sort) O(n 2 ) sortowanie przez scalanie (ang. merge sort) O(nlog(n)), wymaga O(n) dodatkowej pamięci sortowanie przez zliczanie (ang. counting sort lub count sort) O(n + k), wymaga O(n + k) dodatkowej pamięci sortowanie kubełkowe (ang. bucket sort) O(n), wymaga O(k) dodatkowej pamięci sortowanie pozycyjne (ang. radix sort) O(d(n + k)), gdzie k to wielkość domeny cyfr, a d szerokość kluczy w cyfrach. Wymaga O(n + k) dodatkowej pamięci sortowanie biblioteczne (ang. library sort) O(nlog n), pesymistyczny O(n 2 )
Złożoność dla sortowania niestabilnego sortowanie przez wybieranie (ang. selection sort) O(n2 ) może być stabilne po odpowiednich zmianach sortowanie Shella (ang. shellsort) złożoność nieznana; sortowanie grzebieniowe (ang. combsort) złożoność nieznana; sortowanie szybkie (ang. quicksort) Θ(nlog n), pesymistyczny O(n 2 ); sortowanie introspektywne (ang. introspective sort lub introsort) O(nlog n); sortowanie przez kopcowanie (ang. heapsort) O(nlog n);
Sortowanie kubełkowe Podziel zadany przedział liczb na n podprzedziałów (kubełków) o równej długości. Przypisz liczby z sortowanej tablicy do odpowiednich kubełków. Sortuj liczby w niepustych kubełkach. Wypisz po kolei zawartość niepustych kubełków.
Struktura drzewiasta pojęcia podstawowe Korzeń drzewa wierzchołek, do którego nie dochodzi żadna krawędź Następnik węzła p każdy węzeł r dla którego istnieje krawędź ( p, r ) Wierzchołek wewnętrzny każdy wierzchołek który ma następnik Liść wierzchołek nie posiadający następnika Poprzednik węzła p węzeł r z którego prowadzi krawędź ( r, p )
Struktura drzewiasta pojęcia podstawowe (cd..) Potomkami węzła p są wszystkie następniki węzła p jak i następniki tych następników itd. a do liści włącznie. Przodkiem węzła p jest jego poprzednik, ale także poprzednik tego poprzednika itd. a do korzenia włącznie. Synem węzła p jest każdy jego bezpośredni następnik. Ojcem węzła p jest jego bezpośredni poprzednik. Głębokość (lub poziom) wierzchołka w drzewie to jego odległość od korzenia Wysokość wierzchołka to maksymalna długość drogi od danego wierzchołka do liścia.
Struktury drzewiaste pojęcia podstawowe (cd..) Drzewo uporządkowane drzewo w którym gałęzie każdego węzła są uporządkowane Stopień węzła p liczba bezpośrednich potomków węzła p. Stopień drzewa maksymalny stopień z wszystkich węzłów drzewa Długość drogi do węzła x liczba krawędzi (gałęzi) przez które należy przejść od korzenia do węzła x Długość drogi drzewa (długość drogi wewnętrznej) suma dróg wszystkich jego składowych.
Drzewo binarne Drzewo binarne drzewo uporządkowane o stopniu 2) Jest to skończony zbiór elementów (węzłów), który jest albo pusty, albo zawiera korzeń (węzeł) z dwoma rozłącznymi binarnymi drzewami zwanymi lewym i prawym poddrzewem.
Reprezentacja działań arytmetycznych za pomocą DB y=(a+b/c)*(d-c*f)
Binarne drzewo poszukiwań Binarne drzewo poszukiwań (ang. Binary Search Tree (BST)) dynamiczna struktura danych będąca drzewem binarnym, w którym lewe poddrzewo każdego węzła zawiera wyłącznie elementy o kluczach nie większych niż klucz węzła a prawe poddrzewo zawiera wyłącznie elementy o kluczach nie mniejszych niż klucz węzła. Dla pełnego drzewa BST o n węzłach pesymistyczny koszt każdej z podstawowych operacji wynosi O(log n). (operacje wykonywane są wzdłuż drzewa). w skrajnym przypadku, gdy drzewo składa się z jednej gałęzi koszt ten wzrasta do O(n).
Struktury drzewiaste Element strukturalny struct slisc { char napis[25]; Int licznik; slisc* L;//lewy lisc slisc* R;//prawy lisc }; Lisc R L root osoba Lisc R L osoba Lisc R L null osoba Lisc R L null null null Gdzie wskaźnik R przechowuje adres prawego a L lewego elementu węzła null
Sortowanie z wykorzystaniem drzew Załóżmy że w drzewie chcemy przechowywać zbiór słów znajdujących się w pewnym tekście. Umieszczanie słów będzie przebiegało wg następującego algorytmu 1) Pobierz słowo 2) Porównaj słowo ze słowem w aktualnym węźle 3) Jeśli słowo jest: - mniejsze (w sensie alfabetycznym) przejdź do lewego węzła - większe (w sensie alfabetycznym) przejdź do prawego węzła - równe, zwiększ licznik węzła i wróć do pkt 1 4) Jeśli kolejny węzeł nie istnieje dodaj go, umieść w nim słowo i wróć do pkt. 1 null Lisc R L osoba Lisc R L osoba Lisc R L null null osoba Lisc R L null null
Sortowanie z wykorzystaniem drzew Najmniejszy element znajduje się w końcowym liściu lewym Największy element znajduje się w końcowym liściu prawym W każdym elemencie znajduje się ilość wystąpień danego słowa Funkcja wyświetlająca posortowane drzewo void Wyświetl(sLisc* lisc) { if(lisc->l!=null) Wyswietl(lisc->L); cout<<lisc->napis<<, <<lisc->licznik; if(lisc->r!=null) Wyswietl(lisc->R); }
Tablice z haszowaniem (hash table) Tablice z haszowaniem są stosowane, gdy z pewnego dużego zbioru danych w danej chwili musimy znać stosunkowo jego niewielki podzbiór. Elementy z tego podzbioru są zapisywane w tablicy, której indeksy dla danego elementu oblicza specjalna funkcja hashująca.
Kopce Kopiec binarny (czasem używa się też określenia sterta) (ang. binary heap) - tablicowa struktura danych reprezentująca drzewo binarne, którego wszystkie poziomy z wyjątkiem ostatniego muszą być pełne. W przypadku, gdy ostatni poziom drzewa nie jest pełny, liści ułożone są od lewej do prawej strony drzewa. index 1 2 3 4 5 6 7 8 9 10 klucz 20 16 8 10 15 2 5 7 6 3
Tworzenie kopców Na pierwszej pozycji znajduje się korzeń. Indeksy lewego i prawego syna węzła i to odpowiedni 2i oraz 2i+1. Indeks rodzica węzła i niebędącego korzeniem to floor(i/2) Wyróżniamy dwa rodzaje kopców binarnych: kopce binarne typu max w których wartość danego węzła niebędącego korzeniem jest zawsze mniejsza niż wartość jego rodzica oraz kopce binarne typu min w których wartość danego węzła niebędącego korzeniem jest zawsze większa niż wartość jego rodzica
Dodawanie elementu do kopca Dla kopca n-elementowego typu max wstaw wierzchołek na pozycję n+1 zamieniaj pozycjami z rodzicem (przepychaj w górę) aż do przywrócenia warunku kopca (czyli tak długo, aż klucz rodzica jest większy niż k, lub element dotrze na pozycję 1)
Usuwanie wierzchołka kopca usuń wierzchołek ze szczytu kopca przestaw ostatni wierzchołek z pozycji n+1 na szczyt kopca; niech k oznacza jego klucz spychaj przestawiony wierzchołek w dół, zamieniając pozycjami z większymi z dzieci, aż do przywrócenia warunku kopca (czyli aż dzieci będą mniejsze od k lub element dotrze na spód kopca)
Sortowanie kopcowe O obl (nlogn), O pam (1) Stwórz kopiec umieszczając w nim kolejne elementy tablicy począwszy od pierwszego elementu nieposortowanej tablicy kopiec Reszta nieposortowanej tablicy Z kopca o rozmiarze n usuń element nr 1 (element maksymalny) i zapisz go na końcu tablicy do początku kopca dodaj element n-1, powtarzaj aż do wyczerpania elementów kopca kopiec Posortowana tablica