Common Data Structures in Kotlin

date published : October 15, 2021 read time : 15 mins

Recently, I have been using a lot more data structures in Kotlin and I found there was a lack of information for Kotlin vs its older brother Java. In this blog post, I have tried to compile a list of common data structures with their runtimes and syntax in Kotlin. It’s worth noting that because Kotlin is backward-compatible with Java, all the same collections from Java can be used. The main difference between Kotlin collections and their Java counterpart, is the concept of a mutable collection. Immutable collections are read only whereas the mutable collections allow for adding, removing and updating elements within it. Since mutable collections are inherently …mutable… they do not need to be instantiated with var as the reference will not be changing - only the contents of the collection will change.

Arrays

Arrays in Kotlin are a primitive data structure that are fixed in size, and allow for getting and setting values in constant time.

Creating an Array

Using arrayOf

  • Pass in the values for the array and the array will be created with the proper size, type and elements
  • Use arrayOfNulls to create an array of a given size with all null values
// Create an array with values val nums1 = arrayOf(1, 2, 3, 4) //implicit type declaration val nums2 = arrayOf<Int>(1, 2, 3) //explicit type declaration // Create an Array<Int?> of size 4 with values [null, null, null, null] val nulls: Array<Int?> = arrayOfNulls<Int>(4)

Using the Array constructor

  • Pass in the size of the array
  • Pass a function that returns the array value given an index
// Creates an Array<Int> with values [0, 1, 2, 3, 4] val nums = Array(5) { i -> i} // Creates an Array<String> with values ["0", "1", "4", "9", "16"] val strings = Array(5) { i -> (i * i).toString() }

Primitive Arrays

Kotlin also provides Array classes for primitives that can offer some nice syntax. The options are the following:

  • ByteArray
  • CharArray
  • ShortArray
  • IntArray
  • LongArray
  • DoubleArray
  • FloatArray
  • BooleanArray

These arrays can be created by passing the values of the array, the size of the array or the size and a lambda expression.

// Array of int of size 3 with values [1, 2, 3] var arr1 = intArrayOf(1, 2, 3) // Array of booleans of size 3 with values [true, false, true] var arr2 = booleanArrayOf(true, false, true) // Array of int of size 5 with values [0, 0, 0, 0, 0] val arr3 = IntArray(5) // Array of chars of size 5 with values [, , , , ] val arr4 = CharArray(5) // Array of int of size 5 with values [42, 42, 42, 42, 42] val arr5 = IntArray(5) { 42 } // Array of int of size 5 with values [0, 1, 2, 3, 4] // (values initialised to their index value) var arr6 = IntArray(5) { it -> it }

Array Methods and Runtimes

Action Method Time Complexity Alternative
Access value at index array.get(index) O(1) array[index]
Set value at index array.set(index, value) O(1) array[index] = value
Sort array in place by natural order array.sort() O(nlogn)
Sort array in place in reverse order array.sortDescending() O(nlogn)

Lists

Similar to the Java list, the Kotlin List is an interface that allows for the storage of elements in a specified order, with indexed access to these elements. It also allows for the growing and shrinking of the list, given it is mutable. The default implementation of List in Kotlin is an ArrayList but others are available.

Creating A List

A read only list can be created using the listOf() function.

val numbers = listOf("one", "two", "three", "four") //implicit type declaration val numbers2 = listOf<String>("one", "two", "three", "four") // explicit type declaration

A mutable list can be created using mutableListOf()

val numbers = mutableListOf(1, 2, 3, 4) //implicit type declaration val numbers2 = mutableListOf<Int>(1, 2, 3, 4) // explicit type declaration

List Methods and Runtimes

Action Method Runtime
Get the size of the list list.size O(1)
Add element to list list.add(element) O(1) unless a new backing array needs to be created. In this case all the old values will need to be copied over making it O(n)
Add element at index list.add(index, element) O(n)
Get element at index list.get(index) O(1)
Remove a specific element list.remove(element) O(n)
Remove an element at a specific index list.removeAt(index) O(n)
Get the index of a specific element list.indexOf(element) O(n)
Check if the list contains an element list.contains(element) O(n)
Check if the list is null or empty list.isNullOrEmpty() O(1)
randomly shuffle list in place list.shuffle() O(n)
return randomly shuffled version of a list val shuffled = list.shuffled() O(n)
sort list in place by natural order list.sort() O(nlogn)
return sorted list by natural order val s = list.sorted() O(nlogn)
sort list in place in descending order list.sortByDescending{ it } O(nlogn)
return sorted list in descending order val s = list.sortedByDescending{ it } O(nlogn)
sort list in place according to its natural order based on the returned property of the expression. This can be used to sort objects by a certain property. val values = mutableListOf(1 to “a”, 2 to “b”) values.sortBy { it.second } O(nlogn)
return sorted list according to its natural order based on the returned property of the expression. This can be used to sort objects by a certain property. val values = mutableListOf(1 to “a”, 2 to “b”) val sorted = values.sortBy { it.second } O(nlogn)
Return a read only view of a portion of a list from starting index (inclusive) until end index (exclusive) val list = listOf(1, 2, 3, 4, 5) val s = list.subList(2, 4) // [3, 4] O(1) since its backed by the source array
Return list as an array val arr = list.toTypedArray() O(n)

Linked List

This data structure is rarely used and for this reason Kotlin has opted to not implement it. Here is a thread on why they opted to not support it natively although it’s still possible to achieve this functionality using the Java LinkedList. It is worth noting that this class is a doubly linked list.

Creating a Linked List

Importing import java.util.LinkedList allows for instantiating it as would any other class.

import java.util.LinkedList val list = listOf("Dog", "Cat", "Lion") val linkedList = LinkedList<String>() linkedList.addAll(list) println(linkedList) // [Dog, Cat, Lion] linkedList.add("Parrot") println(linkedList) // [Dog, Cat, Lion, Parrot]

Linked List Methods and Runtimes

Action Method Runtime
Append element to the end of the linked list linkedList.add(element) O(1)
Add element at a specific index in the linked list linkedList.add(index, element) O(n)
Get element at index in the linked list linkedList.get(index) O(n)
Remove an element from the linked list linkedList.remove(element) O(1)
Remove element at a specific index linkedList.removeAt(index) O(n)
Check if a linked list contains an element linkedList.contains(element) O(n)
Return the linked list in reverse order val r = linkedList.reversed() O(1) since the list is doubly linked

Set

A Set stores unique elements. Like all collections in Kotlin, there are the read only set and the MutableSet, which offers write access. In some cases the order of a set is not reliable, that is to say that the order of insertion is not maintained. However, the default implementation of a set in Kotlin uses a LinkedHashSet so the order of insertion is preserved.

Creating a Set

A read only set can be created using the setOf function

val mySet = setOf(1, 2, 3, 4) //implicit type declaration val mySet2 = setOf<Int>(1, 2, 3, 4) // explicit type declaration

A mutable set can be created using the mutableSetOf function

val mySet = mutableSetOf(1, 2, 3, 4) //implicit type declaration val mySet2 = mutableSetOf<Int>(1, 2, 3, 4) // explicit type declaration mySet.add(5) mySet.add(3) println(mySet2) // [1, 2, 3, 4, 5], notice how 4 was not added twice

Set Methods and Runtimes

Action Method Runtime
Get the size of the set mySet.size O(1)
Add element to the set mySet.add(element) O(1)
Remove element from set mySet.remove(element) O(1)
Check if a set contains an element mySet.contains(element) O(1)

Map

A Map stores key-value pairs with unique keys. Values in the map can be stored more than once if they are stored under different keys. Similarly to the set, some implementations of this interface do not preserve order, however, the default implementation of LinkedHashMap does.

Creating a Map

A read only map can be created using the mapOf function

A map will always have a type for the key and a type for the value so it’s important to be mindful of that when explicitly declaring a map.

val numsMap = mapOf("one" to 1, "two" to 2, "three" to 3) //implicit type declaration val numsMap2 = mapOf<String, Int>("one" to 1, "two" to 2, "three" to 3) // explicit type declaration // The above will create a map of type <String, Int> with the key/values {one=1, two=2, three=3}

A mutable map can be created using the mutableMapOf function

val numsMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3) //implicit type declaration val numsMap2 = mutableMapOf<String, Int>("one" to 1, "two" to 2, "three" to 3) // explicit type println(numsMap) // {one=1, two=2, three=3} numsMap.put("five", 5) // add an entry println(numsMap) // {one=1, two=2, three=3, five=5} println(numsMap.get("three")) // 3 numsMap.remove("one") // remove an enrty println(numsMap) // {two=2, three=3, five=5}

Map Methods and Runtimes

Action Method Runtime Alternative
Get the size of the map myMap.size O(1)
Add/modify an entry to the map myMap.put(key, value) O(1) myMap[key] = value
Remove an entry from the map myMap.remove(key) O(1)
Check if map contains a key myMap.contains(key) O(1) myMap.contains(key).let {…}
Check if a map contains a value myMap.containsValue(value) O(n) myMap.containsValue(value).let {…}

Heap

Heaps are great! There are generally two types of heaps. A min heap which stores the minimum element at the root of the tree, and a max heap which instead stores the largest value at the root. In Kotlin, we can leverage the PriorityQueue to create this data structure.

Creating a Min Heap with a Priority Queue

The default implementation of a PriorityQueue actually gives the min heap functionality. Since Kotlin does not support this out of the box, it’s necessary to import java.util.PriorityQueue. Once PriorityQueue is imported, one can be declared as such:

import java.util.PriorityQueue val nums = listOf(5, 2, 4, 1, 3) val minHeap = PriorityQueue<Int>() // declare a min heap with int values minHeap.addAll(nums) // add elements of a Collection with addAll assuming they are the same type println(minHeap) // [1, 2, 4, 5, 3] minHeap.add(0) println(minHeap) // [0, 2, 1, 5, 3, 4] val min = minHeap.poll() // 0, removes 0 from the heap println(minHeap) // [1, 2, 4, 5, 3] since 1 is not the smallest value val min2 = minHeap.peek() // returns 1 but does not remove it from the heap println(min2) // 1 println(minHeap) // [1, 2, 4, 5, 3]

Creating a Max Heap with a PriorityQueue

To turn the PriorityQueue into a max heap, it will be required to pass an instance of comparator. Luckily, most of the common comparators can be found here. The one that will enable the max heap functionality is the compareByDescending function. This will tell the PriorityQueue to give the highest priority to the largest value.

import java.util.PriorityQueue val nums = listOf(5, 2, 4, 1, 3) val maxHeap = PriorityQueue<Int>(compareByDescending{it}) maxHeap.addAll(nums) println(maxHeap) // [5, 3, 4, 1, 2] maxHeap.add(0) println(maxHeap) // [5, 3, 4, 1, 2, 0] val max = maxHeap.poll() // returns and removes 5 from the heap println(max) // 5 println(maxHeap) // [4, 3, 0, 1, 2] val max2 = maxHeap.peek() // returns 4 but does not remove it from the heap println(max2) // 4 println(maxHeap) // [4, 3, 0, 1, 2]

Heap (PriorityQueue) Methods and Runtimes

Action Method Runtime
Get the size of the heap heap.size O(1)
Add element to heap heap.add(element) O(logn)
Add a collection of elements to a heap (does not work with arrays) heap.addAll(list) O(nlogn)
Return and remove the root of the heap (min/max value) heap.poll() O(logn) since we will need to re heapify the heap
Return the root of the heap (min/max) heap.peek() O(1) as no reconstruction is needed
Remove a specific element from the heap heap.remove(element) O(n)
Check if an element is present in the heap heap.contains(element) O(n)

Stacks

A Stack is a last in, first out, or LIFO for short, data structure. It can be thought of as a stack of plates, where the first plate grabbed is going to be the top-most plate (the one that was added last). Again, Kotlin does not provide an implementation of this out of the box so we must rely on its older brother Java. Java has a Stack class but as per the documentation, it is recommended to use an ArrayDeque for all stack/queue needs. More information on this decision can be found in this thread. Importingimport java.util.ArrayDeque is all that’s needed to get started.

Creating a Stack

ArrayDeque can be used for both LIFI and FIFO (first-in & first-out) structures. By always pushing/popping from the front of the stack it’s possible to achieve the desired stack behaviour.

import java.util.ArrayDeque val stack = ArrayDeque<Int>() stack.push(1) stack.push(2) stack.push(3) stack.push(4) println(stack) // [4, 3, 2, 1] println(stack.isEmpty()) // false println(stack.peek()) // 4 println(stack) // [4, 3, 2, 1] println(stack.pop()) // 4 println(stack) // [3, 2, 1] stack.push(9) println(stack) // --> [9, 3, 2, 1]

Stack (ArrayDeque) Methods and Runtimes

Action Method Runtime
Get the size of the stack stack.size O(1)
Push element onto the stack stack.push(element) O(1)
Return and remove a the last element from the stack stack.pop() O(1)
Return the last element from the stack (do not remove) stack.peek() O(1)
Remove a specific element from the stack stack.remove(element) O(n)
Check if the stack is empty stack.isEmpty() O(1)

Queue

A queue is a first in first out or FIFO for short, data structure, and can be thought of as the average lineup to get into a concert. The first to get there is the first to get out of the queue. Queue is an interface that can be implemented by either ArrayDeque, PriorityQueue or LinkedList (there are more options but these are by far the most common).

Creating a Queue

Since PriorityQueue will unnecessarily add extra functionality (see heap section above) and the LinkedList is not very cache-friendly, it is advised to use ArrayDeque to implement a queue. To get started importing import java.util.Queue and import java.util.ArrayDeque is required.

import java.util.ArrayDeque import java.util.Queue val queue: Queue<Int> = ArrayDeque<Int>() queue.add(1) queue.add(2) queue.add(3) queue.add(4) println(queue) // [1, 2, 3, 4] println(queue.isEmpty()) // false println(queue.peek()) // 1 println(queue) // [1, 2, 3, 4] println(queue.poll()) // 1 println(queue) // [2, 3, 4] queue.add(9) println(queue) // [2, 3, 4, 9]

By implementing the interface Queue, there will no longer be access to the methods push and pop like there were in the stack example above.

Queue Methods and Runtimes

Action Method Runtime
Get the size of the queue queue.size O(1)
Add element to the queue queue.add(element) or queue.offer(element) O(1)
Return and remove a the first element from the queue queue.poll() O(1)
Return the first element from the queue (do not remove) queue.peek() O(1)
Remove a specific element from the queue queue.remove(element) O(n)
Check if the stack is empty queue.isEmpty() O(1)

Undirected Graphs

There is no default Graph implementation in Kotlin, however, one can easily be createdusing a HashMap of HashSets. Essentially, the adjacency list is stored in a HashMap, which holds a HashSet of nodes.

Creating a Graph

class Graph<T> { val adjacencyMap: HashMap<T, HashSet<T>> = HashMap() fun addEdge(sourceVertex: T, destinationVertex: T) { // Add edge to source vertex / node. adjacencyMap .computeIfAbsent(sourceVertex) { HashSet() } .add(destinationVertex) // Add edge to destination vertex / node. adjacencyMap .computeIfAbsent(destinationVertex) { HashSet() } .add(sourceVertex) } }

The computeIfAbsent will create the edge if it is not present, and add the vertex to that edge. Since it’s using maps/sets behind the scenes, the runtime for adding an edge remains constant with O(1) insertion.