Prezentowane materiały są przeznaczone dla uczniów szkół ponadgimnazjalnych Autor artykułu: mgr Jerzy Wałaszek |
©2014 mgr
Jerzy Wałaszek
|
Rodzaje list
Lista (ang. list) jest sekwencyjną
strukturą danych, która składa się z ciągu elementów tego samego
typu. Dostęp do elementów listy jest sekwencyjny – tzn. z danego
elementu listy możemy przejść do elementu następnego lub do
poprzedniego. Dojście do elementu
i-tego wymaga przejścia przez kolejne elementy od pierwszego
do docelowego. Takie zachowanie się tej struktury jest konsekwencją
budowy jej elementów. Oprócz samych danych każdy element listy
przechowuje zwykle dwa wskaźniki – do elementu następnego listy oraz
do elementu poprzedzającego na liście. Nazwijmy te wskaźniki
odpowiednio (stosujemy nazwy angielskie z powodu
ich powszechności w literaturze):
W przeciwieństwie do tablicy elementy listy nie muszą leżeć obok siebie w pamięci. Zatem lista nie wymaga ciągłego obszaru pamięci i może być rozłożona w różnych jej segmentach – w porównaniu do tablic jest to niewątpliwie zaletą. Wskaźniki przechowywane w każdym elemencie pozwalają powiązać te elementy w ciąg:
Po tak określonej liście możemy się poruszać w obu kierunkach, które wyznaczają pola next i prev. Listę o takiej własności nazywamy listą dwukierunkową (ang. doubly linked list). Pierwszy element listy nie posiada poprzednika, zatem w polu prev przechowuje wskaźnik pusty (czyli zero – adres zero nie wskazuje żadnego elementu). Podobnie ostatni element listy nie posiada następnika i jego pole next zawiera wskaźnik pusty. W niektórych zastosowaniach nie potrzebujemy przechodzić listy w obu kierunkach. W takim przypadku w każdym elemencie wystarczy jeden wskaźnik, np. next:
Taką uproszczoną listę nazywamy listą jednokierunkową (ang. singly linked list). Jeśli za następnik ostatniego elementu listy przyjmiemy pierwszy element na liście, to otrzymamy jednokierunkową listę cykliczną (ang. singly linked cyclic list):
Lista cykliczna nigdy się nie kończy – przechodzimy ją cyklicznie w koło. Jeśli dodatkowo poprzednikiem pierwszego elementu listy zrobimy ostatni element, to otrzymamy dwukierunkową listę cykliczną (ang. doubly linked cyclic list):
Taką listę cykliczną możemy przechodzić w obu kierunkach. W tablicy dostęp do wybranego elementu jest bardzo szybki, ponieważ elementy znajdują się jeden obok drugiego w ciągłym bloku pamięci (aby obliczyć adres elementu o danym indeksie wystarczy do adresu tablicy dodać iloczyn indeksu przez rozmiar elementu). W przypadku listy nie możemy tak postąpić, ponieważ elementy mogą znajdować się w dowolnym obszarze pamięci i niekoniecznie obok siebie. Nawet jeśli znajdują się obok siebie, to ich kolejność na liście wcale nie musi odpowiadać kolejności położenia w pamięci. W przypadku listy musimy przechodzić kolejno od jednego elementu do drugiego wzdłuż ścieżki wyznaczonej przez ich pola next lub prev. Przy długiej liście może to być operacja czasochłonna. To z kolei jest wadą list w porównaniu z tablicami. Tworząc listę w pamięci zwykle dodatkowo rezerwuje się trzy zmienne dla jej obsługi:
Zmienna tego typu umożliwia dostęp do początku i do końca listy oraz zapamiętuje liczbę elementów przechowywanych na liście. Można ją stosować zarówno do list jednokierunkowych jak i dwukierunkowych. Jeśli interesuje nas jedynie dostęp do początku listy, to można pominąć składniki tail i count. W dalszej części artykułu podajemy podstawowe algorytmy operujące na listach. Przyjęliśmy zasadę, iż listy jednokierunkowe będą maksymalnie uproszczone – do obsługi listy będzie stosowana tylko zmienna head, a listy dwukierunkowe będą posiadały wszystkie ułatwienia.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Reprezentacja list w pamięci komputera
Tworząc zmienną typu lista, tworzymy w niej trzy pola: head,
tail oraz count
(obowiązkowe jest pole head, pozostałe
usprawniają obsługę list, ale nie są konieczne – dla list
jednokierunkowych specjalnie pominęliśmy tail oraz count). Zdefiniujmy podstawowe typy danych:
Górna część tablicy określa typy dla list jednokierunkowych. Dolna część to deklaracje dla list dwukierunkowych.
Dla list jednokierunkowych nie ma potrzeby tworzenia osobnego typu dla zmiennej obsługującej listę – zmienna ta będzie przechowywać tylko adres pierwszego elementu listy. Zmienną obsługującą listę tworzymy następująco (u góry dla list jednokierunkowych z uproszczoną obsługą, u dołu dla list dwukierunkowych z pełną obsługą):
Po deklaracji zmiennej należy ją bezwzględnie zainicjować. W tym celu do wszystkich pól wpisujemy zera. Dla listy dwukierunkowej możemy stworzyć prostą procedurę inicjalizacji:
Otrzymujemy w ten sposób pustą listę (ang. empty list), czyli listę, która nie zawiera żadnego elementu. Tak zainicjowana zmienna L jest gotowa do dalszego użycia w programach.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Operacje na listach jednokierunkowychTyp elementu listy
Przejście przez listę
Przechodzenie listy ma na celu przejście kolejno przez wszystkie
elementy listy, począwszy od pierwszego, a skończywszy na ostatnim.
Operacja ta jest częścią składową wielu innych operacji, gdzie algorytm
musi coś wyszukać na liście. W przypadku listy jednokierunkowej możemy
się po niej poruszać tylko w jednym kierunku: od początku do końca. Z
uwagi na sposób powiązania ze sobą elementów listy, do jej przechodzenia
potrzebna jest zmienna typu wskaźnik, która będzie wskazywała na kolejne
elementy. Na początku algorytmu zmienna ta powinna wskazywać pierwszy
element na liście. W pętli przetwarzamy element wskazywany przez tą
zmienną, po czym za nową wartość zmiennej przyjmuje się adres następnego
elementu listy. Adres ten jest przechowywany w polu next
elementu bieżącego. Listę przechodzimy do momentu, aż zmienna wskazująca
przyjmie wartość 0. Stanie się tak po wyjściu z ostatniego elementu
listy, w którego polu next przechowywany jest adres 0.
Algorytm przechodzenia przez listę jednokierunkowąWejście
Wyjście:Przejście przez wszystkie elementy listy Lista kroków:
Liczba elementów listy: l_sizeProblem liczby
elementów listy rozwiązujemy za pomocą algorytmu przejścia przez listę.
Tworzymy licznik, któremu na początku nadajemy wartość 0. Następnie
przechodzimy kolejno przez wszystkie elementy na liście. Przy każdym
mijanym elemencie zwiększamy licznik o 1. Po przejściu całej listy w
liczniku będzie liczba elementów.
Algorytm wyznaczania liczby elementów na liście jednokierunkowejWejście
Wyjście:
Lista kroków:
Dodawanie elementu na początku listy: l_push_front
Algorytm dołączania nowego elementu na początek listy jednokierunkowejWejście
Wyjście:Lista z dołączonym na początku elementem o wartości v. Dane pomocnicze:
Lista kroków:
Uwaga: Elementy są umieszczone na liście w kierunku odwrotnym do kolejności ich wstawiania – ostatnio wstawiony element jest pierwszym elementem listy. Taka struktura pozwala prosto realizować stos.
Usuwanie elementu z początku listy: l_pop_front
Algorytm usuwania elementu z początku listy jednokierunkowejWejście
Wyjście:Lista bez pierwszego elementu. Dane pomocnicze:
Lista kroków:
Dla prostych zastosowań podane powyżej operacje dla list jednokierunkowych zwykle wystarczają. W dalszej części artykułu rozszerzamy tę listę o kolejne operacje.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Operacje na listach dwukierunkowychTyp elementu listy oraz zmiennej obsługującej listę
Przejście przez listę
W porównaniu z listami jednokierunkowymi listy dwukierunkowe są bardziej
skomplikowane, ponieważ każdy ich element posiada dodatkowe pole prev.
Pole to wskazuje element poprzedni na liście. Zatem listy tego typu
można przechodzić w obu kierunkach. Z drugiej strony ta możliwość skraca
czas wykonania wielu operacji na liście, gdzie wymagany jest element
poprzedzający w stosunku do danego. W przypadku listy jednokierunkowej
element ten znajdowany był za pomocą przejścia od początku listy – w
liście dwukierunkowej mamy go danego bezpośrednio poprzez pole prev.
Zasada przechodzenia listy dwukierunkowej jest identyczna jak przy listach jednokierunkowych. Potrzebujemy wskaźnika, który będzie wskazywał poszczególne elementy listy. Wskazywany element przetwarzamy, po czym przechodzimy do następnego poprzez pole next lub do poprzedniego poprzez pole prev. Operację przejścia kontynuujemy dotąd, aż wskaźnik przyjmie wartość 0 (ostatni element listy ma w polu next wartość 0, pierwszy element listy ma w polu prev wartość 0 – zatem wyjście z ostatniego lub z pierwszego elementu poprzez next/prev skutkuje adresem zerowym).
Algorytm przechodzenia przez listę dwukierunkowąWejście
Wyjście:Przejście przez wszystkie elementy listy od pierwszego do ostatniego lub od ostatniego do pierwszego Dane pomocnicze:
Lista kroków : przejście w przód
Lista kroków : przejście wstecz
Liczba elementów listy: l_sizeDla listy
dwukierunkowej nie musimy posiadać osobnego algorytmy wyznaczania liczby
elementów, ponieważ pole count zmiennej obsługującej listę
przechowuje bezpośrednio tę informację.
Dodawanie elementu na początku listy: l_push_front
Algorytm dołączania nowego elementu na początek listy dwukierunkowejWejście
Wyjście:Lista z dołączonym na początku elementem o wartości v. Dane pomocnicze:
Lista kroków:
Dodawanie elementu na koniec listy: l_push_back
Algorytm dołączania nowego elementu do końca listy dwukierunkowejWejście
Wyjście:Lista z dołączonym na końcu nowym elementem o wartości v Dane pomocnicze:
Lista kroków:
Dodawanie nowego elementu przed wybranym elementem listy: l_insert_before
Algorytm dołączania nowego elementu przed wybranym elementem listy dwukierunkowejWejście
Wyjście:Lista z nowym elementem wstawionym przed element wskazywany przez e. Dane pomocnicze:
Lista kroków:
Dodawanie nowego elementu za elementem wybranym: l_insert_after
Algorytm dołączania nowego elementu za wybranym elementem listy dwukierunkowejWejście
Wyjście:Lista z nowym elementem wstawionym za elementem wskazywanym przez e. Dane pomocnicze:
Lista kroków:
Usuwanie wybranego elementu listy: l_remove
Algorytm usuwania wybranego elementu listy dwukierunkowejWejście
Wyjście:Lista bez elementu wskazywanego przez e. Lista kroków:
Usuwanie elementu z początku listy: l_pop_front
Nie musimy tworzyć osobnego algorytmu, wykorzystamy algorytm usuwania
wybranego elementu listy.
Algorytm usuwania elementu z początku listy dwukierunkowejWejście
Wyjście:Lista bez pierwszego elementu. Lista kroków:
Usuwanie elementu z końca listy: l_pop_backTutaj również wykorzystujemy algorytm ogólny.
Algorytm usuwania elementu z końca listy dwukierunkowejWejście
Wyjście:Lista bez ostatniego elementu. Lista kroków:
|
I Liceum Ogólnokształcące |
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe