Programowanie komputerów Jacek Lach Zakład Oprogramowania Instytut Informatyki Politechnika Śląska
Plan Dynamiczne struktury danych Lista jednokierunkowa Lista dwukierunkowa Lista podwieszana Graf Drzewa BST
Dynamiczne struktury danych Struktura dynamiczna struktura tworzona w trakcie działania programu Najczęstsze zastosowanie tworzenie abstrakcyjnych typów/struktur danych (szczególnie przydatne w zastosowaniach algorytmicznych)
Dynamiczne struktury danych Przykłady struktur: lista jednokierunkowa lista dwukierunkowa drzewo graf UWAGA: każdą z w/w struktur można zrealizować stosując struktury statyczne (np. tablice)
Lista jednokierunkowa Składa się z pewnej liczby węzłów (elementów), z których każdy zawiera pewien zestaw danych adres następnego elementu Dodatkowo należy zapamiętać adres pierwszego węzła (tzw. głowa) Ostatni element wyróżnia się poprzez nadanie adresowi określonej wartości, którą można odpowiednio zinterpretować, tzn. inaczej niż adres (w języku C: NULL)
Lista jednokierunkowa Głowa (NULL)
Definicja elementu Elementem listy jest najczęściej zmienna typu struktura #define MAX_ROZMIAR 50; /* definicja elementu listy */ struct elem_tag { char wyraz[max_rozmiar+1]; struct elem_tag *nast; ; /* korzystamy ze zmiennych wskaźnikowych glowa jeśli jest zmienna statyczna, jest inicjowana wartością 0, czyli NULL */ struct elem_tag *glowa;
Wyświetlanie listy Styl Pascala: void wypisz ( char *Akomunikat ) { struct elem_tag *p; printf("\n%s \n", Akomunikat); /* wersja odpowiadająca programowi w "pascalu" */ p = glowa; while (p) { printf ( "kolejny wyraz to: %s\n", p->wyraz ); p = p->nast;
Wyświetlanie listy Styl C: void wypisz (char *Akomunikat) { struct elem_tag *p; printf("\n%s \n", Akomunikat); for(p=glowa;p;p=p->nast) printf ( "kolejny wyraz to: %s\n", p->wyraz );
Metoda fifo glowa ostatni nowy
Metoda fifo glowa ostatni nowy
Metoda fifo /* wczytanie wyrazow i utworzenie listy zgodnie z zasadą nowy na końcu listy */ void wczytaj_fifo ( void ) { char buf[max_rozmiar+1]; struct elem_tag *nowy, *ostatni; printf ( "wpisz [%s] aby zakonczyc\n", KONIEC ); // jesli wczytywanym wyrazem nie jest 'koniec' while (wczytaj_jeden_wyraz(buf)) { /* próba przydzielenia pamięci */ nowy = ( struct elem_tag* )malloc( sizeof(struct elem_tag) ); /* prawdopodobnie brak pamięci */ if (!nowy) exit(1);
Metoda fifo /* kopiowanie wczytanego wyrazu */ strcpy( nowy->wyraz, buf ); nowy->nast=null; /* pierwszy element listy - głowa wskazuje NULL */ if (!glowa) glowa=nowy; else ostatni->nast=nowy; /* nowo utworzony element listy jest jej ostatnim elementem */ ostatni=nowy;
Metoda lifo glowa ostatni nowy
Metoda lifo glowa nowy ostatni
Metoda lifo /* wczytanie wyrazow i utworzenie listy zgodnie z zasadą nowy na początku listy */ void wczytaj_lifo ( void ) { char buf[max_rozmiar+1]; struct elem_tag *poprzednia_glowa; printf ( "wpisz [%s] aby zakonczyc\n", KONIEC ); /* jesli wczytywanym wyrazem nie jest 'koniec' */
Metoda lifo while (wczytaj_jeden_wyraz(buf)) { /* zapamietuje poprzednia glowe */ poprzednia_glowa=glowa; /* próba przydzielenia pamięci */ glowa = ( struct elem_tag* ) malloc( sizeof(struct elem_tag) ); /* prawdopodobnie brak pamięci */ if (!glowa) exit(1); /* wczytanego wyrazu */ strcpy( glowa->wyraz, buf ); /* lacze z poprzednia glowa */ glowa->nast=poprzednia_glowa;
Usuwanie listy z pamięci /* usunięcie listy z pamięci */ void kasuj ( void ) { struct elem_tag *p; /* zaczynamy od pierwszego elementu */ while (glowa) { /* zapamiętanie adresu następnego elementu */ p = glowa->nast; /* zwolnienie pamięci */ free (glowa); /* glowa wskazuje na kolejny element */ glowa = p;
Lista jednokierunkowa Wstawianie elementu na pewnej pozycji : wartości atrybutów numer pozycji na której należy wstawić element
Wstawianie Głowa (NULL)
Wstawianie Metoda 1 Przesuń się na element o danym numerze Wstaw element do listy Uzupełnij połączenia
Wstawianie Głowa (NULL)
Dodawanie v1 void dodaj_po_elem_v1(struct elem_tag *q, char *wyraz) { struct elem_tag *nowy; /* próba przydzielenia pamięci kolejnemu elementowi */ nowy = ( struct elem_tag* ) malloc( sizeof(struct elem_tag) ); /* prawdopodobnie brak pamięci */ if (!nowy) exit(1); /* przepisanie wczytanego wyrazu */ strcpy( nowy->wyraz, wyraz );
Dodawanie v1 /* połączenie listy */ /* na koncu listy zostanie skopiowany NULL */ nowy->nast = q->nast; q->nast = nowy; void dodaj_na_poz_v1(int poz, char *wyraz) { /* wyszukaj wskaźnik na kolejną pozycję */ for(p = glowa ;p && poz; p=p->nast, poz--); if (p) dodaj_po_elem_v1(p, wyraz);
Dodawanie v1 Wady Trudno dodać element na pozycji 0 By dodać element na pozycji 0 należałoby modyfikować wskaźnik na pierwszy element, co skomplikowałoby dodatkowo kod Rozwiązanie Wykorzystać wskaźniki na wskaźniki
Dodawanie Głowa (NULL)
Dodawanie Głowa Głowa struct elem_tag * Głowa struct elem_tag **
Dodawanie v2 void dodaj_po_v2(struct elem_tag **pp, char *wyraz){ /* próba przydzielenia pamięci kolejnemu elementowi */ p = ( struct elem_tag* )malloc( sizeof(struct elem_tag) ); if (!p) exit(1); /* prawdopodobnie brak pamięci */ /*przepisanie wczytanego wyrazu */ strcpy( p->wyraz, wyraz ); // p jest wskaznikiem na nowoutworzony rekord, zawierajacy już slowo /* połączenie listy */ p->nast = *pp; // pp jest adresem glowy, *pp jest glowa, a zatem wskaznikem // na pierwszy element listy; operacja powoduje, ze kopiujemy glowe // do pola nast *pp = p; // *pp jest glowa, kopiuje wskazania na nowoutworzony // element do glowy
Dodawanie v2 void dodaj_na_pozycji_v2(int poz, char *wyraz) { struct elem_tag **pp; /* wyszukaj wskaźnik na kolejną pozycję */ for (pp = &glowa ;*pp && poz; pp=&((*pp)->nast), poz--); // pp jest adresem glowy w PAO // jeżeli glowa istniala if (pp) dodaj_po_v2(pp, wyraz);
Lista dwukierunkowa Elementem listy dwukierunkowej jest najczęściej zmienna typu struktura #define MAX_STR 50; struct elem_tag { /* dane */ int pole1; char [MAX_STR+1] pole2; double pole3; /* adres następnego elementu listy */ struct elem_tag *nast, *poprz; glowa_1, glowa_2
Lista dwukierunkowa Składowe listy wielokierunkowej : wartości atrybutów Wskaźnik na następny element Wskaźnik na poprzedni element Dwa wskaźniki na elementy (pierwszy i ostatni)
Lista dwukierunkowa Głowa_2 Głowa_1 (NULL) (NULL)
Wstawianie elementu Głowa_2 Głowa_1 (NULL) (NULL)
Tworzenie listy void dodaj_do_listy(char *buf) { /* próba przydzielenia pamięci elementowi */ struct elem_tag *p= (struct elem_tag* ) malloc( sizeof(struct elem_tag) ); if (!p) exit(1); /* prawdopodobnie brak pamięci */ strcpy( p->wyraz, buf ); /*przepisanie wczytanego wyrazu */ if (!glowa_1) /* jesli lista nie istniała */ { glowa_1=glowa_2=p; p->nast=p->poprz=0; else /* jeśli był choć jeden element listy */ { glowa_2->nast = p; p->poprz = glowa_2; glowa_2 = p;
Metoda wstawiania Mamy trzy przypadki Dodawanie elementu na pozycji 0 Dodawanie elementu na pozycji n, gdzie 0<=n<liczba elementów listy Dodawanie elementu jako ostatniego elementu listy Pominiemy jawne rozróżnianie tych warunków (ćwiczenie) i przejdziemy do bardziej wyrafinowanego rozwiązania
Wstawianie na danej pozycji void dodaj_na_pozycji_v2(int poz, char *wyraz) { struct elem_tag **pp; for(pp = &glowa_1 ;*pp && poz; pp=&((*pp)->nast), poz--); if (pp) dodaj_po_v2(pp, wyraz);
Wstawianie na danej pozycji void dodaj_po_v2(struct elem_tag **q, char *wyraz) { struct elem_tag *p = (struct elem_tag* ) malloc( sizeof(struct elem_tag) ); if (!p) exit(1); /* prawdopodobnie brak pamięci */ strcpy( p->wyraz, wyraz ); p->nast = *q; if(*q) { p->poprz=(*q)->poprz; (*q)->poprz = p; else { p->poprz=glowa_2; glowa_2 = p; *q = p;
Wstawianie na danej pozycji inny zapis void dodaj_po_compact(struct elem_tag **q, char *wyraz) { /* n - adres elementu, który ma stać się następny */ /* p - adres pola poprz do zmodyfikowania, adres pola poprz następnego rekordu lub głowa */ struct elem_tag *n = *q, **p = n? &(n->poprz):&glowa_2; /* q jest adresem (głowy lub pola nast) do zmodyfikowania */ /* wartość wskazania *q została zapamiętana w n, tak więc if(!(*q = ( struct elem_tag* )malloc( sizeof(struct elem_tag)))) exit(1); strcpy( (*q)->wyraz, wyraz ); (*q)->nast = n; /* połączenie z następnym */ (*q)->poprz = *p; /* pole poprz kopiujemy z rekordu, który stał się następny */ *p = *q; /* pole poprz z następnego rekordu na nasz */
Wstawianie na danej pozycji inny zapis Uwaga: Jak widać na poprzednim przykładzie, pewne konstrukcje języka C mogą prowadzić do zmniejszania się czytelności kodu. Należy raczej unikać takiego stylu programowania, chyba że istnieją ku temu obiektywne powody (wielkość kodu, szybkość działania itp.)
Struktury danych implementowane jako listy Stos (Stack) Last In First Out (LIFO) Operacja push Operacja pop Kolejka (Queue) First In First Out (FIFO) Operacja put Operacja get
Listy podwieszane: definicje Mamy to do czynienia z sytuacją, gdy jedna z list o organizacji liniowej, zawiera wskaźniki do innych list, niejako podwieszonych pod listą główną Elementem listy dwukierunkowej jest najczęściej zmienna typu struktura
Listy podwieszane struct elem_item { /* jakieś dane */ /* adres elementu */ struct elem_item *dol; struct elem_tag { /* jakieś dane */ /* następny element */ struct elem_tag * nast; /* lista podwieszana */ struct elem_item *dol; glowa;
Listy podwieszane Głowa_1 (NULL) D D (NULL) D D D D (NULL) D (NULL)
Szczególny przypadek: graf Głowa_1 o wierzchołku 1 o wierzchołku 2 o wierzchołku 3 (NULL) 1 2 3 D D (NULL) D D D D (NULL) D (NULL)
Zalety reprezentacji Mała objętość w porównaniu do metod opartych o tablice, takich jak macierz sąsiedztwa, tablice wierzchołków wchodzących, wychodzących itp. Wady zmniejszenie czytelności algorytmów grafowych Dany wierzchołek grafu G=(N,E) można znaleźć w O( N ) krokach Wierzchołek można dodać w O(1) krokach Krawędź można znaleźć, dodać lub usunąć w O( N + E ) krokach (znalezienie wierzchołka, a następnie znalezienie, dodanie lub usunięcie krawędzi wychodzących) Reprezentacja zalecana dla grafów dla których N 2 >> E ; w przypadku grafów o dużej liczbie krawędzi w stosunku do kwadratu liczby wierzchołków lepiej zastosować struktury statyczne
Drzewa binarne korzeń Data Data Data Data liście Data Data Data
Drzewo BST (Binary Search Tree) Każdy element zawiera klucz Klucze są takiej postaci, że można je uporządkować liniowo Wszystkie elementy w lewym poddrzewie mają klucz mniejszy niż klucz rodzica Wszystkie elementy w prawym poddrzewie mają klucze większe niż klucz poddrzewa
BST przykład lewe poddrzewo; wszystkie klucze lewego poddrzewa są mniejsze niż klucz korzenia 4 korzeń 6 12 15 liść 17 19 22 9 20 25 prawe poddrzewo; wszystkie klucze prawego poddrzewa są większe niż klucz korzenia
BST Operacje: Poszukiwanie elementu Poszukiwanie poprzednika Poszukiwanie następcy Poszukiwanie minimum Poszukiwanie maksimum Wstawianie elementów Usuwanie elementów Wypisywanie elementów
Rekord drzewa BST typedef struct bstel { int data; struct bstel *left; struct bstel *right; bn;
Poszukiwanie elementu int find(int key, struct bstel *node) { if (!node) return 0; /* nie znaleziono */ if (key == node->data) return 1; /* znaleziono */ if (key < node->data) return find(key, node->left); else return find(key, node->right);
Wstawianie elementu int insert(bn *p, bn **root) { if (*root && (*root)->data == p->data) return 1; /* już jest w drzewie */ if (!*root) { /* drzewo bez elementów */ *root = p; return 0; else if (p->data > (*root)->data) insert(p,&((*root)->right)); else insert(p,&((*root)->left)); return 0;
Wstawianie elementu int ins_el(int key, bn **root) { bn *p; p = (bn*)malloc(sizeof(bn)); if (!p) return -1; /* brak pamięci */ p->data = key; p->left = p->right = NULL; return insert(p,root);
Poszukiwanie minimum struct bstel* min(struct bstel *node) { if (!node) return NULL; /* błąd */ while (node->left) node = node->left; return node;
Poszukiwanie maksimum struct bstel* max(struct bstel *node) { if (!node) return NULL; /* błąd */ while (node->right) node = node->right; return node;
Znajdowanie poprzednika i następnika Wartość succ(17)=19 Jak znaleźć taki węzeł: jeśli węzeł ma prawe poddrzewo, zwróć minimalny wierzchołek w prawym poddrzewie jeśli węzeł nie ma prawego poddrzewa, znajdź wierzchołek, do którego lewego poddrzewa należy dany węzeł jeśli warunki nie są spełnione, węzeł nie posiada następnika (posiada maksymalną wartość) 4 15 6 19 12 17 22 9 20 25
Znajdowanie następnika struct bstel* succ(struct bstel *node) { bn* y; if (!node) return NULL; /* błąd */ if (node->right) return min(node->right); y = parent(node); /* zaimplementować */ while (y && node==y->right) { node = y; y = parent(y); return y;
Usuwanie elementów Załóżmy, że należy usunąć x Jeśli x ma obydwa poddrzewa, znajdź succ(x) i przesuń go na miejsce x 15 Jeśli x ma tylko jedno poddrzewo, zastąp x tym poddrzewem Jeśli x jest liściem, po prostu usuń x 4 6 19 12 17 22 9 20 25
Wydruk całego drzewa void print_tree(bn *node) { if (node) { print_tree(node->left); printf("%d ",node->data); print_tree(node->right);
Wykorzystanie main() { bn *root = NULL; /* na początku puste drzewo */ ins_el(10,&root); /* budowanie drzewa */ ins_el( 6,&root);... ins_el( 7,&root); ins_el(23,&root); ins_el(11,&root); print_tree(root); /* powinno być posortowane! */ printf( \n %d \n", min(root)->data); printf("find(%d)=%d\n",13,find(13,root)); return 0;