Zum Hauptinhalt springen

DoublyLinkedList

Doppelt verkettete Listenimplementierung mit effizienten Operationen an beiden Enden und bidirektionalem Durchlauf. Jeder Knoten hält Referenzen sowohl zum vorherigen als auch zum nächsten Knoten.

Installation

npm install @msnkr/data-structures

Verwendung

import { DoublyLinkedList } from '@msnkr/data-structures';

const list = new DoublyLinkedList<number>();

API-Referenz

Eigenschaften

  • size: number - Anzahl der Elemente in der Liste
  • isEmpty(): boolean - Ob die Liste leer ist

Methoden

Elemente hinzufügen

// Am Ende hinzufügen - O(1)
list.append(1);

// Am Anfang hinzufügen - O(1)
list.prepend(0);

// An Position einfügen - O(n)
list.insertAt(2, 1);
Leistung

Einfügungen sowohl am Anfang als auch am Ende erfolgen in O(1) konstanter Zeit.

Elemente entfernen

// Vom Anfang entfernen - O(1)
const first = list.removeFirst();

// Vom Ende entfernen - O(1)
const last = list.removeLast();

// An Position entfernen - O(n)
const element = list.removeAt(1);

// Erstes Vorkommen eines Werts entfernen - O(n)
const removed = list.remove(42);
O(1) Entfernen an beiden Enden

Anders als bei einfach verketteten Listen unterstützt DoublyLinkedList O(1) Entfernen vom Ende dank Rückwärtszeigern.

Auf Elemente zugreifen

// Element an Position abrufen - O(min(n/2, k))
const element = list.get(0);

// Position eines Elements finden - O(n)
const index = list.indexOf(42);

// Element-Existenz prüfen - O(n)
const exists = list.contains(42);
Optimierter Zugriff

Die get()-Methode ist optimiert und sucht vom nächstgelegenen Ende (Kopf oder Ende), wodurch der Zugriff auf Indizes nahe dem Ende 2x schneller ist als bei einfach verketteten Listen.

Iteration

const list = new DoublyLinkedList<string>();
list.append('a');
list.append('b');
list.append('c');

// Vorwärtsiteration (Kopf zu Ende)
for (const value of list) {
console.log(value); // "a", "b", "c"
}

// Rückwärtsiteration (Ende zu Kopf)
for (const value of list.reverseIterator()) {
console.log(value); // "c", "b", "a"
}

Hilfsmethoden

// In Array konvertieren - O(n)
const array = list.toArray();

// Alle Elemente entfernen - O(1)
list.clear();

Beispiele

Grundlegende Verwendung mit bidirektionalem Durchlauf

const list = new DoublyLinkedList<number>();

// Elemente hinzufügen
list.append(1); // [1]
list.append(2); // [1, 2]
list.append(3); // [1, 2, 3]

// Vorwärts durchlaufen
console.log('Vorwärts:', [...list]); // [1, 2, 3]

// Rückwärts durchlaufen
console.log('Rückwärts:', [...list.reverseIterator()]); // [3, 2, 1]

Browser-Verlaufsimplementierung

class BrowserHistory {
private history = new DoublyLinkedList<string>();
private currentIndex = -1;

visit(url: string): void {
// Vorwärtsverlauf entfernen
while (this.currentIndex < this.history.size - 1) {
this.history.removeLast();
}

this.history.append(url);
this.currentIndex++;
}

back(): string | null {
if (this.currentIndex > 0) {
this.currentIndex--;
return this.history.get(this.currentIndex);
}
return null;
}

forward(): string | null {
if (this.currentIndex < this.history.size - 1) {
this.currentIndex++;
return this.history.get(this.currentIndex);
}
return null;
}
}

const browser = new BrowserHistory();
browser.visit('google.com');
browser.visit('github.com');
browser.visit('stackoverflow.com');

console.log(browser.back()); // "github.com"
console.log(browser.back()); // "google.com"
console.log(browser.forward()); // "github.com"

Musik-Playlist mit Shuffle

const playlist = new DoublyLinkedList<string>();

playlist.append('Song 1');
playlist.append('Song 2');
playlist.append('Song 3');
playlist.append('Song 4');

// Vorwärts abspielen
console.log('Vorwärts abspielen:');
for (const song of playlist) {
console.log(`${song}`);
}

// Rückwärts abspielen
console.log('\nRückwärts abspielen:');
for (const song of playlist.reverseIterator()) {
console.log(`${song}`);
}

Texteditor Rückgängig/Wiederholen

class TextEditor {
private states = new DoublyLinkedList<string>();
private currentIndex = -1;

constructor() {
this.states.append(''); // Initialer leerer Zustand
this.currentIndex = 0;
}

type(text: string): void {
const currentState = this.states.get(this.currentIndex) || '';
const newState = currentState + text;

// Gesamten Wiederholungsverlauf entfernen
while (this.currentIndex < this.states.size - 1) {
this.states.removeLast();
}

this.states.append(newState);
this.currentIndex++;
}

undo(): string {
if (this.currentIndex > 0) {
this.currentIndex--;
}
return this.states.get(this.currentIndex) || '';
}

redo(): string {
if (this.currentIndex < this.states.size - 1) {
this.currentIndex++;
}
return this.states.get(this.currentIndex) || '';
}

getText(): string {
return this.states.get(this.currentIndex) || '';
}
}

const editor = new TextEditor();
editor.type('Hello');
editor.type(' World');
console.log(editor.getText()); // "Hello World"
console.log(editor.undo()); // "Hello"
console.log(editor.undo()); // ""
console.log(editor.redo()); // "Hello"

LRU-Cache-Implementierung

class LRUCache<K, V> {
private capacity: number;
private cache = new Map<K, V>();
private order = new DoublyLinkedList<K>();

constructor(capacity: number) {
this.capacity = capacity;
}

get(key: K): V | undefined {
if (!this.cache.has(key)) {
return undefined;
}

// Zu zuletzt verwendet verschieben
this.order.remove(key);
this.order.append(key);

return this.cache.get(key);
}

put(key: K, value: V): void {
if (this.cache.has(key)) {
this.order.remove(key);
} else if (this.cache.size >= this.capacity) {
// Am wenigsten verwendeten entfernen
const lru = this.order.removeFirst();
if (lru !== undefined) {
this.cache.delete(lru);
}
}

this.cache.set(key, value);
this.order.append(key);
}
}

const cache = new LRUCache<string, number>(3);
cache.put('a', 1);
cache.put('b', 2);
cache.put('c', 3);
console.log(cache.get('a')); // 1
cache.put('d', 4); // Entfernt 'b'

Zeitkomplexität

OperationDurchschnittSchlimmstenfalls
appendO(1)O(1)
prependO(1)O(1)
insertAtO(n)O(n)
removeFirstO(1)O(1)
removeLastO(1)O(1)
removeAtO(n)O(n)
removeO(n)O(n)
getO(n)O(n)
indexOfO(n)O(n)
containsO(n)O(n)

Vergleich mit einfach verketteter Liste

MerkmalDoublyLinkedListLinkedList
Entfernen vom EndeO(1)O(n)
RückwärtsiterationUnterstütztNicht unterstützt
Speichernutzung2 Zeiger pro Knoten1 Zeiger pro Knoten
Zugriff auf Elemente nahe EndeSchneller (vom Ende aus)Langsamer (nur vom Kopf aus)

Best Practices

Wann DoublyLinkedList verwenden

  • ✅ Bidirektionaler Durchlauf erforderlich
  • ✅ Häufiges Entfernen von beiden Enden
  • ✅ Implementierung von LRU-Caches oder Rückgängig/Wiederholen
  • ✅ Zugriff auf Elemente nahe dem Ende erforderlich

Wann LinkedList (einfach verkettet) verwenden

  • ✅ Nur Vorwärtsdurchlauf erforderlich
  • ✅ Speicher ist begrenzt
  • ✅ Selten oder nie vom Ende entfernen

Verwandte Datenstrukturen

  • LinkedList - Einfach verkettete Liste mit geringerer Speichernutzung
  • Deque - Array-basierte doppelendige Warteschlange mit schnellerem Zugriff
  • Queue - FIFO-Warteschlange mit Operationen nur an einem Ende