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.
#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;
}
std::shared_ptr
is used for next
pointers to manage memory automatically.std::weak_ptr
is used for prev
pointers to avoid circular references.pushBack
method adds elements to the end of the list.display
and displayReverse
methods show traversal in both directions.#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;
}
pushFront
and pushBack
methods allow insertion at both ends.popFront
and popBack
methods remove elements from both ends.size
variable keeps track of the number of elements for O(1) size queries.#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;
}
Iterator
class allows for standard C++ iterator usage, including range-based for loops.reverse
method is implemented to demonstrate the advantage of bidirectional links.++
and --
operators).Memory Efficiency: Doubly linked lists use more memory per node than singly linked lists due to the extra pointer.
Complexity: Most operations (insertion, deletion) at known positions are O(1), but finding an element is O(n).
STL Alternative: C++'s Standard Template Library provides std::list
, which is a doubly linked list implementation.
Thread Safety: The provided examples are not thread-safe. In a multi-threaded environment, proper synchronization would be necessary.
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.
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:
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.
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.
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.
#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;
}
std::list
.push_front()
and push_back()
are used to add elements at both ends.sort()
member function efficiently sorts the list.unique()
removes consecutive duplicate elements.#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;
}
std::list
.insert()
is used to insert a range of elements from one list into another.remove_if()
demonstrates removal based on a predicate (lambda function in this case).reverse()
efficiently reverses the order of elements.splice()
moves elements from one list to another without copying.#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;
}
std::list
with custom objects and a custom comparator.Person
struct represents a custom data type stored in the list.comparePersons
is used for sorting based on age.sort()
member function is called with the custom comparator.std::find_if
is used to search for a specific person, showing compatibility with STL algorithms.Performance: std::list
provides constant time insertion and removal at any position, making it efficient for frequent modifications.
Memory Allocation: Each node in std::list
is allocated separately, which can lead to more memory fragmentation compared to contiguous containers like std::vector
.
Iterator Stability: Iterators and references to list elements remain valid even after insertions and deletions (except for the erased elements).
No Random Access: Unlike std::vector
, std::list
does not support random access iterators or indexing.
Reversing and Splicing: Operations like reversing the list or splicing (moving elements from one list to another) are very efficient with std::list
.
std::list
provides a powerful and flexible implementation of a doubly linked list in C++. It offers several advantages:
Standard Compliance: As part of the STL, it ensures compatibility and consistent behavior across different C++ implementations.
Rich Functionality: It comes with a wide range of member functions and is compatible with many STL algorithms.
Efficiency: Offers constant time insertion and deletion at any position in the list.
Bidirectional Iteration: Supports both forward and backward traversal.
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.