• Nie Znaleziono Wyników

Załącznik do ćwiczenia laboratoryjnego. Operacje na listach

N/A
N/A
Protected

Academic year: 2022

Share "Załącznik do ćwiczenia laboratoryjnego. Operacje na listach"

Copied!
7
0
0

Pełen tekst

(1)

Załącznik do ćwiczenia laboratoryjnego

„Operacje na listach”

Struktury dowiązane (ze wskaźnikami)

Pośród konstrukcji programowych ważną rolę odgrywają struktury ze wskaźnikami nazywane strukturami dowiązanymi.

Jeden lub więcej wskaźników są dołączane do każdej porcji danych i razem z porcją danych tworzą element struktury.

Elementy łączą się między sobą w taki sposób, że wartością każdego wskaźnika w elemencie struktury jest adres innego elementu.

Jeżeli elementy są rozmieszczone w różnych miejscach pamięci, to wskaźnik na następny element musi być adresem.

Jeżeli elementy zapisywane są do tablicy, to pole - wskaźnik na następny element - może być in- deksem tablicy.

Programowe struktury danych ze wskaźnikami klasyfikowane są na zasadzie kształtu struktury przy ich graficznej reprezentacji.

Na rysunku pokazane są warianty struktur: liniowa (a), liniowa dwukierunkowa (b), cykliczna jednokierunkowa (c), drzewiasta (d), swobodna (e.

Wskaźniki są przedstawione na rysunku za pomocą prostokątów w zestawach elementów struktury oraz strzałek wskazujących na następny element. Pusty prostokąt oznacza zerową wartość wskaźnika.

Krótkie strzałki wskazują początkowe elementy.

Częściej wykorzystywane są struktury liniowe. Liniowe struktury danych ze wskaźnikami są nazywane zwykle listami.

Rys. 4.1. Warianty struktur dowiązanych:

liniowa (a), liniowa dwukierunkowa (b), cykliczna jednokierunkowa (c), drzewiasta (d), swobodna (e)

(2)

Listy

Listą (ang. list) nazywamy ciąg porcji danych utworzony za pomocą wskaźników dołączonych do każdej porcji danych. Porcja danych i wskaźnik tworzą element listy, gdzie wskaźnik wskazuje na na- stępny lub poprzedni element.

Często wykorzystywane są listy jednokierunkowe, listy dwukierunkowe oraz listy cykliczne tak jednokierunkowe, jak i dwukierunkowe.

Ostatni element jest identyfikowany na podstawie specjalnej wartości pola - wskaźnika na następny element:

w przypadku pola - adresu jest to zwykle zerowy adres, w przypadku pola - indeksu jest to zwykle wartość równa (-1).

Własnościami listy są rozmiar, początek i koniec.

Rozmiar jest równy ilości elementów listy.

Do wskazania na początek i koniec listy służą dodatkowe zmienne - wskaźniki na początkowy i końcowy element.

Wskaźnik na element początkowy jest obowiązkowy, ale wskaźnik na końcowy element jest wykorzystywany rzadko.

W listach niecyklicznych końcowy element można rozpoznać po zerowej wartości wskaźnika na następny element, a w listach cyklicznych -- po wartości, która jest równa wartości wskaźnika na element początkowy.

Tworzenie listy

Poniższy przykład pokazuje, jak utworzyć w języku C++ listę do reprezentacji ciągu liter a, b, c.

W przykładzie należy zwrócić uwagę na to, że nowy element zostaje dodany na początku listy i dlatego litera „c” jest pierwsza z zapisywanych liter.

Przykład w języku C++. Lista do reprezentacji ciągu liter a, b, c struct sElem

{

char dana;//pole elementu danych

sElem *nast;//wskaźnik na element następny };

sElem *pPocz;//wskaźnik na początek listy sElem *p;//tymczasowa zmienna - wskaźnik p=new sElem;//alokacja nowego elementu p->dana = ’c’;

p->nast=NULL;//koniec listy pPocz=p;//lista zawiera literę c

p=new sElem;//alokacja nowego elementu p->dana = ’b’;

p->nast=pPocz;//łączenie elementów pPocz=p;//lista zawiera litery b, c p=new sElem;//alokacja nowego elementu p->dana = ’a’;

p->nast=pPocz;//łączenie elementów pPocz=p;//lista zawiera litery a, b, c

Lista z wykorzystaniem tablicy

Dla danych liczbowych oraz znakowych wygodnie jest rozmieścić listę w tablicy.

Na przykład nieparzyste elementy tablicy można przeznaczyć do przechowywania pola - danej, a parzyste - do przechowywania pola - indeksu następnego elementu listy.

W poniższym przykładzie jest pokazane, jak skonstruować w tablicy tabl listę zawierającą litery a, b, c.

Przykład w języku C++. Lista z wykorzystaniem tablicy do reprezentacji ciągu liter a, b, c

int tabl[6]={'a',3,'b',5,'c',-1}; /* wartość 3 - indeks drugiego elementu listy, wartość 5 - indeks następnego */

int Pocz=0; // wskaźnik na początek listy (indeks początkowy) Operacje na listach

Na listach wykonywane są następujące typowe operacje:

- dodawanie elementu listy na początku, na końcu bądź za elementem z zadanym indeksem albo z zadaną wartością pola danych (atrybutem),

(3)

- usuwanie elementu listy z początku, z końca bądź elementu z zadanym indeksem albo z zadanym atrybutem,

- wyszukiwanie elementu z zadanym atrybutem i możliwa zamiana atrybutu lub wyjmowanie elementu, - wyszukiwanie elementu z najmniejszym (największym) atrybutem i możliwe wyjmowanie elementu, - zamiana elementu listy z zadanym indeksem albo z zadanym atrybutem,

- sortowanie elementów według wartości któregokolwiek atrybutu, - łączenie list,

- rozdzielenie list na podstawie elementu z zadanym indeksem albo z zadaną wartością pola danych, - usuwanie listy,

- skrócenie listy na podstawie elementu z zadanym indeksem albo z zadaną wartością pola danych, - obliczenie liczby elementów.

Algorytmy operacji na listach rozpatrzymy korzystając z przykładów list z bardzo prostym

elementem. Niech element listy zawiera jako dane kod pewnego znaku (literę). Strukturę elementu listy zdefiniujemy w języku C++ następująco:

struct sElem2 {

char dana;//pole elementu danych

sElem2 *nast;//wskaźnik na element następny sElem2(char c, sElem2 *p) //konstruktor { dana=c; nast=p; }

};

Wyszukiwanie elementu

Aby odpowiedzieć na pytanie, czy lista zawiera szukany element danych, potrzebne jest przeglądanie elementów listy, nawet jeśli elementy danych na liście są uporządkowane.

Elementy listy należy przeglądać tak długo, aż będzie przeanalizowany element, który jako ostatni ma zerową wartość pola wskaźnika na następny element.

Średni czas wyszukiwania elementu danych jest proporcjonalny do n/2, gdzie n - rozmiar listy.

Poniższy przykład ilustruje wyszukiwanie elementu na liście jednokierunkowej.

Przykład w języku C++. Procedura wyszukiwania elementu na liście jednokierunkowej bool SzukaElemListJednokier(sElem2 *pPocz, char Szuk, sElem2 **pp) {

(*pp)=NULL;/*wstępnie, na wypadek wyniku negatywnego*/

sElem2 *p=pPocz;

while (p!=NULL) {

(*pp)=p;// wstępnie, jeszcze do porównania if (p->dana==Szuk) return true;

p=p->nast;/* przejście do następnego elementu */

}

return false;/* wynik negatywny */

}

Dodawanie elementu

Do list jednokierunkowych najwygodniej jest dodawać nowy element na początku listy. Algorytm operacji dodawania elementu do listy niecyklicznej jednokierunkowej jest następujący:

Krok 1. Alokować nowy element listy.

Krok 2. Kopiować do nowego elementu dodawane pole danych.

Krok 3. W nowym elemencie wartość wskaźnika na następny element ustawić równym wartości wskaźnika na początek listy.

Krok 4. Wartość wskaźnika na początek listy ustawić równym wartości wskaźnika na nowy element.

W niżej przytoczonym przykładzie ten algorytm jest realizowany w postaci procedury.

Przykład w języku C++. Procedura dodawania elementu do listy niecyklicznej jednokierunkowej void DodawElemListNiecyklJednokier(sElem2 **ppPocz,

char nowaDana) {

sElem2 *p=new sElem2(nowaDana,*ppPocz);/*krok 1 (alokacja), krok 2 (kopiowanie danych) oraz krok 3 (łączenie z listą) */

*ppPocz=p; /*krok 4 (korekta wskaźnika na początek) */

}

(4)

Jeżeli nowy element trzeba dodać na końcu listy niecyklicznej jednokierunkowej, to czas

wykonywania operacji wydłuża się istotnie, ponieważ przed dołączeniem elementu należy przejrzeć listę od początku do końca.

Algorytm operacji dodawania elementu na końcu listy niecyklicznej jednokierunkowej jest następujący:

Krok 1. Alokować element listy.

Krok 2. Kopiować do nowego elementu dodawane dane, a pole wskaźnika na następny element ustawić na zero.

Krok 3. Znaleźć ostatni element listy metodą szukania od początku elementu z zerowym wskaźnikiem na następny element.

Krok 4. W znalezionym elemencie pole wskaźnika na następny element ustawić równym wartości wskaźnika na nowy element.

Ten algorytm jest realizowany w niżej przytoczonym przykładzie.

Przykład w języku C++. Procedura dodawania elementu na końcu listy niecyklicznej jednokierunkowej

void DodawElemListKoniecNiecyklJednokier(sElem2 **ppPocz, char nowaDana)

{

sElem2 *p=new sElem2(nowaDana, NULL);/*krok 1 (alokacja), krok 2 (kopiowanie danych oraz ustawienie końca listy) */

if (*ppPocz==NULL) *ppPocz=p;

else {

sElem2 *pt=*ppPocz;

while (pt->nast!=NULL)

pt=pt->nast;//krok 3 (szukanie końca listy) pt->nast=p;//krok 4 (łączenie z listą)

} }

---

Szybkość operacji dodawania elementu na końcu listy można zwiększyć przez wyeliminowanie szukania ostatniego elementu i w tym celu korzystać ze wskaźnika na ostatni element listy. Taki wskaźnik jest też przydatny w listach dwukierunkowych niecyklicznych oraz listach cyklicznych.

Algorytm dodawania elementu listy za elementem z zadanym indeksem albo z zadaną wartością pola danych jest podobny do algorytmu dodawania elementu na końcu listy, ale zmienia się warunek przeszukania listy. Tym razem należy porównywać indeksy lub wartosci pól danych.

Usuwanie elementu

Usuwanie elementu listy jest proste na początku lub na końcu listy, ponieważ można wykorzystać wskaźnik odpowiednio na pierwszy albo na ostatni element listy.

Aby wykluczyć element, należy jedynie przełączyć wskaźniki, jak to pokazano w poniższym

przykładzie. W językach C++ i Object Pascal usuwanie elementu komplikuje się, jeżeli wziąć pod uwagę to, że należy zwolnić pamięć zajmowaną przez usuwany element. Przypomnimy, że w języku C++ do tego celu służy operator delete, a w języku Object Pascal – procedura Dispose.

Przykład w języku C++. Procedura służąca do usuwania elementu na początku listy niecyklicznej jednokierunkowej

void UsuwElemList(sElem2 **ppPocz) {

if (*ppPocz==NULL)

return;//nic nie robić (lista pusta) sElem2 *p=*ppPocz;//zapamiętanie wartości *ppPocz=p->nast;// przełączenie wskaźnika delete p;//zwolnienie pamięci

}

---

W przypadku usuwania końcowego elementu listy niecyklicznej jednokierunkowej, należy nie tylko zwolnić pamięć, ale i ustawić na zero wskaźnik w elemencie poprzednim.

(5)

Algorytm operacji usuwania elementu z końca listy niecyklicznej jednokierunkowej jest następujący:

Krok 1. Znaleźć ostatni element listy metodą szukania od początku elementu z zerowym wskaźnikiem na następny element. Oprócz wskaźnika na bieżący element należy zapisywać wartość wskaźnika na element poprzedni.

Krok 2. Zwolnić pamięć przydzieloną ostatniemu elementowi.

Krok 3. W poprzednim elemencie ustawić wartość wskaźnika na następny element równą zero.

Niżej przytoczony przykład pokazuje implementację tego algorytmu w postaci procedury.

Przykład w języku C++. Procedura usuwania elementu z końca listy niecyklicznej jednokierunkowej void UsuwElemListKoniec(sElem2 *pPocz)

{

if (pPocz==NULL) return; // lista pusta sElem2 *ppop=NULL; // wskaźnik na poprzedni sElem2 *p=pPocz;

while (p->nast!=NULL)

{ //krok 1: szukanie końca listy

ppop=p; //zapisywanie wartości wskaźnika p=p->nast;

}

delete p;//krok 2: zwolnienie pamięci

ppop->nast=NULL;// krok 3: zerowanie wskaźnika }

---

Usuwanie elementu listy z zadanym indeksem albo z zadaną wartością pola danych realizuje się ze zmienionym warunkiem przeszukania listy. W nowym warunku przeszukania listy należy porównać indeksy bądź pola danych.

Zamiana elementu

Aby zamienić element listy należy najpierw wyszukać element na podstawie zadanego indeksu lub zadanej wartości pola danych.

Algorytm zamiany elementu listy może być następujący:

Krok 1. Znaleźć element metodą szukania od początku i porównania indeksów lub wartości pól danych. Dla dwukierunkowych list poszukiwanie można prowadzić od końca.

Krok 2. Zamienić wartości pól danych.

W poniższym przykładzie ten algorytm jest pokazany w postaci procedury dla wariantu porównania wartości pól danych. Procedura zwraca wartość logiczną, przy czym wartość false (False) oznacza, że element podlegający zamianie nie był znaleziony.

Przykład w języku C++. Procedura zamiany elementu listy

bool ZamianaElemList(sElem2 *pPocz, char szukD, char zamD) {

if (pPocz==NULL) return false;//nic nie robić (lista pusta) sElem2 *p=pPocz;

while (p!=NULL)

{//krok 1: szukanie elementu if (p->dana==szukD)

{

p->dana=zamD;//krok 2: zamiana return true;

}

p=p->nast;//krok 1: szukanie elementu }

return false;//nie znalezione }

Łączenie list

Dodawana lista jest prezentowana zwykle przez wskaźnik na pierwszy element. Łączenie list niecyklicznych jest podobne do dodawania elementu na końcu listy; jedynie zamiast wskaźnika na nowy element należy wykorzystać wskaźnik na listę dodawaną.

Nieco trudniej dodać listę w przypadku list cyklicznych. Najbardziej złożony przypadek łączenia list to łączenie list cyklicznych dwukierunkowych. W tym celu należy zastosować następujący algorytm:

(6)

Krok 1. Wykorzystując wskaźnik na pierwszy element listy podstawowej metodą przeszukania listy uzyskać wskaźnik na ostatni element listy podstawowej.

Krok 2. Wykorzystując wskaźnik na pierwszy element listy dodawanej metodą przeszukania listy uzyskać wskaźnik na ostatni element listy dodawanej.

Krok 3. Wykorzystując wskaźniki na ostatni element listy podstawowej i na pierwszy element listy dodawanej, połączyć te elementy przez zmianę wartości odpowiednich wskaźników na element na- stępny i na element poprzedni.

Krok 4. Wykorzystując wskaźniki na pierwszy element listy podstawowej i na ostatni element listy dodawanej, połączyć te elementy przez zmianę wartości odpowiednich wskaźników na element na- stępny i na element poprzedni.

W implementacji algorytmu powinny być uwzględnione przypadki list pustych.

W poniższym przykładzie jest realizowany opisany algorytm.

Przykład w języku C++. Procedura łączenia list cyklicznych dwukierunkowych struct sElem3

{

char dana;//pole elementu danych

sElem3 *pN;//wskaźnik na element następny sElem3 *pP;//wskaźnik na element poprzedni sElem3(char da) //konstruktor

{ dana=da; pN = NULL; pP = NULL;}

};

void LanczListCyklDwukier(sElem3 **ppPocz, sElem3 *pLista) {

if (pLista==NULL)

return;//nic nie robić (lista dodawana pusta) if (*ppPocz==NULL)

{//zamiana na listę dodawaną *ppPocz=pLista;

return;

}

//krok 1: wskaźnik na koniec listy podstawowej:

sElem3* pKonBaz=(*ppPocz)->pP;

//krok 2: wskaźnik na końcowy element listy dodawanej:

sElem3* pKonDod=pLista->pP;

//krok 3: łączenie pól pN:

pKonBaz->pN = pLista;

pKonDod->pN = *ppPocz;

//krok4: łączenie pól pP:

pLista->pP = pKonBaz;

(*ppPocz)->pP = pKonDod;

}

Rozdzielenie listy

Lista może być rozdzielona na dwie listy na podstawie zadanego indeksu albo zadanej wartości pola danych. Algorytm operacji rozdzielenia listy jest krótki:

Krok 1. Znaleźć na liście element z zadanym indeksem albo z zadaną wartością pola danych.

Krok 2. Wartość pola wskaźnika na następny element w znalezionym elemencie najpierw skopio- wać do wskaźnika na drugą listę, a później ustawić na zero.

W podanym niżej przykładzie pokazano implementację operacji rozdzielenia jednokierunkowej listy za elementem z zadanym indeksem. Do procedury jest dodane sprawdzenie, czy lista jest pusta.

Przykład w języku C++. Procedura rozdzielenia listy jednokierunkowej

void RozlaczListIndeks(sElem2 *pPocz, int ind, sElem2 **ppL2) {

(*ppL2)=NULL;//wstępnie

if (pPocz==NULL) return;//nic nie robić (lista pusta) int ii=0;

sElem2 *p=pPocz;//krok 1: szukanie elementu while (p!=NULL)

{

if (ii==ind)//warunek „zadany indeks"

{

(7)

(*ppL2)=p->nast;//krok 2: kopiowanie p->nast=NULL;//krok 2: koniec listy 1 return;

} ii++;

p=p->nast;//krok 1: szukanie elementu }

return;//nie znalezione }

Usuwanie listy

Usuwanie listy jest potrzebne zwykle na etapie kończenia programu.

Zwolnienie obszaru pamięci zajmowanego przez elementy listy jest warunkiem normalnego działania innych programów uruchomionych później.

W przykładzie przytoczonym niżej pokazano wykorzystanie warunku „p != pPocz” jako warunku końca listy cyklicznej jednokierunkowej.

Chociaż pierwszy element listy, na który pokazuje wskaźnik pPocz, będzie zniszczony jako pierwszy, program będzie działał poprawnie, ponieważ przy zwolnieniu obszaru pamięci wartość wskaźnika na ten obszar nie zmienia się.

Przykład w języku C++. Procedura usuwania listy cyklicznej jednokierunkowej void UsuwListCyklJednokier(sElem2 **ppPocz)

{

if (*ppPocz==NULL) return;//nic nie robić (lista pusta) sElem2 *p=*ppPocz;

while (p!=NULL)

{ sElem2 *ptemp=p->nast;

delete p;//zwolnienie pamięci p=ptemp;

if (p==(*ppPocz)) break;

}

*ppPocz=NULL;// lista pusta }

Skrócenie listy

Operacja skrócenia listy jest wykonywana bądź za elementem z zadanym indeksem, bądź za elementem z zadaną wartością pola danych. Algorytm operacji skrócenia listy składa się z szukania elementu i usuwania pozostałych elementów:

Krok 1. Znaleźć na liście element z zadanym indeksem albo z zadaną wartością pola danych.

Krok 2. Zwolnić pamięć zajmowaną przez pozostałe elementy listy.

W niżej podanej procedurze realizującej operację skrócenia jednokierunkowej listy za elementem z zadaną wartością pola danych dodane są instrukcje sprawdzania, czy lista jest pusta.

Przykład w języku C++. Procedura skrócenia listy jednokierunkowej za elementem z zadaną wartością

void SkrocListJednokierWart (sElem2 *pPocz, char ostatn) { if (pPocz==NULL) return;//nic nie robić (lista pusta) sElem2 *p=pPocz;//zapamiętanie wartości

while (p!=NULL)

{ if (p->dana==ostatn)//porównanie elementów { sElem2 *ptemp=p->nast;

p->nast=NULL;//nowy koniec listy p=ptemp;

while (p!=NULL) {

ptemp=p->nast;

delete p;//krok 2: zwolnienie pamięci p=ptemp;

}

break;//koniec szukania elementu }

else p=p->nast;//krok 1: szukanie elementu }

}

Cytaty

Powiązane dokumenty

Podczas usuwania należy po usunięciu każdego elementu zmniejszyć wartość indeksu i, aby wznowić proces wyszukiwania elementu do usunięcia od elementu.. przesuniętego z

#include lato.h Rzecz między nami była cicha Westchnąłem do ciebie Tak jak się wzdycha I było nam ciasno, miło Dużo się spało i często się piło No i czego, czego

The papers presented here reflect our experience and broad research interests, in particular in the areas of spatial planning, real estate markets

Na jej temat Rousseau pisze: Ale gdyby nawet […] wspomniana różnica między człowiekiem a zwierzęciem mogła być jeszcze przedmiotem niejakiej dyskusji, to dzieli ich jeszcze

W bada­ nych organizacjach zaobserwowałam następujące zachowania sprzyjające zarządzaniu wiedzą: nastawienie na wspólną realizację celów, dzielenie się wiedzą,

Zna- jąc temperaturę powierzchni elementu grzejnego, jego konstrukcję, parametry fizyczne materiałów oraz wartość strumienia ciepła, można wyliczyć temperaturę drutu

[r]

In his ESR Energy Vision paper, nuclear power will continue to play a significant future role in meeting growing energy demand, enhancing energy security and alleviating the risk