Programowanie strukturalne
- Wykład 13

Listy jednokierunkowe
- ciąg dalszy

Dodanie na początek - lista bez głowy

Dodanie na początek wymaga zatem wykonania następujących operacji (bez uwzględnienia sytuacji kiedy bazowa lista jest pusta):

  1. Rezerwacja pamięci na nowy element
  2. Ustawienia wartości pola i na nowym elemencie z punktu 1
  3. Pole next nowego elementu z punktu 1 jest ustawiane jako adres pierwszej elementu początkowej listy
  4. Należy zmienić wskaźnik całej „listy” wskazując jako adres „nowy” element z punktu 1.

Kod w main z kolejnym dodawaniem elementów

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

int main()
{
     struct element * lista = NULL; //tworzymy "pustą listę"
     //dodajemy pierwszy element na listę (tu pojęcie koniec czy początek nie ma większego sensu)
     struct element * wsk = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk->i=-4;
     wsk->next=NULL; //bo nic więcej nie ma na liście
     lista=wsk; // ustawiamy wskaznik początku listy
     // dodajemy nowy element na początek
     struct element * wsk2 = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk2->i=7;
     wsk2->next=lista; // następnik nowego elementu to bieżacy wskaznik listy
     lista=wsk2;  // ustawiamy wskaznik nowego elementy jako początek listy
     // dodajemy jeszcze jeden nowy element na początek
     struct element * wsk3 = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk3->i=5;
     wsk3->next=lista; // następnik nowego elementu to bieżacy wskaznik listy
     lista=wsk3;  // ustawiamy wskaznik nowego elementy jako początek listy
     //tu już mamy listę jak na rysunku - pierwszy eleemnt yo 5, drugi to 7, trzeci to i 4
     // teraz dodajemy 11 na początek
     struct element * wsk4 = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk4->i=11;
     wsk4->next=lista; // następnik nowego elementu to bieżacy wskaznik listy
     lista=wsk4;  // ustawiamy wskaznik nowego elementy jako początek listy
     return 0;
}

Prostsze rozwiązanie gdy lista jest już stworzona:

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

int main()
{
    struct element * lista = malloc(sizeof (struct element));
    lista->i = 5;
    lista->next = malloc(sizeof (struct element));
    lista->next->i = 7;
    lista->next->next = malloc(sizeof (struct element));
    lista->next->next->i = -4;
    lista->next->next->next = NULL;
    struct element * temp = lista;
    while (temp != NULL)
    {
        printf("%d\n", temp->i);
        temp = temp->next;
    }
    printf("dodanie\n");
    struct element * wsk = malloc(sizeof (struct element));
    wsk->i = 11;
    wsk->next = lista;
    lista = wsk;
    temp = lista;
    while (temp != NULL)
    {
        printf("%d\n", temp->i);
        temp = temp->next;
    }
    return 0;
}

Kod w funkcji

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

struct element* dodaj(struct element*Lista, int a)
{
    struct element * wsk = malloc(sizeof(struct element));
    wsk->i=a;
    wsk->next=Lista;
    return wsk;
}

struct element * utworz()
{
    return NULL;
}

int main()
{
    struct element* l1 = utworz();
    l1 = dodaj(l1,2);
    l1 = dodaj(l1,4);
    l1 = dodaj(l1,7);
    l1 = dodaj(l1,-2);
    struct element * wsk = l1;
    while(wsk!=NULL)
    {
        printf("%d\n",wsk->i);
        wsk=wsk->next;
    }
    return 0;
}

Kod w funkcji - argument podwójny wskaźnik

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

void dodaj(struct element**Lista, int a)
{
    struct element * wsk = malloc(sizeof(struct element));
    wsk->i=a;
    wsk->next=*Lista;
    *Lista= wsk;
}

struct element * utworz()
{
    return NULL;
}

int main()
{
    struct element* l1 = utworz();
    dodaj(&l1,2);
    dodaj(&l1,4);
    dodaj(&l1,7);
    dodaj(&l1,-2);
    struct element * wsk = l1;
    while(wsk!=NULL)
    {
        printf("%d\n",wsk->i);
        wsk=wsk->next;
    }
    return 0;
}

Dodanie na początek - lista z głową

Dodanie na początek (przy założeniu że bieżąca lista nie jest pusta) polega na wykonaniu operacji:

  1. Rezerwujemy pamięć na nowy element
  2. Ustawiamy wartość jako pole i
  3. Pole next ustawiamy jako to co znajduje się w „głowie” w polu next
  4. Modyfikujemy pole next w „głowie” ustawiając je jako wskaźnik na nowy element z punktu 1

Ważne: nie możemy zmienić kolejności punktu 3 i 4 bo zmieni to sens.

Kod w main:

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

int main()
{
     //tworzymy głowę
     struct element * lista = malloc(sizeof(struct element));
     //pola i glowy nie ustawiamy
     lista->next= NULL; //następnik glowy jest na NULL, mamy zatem w tym miejscu pustą listę z głową
     //dodajemy pierwszy element na listę - w liscie z głową musimy go postawić po głowie
     struct element * wsk = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk->i=-4;
     wsk->next=lista->next; //tutaj teoretycznie można ustawić na NULL, ale zgodnie z konwencją trzeba ustawić pole next głowy
     lista->next=wsk; // ustawiamy nastepnik glowy jako wskaznik na nowy element
     // dodajemy nowy element na początek
     struct element * wsk2 = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk2->i=7;
     wsk2->next=lista->next; // następnik nowego elementu to nastepnik "głowy"
     lista->next=wsk2;  // ustawiamy następnik głowy jako wskaźnik na nowy element
     // dodajemy jeszcze jeden nowy element na początek
     struct element * wsk3 = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk3->i=5;
     wsk3->next=lista->next; // następnik nowego elementu to nastepnik "głowy"
     lista->next=wsk3;  // ustawiamy następnik głowy jako wskaźnik na nowy element
     //tu już mamy listę jak na rysunku - "głowa", pierwszy eleemnt to 5, drugi to 7, trzeci to i 4
     // teraz dodajemy 11 na początek
     struct element * wsk4 = malloc(sizeof(struct element)); // rezerwujemy miejsce w pamięci na jeden z elementów
     wsk4->i=11;
     wsk4->next=lista->next; // następnik nowego elementu to nastepnik "głowy"
     lista->next=wsk4;  // ustawiamy następnik głowy jako wskaźnik na nowy element
     return 0;
}

Prostsze rozwiązanie gdy lista jest już stworzona:

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

int main()
{
    struct element * lista = malloc(sizeof(struct element));
    lista->next = malloc(sizeof(struct element));
    lista->next->i = 5;
    lista->next->next = malloc(sizeof(struct element));
    lista->next->next->i = 7;
    lista->next->next->next = malloc(sizeof(struct element));
    lista->next->next->next->i = -4;
    lista->next->next->next->next = NULL;
    struct element * temp = lista->next;
    while(temp != NULL)
    {
        printf("%d\n", temp->i);
        temp = temp->next;
    }
    printf("Dodanie\n");
    struct element * wsk = malloc(sizeof(struct element));
    wsk->i = 11;
    wsk->next = lista->next;
    lista->next = wsk;
    temp = lista->next;
    while(temp != NULL)
    {
        printf("%d\n", temp->i);
        temp = temp->next;
    }
    return 0;
}

Kod przez funkcje:

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

struct element * utworz()
{
    struct element * wskaznik = malloc(sizeof(struct element));
    wskaznik->next=NULL;
    return wskaznik;
}

void dodaj(struct element*Lista, int a)
{
    struct element * wsk = malloc(sizeof(struct element));
    wsk->i=a;
    wsk->next=Lista->next;
    Lista->next=wsk;
}

int main()
{
    struct element* l1 = utworz();
    dodaj(l1,2);
    dodaj(l1,4);
    dodaj(l1,-8);
    dodaj(l1,-22);
    struct element * wsk = l1;
    while(wsk->next!=NULL)
    {
        wsk=wsk->next;
        printf("%d\n",wsk->i);
    }
    return 0;
}

Co zyskujemy? To zależy od kontekstu w którym używamy i jakie operacje mamy. W wielu sytuacjach zmiana wskaźnika „początku” przy liście bez głowy jest operacją, która zwiększa tzw. złożoność obliczeniową.

Na dziś można sobie myśleć, że jest to „szybsze” (to też zależy od języka programowania).

Dodanie na koniec - lista bez głowy

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

struct element * dodaj_na_koniec(struct element * Lista, int a)
{
    struct element * wsk;
    wsk = Lista;
    if (Lista == NULL)
    {
        Lista = malloc(sizeof(struct element));
        Lista->i = a;
        Lista->next = NULL;
    }
    else
    {
        while (wsk->next != NULL)
        {
            wsk = wsk->next;
        }
        wsk->next = malloc(sizeof(struct element));
        wsk->next->i = a;
        wsk->next->next = NULL;
    }
    return Lista;
}

void wyswietl(struct element * Lista)
{
    struct element * wsk;
    wsk = Lista;
    if (wsk == NULL)
    {
        printf("Lista jest pusta\n");
        return;
    }
        while (wsk != NULL)
    {
        printf("%d\n", wsk->i);
        wsk = wsk->next;
    }
    printf("---\n");
}

int main()
{
    // przypadek pusty
    struct element * Lista = NULL;
    wyswietl(Lista);
    Lista = dodaj_na_koniec(Lista, 5);
    wyswietl(Lista);
    // przypadek listy dwuelementowej
    struct element * lista2 = malloc(sizeof(struct element));
    lista2->i = 12;
    lista2->next = malloc(sizeof(struct element));
    lista2->next->i = -8;
    lista2->next->next = NULL;
    wyswietl(lista2);
    lista2 = dodaj_na_koniec(lista2, 3);
    wyswietl(lista2);
    return 0;
}

Podwójny wskaźnik

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

void dodaj_na_koniec(struct element ** Lista, int a)
{
    struct element * wsk;
    wsk = *Lista;
    if (wsk == NULL)
    {
        wsk = malloc(sizeof(struct element));
        wsk->i = a;
        wsk->next = NULL;
        *Lista = wsk;
        return;
    }
    while (wsk->next != NULL)
    {
        wsk = wsk->next;
    }
    wsk->next = malloc(sizeof(struct element));
    wsk->next->i = a;
    wsk->next->next = NULL;
}

void wyswietl(struct element * Lista)
{
    struct element * wsk;
    wsk = Lista;
    if (wsk == NULL)
    {
        printf("Lista jest pusta\n");
        return;
    }
        while (wsk != NULL)
    {
        printf("%d\n", wsk->i);
        wsk = wsk->next;
    }
    printf("---\n");
}

int main()
{
    // przypadek pusty
    struct element * Lista = NULL;
    wyswietl(Lista);
    dodaj_na_koniec(&Lista, 5);
    wyswietl(Lista);
    // przypadek listy dwuelementowej
    struct element * lista2 = malloc(sizeof(struct element));
    lista2->i = 12;
    lista2->next = malloc(sizeof(struct element));
    lista2->next->i = -8;
    lista2->next->next = NULL;
    wyswietl(lista2);
    dodaj_na_koniec(&lista2, 3);
    wyswietl(lista2);
    return 0;
}

Dodanie na koniec - lista z głową

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element * next;
};

void dodaj_na_koniec(struct element * Lista, int a)
{
    struct element * wsk;
    wsk = Lista;
    while (wsk->next != NULL)
    {
        wsk = wsk->next;
    }
    wsk->next = malloc(sizeof(struct element));
    wsk->next->i = a;
    wsk->next->next = NULL;
}

void wyswietl(struct element * Lista)
{
    struct element * wsk;
    wsk = Lista->next;
    if (wsk == NULL)
    {
        printf("Lista jest pusta\n");
        return;
    }
        while (wsk != NULL)
    {
        printf("%d\n", wsk->i);
        wsk = wsk->next;
    }
    printf("---\n");
}

int main()
{
    // przypadek pusty
    struct element * Lista1 = malloc(sizeof(struct element));
    Lista1->next = NULL;
    wyswietl(Lista1);
    dodaj_na_koniec(Lista1, 5);
    wyswietl(Lista1);
    // przypadek dwuelementowy
    struct element * Lista2 = malloc(sizeof(struct element));
    Lista2->next = malloc(sizeof(struct element));
    Lista2->next->i = 12;
    Lista2->next->next = malloc(sizeof(struct element));
    Lista2->next->next->i = -8;
    Lista2->next->next->next = NULL;
    wyswietl(Lista2);
    dodaj_na_koniec(Lista2, 3);
    wyswietl(Lista2);
    return 0;
}

Usunięcie pierwszego wystąpienia elementu - lista bez głowy

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element *next;
};

void wyswietlListeBezGlowy(struct element * lista)
{
    if (lista == NULL)
    {
        printf("Lista jest pusta\n---\n");
        return;
    }
    struct element * wsk = lista;
    while(wsk != NULL)
    {
        printf("%d\n", wsk->i);
        wsk = wsk->next;
    }
    printf("---\n");
}

struct element * usun(struct element * lista, int a)
{
    if (lista == NULL)
        return NULL;
    if(lista->i == a)
    {
        struct element * wsk = lista->next;
        free(lista);
        return wsk;
    }
    struct element * wsk= lista;
    while( (wsk->next!=NULL) && (wsk->next->i !=a) )
    {
        wsk=wsk->next;
    }
    if(wsk->next!=NULL)
    {
        struct element * wsk2 =wsk;
        wsk2=wsk2->next;
        wsk->next=wsk->next->next;

        free(wsk2);
    }
    return lista;
}

int main()
{
    struct element * lista = malloc(sizeof(struct element));
    lista->i = 6;
    lista->next = malloc(sizeof(struct element));
    lista->next->i = 2;
    lista->next->next = NULL;
    wyswietlListeBezGlowy(lista);
    lista=usun(lista, 2);
    wyswietlListeBezGlowy(lista);
    return 0;
}

Usunięcie pierwszego wystąpienia elementu - lista z głową

#include <stdio.h>
#include <stdlib.h>

struct element
{
    int i;
    struct element *next;
};

void wyswietlListeZGlowa(struct element * lista)
{
    if (lista->next == NULL)
    {
        printf("Lista jest pusta\n---\n");
        return;
    }
    struct element * wsk = lista->next;
    while(wsk != NULL)
    {
        printf("%d\n", wsk->i);
        wsk = wsk->next;
    }
    printf("---\n");
}

void usun(struct element * lista, int a)
{
    if (lista->next  == NULL)
        return;
    struct element * wsk= lista;
    while( (wsk->next!=NULL) && (wsk->next->i !=a) )
    {
        wsk=wsk->next;
    }
    if(wsk->next!=NULL)
    {
        struct element * wsk2 =wsk;
        wsk2=wsk2->next;
        wsk->next=wsk->next->next;

        free(wsk2);
    }
}

int main()
{
    struct element * lista = malloc(sizeof(struct element));
    lista->next = malloc(sizeof(struct element));
    lista->next->i = 6;
    lista->next->next = malloc(sizeof(struct element));
    lista->next->next->i = 2;
    lista->next->next->next = NULL;
    wyswietlListeZGlowa(lista);
    usun(lista, 6);
    wyswietlListeZGlowa(lista);
    return 0;
}

Struktury danych

Struktury danych

Struktura danych (ang. data structure) – sposób przechowywania danych w pamięci komputera. Na strukturach danych operują algorytmy.

Podczas implementacji programu programista często staje przed wyborem między różnymi strukturami danych, aby uzyskać pożądany efekt. Odpowiedni wybór może zmniejszyć złożoność obliczeniową, ale z drugiej strony trudność implementacji danej struktury może stanowić istotną przeszkodę.

Przykładowe struktury danych to:

  • rekord lub struktura (ang. record, struct), logiczny odpowiednik to krotka
  • tablica
  • lista
  • stos
  • kolejka
  • drzewo i jego liczne odmiany (np. drzewo binarne)
  • graf

Abstrakcyjny typ danych

Abstrakcyjny typ danych

Abstrakcyjny typ danych (ang. abstract data type, ADT) – tworzenie i opisywanie w formalny sposób typów danych tak, że opisywane są jedynie własności danych i operacji wykonywanych na nich (a nie przez reprezentację danych i implementację operacji).

Specyfikacja ADT powinna:

  • być jednoznaczna i dokładna;

  • zawierać wszystkie przypadki warte rozważenia;

  • nie zawierać niepotrzebnych informacji.

  • podając specyfikację ADT (dowolnego typu), powinniśmy uwzględnić:

    • nazwę tego typu;
    • dziedzinę;
    • zbiór funkcji;
    • aksjomaty;
    • warunki początkowe.

Przegląd

Kontener

Własności typu:

  • dostęp, czyli sposób uzyskiwania dostępu do obiektów kontenera. W przypadku tablic dostęp odbywa się za pomocą indeksu. W przypadku stosów dostęp odbywa się zgodnie z kolejnością LIFO, a w przypadku kolejek - zgodnie z kolejnością FIFO.
  • magazynowanie, czyli sposób przechowywania przedmiotów pojemnika,
  • translacji, to jest sposób przechodzenia przedmiotów pojemnika.

Dostępne działania:

  • Inicjalizacja kontenera.
  • Dodawanie pozycji do kontenera.
  • Usuwanie pozycji z kontenera.
  • Opróżnienia kontenera.
  • Dostęp do pozycji z kontenera.
  • Dostęp do liczby pozycji w kontenerze.

Zbiór

Nazwa typu: Zbiór

Właściwości typu:

  • matematyczny odpowiednik pojęcia zbioru.
  • brak duplikatów.
  • brak uporządkowania elementów.

Dostępne działania:

  • Inicjalizacja zbioru.
  • Suma zbiorów.
  • Część wspólna zbiorów.
  • Różnica zbiorów.
  • Sprawdzenie czy jeden zbiór zawiera się w się w drugim (jest podzbiorem).
  • Sprawdzenie, czy pozycja należy do zbioru.
  • Sprawdzenie, czy zbiór jest pusty czy nie.
  • Dostęp do liczby pozycji w zbiorze.

Opcjonalnie:

  • Dodawanie pozycji do zbioru.
  • Usuwanie pozycji ze zbioru.
  • Iterowanie/enumerowanie zbioru.
  • Dostępność do pojemności zbioru (maksymalnej liczby pozycji mogących być dodanych do zbioru).

Prosta lista

Nazwa typu: Prosta lista

Własności typu: Potrafi przechować ciąg pozycji.

Dostępne działania:

  • Inicjalizacja listy.
  • Określenie, czy lista jest pusta.
  • Określenie, czy lista jest pełna.
  • Określenie liczby pozycji w liście.
  • Dodanie pozycji na końcu listy.
  • Przejście przez listę i przetwarzanie każdej pozycji.
  • Opróżnienie listy.

Kolejka

Nazwa typu: Kolejka

Własności typu: Potrafi przechować uporządkowany ciąg pozycji.

Dostępne działania:

  • Inicjalizacja kolejki.
  • Określenie, czy kolejka jest pusta.
  • Określenie, czy kolejka jest pełna.
  • Określenie liczby pozycji w kolejce.
  • Dodanie pozycji z tyłu kolejki.
  • Pobranie i usunięcie pozycji z przodu kolejki.
  • Opróżnienie kolejki.

Drzewo binarne

Własności typu:

  • Drzewo binarne jest albo pustym zbiorem węzłów, albo zbiorem węzłów, z których jeden jest oznaczony jako korzeń.
  • Z każdego węzła wywodzą się dokładnie dwa drzewa, zwane lewym poddrzewem i prawym poddrzewem.
  • Każde poddrzewo jest samo drzewem binarnym (pełnym lub pustym).
  • Każdy węzeł zawiera pozycję, przy czym wszystkie pozycje w lewym poddrzewie poprzedzają pozycję zapisaną w korzeniu, a pozycja w korzeniu poprzedza wszystkie pozycje w prawym poddrzewie.

Dostępne działania:

  • Inicjalizacja drzewa.
  • Określenie, czy drzewo jest puste.
  • Określenie, czy drzewo jest pełne.
  • Określenie liczby pozycji w drzewie.
  • Dodanie pozycji do drzewa.
  • Usunięcie pozycji z drzewa.
  • Poszukiwanie pozycji w drzewie.
  • Przejście po wszystkich pozycjach w drzewie.
  • Opróżnianie drzewa.

Krotka

Krotka (ang. tuple) – struktura danych będąca odzwierciedleniem matematycznej n-ki, tj. uporządkowanego ciągu wartości. Krotki przechowują stałe wartości o różnych typach danych – nie można zmodyfikować żadnego elementu, odczyt natomiast wymaga podania indeksu liczbowego żądanego elementu.

Multizbiór

Multizbiór (także wielozbiór, ang. multiset) – uogólnienie pojęcia zbioru, w którym w odróżnieniu od klasycznych zbiorów jeden element może występować wiele razy. Nie jest jednak dana żadna ich kolejność i tym multizbiór różni się od krotki.

Zbiory \(\{1,2,3\}, \{3,2,1\}\) i \(\{1,2,2,3\}\) są identyczne.

Multizbiory \(\{1,2,3\},\{1, 2, 3\}\) i \(\{3,2,1\}\) są identyczne, \(\{1,2,2,3\}\) jest jednak inny.

Inne

Kolejka priorytetowa - https://pl.wikipedia.org/wiki/Kolejka_priorytetowa

Kopiec - https://pl.wikipedia.org/wiki/Kopiec_(informatyka)

Rekord - https://pl.wikipedia.org/wiki/Rekord_(informatyka)

Tablica mieszająca - https://pl.wikipedia.org/wiki/Tablica_mieszaj%C4%85ca

Tablica asocjacyjna - https://pl.wikipedia.org/wiki/Tablica_asocjacyjna

Lista

Lista

https://www.geeksforgeeks.org/data-structures/linked-list/

Lista jednokierunkowa

struct element
{
    int i;
    struct element * next;
};

Warianty - z głową (z wartownikiem) i bez.

Lista dwukierunkowa

struct element
{
    int i;
    struct element * prev;
    struct element * next;
};

Warianty: * z głową * z ogonem * mieszane

Lista cykliczna

struct element
{
    int i;
    struct element * next;
};

Kolejka

https://www.geeksforgeeks.org/queue-data-structure/

Stos

https://www.geeksforgeeks.org/stack-data-structure/

Inne

https://www.geeksforgeeks.org/data-structures/

Bibliografia