Procedury służące do dynamicznego przydzielania i zwalniania pamięci, zmienne typów wskaźnikowych są wykorzystywane do programowania dynamicznych struktur danych, którym pamięć jest przydzielana i zwalniana w trakcie wykonywania programu. Ze względu na organizację oraz sposoby dołączania, wstawiania i usuwania składników można wyróżnić między innymi następujące metody: kolejki, w których można dołączyć tylko w jednym końcu, a usunąć tylko w drugim końcu, listy, które charakteryzują się tym, że 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, przy czym w dowolnym miejscu takiej struktury można dołączyć nowy składnik lub usunąć składnik istniejący. stosy, w których wstawianie, usuwanie i dostęp są możliwe tylko w jednym końcu zwanym wierzchołkiem stosu Dynamiczne struktury danych to nic innego, jak rozszerzenie możliwości oferowanych przez C poprzez zastosowanie struktur, których definicje zawierają odwołania do samych siebie. Zdefiniowanie takiej struktury możliwe jest poprzez zastosowanie wskaźników. Oto przykład dynamicznego obiektu: int *p; wskaźnik na obiekt typu int - w momencie uruchomienia program istnie tylko wskaźnik, ale nie ma obiektu, na który mógłby on pokazywać, obiekt ten można utworzyć, aby pracować z nim, a potem zniszczyć, aby nie zajmował pamięci Zmienna p jest wskaźnikiem mającym możliwość wskazywania na zmienne typu całkowitego. Można używać go do wskazywania na zmienne istniejące już w programie, za pomocą przypisania adresu zmiennej do wskaźnika: int zmienna; p = &zmienna; ale jest to rzadko stosowane. Prawdziwa moc dawana przez stosowanie wskaźników leży w tym, że zmienne można za ich pomocą tworzyć: int *p; wskaźnik na obiekt typu int - na razie nie zainicjalizowany żadną konkretną wartością p = new int; tworzenie nowego obiektu (new) typu int, i przypisanie adresu tegoż do zmiennej wskaźnikowej p. Teraz p zawiera wskaźnik do liczby typu int i odwołanie *p jest jedynym sposobem dotarcia do tej zmiennej. Powołanie do życia obiektu int wymaga przydzielenia mu pamięci w ilości wymaganej dla typu int (2 lub 4 bajty). Utworzenie zmiennej typu int w momencie działania programu mimo pozornej prostoty okazuje się przydatne wtedy gdy potrzebna jest taka zmienna, a nie chcemy deklarować jej na początku działania programu. Oczywiście w praktyce działania na wskaźnikach nie dotyczą prostych zmiennych int, ale całych struktur lub tablic. Listy Struktura zdefiniowana w sposób następujący: struct LISTA { struct LISTA *next; } nazywana jest ogólnie listą jednokierunkową. Do definicji pól dowolnego elementu takiej struktury używana jest definicja pojedynczego jej elementu. To znaczy, że obiekt LISTA zawiera pola, które definiowane są tak samo, jak LISTA, rekurencyjnie. Nie ma jednak obawy o pozorną nieskończoność takiej struktury. Definicja pola next oznacza, że jest to wskaźnik, a nie sama struktura. Zatem, w momencie definicji, wskaźnik ten istnieje, ale nie istnieje żaden obiekt przez niego wskazywany. Dopiero w przyszłości będzie on pokazywał na takie same elementy typu LISTA, wewnątrz jakiego sam się znajduje. W praktyce do struktury tego typu wprowadza się jeszcze jakieś pola przechowujące informacje, np. (przykład z prawdziwego, działającego programu): struct TNode { TNode *next; int nrdl; } Właściwością tej struktury jest zdolność do przechowywania liczb całkowitych (w polu o nazwie nrdl). Inna lista jednokierunkowa: struct TListNode { TListNode *nastepny; void *dane; } posiada zdolność do przechowywania wskaźników na obiekty dowolnego typu. Można w ten sposób przechowywać wskaźniki do liczb, napisów, tablic, funkcji i wszystkiego, co posiada adres. Oczywiście obsługa tak przechowywanych adresów wymaga potem umiejętnej oceny, czym jest akurat przechowywany wskaźnik, ale to już problemy innej natury. Gdy do pojedynczego elementu typu na przykład LISTA dopiszemy następny, wstawiając w polu next adres nowego elementu, otrzymamy "połączenie" pomiędzy elementami - z pierwszego można trafić do drugiego za pomocą wskaźnika. Odwrotnie już nie - drugi element nie zawiera żadnych informacji o tym, czy posiada poprzednika, czy nie. Dlatego lista jest jednokierunkowa - stojąc na dowolnym elemencie można mieć dostęp tylko do elementów następnych. Graficzna reprezentacja listy jest prosta i czytelna gdy przedstawi się ją za pomocą strzałek i węzłów. Dla struktury typu LISTA wygląda to tak: Graficzna reprezentacja prostej, jednokierunkowej listy. Ostatni element to wskaźnik pusty - czyli next mający wartość NULL. Jak widać, tworzony jest łańcuch elementów, który nie ma z góry określonej swojej długości. Długość może być zmienna, i jest to jedna z najważniejszych zalet struktur tego typu. Na rysunku pokazano tylko cztery elementy, ale może ich być o wiele więcej. Co więcej, ostatni wskaźnik next, na rysunku mając wartość NULL (czyli pusty - nigdzie nie pokazujący) może pokazywać na pierwszy element, tworząc tak zwaną listę cykliczną. Graficzną reprezentację listy cyklicznej można zobaczyć poniżej, tym razem dla struktury zdefiniowanej jako TNode: Lista cykliczna z numerami elementów w polu nrdl Numeracja pól listy jest wprowadzona po to, aby można było wykryć pierwszy element listy. Nie jest to konieczne, jeżeli nie zależy nam na kolejności, albo nie liczymy elementów. Podczas przetwarzania listy cyklicznej można łatwo sprawdzić, w którym jesteśmy miejscu. Lista taka - gdzie jednym z pól jest numer pozycji, nazywa się listą numerowaną, lub listą indeksowaną. Łatwo można sobie wyobrazić bardziej złożone struktury danych podobne tylko z definicji do listy jednokierunkowej - np. listę dwukierunkową, trzykierunkową lub więcej-kierunkową (N-kierunkową). Graficzne reprezentacje takich dziwnych struktur bardzo się komplikują. Powstaje też problem, na co tak naprawdę mają wskazywać poszczególne wskaźniki. Lista dwukierunkowa powinna zawierać dwa wskaźniki obok innych pól informacyjnych - wskazujące na poprzedni element oraz na następny. Taka lista jest strukturą uniwersalną, i do niektórych zastosowań w programowaniu jest wprost idealna. Przykład poniżej: struct T2List { T2List *n, *p; .... /* dane */ } Elementy takiej listy połączone są dwukierunkowo, co widać na poniższym rysunku: Lista dwukierunkowa. Pole p wskazuje na element poprzedni, pole n na następny. Wzdłuż całej listy dwukierunkowej można swobodnie poruszać się za pomocą wskaźników w obie strony. Dlatego, posiadając dowolny element listy dwukierunkowej, mamy dostęp do wszystkich pozostałych. Nie było to tak do końca możliwe w przypadku list jednokierunkowych. Ponieważ lista w ogólnym przypadku zaczyna się jakimś elementem i jakimś elementem się kończy (z wyjątkiem przypadków cyklicznych), początek listy przyjęto nazywać głową listy (head), zaś koniec - ogonem (tail). Dowolny element jest węzłem lub nodem (node). Lista dwukierunkowa to tak jakby dwie listy jednokierunkowe zrealizowane wewnątrz jednej struktury. Charakterystyczne jest to, że wewnątrz jednego elementu wskaźniki pokazują na element poprzedni oraz na element następny. Jeżeli określimy inaczej, na co mają one wskazywać, np. oba na elementy następne, otrzymamy zamiast listy inną strukturę - drzewo. Listy mogą być zależnie od sposobu obsługi traktowane inaczej. Znane są np. kolejki, czyli lista jednokierunkowa obsługiwana tak specyficznie, że pierwszy wstawiony element jest także pierwszym dającym się z listy odczytać (tzw. kolejka FIFO - First in, first out - dokładne odwzorowanie normalnej kolejki przed sklepem mięsnym w czasach kryzysu). Stos jest implementacją listy jednokierunkowej, która pozwana na wstawianie elementu do struktury, a potem na odzyskiwanie ich w odwrotnej kolejności - czyli pierwszy wstawiony jest ostatnim wyjętym, dokładnie tak, jakby ktoś rzucał dane na kupę, lub stos. Jest to również wersja kolejki LIFO (last in first out - ostatni wchodzi, pierwszy wychodzi, pasuje to trochę do pewnego rodzaju studentów). Drzewa Drzewa to równie ciekawe struktury danych jak listy, ponieważ obok innych specyficznych cech, oferują niezwykle mały czas dostępu do swoich elementów (zakładając, że są zoptymalizowane, lub wyważone). Struktura węzła najprostszego drzewa jest identyczna jak w przypadku listy dwukierunkowej: struct BTree { BTree *lewy, *prawy; /* .... dane ... */ } Natomiast sens nadany wskaźnikom jest inny, i w efekcie wymusza to zupełnie inną budowę całej struktury: Drzewo binarne - reprezentacja struktury BTree Pierwszy element drzewa to jakby praprzodek wszystkich pozostałych (analogicznie do drzewa genealogicznego) lub korzeń (analogia do drzew), następnie - w drzewie istnieją elementy mające swojego przodka i swoich potomków (normalne postacie w drzewie genealogicznym) - zwane są one gałęziami. Elementy leżące najniżej w hierarchii pionowej drzewa to liście lub synowie. Zatem widać, że każdy syn ma ojca, z wyjątkiem praprzodka, który nie ma ojca. Co więcej, ojciec jest przodkiem syna, a syn jest przodkiem swoich synów. Drzewa rosną w informatyce w dół, korzeń jest na górze :) Oczywiście zależy to tylko od sposobu narysowania drzewa. Wysokość drzewa określa się przez ilość jego poziomów. Drzewo na rysunku poniżej ma 4 poziomy. Stopień drzewa to ilość gałęzi, jaka można wyrastać z korzenia. Drzewo, które posiada tylko dwie gałęzie wyrastające z dowolnego elementu, nazywa się drzewem binarnym. Struktura drzewa binarnego o głębokości 4 Całkowita ilość elementów, jaka będzie w drzewie o podanym stopniu i wysokości daje się łatwo policzyć, jako prosta zależność wykładnicza. Drzewa wyższych stopni mają oczywiście więcej gałęzi. Poza drzewami różniącymi się stopniem istnieją jeszcze inne ich odmiany. Drzewa zlewowe to takie, w których odwrócono kierunek wskaźników (mają wiele korzeni, a tylko jeden liść lub niewiele liści). Odwrotnym rodzajem są drzewa wylewowe (rozpływowe). Drzewa balansowane (AVL - średniej długości, ważone) to przykłady drzew, które "dbają" o swoją minimalną głębokość. Jeszcze innym rodzajem drzew są sterty, przydatne bardzo w pewnych implementacjach sortowania. Istnieją także drzewa czarno-czerwone (bez skojarzeń) oraz zupełnie oryginalne drzewa mutanty, jak np. drzewa zwrotne, drzewa łączone z listami, itp. Grafy Można zbudować strukturę, która będzie miała zamiast określonej ilości wskaźników (jak lista lub drzewo), po prostu listę wskaźników do następnych elementów. Można także założyć, że nie wszystkie z tych wskaźników będą musiały na coś konkretnego pokazywać. Otrzymamy wtedy najbardziej skomplikowaną strukturę z możliwych, w informatyce nazywaną grafem. Ogólnie rzecz biorąc, graf definiuje się jako zbiór elementów oraz zbiór połączeń pomiędzy nimi. Dlatego struktura, dla której możliwa jest implementacja grafu, zawiera po prostu wskaźniki do dwóch list - na których przechowywane są połączenia pomiędzy elementami oraz odpowiednio same elementy. Grafy mogą posiadać rozmaite "kształty". Szczególnymi przypadkami grafów są np. listy i drzewa. Zależnie od właściwości grafów, określa się je jako zorientowane, niezorientowane, acykliczne, cykliczne, itp. Połączenia pomiędzy elementami nazywa się krawędziami. Krawędź jest ukierunkowana, jeżeli możliwy jest ruch po niej tylko w jedną stronę, natomiast gdy możliwy jest w obie strony - nie ma określonego kierunku lub łączność jest bezkierunkowa. Zagadnieniami związanymi z tymi przedziwnymi i skomplikowanymi strukturami danych zajmuje się odrębna dziedzina wiedzy matematycznej - teoria grafów, pełna straszliwej i pięknej matematyki, niedostępna jak dzika kobieta w czarnej skórze nabijanej ostrymi ćwiekami i wymagająca zaangażowania intelektualnego graniczącego z przepaleniem niektórych obwodów :) DYNAMICZNE STRUKTURY DANYCH Dynamiczne struktury danych to proste i złożone struktury danych, którym pamięć jest przydzielana i zwalniana w trakcie wykonywania programu. 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 (n2) 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.