Skip to content

Practice: Warm-Up Problems ​

Three essential problems to calibrate your interview readiness

These warm-up problems cover fundamental patterns that appear repeatedly in technical interviews. Master these before moving to more complex challenges.


1. Move Zeros to End of Array ​

Problem Statement ​

Given an integer array nums, move all 0's to the end of it while maintaining the relative order of the non-zero elements. You must do this in-place without making a copy of the array.

Example:

  • Input: nums = [0, 1, 0, 3, 12]
  • Output: [1, 3, 12, 0, 0]

Constraints:

  • 1 <= nums.length <= 10^4
  • -2^31 <= nums[i] <= 2^31 - 1

Approach ​

The two-pointer technique is optimal for this problem. We use two pointers:

  • write_ptr: Tracks where the next non-zero element should be placed
  • read_ptr: Iterates through the array to find non-zero elements

Key Insight: Instead of directly placing non-zero elements, we swap them with the element at write_ptr. This ensures zeros are pushed toward the end without being overwritten.

Algorithm Steps:

  1. Initialize write_ptr = 0
  2. Iterate through the array with read_ptr
  3. When a non-zero element is found, swap it with nums[write_ptr]
  4. Increment write_ptr after each swap
  5. Continue until all elements are processed

Solution (Python) ​

python
def moveZeroes(nums: list[int]) -> None:
    """
    Move all zeros to the end while maintaining relative order of non-zeros.
    Modifies the array in-place.

    Time: O(n) - single pass through array
    Space: O(1) - only two pointers used
    """
    write_ptr = 0  # Position to place next non-zero element

    for read_ptr in range(len(nums)):
        if nums[read_ptr] != 0:
            # Swap non-zero element to the write position
            nums[write_ptr], nums[read_ptr] = nums[read_ptr], nums[write_ptr]
            write_ptr += 1

    # Array is now modified in-place with zeros at the end


# Alternative: Two-pass approach (easier to understand)
def moveZeroes_two_pass(nums: list[int]) -> None:
    """
    Alternative approach: First move all non-zeros, then fill remaining with zeros.
    """
    write_ptr = 0

    # Pass 1: Move all non-zero elements to the front
    for num in nums:
        if num != 0:
            nums[write_ptr] = num
            write_ptr += 1

    # Pass 2: Fill remaining positions with zeros
    while write_ptr < len(nums):
        nums[write_ptr] = 0
        write_ptr += 1

Complexity: Time O(n) · Space O(1)

  • Time: Single pass through the array of n elements, with O(1) swap operation per element
  • Space: Only two integer pointer variables used regardless of input size (in-place modification)

Complexity Analysis ​

MetricComplexityExplanation
TimeO(n)Single pass through the array
SpaceO(1)Only two integer pointers used

Mermaid Diagram ​

Interview Variant ​

How Google might phrase this:

"You're processing a stream of sensor readings where 0 represents a failed reading. Design an in-place algorithm to compact all valid readings to the front while preserving their order. Discuss trade-offs between different approaches."

Follow-up questions to expect:

  1. Stability: "What if we need to track the original indices of each element?"
  2. Parallelization: "How would you parallelize this for a very large dataset?"
  3. Generalization: "What if instead of zeros, we want to move all negative numbers to the end?"
  4. Memory constraints: "What if the array is memory-mapped and swaps are expensive?"

Extended variant solution:

python
def moveNegativesToEnd(nums: list[int]) -> None:
    """
    Google follow-up: Move all negative numbers to the end.
    Same two-pointer technique applies.
    """
    write_ptr = 0

    for read_ptr in range(len(nums)):
        if nums[read_ptr] >= 0:  # Only change: condition check
            nums[write_ptr], nums[read_ptr] = nums[read_ptr], nums[write_ptr]
            write_ptr += 1

Complexity: Time O(n) · Space O(1)

  • Time: Single pass through all n elements with constant-time swap operations
  • Space: Two pointer variables only, modifies input array in-place

2. Remove Duplicates in String ​

Problem Statement ​

Given a string s, remove duplicate characters so that each character appears only once. Return the resulting string.

Variations of this problem:

  • Basic: Keep first occurrence of each character
  • LeetCode 1047: Remove all adjacent duplicate characters repeatedly
  • LeetCode 316: Remove duplicates to get lexicographically smallest result

Example (Basic version):

  • Input: s = "abracadabra"
  • Output: "abrcd" (keeping first occurrence of each)

Example (Adjacent duplicates - LeetCode 1047):

  • Input: s = "abbaca"
  • Output: "ca" (remove "bb" -> "aaca", remove "aa" -> "ca")

Approach ​

For Basic Duplicate Removal (Hash Set):

  • Use a set to track seen characters
  • Build result string with only first occurrences

For Adjacent Duplicate Removal (Stack-based):

  • Use a stack to track characters
  • If current character matches stack top, pop (remove pair)
  • Otherwise, push current character
  • The stack-based approach handles cascading removals automatically

Key Insight: "When it comes to removing duplicates, consider using a stack first!" The stack naturally handles the "undo" operation when duplicates are found.

Solution (Python) ​

python
def removeDuplicates_basic(s: str) -> str:
    """
    Remove duplicate characters, keeping first occurrence of each.

    Time: O(n) - single pass through string
    Space: O(k) - where k is the size of character set (at most 26 for lowercase)
    """
    seen = set()
    result = []

    for char in s:
        if char not in seen:
            seen.add(char)
            result.append(char)

    return ''.join(result)

Complexity: Time O(n) · Space O(k)

  • Time: Single pass through string of length n, with O(1) set lookup per character
  • Space: Set stores at most k unique characters (k=26 for lowercase letters), result list stores up to k characters
python
def removeAdjacentDuplicates(s: str) -> str:
    """
    LeetCode 1047: Remove all adjacent duplicates repeatedly.
    Uses stack-based approach for elegant cascading removal.

    Example: "abbaca" -> "ca"
    - Process 'a': stack = ['a']
    - Process 'b': stack = ['a', 'b']
    - Process 'b': matches top, pop -> stack = ['a']
    - Process 'a': matches top, pop -> stack = []
    - Process 'c': stack = ['c']
    - Process 'a': stack = ['c', 'a']
    - Result: "ca"

    Time: O(n) - each character pushed and popped at most once
    Space: O(n) - stack can hold all characters in worst case
    """
    stack = []

    for char in s:
        if stack and stack[-1] == char:
            stack.pop()  # Remove adjacent duplicate
        else:
            stack.append(char)

    return ''.join(stack)

Complexity: Time O(n) · Space O(n)

  • Time: Each character is pushed and popped at most once, giving 2n operations worst case
  • Space: Stack can hold all n characters when no adjacent duplicates exist (e.g., "abcdef")
python
def removeAdjacentDuplicates_k(s: str, k: int) -> str:
    """
    LeetCode 1209: Remove k adjacent duplicates.
    Track character and its consecutive count.

    Example: s = "deeedbbcccbdaa", k = 3 -> "aa"

    Time: O(n)
    Space: O(n)
    """
    # Stack stores [character, count] pairs
    stack = []

    for char in s:
        if stack and stack[-1][0] == char:
            stack[-1][1] += 1
            if stack[-1][1] == k:
                stack.pop()  # Remove k duplicates
        else:
            stack.append([char, 1])

    # Reconstruct string from stack
    return ''.join(char * count for char, count in stack)

Complexity: Time O(n) · Space O(n)

  • Time: Single pass through string; each character processed once with O(1) stack operations
  • Space: Stack stores [char, count] pairs; worst case n entries (each count 1) when no two consecutive characters are equal, e.g. "ababab"

Complexity Analysis ​

ApproachTimeSpaceUse Case
Basic (Set)O(n)O(k)Keep first occurrence only
Adjacent (Stack)O(n)O(n)Remove adjacent pairs
K-Adjacent (Stack)O(n)O(n)Remove k consecutive duplicates

where k = character set size (26 for lowercase letters)

Mermaid Diagram ​

Interview Variant ​

How Google might phrase this:

"You're building a text editor with an 'undo' feature. When a user types the same character twice consecutively, it should auto-delete both. Implement this efficiently for real-time input processing."

Follow-up questions to expect:

  1. Real-time processing: "How would you handle this with streaming input?"
  2. Undo/Redo: "How would you add undo functionality to restore deleted duplicates?"
  3. Case sensitivity: "What if 'A' and 'a' should be considered duplicates?"
  4. K-duplicates: "What if we want to remove exactly k consecutive duplicates?"

Extended variant solution:

python
class TextEditor:
    """
    Google follow-up: Real-time duplicate removal with undo support.
    """
    def __init__(self):
        self.stack = []
        self.undo_stack = []  # Track removed pairs for undo

    def type_char(self, char: str) -> str:
        """Process a single character input."""
        if self.stack and self.stack[-1] == char:
            removed = self.stack.pop()
            self.undo_stack.append((removed, char))  # Save for undo
        else:
            self.stack.append(char)
            self.undo_stack.append((None, char))  # No removal occurred

        return ''.join(self.stack)

    def undo(self) -> str:
        """Undo last operation."""
        if not self.undo_stack:
            return ''.join(self.stack)

        removed, typed = self.undo_stack.pop()
        if removed:
            # Restore the removed pair
            self.stack.append(removed)
            self.stack.append(typed)
        else:
            # Remove the typed character
            if self.stack:
                self.stack.pop()

        return ''.join(self.stack)

Complexity: Time O(1) per operation · Space O(n)

  • Time: type_char and undo are both O(1) for stack operations (join is O(k) where k is current string length)
  • Space: Two stacks storing up to n characters and n operation tuples for full undo history

3. Contiguous Subarray Sum ​

Problem Statement ​

Given an integer array nums and an integer k, return true if the array contains a good subarray, otherwise return false.

A good subarray is a subarray where:

  • Its length is at least two, and
  • The sum of its elements is a multiple of k

Example:

  • Input: nums = [23, 2, 4, 6, 7], k = 6
  • Output: true
  • Explanation: [2, 4] is a subarray of length 2 with sum 6, which is a multiple of 6.

Constraints:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^9
  • 1 <= k <= 2^31 - 1

Approach ​

The prefix sum with hash map approach is optimal for this problem.

Key Mathematical Insight: If two prefix sums have the same remainder when divided by k, the subarray between them has a sum that is a multiple of k.

Why this works:

  • Let prefix[i] = sum of elements from index 0 to i
  • Let prefix[j] = sum of elements from index 0 to j (where j > i)
  • Subarray sum from i+1 to j = prefix[j] - prefix[i]
  • If prefix[i] % k == prefix[j] % k, then (prefix[j] - prefix[i]) % k == 0

Algorithm:

  1. Compute running prefix sum
  2. Store first occurrence of each prefix_sum % k in a hash map
  3. If we see the same remainder at indices that are at least 2 apart, return true

Solution (Python) ​

python
def checkSubarraySum(nums: list[int], k: int) -> bool:
    """
    Check if array has a contiguous subarray (length >= 2) whose sum is multiple of k.

    Uses prefix sum + hash map approach.
    Key insight: If prefix[i] % k == prefix[j] % k (where j > i + 1),
    then subarray from i+1 to j has sum divisible by k.

    Time: O(n) - single pass through array
    Space: O(min(n, k)) - hash map stores at most k different remainders
    """
    # Map: remainder -> first index where this remainder was seen
    # Initialize with 0: -1 to handle case where prefix sum itself is multiple of k
    remainder_to_index = {0: -1}

    prefix_sum = 0

    for i, num in enumerate(nums):
        prefix_sum += num
        remainder = prefix_sum % k

        if remainder in remainder_to_index:
            # Check if subarray length is at least 2
            if i - remainder_to_index[remainder] >= 2:
                return True
            # Don't update index - we want the FIRST occurrence
        else:
            remainder_to_index[remainder] = i

    return False

Complexity: Time O(n) · Space O(min(n, k))

  • Time: Single pass through n elements with O(1) hash map operations per element
  • Space: Hash map stores at most min(n, k) entries since remainders are in range [0, k-1]
python
def subarraySumEqualsK(nums: list[int], k: int) -> int:
    """
    LeetCode 560: Count subarrays with sum exactly equal to k.
    Related problem using same prefix sum technique.

    Time: O(n)
    Space: O(n)
    """
    # Map: prefix_sum -> count of times we've seen this sum
    prefix_count = {0: 1}  # Empty prefix has sum 0

    prefix_sum = 0
    count = 0

    for num in nums:
        prefix_sum += num

        # If (prefix_sum - k) exists, those subarrays sum to k
        target = prefix_sum - k
        if target in prefix_count:
            count += prefix_count[target]

        # Update count for current prefix sum
        prefix_count[prefix_sum] = prefix_count.get(prefix_sum, 0) + 1

    return count

Complexity: Time O(n) · Space O(n)

  • Time: Single pass through array with O(1) hash map lookup and update per element
  • Space: Hash map can store up to n distinct prefix sums (when all elements are unique)

Complexity Analysis ​

MetricComplexityExplanation
TimeO(n)Single pass through the array
SpaceO(min(n, k))Hash map stores at most k unique remainders

Mermaid Diagram ​

Interview Variant ​

How Google might phrase this:

"You're analyzing user session data where each entry represents time spent on a page. Find if there's any contiguous sequence of pages (at least 2) where total time is exactly divisible by the session timeout interval. Optimize for streaming data."

Follow-up questions to expect:

  1. Count all: "Instead of just checking existence, count ALL such subarrays."
  2. Return indices: "Return the actual starting and ending indices of one such subarray."
  3. Maximum length: "Find the longest subarray whose sum is divisible by k."
  4. Negative numbers: "How does your solution change if numbers can be negative?"

Extended variant solution:

python
def findLongestDivisibleSubarray(nums: list[int], k: int) -> tuple[int, int]:
    """
    Google follow-up: Find the longest subarray with sum divisible by k.
    Returns (start_index, end_index) or (-1, -1) if none exists.
    """
    remainder_to_index = {0: -1}
    prefix_sum = 0
    max_length = 0
    result = (-1, -1)

    for i, num in enumerate(nums):
        prefix_sum += num
        remainder = prefix_sum % k

        if remainder in remainder_to_index:
            start = remainder_to_index[remainder] + 1
            length = i - remainder_to_index[remainder]

            if length > max_length:
                max_length = length
                result = (start, i)
            # Don't update - keep first occurrence for longest subarray
        else:
            remainder_to_index[remainder] = i

    return result

Complexity: Time O(n) · Space O(min(n, k))

  • Time: Single pass through n elements; O(1) hash operations and comparisons per element
  • Space: Hash map stores first occurrence of each remainder, bounded by min(n, k)
python
def countDivisibleSubarrays(nums: list[int], k: int) -> int:
    """
    Google follow-up: Count ALL subarrays (length >= 2) with sum divisible by k.
    """
    from collections import defaultdict

    # Store count and list of indices for each remainder
    remainder_info = defaultdict(list)
    remainder_info[0].append(-1)

    prefix_sum = 0
    count = 0

    for i, num in enumerate(nums):
        prefix_sum += num
        remainder = prefix_sum % k

        # Count how many previous indices have same remainder and are at least 2 apart
        for prev_idx in remainder_info[remainder]:
            if i - prev_idx >= 2:
                count += 1

        remainder_info[remainder].append(i)

    return count

Complexity: Time O(n^2) worst case · Space O(n)

  • Time: For each element, iterates through all previous indices with same remainder; worst case when all remainders equal
  • Space: Stores all n indices across remainder lists

Key Patterns Demonstrated ​

Pattern Summary Table ​

PatternProblemCore TechniqueTimeSpace
Two-PointerMove ZerosRead/Write pointersO(n)O(1)
Hash SetRemove Duplicates (Basic)Track seen elementsO(n)O(k)
StackRemove Duplicates (Adjacent)LIFO for undo operationsO(n)O(n)
Prefix Sum + HashMapContiguous Subarray SumRemainder trackingO(n)O(k)

When to Use Each Pattern ​

Key Takeaways ​

  1. Two-Pointer Pattern

    • Perfect for in-place array modifications
    • Often reduces O(n^2) brute force to O(n)
    • Common variants: fast/slow, left/right, read/write
  2. Hash Set Pattern

    • O(1) lookup for "have we seen this before?"
    • Essential for duplicate detection
    • Consider space vs time trade-offs
  3. Stack Pattern

    • Natural for "undo" or "matching" operations
    • Key for adjacent element problems
    • Think: "What if the last element changes things?"
  4. Prefix Sum Pattern

    • Transforms range queries from O(n) to O(1)
    • Combined with hash map for sum-based problems
    • Key insight: same remainder means divisible difference

Practice Checklist ​

Before your interview, ensure you can:

  • Implement two-pointer swap without bugs
  • Handle edge cases (empty arrays, single elements)
  • Use stack for adjacent duplicate removal
  • Explain prefix sum mathematics clearly
  • Discuss time/space complexity trade-offs
  • Extend basic solutions to follow-up variants

References ​