Array and Linked List

Array and Linked List

TL;DR

ArrayLinked List
Store in memoryElements are stored in contiguous locations in memory.Elements can be stored anywhere in memory
SizeFixed, must be specified at the time of initializationChangeable, can grow/shrink by insertion/deletion
Access elementRandom access
$O(1)$
Sequence access
$O(n)$
Insert element$O(n)$$O(1)$
Delete element$O(n)$$O(1)$

Storing In Memory

Array store elements in contiguous memory locations/blocks.

By Linked list, elements (also called nodes) can be stored anywhere in memory. Each node stores its next node’s address (link/pointer to next node).

截屏2021-04-05 15.04.20

Operations

Access

Array

Accessing an element in Array is simple. As array is essentially a block of continuous memory, we can directly access an element using index.

截屏2021-04-05 15.09.54

In other words, accessing an element in the array is independent of the size of the array. Hence, time complexity is $O(1)$.

Linked List

However, accessing a node in Linked list is not that easy. Since nodes are scattered out in memory, we have to traverse the linked list from the head node until we reach the target node.

截屏2021-04-05 15.11.07

The worst case is that our target node is at the tail of the linked list. In this case we have to traverse the whole linked list. Time complexity is thus O(n).

Insert

Array

Inserting an element in the Array is a pain.

For example, we want to insert 11 in the 3rd position of array [1, 3, 5, 7, 9]. We need to

  1. allocate a new memory block of suitable size (in our case, 6),
  2. put new element 11 in the 3rd position, and copy other elements to the right place.
截屏2021-04-05 15.33.30

Linked list

Key of insertion is to maintain the order/sequence of nodes using links.

Insert in the middle

Let’s say we want to insert a new_node between left_node and right_node.

Before insertion:

left_node --> right_node

After insertion, the linked should look like:

left_node --> new_node --> right_node

Keeping this in mind, we have the idea of insertion:

new_node.next = left_node.next
left_node.next = new_node
截屏2021-04-05 22.47.40

Insert at head

By inserting a new node at the head of linked list, we need to specify the new head after the insertion.

Assume that we want to insert new_node before linked_list.head:

new_node.next = linked_list.head
linked_list.head = new_node

截屏2021-04-05 22.04.59

Insert at tail

By inserting a new node at the tail of linked list, we need to specify that the new node is the tail.

Assume that we want to insert new_node after tail_node.

Before insertion:

tail_node --> None

After insertion:

tail_node --> new_node --> None

Thus, the code of insertion should be:

# traverse linked list until reaching the tail node
tail_node.next = new_node
new_node.next = None

截屏2021-04-05 22.06.03

Delete

Array

Deleting of element from an array is similar to insertion. We need to allocate a new memory of suitable size, and copy remaining elements to the right place.

截屏2021-04-05 16.15.44

Linked list

Similar to insertion, we just need to maintain the order/sequence of nodes using links.

Insert a node in the middle

Let’s say we want to delete del_node between left_node and right_node.

Before deletion:

left_node --> del_node --> right_node

After deletion:

left_node --> right_node

Therefore, we just need to do the followings

left_node.next = right_node

Example:

截屏2021-04-05 16.17.07

Delete head node

Let’s say we want to delete linked_list.head. After deletion, we need to announce that the new head is linked_list.head.next.

linked_list.head = linked_list.head.next

截屏2021-04-05 22.08.27

Delete tail node

After deleting the tail node, the second last node becomes the new tail node.

Before deletion:

left_node --> tail_node --> None

After deletion:

left_node --> None

Therefore, we just need to do the followings

left_node.next = None

截屏2021-04-05 22.08.52

Array vs. Linked List

ArrayLinked List
Access elementFastSlow
Insert elementSlowFast
Delete elementSlowFast
  • By frequent retrieval/accessing, use Array
  • By frequent insertion/deletion, use Linked List

LinkedList in Python

Implement our own Linked List

To implement the linked list, we firstly need to implment a class for node, element of the list:

class Node:

    def __init__(self, data):
        self.data = data
        self.next = None
    
    def __repr__(self):
        return self.data

Then we implment the linked list:

class LinkedList:

    def __init__(self, data_list=None):
        self.head = None
        if data_list is not None:
            node = Node(data=data_list.pop(0))
            self.head = node
            for element in data_list:
                node.next = Node(data=element)
                node = node.next

    def __repr__(self):
        node = self.head
        nodes = list()
        while node is not None:
            nodes.append(node.data)
            node = node.next
        nodes.append("None")
        return " -> ".join(nodes)

    def __iter__(self):
        node = self.head
        while node is not None:
            yield node
            node = node.next

    def add_first(self, new_node):
        """
        Insert new_node at head
        """
        new_node.next = self.head
        self.head = new_node

    def add_last(self, new_node):
        """
        Insert new_node at tail
        """

        # If linked list is empty,
        # new node will be the only node in linked list,
        # i.e. head and tail node are the same
        if self.head is None:
            self.head = new_node
            return

        # If linked list is not empty:
        # 1. we need to traverse the whole list until we reach the current last node
        # 2. we add the new node as the next node of the current last node
        for current_node in self:
            pass
        current_node.next = new_node

    def add_after(self, target_node_data, new_node):
        """
        Insert new_node after the node whose data is target_node_data
        """
        if self.head is None:
            raise Exception("Linked list is empty.")
        
        for node in self:
            if node.data == target_node_data:
                new_node.next = node.next
                node.next = new_node
                return

        raise Exception(f"Node with data '{target_node_data}' not found.")

    def remove_node(self, target_node_data):
        """
        Remove node whose data is target_node_data
        """

        # If linked list is empty, then raise an exception
        if self.head is None:
            raise Exception("Linked list is empty.")

        # If the node to be removed is the current head,
        # then we want the next node in the list to become the new head
        if self.head.data == target_node_data:
            self.head = self.head.next
            return

        # If list is not empty and node to be removed is not the current head,
        # we traverse the list looking for the node to be removed.
        # If we find it, then we need to update its previous node to point to its next node
        previous_node = self.head
        for node in self:
            if node.data == target_node_data:
                previous_node.next = node.next
                return

            previous_node = node

        # If we traverse the whole list without finding the node to be removed,
        # then raise an exception
        raise Exception(f"Node with data '{target_node_data}' not found.")

Traverse

llist = LinkedList(["a", "b", "c", "d", "e"])
llist
a -> b -> c -> d -> e -> None
for node in llist:
    print(node)
a
b
c
d
e

Insert

llist = LinkedList()
llist
None

Insert at head:

llist.add_first(Node("a"))
llist
a -> None

Insert at tail:

llist.add_last(Node("b"))
llist
a -> b -> None
llist.add_last(Node("d"))
llist
a -> b -> d -> None

Insert in the middle:

llist.add_after("b", Node("c"))
llist
a -> b -> c -> d -> None

Remove

llist = LinkedList(["a", "b", "c", "d", "e"])
llist
a -> b -> c -> d -> e -> None

Remove head:

llist.remove_node("a")
llist
b -> c -> d -> e -> None

Remove tail:

llist.remove_node("e")
llist
b -> c -> d -> None

Remove node in the middle:

llist.remove_node("c")
llist
b -> d -> None

Double-ended Queue (Deque)

collections.deque uses an implementation of a linked list in which you can access, insert, or remove elements from the beginning or end of a list with constant 𝑂(1) performance.

  • Append/Remove element from the right side: append() / pop()

  • Append/Remove element from the left side: appendleft() / popleft()

Implment Queue using deque

For a queue, we use a First-In/First-Out (FIFO) approach. I.e., the first element inserted in the list is the first one to be retrieved.

queue = deque()
queue
deque([])
queue.append("Mary")
queue.append("John")
queue.append("Susan")
queue
deque(['Mary', 'John', 'Susan'])

Retrieve: (Order should be Mary --> John --> Susan)

queue.popleft()
'Mary'
queue.popleft()
'John'
queue.popleft()
'Susan'

Implment Stack using deque

For a stack, we use a Last-In/Fist-Out (LIFO) approach, meaning that the last element inserted in the list is the first to be retrieved.

queue = deque()
queue
deque([])
queue.append("Mary")
queue.append("John")
queue.append("Susan")
queue
deque(['Mary', 'John', 'Susan'])

Retrieve: (Order should be Susan --> John --> Mary)

queue.pop()
'Susan'
queue.pop()
'John'
queue.pop()
'Mary'

Reference