Hello there! Today, we will explore Linked Lists, a core data structure crucial for organized data management and establishing relationships between data.
We will cover essential aspects, including an introduction to Linked Lists, their real-world applications, their implementation in C++, and the different operations you can perform on them.
By the end of this lesson, you will be well-equipped to implement and operate Linked Lists using C++. Let's get started!
A Linked List is a linear data structure similar to arrays. However, unlike arrays, they are not stored in contiguous memory locations. Each element in a Linked List is part of a node
. A node
comprises data and a pointer to the next node
in the sequence. This structure facilitates efficient insertions and deletions.
The head
is also an essential concept in Linked Lists. It is the first node
in the list and serves as a reference to the entire list. The head
is a nullptr
if the Linked List is empty.
Singly linked lists come up quite often in coding interviews. Interviewers from tech giants, start-ups, and just about any company testing your coding abilities will pose challenges based on this concept.
Here's another interesting point: While singly linked lists might not be extensively used in real-world applications, they form the foundational knowledge for understanding doubly linked lists, which are indeed quite common. A singly linked list contains nodes with a single pointer pointing to the next node, whereas a doubly linked list has nodes with pointers to both the next and the previous nodes.
To begin implementing Linked Lists, we first need to understand the structure of a node
, the building block of a Linked List. In C++, we'll create a class to serve as a blueprint for a node
.
A Node
class mainly consists of data
(the data you want to store) and next
(a pointer to the next node
). In our case, we'll create a Node
class to store integer data, with a constructor to initialize the node.
C++1class Node { 2public: 3 int data; // Node data 4 Node* next; // Pointer to next node 5 6 Node(int data) : data(data), next(nullptr) {} // Initialize node with data 7};
In this section, we'll learn how to add a new node at the end of our Linked List. We'll create a LinkedList
class and implement an append
method within it.
C++1class Node { 2public: 3 int data; 4 Node* next; 5 6 Node(int data) : data(data), next(nullptr) {} 7}; 8 9class LinkedList { 10private: 11 Node* head; // Pointer to first node 12 13public: 14 LinkedList() : head(nullptr) {} // Initialize empty list 15 16 void append(int data) { 17 Node* node = new Node(data); // Create new node 18 if (!head) { 19 head = node; // Set head if list is empty 20 } else { 21 Node* last = head; 22 while (last->next) { 23 last = last->next; // Traverse to end 24 } 25 last->next = node; // Append new node 26 } 27 } 28};
The code begins by creating a new node on the heap, ensuring it persists. The code then checks if head
is nullptr
, which would be the case for an empty list. If that's true, head
is set to the new node, making this new node the first and only node in the list. If the linked list is not empty (head
is not nullptr
), the code traverses to the end of the list using a while
loop and appends the new node after the last node.
Now, what if we want to add a new node at the beginning of our list? We'll write a method addFirst
to achieve this operation.
C++1class LinkedList { 2private: 3 Node* head; 4 5public: 6 LinkedList() : head(nullptr) {} 7 8 void addFirst(int data) { 9 Node* node = new Node(data); // Create new node 10 11 if (head != nullptr) { 12 node->next = head; // Link new node to the current head 13 } 14 head = node; // Update head to new node 15 } 16};
It simply reassigns the head
to the new node, making it the first node in the list.
Removing a node from a Linked List is also an essential functionality. We will add a deleteNode
method to our LinkedList
class to remove a node with a particular data value.
C++1class LinkedList { 2private: 3 Node* head; 4 5public: 6 LinkedList() : head(nullptr) {} 7 8 void deleteNode(int data) { 9 if (head == nullptr) return; // Check if list is empty 10 11 // Check if the head node is the one to be deleted 12 if (head->data == data) { 13 Node* temp = head; 14 head = head->next; // Move head to the next node 15 delete temp; // Free memory of the old head 16 return; 17 } 18 19 Node* current = head; 20 // Traverse the list to find the node to delete 21 while (current->next != nullptr) { 22 if (current->next->data == data) { 23 Node* temp = current->next; 24 current->next = current->next->next; // Bypass the node to delete 25 delete temp; // Free memory of the bypassed node 26 return; 27 } 28 current = current->next; // Move to the next node 29 } 30 } 31};
Deleting a node is straightforward, involving some pointer adjustments. However, locating the node can be time-consuming due to the need to traverse the list. Similar to the append
method, we traverse the list to find a node with specific data. Once located, the target node is removed by updating the pointer of the previous node to bypass the target, followed by deallocating the target node's memory.
C++1class LinkedList { 2private: 3 Node* head; 4 5public: 6 LinkedList() : head(nullptr) {} 7 8 ~LinkedList() { 9 Node* current = head; // Start with the head node 10 while (current != nullptr) { // Iterate until the end of the list 11 Node* nextNode = current->next; // Store the next node 12 delete current; // Delete the current node 13 current = nextNode; // Move to the next node 14 } 15 } 16};
The destructor in the LinkedList
class is responsible for cleaning up any dynamically allocated memory to prevent memory leaks. It iterates through the linked list, deleting each Node
to free up the memory that was allocated for storing the list's elements. This is crucial because it ensures that when a linked list object is destroyed, all resources it used are properly released, maintaining the efficiency and stability of your program.
While understanding the implementation of Linked Lists is great, it's equally crucial to comprehend the performance of our operations. This understanding generally comes through complexity analysis, examining the time (number of operations) and space (memory used) needed for an operation.
Here's a summary of the performance of particular operations in Linked Lists:
-
Accessing an element: It has
O(n)
time complexity because, in the worst-case scenario, we'd have to traverse through the entire list. The space complexity isO(1)
as accessing an element requires only a single pointer for traversal. -
Inserting or deleting a node: It's
O(1)
if we're adding or removing from the front of the list. However, if it's at the end or in the middle, it would beO(n)
because we'd have to traverse the list to find the position. The space complexity of this operation isO(1)
since the operations rely on a constant amount of additional space for pointers, regardless of the list size. -
The space complexity of the Linked List is
O(n)
.
The LinkedList
class acts as a convenient wrapper to manage operations on a Linked List, making it easy to use and organize. However, it's important to note that the essential part of a Linked List is the Node
class and the functions that handle link management. Each node inherently holds a pointer to the next node, which means that Node
instances, along with a reference to the head
, are all that's needed to build and manage a Linked List with full functionality.
Great job sticking with it throughout this intriguing journey, from understanding the concept of Linked Lists to implementing them in C++, exploring critical operations, and understanding their performance!
Up ahead, we have lined up some practice sessions. These sessions will provide you with real-time experience implementing and manipulating Linked Lists using C++.