Understanding Data Structures: Linked Lists

Understanding Data Structures: Linked Lists

Linked Lists

The most commonly used data structure; arrays, had quite an influence on computation and programming over the years but other data structures are working under the hood to keep the pillars of computation standing and I would be explaining one of those data structures; the linked list.

Being one of the most commonly used data structures, you might have heard of or encountered the Linked List data structure. Linked Lists are the most popular data structure after arrays; they have contributed a lot to computation and you can find them in almost every digital system architecture. So, the big question is what are Linked Lists? I bet you have been hearing a lot about them but in this article, I’ll be explaining the types, their implementation and how they can be used in your project.

Definition

A Linked List is a linear data structure where each node contains a pointer to the next or previous node in its sequence.

A graphical representation:

A Graphical Representation

Types Of Linked Lists

Singly Linked List:

  • This is a type of linked list where the pointer goes in one direction, which is usually forward. Each node references the next node in front of it but the one in the front can't reference the ones behind.

  • Example:

Implementation

To implement the singly Linked List data structure we would start with creating a node class for all the nodes that would be in the linked list. The node class of a singly linked list has three important attributes which are the 'value' which points to the node's value, the 'next' attribute which has a reference to the next node in the structure and the 'index' which stores the position of each node in the structure.

Your Node Class:

class Node:
    def __init__(self,value,next,index):
      self.value=value
      self.next=next
      self.index=index

The next step is to create the singly linked list structure itself; the singly linked class is used to arrange the nodes in a particular pattern i.e. the singly linked list pattern. The class has two attributes which are the 'list' attribute which contains all the nodes and the 'length' attribute which contains the length of the linked list.

The Singly Linked Class:

class SinglyLinked:
   def __init__(self):
      self.list=None
      self.length=0

After creating the singly linked class we need to create some methods that would make the class functional, and one of those methods is the insert method which inserts nodes into the 'list' attribute.

The Insert Method:

def insert(self,value):
      node=Node(value,self.list,self.length) 
      self.list=node
      self.length+=1

The insert method is the most compulsory method a Linked List ought to have because it is what adds new elements to the Linked List, but I'll be adding a few more methods that will be used in the Linked List such as print(),getIndex(), and remove() methods etc.

The getIndex Method:

 def getIndex(self,index):
      data=self.list
      ans='Index Not Found'
      while data:
         if data.index==index:
            ans=data
            break
         else:
            data=data.next
      return ans

This method is used to get an individual node at a given index and it finds them using the node's index attribute and a while loop that iterates through the linked lists. The data variable is used to clone the Linked List collection by assigning it to the self.list attribute of the class; the reason why we clone the collection is to prevent it from being tampered with during the iteration. The while loop runs while data is not null, remember the last element of the Singly Linked List is null; so, once data is null it means we have reached the end of the list. The if statement is used to check whether the current data index attribute is equal to the index we are searching for, if the condition is then satisfied we assign the ans variable to the current data and break the while loop, else the current data would be set to the next node. When the loop finishes running it returns the value of the ans variable.

The find() Method:

This method is similar to the getIndex method, but it has a slight difference. The getIndex() method tries to match the index of the list nodes while the find() method tries to match the values of the nodes. It is used to check if a node is present in the linked list.

  def find(self,val):
      data=self.list 
      ans='Not Found'   
      while data:
         if data.value==val:
            ans='Found'
            break
         else:
            data=data.next
      return ans

The remove() Method:

This method is the most complex of the previous methods but it is easy to understand, trust me. First of all, we need to create a helper method in our Node Class named delete(); this method would be used to set the next node of an individual node to its next next node😆 Confused right? let me explain. For example, we have a node named A and its next node is B and the next node of B is C, when the delete() method is called on A it would set B to become C. Easy stuff!

class Node:
    def __init__(self,value,next,index):
      self.value=value
      self.next=next
      self.index=index
    def delete(self):
      self.next=self.next.next

This is just the first part of the remove() method.

The remove() would be written in the Singly Linked Class and it would be used to remove nodes by getting the index of the previous node and calling the delete method on it. Hurray! 😸

Then we use a while loop to decrement the index of every node lesser than the node's index.

  def remove(self,index):
      self.getIndex(index+1).delete()
      while data:
         if data.index<index:
            break
         data.index-=1
         data=data.next

Now this is what your code should look like:

class Node:
    def __init__(self,value,next,index):
      self.value=value
      self.next=next
      self.index=index
    def delete(self):
      self.next=self.next.next

class SinglyLinked:
   def __init__(self):
      self.list=None
      self.length=0

   def insert(self,value):
      node=Node(value,self.list,self.length)
      self.list=node
      self.length+=1

   def find(self,val):
      data=self.list 
      ans='Not Found'   
      while data:
         if data.value==val:
            ans='Found'
            break
         else:
            data=data.next
      return ans

   def getIndex(self,index):
      data=self.list
      ans='Index Not Found'
      while data:
         if data.index==index:
            ans=data
            break
         else:
            data=data.next
      return ans

   def remove(self,index):
      self.getIndex(index+1).delete()
      while data:
         if data.index<index:
            break
         data.index-=1
         data=data.next

   def print(self):
      data=self.list
      res=''
      while data:
         res+=f'{data.index}->{data.value } \n'
         data=data.next
      return print(res)

Doubly Linked List:

This is a type of Linked List where each node has two references, one to the node in its front and the other to the node behind.

Implementation

To implement the Doubly Linked List data structure we would start with creating a node class for all the nodes that would be in the linked list. The node class of a doubly linked list has four important attributes which are the 'value' which points to the node's value, the 'next' attribute which has a reference to the next node in the structure, the 'prev' attribute which has a reference to the previous node in the structure and the 'index' which stores the position of each node in the structure.

The Node Class:

class Node:
    def __init__(self,val,next,prev,index):
        self.val=val
        self.next=next
        self.prev=prev
        self.index=index

The next step is to create the doubly linked list structure itself; the doubly linked class is used to arrange the nodes in the doubly linked pattern just as we did in the Singly Linked List implementation above.

class DoublyLinked:
    def __init__(self):
        self.head=None
        self.length=0

We would be repeating the same methods we used in the Singly Linked List here but there would be a little variation in the insert() of the Doubly Linked List.

insert() Method:

def insert(self,value):
        node= Node(value,self.head,None,self.length)
        if self.head:
            self.head.prev=node
        self.head=node
        self.length+=1

Just like the Singly Linked List(SLL) we, first of all, instantiate the node class in our method and in the previous parameter we set it to "None"; then we make a conditional that checks if the head is not null so that we can set the current node's prev attribute to the node about to be inserted.

Every other method except the remove() from here henceforth follows the same pattern as the SLL methods, so make sure you read the explanations of the SLL methods above.

find() Method:

  def find(self,val):
      data=self.head
      ans='Not Found'   
      while data:
         if data.val==val:
            ans='Found'
            break
         else:
            data=data.next
      return ans

No variations 😁

getIndex() Method:

  def getIndex(self,index):
       data=self.head
       while data:
          if data.index==index:
             break
          else:
             data=data.next
       if data:
        return data
       else:
          return 'Index Not Found'

print() Method:

  def print(self):
        data=self.head
        listed=''
        while data:
            if data.next:
             if data.prev:
              listed+=f'''
                ---------------
                next: {data.next.val}
                value:{data.val}
                prev: {data.prev.val}
                ------------------
                    |
                   \ /
                '''
             else:
                listed+=f'''
                ---------------
                next: {data.next.val}
                value:{data.val}
                prev: Null
                ------------------
                    |
                   \ /
                '''
            else:
                listed+=f'''
                   / \
                    |
                ---------------
                next: Null
                value:{data.val}
                prev:{data.prev.val}
                ------------------
                '''
            data=data.next
        return print(listed)

Not minding all the long lines of codes in this method, this method is super easy all it does is traverse through the list with a while loop that checks if the current node is null, because if it is that means we are at the end of the list. That's why data is set to data.next at every iteration.

remove() Method:

Just like the SLL we would go to the node class and create the delete method; this is where the variation is. Remember in the SLL class we were setting the next node to its next node because it is one-dimensional, but the Doubly Linked List on the other hand is two dimensional that is we would be setting the next node to its next node and we would be setting that next next node's previous attribute to the node calling the method. Ughhh! 😣 Pretty complicated right? Look at the implementation and you'll understand.

class Node:
    def __init__(self,val,next,prev,index):
        self.val=val
        self.next=next
        self.prev=prev
        self.index=index

    def delete(self):
      self.next=self.next.next
      self.next.prev=self #the next next node's prev attribute

The remove() method used in the DLL class remains the same as its SLL counterpart.

def remove(self,index):
      self.getIndex(index+1).delete()
      data=self.head
      self.length-=1
      while data:
         if data.index<index:
            break
         data.index-=1
         data=data.next

The End Result

Your code should look like this:

class Node:
    def __init__(self,val,next,prev,index):
        self.val=val
        self.next=next
        self.prev=prev
        self.index=index

    def delete(self):
      self.next=self.next.next
      self.next.prev=self


class DoublyLinked:
    def __init__(self):
        self.head=None
        self.length=0
    def insert(self,value):
        node= Node(value,self.head,None,self.length)
        if self.head:
            self.head.prev=node
        self.head=node
        self.length+=1
    def print(self):
        data=self.head
        listed=''
        while data:
            if data.next:
             if data.prev:
              listed+=f'''
                ---------------
                next: {data.next.val}
                value:{data.val}
                prev: {data.prev.val}
                ------------------
                    |
                   \ /
                '''
             else:
                listed+=f'''
                ---------------
                next: {data.next.val}
                value:{data.val}
                prev: Null
                ------------------
                    |
                   \ /
                '''
            else:
                listed+=f'''
                   / \
                    |
                ---------------
                next: Null
                value:{data.val}
                prev:{data.prev.val}
                ------------------
                '''
            data=data.next
        return print(listed)
    def find(self,val):
      data=self.head
      ans='Not Found'   
      while data:
         if data.val==val:
            ans='Found'
            break
         else:
            data=data.next
      return ans
    def getIndex(self,index):
       data=self.head
       while data:
          if data.index==index:
             break
          else:
             data=data.next
       if data:
        return data
       else:
          return 'Index Not Found'

    def remove(self,index):
      self.getIndex(index+1).delete()
      data=self.head
      self.length-=1
      while data:
         if data.index<index:
            break

         data.index-=1
         data=data.next





test=DoublyLinked()
test.insert(2)
test.insert(3)
test.insert(5)
test.insert(6)
test.insert(8)
test.remove(3)
test.print()

'''
result:
      ---------------
                next: 5
                value:8
                prev: Null
                ------------------
                    |
                   \ /

                ---------------
                next: 3
                value:5
                prev: 8
                ------------------
                    |
                   \ /

                ---------------
                next: 2
                value:3
                prev: 5
                ------------------
                    |
                   \ /


                ---------------
                next: Null
                value:2
                prev:3
                ------------------


'''

Satisfying 😻

Circular Linked List:

This Linked List is a variation of the Singly Linked List. Each node in a circular linked list has a pointer to the next node including the last node which has a pointer to the first node of the list.

Implementation

The Circular Linked List(CLL) is very much similar to the Singly Linked List(SLL) but it varies in methods that use while loops because nodes can never be null meaning that the loop can't be broken by the null condition because in the CLL the next pointer at the last node points to the first and keeps it infinitely linked like a ring. So, we use a different approach to break the loops by setting a conditional that checks if the node's index is 0.

Node Class:

class Node:
    def __init__(self,value,next,index):
      self.value=value
      self.next=next
      self.index=index

    def delete(self):
      self.next=self.next.next

insert() Method:

class CircularLinked:
   def __init__(self):
      self.list=None
      self.length=0

   def insert(self,value):
      node=Node(value,self.list,self.length)
      self.list=node
      self.length+=1
      first=self.getIndex(0)
      first.next=node

   def getIndex(self,index):
      data=self.list

      while data:
         if data.index==index:
            break
         if data.index==0:
            break
         else:
            data=data.next 
      if data.index==index:
       return data
      else:
         return 'Index Not Found'

To set the first node's next attribute to the last node we use the getIndex() method to fetch the first node and set its next attribute to the node currently being inserted at the moment the insert() method is called.

Every other method remains the same except for the break loop condition when the node index is 0.

The Code Results:

class Node:
    def __init__(self,value,next,index):
      self.value=value
      self.next=next
      self.index=index

    def delete(self):
      self.next=self.next.next

class CircularLinked:
   def __init__(self):
      self.list=None
      self.length=0

   def insert(self,value):
      node=Node(value,self.list,self.length)
      self.list=node
      self.length+=1
      first=self.getIndex(0)
      first.next=node


   def find(self,val):
      data=self.list
      ans='Not Found'   
      while data:
         if data.value==val:
            ans='Found'
            break
         else:
            data=data.next
      return ans
   def remove(self,index):
      self.getIndex(index+1).delete()

   def getIndex(self,index):
      data=self.list

      while data:
         if data.index==index:
            break
         if data.index==0:
            break
         else:
            data=data.next 
      if data.index==index:
       return data
      else:
         return 'Index Not Found'

   def print(self):
      data=self.list
      string='\n Circular Linked List'
      while data:
         if data.index== (0):
            string+=f'''
        ------------------------- 
        index:{data.index}
        value: {data.value}
        next:({data.next.value})
        -------------------------
        '''
            break

         else:
             string+=f'''
        ------------------------- 
        index:{data.index}
        value: {data.value}
        next:({data.next.value})
        -------------------------
         |
        \ /
        '''
             data=data.next
      return print(string)


test=CircularLinked()
test.insert(3)
test.insert(13)
test.insert(23)
test.insert(33)
test.insert(43)
test.print()

'''
Result:

 Circular Linked List
        -------------------------
        index:4
        value: 43
        next:(33)
        -------------------------
         |
        \ /

        -------------------------
        index:3
        value: 33
        next:(23)
        -------------------------
         |
        \ /

        -------------------------
        index:2
        value: 23
        next:(13)
        -------------------------
         |
        \ /

        -------------------------
        index:1
        value: 13
        next:(3)
        -------------------------
         |
        \ /

        -------------------------
        index:0
        value: 3
        next:(43)
        -------------------------

'''

Double Circular Linked List:

This is a variation of the doubly linked list where every node has a pointer to its previous and next node including the first and the last node which have references to each other.

Implementation

It is just like the Doubly Linked List in a circular format. Its methods are similar to the DLL methods but like the SCLL it has the 0 node index conditional for breaking the loops and its insert method is similar to the SCLL's insert method.

class Node:
     def __init__(self,val,next,prev,index):
        self.value=val
        self.next=next
        self.prev=prev
        self.index=index

     def delete(self):
      self.next=self.next.next
      self.next.prev=self


class CircularLinked:
   def __init__(self):
      self.list=None
      self.length=0

   def insert(self,value):
      node=Node(value,self.list,None,self.length)
      if self.list:
            self.list.prev=node
      self.list=node
      self.length+=1
      first=self.getIndex(0)
      first.next=node
      self.list.prev=first

   def find(self,val):
      data=self.list
      ans='Not Found'   
      while data:
         if data.value==val:
            ans='Found'
            break
         else:
            data=data.next
      return ans

   def getIndex(self,index):
      data=self.list

      while data:
         if data.index==index:
            break
         if data.index==0:
            break
         else:
            data=data.next 
      if data.index==index:
       return data
      else:
         return 'Index Not Found'
   def remove(self,index):
      self.getIndex(index+1).delete()

   def print(self):
      data=self.list
      string='\nDOUBLY CIRCULAR LINKED LIST'
      while data:
        if data.index!=0:
         string+=f'''
        -------------------------
        prev: ({data.prev.value})
        index: {data.index}
        value: {data.value}
        current ({data.next.value})
        -------------------------
         |
        \ /
        '''
        if data.index==0:
            string+=f'''
        ------------------------
        prev: ({data.prev.value})
        index: {data.index}
        value: {data.value}
        next: ({data.next.value})
        ---------------------------
        '''
            break


        data=data.next
      return print(string)
   def remove(self,index):
      self.getIndex(index+1).delete()

      self.length-=1



test=CircularLinked()
test.insert(3)
test.insert(13)
test.insert(23)
test.insert(33)
test.insert(43)
test.print()

'''
Result:

DOUBLY CIRCULAR LINKED LIST
        -------------------------
        prev: (3)
        index: 4
        value: 43
        current (33)
        -------------------------
         |
        \ /

        -------------------------
        prev: (43)
        index: 3
        value: 33
        current (23)
        -------------------------
         |
        \ /

        -------------------------
        prev: (33)
        index: 2
        value: 23
        current (13)
        -------------------------
         |
        \ /

        -------------------------
        prev: (23)
        index: 1
        value: 13
        current (3)
        -------------------------
         |
        \ /

        ------------------------
        prev: (13)
        index: 0
        value: 3
        next: (43)
        ---------------------------

'''

This is just an excerpt from my GitHub Repo on Data Structures and Algorithms, I will be dropping detailed explanations of other data structures and algorithms on the repo.

Stay Tuned, Thank You.