Overview
Single linked lists are a versatile data structure commonly used in computer science and programming. In this article, we will explore different approaches to implement single linked lists in C++. Each method provides a unique perspective on how to manage linked data structures, giving you the flexibility to choose the one that best suits your needs. By the end, you will have a solid understanding of how to use and manipulate single linked lists in your C++ programs.
What is a Single Linked List?
A single linked list is a linear data structure consisting of a sequence of elements called nodes. Each node contains two parts: the data or value it holds and a reference (a pointer) to the next node in the list. The last node typically points to a null reference, indicating the end of the list. This linked structure allows for efficient insertions and deletions.
Implementation of Single Linked Lists
Method 1: Using Structs
One of the most straightforward ways to create a single linked list in C++ is by using structs to define nodes and a class to manage the list.
#include <iostream>
// Define the Node structure
struct Node {
int data;
Node* next;
};
// Define the Linked List class
class LinkedList {
public:
LinkedList() : head(nullptr) {}
// Append a new element to the end of the list
void append(int value) {
Node* newNode = new Node{value, nullptr};
if (!head) {
head = newNode;
} else {
Node* current = head;
while (current->next) {
current = current->next;
}
current->next = newNode;
}
}
// Display the elements in the list
void display() {
Node* current = head;
while (current) {
std::cout << current->data << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl;
}
private:
Node* head;
};
int main() {
LinkedList myList;
myList.append(1);
myList.append(2);
myList.append(3);
myList.display();
return 0;
}
Method 2: Using Custom Classes
Another way to implement a single linked list is by creating custom classes for nodes and the linked list. This approach provides more encapsulation and can be useful in complex scenarios.
#include <iostream>
// Define the Node class
class Node {
public:
int data;
Node* next;
Node(int value) : data(value), next(nullptr) {}
};
// Define the Linked List class
class LinkedList {
public:
LinkedList() : head(nullptr) {}
// Append a new element to the end of the list
void append(int value) {
Node* newNode = new Node(value);
if (!head) {
head = newNode;
} else {
Node* current = head;
while (current->next) {
current = current->next;
}
current->next = newNode;
}
}
// Display the elements in the list
void display() {
Node* current = head;
while (current) {
std::cout << current->data << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl;
}
private:
Node* head;
};
int main() {
LinkedList myList;
myList.append(1);
myList.append(2);
myList.append(3);
myList.display();
return 0;
}
Method 3: Using Templates
C++ templates provide a way to create a generic linked list that can store elements of any data type. This approach is highly flexible and can be adapted for various use cases.
#include <iostream>
// Define the Node template
template <typename T>
struct Node {
T data;
Node* next;
Node(T value) : data(value), next(nullptr) {}
};
// Define the Linked List template
template <typename T>
class LinkedList {
public:
LinkedList() : head(nullptr) {}
// Append a new element to the end of the list
void append(T value) {
Node<T>* newNode = new Node<T>(value);
if (!head) {
head = newNode;
} else {
Node<T>* current = head;
while (current->next) {
current = current->next;
}
current->next = newNode;
}
}
// Display the elements in the list
void display() {
Node<T>* current = head;
while (current) {
std::cout << current->data << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl;
}
private:
Node<T>* head;
};
int main() {
LinkedList<int> intList;
intList.append(1);
intList.append(2);
intList.append(3);
intList.display();
LinkedList<std::string> strList;
strList.append("Hello");
strList.append("World");
strList.display();
return 0;
}
Single Linked List Operations and Their Complexities
Single linked lists support various operations, and it's important to understand their time complexities.
Insertion (at the beginning):
- Operation: insertAtBeginning(value)
- Time Complexity: O(1)
- Explanation: Adding a node at the beginning involves changing a few pointers.
Insertion (at the end):
- Operation: append(value)
- Time Complexity: O(n)
- Explanation: To add an element at the end, you need to traverse the entire list to find the last node, making it a linear-time operation.
Deletion (from the beginning):
- Operation: deleteFromBeginning()
- Time Complexity: O(1)
- Explanation: Removing the first element requires updating the head pointer.
Deletion (from a specific position):
- Operation: deleteAtPosition(position)
- Time Complexity: O(n)
- Explanation: Deleting a node at a specific position involves traversing the list to find the previous node.
Traversal:
- Operation: display()
- Time Complexity: O(n)
- Explanation: To display all elements, you need to traverse the entire list.
Searching:
- Operation: search(value)
- Time Complexity: O(n)
- Explanation: In the worst case, you may have to search through the entire list to find a specific value.
Below is the code implementation that includes common operations:
#include <iostream>
// Define the Node structure
struct Node {
int data;
Node* next;
Node(int value) : data(value), next(nullptr) {}
};
// Define the Linked List class
class LinkedList {
public:
LinkedList() : head(nullptr) {}
// Append a new element to the end of the list
void append(int value) {
Node* newNode = new Node(value);
if (!head) {
head = newNode;
} else {
Node* current = head;
while (current->next) {
current = current->next;
}
current->next = newNode;
}
}
// Insert a new element at the beginning of the list
void insertAtBeginning(int value) {
Node* newNode = new Node(value);
newNode->next = head;
head = newNode;
}
// Delete the first element from the list
void deleteFromBeginning() {
if (head) {
Node* temp = head;
head = head->next;
delete temp;
}
}
// Display the elements in the list
void display() {
Node* current = head;
while (current) {
std::cout << current->data << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl;
}
// Search for a specific value in the list
bool search(int value) {
Node* current = head;
while (current) {
if (current->data == value) {
return true;
}
current = current->next;
}
return false;
}
};
int main() {
LinkedList myList;
myList.append(1);
myList.append(2);
myList.append(3);
std::cout << "Original List: ";
myList.display();
myList.insertAtBeginning(0);
std::cout << "After Insertion at Beginning: ";
myList.display();
myList.deleteFromBeginning();
std::cout << "After Deletion from Beginning: ";
myList.display();
int searchValue = 2;
if (myList.search(searchValue)) {
std::cout << searchValue << " found in the list." << std::endl;
} else {
std::cout << searchValue << " not found in the list." << std::endl;
}
return 0;
}
append
: Adds an element at the end of the list.insertAtBeginning
: Inserts an element at the beginning of the list.deleteFromBeginning
: Removes the first element.display
: Displays the elements in the list.search
: Searches for a specific value in the list.
Difference between arrays and linked lists:
Memory Allocation:
- Arrays: Arrays are allocated in contiguous memory locations. The size of an array is fixed when it is declared.
- Linked Lists: Linked lists do not require contiguous memory. They are dynamically allocated as new elements are added, making them more flexible in terms of size.
Insertions and Deletions:
- Arrays: Insertions and deletions in an array can be slow because elements may need to be shifted to maintain the contiguous memory layout. This can result in a time complexity of O(n) in the worst case.
- Linked Lists: Linked lists excel at insertions and deletions, as they only require changing a few pointers. Insertions and deletions can be performed in O(1) time, assuming you have a reference to the element to be modified.
Access Time:
- Arrays: Accessing elements in an array is fast and has a constant time complexity of O(1), assuming you know the index.
- Linked Lists: Accessing elements in a linked list is slower because you must traverse the list from the beginning. The time complexity for access is O(n) in the worst case.
Dynamic Sizing:
- Arrays: Arrays have a fixed size, which makes dynamic resizing complex and potentially inefficient. You may need to allocate a new array, copy elements, and deallocate the old array.
- Linked Lists: Linked lists are dynamic in size and can easily accommodate the addition or removal of elements without resizing concerns.
Random Access:
- Arrays: Arrays support efficient random access to elements using indices.
- Linked Lists: Linked lists do not support efficient random access; you must traverse the list sequentially to access an element.