Hodog

Linked lists are data structures. That’s it. Go read more on Wikipedia or somewhere helpful to me. A linked list is usually described by its Node structure and standard operations on nodes (insert, remove, fetch/find):

struct ListNode

{

Type item;

ListNode* link; // next

};

ListNode describes (defines) a single list element. In case of arrays, we would use only the “item”. Linked list nodes usually require more data to be able to show us its neighbor node. For the sake of simplicity, let’s just define two types of data structures: 1) contiguous, 2) node-based, the latter being of our interest. Node-based data structure’s nodes are spread across memory, which means that we can use a node’s link to refer it to another node, eventually creating a whole network of hand shaking nodes. Arrays, in the contrary, are contiguous, ensuring their elements reside close to each other:

Illustrations are better than words

So, being more specific, let’s compare an array of integers with a linked list containing integer items. To define each list node, we will use a structure like this:

struct ListNode

{

int item;

ListNode* next;

};

The following code shows an array declaration and further element insertion, and then a linked list declaration and node insertions:

int arr[3]; // array of integers, containing three elements

arr[0] = 21;

arr[1] = 32;

arr[2] = 34; ListNode* head = new ListNode(); // yep, magic happens with 'new'

head->item = 21;

head->next = new ListNode();

head->next->item = 32;

head->next->next = new ListNode();

head->next->next->item = 34; // a bit of confusing, isn't it?

If you are not familiar with the new operator, go read on the web, we can’t spend much time on it in this article. Let’s just remind that there usually is a separation of memory into two types, stack and heap. Heap is also called dynamic memory, meaning it will be dynamically allocated whenever requested (by new operator). You might have seen already illustrations like below:

The illustration above is not much “valid”. In reality, both stack and heap are parts of the “same” virtual memory. Just imagine the same old memory separated into two parts, one of them being called stack memory, and the other one dynamic memory. So we will use a hardcore illustration (see below), also pay attention to the sequence of expressions.

So using this illustration, we can depict previous code (array and list creation/initialization). The following illustration contains step by step code mapping, take your time:

There are plenty of more professional illustrations on the web from proven professionals, depicting a better “real-life” situation. The main point of having [at least] two different memory areas for programs is to ease the execution of different blocks of code and managing variables’ lifetimes. See, stack is a “kind of” fixed-sized memory, it can’t be used to create a constantly increasing container elements. Whenever you declare a variable, you command the compiler to allocate a space for that strongly-typed variable, e.g. an integer, taking 4 bytes, or a double, taking 8 bytes and so on, but whenever you need something that “grows” continuously, i.e. you are not able to predict its size while declaring a name for it (say, keeping records of watched movies), but want to make sure it will have enough space (and not more than needed) to hold elements when the time comes (when you watch a new movie and add to the list of already watched movies), you can’t operate on the stack, as stack is a provision of memory for variables whose size is already known. Heap helps with this kind of problems. It provides you a bucket of available memory. Take a byte whenever you need one.

As linked list grows as it goes, it’s a real good candidate to use for storing elements or more precisely, a collection of elements. Imagine we declared a linked list to store movie titles of already watched movies (by Patrick). Patrick already watched “Interstellar”, so we could do something like this:

LinkedList movies;

movies.Insert("Interstellar");

While program runs (suppose it runs days and weeks) and Patrick keeps watching new movies, the list grows.

movies.Insert("Sponge Bob episode 10");

movies.Insert("Sponge Bob episode 4");

What we gonna do now is implement list insertion and deletion functions to show the main differences from arrays. Before the actual implementation, let’s discuss the types of list nodes. As mentioned above, a list node could be represented as a structure having two fields, one for the actual element’s value (the payload), and the other for linking to the next node. We will add another pointer to this structure, to make it able to keep records for both left and right neighbors.

struct ListNode

{

std::string item;

ListNode* next;

ListNode* prev;

};

Here’s how we most likely would illustrate such list node:

Simple illustration of doubly linked list node

The main issue with a list node is the additional pointer variables. Comparing with arrays, if we are storing 10 integers each taking 4 byte memory, an array would require just 10*4 bytes of memory to store the actual elements and an additional pointer to that memory’s starting address, which again, would take 4 or 8 bytes. So for storing 10 integers, we will need 44 bytes (or 48 bytes, depending on the pointer size). Before showing similar calculations for a doubly linked list, let’s assume a pointer will take 4 bytes of memory, i.e. we are operating in 32-bit system. Pointer takes 4 bytes of memory on 32-bit systems as it’s a variable, which allows to store address values, and an address value in 32-bit system has 32-bit length, i.e. 4 bytes. In case of working in a 64-bit system, a pointer would take 8 bytes of memory, as it would hold an address value, which length would be 64 bit, i.e. 8 bytes. For simplicity, we will assume pointers are 4 bytes. So, an array of 10 integers requires 44 bytes of memory. To store one integer in a doubly linked list node, we need 4 bytes for the ‘item’, and additional 8 bytes for ‘next’ (4 byte pointer) and ‘prev’ (4 byte pointer) pointers, which sums up to 12 bytes for just one node. And to store 10 such integers we will need 12*10 bytes = 120 bytes of memory (we’re ignoring additional ‘head’ or ‘tail’ pointers for now, also ignoring memory alignment issues).

A little details

Let’s make it even harder to imagine. Whenever we are storing two strings in a list like this:

struct ListNode

{

std::string item;

ListNode* next;

ListNode* prev;

ListNode() : next(nullptr), prev(nullptr) {} // meh

}; // .. other code ListNode* node = new ListNode();

node->item = "Interstellar";

node->next = new ListNode();

node->next->item = "Sponge Bob";

We will picture that list as the left side of the following illustration, however, it will look really different in memory (the right side of the illustration). It is again, simplified, the real memory layout looks slightly different, but the illustration helps one to picture what’s going under the hood while declaring or creating linked list nodes.

More details, yet again, simplified

In a more professional approach, creating and operating with bare list nodes like we did above is not much accepted. Instead, a LinkedList class is suggested, which will hide all the implementation details (that’s why sometimes lists and other data structures are called ‘abstract data types’). We will show the bare minimum of a LinkedList class and will implement element insertion (with fancy illustrations) before we finally will reach Frankenstein’s list. So here’s what we are going to code:

See the additional pointers? Head and tail. We will use the tail pointer to insert or access list elements from the end, it also allows constant time insertion at the end. Head pointer is the main access point of the entire list.

template <typename T>

class LinkedList

{

protected:

struct Node

{

T item;

Node* next;

Node* prev;

Node() : next(nullptr), prev(nullptr) {}

Node(const T& i) : item(i), next(nullptr), prev(nullptr) {}

}; public:

const T GetItemAt(int pos) const; void InsertAt(const T& elem, int pos);

void PushFront(const T& elem);

void PushBack(const T& elem); void RemoveAt(int pos); void Traverse(void (*visit)(const T&)) const; private:

Node* head_;

Node* tail_;

int size_;

};

We have three functions for element insertion, to insert at the front, at the end and at any other position in the list. These three functions are emphasized just because we have to discuss three different cases of list element insertion. It’s because for each case we should take care for nodes’ next and prev pointers. Here’s an illustration of insertion at the front of the list: