Nasza lista jest zbudowana z dwóch klas.
Aby pokazać różnice pomiędzy listami dwukierunkowymi a jednokierunkowymi postanowiliśmy opracować własną implementację. Ponownie naszym celem jest pokazanie działania tej struktury danych, a nie możliwości interfejsów, takich jak Comparable lub Iterable. Aby maksymalnie uprościć kod przechowywanym typem danych będzie int.
Naszym celem było napisanie jak najprostszego algorytmu, aby pokazać jak operować na listach jednokierunkowych. Tak, aby w praktyczny sposób przekazać wiedzę z naszego teoretycznego wstępu odnośnie list jednokierunkowych. Na pierwszy rzut oka implementacja jest bardzo podobna do listy dwukierunkowej, jednakże w przypadku usuwania węzła jesteśmy zmuszeni utworzyć tymczasową zmienną, która będzie go przechowywać. W przeciwnym wypadku stracimy możliwość np. zmiany referencji w poprzednim ogniwie listy, gdyż aktualnie usuwany element „nie zna” położenie swojego poprzednika.
Aby uruchomić poniższy kod należy skopiować kod, a następnie skompilować.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
package pl.programistajava.listaJednokierunkowa; public class Lista { private Wezel head; class Wezel { private Wezel next; private int data; public Wezel(int d){ next = null; data = d; } } public Lista() { this.head = null; } public static void main(String[] args){ Lista list = new Lista(); list.addWezel(1); list.addWezel(2); list.addWezel(3); list.wypiszListe(); System.out.println(); list.removeWezel(1); list.wypiszListe(); } public void wypiszListe() { Wezel curr = this.head; System.out.print("[ "); while(curr != null) { System.out.print(curr.data + " "); curr = curr.next; } System.out.print("]"); } public Wezel getWezel(int n) { if(n < 0) { throw new IndexOutOfBoundsException("Nie można podać ujemnego argumentu"); } int i = 0; Wezel curr = this.head; while(curr != null && i++ < n) { curr = curr.next; } if (curr == null) { throw new IndexOutOfBoundsException("Nie istnieje obiekt o podanym argumencie."); } return curr; } public void addWezel(int s) { Wezel curr = head; Wezel newWezel = new Wezel(s); if(curr == null) { this.head = newWezel; }else { while(curr.next != null) { curr = curr.next; } curr.next = newWezel; } } public void removeWezel(int n) { Wezel curr = head; Wezel prev = null; int i = 0; if(curr == null) { //gdy lista jest pusta throw new NullPointerException("Lista jest pusta."); }else { while(curr.next != null && i++ < n) { prev = curr; curr = curr.next; } } if(n > i) { throw new IndexOutOfBoundsException("Nie istnieje obiekt o podanym argumencie"); } if(prev == null) { //gdy usuwamy głowę this.head = curr.next; }else if(curr.next == null) { //gdy usuwamy ogon prev.next = null; }else { //gdy usuwamy środek prev.next = curr.next; } } } |
Powyższy program można podzielić na dwie części. Pierwszą jest obiekt, który przechowuje dane i jest nią klasa Wezel. Ze względu na to, że ta struktura danych jest mało elastyczna umieściliśmy ją wewnątrz klasy Lista, gdyż i tak nie wykorzystamy jej nigdzie indziej. Takie podejście jest wygodnie, ponieważ mamy ułatwiony dostęp do pól obu klas.
Obiekt klasy Wezel zawiera jedną referencję do kolejnego obiektu oraz zmienną przechowującą dane. Jeśli dany węzeł jest ostatni (ogon), to jego referencją na następny obiekt jest wartość null.
Inicjuje dane – referencję do następnego obiektu na null, a pole z danymi uzupełnia wartością przekazaną w argumencie.
Obiekt klasy Lista przechowuje referencję do pierwszego elementu listy, czyli głowę (ang. head). Zawiera również niezbędne algorytmy, takie jak:
W metodzie main znajduje się przykładowy przebieg programu. Wykonanie metody main spowoduje wyświetlenie następujących komunikatów w konsoli.
1 2 |
[ 1 2 3 ] [ 1 3 ] |
Zachęcamy do eksperymentów z kodem.
Podstawową różnicą między implementacją listy dwukierunkowej jest algorytm usuwania elementu. Żaden węzeł nie zna położenia swojego poprzednika zatem przy przechodzeniu listy musimy utworzyć zmienną pomocniczą, którą będzie przechowywała jego referencję.
Czemu jednak nie postąpić jak w przypadku list dwukierunkowych i do zmiennej pomocniczej nie użyć po prostu argumentu n-1? Przecież byłoby to dużo wygodniejsze i kod zajmowałby mniej linii. Powodem jest wydajność. Zwróćmy uwagę, że metoda getWezel() byłaby wywołana dwa razy – raz z argumentem n, a za drugim razem n-1. Jednakże dwukrotnie przejście przez listę zaczynałoby się od głowy listy. Oznacza to podwojenie liczby wykonywanych operacji. W przypadku małej kolekcji oczywiście nie ma to znaczenia, ale kolekcja może składać się z bardzo wielu elementów.
Jeżeli spróbujemy wpisać do metody szukającej indeks ujemny lub spoza naszej listy zostanie wyrzucony wyjątek.
W Javie nie ma potrzeby ręcznego czyszczenia pamięci. Jeśli nie mamy już referencji do obiektu, to nie musimy z tym nic robić. System zbierania nieużytków oczyści pamięć ze zbędnych obiektów.
Nasza lista jest zbudowana z dwóch klas.
Wezel
– klasa wewnętrzna – reprezentuje pojedynczy węzeł na liście.Lista
– klasa zewnętrzna – zawiera algorytmy dodawania, usuwania oraz wyświetlania elementów oraz przechowuje pierwszy element listy.Lista ta powstała w celu dydaktycznym, aby pokazać jak wygląda własne opracowanie algorytmu. W związku z tym nie implementujemy żadnych interfejsów, takich jak Comparable
, który mógłby posłużyć do sortowania elementów. Z tego samego powodu nie jest to również klasa generyczna, dlatego nie można w tej kolekcji przechowywać żadnych typów własnych, ani innych typów prostych niż int.
Naszym celem było napisanie jak najprostszego algorytmu, aby skupić się na działaniu tej konkretnej struktury danych. Tak, aby w praktyczny sposób przekazać wiedzę z naszego teoretycznego wstępu odnośnie list dwukierunkowych.
Typem danych do przechowywania jest int. Aby uruchomić poniższy kod należy skopiować kod, a następnie skompilować.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
package pl.programistajava.lista; public class Lista { private Wezel head; class Wezel { private Wezel next; private Wezel prev; private int data; public Wezel(int d){ next = null; prev = null; data = d; } } public Lista() { this.head = null; } public static void main(String[] args){ Lista list = new Lista(); list.addWezel(1); list.addWezel(2); list.addWezel(3); list.wypiszListe(); System.out.println(); list.removeWezel(1); list.wypiszListe(); } public void wypiszListe() { Wezel w = this.head; System.out.print("[ "); while(w != null) { System.out.print(w.data + " "); w = w.next; } System.out.print("]"); } public Wezel getWezel(int n) throws IndexOutOfBoundsException{ if(n < 0) { throw new IndexOutOfBoundsException("Nie można podać ujemnego argumentu"); } int i = 0; Wezel w = this.head; while(w != null && i++ < n) { w = w.next; } if (w == null) { throw new IndexOutOfBoundsException("Nie istnieje obiekt o podanym argumencie."); } return w; } public void addWezel(int s) { Wezel w = head; Wezel last = null; Wezel tmp = new Wezel(s); while(w != null) { last = w; w = w.next; } if(last == null) { this.head = tmp; }else { last.next = tmp; tmp.prev = last; } } public void removeWezel(int n) { Wezel w = getWezel(n); if(w.prev == null) { //gdy usuwamy głowę this.head = w.next; this.head.prev = null; } else if(w.next == null) { //gdy usuwamy ogon w = w.prev; w.next = null; } else { //gdy usuwamy środek w.prev.next = w.next; w.next.prev = w.prev; } } } |
Powyższy program można podzielić na dwie części. Pierwszą jest obiekt, który przechowuje dane i jest nią klasa Wezel
. Ze względu na to, że ta struktura danych jest mało elastyczna umieściliśmy ją wewnątrz klasy Lista, gdyż i tak nie wykorzystamy jej nigdzie indziej. Takie podejście jest wygodnie, ponieważ mamy ułatwiony dostęp do pól obu klas.
Obiekt klasy Wezel
zawiera dwie referencje do innych obiektów oraz zmienną przechowującą dane.
Inicjuje dane – pola obiektowe czyli referencje do poprzednie i następnego obiektu ustawia na null, a pole z danymi uzupełnia wartością przekazaną w argumencie.
Obiekt klasy Lista
przechowuje referencję do pierwszego elementu listy, czyli głowę (ang. head). Zawiera również niezbędne algorytmy, takie jak:
W metodzie main()
znajduje się przykładowy przebieg programu. Dane zostały dobrane w taki sposób, aby nie spowodować wyjątku. Wykonanie metody main()
spowoduje wyświetlenie następujących komunikatów w konsoli.
1 2 |
[ 1 2 3 ] [ 1 3 ] |
Zachęcamy do eksperymentów z kodem.
Wielokrotne wykorzystanie kodu pozytywnie wpływa na jego objętość. Lista dwukierunkowa pozwala na ponowne wykorzystanie metody getWezel()
w celu znalezienia usuwanego węzła zamiast pisać ponownie kod, który robi tę samą czynność.
Jeżeli spróbujemy wpisać do metody szukającej indeks ujemny lub spoza naszej listy zostanie wyrzucony wyjątek.
Być może programiści innych języków zwrócili uwagę na to, że po usunięciu elementów nie czyścimy ręcznie pamięci. W językach z rodziny C oprócz zmiany wskaźników na odpowiednie elementy należy użyć funkcji free(), która czyści pamięć – w przeciwnym wypadku po wyjściu z funkcji usuwania elementu stracimy wskaźnik na usuwany element i nigdy już go nie będziemy w stanie usunąć – do restartu maszyny.
W Javie jeśli nie mamy dostępu do obiektu, to nie musimy z tym nic robić. System zbierania nieużytków zrobi wszystko za nas i oczyści pamięć ze zbędnych obiektów.
Polecamy również zapoznać się z implementacją listy jednokierunkowej. Pozornie obie wydają się podobne, jednakże różnią się znacznie przy samej budowie.
W jednym z naszych wpisów poznaliśmy bliskiego krewnego tablic, a mianowicie klasę ArrayList
. Jej budowę, sposób działania oraz metody, które pozwalają na praktyczne korzystanie z niej. Implementacja klasy ArrayList
daje świetne efekty jeśli chodzi o wyszukiwanie elementów(wszystkie są obok siebie) oraz zamortyzowany koszt dodawania elementów (pojemność kolekcji jest nieco większa niż liczba elementów). Jednak problemem jest dodawanie wielu elementów oraz usuwanie ze środka listy. Wpis jest częścią artykułów o kolekcjach w Javie.
W tym artykule przyjrzymy się gotowym kolekcjom opartych na interfejsie List
. Obie omawiane klasy implementują go oraz interfejs Iterable
w związku z tym ich obsługa z puntu widzenia programisty będzie bardzo podobna. Różnice polegają głównie na sposobie reprezentacji danych w pamięci i algorytmów obsługi takich jak dodawanie elementów, odczyt czy też usuwanie. Wpis jest częścią artykułów o kolekcjach w Javie.
Naszą podróż po kolekcjach w Javie zaczniemy od Klas implementujących interfejs List czyli od list.
Listy są liniowymi strukturami danych o dynamicznym rozmiarze co oznacza, że mogą zmieniać swój rozmiar w czasie działania programu (w przeciwieństwie do tablic). Najważniejszymi pojęciami, które należy znać są:
W zależności od tego czy lista jest jednokierunkowa czy dwukierunkowa, ostatni element jest opcjonalny.
Poprzedni wpis dotyczył interfejsów.
W języku C standardowo jesteśmy ograniczeni do tablic, które w momencie kompilacji muszą mieć zdefiniowany rozmiar. Java w swojej bibliotece standardowej posiada wiele implementacji interfejsów kolekcji np. List, które mogą przyjmować elementy różnych typów. Są to implementacje różnych znanych struktur danych, takich jak lista łączona dwukierunkowa (ang. Linked List) i lista tablicowa (ang. Array List).
Read More