Sorting and binary search are foundational topics in coding interviews. Most companies expect you to implement merge sort or quick sort from scratch and apply binary search to non-obvious problems beyond simple array lookup.
Binary search finds a target in a sorted array by halving the search space each step.
fun binarySearch(arr: IntArray, target: Int): Int {
var left = 0
var right = arr.size - 1
while (left <= right) {
val mid = left + (right - left) / 2
when {
arr[mid] == target -> return mid
arr[mid] < target -> left = mid + 1
else -> right = mid - 1
}
}
return -1
}
Time O(log n). Common pitfalls: using (left + right) / 2 (integer overflow), wrong loop condition (<= vs <), and off-by-one with mid + 1 and mid - 1.
At each step, determine which half is sorted by comparing arr[left] with arr[mid]. If the target falls in the sorted range, search there. Otherwise search the other half.
fun searchRotated(arr: IntArray, target: Int): Int {
var left = 0; var right = arr.size - 1
while (left <= right) {
val mid = left + (right - left) / 2
if (arr[mid] == target) return mid
if (arr[left] <= arr[mid]) {
if (target >= arr[left] && target < arr[mid]) right = mid - 1
else left = mid + 1
} else {
if (target > arr[mid] && target <= arr[right]) left = mid + 1
else right = mid - 1
}
}
return -1
}
Time O(log n). With duplicates, worst case degrades to O(n).
Divide the array in half recursively, sort each half, merge. The merge step does the real work.
fun mergeSort(arr: IntArray, left: Int, right: Int) {
if (left >= right) return
val mid = left + (right - left) / 2
mergeSort(arr, left, mid)
mergeSort(arr, mid + 1, right)
merge(arr, left, mid, right)
}
fun merge(arr: IntArray, left: Int, mid: Int, right: Int) {
val temp = IntArray(right - left + 1)
var i = left; var j = mid + 1; var k = 0
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) temp[k++] = arr[i++]
else temp[k++] = arr[j++]
}
while (i <= mid) temp[k++] = arr[i++]
while (j <= right) temp[k++] = arr[j++]
temp.copyInto(arr, left)
}
Time O(n log n) always, space O(n).
Pick a pivot, partition so everything less goes left, greater goes right. Recurse on each side.
fun quickSort(arr: IntArray, low: Int, high: Int) {
if (low >= high) return
val pivotIndex = partition(arr, low, high)
quickSort(arr, low, pivotIndex - 1)
quickSort(arr, pivotIndex + 1, high)
}
fun partition(arr: IntArray, low: Int, high: Int): Int {
val pivot = arr[high]
var i = low
for (j in low until high) {
if (arr[j] < pivot) {
arr[i] = arr[j].also { arr[j] = arr[i] }
i++
}
}
arr[i] = arr[high].also { arr[high] = arr[i] }
return i
}
Average O(n log n), worst O(n^2). Random pivot selection avoids worst case in practice.
Merge sort for stability or guaranteed performance. Quick sort usually faster in practice due to cache locality.
Binary search works because if arr[mid] < arr[mid + 1], a peak exists on the right. If arr[mid] >= arr[mid + 1], a peak exists on the left (including mid).
fun findPeakElement(arr: IntArray): Int {
var left = 0; var right = arr.size - 1
while (left < right) {
val mid = left + (right - left) / 2
if (arr[mid] < arr[mid + 1]) left = mid + 1
else right = mid
}
return left
}
Time O(log n). The array doesnβt need to be sorted.
Uses partition from quick sort but only recurses into the side containing the kth element. Average O(n), worst O(n^2).
fun findKthLargest(nums: IntArray, k: Int): Int {
val targetIndex = nums.size - k
return quickSelect(nums, 0, nums.size - 1, targetIndex)
}
fun quickSelect(arr: IntArray, low: Int, high: Int, k: Int): Int {
val pivotIndex = partition(arr, low, high)
return when {
pivotIndex == k -> arr[pivotIndex]
pivotIndex < k -> quickSelect(arr, pivotIndex + 1, high, k)
else -> quickSelect(arr, low, pivotIndex - 1, k)
}
}
Sort by start time, then iterate and merge overlapping intervals.
fun mergeIntervals(intervals: Array<IntArray>): List<IntArray> {
if (intervals.isEmpty()) return emptyList()
intervals.sortBy { it[0] }
val result = mutableListOf(intervals[0])
for (i in 1 until intervals.size) {
val last = result.last()
if (intervals[i][0] <= last[1]) {
last[1] = maxOf(last[1], intervals[i][1])
} else {
result.add(intervals[i])
}
}
return result
}
Time O(n log n).
Lower bound: first index where arr[index] >= target. Upper bound: first index where arr[index] > target. Count of elements equal to target is upperBound - lowerBound.
fun lowerBound(arr: IntArray, target: Int): Int {
var left = 0; var right = arr.size
while (left < right) {
val mid = left + (right - left) / 2
if (arr[mid] < target) left = mid + 1
else right = mid
}
return left
}
fun upperBound(arr: IntArray, target: Int): Int {
var left = 0; var right = arr.size
while (left < right) {
val mid = left + (right - left) / 2
if (arr[mid] <= target) left = mid + 1
else right = mid
}
return left
}
Note: right starts at arr.size and loop condition is < because the answer can be past the last element.
If arr[mid] > arr[right], minimum is in the right half. Otherwise itβs in the left half (including mid).
fun findMin(arr: IntArray): Int {
var left = 0; var right = arr.size - 1
while (left < right) {
val mid = left + (right - left) / 2
if (arr[mid] > arr[right]) left = mid + 1
else right = mid
}
return arr[left]
}
Binary search works on any search space with a monotonic property. Examples:
The pattern: binary search on the answer, check feasibility with a helper function.
Comparison-based sorts (merge, quick, heap) have a lower bound of O(n log n). Non-comparison sorts (counting, radix, bucket) can achieve O(n) but only under specific constraints β integers in a bounded range, fixed-length strings.
A stable sort preserves relative order of equal elements. Merge sort, counting sort, and radix sort are stable. Quick sort and heap sort are not. Stability matters when sorting by multiple criteria β Kotlinβs sortedBy uses TimSort (stable), so chained sorts work correctly.
Sort by start time, use a min-heap tracking end times. If a meeting starts before the earliest ending meeting, you need another room.
fun minMeetingRooms(intervals: Array<IntArray>): Int {
if (intervals.isEmpty()) return 0
intervals.sortBy { it[0] }
val endTimes = PriorityQueue<Int>()
endTimes.add(intervals[0][1])
for (i in 1 until intervals.size) {
if (intervals[i][0] >= endTimes.peek()) endTimes.poll()
endTimes.add(intervals[i][1])
}
return endTimes.size
}
Implement from scratch when explicitly asked or when the problem needs a modified sort (counting inversions with merge sort, kth element with quickselect). For everything else, use sortedBy. Interviewers care about problem-solving, but you should explain time complexity.