← Back to Tutorial Index

Chapter 1: Arrays & Strings

Master array and string manipulation techniques essential for coding interviews. Learn when and where to use each algorithm with detailed step-by-step explanations.

1. Two Pointers Technique

šŸ“Œ When and Where to Use

  • Sorted arrays: Finding pairs, triplets, or subarrays that meet certain conditions
  • Palindrome checking: Comparing characters from both ends
  • Removing duplicates: In-place array modification
  • Partitioning: Separating elements based on conditions
  • Real-world example: Finding two numbers in a sorted array that sum to a target (like finding two products in a sorted price list)

Algorithm Explanation

The two pointers technique uses two pointers (indices) that traverse the array from different positions, typically:

  • Opposite ends: One pointer starts at the beginning, another at the end
  • Same direction: Both pointers move in the same direction at different speeds
  • Key insight: By moving pointers based on conditions, we can solve problems in O(n) time instead of O(n²)

Example: Two Sum in Sorted Array

Problem: Given a sorted array and a target, find two numbers that add up to the target.

def two_sum_sorted(nums, target):
    """
    Find two numbers in sorted array that sum to target.
    
    Args:
        nums: List of integers (sorted in ascending order)
        target: Target sum
    
    Returns:
        List of two indices [i, j] where nums[i] + nums[j] == target
        Returns [-1, -1] if no such pair exists
    """
    # Step 1: Initialize two pointers
    # left starts at the beginning (smallest values)
    # right starts at the end (largest values)
    left = 0
    right = len(nums) - 1
    
    # Step 2: Continue until pointers meet
    while left < right:
        # Step 3: Calculate current sum
        current_sum = nums[left] + nums[right]
        
        # Step 4: Compare with target
        if current_sum == target:
            # Found the pair!
            return [left, right]
        elif current_sum < target:
            # Sum is too small, move left pointer right
            # Why? Because array is sorted, moving left right increases sum
            left += 1
        else:
            # Sum is too large, move right pointer left
            # Why? Moving right left decreases sum
            right -= 1
    
    # Step 5: No pair found
    return [-1, -1]

Step-by-Step Code Walkthrough:

Step 1 - Initialization: We set left = 0 (first element) and right = len(nums) - 1 (last element). This gives us the smallest and largest values in the sorted array.
Step 2 - Loop Condition: We continue while left < right. When they meet or cross, we've checked all possible pairs.
Step 3 - Calculate Sum: At each iteration, we compute nums[left] + nums[right] to see if it equals our target.
Step 4 - Decision Logic:
  • If sum equals target → Found! Return indices
  • If sum < target → Need larger sum → Move left pointer right (to larger values)
  • If sum > target → Need smaller sum → Move right pointer left (to smaller values)
Step 5 - Termination: If loop ends without finding a pair, return [-1, -1] indicating no solution.

Time Complexity:

O(n) - Each element is visited at most once

Space Complexity:

O(1) - Only using a constant amount of extra space

2. Sliding Window Technique

šŸ“Œ When and Where to Use

  • Subarray problems: Finding subarrays of fixed or variable size
  • String problems: Finding longest substring with unique characters
  • Maximum/minimum: Finding max/min in all subarrays of size k
  • Real-world example: Finding the maximum sum of k consecutive days in a sales dataset, or the longest unique product ID sequence in a shopping cart

Algorithm Explanation

The sliding window technique maintains a "window" of elements in the array/string. Instead of recalculating everything, we:

  • Expand: Add elements to the window
  • Shrink: Remove elements from the window
  • Update: Maintain the current state (sum, count, etc.) incrementally

Example: Maximum Sum Subarray of Size K

Problem: Find the maximum sum of any contiguous subarray of size k.

def max_sum_subarray_k(nums, k):
    """
    Find maximum sum of subarray of size k using sliding window.
    
    Args:
        nums: List of integers
        k: Size of subarray
    
    Returns:
        Maximum sum of any subarray of size k
    """
    # Step 1: Handle edge case
    if len(nums) < k:
        return 0
    
    # Step 2: Calculate sum of first window
    # We start by computing the sum of the first k elements
    window_sum = sum(nums[:k])
    max_sum = window_sum
    
    # Step 3: Slide the window
    # Start from index k (after the first window)
    for i in range(k, len(nums)):
        # Step 3a: Remove leftmost element from window
        # Subtract the element that's leaving the window
        window_sum -= nums[i - k]
        
        # Step 3b: Add new element to window
        # Add the element that's entering the window
        window_sum += nums[i]
        
        # Step 3c: Update maximum sum
        # Compare current window sum with maximum seen so far
        max_sum = max(max_sum, window_sum)
    
    return max_sum

Step-by-Step Code Walkthrough:

Step 1 - Edge Case: If array length is less than k, no valid subarray exists, return 0.
Step 2 - Initial Window: Calculate sum of first k elements: nums[0] + nums[1] + ... + nums[k-1]. This is our starting window.
Step 3 - Sliding: For each position i from k to len(nums)-1:
  • Remove: Subtract nums[i-k] (element leaving window)
  • Add: Add nums[i] (element entering window)
  • Update: Compare current sum with maximum
Key Insight: Instead of recalculating sum from scratch (O(k) each time), we update in O(1) by subtracting one element and adding another.

Visualization Example:

Array: [1, 4, 2, 10, 23, 3, 1, 0, 20], k = 4

  • Window 1: [1, 4, 2, 10] → sum = 17
  • Window 2: [4, 2, 10, 23] → sum = 17 - 1 + 23 = 39
  • Window 3: [2, 10, 23, 3] → sum = 39 - 4 + 3 = 38
  • ... and so on

Time Complexity:

O(n) - Each element is visited exactly once

Space Complexity:

O(1) - Only using constant extra space

3. Hash Maps and Hash Sets

šŸ“Œ When and Where to Use

  • Frequency counting: Counting occurrences of elements
  • Lookup optimization: O(1) average time lookups instead of O(n)
  • Duplicate detection: Checking if element exists
  • Complement finding: Finding pairs where one complements the other
  • Real-world example: Tracking user sessions, counting word frequencies in text analysis, or finding duplicate transactions in a payment system

Algorithm Explanation

Hash maps (dictionaries) and hash sets provide:

  • O(1) average: Insert, lookup, and delete operations
  • Key-value storage: Maps store key-value pairs
  • Set operations: Sets store unique elements
  • Trade-off: Uses extra space for speed

Example: Two Sum (Unsorted Array)

Problem: Find two numbers in an unsorted array that sum to target.

def two_sum_unsorted(nums, target):
    """
    Find two numbers that sum to target using hash map.
    
    Args:
        nums: List of integers (unsorted)
        target: Target sum
    
    Returns:
        List of two indices [i, j] where nums[i] + nums[j] == target
    """
    # Step 1: Create hash map to store number -> index mapping
    # Key: number value, Value: its index in array
    num_map = {}
    
    # Step 2: Iterate through array
    for i, num in enumerate(nums):
        # Step 3: Calculate complement
        # If nums[i] + complement == target, then complement = target - nums[i]
        complement = target - num
        
        # Step 4: Check if complement exists in map
        if complement in num_map:
            # Found! Return indices
            # num_map[complement] is the index where we saw the complement
            return [num_map[complement], i]
        
        # Step 5: Store current number and its index
        # We store it after checking to avoid using same element twice
        num_map[num] = i
    
    # Step 6: No pair found
    return [-1, -1]

Step-by-Step Code Walkthrough:

Step 1 - Initialize Map: Create empty dictionary to store number → index mappings. This allows O(1) lookup of where we've seen a number.
Step 2 - Iterate: Go through each element with its index using enumerate().
Step 3 - Calculate Complement: For number num, the complement is target - num. If we've seen the complement before, we have a pair!
Step 4 - Check Map: If complement exists in map, return the index where we saw it (num_map[complement]) and current index i.
Step 5 - Store Current: Store current number and index in map for future lookups. We do this AFTER checking to avoid using the same element twice.
Step 6 - Not Found: If loop completes without finding a pair, return [-1, -1].

Example Trace:

nums = [2, 7, 11, 15], target = 9

  • i=0, num=2: complement=7, not in map → store {2: 0}
  • i=1, num=7: complement=2, found in map! → return [0, 1]

Time Complexity:

O(n) - Single pass through array

Space Complexity:

O(n) - Hash map can store up to n elements

4. Kadane's Algorithm (Maximum Subarray Sum)

šŸ“Œ When and Where to Use

  • Maximum subarray: Finding contiguous subarray with maximum sum
  • Stock prices: Best time to buy and sell stock (one transaction)
  • Profit analysis: Finding maximum profit in time series data
  • Real-world example: Finding the most profitable consecutive days in stock trading, or the best consecutive period in sales data

Algorithm Explanation

Kadane's algorithm uses dynamic programming thinking:

  • Key insight: At each position, decide whether to extend the previous subarray or start fresh
  • Decision rule: If current element makes the sum negative, start fresh (negative sum can't help future sums)
  • Optimal substructure: Maximum sum ending at position i depends on maximum sum ending at position i-1

Example: Maximum Subarray Sum

Problem: Find the contiguous subarray with the largest sum.

def max_subarray_sum(nums):
    """
    Find maximum sum of contiguous subarray using Kadane's algorithm.
    
    Args:
        nums: List of integers (can contain negative numbers)
    
    Returns:
        Maximum sum of any contiguous subarray
    """
    # Step 1: Initialize
    # max_ending_here: maximum sum of subarray ending at current position
    # max_so_far: maximum sum seen so far (our answer)
    max_ending_here = nums[0]
    max_so_far = nums[0]
    
    # Step 2: Process each element
    for i in range(1, len(nums)):
        # Step 3: Decision: extend or start fresh?
        # Option 1: Extend previous subarray: max_ending_here + nums[i]
        # Option 2: Start fresh: nums[i]
        # Choose the maximum (greedy choice)
        max_ending_here = max(max_ending_here + nums[i], nums[i])
        
        # Step 4: Update global maximum
        # Track the best sum we've seen across all positions
        max_so_far = max(max_so_far, max_ending_here)
    
    return max_so_far

Step-by-Step Code Walkthrough:

Step 1 - Initialize:
  • max_ending_here: Tracks maximum sum of subarray ending at current position
  • max_so_far: Tracks the overall maximum sum we've seen
  • Both start with first element
Step 2 - Iterate: Process each element from index 1 onwards.
Step 3 - Decision: At each position i, we have two choices:
  • Extend: Add nums[i] to previous subarray → max_ending_here + nums[i]
  • Start fresh: Begin new subarray from nums[i] → nums[i]
  • Choose the maximum (greedy choice)
Step 4 - Update Global: Compare max_ending_here with max_so_far and update if we found a better sum.

Example Trace:

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

  • i=0: max_ending_here=-2, max_so_far=-2
  • i=1: max_ending_here=max(-2+1, 1)=1, max_so_far=max(-2, 1)=1
  • i=2: max_ending_here=max(1-3, -3)=-2, max_so_far=1
  • i=3: max_ending_here=max(-2+4, 4)=4, max_so_far=4
  • i=4: max_ending_here=max(4-1, -1)=3, max_so_far=4
  • i=5: max_ending_here=max(3+2, 2)=5, max_so_far=5
  • i=6: max_ending_here=max(5+1, 1)=6, max_so_far=6
  • ... final answer: 6 (subarray [4, -1, 2, 1])

Time Complexity:

O(n) - Single pass through array

Space Complexity:

O(1) - Only using constant extra space

5. String Manipulation Techniques

šŸ“Œ When and Where to Use

  • Palindrome checking: Validating if string reads same forwards and backwards
  • Anagram detection: Checking if two strings contain same characters
  • Pattern matching: Finding patterns in strings
  • Real-world example: Validating credit card numbers, checking if product codes are palindromes, or detecting duplicate usernames

Example: Valid Palindrome

Problem: Check if a string is a palindrome (ignoring case and non-alphanumeric characters).

def is_palindrome(s):
    """
    Check if string is palindrome using two pointers.
    
    Args:
        s: Input string (may contain spaces, punctuation)
    
    Returns:
        True if palindrome, False otherwise
    """
    # Step 1: Initialize two pointers
    left = 0
    right = len(s) - 1
    
    # Step 2: Continue until pointers meet
    while left < right:
        # Step 3: Skip non-alphanumeric characters
        # Move left pointer until we find alphanumeric
        while left < right and not s[left].isalnum():
            left += 1
        
        # Move right pointer until we find alphanumeric
        while left < right and not s[right].isalnum():
            right -= 1
        
        # Step 4: Compare characters (case-insensitive)
        if s[left].lower() != s[right].lower():
            return False
        
        # Step 5: Move pointers inward
        left += 1
        right -= 1
    
    # Step 6: All characters matched
    return True

Step-by-Step Code Walkthrough:

Step 1 - Initialize: Set pointers at both ends of string.
Step 2 - Loop: Continue while left < right.
Step 3 - Skip Non-Alphanumeric: Move pointers past spaces, punctuation, etc. until we find valid characters to compare.
Step 4 - Compare: Compare characters at both pointers (case-insensitive). If different, not a palindrome.
Step 5 - Advance: Move both pointers inward for next comparison.
Step 6 - Success: If loop completes, all characters matched → palindrome!

Time Complexity:

O(n) - Each character visited at most once

Space Complexity:

O(1) - Only using constant extra space

šŸ“š Chapter Summary

  • Two Pointers: Use for sorted arrays, palindrome checking, in-place modifications
  • Sliding Window: Use for subarray/substring problems with fixed or variable size
  • Hash Maps: Use when you need O(1) lookups, frequency counting, or complement finding
  • Kadane's Algorithm: Use for maximum subarray sum problems
  • String Manipulation: Combine two pointers with string-specific operations

Key Takeaway: These techniques often reduce time complexity from O(n²) to O(n) by avoiding nested loops and using smart pointer movement or hash-based lookups.

Next: Chapter 2 - Trees & Binary Trees →