Stos, kolejka i lista jednokierunkowa w Javie

Zaktualizowano 22 października 2026

Stos, kolejka i lista jednokierunkowa to najważniejsze struktury danych, które możesz zbudować sam w Javie. Zwykle przechowujesz dane w tablicach albo w gotowych kolekcjach, takich jak ArrayList. W tym artykule sam zbudujesz stos, kolejkę i listę jednokierunkową. Różnią się one przede wszystkim tym, gdzie elementy są dodawane i skąd są wyjmowane, a od tego zależy, jak szybka jest każda operacja.

LIFO i FIFO

Stos (stack) działa według zasady LIFO, Last In, First Out: co położysz na wierzch jako ostatnie, zdejmujesz jako pierwsze. Operacje nazywają się push (połóż na wierzch), pop (zdejmij) i peek (tylko podejrzyj). Stosy są wszędzie tam, gdzie coś się cofa albo zagnieżdża: przycisk Wstecz w przeglądarce, Ctrl+Z, sprawdzanie nawiasów, wywołania metod w rekurencji.

Kolejka (queue) działa według zasady FIFO, First In, First Out: kto pierwszy przyszedł, ten pierwszy obsłużony. Operacje nazywają się enqueue (ustaw się na końcu) i dequeue (wyjmij z początku). Przykłady to zadania drukowania albo zapytania do serwera.

ArrayDeque z java.util potrafi jedno i drugie. W przykładzie te same trzy litery wychodzą raz w odwrotnej kolejności, a raz w kolejności dodania.

import java.util.ArrayDeque;import java.util.Deque;
public class Main {    public static void main(String[] args) {        Deque<String> stack = new ArrayDeque<>();        stack.push("A");        stack.push("B");        stack.push("C");        System.out.print("Stack: ");        while (!stack.isEmpty()) {            System.out.print(stack.pop() + " ");        }        System.out.println();
        Deque<String> queue = new ArrayDeque<>();        queue.offer("A");        queue.offer("B");        queue.offer("C");        System.out.print("Queue: ");        while (!queue.isEmpty()) {            System.out.print(queue.poll() + " ");        }        System.out.println();    }}

Stos na tablicy

Do własnego stosu wystarczą tablica i licznik size. size jest jednocześnie indeksem następnego wolnego miejsca, a element na wierzchu leży pod data[size - 1].

  • push: zapisz do data[size], potem size++.
  • pop: size--, potem zwróć data[size]. Nic nie jest usuwane, miejsce po prostu uznaje się za wolne.

Żadna z tych operacji nie potrzebuje pętli, bez względu na to, ile elementów leży na stosie. Ich złożoność to O(1), czyli stała. Co dzieje się przy pustym albo pełnym stosie, musisz ustalić sam: tutaj rzucany jest wyjątek IllegalStateException.

public class Main {    static int[] data = new int[10];    static int size = 0;
    static void push(int value) {        if (size == data.length) {            throw new IllegalStateException("Stack is full");        }        data[size] = value;        size++;    }
    static int pop() {        if (size == 0) {            throw new IllegalStateException("Stack is empty");        }        size--;        return data[size];    }
    public static void main(String[] args) {        push(1);        push(2);        push(3);        System.out.println(pop());        System.out.println(pop());        push(9);        System.out.println(pop());        System.out.println("Size: " + size);    }}

Kolejka jako pierścień

W kolejce na tablicy elementy dochodzą na końcu i odchodzą z początku. Gdybyś po każdym dequeue przesuwał wszystkie elementy o jedno miejsce do przodu, kosztowałoby to O(n). Zamiast tego przesuwa się początek: head wskazuje na pierwszy element, a count liczy elementy.

Gdy indeks dojdzie do końca tablicy, wraca do 0. Załatwia to operator modulo:

int tail = (head + count) % data.length;   // next free slothead = (head + 1) % data.length;           // after dequeue

W ten sposób tablica staje się buforem cyklicznym (ring buffer), a zarówno enqueue, jak i dequeue pozostają O(1). W przykładzie 40 i 50 trafiają z powrotem na indeksy 0 i 1, bo tam zwolniło się już miejsce.

public class Main {    static int[] data = new int[3];    static int head = 0;    static int count = 0;
    static void enqueue(int value) {        if (count == data.length) {            throw new IllegalStateException("Queue is full");        }        int tail = (head + count) % data.length;        data[tail] = value;        count++;        System.out.println("enqueue " + value + " at index " + tail);    }
    static int dequeue() {        if (count == 0) {            throw new IllegalStateException("Queue is empty");        }        int value = data[head];        head = (head + 1) % data.length;        count--;        return value;    }
    public static void main(String[] args) {        enqueue(10);        enqueue(20);        enqueue(30);        System.out.println("dequeue " + dequeue());        System.out.println("dequeue " + dequeue());        enqueue(40);        enqueue(50);        while (count > 0) {            System.out.println("dequeue " + dequeue());        }    }}

Listy wiązane

Lista jednokierunkowa składa się z węzłów. Każdy węzeł zna swoją wartość i następny węzeł (next), a ostatni wskazuje na null. Sama lista pamięta tylko head, czyli pierwszy węzeł.

Przechodzi się ją zawsze tak samo:

Node current = head;while (current != null) {    // use current.value    current = current.next;}

Tablica i lista wiązana w porównaniu:

  • Odczyt elementu o indeksie i: tablica O(1), lista O(n), bo musisz iść węzeł po węźle.
  • Wstawianie na początek: tablica O(n), bo wszystko trzeba przesunąć, lista O(1) z nowym head.
  • Wstawianie na koniec: tablica O(1), dopóki jest miejsce, lista O(n), a z referencją tail O(1).
  • Rozmiar: tablica ma stały rozmiar, lista rośnie dowolnie.

Stos da się zbudować na jednym i na drugim. W kolejce na węzłach pamięta się dodatkowo tail, żeby enqueue nie musiało iść aż do końca.

public class Main {    static class Node {        int value;        Node next;
        Node(int value) {            this.value = value;        }    }
    public static void main(String[] args) {        Node head = new Node(3);        head.next = new Node(5);        head.next.next = new Node(8);
        Node node = new Node(1);        node.next = head;        head = node;
        int sum = 0;        Node current = head;        while (current != null) {            System.out.print(current.value + " -> ");            sum += current.value;            current = current.next;        }        System.out.println("null");        System.out.println("Sum: " + sum);    }}

Typowe błędy

  • Wyjście poza null

    Ostatni węzeł ma next == null. Jeśli w pętli odczytujesz current.next.value albo przy pustej liście sięgasz do head.next, dostajesz NullPointerException. Sprawdź najpierw, czy head == null, i dobierz odpowiedni warunek: current != null, żeby przejść przez wszystkie węzły, current.next != null, żeby zatrzymać się na ostatnim.

  • Zgubiona referencja

    Węzeł, na który nic już nie wskazuje, jest stracony na zawsze. Dlatego przy przepinaniu ważna jest kolejność:

    current.next = previous;      // the rest of the list is lostNode next = current.next;     // too late

    Zapamiętaj current.next, zanim go nadpiszesz. A gdy kolejka na węzłach staje się pusta, ustaw też tail na null, bo inaczej następne enqueue doczepi element do starego węzła.

  • Pomyłka o jeden przy zawijaniu

    W buforze cyklicznym ostatni poprawny indeks to data.length - 1. Własne zawijanie z if łatwo może pójść nie tak:

    if (tail == data.length - 1) {    tail = 0;}tail++;   // after the wrap-around, tail becomes 1

    Indeks 0 zostaje pominięty, a kolejka nadpisuje zajęte miejsca. Bezpieczna jest forma z modulo: tail = (tail + 1) % data.length.

Zadania z tego tematu

Przećwicz to, czego się nauczyłeś, od razu w przeglądarce. Testy natychmiast sprawdzają twoje rozwiązanie.

Powiązane artykuły