Linked Lists#
A linked list is a data structure in computer science that consists of a sequence of nodes, where each node contains data and a reference (or pointer) to the next node in the sequence.
Unlike arrays, where the elements are stored in contiguous memory locations, the nodes in a linked list can be scattered throughout the memory. This makes linked lists more flexible than arrays, as they can be easily modified by adding, removing or rearranging the nodes.
Linked lists come in different types, such as singly linked lists, doubly linked lists, and circular linked lists. In a singly linked list, each node has a reference to only the next node in the sequence. In a doubly linked list, each node has references to both the previous and the next nodes in the sequence. In a circular linked list, the last node in the sequence has a reference to the first node, thus forming a circular chain.
Linked lists are commonly used in programming for various purposes, such as implementing dynamic data structures, like stacks, queues, and hash tables, and for managing memory allocation and deallocation in operating systems. However, linked lists can be less efficient than arrays for certain operations, such as random access and searching, as they require traversing the list sequentially from the beginning.
Background#
🐍 Timing Sample
import time
n =- 100000
start = time.time()
array = []
for i in range(n):
array.append('s')
print(time.time() - start)
start = time.time()
array = []
for i in range(n):
array = array + ['s']
print(time.time() - start)
How are lists implemented in CPython
CPython’s lists are really variable-length arrays, not Lisp-style linked lists. The implementation uses a contiguous array of references to other objects, and keeps a pointer to this array and the array’s length in a list head structure.
This makes indexing a list \(a[i]\) an operation whose cost is independent of the size of the list or the value of the index.
When items are appended or inserted, the array of references is resized. Some cleverness is applied to improve the performance of appending items repeatedly; when the array must be grown, some extra space is allocated so the next few times don’t require an actual resize.
CPython is the reference implementation of the Python programming language
Some STL Containers#
- Sequence Containers#
Maintain the ordering of inserted elements that you specify.
Arrays, vectors, forward lists, and lists are all data structures used in computer science and programming to store collections of data. Each of these data structures has its own strengths and weaknesses, and understanding the differences between them can help you choose the best one for your particular application.
- Array#
a data structure that stores a fixed-size sequential collection of elements of the same type. The elements of an array are stored in contiguous memory locations, making it easy to access them using an index. Arrays are efficient when you know the size of the data in advance, and you need to access the elements randomly.
- Vector#
a dynamic array that can grow or shrink as needed. It is similar to an array, but it can change its size dynamically during runtime. The elements of a vector are stored in contiguous memory locations, making it easy to access them using an index. Vectors are useful when you don’t know the size of the data in advance, or when you need to insert or delete elements from the middle of the collection.
- Forward List#
a singly linked list that stores a sequence of elements. Each element in a forward list contains a pointer to the next element in the list, but not to the previous one. This means that you can only traverse a forward list in one direction. Forward lists are useful when you need to insert or delete elements from the beginning or middle of the collection, but not the end.
- List#
a doubly linked list that stores a sequence of elements. Each element in a list contains a pointer to the next and previous elements in the list, allowing you to traverse the list in both directions. Lists are useful when you need to insert or delete elements from any position in the collection, or when you need to maintain a sorted collection.
In summary, arrays and vectors are best for situations where you need random access to elements and the size of the collection is known in advance. Forward lists and lists are better for situations where you need to insert or delete elements from the beginning, middle, or end of the collection, or when you need to maintain a sorted collection.
Stack Allocation
When an array is declared as a local variable inside a function without using the new operator, the memory for the array is allocated on the stack. The size of the array must be known at compile-time. Here’s an example:
void foo() {
int myArray[10]; // allocate 10 integers on the stack
// ...
}
Stack allocation is fast and efficient, but the size of the array is limited by the available stack space, which is typically smaller than the heap.
Heap Allocation
When an array is declared using the new operator, the memory for the array is allocated on the heap. The size of the array can be determined at runtime. Here’s an example:
void bar() {
int* myArray = new int[10]; // allocate 10 integers on the heap
// ...
delete[] myArray; // free the memory when done
}
Heap allocation is more flexible than stack allocation, but it’s slower and more prone to memory leaks if the programmer forgets to free the memory when done.
It’s important to note that C++ also provides a number of container classes, such as std::vector
, std::list
, and std::array
, which provide more advanced memory management capabilities than plain C-style arrays.
1#include <iostream>
2#include <array>
3#include <algorithm>
4
5int main() {
6 // Declare an array with initial values
7 std::array<int, 5> arr = {1, 2, 3, 4, 5};
8
9 // Access an element by index
10 std::cout << arr[0] << std::endl; // prints 1
11
12 // Modify an element by index
13 arr[0] = 10; // arr is now {10, 2, 3, 4, 5}
14
15 // Get the length of the array
16 std::cout << arr.size() << std::endl; // prints 5
17
18 // Add an element to the end of the array
19 arr.fill(0);
20 arr.back() = 6; // arr is now {0, 0, 0, 0, 6}
21
22 // Remove and return the last element of the array
23 int last_element = arr.back();
24 arr.back() = 0; // arr is now {0, 0, 0, 0, 0}
25
26 // Add an element to the beginning of the array
27 arr.fill(0);
28 for (int i = arr.size()-1; i > 0; i--) {
29 arr[i] = arr[i-1];
30 }
31 arr[0] = 0; // arr is now {0, 0, 0, 0, 0}
32
33 // Remove and return the first element of the array
34 int first_element = arr.front();
35 for (int i = 0; i < arr.size()-1; i++) {
36 arr[i] = arr[i+1];
37 }
38 arr.back() = 0; // arr is now {0, 0, 0, 0, 0}
39
40 // Find the index of a given element in the array
41 int index = std::distance(arr.begin(), std::find(arr.begin(), arr.end(), 4));
42
43 // Sort the array in ascending order
44 std::sort(arr.begin(), arr.end());
45
46 // Reverse the order of the elements in the array
47 std::reverse(arr.begin(), arr.end());
48
49 // Concatenate two arrays into a new array
50 std::array<int, 3> arr2 = {6, 7, 8};
51 std::array<int, 8> concatenated;
52 std::copy(arr.begin(), arr.end(), concatenated.begin());
53 std::copy(arr2.begin(), arr2.end(), concatenated.begin()+arr.size());
54
55 // Slice a portion of the array into a new array
56 std::array<int, 2> slice;
57 std::copy(arr.begin()+1, arr.begin()+3, slice.begin());
58
59 // Iterate over the elements of the array
60 for (auto i : arr) {
61 std::cout << i << std::endl;
62 }
63
64 return 0;
65}
In C++, std::vector
is a dynamic container that is implemented as a wrapper around a dynamically-allocated array. The memory for a std::vector is allocated on the heap, and it grows or shrinks dynamically as elements are added or removed.
When a std::vector
is created, it allocates a block of memory on the heap to hold the elements of the vector. The size of this block of memory is determined by the vector’s capacity, which is the maximum number of elements that the vector can hold without allocating more memory. By default, a vector’s capacity is 0, and it grows dynamically as elements are added.
When a std::vector
is resized (by calling resize()
or by inserting or erasing elements), it may need to allocate more memory on the heap. To avoid reallocating memory too often, which can be expensive, a std::vector
typically over-allocates by reserving more memory than it needs. The amount of over-allocation is implementation-dependent, but it is usually at least enough to double the capacity of the vector.
Here’s an example of creating and using a std::vector
:
1#include <vector>
2#include <iostream>
3
4int main() {
5 std::vector<int> v; // create an empty vector
6 v.reserve(10); // reserve space for 10 integers (but size is still 0)
7
8 // add some elements to the vector
9 for (int i = 0; i < 5; i++) {
10 v.push_back(i); // append an element to the vector
11 }
12
13 // iterate over the elements of the vector
14 for (auto it = v.begin(); it != v.end(); ++it) {
15 std::cout << *it << std::endl;
16 }
17
18 return 0;
19}
In this example,
the
std::vector
is created with a default capacity of 0reserve()
is called to allocate space for 10 integers on the heappush_back()
is called to add elements to the vectorthe vector grows dynamically as needed
elements of the vector are iterated over using a forward iterator
1#include <iostream>
2#include <vector>
3#include <algorithm>
4
5int main() {
6 // Declare a vector with initial values
7 std::vector<int> vec = {1, 2, 3, 4, 5};
8
9 // Access an element by index
10 std::cout << vec[0] << std::endl; // prints 1
11
12 // Modify an element by index
13 vec[0] = 10; // vec is now {10, 2, 3, 4, 5}
14
15 // Get the length of the vector
16 std::cout << vec.size() << std::endl; // prints 5
17
18 // Add an element to the end of the vector
19 vec.push_back(6); // vec is now {10, 2, 3, 4, 5, 6}
20
21 // Remove and return the last element of the vector
22 int last_element = vec.back();
23 vec.pop_back(); // vec is now {10, 2, 3, 4, 5}
24
25 // Add an element to the beginning of the vector
26 vec.insert(vec.begin(), 0); // vec is now {0, 10, 2, 3, 4, 5}
27
28 // Remove and return the first element of the vector
29 int first_element = vec.front();
30 vec.erase(vec.begin()); // vec is now {10, 2, 3, 4, 5}
31
32 // Find the index of a given element in the vector
33 auto itr = std::find(vec.begin(), vec.end(), 4);
34 int index = std::distance(vec.begin(), itr);
35
36 // Sort the vector in ascending order
37 std::sort(vec.begin(), vec.end());
38
39 // Reverse the order of the elements in the vector
40 std::reverse(vec.begin(), vec.end());
41
42 // Concatenate two vectors into a new vector
43 std::vector<int> vec2 = {6, 7, 8};
44 std::vector<int> concatenated;
45 concatenated.reserve(vec.size() + vec2.size());
46 concatenated.insert(concatenated.end(), vec.begin(), vec.end());
47 concatenated.insert(concatenated.end(), vec2.begin(), vec2.end());
48
49 // Slice a portion of the vector into a new vector
50 std::vector<int> slice(vec.begin()+1, vec.begin()+3);
51
52 // Iterate over the elements of the vector
53 for (auto i : vec) {
54 std::cout << i << std::endl;
55 }
56
57 return 0;
58}
In C++, std::forward_list
is a dynamic container that is implemented as a singly linked list. Each element in the list is stored in a separate node on the heap, and each node contains a pointer to the next node in the list.
When a std::forward_list
is created, it allocates memory on the heap to hold the first node of the list. Each subsequent node is allocated as needed when elements are added to the list. When an element is added to the list, a new node is allocated on the heap to hold the element, and the new node’s pointer is inserted into the list by updating the pointers of the existing nodes.
Here’s an example of creating and using a std::forward_list
:
1#include <forward_list>
2#include <iostream>
3
4int main() {
5 std::forward_list<int> flist; // create an empty forward list
6
7 // add some elements to the list
8 for (int i = 0; i < 5; i++) {
9 flist.push_front(i); // insert an element at the beginning of the list
10 }
11
12 // iterate over the elements of the list
13 for (auto it = flist.begin(); it != flist.end(); ++it) {
14 std::cout << *it << std::endl;
15 }
16
17 return 0;
18}
In this example,
std::forward_list
is created with no elementspush_front()
is called to insert elements at the beginning of the listelement is stored in a new node on the heap
the pointers of the existing nodes are updated to insert the new node into the list
elements of the list are iterated over using a forward iterator
Because std::forward_list
is implemented as a singly linked list, it has some advantages over other containers in terms of memory usage and performance for certain operations. However, it also has some limitations, such as the inability to access elements by index and the inability to iterate in reverse.
1#include <iostream>
2#include <forward_list>
3
4int main() {
5 // Declare a forward list with initial values
6 std::forward_list<int> flist = {1, 2, 3, 4, 5};
7
8 // Access the first element of the list
9 std::cout << flist.front() << std::endl; // prints 1
10
11 // Modify the first element of the list
12 flist.front() = 10; // flist is now {10, 2, 3, 4, 5}
13
14 // Add an element to the beginning of the list
15 flist.push_front(0); // flist is now {0, 10, 2, 3, 4, 5}
16
17 // Remove the first element of the list
18 flist.pop_front(); // flist is now {10, 2, 3, 4, 5}
19
20 // Insert an element after a given position in the list
21 auto pos = flist.begin();
22 std::advance(pos, 2);
23 flist.insert_after(pos, 6); // flist is now {10, 2, 6, 3, 4, 5}
24
25 // Remove an element after a given position in the list
26 pos = flist.begin();
27 std::advance(pos, 3);
28 flist.erase_after(pos); // flist is now {10, 2, 6, 4, 5}
29
30 // Find the position of a given element in the list
31 pos = std::find(flist.begin(), flist.end(), 4);
32
33 // Iterate over the elements of the list
34 for (auto i : flist) {
35 std::cout << i << std::endl;
36 }
37
38 return 0;
39}
In C++, std::list
is a dynamic container that is implemented as a doubly linked list. Each element in the list is stored in a separate node on the heap, and each node contains pointers to the previous and next nodes in the list.
When a std::list
is created, it allocates memory on the heap to hold the first node of the list. Each subsequent node is allocated as needed when elements are added to the list. When an element is added to the list, a new node is allocated on the heap to hold the element, and the new node’s pointers are inserted into the list by updating the pointers of the existing nodes.
1#include <list>
2#include <iostream>
3
4int main() {
5 std::list<int> l; // create an empty list
6
7 // add some elements to the list
8 for (int i = 0; i < 5; i++) {
9 l.push_back(i); // insert an element at the end of the list
10 }
11
12 // iterate over the elements of the list
13 for (auto it = l.begin(); it != l.end(); ++it) {
14 std::cout << *it << std::endl;
15 }
16
17 return 0;
18}
In this example,
std::list
is created with no elementspush_back()
is called to insert elements at the end of the listelement is stored in a new node on the heap
the pointers of the existing nodes are updated to insert the new node into the list
elements of the list are iterated over using a bidirectional iterator.
Because std::list
is implemented as a doubly linked list, it has some advantages over other containers in terms of flexibility and performance for certain operations. However, it also has some disadvantages, such as higher memory overhead due to the need for extra pointers and slower performance for accessing elements by index.
1#include <iostream>
2#include <list>
3
4int main() {
5 // Declare a list with initial values
6 std::list<int> mylist = {1, 2, 3, 4, 5};
7
8 // Access an element by iterator
9 std::list<int>::iterator it = mylist.begin();
10 std::advance(it, 2);
11 std::cout << *it << std::endl; // prints 3
12
13 // Modify an element by iterator
14 *it = 10; // mylist is now {1, 2, 10, 4, 5}
15
16 // Get the length of the list
17 std::cout << mylist.size() << std::endl; // prints 5
18
19 // Add an element to the end of the list
20 mylist.push_back(6); // mylist is now {1, 2, 10, 4, 5, 6}
21
22 // Remove the last element of the list
23 mylist.pop_back(); // mylist is now {1, 2, 10, 4, 5}
24
25 // Add an element to the beginning of the list
26 mylist.push_front(0); // mylist is now {0, 1, 2, 10, 4, 5}
27
28 // Remove the first element of the list
29 mylist.pop_front(); // mylist is now {1, 2, 10, 4, 5}
30
31 // Insert an element before a given position in the list
32 it = mylist.begin();
33 std::advance(it, 3);
34 mylist.insert(it, 6); // mylist is now {1, 2, 10, 6, 4, 5}
35
36 // Remove an element at a given position in the list
37 it = mylist.begin();
38 std::advance(it, 2);
39 mylist.erase(it); // mylist is now {1, 2, 6, 4, 5}
40
41 // Find the position of a given element in the list
42 it = std::find(mylist.begin(), mylist.end(), 4);
43
44 // Iterate over the elements of the list
45 for (auto i : mylist) {
46 std::cout << i << std::endl;
47 }
48
49 return 0;
50}
Linked Lists#
Arrays#
Think about making insertions and deletions efficiently…
What is the computational cost of inserting or deleting 1 element?
In C++, arrays are fixed-size data structures, which means that the size of the array is determined at the time of declaration and cannot be changed afterwards. Therefore, inserting or deleting elements in an array requires shifting the existing elements to create or close gaps.
Here are the computational costs of inserting and deleting from the rear, front, and at an index of an array:
Inserting or deleting from the rear of an array takes constant time \(O(1)\)
only requires updating the index of the last element in the array.
Inserting or deleting from the front of an array takes linear time \(O(n)\) where
n
is the number of elements in the arrayrequires shifting all existing elements by one position to make room for the new element or to close the gap left by the deleted element.
Inserting or deleting at an arbitrary index of an array takes linear time \(O(n)\), where
n
is the number of elements in the arrayrequires shifting all elements from the insertion or deletion index to the end of the array by one position to create or close gaps.
Note that in addition to the computational costs, inserting or deleting from an array also incurs memory costs, as the size of the array may need to be adjusted to accommodate the new or deleted elements. Also, if the array is dynamically allocated, memory allocation and de-allocation costs may apply.
rear?
front?
index?
Linked Lists#
Defined
Collections of sequential elements stored at non-contiguous locations in memory
Elements are stored in nodes
Nodes are connected by links
every node keeps a pointer to the next node
Can grow and shrink dynamically
Allow for fast insertions/deletions
Supported operations
Linked lists are just collections of sequential data
can insert
1 or more elements
- front, end, by index, by value (sorted lists)
can delete
1 or more elements
- front, end, by index, by value
can search
for a specific element
can get
an element at a given index
can traverse
the list
- visit all nodes and perform an operation (e.g. print or destroy)
Singly Linked List#
Note
Node Left: data
Node Right: memory location of next element in LL
Implementing a Singly Linked List#
Prerequisites
C++ Classes
Pointers
NULL
pointers
Dynamic Memory Allocation
new
delete
Pointers and Classes
dot notation (
.
)arrow notation (
->
)
Creating a Linked List#
class Node
1class Node
2{
3 private:
4 int data;
5 Node *next;
6 // private data/methods
7 // ...
8
9 public:
10 Node (int d);
11 ~Node();
12
13 Friend class List;
14};
class List
1class List
2{
3 private:
4 Node *head;
5 Node *tail;
6 // private data/methods
7 // ...
8
9 public:
10 List();
11 ~List();
12 // public methods
13 // ...
14};
Linked List Operations#
Create a new node
Assign its data value
Assign newly created node’s next ptr to current head reference. So, it points to the previous start node of the linked list address
Change the head reference to the new node’s address.
Create a new node
Assign its data value
Assign its next node to NULL as this will be the last(tail) node
Check if the list is empty
Change the head node to the new node
If not then traverse till the last node
Assign the last node’s next pointer to this new node
Now, the new node has become the last node.
First we will create a new node named by newnode and put the position where u want to insert the node.
Now give the address of the new node in previous node means link the new node with previous node.
After this, give the address of current node in new node.Means link your new node also with current node.
Check if the Linked List is empty as we can not delete from an empty Linked List
Check if the Linked List has only one Node
In this case, just point the head to NULL and free memory for the existing node
Otherwise, if the linked list has more than one node, traverse to the end of the Linked List
Point next of 2nd Last node to NULL
Free the memory for the last node.
First we will create a new node named by newnode and put the position where u want to insert the node.
Now give the address of the new node in previous node means link the new node with previous node.
After this, give the address of current node in new node.Means link your new node also with current node.
Insert the initial items in the linked list
Calculate the current size of the linked list
Ask the user for nth position he wants to delete
if(n < 1 || n > size) then say invalid
If deleting the first node, just change the head to the next item in Linked List
Else traverse to the nth node to delete
Change the next of (n-1)th node to (n+1)th node
Free the memory for th nth node
Images: Prepinsta
Linked Lists : Variations#
Pseudocode
1// Define a Node class to represent each node in the circular singly-linked list
2class Node {
3public:
4 T data; // data stored in the node
5 Node* next; // pointer to the next node in the list
6
7 // constructor to initialize a new node with the given data and next pointer
8 Node(T data, Node* next = nullptr) {
9 this->data = data;
10 this->next = next;
11 }
12};
13
14// Define the CircularSinglyLinkedList class to represent the circular singly-linked list
15class CircularSinglyLinkedList {
16private:
17 Node* tail; // pointer to the last node in the list
18
19public:
20 // constructor to initialize an empty list with a null tail pointer
21 CircularSinglyLinkedList() {
22 tail = nullptr;
23 }
24};
Note a circular singly-linked list has the following built-in functions:
empty()
: returns true if the list is empty, false otherwise.size()
: returns the number of elements in the list.push_front(const T& value)
: inserts a new node with the given value at the front of the list.pop_front()
: removes the first node from the list.front()
: returns a reference to the data stored in the first node of the list.insert_after(Node* position, const T& value)
: inserts a new node with the given value after the node pointed to by position.erase_after(Node* position)
: removes the node after the node pointed to by position.clear()
: removes all nodes from the list.rotate()
: rotates the list by moving the first element to the end of the list.
Pseudocode
1// Node of a doubly linked list
2class Node {
3public:
4 int data;
5
6 // Pointer to next node in DLL
7 Node* next;
8
9 // Pointer to previous node in DLL
10 Node* prev;
11};
Pseudocode
https://www.geeksforgeeks.org/doubly-circular-linked-list-set-1-introduction-and-insertion/
1// Define a Node class to represent each node in the circular doubly-linked list
2class Node {
3public:
4 T data; // data stored in the node
5 Node* prev; // pointer to the previous node in the list
6 Node* next; // pointer to the next node in the list
7
8 // constructor to initialize a new node with the given data and next pointer
9 Node(T data, Node* next = nullptr) {
10 this->data = data;
11 this->prev = prev;
12 this->next = next;
13 }
14};
15
16// Define the CircularDoublyLinkedList class to represent the circular doubly-linked list
17class CircularDoublyLinkedList {
18private:
19 Node* tail; // pointer to the last node in the list
20
21public:
22 // constructor to initialize an empty list with a null tail pointer
23 CircularDoublyLinkedList() {
24 head = nullptr;
25 tail = nullptr;
26 size = 0;
27 }
28};
Time Complexities#
array v. linked list
access |
search |
insert |
remove |
|
---|---|---|---|---|
array |
\(O(1)\) |
\(O(1)\) |
\(O(1)\) |
\(O(1)\) |
singly linked list |
\(O(1)\) |
\(O(1)\) |
\(O(1)\) |
\(O(1)\) |
doubly linked list |
\(O(1)\) |
\(O(1)\) |
\(O(1)\) |
\(O(1)\) |
access |
search |
insert |
remove |
|
---|---|---|---|---|
array |
\(\color{red}{O(1)}\) |
\(O(n)\) |
\(O(n)\) |
\(O(n)\) |
singly linked list |
\(O(n)\) |
\(O(n)\) |
\(O(n)\) |
\(O(n)\) |
doubly linked list |
\(O(n)\) |
\(O(n)\) |
\(\color{red}{O(1)}\) |
\(\color{red}{O(1)}\) |
access |
search |
insert |
remove |
|
---|---|---|---|---|
array |
\(\color{red}{O(1)}\) |
\(O(n)\) |
\(O(n)\) |
\(O(n)\) |
singly linked list |
\(O(n)\) |
\(O(n)\) |
\(\color{red}{O(1)}\) |
\(\color{red}{O(1)}\) |
doubly linked list |
\(O(n)\) |
\(O(n)\) |
\(\color{red}{O(1)}\) |
\(\color{red}{O(1)}\) |