V desátém dílu jsme si ukázali první kolekci – dynamické pole (ArrayList). Dnes si vytvoříme další kontejner – spojový seznam (LinkedList). Spojový seznam je jedna z nejvděčnějších datových struktur. Přestože je implementačně poměrně jednoduchý, je zároveň velmi použitelný – je základem mnoha implementací zásobníku, fronty, grafu a dalších datových struktur.

Dnes vytvoříme implementaci, jež nám zajistí základní funkcionalitu. Příště si ukážeme, jak seznam zobecnit pro ukládání objektů libovolného typu a jak zefektivnit jeho procházení pomocí for-each cyklu.

Co je to spojový seznam?

Spojový seznam je datový kontejner, který se skládá z mnoha lineárně provázaných uzlů. Každý uzel obsahuje část obsahující uloženou entitu a z ukazatel na další prvek (jednosměrně zřetězený seznam), případně i z ukazatel na prvek předchozí (obousměrně zřetězený seznam).

Jednosměrně zřetězený spojový seznam
Jednosměrně zřetězený spojový seznam

Rozhraní (spojového) seznamu

Nejprve si specifikujme operace, které musí splňovat každý seznam. Tato abstrakce nám v případě potřeby umožní nahradit spojový seznam jinou implementací (například již zmíněným dynamickým polem).

01./**
02. * Rozhrani, jez definuje operace, ktere musi splnit kazda implementace seznamu
03. * @author Pavel Micka
04. */
05.public interface ListIface {
06. 
07.    /**
08.     * Vlozi na konec seznamu prvek
09.     * @param i prvek k vlozeni
10.     */
11.    public void add(int i);
12. 
13.    /**
14.     * Vrati prvek na indexu i
15.     * @param i index prvku
16.     * @return prvek na indexu i
17.     */
18.    public int get(int i);
19. 
20.    /**
21.     * Smaze prvek na indexu i
22.     * @param i index mazaneho prvku
23.     */
24.    public void remove(int i);
25. 
26.    /**
27.     * Dotaz na delku seznamu
28.     * @return delka seznamu
29.     */
30.    public int size();
31.}

Implementace

Samotný spojový seznam budeme implementovat jako jednosměrně zřetězený s ukazateli na první a poslední prvek. Ukládané hodnoty budou primitivního typu int.

Třídní proměnné, konstruktory a vnitřní třída Node

01./**
02. * Jednosmerne zretezeny spojovy seznam
03. * @author Pavel Micka
04. */
05.public class MyLinkedList implements ListIface {
06. 
07.    private Node first;
08.    private Node last;
09.    private int size;
10. 
11.    /**
12.     * Konstruktor spojoveho seznamu
13.     */
14.    public MyLinkedList() {
15.        this.size = 0;
16.    }
17. 
18.    /**
19.     * Vnitrni trida reprezentujici jeden uzel spojoveho seznamu
20.     */
21.    private class Node {
22. 
23.        private int value;
24.        private Node next;
25. 
26.        private Node(int value) {
27.            this.value = value;
28.        }
29.    }
30.}

Třídu spojového seznamu pojmenujeme MyLinkedList, aby nedocházelo ke kolizi jmen s již předpřipravenou třídou java.util.LinkedList (dokumentace). Třída bude implementovat operace, které jsme si definovali v rozhraní ListIface.

Objekt spojového seznamu bude obsahovat ukazatel na první uzel first, ukazatel na poslední uzel last a hodnotu size obsahující počet prvků uložených v seznamu.

Reference na první uzel použijeme pro průchod skrz seznam (je jednosměrný). Reference na poslední prvek zjednoduší operaci ukládání nové hodnoty, protože nebudeme muset napřed proiterovat celý seznam (vkládáme nakonec). Obdobným způsobem proměnná size zjednodušší dotaz na délku seznamu.

Konstruktor obsahuje pouze jedinou operaci – inicializaci proměnné size na hodnotu 0.

Třída Node

Základem spojového seznamu je soukromá vnitřní třída Node. Tuto třídu jsme vytvořili jako vnitřní a soukromou, protože ji takto v duchu zásad zapouzdření (encapsulation) dokonale skryjeme před okolním světem.

Node obsahuje uloženou hodnotu value a referenci na další prvek next (nullový v případě, že se jedná o poslední uzel). Gettery a settery jednotlivých proměnných nejsou nevyhnutelně nutné, jelikož má k typu Node přístup pouze třída MyLinkedList (a ta může použít přímý přístup).

Metoda add

01./**
02. * Vlozi na konec seznamu prvek
03. * @param i prvek k vlozeni
04. */
05.public void add(int i) {
06.    Node n = new Node(i);
07.    if (size == 0) {
08.        this.first = n;
09.        this.last = n;
10.    } else {
11.        this.last.next = n;
12.        this.last = n;
13.    }
14.    size++;
15.}

Metoda add přidá zadanou hodnotu na konec seznamu. Jako první musíme vytvořit nový uzel seznamu pro předávanou hodnotu. Pokud je seznam prázdný, tak se nově přidávaný uzel stane prvním (first) a posledním (last) prvkem seznamu. Pokud není seznam prázdný, tak vybereme pomocí reference last poslední prvek seznamu a do jeho proměnné next vložíme referenci na nově přidávaný uzel. Jako poslední krok v každém případě inkrementujeme proměnnou size.

Metoda get

01./**
02. * Vrati prvek na indexu i
03. * @param i index prvku
04. * @throws IndexOutOfBoundsException pokud je i vyssi nebo rovna delce seznamu
05. * @throws IllegalArgumentException pokud je i zaporne
06. * @return prvek na indexu i
07. */
08.public int get(int i) {
09.    if (i >= size) {
10.        throw new IndexOutOfBoundsException("Mimo meze index:" + i + ", size:" + this.size);
11.    }
12.    if (i < 0) {
13.        throw new IllegalArgumentException("Index mensi nez 0");
14.    }
15.    Node curr = first;
16.    for (int j = 0; j < i; j++) {
17.        curr = curr.next;
18.    }
19.    return curr.value;
20.}

Metoda get vrátí prvek na zadaném indexu seznamu. Nejprve ošetříme chyby, které mohou nastat – předání buď příliš vysokého nebo příliš nízkého (záporného) indexu. Na obě chyby zareagujeme adekvátní nekontrolovanou výjimkou (jedná se o chybu programátora).

Nyní proiterujeme celý seznam až na daný index a vrátíme hodnotu uschovanou v daném uzlu.

Metoda remove

01./**
02. * Smaze prvek na indexu i
03. * @param i index mazaneho prvku
04. * @throws IndexOutOfBoundsException pokud je i vyssi nebo rovna delce seznamu
05. * @throws IllegalArgumentException pokud je i zaporne
06. */
07.public void remove(int i) {
08.    if (i >= size) {
09.        throw new IndexOutOfBoundsException("Mimo meze index:" + i + ", size:" + this.size);
10.    }
11.    if (i < 0) {
12.        throw new IllegalArgumentException("Index mensi nez 0");
13.    }
14.    if (i == 0) {
15.        first = first.next;
16.    } else {
17.        Node curr = first;
18.        for (int j = 0; j < i - 1; j++) { //najdeme predchozi
19.            curr = curr.next;
20.        }
21.        curr.next = curr.next.next; //a mazany prvek vynechame
22.        if (i == size - 1) { //pokud mazeme posledni
23.            last = curr;
24.        }
25.    }
26.    size--;
27.}

Při odstraňování prvku opět nejprve zkontrolujeme platnost zadaného indexu. Pokud mažeme první prvek, tak nám stačí změnit ukazatel na první prvek seznamu (first) na prvek následující. V ostatních případech nalezneme prvek, který je v seznamu před mazaným prvkem (označíme jej curr) a změníme jeho ukazatel na next na prvek, který je za mazaným prvkem (mazaný prvek vynecháme). Pokud jsme mazali poslední prvek, pak je nyní posledním prvkem curr. Na závěr dekrementujeme proměnnou size.

Pro úplnost si připomeňme, že poté, co na daný uzel zmizí poslední reference, bude odstraněn pomocí garbage collectoru.

Metoda size

1./**
2. * Dotaz na delku seznamu
3. * @return delka seznamu
4. */
5.public int size() {
6.    return this.size;
7.}

Metoda size pouze vrátí předpočítanou délku seznamu.

Metoda toString

01./**
02. * Klasicka toString metoda, vraci textovou reprezentaci objektu
03. * @return textova reprezentace objektu
04. */
05.@Override
06.public String toString() {
07.    StringBuilder builder = new StringBuilder();
08.    Node curr = first;
09.    for (int i = 0; i < this.size; i++) {
10.        builder.append(curr.value);
11.        builder.append(" ");
12.        curr = curr.next;
13.    }
14.    return builder.toString();
15.}

Nakonec ještě překryjeme metodu toString zděděnou z Object. Tato metoda vrací textovou reprezentaci daného objektu.

V této metodě využíváme StringBuilder (dokumentace), což je obdoba ArrayListu pro znaky. Kdybychom používali pouze operátor zřetězení +, tak bychom každým jeho použitím vytvořili nový řetězec, který bychom v zápětí zahodili (jakožto vstup nového zřetězení). Tento postup by byl velmi nehospodárný.


Kód

Takto zatím vypadá kompletní kód naší implementace spojového seznamu:

001./**
002. * Jednosmerne zretezeny spojovy seznam
003. * @author Pavel Micka
004. */
005.public class MyLinkedList implements ListIface {
006. 
007.    private Node first;
008.    private Node last;
009.    private int size;
010. 
011.    /**
012.     * Konstruktor spojoveho seznamu
013.     */
014.    public MyLinkedList() {
015.        this.size = 0;
016.    }
017. 
018.    /**
019.     * Vlozi na konec seznamu prvek
020.     * @param i prvek k vlozeni
021.     */
022.    public void add(int i) {
023.        Node n = new Node(i);
024.        if (size == 0) {
025.            this.first = n;
026.            this.last = n;
027.        } else {
028.            this.last.next = n;
029.            this.last = n;
030.        }
031.        size++;
032.    }
033. 
034.    /**
035.     * Vrati prvek na indexu i
036.     * @param i index prvku
037.     * @throws IndexOutOfBoundsException pokud je i vyssi nebo rovna delce seznamu
038.     * @throws IllegalArgumentException pokud je i zaporne
039.     * @return prvek na indexu i
040.     */
041.    public int get(int i) {
042.        if (i >= size) {
043.            throw new IndexOutOfBoundsException("Mimo meze index:" + i + ", size:" + this.size);
044.        }
045.        if (i < 0) {
046.            throw new IllegalArgumentException("Index mensi nez 0");
047.        }
048.        Node curr = first;
049.        for (int j = 0; j < i; j++) {
050.            curr = curr.next;
051.        }
052.        return curr.value;
053.    }
054. 
055.    /**
056.     * Smaze prvek na indexu i
057.     * @param i index mazaneho prvku
058.     * @throws IndexOutOfBoundsException pokud je i vyssi nebo rovna delce seznamu
059.     * @throws IllegalArgumentException pokud je i zaporne
060.     */
061.    public void remove(int i) {
062.        if (i >= size) {
063.            throw new IndexOutOfBoundsException("Mimo meze index:" + i + ", size:" + this.size);
064.        }
065.        if (i < 0) {
066.            throw new IllegalArgumentException("Index mensi nez 0");
067.        }
068.        if (i == 0) {
069.            first = first.next;
070.        } else {
071.            Node curr = first;
072.            for (int j = 0; j < i - 1; j++) { //najdeme predchozi
073.                curr = curr.next;
074.            }
075.            curr.next = curr.next.next; //a mazany prvek vynechame
076.            if (i == size - 1) { //pokud mazeme posledni
077.                last = curr;
078.            }
079.        }
080.        size--;
081.    }
082. 
083.    /**
084.     * Dotaz na delku seznamu
085.     * @return delka seznamu
086.     */
087.    public int size() {
088.        return this.size;
089.    }
090. 
091.    /**
092.     * Klasicka toString metoda, vraci textovou reprezentaci objektu
093.     * @return textova reprezentace objektu
094.     */
095.    @Override
096.    public String toString() {
097.        StringBuilder builder = new StringBuilder();
098.        Node curr = first;
099.        for (int i = 0; i < this.size; i++) {
100.            builder.append(curr.value);
101.            builder.append(" ");
102.            curr = curr.next;
103.        }
104.        return builder.toString();
105.    }
106. 
107. 
108.    /**
109.     * Vnitrni trida reprezentujici jeden uzel spojoveho seznamu
110.     */
111.    private class Node {
112. 
113.        private int value;
114.        private Node next;
115. 
116.        private Node(int value) {
117.            this.value = value;
118.        }
119.    }
120.}

Volání

01./**
02. * @param args the command line arguments
03. */
04.public static void main(String[] args) {
05.    ListIface l = new MyLinkedList();
06. 
07.    l.add(0);
08.    l.add(10);
09.    l.add(20);
10.    l.add(30);
11.    l.add(40);
12. 
13.    //vypiseme spojovy seznam pomoci toString metody
14.    System.out.println(l);
15.    System.out.println(l.get(2)); //20
16. 
17. 
18.    l.remove(1); //odstranime prvek na prvnim indexu
19.    System.out.println(l.get(2)); //30
20.     
21.    l.remove(0);
22.    System.out.println(l);
23.    System.out.println(l.size()); //3
24.}
1.0 10 20 30 40
2.20
3.30
4.20 30 40
5.3

V kódu volání používáme na levé straně přiřazení rozhraní ListIface. Stejně tak bychom postupovali v libovolném jiném případě, kdy by vytvářená entita měla vyhovující interface.

Tomuto postupu se říká programování proti rozhraní. Výhody jsou poměrně jasné – pokud se rozhodneme změnit implementační třídu, tak ji vyměníme na jediném místě (tam kde vytváříme instanci) a zbytek programu může zůstat totožný. Kdybychom programovali proti implementaci – implementační třída by se objevovala na levých stranách přiřazení, v hlavičkách metod a podobně – tak bychom v případě výměny této třídy museli přepsat skoro celý program.

Reference vs. ukazatel

Poznámka: Bylo mi vytknuto zaměňování termínů reference a ukazatel (které nejsou v C++ totožné). Jak je nám známo, tak Java má pouze reference a nemá ukazatele. Názvoslovný problém je v tom, že javovská reference výrazně více odpovídá C++ ukazateli (oproti C++ referenci).

Pokud tedy budu mluvit v tom seriálu a ukazatelích a referencích, tak tím budu mít vždy na mysli Java referenci.


SEO od společnosti Digital Pylon


Online casino s algoritmem

České casino online online slot-vegas.cz

Hrajte nejlepší hry jako je GoodGame Empire.





Zajímavé články: Jak najít práci snů? Zvolte kariéru v IT!, Češi mají rádi hrací automaty online, Jak funguje algoritmické obchodování Casino, Online výuka Algoritmus a online marketing mají svá pravidla, Automaty, Matematický vliv, Ratings, Jak fungují algoritmy hazardních her online: více znalostí, více peněz, SYPWAI - nástroj pro vědecký vývoj, Vynikají na globálním trhu: Nejlepší vývojáři softwaru pro online výherní automaty, Jak si vybrat nejlepší české online casino, Proč byste měli hrát online casino VPN revoluce, Kde najdeme algoritmy v každodenním životě?, Čeká vás pracovní pohovor mimo město? Podívejte se, jak dokonale zvládnout včasný příchod, 5 úžasných technologií ze světa hazardních her, Mirror and access to Mostbet, Svou kancelář můžete mít stále po ruce, Jaké výhody má digitalizovaná firma oproti off-line konkurenci?, Jaký systém vybrat pro snadné řízení výroby?, Nahradí umělá inteligence ajťáky?, Důvody, proč používat SnapTik ke stahování videí TikTok, Dokonalý den na pláži: Co si vzít s sebou, aby byl výlet zábavný a bezpečný?, Jak přežít dlouhý let?, Go pay GoodGame Empire, Blockchain, Rozhovor, Umělá inteligence, Ochranná známka pre softvér: Prečo ju registrovať?, Role kryptografických algoritmů v zabezpečení online kasin, Jaké jsou náklady na nákup 3D tiskárny?, Jak algoritmy vylepšují online zážitky v roce 2025,


Doporučujeme

Internet pro vaši firmu na míru

https://www.algoritmy.net