Ten Coding Interview Questions in Kotlin, With the Gotchas Interviewers Are Watching For
Architect loops still have a coding screen. Here are the ten problems that cover the ten patterns that matter, each with an idiomatic Kotlin solution, the mistakes interviewers are listening for, and the Kotlin-specific traps that trip up people coming from Java.
Architect interview loops still have a coding round. It is usually shorter and easier than the one a new graduate gets, and the interviewer is not checking whether you can invert a binary tree. They are checking whether you can still write correct code under mild pressure, whether you know the standard patterns well enough to reach for the right one in thirty seconds, and whether you talk about complexity, edge cases, and trade-offs without being prompted. A senior candidate who fumbles a sliding window loses more credibility than a junior who does, because the bar for “obviously fluent” is higher.
This post is ten problems, chosen so that each one is the canonical example of a pattern: hash map, stack, sort and sweep, sliding window, grid breadth-first search, cache design, heap, binary search, linked-list pointers, and dynamic programming. Each comes with an idiomatic Kotlin solution and the specific mistakes interviewers listen for. The language-level traps that cut across all ten are collected in the gotchas section, because most of them are the same trap wearing different clothes.
The Question
The coding round in an architect loop tends to take one of these shapes:
- “Here is a problem. Solve it in the language of your choice.” Then a follow-up that changes a constraint.
- “Implement an LRU cache.” A design-flavored coding question, the most common one for senior candidates.
- “Here is a function with a bug. Find it.” Increasingly common, and the bug is usually one of the gotchas below.
- “Write the core of the thing you just designed.” A hold, a rate limiter’s token bucket, a dedupe check.
Kotlin is a good choice if you know it well and a poor one if you are translating from Java in your head. The interviewer will notice Array<Int> where IntArray belonged.
What They Are Really Checking
- Pattern recognition. Do you see “longest substring without” and reach for a sliding window in the first ten seconds? Naming the pattern out loud is worth as much as the code.
- Correctness at the edges. Empty input, one element, duplicates, the last element, the boundary case that the naive version gets wrong. Senior candidates enumerate these before writing.
- Complexity, unprompted. Time and space, stated when you finish, with the alternative you did not choose and why.
- Fluency in the language you picked. Idiomatic collections, no boxing where a primitive array exists, the right deque API, and the difference between
==and===. - Talking while coding. The interviewer cannot read your mind. A silent minute followed by correct code scores lower than a narrated minute with one typo.
The Gotchas
These are the Kotlin-specific traps that recur across all ten problems. They are the ones that make an interviewer conclude you write Kotlin occasionally rather than daily.
Gotcha 1: Array<Int> where IntArray belongs. Array<Int> boxes every element. IntArray, CharArray, BooleanArray, and LongArray are primitive and are what the standard library and any interviewer expect for numeric problems.
Gotcha 2: Mixing Kotlin’s ArrayDeque with Java’s. kotlin.collections.ArrayDeque uses addLast, removeLast, removeFirst, and last(). java.util.ArrayDeque uses push, pop, poll, and peek. Import one, use its API, and do not switch halfway through the whiteboard.
Gotcha 3: == when you meant ===. == calls equals. On a data class that compares fields. For linked-list nodes, sentinels, and anything where identity matters, use ===.
Gotcha 4: Expecting a smart cast on a mutable property. if (node.next != null) node.next.value does not compile, because next could change between the check and the use. Copy to a local val, or use ?. and !! deliberately.
Gotcha 5: .. versus until. 0..n includes n. 0 until n does not. Most Kotlin off-by-one bugs in interviews are this one.
Gotcha 6: Negative modulus. -1 % 5 is -1, as in Java. Ring buffers and hash bucketing need Math.floorMod or ((x % n) + n) % n.
Gotcha 7: Forgetting that PriorityQueue and LinkedHashMap overrides are Java. They live in java.util. The import is easy to forget when the rest of the file is pure Kotlin, and object : LinkedHashMap<K, V>(...) with removeEldestEntry is Java’s API, not a Kotlin one.
Gotcha 8: Int overflow in sums, midpoints, and sentinels. Int.MAX_VALUE + 1 wraps. Use lo + (hi - lo) / 2 for midpoints, amount + 1 as a dynamic-programming infinity, and Long for accumulators when the input bounds justify it.
Gotcha 9: sortedBy when you meant sortBy. sortedBy allocates a new list. sortBy and sortWith sort in place. Say which one you are using and why; in-place sorting mutates the caller’s input.
Gotcha 10: Recursion where the JVM stack cannot follow. Depth-first search on a large grid and recursive list reversal both overflow on realistic inputs. Iterative with an explicit queue or loop is the answer, and saying why is the point.
How to Answer
For each problem: name the pattern, state the edge cases, write the code, state the complexity, and mention the alternative. The code below is what the finished whiteboard should look like.
1. Two Sum (hash map)
Return the indices of two numbers that add to target.
1
2
3
4
5
6
7
8
fun twoSum(nums: IntArray, target: Int): IntArray {
val seen = HashMap<Int, Int>() // value -> index
for ((i, n) in nums.withIndex()) {
seen[target - n]?.let { return intArrayOf(it, i) }
seen[n] = i
}
throw IllegalArgumentException("no solution")
}
O(n) time, O(n) space. The alternative is sort and two-pointer at O(n log n) with O(1) extra space, which loses the original indices.
- Check the map before inserting the current number, or
[3, 3]with target 6 returns the same index twice. - If inputs can be near
Int.MAX_VALUE,target - noverflows. Say it, and useLongif asked.
2. Valid Parentheses (stack)
1
2
3
4
5
6
7
8
9
fun isValid(s: String): Boolean {
val pairs = mapOf(')' to '(', ']' to '[', '}' to '{')
val stack = ArrayDeque<Char>()
for (c in s) {
if (c in pairs.values) stack.addLast(c)
else if (stack.isEmpty() || stack.removeLast() != pairs[c]) return false
}
return stack.isEmpty()
}
O(n) time and space.
- The final
stack.isEmpty()check."(("passes the loop and is still invalid. - Check
isEmpty()before popping, or")"throws.
3. Merge Intervals (sort and sweep)
1
2
3
4
5
6
7
8
9
10
11
12
13
fun merge(intervals: Array<IntArray>): Array<IntArray> {
if (intervals.isEmpty()) return intervals
intervals.sortBy { it[0] }
val out = ArrayList<IntArray>()
var cur = intervals[0]
for (i in 1 until intervals.size) {
val next = intervals[i]
if (next[0] <= cur[1]) cur[1] = maxOf(cur[1], next[1])
else { out.add(cur); cur = next }
}
out.add(cur)
return out.toTypedArray()
}
O(n log n) for the sort, O(n) space for the output.
- Sort by start first. Nothing works without it.
maxOf(cur[1], next[1]), notnext[1].[1, 10]followed by[2, 3]must stay[1, 10].<=merges touching intervals like[1, 4]and[4, 5]. Ask whether touching counts as overlapping.- This mutates the input, both by sorting in place and by writing
cur[1]. Say so, or copy first.
4. Longest Substring Without Repeating Characters (sliding window)
1
2
3
4
5
6
7
8
9
10
11
fun lengthOfLongestSubstring(s: String): Int {
val lastSeen = HashMap<Char, Int>()
var best = 0
var left = 0
for ((right, c) in s.withIndex()) {
lastSeen[c]?.let { if (it >= left) left = it + 1 }
lastSeen[c] = right
best = maxOf(best, right - left + 1)
}
return best
}
O(n) time, O(k) space for the alphabet.
- The
it >= leftguard. Without it,"abba"movesleftbackwards when the secondais seen and the answer is wrong. This is the single most common bug in this problem. - The window only ever moves right. If you find yourself shrinking from the right, the invariant is wrong.
Charis a UTF-16 code unit. If the interviewer mentions emoji, say the solution counts code units, not characters.
5. Number of Islands (grid breadth-first search)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fun numIslands(grid: Array<CharArray>): Int {
if (grid.isEmpty()) return 0
val rows = grid.size
val cols = grid[0].size
val dirs = intArrayOf(1, 0, -1, 0, 0, 1, 0, -1)
val queue = ArrayDeque<Int>()
var count = 0
for (r in 0 until rows) for (c in 0 until cols) {
if (grid[r][c] != '1') continue
count++
grid[r][c] = '0'
queue.addLast(r * cols + c)
while (queue.isNotEmpty()) {
val cell = queue.removeFirst()
val cr = cell / cols
val cc = cell % cols
for (d in 0 until 8 step 2) {
val nr = cr + dirs[d]
val nc = cc + dirs[d + 1]
if (nr in 0 until rows && nc in 0 until cols && grid[nr][nc] == '1') {
grid[nr][nc] = '0'
queue.addLast(nr * cols + nc)
}
}
}
}
return count
}
O(rows × cols) time; O(min(rows, cols)) queue space in practice, O(rows × cols) worst case.
- Recursive depth-first search overflows the JVM stack on a large grid. Iterative breadth-first search is the safe default, and saying why is worth points.
- Mark a cell visited when you enqueue it, not when you dequeue it. Otherwise the same cell enters the queue several times.
- Encoding the cell as one
Intavoids allocating aPairper cell. - This mutates the grid. Ask if that is allowed; if not, use a
BooleanArrayvisited set.
6. LRU Cache (design)
1
2
3
4
5
6
7
class LRUCache(private val capacity: Int) {
private val map = object : LinkedHashMap<Int, Int>(capacity, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, Int>) = size > capacity
}
fun get(key: Int): Int = map[key] ?: -1
fun put(key: Int, value: Int) { map[key] = value }
}
O(1) for both operations.
- The third constructor argument,
accessOrder = true, is what makesgetcount as a use. Without it you have a first-in-first-out cache. - Most interviewers will say “now do it without
LinkedHashMap.” Be ready to write the hash map plus doubly linked list version: aNode(key, value, prev, next), sentinel head and tail,moveToFront, andremoveLast. Practice it once so the pointer surgery is automatic. geton an access-ordered map is a structural modification. Iterating while callinggetthrows.- Not thread-safe. Say so before they ask.
7. Top K Frequent Elements (heap)
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.PriorityQueue
fun topKFrequent(nums: IntArray, k: Int): IntArray {
val freq = HashMap<Int, Int>()
for (n in nums) freq[n] = (freq[n] ?: 0) + 1
val heap = PriorityQueue<Map.Entry<Int, Int>>(compareBy { it.value }) // min-heap on count
for (e in freq.entries) {
heap.add(e)
if (heap.size > k) heap.poll()
}
return IntArray(heap.size) { heap.poll().key }
}
O(n log k) time, O(n) space for the frequency map.
- A min-heap of size
k, not a max-heap of everything. That is the difference between O(n log k) and O(n log n), and it is the point of the question. compareBy { it.value }returns aComparator, which is what the constructor wants.- The result order is unspecified. If they want descending, reverse it, or use bucket sort by frequency for O(n).
8. Search in Rotated Sorted Array (binary search)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun search(nums: IntArray, target: Int): Int {
var lo = 0
var hi = nums.size - 1
while (lo <= hi) {
val mid = lo + (hi - lo) / 2
when {
nums[mid] == target -> return mid
nums[lo] <= nums[mid] -> // left half is sorted
if (target in nums[lo] until nums[mid]) hi = mid - 1 else lo = mid + 1
else -> // right half is sorted
if (target in nums[mid] + 1..nums[hi]) lo = mid + 1 else hi = mid - 1
}
}
return -1
}
O(log n) time, O(1) space.
nums[lo] <= nums[mid]with<=, not<. The two-element case[3, 1]breaks otherwise.- Duplicates break the “which half is sorted” test. If the interviewer adds them, you need an extra
lo++whennums[lo] == nums[mid] == nums[hi], and the worst case becomes O(n). untilexcludes the end, which is correct here becausemidwas already checked.
9. Reverse a Linked List and Detect a Cycle (pointers)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ListNode(var value: Int, var next: ListNode? = null)
fun reverse(head: ListNode?): ListNode? {
var prev: ListNode? = null
var cur = head
while (cur != null) {
val next = cur.next
cur.next = prev
prev = cur
cur = next
}
return prev
}
fun hasCycle(head: ListNode?): Boolean {
var slow = head
var fast = head
while (fast?.next != null) {
slow = slow!!.next
fast = fast.next!!.next
if (slow === fast) return true
}
return false
}
Both O(n) time, O(1) space. The cycle check is Floyd’s tortoise and hare.
- Save
nextbefore rewiringcur.next, and returnprev, notcur. Both are classic off-by-one-pointer mistakes. ===for reference equality. IfListNodewere adata class,==would compare values and two different nodes with the same value would look like a cycle.- Recursive reversal is elegant and overflows on long lists. Iterative is the answer.
10. Coin Change (dynamic programming)
Fewest coins to make amount, or -1.
1
2
3
4
5
6
7
8
9
fun coinChange(coins: IntArray, amount: Int): Int {
val inf = amount + 1 // safe "infinity"
val dp = IntArray(amount + 1) { inf }
dp[0] = 0
for (a in 1..amount) for (c in coins) {
if (c <= a && dp[a - c] + 1 < dp[a]) dp[a] = dp[a - c] + 1
}
return if (dp[amount] == inf) -1 else dp[amount]
}
O(amount × coins) time, O(amount) space. Pseudo-polynomial, because the runtime depends on the magnitude of amount, not the size of the input.
Int.MAX_VALUEas infinity overflows the moment you add 1 to it.amount + 1is the idiom.- Bottom-up avoids the recursion depth of memoized top-down. If you write the recursive version, say why you would not ship it.
- “Fewest coins” and “number of ways” are different problems with different loop orders. For counting ways, the coin loop goes on the outside to avoid counting permutations.
The patterns, in one table
| Problem | Pattern | Time | Space | The one thing to say |
|---|---|---|---|---|
| Two Sum | Hash map | O(n) | O(n) | Check before insert |
| Valid Parentheses | Stack | O(n) | O(n) | Empty stack at the end |
| Merge Intervals | Sort and sweep | O(n log n) | O(n) | Sort first; maxOf on the end |
| Longest Substring | Sliding window | O(n) | O(k) | it >= left |
| Number of Islands | Grid BFS | O(rc) | O(rc) | Mark on enqueue; iterative |
| LRU Cache | Hash map plus linked list | O(1) | O(capacity) | accessOrder = true; know the manual version |
| Top K Frequent | Min-heap of size k | O(n log k) | O(n) | Min-heap, not max |
| Rotated Search | Binary search | O(log n) | O(1) | <= on the sorted-half test |
| Reverse and Cycle | Pointers | O(n) | O(1) | Save next; === |
| Coin Change | Bottom-up DP | O(amount × coins) | O(amount) | amount + 1 as infinity |
Follow-Up Questions to Expect
- “What is the complexity?” You should already have said it. If not, say it now with the alternative you rejected.
- “Can you do it with less memory?” Two Sum has a sort-and-two-pointer version. Islands can use the grid as its own visited set. Coin Change is already O(amount). Know which of your solutions has a cheaper-space cousin.
- “Make the LRU cache thread-safe.” A lock around both operations is the honest answer. Striped locks or a concurrent map with a separate eviction structure are the next level, and the trade-off is contention versus complexity.
- “The input does not fit in memory.” Top K becomes a streaming problem: count-min sketch or a heavy-hitters algorithm. Merge Intervals becomes an external sort. Saying “that changes the problem class” is the senior answer.
- “Write a test.” Pick the edge case you named first: empty input, one element, the
"abba"case, the[3, 1]rotation. Interviewers grade the choice of test as much as the test. - “Why Kotlin?” Because you use it daily. If that is not true, this is the moment it shows.
Key Takeaways
- Name the pattern before writing. Ten patterns cover the coding round for a senior loop.
- Enumerate the edge cases out loud, then write code that handles them.
- State complexity when you finish, with the alternative and why you did not choose it.
IntArraynotArray<Int>;===for nodes; no smart casts on mutable properties;..includes the end.- Iterative over recursive whenever the input can be large.
- Know the LRU cache both ways. It is the most common senior coding question.
- Narrate. Silent correct code scores lower than narrated code with a typo.
Further Reading
- Kotlin documentation, Collections overview and
ArrayDeque - Java documentation,
LinkedHashMap(theaccessOrderconstructor andremoveEldestEntry) andPriorityQueue - Gayle Laakmann McDowell, Cracking the Coding Interview, for the pattern catalogue
- The Blind 75 and NeetCode 150 problem lists, which these ten are drawn from