Your interview starts soon. This is not the moment to learn a new graph algorithm. It is the moment to make sure Kotlin syntax does not get between you and a solution you already understand.
Use this as a quick scan before a data structures and algorithms round. The examples are deliberately small. Type a few of them yourself, especially the ones you tend to forget.
10-minute Kotlin sprint Can you read the output and write the missing code? Take the focused test, then come back to the sections you missed. →The basic program shape
Most coding platforms call a top-level main function:
// Input: Ada
fun main() {
val name = readln()
println("Hello, $name")
}
Hello, AdaOn sites such as LeetCode, the platform may call your method for you. For
example, it may provide class Solution with a method such as
fun twoSum(nums: IntArray, target: Int): IntArray. Add your logic inside that
method. Do not rename the class, change the parameter types, or add main unless
the platform asks for it, because its test runner calls that exact signature.
For space-separated input, split is fine for modest test cases:
// Input:
// 5 9
// 2 7 9 3 1
fun main() {
val (n, target) = readln().trim().split(" ").map(String::toInt)
val nums = readln().trim().split(" ").map(String::toInt)
println("n = $n, target = $target, nums = $nums")
}
n = 5, target = 9, nums = [2, 7, 9, 3, 1]readln() assumes another line exists. readLine() returns null at the end of
input, which can be useful when the number of lines is unknown. Large competitive
programming inputs may need buffered input, but do not build a fast scanner in an
interview unless input size makes it necessary.
Build output with StringBuilder when printing inside a large loop:
val output = StringBuilder()
repeat(3) { i -> output.appendLine("Case ${i + 1}") }
print(output)
Case 1
Case 2
Case 3Variables, types, and conversions
Prefer val. Use var only when the reference itself must change.
val limit: Int = 10
var answer = 0
val big: Long = 3_000_000_000L
val ok: Boolean = true
val letter: Char = 'k'
val text: String = "kotlin"
println("limit = $limit, answer = $answer, big = $big, ok = $ok, letter = $letter, text = $text")
limit = 10, answer = 0, big = 3000000000, ok = true, letter = k, text = kotlinKotlin does not perform implicit numeric widening. Convert explicitly:
val count = "42".toInt()
val safeCount = "42x".toIntOrNull() // null
val total = count.toLong() + 3_000_000_000L
val digit = '7'.digitToInt()
val digitChar = 7.digitToChar()
println("count = $count, safeCount = $safeCount, total = $total, digit = $digit, digitChar = $digitChar")
count = 42, safeCount = null, total = 3000000042, digit = 7, digitChar = 7Use Long when a sum, product, distance, or count can exceed about 2.1 billion.
Converting after the arithmetic is too late:
val wrong = (1_000_000 * 1_000_000).toLong() // Int overflows first
val right = 1_000_000L * 1_000_000L
println("wrong = $wrong")
println("right = $right")
wrong = -727379968
right = 1000000000000If, when, and comparisons
Both if and when are expressions, so they can produce a value.
val score = 72
val command = 'L'
val label = if (score >= 60) "pass" else "retry"
val direction = when (command) {
'L' -> "left"
'R' -> "right"
'U', 'D' -> "vertical"
else -> "unknown"
}
println("With score = $score and command = $command:")
println("label = $label, direction = $direction")
With score = 72 and command = L:
label = pass, direction = leftwhen can also express ranges and conditions:
val age = 16
val group = when {
age < 0 -> "invalid"
age in 0..12 -> "child"
age in 13 until 18 -> "teen"
else -> "adult"
}
println("With age = $age:")
println("group = $group")
With age = 16:
group = teenUse == and != for structural equality. === and !== compare object
identity, which is almost never what a DSA solution needs.
Boolean operators are &&, ||, and !. They short-circuit, so this is safe:
val nums = intArrayOf(4, 8, 12)
val index = 1
val target = 8
if (index < nums.size && nums[index] == target) {
println(index)
}
1Ranges and loops
These are the loop forms worth memorising:
val n = 4
val nums = intArrayOf(10, 20, 30, 40)
val exclusive = (0 until n).toList()
val inclusive = (0..n).toList()
val descending = (n downTo 0).toList()
val everyOther = (0 until n step 2).toList()
println("until: $exclusive")
println("range: $inclusive")
println("downTo: $descending")
println("step: $everyOther")
println("indices: ${nums.indices.toList()}")
for ((index, value) in nums.withIndex()) {
println("nums[$index] = $value")
}
repeat(n) { index ->
println("repeat: $index")
}
until: [0, 1, 2, 3]
range: [0, 1, 2, 3, 4]
downTo: [4, 3, 2, 1, 0]
step: [0, 2]
indices: [0, 1, 2, 3]
nums[0] = 10
nums[1] = 20
nums[2] = 30
nums[3] = 40
repeat: 0
repeat: 1
repeat: 2
repeat: 3until excludes n; .. includes it. 0..<n is another spelling of
0 until n on newer Kotlin versions. Use downTo for descending ranges,
step to skip values, indices for valid indexes, and withIndex() when you
need both the index and value.
In a nested loop, labels let you continue or break the outer loop:
val grid = arrayOf(intArrayOf(1, 2), intArrayOf(3, 9))
val target = 9
outer@ for (row in grid.indices) {
for (col in grid[row].indices) {
if (grid[row][col] == target) {
println("found at row $row, column $col")
break@outer
}
}
}
found at row 1, column 1Functions you can write quickly
A normal function declares parameter types and a return type:
fun maxOfThree(a: Int, b: Int, c: Int): Int {
return maxOf(a, maxOf(b, c))
}
println("maxOfThree(3, 9, 4) = ${maxOfThree(3, 9, 4)}")
maxOfThree(3, 9, 4) = 9Use an expression body for a short function:
fun isEven(n: Int): Boolean = n % 2 == 0
println("isEven(8) = ${isEven(8)}")
println("isEven(7) = ${isEven(7)}")
isEven(8) = true
isEven(7) = falseDefault and named arguments can keep helper calls readable:
fun search(nums: IntArray, target: Int, start: Int = 0): Int {
for (i in start until nums.size) {
if (nums[i] == target) return i
}
return -1
}
val values = intArrayOf(4, 8, 12)
val index = search(nums = values, target = 12)
println("index = $index")
index = 2Use vararg when a function accepts any number of values. The spread operator
passes an existing array:
fun total(vararg values: Int) = values.sum()
val nums = intArrayOf(2, 4, 6)
val sum = total(*nums)
println("sum = $sum")
sum = 12Local functions are handy for DFS because they can capture nearby state:
class Node(
val value: Int,
val left: Node? = null,
val right: Node? = null,
)
fun countNodes(root: Node?): Int {
fun dfs(node: Node?): Int {
if (node == null) return 0
return 1 + dfs(node.left) + dfs(node.right)
}
return dfs(root)
}
val root = Node(1, left = Node(2), right = Node(3))
println("For a three-node tree: countNodes(root) = ${countNodes(root)}")
For a three-node tree: countNodes(root) = 3Remember the base case before writing a recursive call. For deep inputs, prefer an iterative stack because recursion can exhaust the call stack.
Strings and characters
Strings are immutable. Operations such as reversed, replace, and substring
return a new value.
val s = "Kotlin"
println("length = ${s.length}")
println("first = ${s.first()}")
println("last = ${s.last()}")
println("substring = ${s.substring(1, 4)}")
println("lowercase = ${s.lowercase()}")
println("startsWith = ${s.startsWith("Kot")}")
println("contains t = ${'t' in s}")
length = 6
first = K
last = n
substring = otl
lowercase = kotlin
startsWith = true
contains t = trueCommon character checks:
val s = "Kotlin"
for (ch in s) {
when {
ch.isDigit() -> println(ch.digitToInt())
ch.isLetter() -> println(ch.lowercaseChar())
ch.isWhitespace() -> continue
}
}
k
o
t
l
i
nUse a CharArray or StringBuilder when modifying characters:
val s = "Kotlin"
val chars = s.toCharArray()
chars[0] = chars[0].lowercaseChar()
val changed = chars.concatToString()
val reversed = StringBuilder(s).reverse().toString()
println("changed = $changed")
println("reversed = $reversed")
changed = kotlin
reversed = niltoKTwo-pointer palindrome check:
fun isPalindrome(s: String): Boolean {
var left = 0
var right = s.lastIndex
while (left < right) {
if (s[left] != s[right]) return false
left++
right--
}
return true
}
println("isPalindrome(\"racecar\") = ${isPalindrome("racecar")}")
println("isPalindrome(\"kotlin\") = ${isPalindrome("kotlin")}")
isPalindrome("racecar") = true
isPalindrome("kotlin") = falseFor lowercase English letters, an IntArray(26) is a compact frequency table:
val word = "aba"
val frequency = IntArray(26)
for (ch in word) frequency[ch - 'a']++
println("With word = $word: a = ${frequency[0]}, b = ${frequency[1]}, every other count = 0")
With word = aba: a = 2, b = 1, every other count = 0Use a map instead when the character set is not limited to those 26 letters.
Also remember that a Kotlin Char is one UTF-16 code unit, not necessarily one
complete user-perceived character. Interview problems usually state a restricted
alphabet when that distinction does not matter.
Arrays
Primitive arrays avoid boxed values:
val zeros = IntArray(5) // [0, 0, 0, 0, 0]
val squares = IntArray(5) { i -> i * i }
val given = intArrayOf(4, 1, 9)
println("zeros = ${zeros.contentToString()}")
println("squares = ${squares.contentToString()}")
println("given = ${given.contentToString()}")
zeros = [0, 0, 0, 0, 0]
squares = [0, 1, 4, 9, 16]
given = [4, 1, 9]General arrays can hold objects or nested arrays:
val names = arrayOf("Ada", "Lin")
val rows = 3
val cols = 4
val grid = Array(rows) { IntArray(cols) }
grid[1][2] = 7
println("names = ${names.contentToString()}, grid[1][2] = ${grid[1][2]}")
names = [Ada, Lin], grid[1][2] = 7Useful array operations:
val given = intArrayOf(4, 1, 9)
println("size = ${given.size}")
println("lastIndex = ${given.lastIndex}")
println("getOrNull(10) = ${given.getOrNull(10)}")
println("range = ${given.copyOfRange(1, 3).contentToString()}")
given.fill(-1)
println("given = ${given.contentToString()}")
println("sum = ${given.sum()}, min = ${given.minOrNull()}, max = ${given.maxOrNull()}")
size = 3
lastIndex = 2
getOrNull(10) = null
range = [1, 9]
given = [-1, -1, -1]
sum = -3, min = -1, max = -1IntArray and Array<Int> are different types. Coding platforms commonly use
IntArray in Kotlin method signatures.
Lists, sets, and maps
List exposes read-only operations. It is not a guarantee that the backing
collection can never change. Use MutableList when your code must add, remove,
or replace values.
val fixed: List<Int> = listOf(1, 2, 3)
val nums = mutableListOf(1, 2, 3)
nums.add(4)
nums += 5
nums[0] = 9
nums.removeAt(1) // removes by index
nums.remove(3) // removes the value 3
println("nums = $nums")
nums = [9, 4, 5]Create a sized mutable list when you need indexed assignment:
val n = 4
val start = 1
val distance = MutableList(n) { Int.MAX_VALUE }
distance[start] = 0
println(distance)
[2147483647, 0, 2147483647, 2147483647]Sets answer membership and uniqueness questions:
val nums = listOf(2, 1, 2, 3, 1)
val seen = mutableSetOf<Int>()
for (value in nums) {
if (!seen.add(value)) println("duplicate: $value")
}
duplicate: 2
duplicate: 1Maps store key-value relationships:
val word = "aba"
val frequency = mutableMapOf<Char, Int>()
for (ch in word) {
frequency[ch] = frequency.getOrDefault(ch, 0) + 1
}
for ((key, value) in frequency) {
println("$key -> $value")
}
a -> 2
b -> 1getOrPut is convenient when each key owns a mutable collection:
val word = "cat"
val groups = mutableMapOf<Int, MutableList<String>>()
groups.getOrPut(word.length) { mutableListOf() }.add(word)
println("groups = $groups")
groups = {3=[cat]}A map lookup returns a nullable value because the key may be absent. Avoid !!
when getOrDefault, getOrPut, or an explicit null check expresses the case.
Transforming collections
These standard operations are useful when they make the solution clearer:
val nums = listOf(1, 2, 3)
val words = listOf("a", "bb", "a")
val doubled = nums.map { it * 2 }
val evens = nums.filter { it % 2 == 0 }
val firstLarge = nums.firstOrNull { it > 100 }
val hasZero = nums.any { it == 0 }
val allPositive = nums.all { it > 0 }
val grouped = words.groupBy { it.length }
val counts = words.groupingBy { it }.eachCount()
val total = nums.fold(0L) { acc, value -> acc + value }
println("doubled = $doubled")
println("evens = $evens")
println("firstLarge = $firstLarge")
println("hasZero = $hasZero, allPositive = $allPositive")
println("grouped = $grouped")
println("counts = $counts")
println("total = $total")
doubled = [2, 4, 6]
evens = [2]
firstLarge = null
hasZero = false, allPositive = true
grouped = {1=[a, a], 2=[bb]}
counts = {a=2, bb=1}
total = 6Most of these create new collections. In a hot loop or memory-constrained problem, a direct loop is often easier to reason about and cheaper to run. Do not hide the important state of a sliding window or traversal inside a clever chain.
Sorting and binary search
Midpoint check Put the collection APIs into practice Output questions and short Kotlin prompts, with answers revealed only after you commit. →Know whether an operation mutates or returns a copy:
val a = intArrayOf(3, 1, 2)
a.sort() // mutates a
val descending = a.sortedDescending() // returns List<Int>
val copy = a.sortedArray() // returns IntArray
val words = mutableListOf("pear", "fig", "apple")
words.sortBy { it.length }
words.sortWith(compareBy<String> { it.length }.thenBy { it })
println("a = ${a.contentToString()}")
println("descending = $descending")
println("copy = ${copy.contentToString()}")
println("words = $words")
a = [1, 2, 3]
descending = [3, 2, 1]
copy = [1, 2, 3]
words = [fig, pear, apple]Avoid comparators such as { a, b -> a - b }, which can overflow. Use
compareBy, compareValues, or a.compareTo(b).
Binary search requires sorted input:
val values = intArrayOf(2, 5, 8, 12)
val index = values.binarySearch(8) // 2
println("index = $index")
index = 2If the value is absent, binarySearch returns a negative value that encodes the
insertion point. Do not treat every negative result as a usable index.
Stack, queue, deque, and heap
ArrayDeque covers both stack and queue behavior:
val stack = ArrayDeque<Int>()
stack.addLast(10)
stack.addLast(20)
val top = stack.removeLast()
val queue = ArrayDeque<Int>()
queue.addLast(10)
queue.addLast(20)
val first = queue.removeFirst()
println("top = $top")
println("first = $first")
top = 20
first = 10Check isNotEmpty() before removal unless the algorithm guarantees an element.
Calling removeFirst() or removeLast() on an empty deque throws
NoSuchElementException.
On the JVM, use PriorityQueue for a heap:
import java.util.PriorityQueue
val minHeap = PriorityQueue<Int>()
minHeap.add(7)
minHeap.add(2)
minHeap.add(5)
val smallest = minHeap.poll() // 2
val maxHeap = PriorityQueue<Int>(compareByDescending { it })
println("smallest = $smallest")
println("maxHeap is created empty and orders larger values first")
smallest = 2
maxHeap is created empty and orders larger values firstpeek() and poll() return null when the queue is empty. The default is a
min-heap.
Pairs, data classes, and nodes
Use Pair for a tiny local relationship:
val row = 2
val col = 3
val cell = row to col
val (r, c) = cell
println("With row = $row and col = $col: r = $r, c = $c")
With row = 2 and col = 3: r = 2, c = 3For three or more fields, or when names improve the algorithm, use a data class:
data class State(val row: Int, val col: Int, val distance: Int)
println(State(1, 2, 4))
State(row=1, col=2, distance=4)Typical interview node definitions look like this:
class ListNode(var value: Int, var next: ListNode? = null)
class Node(
val value: Int,
var left: Node? = null,
var right: Node? = null,
)
println("The classes define node shapes. Example: Node(7).value = ${Node(7).value}")
The classes define node shapes. Example: Node(7).value = 7Follow the platform’s supplied definition when it provides one.
Null safety without noise
Nullable values use ?:
class Node(val value: Int, val left: Node? = null)
val root = Node(1, left = Node(5))
val text: String? = null
val node: Node? = root.left
val value = node?.value // null if node is null
val size = text?.length ?: 0 // Elvis operator
if (node != null) {
println(node.value) // smart cast inside this branch
}
5Use ?.let only when it genuinely reads better than an if:
fun pairWithTarget(map: Map<Int, Int>, target: Int, i: Int): IntArray? {
return map[target]?.let { index -> intArrayOf(index, i) }
}
val found = pairWithTarget(mapOf(9 to 3), target = 9, i = 8)
val missing = pairWithTarget(emptyMap(), target = 9, i = 8)
println("found = ${found?.contentToString()}")
println("missing = $missing")
found = [3, 8]
missing = nullThe not-null assertion !! throws NullPointerException when its value is
null. In interview code, it is usually clearer to prove the value exists with
a guard or handle the absent case explicitly.
Math and bit operations
Common math helpers:
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
val a = 9
val b = 4
val left = 0
val right = 8
val distance = abs(a - b)
val middle = left + (right - left) / 2
println("distance = $distance, middle = $middle")
distance = 5, middle = 4Integer division truncates toward zero. 7 / 2 is 3, while 7.0 / 2 is
3.5. A common positive-integer ceiling division is:
val items = 10
val size = 3
val groups = (items + size - 1) / size
println("groups = $groups")
groups = 4That expression can overflow for very large values, so use Long or an
overflow-safe rearrangement when constraints require it.
Kotlin uses named functions for bit operations on integers:
val position = 2
val mask = 10
val bit = 1 shl position
val isSet = (mask and bit) != 0
val added = mask or bit
val toggled = mask xor bit
val shifted = mask shr 1 // keeps sign
val unsignedShift = mask ushr 1
val ones = mask.countOneBits()
println("bit = $bit, isSet = $isSet, added = $added, toggled = $toggled")
println("shifted = $shifted, unsignedShift = $unsignedShift, ones = $ones")
bit = 4, isSet = false, added = 14, toggled = 14
shifted = 5, unsignedShift = 5, ones = 2Four patterns worth being able to type
Frequency map
fun counts(nums: IntArray): Map<Int, Int> {
val result = mutableMapOf<Int, Int>()
for (n in nums) result[n] = result.getOrDefault(n, 0) + 1
return result
}
println("counts([2, 1, 2]) = ${counts(intArrayOf(2, 1, 2))}")
counts([2, 1, 2]) = {2=2, 1=1}Two pointers on a sorted array
fun hasPair(nums: IntArray, target: Int): Boolean {
var left = 0
var right = nums.lastIndex
while (left < right) {
val sum = nums[left] + nums[right]
when {
sum == target -> return true
sum < target -> left++
else -> right--
}
}
return false
}
println("hasPair([1, 3, 4, 8], 7) = ${hasPair(intArrayOf(1, 3, 4, 8), 7)}")
hasPair([1, 3, 4, 8], 7) = trueSliding window
fun maxWindowSum(nums: IntArray, k: Int): Int {
require(k in 1..nums.size)
var window = 0
for (i in 0 until k) window += nums[i]
var best = window
for (right in k until nums.size) {
window += nums[right] - nums[right - k]
best = maxOf(best, window)
}
return best
}
println("maxWindowSum([2, 1, 5, 1, 3], 3) = ${maxWindowSum(intArrayOf(2, 1, 5, 1, 3), 3)}")
maxWindowSum([2, 1, 5, 1, 3], 3) = 9require throws IllegalArgumentException when its condition is false. In a
platform method, you may return a specified sentinel instead if invalid input is
part of the problem contract.
Breadth-first search
fun shortestSteps(graph: List<List<Int>>, start: Int): IntArray {
val distance = IntArray(graph.size) { -1 }
val queue = ArrayDeque<Int>()
distance[start] = 0
queue.addLast(start)
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
for (next in graph[node]) {
if (distance[next] != -1) continue
distance[next] = distance[node] + 1
queue.addLast(next)
}
}
return distance
}
val graph = listOf(listOf(1, 2), listOf(3), listOf(3), emptyList())
println(shortestSteps(graph, 0).contentToString())
[0, 1, 1, 2]For a grid, store (row, col) pairs or a small State, and keep directions in
one place:
val directions = arrayOf(
intArrayOf(-1, 0),
intArrayOf(1, 0),
intArrayOf(0, -1),
intArrayOf(0, 1),
)
println(directions.joinToString(prefix = "[", postfix = "]") { it.contentToString() })
[[-1, 0], [1, 0], [0, -1], [0, 1]]Complexity traps in friendly-looking code
list.contains(value)andlist.remove(value)are linear searches. A hash set usually gives average constant-time membership.substring,filter,map,reversed, andsortedcreate results. Repeating them inside a loop can quietly increase time and memory use.- Adding to the end of a mutable list is normally cheap. Removing index
0shifts the remaining elements, so useArrayDequefor a queue. - Sorting is usually
O(n log n). Hash maps and sets have averageO(1)lookup, but that is not a worst-case guarantee. - String concatenation in a long loop can repeatedly copy text. Use
StringBuilder. - A recursive DFS uses call-stack space proportional to its depth.
The final five-minute checklist
Before you submit, say these questions out loud:
- What happens for empty input, one element, duplicates, and all-equal values?
- Are my range endpoints inclusive or exclusive?
- Can a sum, product, or midpoint overflow
Int? - Does this lookup return
null, and have I handled it? - Am I mutating the original collection or creating a copy?
- Does my comparator handle equal values and extreme integers?
- Have I tested the loop once by hand, including the condition that stops it?
- Can I state the time and space complexity in one sentence?
The goal is not to show off every Kotlin feature. It is to make the algorithm obvious. Use the smallest API that expresses your intent, name your state well, and narrate the trade-off you are making. Clean, boring code is excellent interview code.
Ready to check it? Take the last-minute Kotlin DSA test Ten focused questions. Output tracing, tiny functions, no Android trivia. Start →If you have more than a few minutes, pair this reference with the focused coding interview practice guide and the broader Android interview plan.
Discussion
Comments are powered by GitHub Discussions. Sign in with GitHub to join in.