Blog
DS with Go Part 2: Linked List Variants
Data Structures with Go - Part 2: Linked List Variants
All code from this tutorial can be found here.
NOTE: In this tutorial, we will be borrowing heavily from the last tutorial on linked lists. Do read that in case you missed it here.
After completing this tutorial you should be able to:
- Create doubly & circular linked lists
- Perform insertion, deletion, and traversal operations on both these variants.
Doubly Linked Lists
Figure 1: Doubly linked lists have 2 pointers.
Each node in a doubly linked list has 2 pointers, one pointing to its next node and one to its previous node. The first node has its previous pointer set to null and the last node has its next pointer set to null.
And.. that’s it basically.
Basic structure
// Doubly Linked List Structure.go
type DLNode struct {
Data int
Prev *DLNode
Next *DLNode
}
In our definition, Prev points to the previous node and Next points to the next node in the list.
Traversal
// DL List traversal and length.go
func DisplayList(head *DLNode) {
fmt.Printf("The list is: ")
for ; head != nil; head = head.Next {
fmt.Print(" -> ", head)
}
fmt.Println()
fmt.Println()
}
func Length(head *DLNode) int {
len := 0
for ; head != nil; head = head.Next {
len++
}
fmt.Println("Length = ", len)
return len
}
Traversal and finding the length of a doubly linked list is the same as the singly linked version. Not much difference here.
Insertion
Inserting at the start:
Figure 2: Inserting a new node at the start of the list.
Insertion at the start of the list is also the same as in the case of a singly linked list. The only addition is changing the pointer Prev of the current start node to point to the new node.
// InsertAtStart.go
func InsertAtStart(head **DLNode, x int) {
// We dont need to explicitly initialize Next & Prev. They will be given the zero value automatically.
tmp := &DLNode{Data: x}
fmt.Println("\nInserting", tmp, "at the start")
if *head == nil {
*head = tmp
return
}
tmp.Next = *head
(*head).Prev = tmp
*head = tmp
}
To make the code more readable, I now create new nodes using
&Node{}rather thanNode{}.
Inserting at the end:
Figure 3: Inserting a new node at the end of the list.
This is similar to inserting nodes at the end of singly linked lists. The only addition is that the Prevpointer of the new node will now point to the last node of the list.
// InsertAtEnd.go
func InsertAtEnd(head **DLNode, x int) {
tmp := &DLNode{Data: x}
fmt.Println("Inserting", tmp, "at the end")
if *head == nil {
*head = tmp
} else {
p := *head
for ; p.Next != nil; p = p.Next {
}
p.Next = tmp
tmp.Prev = p
}
}
Inserting at a specific position:
Figure 4: To insert the new node at a specific position, 4 pointers are changed.
Once again we borrow the strategy of finding a node at a specific position from singly linked lists. The new node is inserted before the current node at position p . After insertion, the new node is at position p and the node previously at p is now at the position p+1 .
4 pointers are changed when inserting a node at a specific position:
-
NewNode.Nextpoints to the node at positionp -
NewNode.Prevpoints to the node at positionp-1 -
NodeAtP.Prevpoints to the new node -
NodeBeforeP.Nextpoints to the new node
// InsertAtP.go
func InsertAtP(head **DLNode, p int, x int) {
if p < 1 {
fmt.Println("Positions start at 1. Invalid Position.")
return
}
if p == 1 {
InsertAtStart(head, x)
return
}
if *head == nil {
fmt.Println("Position Invalid.")
return
}
current := *head
i := 1
// Find the penultimate node
for i < p-1 && current.Next != nil {
current = current.Next
i++
}
// This means we reached the end of the list and i != p-1
if i < p-1 {
fmt.Println("Invalid Position")
return
}
tmp := &DLNode{Data: x}
fmt.Println("Inserting", tmp, "at position", p)
tmp.Prev = current
tmp.Next = current.Next
if current.Next != nil {
current.Next.Prev = tmp
}
current.Next = tmp
}
Here we find the penultimate node (i.e. the one right before the insertion point. Penultimate here is with respect to the point of insertion.) which in this case is currentand change its Next pointer to point to the new node i.e. current.Next = tmp (line 34).
But before we do that, we need to change the Prev pointer of the node at position p i.e. current.Next.Prev to point to tmp. But we can’t do this if current.Next is nil which it will be if the insertion point is at 1+Length of the list. Hence lines 31–34.
Putting together the functions above and executing would result in an output similar to the one below. Code for this sample execution is here.
Inserting &{1 <nil> <nil>} at the start
The list is: -> &{1 <nil> <nil>}
Inserting &{2 <nil> <nil>} at the end
The list is: -> &{1 <nil> 0xc00000c0a8} -> &{2 0xc00000c060 <nil>}
Inserting &{3 <nil> <nil>} at the end
The list is: -> &{1 <nil> 0xc00000c0a8} -> &{2 0xc00000c060 0xc00000c108} -> &{3 0xc00000c0a8 <nil>}
Inserting &{4 <nil> <nil>} at position 4
The list is: -> &{1 <nil> 0xc00000c0a8} -> &{2 0xc00000c060 0xc00000c108} -> &{3 0xc00000c0a8 0xc00000c180} -> &{4 0xc00000c108 <nil>}
Have a look at the Prev and Next addresses in the output above. It’s very cool to see the actual addresses used by the nodes on the hardware.
Deletion
Deleting the first node:
// DeleteFirst.go
func DeleteFirst(head **DLNode) {
if *head == nil {
fmt.Println("\nList empty")
return
}
fmt.Println("\nDeleting first Node: ", *head)
// We change head to point to the next Node
*head = (**head).Next
// To remove all references to the deleted node, set the Prev pointer of head to nil
if *head != nil {
(**head).Prev = nil
}
}
In addition to moving the head pointer to the 2nd node as in the case of a singly linked list, we change the Prev pointer of the 2nd node to nil. Easy!!!
Deleting the last node:
// DeleteLast.go
func DeleteLast(head **DLNode) {
if *head == nil {
fmt.Println("\nList is empty")
return
}
// If there is only 1 node in the list
if (*head).Next == nil {
*head = nil
return
}
current := *head
// Find the last node
for current.Next != nil {
current = current.Next
}
fmt.Println("\nDeleting last node: ", current)
// Change the Next pointer of the penultimate node to nil
current = current.Prev
current.Next = nil
}
The logic here is the same as a singly linked list. Find the penultimate node, change it’s Next pointer to nil.
Deleting at a specific position:
Figure 5: Logic for deleting a node at a specific position
Referring to the diagram above, let’s say we want to delete Node 2. All we need to do is change Node1.Next to point to Node 3 and change Node3.Prev to point to Node 1.
// DeleteAtP.go
func DeleteAtP(head **DLNode, p int) {
// Edge case 1: Positions less than 1
if p < 1 {
fmt.Println("Positions start from 1. Position Invaid.")
return
}
// Edge case 2: List is empty
if *head == nil {
fmt.Println("List empty")
return
}
// Edge case 3: Deletion position is 1
if p == 1 {
DeleteFirst(head)
return
}
current := *head
i := 1
// Find node to delete
for i < p && current.Next != nil {
current = current.Next
i++
}
// Edge case 4: Position exceeds length of the list
if i < p {
fmt.Println("Position Invalid")
return
}
previousNode := current.Prev
nextNode := current.Next
previousNode.Next = nextNode
// Edge case 5: The node to delete is the last in the list. If it is nextNode will be nil
if nextNode != nil {
nextNode.Prev = previousNode
}
}
There are 5 edge cases we need to consider:
-
Deletion position < 1: These are invalid (Lines 3–5).
-
Empty list (Lines 8–10).
-
Deletion position is at the start of the list: In this case, we can reuse the
DeleteFirstfunction from the previous section (Lines 13–15). -
Deletion position exceeds the length of the list (Lines 26–28)
-
The node to delete is the last in the list: In this case, there is no node after and we can’t change
nextNode.Prevto point to thepreviousNode.
Note: Doubly linked lists have 2 pointers. Take care to change them both when inserting or deleting elements.
Deleting the entire list:
The logic is exactly the same as singly linked lists.
Borrowing some logic from singly linked lists.
Circular Linked Lists
Figure 6: The last node points to the first making the list circular.
Circular linked lists don’t have an ‘end’ in the sense that there is no node that points to null. All nodes point to the next in line. The last node points back to the start of the list which makes the list circular.
We now look at the basic ops of circular linked lists.
Basic Structure:
The basic structure is exactly the same as a singly linked list.
There is always one node that points to the start of the list. This node is the last node in the list.
Traversal:
If there is no last node then how do we know when to stop the traversal loop??
We use a 2 pointer approach. One stays put pointing to the symbolic start of the list, the other is used to traverse the list. When the second pointer (current in this case) and the head point to the same node, we have successfully traversed the list.
This logic is used for both displaying the list as well as finding the length of the list.
// TraversalOps.go
func DisplayList(head *Node) {
fmt.Printf("The list is: ")
current := head
if current == nil {
fmt.Println("empty.")
return
} else {
fmt.Print(" -> ", current)
current = current.Next
for ; current != head; current = current.Next {
fmt.Print(" -> ", current)
}
fmt.Println()
fmt.Println()
}
}
func Length(head *Node) int {
current := head
if current == nil {
return 0
} else {
len := 1
current = current.Next
for ; current != head; current = current.Next {
len++
}
return len
}
}
Insertion
For any insertion op, we will in the worst case need to traverse the whole list. But why??
-
Inserting at the start: Need to change the last node to point to the new node at the start.
-
Inserting at the end: Need to find the last node, make it point to the new node & make the new node point to the start of the list.
-
Inserting at a specific position: If the insertion point is the start or the end of the list, we have to traverse the entire list anyways. If the insertion point is somewhere in between, only then will we not have to traverse the entire list.
Inserting at the start:
Figure 7: Make the last node point to the new one. Make the new node point to the start.
// InsertAtStart.go
func InsertAtStart(head **Node, x int) {
// Create new node
tmp := &Node{Data: x}
tmp.Next = tmp
fmt.Println("\nInserting", tmp, "at the start ")
if *head == nil {
*head = tmp
return
}
p := *head
for ; p.Next != *head; p = p.Next {
}
p.Next = tmp
tmp.Next = *head
*head = tmp
}
To insert a new node at the start, first find the last node using the 2 pointer approach. Then make the last node point to the new node and the new node point to the start of the list (Lines 11–16).
Inserting at the end:
Figure 8: Make the new node point to the start of the list. Make the last node point to the new node.
// InsertAtEnd.go
func InsertAtEnd(head **Node, x int) {
// Create a new node & make it point to itself.
tmp := &Node{Data: x}
tmp.Next = tmp
fmt.Println("\nInserting", tmp, "at the end")
if *head == nil {
*head = tmp
} else {
p := *head
// Find the last node
for ; p.Next != *head; p = p.Next {
}
p.Next = tmp
// Make the new node point to head
tmp.Next = *head
}
}
The logic here is simple. Make the new node point to the start of the list. Find the last node (the last node always points to the start of the list). Make the last node point to the new node (Lines 10–16).
Inserting at a specific position:
// InsertAtP.go
func InsertAtP(head **Node, p int, x int) {
if p < 1 {
fmt.Println("\nPositions start at 1. Invalid input position.")
return
}
if p == 1 {
InsertAtStart(head, x)
return
}
// If insertion point is not the start and the list is empty, insertion point is invalid.
if *head == nil {
fmt.Println("\nInvalid Position.")
return
}
current := *head
i := 1
for i < p-1 && current.Next != *head {
current = current.Next
i++
}
if i < p-1 {
fmt.Println("\nInvalid Position")
return
}
tmp := &Node{Data: x}
tmp.Next = tmp
fmt.Println("\nInserting", tmp, "at the position", p)
tmp.Next = current.Next
current.Next = tmp
}
The logic for inserting at a specific position is the same as the singly linked list.
As you see, a lot of the logic here is borrowed from or is the same as the previous article on singly linked lists.
Deletion:
Deleting from the start:
Figure 9: Make the last node point to the 2nd node in the list.
To delete the first node, find the last node, make it point to the 2nd node in the list and change the head to point to the second node.
// DeleteFirst.go
func DeleteFirst(head **Node) {
if *head == nil {
fmt.Println("\nList empty")
return
}
fmt.Println("\nDeleting first Node: ", *head)
// Edge Case 1: Only one node exists in the list
if (*head).Next == *head {
*head = nil
return
}
// Traverse to last node
p := *head
for ; p.Next != *head; p = p.Next {
}
p.Next = (*head).Next
*head = (*head).Next
}
Deleting the last node:
Figure 10: Change the penultimate node to point to the head.
To delete the last node, find the penultimate node and make it point to the first node in the list. Super easy!!!
// DeleteLast.go
func DeleteLast(head **Node) {
if *head == nil {
fmt.Println("\nList is empty")
return
}
// Edge Case 1: If there is only 1 node in the list
if (*head).Next == *head {
*head = nil
return
}
current := (*head).Next
previous := *head
for current.Next != *head {
previous = current
current = current.Next
}
fmt.Println("\nDeleting last node: ", current)
previous.Next = *head
current.Next = nil
}
Delete at a specific position:
The logic here too is the same as a singly linked list.
// DeleteAtP.go
func DeleteAtP(head **Node, p int) {
if p < 1 {
fmt.Println("\nPositions start from 1. Cannot delete.")
return
}
if *head == nil {
fmt.Println("\nList is empty")
return
}
if p == 1 {
DeleteFirst(head)
return
}
i := 1
var previous *Node = nil
current := *head
for i < p && current.Next != *head {
previous = current
current = current.Next
i++
}
if i < p {
fmt.Println("Invalid position.")
return
}
fmt.Println("Deleting", current, "at position", p)
previous.Next = current.Next
current = nil
}
Deleting the entire list:
// DeleteList.go
func DeleteList(head **Node) {
fmt.Println("\nDeleting entire linkedlist")
var aux *Node
for *head != nil {
aux = (*head).Next
if aux == *head || aux == nil {
*head = nil
return
}
(*head).Next = nil
*head = aux
}
}
Just like singly and doubly linked lists, we use an auxiliary pointer to help traverse the list. And the similarities end there.
Here we change the Next pointer of each node to point to nil. This is to make sure that eventually there are no nodes that point to either themselves or to the start of the list. By doing this we make sure that the deletion loop is finite. Towards the end, the pointer aux will point to the first node in the list. But this time that node will point to nil and the block at lines 6–8 will cause the loop to exit.
A sample script to view the outputs of the insertion and deletion operations can be found here.
Putting insertion and deletion together would produce outputs like this:
Inserting &{1 0xc00018a190} at the start
The list is: -> &{1 0xc00018a190}
Inserting &{2 0xc00018a1c0} at the end
The list is: -> &{1 0xc00018a1c0} -> &{2 0xc00018a190}
Inserting &{3 0xc00018a200} at the end
The list is: -> &{1 0xc00018a1c0} -> &{2 0xc00018a200} -> &{3 0xc00018a190}
Inserting &{4 0xc00018a250} at the position 4
The list is: -> &{1 0xc00018a1c0} -> &{2 0xc00018a200} -> &{3 0xc00018a250} -> &{4 0xc00018a190}
Deleting first Node: &{1 0xc00018a1c0}
The list is: -> &{2 0xc00018a200} -> &{3 0xc00018a250} -> &{4 0xc00018a1c0}
Deleting last node: &{4 0xc00018a1c0}
The list is: -> &{2 0xc00018a200} -> &{3 0xc00018a1c0}
Deleting &{3 0xc00018a1c0} at position 2
The list is: -> &{2 0xc00018a1c0}
Deleting first Node: &{2 0xc00018a1c0}
The list is: empty.
Pay attention to the addresses. Every new node when initialized points to itself. When inserted in the list, it points to the first node in the list. When deleting the addresses are changed accordingly to point to the next node in the list (which would be the first node for the node at the end of the list).
You should now be able to:
-
Create doubly & circular linked lists
-
Perform insertion, deletion, and traversal operations on both the list types
If you enjoyed reading this article or if it helped you in any way, a 👏 is appreciated. Do share this article with others who may be interested in the topic too.
Stay in touch with me on Linkedin and Twitter . Follow me on GitHub.