Podstawowe operacje na listach

Kurs: Wstęp do programowania
Lekcja 12: Struktury danych o dynamicznej budowie
Temat 3: Podstawowe operacje na listach

⇓ spis treści ⇓


Listy wskaźnikowe są dynamicznymi strukturami danych, które umożliwiają elastyczne zarządzanie pamięcią. Aby w pełni wykorzystać potencjał tych struktur, musimy znać i rozumieć podstawowe operacje, które można na nich wykonywać. W tej lekcji szczegółowo omówimy najważniejsze operacje, takie jak wstawianie, usuwanie, przeszukiwanie, iterowanie i inne. Zrozumienie tych operacji jest kluczowe dla efektywnej pracy z listami wskaźnikowymi i tworzenia złożonych struktur danych w bardziej zaawansowanych projektach.

Wstawianie elementów do listy

Jedną z najważniejszych operacji na listach wskaźnikowych jest wstawianie nowych elementów. W zależności od potrzeb możemy wstawiać elementy na początku, na końcu lub w dowolnym miejscu listy. Omówmy każdą z tych operacji szczegółowo.

1. Wstawianie na początku listy

Wstawianie elementu na początku listy jest operacją stosunkowo prostą i wydajną. Wystarczy utworzyć nowy węzeł, ustawić jego wskaźnik next na aktualny węzeł głowy, a następnie zaktualizować wskaźnik głowy, aby wskazywał na nowy węzeł.

void insertAtBeginning(int value) {
    Node* newNode = new Node(value);
    newNode->next = head;
    head = newNode;
}

W tej implementacji tworzymy nowy węzeł i ustawiamy jego wskaźnik next na aktualny węzeł głowy. Następnie aktualizujemy wskaźnik head, aby wskazywał na nowy węzeł. Ta operacja ma złożoność czasową O(1).

2. Wstawianie na końcu listy

Wstawianie elementu na końcu listy jest nieco bardziej skomplikowane, ponieważ musimy przejść przez całą listę, aby znaleźć ostatni węzeł. Po znalezieniu ostatniego węzła ustawiamy jego wskaźnik next na nowy węzeł.

void insertAtEnd(int value) {
    Node* newNode = new Node(value);
    if (!head) {
        head = newNode;
        return;
    }
    Node* temp = head;
    while (temp->next) {
        temp = temp->next;
    }
    temp->next = newNode;
}

W tej implementacji przechodzimy przez całą listę, aby znaleźć ostatni węzeł, a następnie ustawiamy jego wskaźnik next na nowy węzeł. Ta operacja ma złożoność czasową O(n), gdzie n to liczba elementów w liście.

3. Wstawianie w środku listy

Wstawianie elementu w środku listy wymaga znalezienia odpowiedniej pozycji, a następnie wstawienia nowego węzła między dwa istniejące węzły. Jest to operacja bardziej skomplikowana, ale niezbędna w wielu sytuacjach.

void insertAtPosition(int value, int position) {
    if (position == 0) {
        insertAtBeginning(value);
        return;
    }
    Node* newNode = new Node(value);
    Node* temp = head;
    for (int i = 0; i < position - 1 && temp; ++i) {
        temp = temp->next;
    }
    if (!temp) {
        std::cerr << "Pozycja poza zakresem" << std::endl;
        return;
    }
    newNode->next = temp->next;
    temp->next = newNode;
}

W tej implementacji przechodzimy przez listę, aby znaleźć odpowiednią pozycję, a następnie wstawiamy nowy węzeł. Jeśli pozycja jest niepoprawna (np. poza zakresem), wyświetlamy komunikat o błędzie.

Usuwanie elementów z listy

Usuwanie elementów z listy wskaźnikowej jest równie ważne, jak ich wstawianie. Możemy usuwać elementy z początku, końca lub dowolnego miejsca na liście.

1. Usuwanie z początku listy

Usuwanie elementu z początku listy jest operacją prostą i wydajną. Wystarczy zaktualizować wskaźnik head, aby wskazywał na następny węzeł, a następnie zwolnić pamięć usuniętego węzła.

void deleteFromBeginning() {
    if (!head) return;
    Node* temp = head;
    head = head->next;
    delete temp;
}

W tej implementacji ustawiamy wskaźnik head na następny węzeł i zwalniamy pamięć usuniętego węzła. Ta operacja ma złożoność czasową O(1).

2. Usuwanie z końca listy

Usuwanie elementu z końca listy jest bardziej skomplikowane, ponieważ musimy przejść przez całą listę, aby znaleźć przedostatni węzeł, a następnie zwolnić pamięć ostatniego węzła.

void deleteFromEnd() {
    if (!head) return;
    if (!head->next) {
        delete head;
        head = nullptr;
        return;
    }
    Node* temp = head;
    while (temp->next->next) {
        temp = temp->next;
    }
    delete temp->next;
    temp->next = nullptr;
}

W tej implementacji przechodzimy przez listę, aby znaleźć przedostatni węzeł, a następnie zwalniamy pamięć ostatniego węzła i ustawiamy wskaźnik next przedostatniego węzła na nullptr. Ta operacja ma złożoność czasową O(n).

3. Usuwanie elementu z dowolnej pozycji

Usuwanie elementu z dowolnej pozycji wymaga znalezienia odpowiedniego węzła, a następnie zaktualizowania wskaźników, aby pominąć usuwany węzeł.

void deleteFromPosition(int position) {
    if (!head) return;
    if (position == 0) {
        deleteFromBeginning();
        return;
    }
    Node* temp = head;
    for (int i = 0; i < position - 1 && temp; ++i) {
        temp = temp->next;
    }
    if (!temp || !temp->next) {
        std::cerr << "Pozycja poza zakresem" << std::endl;
        return;
    }
    Node* nodeToDelete = temp->next;
    temp->next = temp->next->next;
    delete nodeToDelete;
}

W tej implementacji przechodzimy przez listę, aby znaleźć odpowiedni węzeł, a następnie aktualizujemy wskaźniki, aby pominąć usuwany węzeł i zwalniamy pamięć. Jeśli pozycja jest niepoprawna, wyświetlamy komunikat o błędzie.

Przeszukiwanie listy

Przeszukiwanie listy polega na iterowaniu przez węzły w celu znalezienia węzła o określonej wartości. Jest to operacja liniowa, ponieważ musimy przejść przez każdy węzeł, aby znaleźć element.

bool search(int value) {
    Node* temp = head;
    while (temp) {
        if (temp->data == value) return true;
        temp = temp->next;
    }
    return false;
}

W tej implementacji przechodzimy przez listę, porównując wartość każdego węzła z szukaną wartością. Jeśli znajdziemy węzeł o odpowiedniej wartości, zwracamy true, w przeciwnym razie false. Ta operacja ma złożoność czasową O(n).

Iterowanie przez listę

Iterowanie przez listę polega na przechodzeniu przez każdy węzeł w celu wykonania pewnych operacji, takich jak wyświetlanie wartości węzłów lub przetwarzanie danych.

void iterateAndPrint() {
    Node* temp = head;
    while (temp) {
        std::cout << temp->data << " ";
        temp = temp->next;
    }
    std::cout << std::endl;
}

W tej implementacji przechodzimy przez listę i wyświetlamy wartość każdego węzła. Ta operacja ma złożoność czasową O(n).

Podsumowanie

Podstawowe operacje na listach wskaźnikowych, takie jak wstawianie, usuwanie, przeszukiwanie i iterowanie, są niezbędne do efektywnego zarządzania danymi w programach. W tej lekcji omówiliśmy każdą z tych operacji szczegółowo i przedstawiliśmy przykłady implementacji w języku C++. Zrozumienie tych operacji pozwoli Ci na tworzenie bardziej zaawansowanych struktur danych i optymalizowanie kodu pod kątem wydajności i efektywności pamięci.

Następny temat ==> Listy jednokierunkowe, dwukierunkowe i cykliczne



Spis Treści - Wstęp do programowania

Lekcja 3: Rozwiązywanie problemów i poprawność programów Lekcja 4: Praca z różnymi typami danych Lekcja 5: Obsługa plików i pamięci Lekcja 6: Zaawansowane techniki programistyczne Lekcja 7: Wskaźniki i pamięć dynamiczna Lekcja 8: Struktura kodu i abstrakcja Lekcja 9: Rekurencja i jej zastosowania Lekcja 10: Analiza wydajności algorytmów Lekcja 11: Technika "dziel i zwyciężaj" Lekcja 12: Struktury danych o dynamicznej budowie Lekcja 13: Struktury hierarchiczne: Drzewa Lekcja 14: Struktury danych z bibliotek Lekcja 15: Algorytmy z nawrotami Lekcja 16: Programowanie dynamiczne Lekcja 17: Programowanie zachłanne Lekcja 18: Praca z grafami

Jeśli chciałbyś być poinformowany o następnych kursach to zapisz się do naszego newslettera: