When working with data structures in Kotlin, arrays and lists often come to mind first. They’re built-in, simple, and cover most scenarios. But sometimes you need more control over how elements are connected, inserted, or removed. That’s where a Doubly Linked List in Kotlin shines.
In this blog, we’ll explore what a doubly linked list is, why it’s useful, real-world applications, and most importantly — how to implement one in Kotlin.
Doubly Linked List in Kotlin
A doubly linked list is a data structure made up of nodes. Each node stores three things:
- Data — the actual value.
- A reference to the next node.
- A reference to the previous node.
This dual-link system allows navigation forward and backward through the list. That’s the main difference from a singly linked list, which only moves forward.
Why Use a Doubly Linked List in Kotlin?
You might ask: “Why bother with a doubly linked list when Kotlin already has List and MutableList?”
Here are a few reasons:
- Fast insertions and deletions: Unlike arrays, you don’t need to shift elements when adding or removing.
- Bidirectional traversal: You can move in both directions, which can be handy in scenarios like undo/redo features.
- Custom data structures: Sometimes you want full control over memory and connections.
Real-World Use Cases
Let’s look at where a Doubly Linked List in Kotlin can be practical:
- Browser history navigation (go back and forward between pages).
- Undo/Redo operations in editors.
- Music playlists where you can jump to the previous or next song.
- Deque (Double-Ended Queue) implementations for efficient queue operations.
Implementing a Doubly Linked List in Kotlin
Let’s write a clean, easy-to-follow implementation.
Define the Node
class Node<T>(
var data: T,
var prev: Node<T>? = null,
var next: Node<T>? = null
)Here, Node is generic (<T>) so it can store any type (Int, String, custom objects, etc.). Each node keeps track of its data, the previous node (prev), and the next node (next).
Create the DoublyLinkedList Class
class DoublyLinkedList<T> {
private var head: Node<T>? = null
private var tail: Node<T>? = null
fun isEmpty() = head == null
}We keep track of two references:
- head → the first node.
- tail → the last node.
Add Elements
Let’s add items to the end of the list.
fun append(data: T) {
val newNode = Node(data)
if (head == null) {
head = newNode
tail = newNode
} else {
tail?.next = newNode
newNode.prev = tail
tail = newNode
}
}
- If the list is empty, both
headandtailpoint to the new node. - Otherwise, we connect the new node after the current
tailand updatetail.
Prepend Elements
Adding to the beginning works similarly:
fun prepend(data: T) {
val newNode = Node(data)
if (head == null) {
head = newNode
tail = newNode
} else {
newNode.next = head
head?.prev = newNode
head = newNode
}
}
Remove Elements
Removing a node requires updating both previous and next references.
fun remove(data: T) {
var current = head
while (current != null) {
if (current.data == data) {
if (current.prev != null) {
current.prev?.next = current.next
} else {
head = current.next
}
if (current.next != null) {
current.next?.prev = current.prev
} else {
tail = current.prev
}
break
}
current = current.next
}
}
Here we search for the node, reconnect neighbors around it, and update head or tail if needed.
Print the List
For debugging, let’s add a print function.
fun printForward() {
var current = head
while (current != null) {
print("${current.data} ")
current = current.next
}
println()
}
fun printBackward() {
var current = tail
while (current != null) {
print("${current.data} ")
current = current.prev
}
println()
}
Full Example in Action
Before running the code, make sure all the above functions are inside the DoublyLinkedList<T> class.
fun main() {
val list = DoublyLinkedList<Int>()
list.append(10)
list.append(20)
list.append(30)
list.prepend(5)
println("Forward traversal:")
list.printForward()
println("Backward traversal:")
list.printBackward()
println("Removing 20...")
list.remove(20)
list.printForward()
}
Output:
Forward traversal:
5 10 20 30
Backward traversal:
30 20 10 5
Removing 20...
5 10 30
Conclusion
A Doubly Linked List in Kotlin gives you more control when working with dynamic data. While Kotlin’s standard library handles most needs with List or MutableList, knowing how to build and use a doubly linked list can be a powerful skill.
You now know:
- What a doubly linked list is.
- Real-world scenarios where it’s useful.
- How to implement it step by step in Kotlin.
This structure shines in apps where insertion, deletion, or bidirectional navigation matters — like history tracking, playlists, or undo/redo stacks.
