double_linked_list


Data Structure: Doubly Linked List

A doubly linked list is a type of linked data structure where each node contains data and two links (or pointers) - one to the next node and one to the previous node. This bidirectional linking allows for more flexible operations compared to a singly linked list, as it enables efficient traversal in both directions.

Key Characteristics

Example 1: Basic Doubly Linked List Implementation

#include <iostream>
#include <memory>

template<typename T>
class DoublyLinkedList {
private:
    struct Node {
        T data;
        std::shared_ptr<Node> next;
        std::weak_ptr<Node> prev;
        Node(const T& value) : data(value), next(nullptr) {}
    };

    std::shared_ptr<Node> head;
    std::shared_ptr<Node> tail;

public:
    void pushBack(const T& value) {
        auto newNode = std::make_shared<Node>(value);
        if (!head) {
            head = tail = newNode;
        } else {
            newNode->prev = tail;
            tail->next = newNode;
            tail = newNode;
        }
    }

    void display() const {
        auto current = head;
        while (current) {
            std::cout << current->data << " <-> ";
            current = current->next;
        }
        std::cout << "nullptr" << std::endl;
    }

    void displayReverse() const {
        auto current = tail;
        while (current) {
            std::cout << current->data << " <-> ";
            current = current->prev.lock();
        }
        std::cout << "nullptr" << std::endl;
    }
};

int main() {
    DoublyLinkedList<int> list;
    list.pushBack(1);
    list.pushBack(2);
    list.pushBack(3);

    std::cout << "Forward: ";
    list.display();
    std::cout << "Reverse: ";
    list.displayReverse();

    return 0;
}

Explanation

Example 2: Doubly Linked List with Insertion and Deletion

#include <iostream>
#include <memory>
#include <stdexcept>

template<typename T>
class DoublyLinkedList {
private:
    struct Node {
        T data;
        std::shared_ptr<Node> next;
        std::weak_ptr<Node> prev;
        Node(const T& value) : data(value), next(nullptr) {}
    };

    std::shared_ptr<Node> head;
    std::shared_ptr<Node> tail;
    size_t size;

public:
    DoublyLinkedList() : head(nullptr), tail(nullptr), size(0) {}

    void pushFront(const T& value) {
        auto newNode = std::make_shared<Node>(value);
        if (!head) {
            head = tail = newNode;
        } else {
            newNode->next = head;
            head->prev = newNode;
            head = newNode;
        }
        ++size;
    }

    void pushBack(const T& value) {
        auto newNode = std::make_shared<Node>(value);
        if (!tail) {
            head = tail = newNode;
        } else {
            newNode->prev = tail;
            tail->next = newNode;
            tail = newNode;
        }
        ++size;
    }

    void popFront() {
        if (!head) throw std::out_of_range("List is empty");
        head = head->next;
        if (head) {
            head->prev.reset();
        } else {
            tail.reset();
        }
        --size;
    }

    void popBack() {
        if (!tail) throw std::out_of_range("List is empty");
        tail = tail->prev.lock();
        if (tail) {
            tail->next.reset();
        } else {
            head.reset();
        }
        --size;
    }

    size_t getSize() const { return size; }

    void display() const {
        auto current = head;
        while (current) {
            std::cout << current->data << " <-> ";
            current = current->next;
        }
        std::cout << "nullptr" << std::endl;
    }
};

int main() {
    DoublyLinkedList<int> list;

    list.pushBack(3);
    list.pushFront(1);
    list.pushBack(4);
    list.pushFront(0);

    std::cout << "List: ";
    list.display();
    std::cout << "Size: " << list.getSize() << std::endl;

    list.popFront();
    list.popBack();

    std::cout << "After pop operations: ";
    list.display();
    std::cout << "Size: " << list.getSize() << std::endl;

    return 0;
}

Explanation

Example 3: Doubly Linked List with Iterator

#include <iostream>
#include <memory>
#include <iterator>

template<typename T>
class DoublyLinkedList {
private:
    struct Node {
        T data;
        std::shared_ptr<Node> next;
        std::weak_ptr<Node> prev;
        Node(const T& value) : data(value), next(nullptr) {}
    };

    std::shared_ptr<Node> head;
    std::shared_ptr<Node> tail;

public:
    class Iterator : public std::iterator<std::bidirectional_iterator_tag, T> {
    private:
        std::shared_ptr<Node> current;
    public:
        Iterator(std::shared_ptr<Node> node) : current(node) {}

        T& operator*() { return current->data; }
        Iterator& operator++() { current = current->next; return *this; }
        Iterator& operator--() { 
            current = current ? current->prev.lock() : nullptr;
            return *this;
        }
        bool operator!=(const Iterator& other) { return current != other.current; }
    };

    Iterator begin() { return Iterator(head); }
    Iterator end() { return Iterator(nullptr); }

    void pushBack(const T& value) {
        auto newNode = std::make_shared<Node>(value);
        if (!head) {
            head = tail = newNode;
        } else {
            newNode->prev = tail;
            tail->next = newNode;
            tail = newNode;
        }
    }

    void reverse() {
        auto current = head;
        std::shared_ptr<Node> temp = nullptr;

        while (current) {
            temp = current->prev.lock();
            current->prev = current->next;
            current->next = temp;
            current = current->prev.lock();
        }

        if (temp) {
            head = temp->prev.lock();
        }
        std::swap(head, tail);
    }
};

int main() {
    DoublyLinkedList<int> list;
    list.pushBack(1);
    list.pushBack(2);
    list.pushBack(3);

    std::cout << "Original list: ";
    for (const auto& item : list) {
        std::cout << item << " ";
    }
    std::cout << std::endl;

    list.reverse();

    std::cout << "Reversed list: ";
    for (const auto& item : list) {
        std::cout << item << " ";
    }
    std::cout << std::endl;

    return 0;
}

Explanation

Additional Considerations

  1. Memory Efficiency: Doubly linked lists use more memory per node than singly linked lists due to the extra pointer.

  2. Complexity: Most operations (insertion, deletion) at known positions are O(1), but finding an element is O(n).

  3. STL Alternative: C++'s Standard Template Library provides std::list, which is a doubly linked list implementation.

  4. Thread Safety: The provided examples are not thread-safe. In a multi-threaded environment, proper synchronization would be necessary.

  5. Use Cases: Doubly linked lists are particularly useful when frequent insertions and deletions are required at both ends of the list, or when bidirectional traversal is needed.

Summary

Doubly linked lists offer a flexible data structure with efficient insertion and deletion at both ends, as well as bidirectional traversal. The examples provided demonstrate various implementations and use cases, including:

  1. A basic doubly linked list with forward and reverse traversal.
  2. An expanded version with insertion and deletion operations at both ends.
  3. An implementation featuring a custom iterator, allowing for more idiomatic C++ usage.

The bidirectional nature of doubly linked lists makes them suitable for scenarios where backwards traversal or manipulation is required, such as undo functionality in applications or certain types of caches. While they consume more memory than singly linked lists, the added flexibility can be beneficial in many scenarios.

The use of smart pointers (std::shared_ptr and std::weak_ptr) in these examples demonstrates modern C++ practices for memory management, helping to prevent memory leaks and simplify resource management.

Using import <list>

Certainly! Using the <list> container from the C++ Standard Template Library (STL) is indeed very useful when working with doubly linked lists. Let's explore the std::list implementation and its features.

C++ Doubly Linked List using std::list

std::list is the STL implementation of a doubly linked list. It provides a robust, efficient, and feature-rich container that adheres to the C++ standard and integrates well with other STL components.

Key Characteristics

Example 4: . Basic Usage of std::list

#include <iostream>
#include <list>
#include <algorithm>

int main() {
    std::list<int> myList = {3, 1, 4, 1, 5, 9, 2, 6};

    // Display the original list
    std::cout << "Original list: ";
    for (const auto& elem : myList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    // Add elements to front and back
    myList.push_front(0);
    myList.push_back(8);

    // Sort the list
    myList.sort();

    // Remove duplicate elements
    myList.unique();

    // Display the modified list
    std::cout << "Modified list: ";
    for (const auto& elem : myList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

Explanation

Example 5: Advanced Operations with std::list

#include <iostream>
#include <list>
#include <algorithm>

void printList(const std::list<int>& lst, const std::string& name) {
    std::cout << name << ": ";
    for (const auto& elem : lst) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::list<int> list1 = {1, 2, 3, 4, 5};
    std::list<int> list2 = {10, 20, 30, 40, 50};

    printList(list1, "List 1");
    printList(list2, "List 2");

    // Insert elements from list2 into list1
    auto it = std::find(list1.begin(), list1.end(), 3);
    list1.insert(it, list2.begin(), list2.end());

    printList(list1, "List 1 after insertion");

    // Remove elements from list1
    list1.remove_if([](int n) { return n % 2 == 0; });

    printList(list1, "List 1 after removing even numbers");

    // Reverse list2
    list2.reverse();

    printList(list2, "List 2 after reversal");

    // Splice operation: move elements from list2 to list1
    list1.splice(list1.begin(), list2);

    printList(list1, "List 1 after splicing");
    printList(list2, "List 2 after splicing (now empty)");

    return 0;
}

Explanation

Exmaple 6: Custom Comparator and Sorting with std::list

#include <iostream>
#include <list>
#include <string>
#include <algorithm>

struct Person {
    std::string name;
    int age;

    Person(std::string n, int a) : name(std::move(n)), age(a) {}
};

// Custom comparator for sorting
bool comparePersons(const Person& a, const Person& b) {
    return a.age < b.age;
}

int main() {
    std::list<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
        {"David", 28}
    };

    // Display original list
    std::cout << "Original list:" << std::endl;
    for (const auto& person : people) {
        std::cout << person.name << ": " << person.age << std::endl;
    }

    // Sort using custom comparator
    people.sort(comparePersons);

    // Display sorted list
    std::cout << "\nSorted list by age:" << std::endl;
    for (const auto& person : people) {
        std::cout << person.name << ": " << person.age << std::endl;
    }

    // Find a person by name
    auto it = std::find_if(people.begin(), people.end(),
                           [](const Person& p) { return p.name == "Charlie"; });
    if (it != people.end()) {
        std::cout << "\nFound: " << it->name << ", age: " << it->age << std::endl;
    }

    return 0;
}

Explanation

Additional Considerations

  1. Performance: std::list provides constant time insertion and removal at any position, making it efficient for frequent modifications.

  2. Memory Allocation: Each node in std::list is allocated separately, which can lead to more memory fragmentation compared to contiguous containers like std::vector.

  3. Iterator Stability: Iterators and references to list elements remain valid even after insertions and deletions (except for the erased elements).

  4. No Random Access: Unlike std::vector, std::list does not support random access iterators or indexing.

  5. Reversing and Splicing: Operations like reversing the list or splicing (moving elements from one list to another) are very efficient with std::list.

Summary

std::list provides a powerful and flexible implementation of a doubly linked list in C++. It offers several advantages:

  1. Standard Compliance: As part of the STL, it ensures compatibility and consistent behavior across different C++ implementations.

  2. Rich Functionality: It comes with a wide range of member functions and is compatible with many STL algorithms.

  3. Efficiency: Offers constant time insertion and deletion at any position in the list.

  4. Bidirectional Iteration: Supports both forward and backward traversal.

  5. Automatic Memory Management: Handles memory allocation and deallocation automatically.

Using std::list is often preferable to implementing a custom doubly linked list, as it provides a well-tested, efficient, and feature-rich solution. It's particularly useful in scenarios requiring frequent insertions and deletions at arbitrary positions, or when bidirectional traversal is needed.

However, for applications requiring random access or when memory locality is crucial for performance, other containers like std::vector might be more appropriate. The choice between std::list and other containers should be based on the specific requirements of your application.

Previous Page | Course Schedule | Course Content