single_linked_list


Data Structure: Linked List Examples without STL

A linked list is a fundamental data structure in computer science and programming. It consists of a sequence of elements, called nodes, where each node contains data and a reference (or link) to the next node in the sequence. Linked lists are particularly useful when you need dynamic data storage that can easily grow or shrink in size.

Key Characteristics

Example 1: Basic Singly Linked List Implementation

#include <iostream>
#include <memory>

class Node {
public:
    int data;
    std::unique_ptr<Node> next;

    Node(int val) : data(val), next(nullptr) {}
};

class LinkedList {
private:
    std::unique_ptr<Node> head;

public:
    void insert(int val) {
        auto newNode = std::make_unique<Node>(val);
        if (!head) {
            head = std::move(newNode);
        } else {
            newNode->next = std::move(head);
            head = std::move(newNode);
        }
    }

    void display() {
        Node* current = head.get();
        while (current) {
            std::cout << current->data << " -> ";
            current = current->next.get();
        }
        std::cout << "nullptr" << std::endl;
    }
};

int main() {
    LinkedList list;
    list.insert(3);
    list.insert(2);
    list.insert(1);

    list.display();

    return 0;
}

Explanation

Example 2: Doubly Linked List with Tail Pointer

#include <iostream>
#include <memory>

class Node {
public:
    int data;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;

    Node(int val) : data(val), next(nullptr) {}
};

class DoublyLinkedList {
private:
    std::shared_ptr<Node> head;
    std::shared_ptr<Node> tail;

public:
    void insertBack(int val) {
        auto newNode = std::make_shared<Node>(val);
        if (!head) {
            head = tail = newNode;
        } else {
            newNode->prev = tail;
            tail->next = newNode;
            tail = newNode;
        }
    }

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

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

int main() {
    DoublyLinkedList list;
    list.insertBack(1);
    list.insertBack(2);
    list.insertBack(3);

    std::cout << "Forward: ";
    list.displayForward();
    std::cout << "Backward: ";
    list.displayBackward();

    return 0;
}

Explanation

Example 3: Circular Linked List

#include <iostream>
#include <memory>

class Node {
public:
    int data;
    std::shared_ptr<Node> next;

    Node(int val) : data(val), next(nullptr) {}
};

class CircularLinkedList {
private:
    std::shared_ptr<Node> head;

public:
    void insert(int val) {
        auto newNode = std::make_shared<Node>(val);
        if (!head) {
            head = newNode;
            head->next = head;  // Point to itself to make it circular
        } else {
            newNode->next = head->next;
            head->next = newNode;
            std::swap(head->data, newNode->data);
        }
    }

    void display() {
        if (!head) {
            std::cout << "Empty list" << std::endl;
            return;
        }

        auto current = head;
        do {
            std::cout << current->data << " -> ";
            current = current->next;
        } while (current != head);
        std::cout << "... (circular)" << std::endl;
    }
};

int main() {
    CircularLinkedList list;
    list.insert(1);
    list.insert(2);
    list.insert(3);

    list.display();

    return 0;
}

Explanation

Additional Considerations

  1. Performance: Linked lists offer O(1) insertion and deletion at the beginning (and end, with a tail pointer), but O(n) for random access.

  2. Memory Usage: Each node in a linked list requires extra memory for storing the link(s), which can be a consideration for large lists.

  3. STL Alternative: C++ Standard Template Library (STL) provides std::list and std::forward_list for doubly and singly linked lists, respectively. These are often preferable for general use due to their robust implementation and integration with other STL components.

  4. Custom Allocators: For advanced use cases, you might consider implementing custom allocators to optimize memory management for your specific needs.

  5. Thread Safety: The examples provided are not thread-safe. In a multi-threaded environment, proper synchronization mechanisms would need to be implemented.

Summary

Linked lists are versatile data structures that offer efficient insertion and deletion operations. We've explored three main types of linked lists: singly linked, doubly linked, and circular linked lists. Each type has its own advantages and use cases:

The examples provided demonstrate modern C++ implementations using smart pointers for automatic memory management. While linked lists have some limitations, such as lack of random access and potential cache inefficiency, they remain an important data structure in many scenarios, particularly when frequent insertion and deletion operations are required.

C++ Linked List Examples with STL

Certainly! I'll provide examples using the Standard Template Library (STL) implementations of linked lists in C++. The STL provides two types of linked lists: std::list (doubly linked list) and std::forward_list (singly linked list). I'll demonstrate various operations and use cases for both.

C++ STL Linked List Examples

The C++ Standard Template Library (STL) provides robust implementations of linked lists through std::list and std::forward_list. These container classes offer efficient insertion and deletion operations along with a wide range of member functions for list manipulation.

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};

    // Insert at the beginning and end
    myList.push_front(0);
    myList.push_back(2);

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

    // Sort the list
    myList.sort();

    // Remove duplicates
    myList.unique();

    // Display the sorted list without duplicates
    std::cout << "Sorted list without duplicates: ";
    for (const auto& elem : myList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    return 0;
}

Explanation

Example 5: std::forward_list for Memory-Efficient Operations

#include <iostream>
#include <forward_list>
#include <algorithm>

int main() {
    std::forward_list<int> myForwardList = {3, 1, 4, 1, 5, 9};

    // Insert at the beginning
    myForwardList.push_front(0);

    // Insert after a specific position
    auto it = myForwardList.begin();
    std::advance(it, 2);
    myForwardList.insert_after(it, 2);

    // Display the list
    std::cout << "Forward list contents: ";
    for (const auto& elem : myForwardList) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    // Remove all elements with a specific value
    myForwardList.remove(1);

    // Reverse the list
    myForwardList.reverse();

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

    return 0;
}

Explanation

Example 6: Using std::list with Custom Objects

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

class Person {
public:
    std::string name;
    int age;

    Person(const std::string& n, int a) : name(n), age(a) {}

    // For sorting based on age
    bool operator<(const Person& other) const {
        return age < other.age;
    }
};

// For displaying Person objects
std::ostream& operator<<(std::ostream& os, const Person& p) {
    return os << p.name << " (" << p.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 << std::endl;
    }

    // Sort the list based on age
    people.sort();

    // Display sorted list
    std::cout << "\nSorted list by age:" << std::endl;
    for (const auto& person : people) {
        std::cout << person << 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 << std::endl;
    }

    return 0;
}

Explanation

Additional Considerations

  1. Performance: While std::list and std::forward_list provide O(1) insertion and deletion, they may have worse cache performance compared to contiguous containers like std::vector for traversal operations.

  2. Memory Allocation: These containers allocate memory for each node separately, which can lead to memory fragmentation in some scenarios.

  3. Iterator Invalidation: Iterators to std::list and std::forward_list remain valid after insertion or removal operations, except for the erased elements.

  4. Use Cases: These containers are particularly useful when frequent insertion and deletion operations are required at arbitrary positions in the sequence.

  5. Algorithms: Many STL algorithms work efficiently with these containers, but some (like std::sort) are not applicable to std::forward_list due to its unidirectional nature.

Summary

The STL provides powerful and flexible implementations of linked lists through std::list and std::forward_list. These containers offer efficient insertion and deletion operations, along with a rich set of member functions and compatibility with STL algorithms.

std::list is a doubly linked list that allows bidirectional traversal and provides operations like push_back(), which are not available in std::forward_list. It's more versatile but uses more memory per node.

std::forward_list is a singly linked list, offering a more memory-efficient solution when only forward traversal is needed. It's particularly useful in scenarios where memory usage is a critical factor.

Both containers are excellent choices when the primary operations involve frequent insertions and deletions at arbitrary positions in the sequence. They provide a balance of functionality and performance, making them suitable for a wide range of applications in C++ programming.

Related

Previous Page | Course Schedule | Course Content