Listy dwukierunkowe - implementacja i zastosowania
1. Czym jest lista dwukierunkowa?
Lista dwukierunkowa (doubly linked list) to struktura danych, gdzie każdy węzeł zawiera:
- Dane (wartość)
- Wskaźnik do następnego węzła (next)
- Wskaźnik do poprzedniego węzła (prev)
Wizualizacja:
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
None ← │ 3 │ ←──→ │ 7 │ ←──→ │ 1 │ ←──→ │ 9 │ → None
│prev │ │prev │ │prev │ │prev │
│data │ │data │ │data │ │data │
│next │ │next │ │next │ │next │
└─────┘ └─────┘ └─────┘ └─────┘
HEAD TAIL
Różnice vs lista jednokierunkowa:
| Cecha | Jednokierunkowa | Dwukierunkowa |
|---|---|---|
| Wskaźniki w węźle | 1 (next) | 2 (prev, next) |
| Przechodzenie wstecz | Niemożliwe | Możliwe |
| Usuwanie węzła | O(n) | O(1) z węzłem |
| Zużycie pamięci | Mniejsze | Większe |
| Implementacja | Prostsza | Bardziej złożona |
2. Implementacja węzła
Tworzenie i łączenie węzłów:
3. Implementacja listy dwukierunkowej
4. Dodawanie elementów
4.1. Dodawanie na początku – O(1)
Krok po kroku:
1. Przed: HEAD → [7|↔] ↔ [1|↔] ↔ [9] ← TAIL
2. Nowy węzeł: [3|↔]
3. Łączenie:
[3] → [7]
[7] ← [3]
HEAD = [3]
4. Po: HEAD → [3|↔] ↔ [7|↔] ↔ [1|↔] ↔ [9] ← TAIL
4.2. Dodawanie na końcu – O(1)
Przykład:
4.3. Wstawianie po określonym węźle – O(1)
4.4. Wstawianie na określonej pozycji – O(n)
5. Usuwanie elementów
5.1. Usuwanie z początku – O(1)
5.2. Usuwanie z końca – O(1)
Uwaga: W liście jednokierunkowej pop_last() to O(n), tutaj O(1)!
5.3. Usuwanie określonego węzła – O(1)
To jest ogromna zaleta listy dwukierunkowej!
5.4. Usuwanie przez wartość – O(n)
6. Wyszukiwanie i dostęp
6.1. Wyszukiwanie węzła – O(n)
6.2. Dostęp po indeksie – O(n)
7. Odwracanie listy
7.1. Iteracyjne odwracanie – O(n)
Przykład:
8. Iterowanie w obu kierunkach
8.1. Iterator do przodu
8.2. Iterator do tyłu
Przykład:
9. Pełna implementacja
10. Złożoność czasowa
| Operacja | Jednokierunkowa | Dwukierunkowa |
|---|---|---|
prepend(data) |
O(1) | O(1) |
append(data) |
O(n)* lub O(1) | O(1) |
pop_first() |
O(1) | O(1) |
pop_last() |
O(n) | O(1) |
delete_node(node) |
O(n) | O(1) |
insert_after(node) |
– | O(1) |
reverse() |
O(n) | O(n) |
*bez tail pointer
Kluczowa przewaga: O(1) dla operacji na końcu i usuwania węzła!
11. Praktyczne zastosowania
11.1. Deque (Double-Ended Queue)
11.2. LRU Cache (Least Recently Used)
11.3. Historia przeglądarki (z nawigacją)
11.4. Edytor tekstu (Undo/Redo)
12. Lista cykliczna
Zastosowania: - Odtwarzacz muzyki (zapętlona playlista) - Round-robin scheduling - Bufory cykliczne
13. Porównanie z collections.deque
Python ma wbudowaną implementację deque:
Kiedy używać własnej implementacji? - Nauka struktur danych - Potrzebujesz dostępu do węzłów (LRU cache) - Specyficzne wymagania
Kiedy używać collections.deque? - Większość praktycznych zastosowań - Zoptymalizowana implementacja w C - Prosta i szybka
14. Typowe błędy
Błąd 1: Zapomnienie o aktualizacji prev
Błąd 2: Nieprawidłowa obsługa tail przy usuwaniu
15. Podsumowanie
Lista dwukierunkowa to rozszerzenie listy jednokierunkowej:
- Dwa wskaźniki w każdym węźle (prev, next)
- O(1) dla operacji na obu końcach
- Możliwość przechodzenia w obie strony
- Idealna do deque, LRU cache, undo/redo
Zalety:
- O(1) usuwanie z końca
- O(1) usuwanie dowolnego węzła (jeśli mamy do niego dostęp)
- Przechodzenie w obie strony
- Łatwiejsze operacje wstawiania/usuwania
Wady:
- Więcej pamięci (dodatkowy wskaźnik)
- Bardziej skomplikowana implementacja
- Więcej operacji przy modyfikacji
Kiedy używać:
- Deque (kolejka dwustronna)
- LRU Cache
- Historia z nawigacją (undo/redo)
- Często usuwasz z końca
Co dalej warto poznać:
- Skip list
- XOR linked list
- Sentinel nodes (węzły strażnicze)
- collections.deque w praktyce