Design a Doubly Linked List

Here we are opt to design a Doubly Linked List, basically designing a Data Structure. Unlike the Singly Linked List, the nodes in a Doubly Linked List has next and prev pointer. By using next pointer, Nodes are connected with it's Next Node and by using prev pointer Nodes are connected with it's Previous Node. In a Doubly Linked List, one can move forward from Head to Tail and also can move backward from Tail to Head.

A Doubly Linked List is ore like an Optimized version of Singly Linked List, we will see the usage of a Doubly Linked List in LRU Cache implementation shortly. Without further do, let's design a Doubly Linked List

Intuition

Imagine we already have a Doubly Linked List formed like this
Assets/Leetcode/LinkedList/doubly_linked_list/1.png
The Head is connected to the Tail using next pointer and the Tail is connected to the Head using prev pointer like this bellow
Assets/Leetcode/LinkedList/doubly_linked_list/2.png
Let's write the code for this above template.

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

class DoublyLinkedList:
	def __init__(self):
		self.size = 0
		# Dummy Head and Tail initialization
		self.head = Node(-1)
		self.tail = Node(-1)
		# Linking Dummy Head and Tail
		self.head.next = self.tail
		self.tail.prev = self.head

Now we are going to implement the features of the Linked List

Add at Head

This function will take an Integer number as create a new node with that value. Then it will add the new node after the Head
Assets/Leetcode/LinkedList/doubly_linked_list/3.png
Suppose the new node is created with value 2 and linked with the Next and Previous nodes

def addAtHead(self, value: int):
	node = Node(val=value, prev=self.head, next=self.head.next)

Now the Head and Next Node of the Head needs to be linked with the newly created Node
Assets/Leetcode/LinkedList/doubly_linked_list/4.png

def addAtHead(self, value: int):
	prevNode = self.head
	nextNode = self.head.next
	# connecting the new node with Head and it's Next Node
	node = Node(val=value, prev=prevNode, next=nextNode)
	# back connecting the Head and it's Next with the New Node
	prevNode.next = node
	nextNode.prev = node
	# increment the total count of nodes by one
	self.size += 1

Add at Tail

Now let's Add At Tail
Assets/Leetcode/LinkedList/doubly_linked_list/5.png

def addAtTail(self, value: int):
	prevNode = self.tail.prev
	nextNode = self.tail
	# connecting the new node with Tail and it's Prev Node
	node = Node(val=value, prev=prevNode, next=nextNode)
	# back connecting the Tail and it's Prev node with the New Node
	prevNode.next = node
	nextNode.prev = node
	# increment the total count of nodes by one
	self.size += 1

If you look carefully, this code is similar to addAtHade function we have wrote earlier. The only change in prevNode and nextNode selection.
For addAtHead, the prevNode is the Head itself because we need to add a new node after the Head. On the other hand, for addAtTail, the nextNode is the Tail itself because the new node will be added before the Tail.

Insert into Linked List

So Let's write a common function for insertion

def insert(self, value: int, prevNode: Node, nextNode: Node):
	# connecting the new node with Tail and it's Prev Node
	node = Node(val=value, prev=prevNode, next=nextNode)
	# back connecting the Tail and it's Prev node with the New Node
	prevNode.next = node
	nextNode.prev = node
	# increment the total count of nodes by one
	self.size += 1

def addAtHead(self, value: int):
	self.insert(value=value, prevNode=self.head, nextNode=self.head.next)

def addAtTail(self, value: int):
	self.insert(value=value, prevNode=self.tail.prev, nextNode=self.tail)

Add at Index

This function takes Two Integer Parameters, one indicates the Index and another one is the integer value we have already seen. In this function we need to treat the Linked List like an Array where the First Node is at Index [0] and the last is at Index [size-1]. So if the given index=0 it means we need to add a new node after the Head by using addAtHead function and if the index=size that means we need to add a new node before the Tail by using addAtTail function.

def addAtIndex(self, index: int, value: int):
	# validating index
	if index > self.size:
		return
	if index == 0:
		self.addAtHead(value)
	elif index == size:
		self.addAtTail(value)
	else:
		# need to find the node before the given index

But when the index is in between 1 ... size-1 we need to Pick the previous node of that Index and Insert the new node using our insert function. Let's visualize and add a new node at index 1
Assets/Leetcode/LinkedList/doubly_linked_list/6.png

To add a new node at Index 1, we need to use the node at Index 0 basically node before the given index. Let's write a helper function to find the node before the given index

def getNthNode(self, curr: Node, index: int) -> Optional[Node]:
	while curr and index > 0:
		curr = curr.next
		index -= 1
	return curr

Let's visualize the outcome of this function
Assets/Leetcode/LinkedList/doubly_linked_list/7.png
If we start iterating from the Head we will find the node before the given index. Then we can insert the new node using the same insert function.
Assets/Leetcode/LinkedList/doubly_linked_list/8.png
Let's code it up

def addAtIndex(self, index: int, value: int):
	# validating index
	if index > self.size:
		return
	if index == 0:
		self.addAtHead(value)
	elif index == size:
		self.addAtTail(value)
	else:
		# need to find the node before the given index
		beforeNthNode = self.getNthNode(self.head, index)
		self.insert(value, beforeNthNode, beforeNthNode.next)

The final outcome is
Assets/Leetcode/LinkedList/doubly_linked_list/9.png
The new node [4] inserted at index 1. Now let's implement the get function.

Get Value by Index

This function takes index as parameter and return the node value. If there is no node found, return -1.
This function is pretty simple, we don't actually need to write much code for it, we will use getNthNode function for it. But instead of starting from the HEAD, we will place the curr pointer at the First Node which is next to HEAD. Let's find the value at index 2
Assets/Leetcode/LinkedList/doubly_linked_list/10.png
Let's implement the get function

def get(self, index: int):
	firstNode = self.head.next
	node = self.getNthNode(firstNode, index)
	return node.val if node else -1

Delete Node at Index

This takes index as parameter and and Unlink the Nth Node. We will use getNthNode to find out the node to be deleted and unlink the node from the Linked List.
Let's Delete Node 4 which is at Index 1. To find it we need to call getNthNode(firstNode, index=1).
Assets/Leetcode/LinkedList/doubly_linked_list/11.png

def deleteAtindex(index: int):
	if index > self.size - 1:
		return
	firstNode = self.head.next
	# find the Nth Node string from the firstNode
	nthNode = self.getNthNode(firstNode, index)
	# delete the nthNode

Now let's delete the node
Assets/Leetcode/LinkedList/doubly_linked_list/13.png
Let's write a helper function for this

def deleteNode(nthNode: Node):
	prevNode = nthNode.prev
	nextNode = nthNode.next
	# Unlink the Nth Node
	prevNode.next = nextNode
	nextNode.prev = prevNode
	# decrement number of nodes by one
	size =- 1

def deleteAtindex(index: int):
	if index > self.size - 1:
		return
	firstNode = self.head.next
	# find the Nth Node string from the firstNode
	nthNode = self.getNthNode(firstNode, index)
	# delete the nthNode
	self.deleteNode(nthNode)

The outcome of this code is
Assets/Leetcode/LinkedList/doubly_linked_list/14.png
That's it, Let's complete the whole code.

Complete Code Snippet

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

class DoublyLinkedList:
	def __init__(self):
		self.size = 0
		# Dummy Head and Tail initialization
		self.head = Node(-1)
		self.tail = Node(-1)
		# Linking Dummy Head and Tail
		self.head.next = self.tail
		self.tail.prev = self.head

	
	# Helper Function
	def getNthNode(self, curr: Node, index: int) -> Optional[Node]:
		while curr and index > 0:
			curr = curr.next
			index -= 1
		return curr
		

	def get(self, index: int) -> int:
		firstNode = self.head.next
		nthNode = self.getNthNode(firstNode, index)
		return nthNode.val if nthNode else -1

  
	# Helper Function
	def insertNode(self, value: int, prevNode: Node, nextNode: Node):
		newNode = Node(val=value, prev=prevNode, next=nextNode)
		prevNode.next = newNode
		nextNode.prev = newNode
		# increment node count by 1
		self.size += 1
		

	def addAtHead(self, value: int):
		# add node after the head node
		self.insertNode(value, prevNode=self.head, nextNode=self.head.next)

  
	def addAtTail(self, value: int):
		# add node before tail node	
		self.insertNode(value, prevNode=self.tail.prev, nextNode=self.tail)

  
	def addAtIndex(self, index: int, value: int):
		if index > self.size:
			return
		
		if index == 0:
			self.addAtHead(value)
		elif index == self.size:
			self.addAtTail(value)
		else:
			beforeNthNode = self.getNthNode(self.head, index)
			self.insertNode(
				value,
				prevNode=beforeNthNode,
				nextNode=beforeNthNode.next
			)

  
	# helper function
	def deleteNode(self, nthNode: Node):
		prevNode = nthNode.prev
		nextNode = nthNode.next
		# unlink the nth node from linked list
		prevNode.next = nextNode
		nextNode.prev = prevNode
		# decrement node count by 1
		self.size -= 1

  
	def deleteAtIndex(self, index: int):
		if index > self.size - 1:
			return
		nthNode = self.getNthNode(self.head.next, index)
		self.deleteNode(nthNode)

Thanks for Stopping by! Happy Leet coding

Connect with me on

Github LinkedIn Gmail Youtube