Linked Lists#

TL;DR

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.

https://hackingcpp.com/cpp/std/sequence_containers.png

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.

array

 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}

link: cppreference

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 0

  • reserve() is called to allocate space for 10 integers on the heap

  • push_back() is called to add elements to the vector

    • the vector grows dynamically as needed

  • elements of the vector are iterated over using a forward iterator

vector

 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}

link: cppreference

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 elements

  • push_front() is called to insert elements at the beginning of the list

  • element 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.

forward-list

 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}

link: cppreference

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 elements

  • push_back() is called to insert elements at the end of the list

  • element 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.

list

 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}

link: cppreference

Linked Lists#

Arrays#

Think about making insertions and deletions efficiently…

  • rear?

  • front?

  • index?

https://www.simplilearn.com/ice9/free_resources_article_thumb/Vaibhav-Arrays%20Article/Arrays_in_ds-what-is-array-img1.PNG

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#

https://media.geeksforgeeks.org/wp-content/uploads/20220816144425/LLdrawio.png

Note

Node Left: data
Node Right: memory location of next element in LL

Implementing a Singly Linked List#

Linked lists in C++

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

https://www.geeksforgeeks.org/what-is-linked-list/

Linked List Operations#

Algorithm of insertion at the beginning
  • 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.

../../_images/ll_ins_head.png
To insert element in linked list last we would use the following steps to insert a new Node at the last of the doubly linked list.
  • 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.

../../_images/ll_ins_tail.png
Insertion at After nth position 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.

../../_images/ll_ins_nth.png
Delete an element from end in singly linked list
  • 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.

../../_images/ll_del_tail.png
Insertion at After nth position 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.

../../_images/ll_del_head.png
Delete a Linked List node at a given position
  • 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/ll_del_nth.png

Images: Prepinsta

Linked Lists : Variations#

https://i1.faceprep.in/Companies-1/types-of-linked-list.png

Circular Singly Linked List

Pseudocode

https://media.geeksforgeeks.org/wp-content/uploads/20220817185024/CircularSinglyLinkedList.png
 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.

https://www.geeksforgeeks.org/circular-linked-list/?ref=lbp

Doubly Linked List

Pseudocode

https://media.geeksforgeeks.org/wp-content/cdn-uploads/gq/2014/03/DLL1.png
 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};

https://www.geeksforgeeks.org/doubly-linked-list/?ref=lbp

Circular Doubly Linked List

Pseudocode

https://media.geeksforgeeks.org/wp-content/uploads/20220830114920/doubly.jpg

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)}\)