Sebastian Gomez
Data Structures in Go: Circular Linked Lists
Let's explore the fascinating world of circular linked lists in Go. Get ready for an adventure full of history, code, examples, and challenges.
History of circular linked lists
Circular linked lists are an evolution of the singly and doubly linked lists we have already explored in earlier articles. The idea became popular decades ago as a way to make the most of the memory and resources available on the systems of that era.
In a circular linked list, the last node points back to the first node, creating a cycle. This can be useful in certain use cases that we'll see later. Now, let's dive into the wonderful world of Go.
The most common implementation in Go
package main
import "fmt"
type Node struct {
value int
next *Node
}
type CircularLinkedList struct {
head *Node
tail *Node
size int
}
func (c *CircularLinkedList) Add(value int) {
newNode := &Node{value: value}
if c.head == nil {
c.head = newNode
c.tail = newNode
newNode.next = c.head
} else {
c.tail.next = newNode
newNode.next = c.head
c.tail = newNode
}
c.size++
}
func (c *CircularLinkedList) Display() {
if c.size == 0 {
fmt.Println("La lista está vacía")
return
}
temp := c.head
for i := 0; i < c.size; i++ {
fmt.Printf("%d -> ", temp.value)
temp = temp.next
}
fmt.Println()
}
func main() {
cll := &CircularLinkedList{}
cll.Add(1)
cll.Add(2)
cll.Add(3)
cll.Display()
}This implementation uses a Node struct to represent each node in the list and a CircularLinkedList struct to manage the circular linked list. We implement the Add method to add elements and the Display method to print the list.
Notice one important detail: when we add the first node, it points to itself (newNode.next = c.head), and from then on every new node links the end back to the start. That way we always keep the invariant of a circular list: the tail points to the head (tail.next == head).
Use cases and examples
Circular linked lists are especially useful in situations where we need a circular data structure. Some use cases include:
- Process scheduling: In operating systems, circular linked lists can be used to implement process scheduling algorithms such as Round Robin.
- Music playback: In music apps, circular linked lists can be used to create a looping playlist, where the last song connects back to the first, allowing continuous playback.
- Cyclic event simulations: In simulations where events happen in cycles, such as moon phases or the seasons of the year, circular linked lists can be an excellent option to model and manage these events.
- Shared resource management: Circular linked lists can be used to allocate and manage shared resources among different users or processes in a fair and efficient way.
Now that we know some use cases, let's explore an example in Go to make everything clearer.
Example: Rotating elements in a circular linked list
func (c *CircularLinkedList) Rotate(k int) {
if c.size == 0 {
return
}
k = k % c.size
if k == 0 {
return
}
// Advance k steps from the head to locate the node that
// will become the new tail.
kthNode := c.head
for count := 1; count < k; count++ {
kthNode = kthNode.next
}
// The new head is the node right after the kth node.
newHead := kthNode.next
// Rebuild the links while keeping the circular invariant:
// the current tail already points to the current head, so we
// only need to move head and tail and close the cycle.
c.tail.next = c.head
c.head = newHead
c.tail = kthNode
c.tail.next = c.head
}
func main() {
cll := &CircularLinkedList{}
cll.Add(1)
cll.Add(2)
cll.Add(3)
cll.Add(4)
cll.Add(5)
cll.Display()
cll.Rotate(2)
cll.Display()
}In this example we implement the Rotate method to rotate the elements of the circular linked list. When we run the program with the list 1 -> 2 -> 3 -> 4 -> 5, a rotation of k = 2 leaves us with 3 -> 4 -> 5 -> 1 -> 2.
Why does it work? The key is to always respect the invariant of a circular list: the tail must point to the head. First we advance k steps to find the node that will become the new tail (kthNode). Its successor (kthNode.next) will be the new head. Since in a circular list the tail already points to the head, all we need to do is move the head and tail pointers and close the cycle again with c.tail.next = c.head. That way no node is orphaned and the ring stays valid.
Note: The code above is plain standard Go and only uses the fmt package, so it remains valid on current versions of Go.go run main.go to confirm the output before adapting it to your project.
10 exercises to practice
It's time to put your skills into practice. Here is a list of 10 exercises so you can practice with circular linked lists in Go.
- Implement a method to remove a node from the circular linked list.
- Implement a method to insert a node at a specific position in the list.
- Create a program that simulates a prize wheel using a circular linked list.
- Implement a method to reverse the order of the elements in the circular linked list.
- Create a program that simulates a music app using circular linked lists.
- Implement a method to split a circular linked list into two circular linked lists.
- Create a program that simulates the Round Robin scheduling algorithm using circular linked lists.
- Implement a method to merge two circular linked lists into a single list.
- Create a program that simulates a relay race using circular linked lists.
- Implement a method to detect whether a circular linked list contains an unwanted cycle (for example, a link that points to a middle node instead of the head) and fix it. Keep in mind that a well formed circular list always has exactly one cycle: the one from the tail back to the head.
Don't forget to share your solutions
We invite you to submit your solution as a Pull Request to this repository: Go Para Principiantes. I would love to see how you tackle these challenges so we can learn together.
Remember that practice makes the master, so don't hesitate to experiment, make mistakes, and learn from them. Keep going, brave programmer.
Conclusions
We have explored the interesting world of circular linked lists in Go, learning about their history, use cases, and examples. We have also seen how to implement and use a circular linked list with a Node struct and a CircularLinkedList struct, and even how to rotate its elements without breaking the circular invariant. Circular linked lists are a powerful and versatile tool that fits a wide range of applications.
Don't forget to put your skills into practice by solving the 10 proposed exercises and sharing your solutions with our community. And remember to check the blog regularly to discover more adventures in the world of programming.
That's all, I hope this post is useful to you and that you can apply it to a project you have in mind. Leave me a comment if it helped or if you have any questions, and if you liked it, share it using the social links below. Until next time.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.