Lineare Listen (=Einfach verkettete Listen)

Einfach verkettete Listen oder linked lists sind eine fundamentale Datenstruktur, die ich hier anhand von Code-Beispielen und Grafiken erklären will.

Einfach verkettete Listen zeichnen sich dadurch aus, dass man besonders einfach Elemente einfügen kann, wodurch sie sich besonders gut für Insertion Sort eignen.

Listen vs. Arrays

Listen (besonders verkettete) bieten gegenüber Arrays den großen Vorteil der dynamischen Größenanpassung und effizienter Einfüge-/Löschoperationen (oft mit konstantem Aufwand), da sie nicht an feste Speicherblöcke gebunden sind und sich zur Laufzeit anpassen können, was Speicherplatzverschwendung reduziert; Arrays punkten hingegen bei direktem Zugriff auf Elemente (Index-basiert) und besserer Speicher-Performance bei statischen Datenmengen.

Vorteile von verketteten Listen

Dynamische Größe

Listen können zur Laufzeit wachsen oder schrumpfen, ohne dass der gesamte Speicher neu zugewiesen werden muss, was sie ideal für unbekannte Datenmengen macht.

Effizientes Einfügen/Löschen

Das Hinzufügen oder Entfernen von Elementen ist oft sehr schnell (konstanter Zeitaufwand), da nur Zeiger angepasst werden müssen, anstatt Daten zu verschieben.

Speichereffizienz bei dynamischen Daten

Sie vermeiden die Reservierung von zu viel oder zu wenig Speicher, da der Speicherbedarf sich an die tatsächliche Anzahl der Elemente anpasst.

Vorteile von Arrays

Schnellerer Zugriff

Der Zugriff auf ein Element über seinen Index ist sehr schnell, da der Speicher zusammenhängend ist.

Geringerer Speicher-Overhead

Arrays speichern nur die Daten selbst, ohne die zusätzlichen Zeiger, die verkettete Listen benötigen, was zu geringerem Speicherverbrauch führt.

Einfachheit

Für feste Datengrößen sind Arrays oft einfacher zu implementieren und zu handhaben.

Wann was verwenden?

Listen

Wenn sich die Anzahl der Elemente häufig ändert, Sie oft Elemente am Anfang, Ende oder in der Mitte einfügen oder löschen müssen, oder wenn die Anzahl der Elemente unbekannt ist.

Arrays

Wenn die Größe der Sammlung bekannt ist und sich nicht ändert und Sie häufig Elemente direkt über ihren Index ansprechen müssen (z.B. bei mathematischen Operationen

Implementierung

Eine einfach verkettete Liste besteht aus Knoten, Englisch nodes, die einen Zeiger auf das nächste Element und Daten beinhalten.

struct list{
 
	int data;
	list *next;
 
};

Eine leere Liste besteht aus einem Kopf (Head) und nichts sonst:

Wenn man mehrere Elemente einfügt, sieht das so aus:

Eine einfach verkettete Liste mit einem Kopf und zwei Knoten.

Knoten befüllen & einfügen

Wenn man einen Zeiger auf ein Element der Liste hat, ist es einfach, ein Element dahinter einzufügen.

Dazu muss man den next-Zeiger der Liste auf das neue Element setzen, und den next-Zeiger des neuen Element auf den alten Wert des next-Zeigers der Liste:

	//1. Knoten erstellen
	list *node1 = new List();
 
	//1. Knoten befüllen
	node1->data=1;    //oder (*node1).data=1;
	node1->next=NULL;

 
	//2. Knoten erstellen
	list *node2 = new List();;
  	node2->data=2;
	node2->next=NULL;
	//1. Knoten zeigt auf 2. Knoten
	node1->next=node2;

	//Knoten 12 zwischen Knoten 1 und 2 einfügen
	list *node12 = new List();;
 	node12->data=12;
	node12->next=node2;
	node1->next=node12;

Schritt 1

Schritt 2

Automatisiertes Hinzufügen von Knoten

Ein neuer Listenknoten wird durch Aufruf von new erzeugt. Dabei muss darauf geachtet werden, dass der Zeiger next gleich korrekt gesetzt wird. Wenn Sie nicht sofort den Nachfolger einhängen können, setzen Sie den Zeiger auf NULL.

#include <iostream>
 
 
using namespace std;
 
typedef struct list{
 
	int data;
	list *next;
 
};
 
int main(int argc, char** argv) {
 
 
	//Kopf der Liste
	list *head=NULL;
 
 
	//10 Knoten hinzufügen
	for(int i=1; i<10; i++)
	{
		list *node = new list;
		node->data=i;
		node->next=head;
		head=node;
	}
 
 
	//Knoten ausgeben
	list *help=NULL;
	help=head;
 
	while(help!=NULL)		//Solange Hilfszeiger nicht auf NULL zeigt
	{
		cout << help->data << endl;	
		help=help->next;
	}
 
        return 0;
}

Ausgeben, Einfügen und Löschen von Elementen mithilfe von Funktionen

//Datum: 30.09.2025
//Autor: Lahan
 
#include <iostream>
 
using namespace std;
 
struct Auto
{
	string marke = "";
	int tueren = 0;
	Auto* next = NULL;
};
 
//Funktionsdeklaration für das Einfügen eines Elements in die Liste
void insertElement(Auto *begin, string m, int t);
void printListe(Auto* begin);
void deleteElement(Auto** begin, string m); //Löscht ein Element anhand der eindeutigen Marke
 
//Funktionsdeklaration für das Ausgeben der Elemente der Liste
 
int main()
{
	Auto* start = new Auto();
 
 
 
	insertElement(start, "Mercedes", 3);
	//cout << start->marke;
	insertElement(start, "Dacia", 5);
	//cout << start->next->marke;
	insertElement(start, "BMW", 5);
	//cout << start->next->next->marke;
	insertElement(start, "Renault", 5);
	insertElement(start, "Peugeot", 5);
	insertElement(start, "Mustang", 5);
	insertElement(start, "Toyota", 5);
	insertElement(start, "VW", 5);
	insertElement(start, "Audi", 5);
	insertElement(start, "Porsche", 5);
 
	printListe(start);
 
	deleteElement(&start, "BMW");
 
	printListe(start);
 
	deleteElement(&start, "Mercedes");
 
	printListe(start);
 
	deleteElement(&start, "Porsche");
	printListe(start);
 
}
 
//Funktionsdefinition für das Einfügen eines Elements in die Liste
void insertElement(Auto* begin, string m, int t)
{
	Auto* hilf = NULL;
 
	//Wenn das erste Element noch nicht befüllt, dann füge bei begin die Werte ein
	if (begin->marke.length() == 0)
	{
		begin->marke = m;
		begin->tueren = t;
		begin->next = NULL;
	}
	else //Liste beinhaltet zumindest schon ein Element 
	{
		hilf = begin;
		while (hilf->next != NULL)
		{
			hilf = hilf->next;
		}
		hilf->next = new Auto();
		hilf->next->marke = m;
		hilf->next->tueren = t;
		hilf->next->next = NULL;
	}
 
}
 
void printListe(Auto* begin) 
{
	Auto* hilf = begin;
	int zaehler = 1;
 
	while (hilf != NULL)
	{
		cout << zaehler << ". Element: \t" << hilf->marke << "\t"
			 << hilf->tueren << " Tueren" << endl;
		hilf = hilf->next;
		zaehler++;
	}
}
 
void deleteElement(Auto** begin, string m)
{
	Auto* hilf = *begin, *hilf2 = NULL;
 
 
	//1. FALL - Das zu löschende Element ist das erste Element
	//Schritt 1: Prüfen ob das erste Element das zu löschende Element ist
	if ((*begin)->marke == m)
	{
		//Schritt 2: Speichere die Adresse des nachfolgenden Elements
		hilf = (*begin)->next;
 
		//Schritt 3: Löschen des Elements
		delete(*begin);
 
		//Schritt 4: Zeiger begin auf das "neue" erste Element zeigen lassen
		*begin = hilf;
	}
 
	//2. FALL - Element ist zwischen zwei anderen Elementen 
	//Schritt 1: Finde die Position vor dem zu löschenden Element
	while (hilf->next != NULL)
	{
		if (hilf->next->marke == m)
		{
			cout << endl << "Zu loeschendes Element:" << hilf->next->marke << endl << endl;
 
			//Lösche das Element und verbinde das vorherige Element mit dem nachfolgenden
			//Schritt 2: Speichere die Adresse des nachfolgenden Elements zwischen (hilf2)
			hilf2 = hilf->next->next;
 
			//Schritt 3: Löschen des Elements
			delete(hilf->next);
 
			//Schritt 4: Einfach verkettete Liste wieder verbinden
			hilf->next = hilf2;
 
			break;
		}
		hilf = hilf->next;
	}
 
}

Mögliche Speicherabbildung im Hauptspeicher

main()

insertElement()

deleteElement()