Lesson 63 of 83 intermediate

DSA I: Arrays, Strings, HashMaps, Sets & Two Pointers

The patterns that solve 60% of coding interview problems — mastered in Kotlin

Open interactive version (quiz + challenge)

Real-world analogy

Solving DSA problems is like being a detective. Your first instinct (brute force) is to interview every witness one by one. But a good detective builds a 'suspect board' (HashMap) first — then each new clue is a O(1) lookup instead of interviewing everyone again. The Two Pointer technique is like two detectives starting from opposite ends of a crime scene and walking toward each other.

What is it?

Arrays, HashMaps, Sets, and the Two-Pointer technique form the foundation of coding interviews. Together, they solve the majority of easy and medium problems. The core insight is that nested loops (O(n^2)) can almost always be replaced by a single pass with a HashMap or two-pointer scan (O(n)). Mastering these patterns means recognizing the shape of a problem and immediately knowing which data structure breaks the inner loop.

Real-world relevance

In the Hazira Khata app, attendance deduplication was implemented using a HashSet of (employeeId + date) composite keys. The initial implementation used a nested loop to check existing records — O(n^2) and causing UI jank on large attendance sheets. The fix used a HashSet populated at load time, making duplicate detection O(1) per record — O(n) total. This is the two-sum pattern applied to a real product.

Key points

Code example

// ─── TWO SUM (HashMap, O(n)) ───
fun twoSum(nums: IntArray, target: Int): IntArray {
    val seen = HashMap<Int, Int>() // value -> index
    for (i in nums.indices) {
        val complement = target - nums[i]
        if (seen.containsKey(complement)) {
            return intArrayOf(seen[complement]!!, i)
        }
        seen[nums[i]] = i
    }
    return intArrayOf() // no solution found
}

// ─── TWO SUM II — SORTED ARRAY (Two Pointers, O(n), O(1) space) ───
fun twoSumSorted(numbers: IntArray, target: Int): IntArray {
    var left = 0
    var right = numbers.size - 1
    while (left < right) {
        val sum = numbers[left] + numbers[right]
        when {
            sum == target -> return intArrayOf(left + 1, right + 1) // 1-indexed
            sum < target -> left++
            else -> right--
        }
    }
    return intArrayOf()
}

// ─── CONTAINS DUPLICATE (HashSet, O(n)) ───
fun containsDuplicate(nums: IntArray): Boolean {
    val seen = HashSet<Int>()
    for (num in nums) {
        if (!seen.add(num)) return true // add returns false if already present
    }
    return false
}

// ─── CONTAINER WITH MOST WATER (Two Pointers, O(n)) ───
fun maxWater(height: IntArray): Int {
    var left = 0
    var right = height.size - 1
    var maxArea = 0
    while (left < right) {
        val area = minOf(height[left], height[right]) * (right - left)
        maxArea = maxOf(maxArea, area)
        if (height[left] < height[right]) left++ else right--
    }
    return maxArea
}

// ─── SLIDING WINDOW — MAX SUM SUBARRAY OF SIZE K ───
fun maxSumSubarrayK(nums: IntArray, k: Int): Int {
    var windowSum = nums.take(k).sum() // initial window
    var maxSum = windowSum
    for (i in k until nums.size) {
        windowSum += nums[i] - nums[i - k] // slide: add new, remove old
        maxSum = maxOf(maxSum, windowSum)
    }
    return maxSum
}

Line-by-line walkthrough

  1. 1. twoSum iterates nums once. For each nums[i], the complement is target - nums[i] — if the complement was seen before, we have our pair.
  2. 2. seen[nums[i]] = i stores the current element's value as key and its index as value — we store AFTER checking so an element doesn't pair with itself.
  3. 3. twoSumSorted uses left and right pointers starting at both ends. If the sum is too small, we need a larger number so left moves right. If too large, right moves left.
  4. 4. containsDuplicate uses HashSet.add() which returns false if the element already exists — this is cleaner than checking contains() then add() which would be two operations.
  5. 5. maxWater computes area as min(height[L], height[R]) * (R - L). Moving the smaller height pointer is the key insight — moving the larger one cannot possibly improve the minimum.
  6. 6. maxSumSubarrayK initializes the window sum for the first k elements, then for each new position adds nums[i] (new right) and subtracts nums[i-k] (old left) — the window slides in O(1) per step.
  7. 7. The sliding window avoids recomputing the entire window sum each time — without it, each window computation would be O(k), making the total O(n*k).
  8. 8. All HashMap-based solutions trade O(n) extra space for O(n) time — always mention this space-time tradeoff in interviews and confirm whether space constraints exist.

Spot the bug

fun twoSum(nums: IntArray, target: Int): IntArray {
    val seen = HashMap<Int, Int>()
    for (i in nums.indices) {
        val complement = target - nums[i]
        seen[nums[i]] = i
        if (seen.containsKey(complement)) {
            return intArrayOf(seen[complement]!!, i)
        }
    }
    return intArrayOf()
}

// Test: nums = [3, 3], target = 6
// Expected: [0, 1]
// Actual: ???
Need a hint?
The order of operations inside the loop is wrong. Think about what happens when the same value is both the number and its own complement.
Show answer
Bug: The element is added to the map (seen[nums[i]] = i) BEFORE checking if the complement exists. When nums = [3, 3] and target = 6, at i=0: nums[0]=3 is added to the map as seen[3]=0. Then complement = 6-3 = 3, which IS in the map (just added), so it returns [0, 0] — using the same element twice. The fix is to CHECK for the complement FIRST, then add the current element to the map. This ensures you only find pairs using two different elements at different indices.

Explain like I'm 5

Imagine you need to find two kids in class whose ages add to 20. The brute-force way: ask every kid their age, then ask every OTHER kid if they're the right age — super slow. The smart way: write down each kid's age on a sticky note on the wall. For each new kid, just look at the wall for '20 minus their age'. The wall is your HashMap — instant lookup.

Fun fact

The Two-Sum problem was one of the first problems on LeetCode and remains the #1 most solved problem on the platform with over 10 million accepted solutions. It appears in Google, Meta, Amazon, and Microsoft interviews because it perfectly tests whether a candidate knows to trade O(n) space for O(n) time using a HashMap — the foundational insight of most algorithmic optimization.

Hands-on challenge

Implement the 'Longest Substring Without Repeating Characters' problem using a sliding window and HashMap. Input: a string s. Output: the length of the longest substring without duplicate characters. Example: 'abcabcbb' returns 3 ('abc'). Trace through 'pwwkew' step by step showing the window boundaries and HashMap state at each character. What is the time and space complexity?

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery