Module 2.1

Array Operations and Techniques

Master the fundamental building block of data structures. Learn array declaration, traversal, searching, sorting, and advanced interview techniques like two pointers and sliding window.

45 min
Beginner
Hands-on
What You Will Learn
  • Array declaration and initialization
  • Traversal, searching, and sorting
  • Two pointer technique
  • Sliding window pattern
  • 2D arrays and matrix operations
Contents
01

Introduction to Arrays

Imagine a row of numbered lockers in a school hallway—each locker has a number (starting from 0), they're all the same size, and they're right next to each other. That's exactly what an array is in programming! An array is a contiguous block of memory that stores elements of the same data type in a fixed sequence. Think of it as the most basic container in programming—just like how atoms are building blocks for molecules, arrays are the building blocks for more complex data structures like ArrayLists, stacks, queues, and heaps. Mastering arrays is essential because they appear in over 60% of technical interview questions, and once you understand arrays, every other data structure becomes easier to learn.

Key Concept

What is an Array?

An array is a fixed-size, ordered collection of elements of the same data type stored in contiguous (adjacent) memory locations. Each element is accessed using a zero-based index that represents its position in the array, starting from 0 for the first element.

Real-World Analogy: Think of a Train!

Imagine a train with numbered carriages (0, 1, 2, 3...). Each carriage is the same size, they're connected in a line, and each has a seat number. If someone asks "Who's in carriage 5?", you can go directly there—you don't need to walk through carriages 0-4 first. That's exactly how array indexing works!

🚂 Carriage 0 → Carriage 1 → Carriage 2 → ... (just like array indices!)

Why it matters: Arrays provide O(1) random access—meaning you can jump to ANY element instantly, regardless of array size. Whether your array has 10 elements or 10 million, accessing the 5th element takes the same amount of time! This is possible because the computer calculates the exact memory location using: address = base + (index × element_size).

Key Characteristics (Remember These!):
  • Homogeneous: All elements must be of the same data type. An int[] can only hold integers, a String[] can only hold strings. You can't mix apples and oranges!
  • Fixed Size: Once you create an array of size 5, it stays size 5 forever. Need more space? You'll have to create a new, bigger array and copy elements over (or use ArrayList which does this automatically).
  • Zero-Indexed: The first element is at index 0, not 1. This trips up many beginners! An array of 5 elements has indices 0, 1, 2, 3, 4.
  • Contiguous Memory: Elements are stored physically next to each other in RAM. This makes iteration super fast because the CPU can predict and pre-load the next elements.
  • Reference Type: In Java, the array itself lives on the heap memory, and your variable just holds a "pointer" to it—like having a sticky note with the array's address.

Why Arrays Matter in Interviews

Arrays are the most commonly tested data structure in coding interviews. Over 60% of LeetCode problems involve arrays in some form. Mastering array manipulation patterns like two pointers, sliding window, and prefix sums will help you solve a wide variety of problems efficiently.

O(1) Access

Instant access to any element by index using memory address calculation

Contiguous Memory

Elements stored sequentially in memory for excellent cache performance

Fixed Size

Size is fixed at creation. Use ArrayList for dynamic sizing needs

Array Time Complexity Overview

Understanding the time complexity of array operations is crucial for writing efficient code and analyzing algorithms during interviews.

Operation Time Complexity Explanation
Access by index O(1) Direct calculation: base + (index * size)
Search (unsorted) O(n) Must check each element sequentially
Search (sorted) O(log n) Binary search halves the search space
Insert at end O(1) If space available, just place element
Insert at beginning/middle O(n) Must shift all subsequent elements
Delete O(n) Must shift elements to fill gap
02

Declaration and Initialization

Java provides several ways to declare and initialize arrays. Understanding these methods is fundamental to working with arrays effectively. The declaration reserves a reference variable, while initialization allocates memory on the heap and assigns values.

Key Concept

Declaration vs Initialization

Declaration creates a reference variable that can point to an array object. Initialization allocates memory for the array and optionally assigns initial values to its elements. Think of it as two separate steps: first, you create a label/name for your array (declaration), then you actually build the array and attach the label to it (initialization).

Real-World Analogy: Buying a House

Declaration = Writing "My Dream House" on a sticky note. You have a name for it, but there's no actual house yet—the sticky note points to nothing (null).

Initialization = Actually building the house. Now your sticky note points to a real house with real rooms (memory spaces) you can use!

Declaration Only
int[] numbers;

Creates reference variable, no memory allocated yet.

Value is null — using it now causes NullPointerException!

Declaration + Initialization
int[] numbers = new int[5];

Allocates heap memory for exactly 5 integers.

Default values: int→0, boolean→false, Object→null

Basic Declaration Syntax

In Java, you can declare arrays using square brackets either after the type or after the variable name. The preferred style is to place brackets after the type for clarity, as it keeps the type information together.

Preferred Syntax
int[] numbers = new int[5];

Type and brackets together — clearly shows "array of int"

C-Style (Avoid)
int numbers[] = new int[5];

Works but separates type info — confusing with multiple declarations

// Method 1: Declare then initialize (two steps)
int[] numbers;                    // Declaration: creates reference variable
numbers = new int[5];             // Initialization: allocates memory for 5 ints

// Method 2: Declare and initialize in one line (most common)
int[] scores = new int[10];       // Creates array of 10 integers (all default to 0)

// Method 3: Initialize with values (inline) — size is inferred
int[] primes = {2, 3, 5, 7, 11};  // Size is 5, automatically determined

// Method 4: Initialize with new keyword and values (explicit)
int[] fibonacci = new int[]{1, 1, 2, 3, 5, 8};

Method 1 is useful when you need to declare the array in one scope but initialize it later (like in a constructor). Method 2 is the most common approach for local variables. Method 3 is perfect when you know the exact values at compile time—Java counts the elements and sets the size automatically. Method 4 is required when passing an array directly to a method.

Default Values: When you create an array with new, all elements are initialized to default values: 0 for numeric types, false for boolean, null for objects.

Common Array Types

Java supports arrays of any data type, including primitives, objects, and even other arrays (multidimensional).

// Primitive type arrays
int[] integers = new int[5];          // Default: 0
double[] decimals = new double[5];    // Default: 0.0
boolean[] flags = new boolean[5];     // Default: false
char[] characters = new char[5];      // Default: '\u0000' (null char)

// Object arrays
String[] names = new String[3];       // Default: null
Integer[] wrapped = new Integer[3];   // Default: null

// Initialize String array with values
String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri"};

// Get array length (property, not method)
int length = days.length;  // 5 (no parentheses!)

Primitive arrays hold actual values directly in memory, making them more memory-efficient. Object arrays (like String[]) store references to objects, so each element points to a location elsewhere in heap memory. This distinction matters for understanding default values and memory usage.

Numeric Types
int[]→ 0
double[]→ 0.0
long[]→ 0L
float[]→ 0.0f
Character & Boolean
char[]→ '\u0000'
boolean[]→ false
byte[]→ 0
short[]→ 0
Object Types
String[]→ null
Integer[]→ null
Object[]→ null
Custom[]→ null
Common Mistake: Remember that .length is a property for arrays, not a method. Do not use length() - that is for Strings.

Memory Visualization

Understanding how arrays are stored in memory helps you appreciate why access is O(1) and why resizing requires creating a new array.

How Arrays Live in Memory
Index 0
10
1000
Index 1
20
1004
Index 2
30
1008
Index 3
40
1012
Index 4
50
1016

Contiguous memory addresses (each int = 4 bytes)

int[] arr = {10, 20, 30, 40, 50};

// Address formula: address = base + (index × element_size)
// To access arr[2]:
//   address = 1000 + (2 × 4) = 1008  →  Value: 30

// This simple math is why array access is O(1)!
// No matter how big the array, finding any element takes the same time.

Arrays store elements in contiguous (side-by-side) memory locations. The JVM knows the base address and the size of each element, so accessing arr[i] is just simple arithmetic—no searching required. This is why random access is O(1), but also why you can't insert elements in the middle without shifting everything after it.

Why O(1) Access?

Direct calculation: base + (index × size) gives the exact memory location instantly. No iteration needed!

Why Fixed Size?

Memory is allocated as one continuous block. To "resize," you must create a new larger array and copy elements over.

03

Basic Operations

Mastering basic array operations is essential before moving to advanced techniques. These operations form the foundation for solving complex array problems and appear repeatedly in coding interviews.

Key Concept

Array Operations Overview

Array operations are the fundamental actions you perform on arrays—like the basic moves in a video game that you combine to achieve your goal. Understanding when and how to use each operation efficiently is critical for writing optimized code and solving interview problems.

Think of Array Operations Like a Library

Traversal = Walking down every aisle to see all books. Searching = Looking for a specific book title. Modification = Replacing a book on the shelf with a new one. Each action has a "cost" (time complexity) that tells you how long it takes as the library grows.

Traversal

Visiting each element exactly once, like checking every item on a shopping list.

O(n) time
Searching

Finding a specific element—either check one-by-one or use smart divide-and-conquer.

O(n) O(log n)
Modification

Changing a value at a known index—super fast because you go directly there!

O(1) time

Traversing Arrays

There are multiple ways to iterate through an array in Java. Each has its use case depending on whether you need the index, just the values, or need to modify elements during iteration.

int[] numbers = {10, 20, 30, 40, 50};

// Method 1: Traditional for loop (when you need index)
for (int i = 0; i < numbers.length; i++) {
    System.out.println("Index " + i + ": " + numbers[i]);
}

// Method 2: Enhanced for-each loop (when you only need values)
for (int num : numbers) {
    System.out.println(num);
}

// Method 3: Reverse traversal
for (int i = numbers.length - 1; i >= 0; i--) {
    System.out.println(numbers[i]);
}

// Method 4: While loop
int i = 0;
while (i < numbers.length) {
    System.out.println(numbers[i]);
    i++;
}

Common Array Operations

The java.util.Arrays class provides many utility methods for common array operations. These methods save you from writing boilerplate code and are highly optimized.

Printing Arrays: When you try to print an array directly using System.out.println(arr), Java doesn't show you the actual elements. Instead, it displays something cryptic like [I@1a2b3c4 — this is the array's type ([I means int array) followed by its memory address (hash code). This is almost never what you want! The solution is Arrays.toString(), which iterates through all elements and returns a nicely formatted string like [5, 2, 8, 1, 9]. For 2D arrays, use Arrays.deepToString() instead.

import java.util.Arrays;

int[] arr = {5, 2, 8, 1, 9};

// Without Arrays.toString() - prints memory address
System.out.println(arr);                    // [I@6d06d69c (not useful!)

// With Arrays.toString() - prints actual contents
System.out.println(Arrays.toString(arr));   // [5, 2, 8, 1, 9] ✓

Sorting Arrays: The Arrays.sort() method arranges elements in ascending order (smallest to largest). It's highly optimized — for primitive types like int[], Java uses Dual-Pivot Quicksort which has O(n log n) average time complexity. For object arrays like String[] or Integer[], it uses TimSort (a stable hybrid of merge sort and insertion sort). Warning: This method modifies the original array in-place! If you need to keep the original unchanged, create a copy first using Arrays.copyOf() before sorting. For descending order, you must use wrapper classes (Integer[] instead of int[]) with Collections.reverseOrder().

int[] arr = {5, 2, 8, 1, 9};

// Sort modifies the original array
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));   // [1, 2, 5, 8, 9]

// For descending order, use Integer[] with Collections.reverseOrder()
Integer[] arr2 = {5, 2, 8, 1, 9};
Arrays.sort(arr2, Collections.reverseOrder());  // [9, 8, 5, 2, 1]

Binary Search: When you need to find whether an element exists in an array (and where), Arrays.binarySearch() is incredibly fast — it works in O(log n) time, meaning it can search through 1 million elements in just ~20 comparisons! The array must be sorted before calling this method, otherwise you'll get incorrect results. If the element is found, it returns the index. If not found, it returns a negative number: specifically -(insertion point) - 1, where insertion point is where the element would be inserted to maintain sorted order. This negative value is actually useful — you can calculate where to insert a new element!

int[] arr = {1, 2, 5, 8, 9};  // Must be sorted!

int index = Arrays.binarySearch(arr, 5);    // Returns 2 (index of 5)
int notFound = Arrays.binarySearch(arr, 7); // Returns -4 (negative = not found)

// The negative value indicates where the element would be inserted:
// -(insertion point) - 1
Tip: If binarySearch() returns negative, the element doesn't exist. The insertion point would be -(returnValue + 1).

Filling Arrays: The Arrays.fill() method sets every element in an array to a specific value. This is extremely useful for initialization — for example, filling a distance array with Integer.MAX_VALUE for Dijkstra's algorithm, or resetting a visited array to false. You can also fill just a portion of the array by specifying a range fill(arr, fromIndex, toIndex, value) where fromIndex is inclusive and toIndex is exclusive (following Java's standard convention). Time complexity is O(n) where n is the number of elements being filled.

int[] zeros = new int[5];
Arrays.fill(zeros, 7);                      // [7, 7, 7, 7, 7]

// Fill only a range [fromIndex, toIndex)
int[] partial = new int[5];
Arrays.fill(partial, 1, 4, 9);              // [0, 9, 9, 9, 0]
//                   ^  ^
//                   |  └── toIndex (exclusive)
//                   └── fromIndex (inclusive)

Copying Arrays: Creating a true copy of an array is essential when you want to preserve the original data. Arrays.copyOf(arr, length) creates a new array with the specified length — if the new length is shorter, elements are truncated; if longer, the extra positions are filled with default values (0 for int, null for objects). Arrays.copyOfRange(arr, from, to) copies a specific slice of the array. Why this matters: In Java, arrays are objects stored in heap memory. When you write int[] b = a;, you're not copying the array — you're copying the reference (memory address). Both a and b now point to the same array, so changes through one variable affect the other. Use these copy methods to create an independent array.

int[] original = {1, 2, 5, 8, 9};

// Copy entire array
int[] copy = Arrays.copyOf(original, original.length);  // [1, 2, 5, 8, 9]

// Copy with different length (truncate or pad with zeros)
int[] shorter = Arrays.copyOf(original, 3);             // [1, 2, 5]
int[] longer = Arrays.copyOf(original, 7);              // [1, 2, 5, 8, 9, 0, 0]

// Copy a specific range [from, to)
int[] range = Arrays.copyOfRange(original, 1, 4);       // [2, 5, 8]
Why copy matters: Arrays are passed by reference. If you do int[] copy = arr;, both variables point to the SAME array. Use Arrays.copyOf() to create an independent copy.

Comparing Arrays: A common mistake is using == to compare arrays — this only checks if two variables point to the exact same array object in memory, not whether they contain the same elements. Two arrays with identical content will return false with == if they're different objects! Use Arrays.equals(a, b) instead, which compares arrays element-by-element and returns true only if both arrays have the same length and all corresponding elements are equal. For multidimensional arrays (2D, 3D, etc.), use Arrays.deepEquals() which recursively compares nested arrays. Time complexity is O(n) where n is the array length.

int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
int[] c = a;

// == compares references (memory addresses)
System.out.println(a == b);           // false (different objects)
System.out.println(a == c);           // true (same reference)

// Arrays.equals() compares content
System.out.println(Arrays.equals(a, b));  // true ✓ (same content)

// For 2D arrays, use deepEquals()
int[][] matrix1 = {{1, 2}, {3, 4}};
int[][] matrix2 = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepEquals(matrix1, matrix2));  // true
Quick Reference: Arrays Utility Methods
Method Description Time
Arrays.toString(arr) Convert to readable string O(n)
Arrays.sort(arr) Sort in ascending order O(n log n)
Arrays.binarySearch(arr, key) Find index (sorted array only) O(log n)
Arrays.fill(arr, value) Set all elements to value O(n)
Arrays.copyOf(arr, len) Create copy with new length O(n)
Arrays.equals(a, b) Compare array contents O(n)

Finding Min, Max, and Sum

These are the most common operations you will perform on arrays. Here are efficient implementations.

int[] arr = {5, 2, 8, 1, 9, 3};

// Find maximum
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
    if (arr[i] > max) {
        max = arr[i];
    }
}
System.out.println("Max: " + max);  // 9

Finding the maximum value requires comparing each element against our current best candidate. We start by assuming the first element is the largest, then scan through the remaining elements. If we find a bigger number, it becomes our new maximum. This approach guarantees we check every element exactly once. Time: O(n) | Space: O(1)

// Find minimum
int min = arr[0];
for (int num : arr) {
    min = Math.min(min, num);
}
System.out.println("Min: " + min);  // 1

Finding the minimum follows the same logic as maximum, but we look for smaller values instead. Here we use Math.min() which returns the smaller of two numbers. This is cleaner than writing an if-statement and is commonly used in professional code. The enhanced for-loop makes the code more readable. Time: O(n) | Space: O(1)

// Calculate sum
int sum = 0;
for (int num : arr) {
    sum += num;
}
System.out.println("Sum: " + sum);  // 28

Calculating the sum is one of the most fundamental operations. We use an accumulator pattern: start with a sum of zero, then add each element to it. This pattern appears in countless algorithms including calculating averages, totals, and running sums. The enhanced for-loop is perfect here since we don't need the index. Time: O(n) | Space: O(1)

// Calculate average
double avg = (double) sum / arr.length;
System.out.println("Avg: " + avg);  // 4.666...

The average (mean) is calculated by dividing the sum by the count of elements. Important: we cast to double before dividing to get a decimal result. Without the cast, integer division would truncate the result (e.g., 28/6 would give 4 instead of 4.666...). This is a common source of bugs for beginners. Time: O(n) | Space: O(1)

Practice: Basic Operations

Task: Write a method to reverse an array in-place without using extra space.

Show Solution
void reverseArray(int[] arr) {
    int left = 0, right = arr.length - 1;
    
    while (left < right) {
        // Swap elements at left and right
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
        
        left++;
        right--;
    }
}
// Time: O(n), Space: O(1)

Task: Find the second largest element in an array without sorting. Handle edge cases.

Show Solution
int secondLargest(int[] arr) {
    if (arr.length < 2) {
        throw new IllegalArgumentException("Need at least 2 elements");
    }
    
    int first = Integer.MIN_VALUE;
    int second = Integer.MIN_VALUE;
    
    for (int num : arr) {
        if (num > first) {
            second = first;  // Old first becomes second
            first = num;     // Update first
        } else if (num > second && num != first) {
            second = num;    // Update second
        }
    }
    
    if (second == Integer.MIN_VALUE) {
        throw new IllegalArgumentException("No second largest");
    }
    return second;
}
// Time: O(n), Space: O(1)

Task: Rotate an array to the right by K steps. For example, [1,2,3,4,5] rotated by 2 becomes [4,5,1,2,3].

Show Solution
void rotate(int[] nums, int k) {
    k = k % nums.length;  // Handle k > length
    
    // Reverse entire array
    reverse(nums, 0, nums.length - 1);
    // Reverse first k elements
    reverse(nums, 0, k - 1);
    // Reverse remaining elements
    reverse(nums, k, nums.length - 1);
}

void reverse(int[] arr, int start, int end) {
    while (start < end) {
        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
        start++;
        end--;
    }
}
// Time: O(n), Space: O(1)

Task: Move all zeros in the array to the end while maintaining the relative order of non-zero elements. Do it in-place.

Show Solution
void moveZeroes(int[] nums) {
    int insertPos = 0;  // Position to insert non-zero
    
    // Move all non-zero elements to the front
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] != 0) {
            nums[insertPos] = nums[i];
            insertPos++;
        }
    }
    
    // Fill remaining positions with zeros
    while (insertPos < nums.length) {
        nums[insertPos] = 0;
        insertPos++;
    }
}
// Time: O(n), Space: O(1)
// Example: [0,1,0,3,12] -> [1,3,12,0,0]

Task: Given an array containing n distinct numbers from 0 to n, find the one number missing from the range.

Show Solution
// Method 1: Using sum formula
int missingNumber(int[] nums) {
    int n = nums.length;
    int expectedSum = n * (n + 1) / 2;
    int actualSum = 0;
    
    for (int num : nums) {
        actualSum += num;
    }
    
    return expectedSum - actualSum;
}

// Method 2: Using XOR (handles overflow better)
int missingNumberXOR(int[] nums) {
    int xor = nums.length;  // Start with n
    
    for (int i = 0; i < nums.length; i++) {
        xor ^= i ^ nums[i];
    }
    
    return xor;
}
// Time: O(n), Space: O(1)
// Example: [3,0,1] -> 2 (missing from 0,1,2,3)

Task: Given an array of n+1 integers where each integer is in range [1, n], find the duplicate number. There is only one duplicate but it could appear multiple times.

Show Solution
// Floyd's Cycle Detection (Tortoise and Hare)
int findDuplicate(int[] nums) {
    // Phase 1: Find intersection point
    int slow = nums[0];
    int fast = nums[0];
    
    do {
        slow = nums[slow];         // Move 1 step
        fast = nums[nums[fast]];   // Move 2 steps
    } while (slow != fast);
    
    // Phase 2: Find cycle entrance
    slow = nums[0];
    while (slow != fast) {
        slow = nums[slow];
        fast = nums[fast];
    }
    
    return slow;
}
// Time: O(n), Space: O(1)
// Example: [1,3,4,2,2] -> 2

Task: Given an unsorted array, find the smallest missing positive integer. Must run in O(n) time and O(1) space.

Show Solution
int firstMissingPositive(int[] nums) {
    int n = nums.length;
    
    // Place each number in its correct position
    // nums[i] should be i+1
    for (int i = 0; i < n; i++) {
        while (nums[i] > 0 && nums[i] <= n 
               && nums[nums[i] - 1] != nums[i]) {
            // Swap nums[i] with nums[nums[i]-1]
            int correctIdx = nums[i] - 1;
            int temp = nums[i];
            nums[i] = nums[correctIdx];
            nums[correctIdx] = temp;
        }
    }
    
    // Find first position where nums[i] != i+1
    for (int i = 0; i < n; i++) {
        if (nums[i] != i + 1) {
            return i + 1;
        }
    }
    
    return n + 1;
}
// Time: O(n), Space: O(1)
// Example: [3,4,-1,1] -> 2
04

Searching Algorithms

Searching is one of the most fundamental operations on arrays. The choice between linear and binary search depends on whether the array is sorted and the performance requirements.

Key Concept

Searching in Arrays

Searching is the process of finding whether a specific element exists in an array and determining its position (index). Think of it as playing "Where's Waldo?"—you need to find one specific item among many. The efficiency of your search algorithm can dramatically impact performance—a poor choice can make your program 1000x slower with large datasets!

Real-World Analogy: Finding a Word in a Dictionary

Linear Search = Flipping through every page from the beginning until you find the word. Works, but slow!

Binary Search = Open the dictionary in the middle. Is "cat" before or after this page? Keep halving until found. Much faster!

Key Decision Factor: Is the array sorted?

  • YES → Use Binary Search (O(log n)) — A 1 million element array needs only ~20 comparisons!
  • NO → Use Linear Search (O(n)) or sort first, then binary search if searching multiple times.
Linear Search (Sequential)

Like checking every locker in a hallway one by one until you find your stuff.

  • Checks elements one by one from start
  • Works on ANY array (sorted or not)
  • Time: O(n) — checks n elements worst case
  • Best for: small arrays, one-time searches
Binary Search (Divide & Conquer)

Like the number guessing game—"higher or lower?" cuts possibilities in half!

  • Halves search space each comparison
  • REQUIRES sorted array (won't work otherwise!)
  • Time: O(log n) — amazingly fast!
  • Best for: large sorted arrays, frequent searches

Linear Search

Linear search (also called sequential search) checks each element one by one from the beginning until the target is found or the array ends. It's the simplest search algorithm and works on both sorted and unsorted arrays, but has O(n) time complexity.

// Linear Search - O(n) time, O(1) space
int linearSearch(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == target) {
            return i;  // Found at index i
        }
    }
    return -1;  // Not found
}

// Example usage
int[] nums = {64, 34, 25, 12, 22, 11, 90};
int index = linearSearch(nums, 22);  // Returns 4

The loop starts at index 0 and checks each element one by one. If arr[i] matches the target, we immediately return the index i where we found it—no need to check the rest!

If the loop completes without finding the target, we return -1 as a signal that the element doesn't exist in the array. This is a common convention since -1 is never a valid array index.

Binary Search

Binary search is dramatically faster at O(log n) but requires a sorted array. It repeatedly divides the search space in half by comparing the middle element with the target.

// Binary Search - O(log n) time, O(1) space
int binarySearch(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;  // Prevents overflow
        
        if (arr[mid] == target) {
            return mid;           // Found!
        } else if (arr[mid] < target) {
            left = mid + 1;       // Search right half
        } else {
            right = mid - 1;      // Search left half
        }
    }
    return -1;  // Not found
}

// Example: Find 23 in [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
// Step 1: mid=16 < 23, search right
// Step 2: mid=56 > 23, search left  
// Step 3: mid=23 == 23, found at index 5!

We use two pointers, left and right, to track the current search range. Initially, the entire array is our search space. The loop continues as long as there's at least one element to check (left <= right).

Each iteration, we calculate the middle index and compare arr[mid] with our target. If they match, we're done! If the target is larger, it must be in the right half, so we move left past mid. If smaller, it's in the left half, so we move right before mid.

We use left + (right - left) / 2 instead of (left + right) / 2 to prevent integer overflow when both values are large. If the loop exits without finding the target, we return -1.

Interview Tip: Always use mid = left + (right - left) / 2 instead of (left + right) / 2 to prevent integer overflow when left and right are large.
Algorithm Time Requirement Best Use Case
Linear Search O(n) None Small arrays, unsorted data
Binary Search O(log n) Sorted array Large sorted arrays

Practice: Searching

Task: Given a sorted array of integers and a target value, find the starting and ending position of target. Return [-1,-1] if not found.

Show Solution
int[] searchRange(int[] nums, int target) {
    int[] result = {-1, -1};
    
    // Find first occurrence
    result[0] = findBound(nums, target, true);
    if (result[0] == -1) return result;
    
    // Find last occurrence
    result[1] = findBound(nums, target, false);
    return result;
}

int findBound(int[] nums, int target, boolean isFirst) {
    int left = 0, right = nums.length - 1;
    int bound = -1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            bound = mid;
            if (isFirst) {
                right = mid - 1;  // Keep searching left
            } else {
                left = mid + 1;   // Keep searching right
            }
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return bound;
}
// Time: O(log n), Space: O(1)
// Example: [5,7,7,8,8,10], target=8 -> [3,4]

Task: An originally sorted array is rotated at some pivot. Search for a target value and return its index, or -1 if not found.

Show Solution
int search(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) return mid;
        
        // Check which half is sorted
        if (nums[left] <= nums[mid]) {
            // Left half is sorted
            if (target >= nums[left] && target < nums[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        } else {
            // Right half is sorted
            if (target > nums[mid] && target <= nums[right]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }
    return -1;
}
// Time: O(log n), Space: O(1)
// Example: [4,5,6,7,0,1,2], target=0 -> 4

Task: Find the minimum element in a rotated sorted array. Assume no duplicates.

Show Solution
int findMin(int[] nums) {
    int left = 0, right = nums.length - 1;
    
    // Array is not rotated
    if (nums[left] < nums[right]) return nums[left];
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        
        // If mid > right, min is in right half
        if (nums[mid] > nums[right]) {
            left = mid + 1;
        } else {
            // Min is at mid or in left half
            right = mid;
        }
    }
    return nums[left];
}
// Time: O(log n), Space: O(1)
// Example: [3,4,5,1,2] -> 1
// Example: [4,5,6,7,0,1,2] -> 0

Task: A peak element is greater than its neighbors. Find any peak element's index. Assume nums[-1] = nums[n] = -∞.

Show Solution
int findPeakElement(int[] nums) {
    int left = 0, right = nums.length - 1;
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] > nums[mid + 1]) {
            // Peak is at mid or to the left
            right = mid;
        } else {
            // Peak is to the right
            left = mid + 1;
        }
    }
    return left;  // left == right == peak
}
// Time: O(log n), Space: O(1)
// Example: [1,2,3,1] -> 2 (index of 3)
// Example: [1,2,1,3,5,6,4] -> 5 (index of 6)

Task: Given a sorted array and a target, return the index if found. If not, return the index where it would be inserted.

Show Solution
int searchInsert(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return left;  // Insert position
}
// Time: O(log n), Space: O(1)
// Example: [1,3,5,6], target=5 -> 2 (found)
// Example: [1,3,5,6], target=2 -> 1 (insert position)

Task: Given two sorted arrays nums1 and nums2, return the median of the two sorted arrays. Time complexity must be O(log(m+n)).

Show Solution
double findMedianSortedArrays(int[] nums1, int[] nums2) {
    // Ensure nums1 is smaller for binary search efficiency
    if (nums1.length > nums2.length) {
        return findMedianSortedArrays(nums2, nums1);
    }
    
    int m = nums1.length, n = nums2.length;
    int left = 0, right = m;
    
    while (left <= right) {
        int partitionX = (left + right) / 2;
        int partitionY = (m + n + 1) / 2 - partitionX;
        
        int maxLeftX = (partitionX == 0) ? Integer.MIN_VALUE : nums1[partitionX - 1];
        int minRightX = (partitionX == m) ? Integer.MAX_VALUE : nums1[partitionX];
        int maxLeftY = (partitionY == 0) ? Integer.MIN_VALUE : nums2[partitionY - 1];
        int minRightY = (partitionY == n) ? Integer.MAX_VALUE : nums2[partitionY];
        
        if (maxLeftX <= minRightY && maxLeftY <= minRightX) {
            // Found correct partition
            if ((m + n) % 2 == 0) {
                return (Math.max(maxLeftX, maxLeftY) + 
                        Math.min(minRightX, minRightY)) / 2.0;
            } else {
                return Math.max(maxLeftX, maxLeftY);
            }
        } else if (maxLeftX > minRightY) {
            right = partitionX - 1;
        } else {
            left = partitionX + 1;
        }
    }
    return 0.0;
}
// Time: O(log(min(m,n))), Space: O(1)
// Example: [1,3], [2] -> 2.0
// Example: [1,2], [3,4] -> 2.5
05

Sorting Arrays

Sorting is a fundamental operation that arranges elements in a specific order (ascending or descending). Sorting is crucial because many algorithms (like binary search) require sorted data, and a well-chosen sort can simplify complex problems.

Key Concept

What is Sorting?

Sorting is the process of arranging elements in a defined order—typically ascending (1, 2, 3...) or descending (9, 8, 7...). Imagine you have a messy deck of cards and you want to arrange them from Ace to King—that's sorting! It's one of the most studied problems in computer science because sorted data is SO much easier to work with.

Real-World Analogy: Organizing Books

Imagine a messy bookshelf. Unsorted = books thrown randomly everywhere. Sorted = books arranged A-Z by title. Now finding "Harry Potter" takes seconds instead of searching the entire shelf!

Different sorting methods are like different organizing strategies—some are simple but slow, others are clever and fast.

Why Sort? (It's Incredibly Useful!)

  • Enables binary search (O(n) → O(log n) — huge speedup!)
  • Makes finding duplicates trivial (they'll be adjacent)
  • Finding min/max becomes O(1) (just check first/last element)
  • Data looks cleaner for users (leaderboards, search results)
Java's Built-in Sorting (Just Use These!):

Don't reinvent the wheel—Java's built-in sorts are highly optimized by experts!

  • Primitives (int[], double[]): Uses Dual-Pivot Quicksort — Average O(n log n), extremely fast in practice
  • Objects (Integer[], String[]): Uses TimSort — O(n log n), stable (keeps equal elements in original order)
  • What's "Stable"? If you sort students by grade and two have the same grade, their original order is preserved. Useful for multi-level sorting!

Using Arrays.sort()

Java's Arrays.sort() is highly optimized. It uses Dual-Pivot Quicksort for primitives (average O(n log n)) and TimSort for objects (a hybrid merge-insertion sort that's stable). In most cases, you should use these built-in methods rather than implementing your own sort.

import java.util.Arrays;
import java.util.Collections;

int[] arr = {5, 2, 8, 1, 9};

// Sort in ascending order
Arrays.sort(arr);  // [1, 2, 5, 8, 9]

// Sort a range (from index 1 to 4, exclusive)
int[] partial = {5, 2, 8, 1, 9};
Arrays.sort(partial, 1, 4);  // [5, 1, 2, 8, 9] - only middle sorted

// Sort in descending order (need Integer[], not int[])
Integer[] arr2 = {5, 2, 8, 1, 9};
Arrays.sort(arr2, Collections.reverseOrder());  // [9, 8, 5, 2, 1]

// Custom sorting with comparator
String[] names = {"Charlie", "Alice", "Bob"};
Arrays.sort(names);  // [Alice, Bob, Charlie] - alphabetical
Arrays.sort(names, (a, b) -> b.compareTo(a));  // [Charlie, Bob, Alice]

// Sort 2D array by first column
int[][] intervals = {{3, 5}, {1, 4}, {2, 6}};
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);  // {{1,4}, {2,6}, {3,5}}
Important: Arrays.sort() modifies the original array. If you need to preserve the original, create a copy first using Arrays.copyOf().

Basic Sorting Algorithms

While Arrays.sort() is preferred in production code, understanding basic sorting algorithms is essential for interviews. These O(n²) algorithms are simple to implement and help build intuition for more complex sorts.

Bubble Sort

Repeatedly swap adjacent elements if they're in wrong order. Largest elements "bubble up" to the end.

Time: O(n²) | Space: O(1) | Stable: Yes

Selection Sort

Find minimum element and swap it to the front. Repeat for remaining unsorted portion.

Time: O(n²) | Space: O(1) | Stable: No

Insertion Sort

Build sorted array one element at a time by inserting each element into its correct position.

Time: O(n²) | Space: O(1) | Stable: Yes

1. Bubble Sort

Sorting Algorithm

Bubble Sort

Bubble Sort works by repeatedly comparing adjacent elements and swapping them if they're in the wrong order. After each complete pass through the array, the largest unsorted element "bubbles up" to its correct position at the end—just like air bubbles rising to the surface of water!

Real-World Analogy: Sorting Students by Height

Imagine students standing in a line. You walk down the line comparing each pair of adjacent students. If the left one is taller than the right one, they swap places. After one complete walk, the tallest student will be at the end!

Repeat this process, and each time the next tallest "bubbles" to their correct spot.

How It Works Step-by-Step:

  1. Compare the first two elements. If the first is greater, swap them.
  2. Move to the next pair (2nd and 3rd elements) and repeat.
  3. Continue until end of array—now the largest element is at the end.
  4. Repeat the process for remaining unsorted portion (excluding last element).
  5. Optimization: If no swaps occur in a pass, array is already sorted—exit early!
Pros
  • Very simple to understand and implement
  • Stable (equal elements keep original order)
  • Can detect if array is already sorted (O(n))
  • Good for educational purposes
Cons
  • O(n²) time—very slow for large arrays
  • Many swaps (inefficient data movement)
  • Rarely used in production code
  • Outperformed by almost all other algorithms

Time: O(n²) worst/average, O(n) best (already sorted) | Space: O(1) | Stable: Yes

void bubbleSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n - 1; i++) {
        boolean swapped = false;  // Optimization: detect if already sorted
        for (int j = 0; j < n - 1 - i; j++) {  // -i because last i elements are sorted
            if (arr[j] > arr[j + 1]) {
                // Swap adjacent elements
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }
        if (!swapped) break;  // Array is sorted, exit early
    }
}

// Example: [5, 2, 8, 1] → [2, 5, 1, 8] → [2, 1, 5, 8] → [1, 2, 5, 8]

The outer loop (i) controls the number of passes through the array. After each pass, the largest unsorted element is guaranteed to be in its final position, so we need at most n-1 passes.

The inner loop (j) compares adjacent elements. Notice n - 1 - i in the condition—this is an optimization that skips already-sorted elements at the end. After pass 1, the last element is sorted; after pass 2, the last 2 are sorted, and so on.

The swapped boolean is a clever optimization. If we complete an entire pass without any swaps, the array is already sorted and we can exit early. This gives bubble sort its best-case O(n) time complexity for already-sorted arrays.

2. Selection Sort

Sorting Algorithm

Selection Sort

Selection Sort works by dividing the array into two parts: sorted (left) and unsorted (right). It repeatedly selects the minimum element from the unsorted portion and swaps it to the end of the sorted portion. Think of it as "selecting the best candidate" in each round!

Real-World Analogy: Picking Teams in Gym Class

Remember picking teams for a game? The captain scans everyone and picks the best player first, then the second best, and so on. Selection sort does exactly this—it scans the unsorted part, finds the smallest (or best), and puts it in the next sorted position!

Unlike bubble sort (which swaps many times), selection sort makes exactly ONE swap per pass.

How It Works Step-by-Step:

  1. Start at position 0. Assume this is where the minimum should go.
  2. Scan the entire unsorted portion to find the actual minimum element.
  3. Swap the minimum with the element at position 0.
  4. Move to position 1 and repeat—find minimum from remaining unsorted portion.
  5. Continue until the entire array is sorted.
Pros
  • Simple and intuitive logic
  • Minimal swaps—only n-1 swaps maximum
  • Good when writing/swapping is expensive
  • Performs well on small arrays
Cons
  • O(n²) always—no best case optimization
  • Not stable (equal elements may be reordered)
  • Always does n² comparisons (even if sorted)
  • Slower than insertion sort in practice

Time: O(n²) always | Space: O(1) | Stable: No

void selectionSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n - 1; i++) {
        int minIdx = i;  // Assume current position has minimum
        
        // Find actual minimum in unsorted portion
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIdx]) {
                minIdx = j;
            }
        }
        
        // Swap minimum to its correct position
        if (minIdx != i) {
            int temp = arr[i];
            arr[i] = arr[minIdx];
            arr[minIdx] = temp;
        }
    }
}

// Example: [5, 2, 8, 1] → [1, 2, 8, 5] → [1, 2, 8, 5] → [1, 2, 5, 8]

The outer loop (i) represents the current position we're trying to fill with the correct (minimum) element. It runs from 0 to n-2 because once we've placed n-1 elements, the last one is automatically in the right place.

We start by assuming the element at position i is the minimum (minIdx = i). The inner loop then scans through all remaining elements (j = i+1 to end) looking for anything smaller. If found, we update minIdx to track this new minimum's position.

After the inner loop completes, minIdx holds the position of the actual minimum in the unsorted portion. We then swap it with position i (only if needed, hence the if (minIdx != i) check). This ensures exactly one swap per pass, making selection sort efficient when swap operations are costly.

3. Insertion Sort

Sorting Algorithm

Insertion Sort

Insertion Sort builds the sorted array one element at a time. It picks each element and inserts it into its correct position among the previously sorted elements—similar to how you sort playing cards in your hand. The left portion is always sorted, and each new element slides into place.

Real-World Analogy: Sorting Cards in Your Hand

When playing cards, you pick up cards one at a time. Each new card you pick up, you insert into the correct position among the cards already in your hand. You don't re-sort all the cards—you just slide the new one into place!

If you pick up a 7 and have 3, 5, 9 in hand, you slide the 7 between 5 and 9.

How It Works Step-by-Step:

  1. Start from index 1 (element at index 0 is already "sorted" by itself).
  2. Pick the current element (call it "key") and remember it.
  3. Compare key with elements to its left. Shift larger elements one position right.
  4. Insert key into the gap created by shifting.
  5. Move to next element and repeat until array is fully sorted.
Pros
  • O(n) for nearly sorted data! (best algorithm)
  • Stable (preserves order of equal elements)
  • Simple to implement
  • Very efficient for small arrays
  • Works well as data arrives (online algorithm)
Cons
  • O(n²) for reverse-sorted arrays
  • Many shifts (data movement) in worst case
  • Not efficient for large random arrays
  • Slower than merge/quick sort for big data

Time: O(n²) worst, O(n) best (nearly sorted) | Space: O(1) | Stable: Yes

void insertionSort(int[] arr) {
    int n = arr.length;
    for (int i = 1; i < n; i++) {
        int key = arr[i];  // Element to insert
        int j = i - 1;
        
        // Shift elements greater than key to the right
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;  // Insert key at correct position
    }
}

// Example: [5, 2, 8, 1] → [2, 5, 8, 1] → [2, 5, 8, 1] → [1, 2, 5, 8]

We start from index 1 (not 0) because a single element is already "sorted" by itself. The outer loop picks each element as the key that needs to be inserted into the sorted portion to its left.

The while loop is where the magic happens. We compare the key with elements to its left, moving backwards (j--). For each element larger than key, we shift it one position right (arr[j+1] = arr[j]). This creates a "gap" that moves leftward until we find the right spot for key.

The loop stops when either we reach the beginning (j >= 0 fails) or we find an element smaller than or equal to key (arr[j] > key fails). We then insert key into the gap at position j + 1. This is why insertion sort is O(n) for nearly-sorted data—elements barely need to shift!

When to Use: Insertion sort is excellent for nearly-sorted arrays (O(n) in best case) and small arrays. It's used as a subroutine in TimSort for small partitions.
06

Two Pointer Technique

Imagine you're at a dance where two partners start at opposite ends of the dance floor and walk toward each other—or two runners start together but one runs faster than the other. That's the essence of the two pointer technique! It uses two index variables to traverse an array strategically, often from opposite ends or at different speeds. This elegant approach can transform slow O(n²) brute force solutions into lightning-fast O(n) optimal solutions—a skill that impresses interviewers!

Key Concept

Two Pointer Technique

The two pointer technique involves using two index variables that move through the array based on certain conditions. Instead of checking every possible pair with nested loops (O(n²)), we use two smart "cursors" that work together to find the answer in a single pass (O(n)).

Why It's Powerful (The "Aha!" Moment)

Brute Force: To find two numbers that sum to 10, check every pair: (1,2), (1,3), (1,4)... (2,3), (2,4)... This is O(n²)—terribly slow for large arrays!

Two Pointers: Put one finger at the start, one at the end. Sum too big? Move the right finger left (smaller number). Too small? Move the left finger right (bigger number). You'll find the answer in ONE pass—O(n)!

🎯 When to Use Two Pointers:

  • Sorted array problems → sorted order lets you make smart decisions about which pointer to move
  • Finding pairs/triplets with specific sums or properties
  • In-place modifications like removing elements or partitioning
  • Palindrome checking → compare from both ends moving inward
Opposite Direction

Pointers start at both ends and walk toward each other, like two people meeting in the middle.

Classic Problems: Two Sum (sorted), Valid Palindrome, Container With Most Water, Reverse Array

Same Direction (Fast/Slow)

Both start at beginning—slow pointer marks "where to put good elements", fast pointer scans ahead.

Classic Problems: Remove Duplicates, Remove Element, Move Zeros, Linked List Cycle

Pattern 1: Opposite Ends

Start pointers at both ends and move them toward each other. Great for problems involving pairs in sorted arrays.

// Two Sum II - Input array is sorted
int[] twoSum(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    
    while (left < right) {
        int sum = arr[left] + arr[right];
        
        if (sum == target) {
            return new int[]{left, right};  // Found!
        } else if (sum < target) {
            left++;   // Need bigger sum
        } else {
            right--;  // Need smaller sum
        }
    }
    return new int[]{-1, -1};  // Not found
}

// Example: arr = [2, 7, 11, 15], target = 9
// Step 1: 2 + 15 = 17 > 9, move right
// Step 2: 2 + 11 = 13 > 9, move right
// Step 3: 2 + 7 = 9, found! Return [0, 1]

We place left at the start (smallest element) and right at the end (largest element). Since the array is sorted, arr[left] + arr[right] gives us the sum of smallest and largest available values.

If the sum is too small, we need a bigger number—so we move left right to get a larger value. If too big, we move right left to get a smaller value. The loop continues until pointers meet or we find the target.

Pattern 2: Fast and Slow

Both pointers start at the beginning. The slow pointer marks the position for valid elements while the fast pointer scans through the array.

// Remove Duplicates from Sorted Array
int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;
    
    int slow = 0;  // Position for next unique element
    
    for (int fast = 1; fast < nums.length; fast++) {
        if (nums[fast] != nums[slow]) {
            slow++;
            nums[slow] = nums[fast];
        }
    }
    return slow + 1;  // Length of unique elements
}

// Example: [1, 1, 2, 2, 3] -> [1, 2, 3, _, _]
// Returns 3 (first 3 elements are unique)

The slow pointer marks the position where the next unique element should go. It stays at the last unique element we've placed. The fast pointer scans ahead looking for new unique values.

When fast finds a value different from nums[slow], we've found a new unique element! We increment slow to make room, then copy the new unique value there. Elements after slow don't matter—we return slow + 1 as the count of unique elements.

Practice: Two Pointers

Task: Given an array of heights, find two lines that form a container holding the most water.

Show Solution
int maxArea(int[] height) {
    int left = 0, right = height.length - 1;
    int maxWater = 0;
    
    while (left < right) {
        int width = right - left;
        int h = Math.min(height[left], height[right]);
        maxWater = Math.max(maxWater, width * h);
        
        // Move the shorter line inward
        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }
    return maxWater;
}
// Time: O(n), Space: O(1)

Task: Given a string, determine if it is a palindrome considering only alphanumeric characters and ignoring cases.

Show Solution
boolean isPalindrome(String s) {
    int left = 0, right = s.length() - 1;
    
    while (left < right) {
        // Skip non-alphanumeric from left
        while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
            left++;
        }
        // Skip non-alphanumeric from right
        while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
            right--;
        }
        
        // Compare characters (case-insensitive)
        if (Character.toLowerCase(s.charAt(left)) != 
            Character.toLowerCase(s.charAt(right))) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}
// Time: O(n), Space: O(1)
// Example: "A man, a plan, a canal: Panama" -> true

Task: Given an array, find all unique triplets that sum to zero. Avoid duplicate triplets.

Show Solution
List> threeSum(int[] nums) {
    List> result = new ArrayList<>();
    Arrays.sort(nums);  // Sort first!
    
    for (int i = 0; i < nums.length - 2; i++) {
        // Skip duplicates for first element
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        
        int left = i + 1, right = nums.length - 1;
        int target = -nums[i];
        
        while (left < right) {
            int sum = nums[left] + nums[right];
            
            if (sum == target) {
                result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                
                // Skip duplicates
                while (left < right && nums[left] == nums[left + 1]) left++;
                while (left < right && nums[right] == nums[right - 1]) right--;
                
                left++;
                right--;
            } else if (sum < target) {
                left++;
            } else {
                right--;
            }
        }
    }
    return result;
}
// Time: O(n²), Space: O(1) excluding output
// Example: [-1,0,1,2,-1,-4] -> [[-1,-1,2],[-1,0,1]]

Task: Given an array with red (0), white (1), and blue (2), sort them in-place so that same colors are adjacent. Use only one pass.

Show Solution
void sortColors(int[] nums) {
    int low = 0;              // Boundary for 0s
    int mid = 0;              // Current element
    int high = nums.length - 1;  // Boundary for 2s
    
    while (mid <= high) {
        if (nums[mid] == 0) {
            // Swap with low, move both pointers
            swap(nums, low, mid);
            low++;
            mid++;
        } else if (nums[mid] == 1) {
            // 1 is in correct position, just move mid
            mid++;
        } else {  // nums[mid] == 2
            // Swap with high, only move high
            swap(nums, mid, high);
            high--;
            // Don't increment mid, need to check swapped element
        }
    }
}

void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}
// Time: O(n), Space: O(1)
// Example: [2,0,2,1,1,0] -> [0,0,1,1,2,2]

Task: Given a sorted array (may contain negatives), return an array of squares in sorted order.

Show Solution
int[] sortedSquares(int[] nums) {
    int n = nums.length;
    int[] result = new int[n];
    int left = 0, right = n - 1;
    int pos = n - 1;  // Fill from end (largest first)
    
    while (left <= right) {
        int leftSq = nums[left] * nums[left];
        int rightSq = nums[right] * nums[right];
        
        if (leftSq > rightSq) {
            result[pos] = leftSq;
            left++;
        } else {
            result[pos] = rightSq;
            right--;
        }
        pos--;
    }
    return result;
}
// Time: O(n), Space: O(n)
// Example: [-4,-1,0,3,10] -> [0,1,9,16,100]

Task: Given n non-negative integers representing elevation map where width of each bar is 1, compute how much water it can trap after raining.

Show Solution
int trap(int[] height) {
    if (height.length == 0) return 0;
    
    int left = 0, right = height.length - 1;
    int leftMax = 0, rightMax = 0;
    int water = 0;
    
    while (left < right) {
        if (height[left] < height[right]) {
            // Water at left depends on leftMax
            if (height[left] >= leftMax) {
                leftMax = height[left];
            } else {
                water += leftMax - height[left];
            }
            left++;
        } else {
            // Water at right depends on rightMax
            if (height[right] >= rightMax) {
                rightMax = height[right];
            } else {
                water += rightMax - height[right];
            }
            right--;
        }
    }
    return water;
}
// Time: O(n), Space: O(1)
// Example: [0,1,0,2,1,0,1,3,2,1,2,1] -> 6
07

Sliding Window Pattern

Picture looking out a train window as scenery passes by—you only see a portion of the landscape at any moment, but as the train moves, new views enter while old ones exit. That's exactly how the sliding window pattern works! It maintains a "window" over a contiguous subarray and efficiently slides it across the array. This brilliant technique transforms painfully slow O(n²) or O(n³) brute force solutions into elegant O(n) algorithms—one of the most satisfying optimizations in programming!

Key Concept

Sliding Window Pattern

The sliding window technique maintains a subset (window) of contiguous elements and "slides" this window across the array. The magic? Instead of recalculating everything from scratch for each position, we efficiently update by adding the new element entering the window and removing the element leaving it.

The "Aha!" Moment: Why It's So Fast

Brute Force: "Find max sum of 3 consecutive elements" → Calculate sum of [0,1,2], then [1,2,3], then [2,3,4]... Each sum recalculates 3 elements. With 1 million elements, that's ~3 million operations!

Sliding Window: Calculate first window sum = 10+20+30. Next window? Just subtract 10 (leaving), add 40 (entering): 60-10+40 = 90. ONE operation per slide instead of K!

Key Insight: When the window slides by one position, most elements remain the same. In a window of 1000 elements, 999 stay the same—only 1 leaves and 1 enters. Why recalculate 999 elements when you can just handle the 2 that changed? That's O(1) per slide instead of O(k)!

Fixed Size Window

Window size K is given upfront. Just slide: add the new element on the right, remove the old element on the left.

Examples: Max sum of K elements, Average of all K-sized subarrays, Max in each sliding window

Variable Size Window

Window grows until a condition is met, then shrinks to find the optimal size. Like stretching a rubber band!

Examples: Smallest subarray with sum ≥ K, Longest substring without repeating chars

Recognizing Sliding Window Problems (Interview Pattern):
  • Keywords to look for: "contiguous", "subarray", "substring", "window", "consecutive", "k elements"
  • Question pattern: "Find max/min/count of something in a contiguous subarray/substring"
  • Mental test: Would brute force use nested loops to check all subarrays? → Probably sliding window!
  • Not sliding window if: Elements don't need to be contiguous, or you need to pick scattered elements

Fixed Size Window

When the window size is given, maintain the sum/property as you slide by adding the new element and removing the old one.

// Maximum Sum Subarray of Size K
int maxSumSubarray(int[] arr, int k) {
    if (arr.length < k) return -1;
    
    // Calculate sum of first window
    int windowSum = 0;
    for (int i = 0; i < k; i++) {
        windowSum += arr[i];
    }
    int maxSum = windowSum;
    
    // Slide the window
    for (int i = k; i < arr.length; i++) {
        windowSum += arr[i] - arr[i - k];  // Add new, remove old
        maxSum = Math.max(maxSum, windowSum);
    }
    return maxSum;
}

// Example: arr = [2, 1, 5, 1, 3, 2], k = 3
// Window [2,1,5] = 8
// Window [1,5,1] = 7  
// Window [5,1,3] = 9 (maximum!)
// Window [1,3,2] = 6

First, we calculate the sum of the initial window (first k elements) using a simple loop. This is our starting point and the first candidate for the maximum sum.

Then we slide the window one position at a time. The key insight: arr[i] is the new element entering the window, and arr[i - k] is the element leaving. Instead of recalculating the entire sum, we just add the new element and subtract the old one—a single operation per slide!

Variable Size Window

Unlike fixed-size windows where we know exactly how many elements to consider, variable size windows dynamically grow and shrink based on a condition. Think of it like a stretchy rubber band that expands until it catches what you're looking for, then contracts to find the optimal size.

Expand Phase

Keep adding elements (move right pointer) until your condition is satisfied.

"I need more elements to meet the requirement"

Shrink Phase

Remove elements from the left (move left pointer) to find the minimum valid window.

"Can I make the window smaller and still satisfy the condition?"

The Key Pattern:
  1. Expand: Move right pointer → add element to window
  2. Check: Is the condition satisfied?
  3. Shrink: While condition is satisfied, move left pointer → try to minimize window
  4. Track: Update your answer (min/max length) during shrink phase
Visual Example: Find Smallest Subarray with Sum ≥ 7

Array: [2, 3, 1, 2, 4, 3], Target: 7

Step Window Sum Action Min Length
1 [2] 2 Expand → sum < 7
2 [2, 3] 5 Expand → sum < 7
3 [2, 3, 1] 6 Expand → sum < 7
4 [2, 3, 1, 2] 8 ✓ sum ≥ 7! Record length = 4 4
5 [3, 1, 2] 6 Shrink → sum < 7, stop shrinking 4
6 [3, 1, 2, 4] 10 ✓ sum ≥ 7! Record length = 4 4
7 [1, 2, 4] 7 ✓ sum ≥ 7! Record length = 3 3
8 [2, 4] 6 Shrink → sum < 7, stop shrinking 3
9 [2, 4, 3] 9 ✓ sum ≥ 7! Record length = 3 3
10 [4, 3] 7 ✓ sum ≥ 7! Record length = 2 🎉 2

Answer: Minimum length = 2 (subarray [4, 3])

// Minimum Size Subarray Sum >= target
int minSubArrayLen(int target, int[] nums) {
    int left = 0, sum = 0;
    int minLen = Integer.MAX_VALUE;
    
    for (int right = 0; right < nums.length; right++) {
        sum += nums[right];  // EXPAND: Add new element to window
        
        // SHRINK: While condition is satisfied, try to minimize
        while (sum >= target) {
            minLen = Math.min(minLen, right - left + 1);  // Record current valid window size
            sum -= nums[left];  // Remove leftmost element
            left++;             // Shrink window from left
        }
    }
    return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
// Time: O(n) - each element visited at most twice (once by right, once by left)
// Space: O(1)

The right pointer expands the window by adding elements to sum. We keep expanding until our condition (sum >= target) is satisfied.

Once the condition is met, we enter the while loop to shrink. We record the current window size, then remove the leftmost element and move left forward. We keep shrinking as long as the condition remains satisfied—this finds the smallest valid window ending at the current right position.

The algorithm is O(n) because each element is added once (when right visits it) and removed at most once (when left passes it). Even though there's a while loop inside a for loop, the total operations are at most 2n.

Common Beginner Questions:
  • Why while loop, not if? — We may need to shrink multiple times. After removing one element, the condition might still be satisfied!
  • Why is it O(n), not O(n²)? — Each element is added once (when right moves) and removed at most once (when left moves). Total operations = 2n = O(n).
  • When to expand vs shrink? — Expand when condition NOT met, shrink when condition IS met (to find smaller valid window).

Practice: Sliding Window

Task: Find the contiguous subarray of length k with the maximum average value. Return the maximum average.

Show Solution
double findMaxAverage(int[] nums, int k) {
    // Calculate sum of first window
    double windowSum = 0;
    for (int i = 0; i < k; i++) {
        windowSum += nums[i];
    }
    double maxSum = windowSum;
    
    // Slide the window
    for (int i = k; i < nums.length; i++) {
        windowSum += nums[i] - nums[i - k];
        maxSum = Math.max(maxSum, windowSum);
    }
    
    return maxSum / k;
}
// Time: O(n), Space: O(1)
// Example: nums = [1,12,-5,-6,50,3], k = 4 -> 12.75

Task: Given a string, find the length of the longest substring without repeating characters.

Show Solution
int lengthOfLongestSubstring(String s) {
    Map charIndex = new HashMap<>();
    int maxLen = 0;
    int left = 0;
    
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        
        // If char exists and is within current window
        if (charIndex.containsKey(c) && charIndex.get(c) >= left) {
            left = charIndex.get(c) + 1;  // Shrink window
        }
        
        charIndex.put(c, right);  // Update char position
        maxLen = Math.max(maxLen, right - left + 1);
    }
    
    return maxLen;
}
// Time: O(n), Space: O(min(m,n)) where m is charset size
// Example: "abcabcbb" -> 3 ("abc")

Task: Given a binary array and integer k, return the maximum number of consecutive 1's if you can flip at most k 0's.

Show Solution
int longestOnes(int[] nums, int k) {
    int left = 0, maxLen = 0;
    int zeroCount = 0;
    
    for (int right = 0; right < nums.length; right++) {
        if (nums[right] == 0) {
            zeroCount++;
        }
        
        // Shrink window if too many zeros
        while (zeroCount > k) {
            if (nums[left] == 0) {
                zeroCount--;
            }
            left++;
        }
        
        maxLen = Math.max(maxLen, right - left + 1);
    }
    
    return maxLen;
}
// Time: O(n), Space: O(1)
// Example: nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2 -> 6

Task: You have two baskets. Find the maximum number of fruits you can pick if each basket can only hold one type of fruit (longest subarray with at most 2 distinct elements).

Show Solution
int totalFruit(int[] fruits) {
    Map basket = new HashMap<>();
    int left = 0, maxFruits = 0;
    
    for (int right = 0; right < fruits.length; right++) {
        // Add fruit to basket
        basket.put(fruits[right], 
                   basket.getOrDefault(fruits[right], 0) + 1);
        
        // Shrink window if more than 2 types
        while (basket.size() > 2) {
            int leftFruit = fruits[left];
            basket.put(leftFruit, basket.get(leftFruit) - 1);
            if (basket.get(leftFruit) == 0) {
                basket.remove(leftFruit);
            }
            left++;
        }
        
        maxFruits = Math.max(maxFruits, right - left + 1);
    }
    
    return maxFruits;
}
// Time: O(n), Space: O(1) - at most 3 entries in map
// Example: [1,2,1] -> 3, [0,1,2,2] -> 3

Task: Given strings s and t, return the minimum window substring of s that contains all characters of t. If no such substring exists, return "".

Show Solution
String minWindow(String s, String t) {
    if (s.length() < t.length()) return "";
    
    // Count required characters
    Map required = new HashMap<>();
    for (char c : t.toCharArray()) {
        required.put(c, required.getOrDefault(c, 0) + 1);
    }
    
    int left = 0, minLen = Integer.MAX_VALUE, minStart = 0;
    int matched = 0;  // Number of unique chars matched
    Map window = new HashMap<>();
    
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        window.put(c, window.getOrDefault(c, 0) + 1);
        
        // Check if this char is now fully matched
        if (required.containsKey(c) && 
            window.get(c).equals(required.get(c))) {
            matched++;
        }
        
        // Shrink window while valid
        while (matched == required.size()) {
            if (right - left + 1 < minLen) {
                minLen = right - left + 1;
                minStart = left;
            }
            
            char leftChar = s.charAt(left);
            window.put(leftChar, window.get(leftChar) - 1);
            if (required.containsKey(leftChar) && 
                window.get(leftChar) < required.get(leftChar)) {
                matched--;
            }
            left++;
        }
    }
    
    return minLen == Integer.MAX_VALUE ? "" : 
           s.substring(minStart, minStart + minLen);
}
// Time: O(s + t), Space: O(s + t)
// Example: s = "ADOBECODEBANC", t = "ABC" -> "BANC"

Task: Given an array and sliding window size k, return the max value in each window as it slides from left to right.

Show Solution
int[] maxSlidingWindow(int[] nums, int k) {
    if (nums.length == 0) return new int[0];
    
    int[] result = new int[nums.length - k + 1];
    // Deque stores indices, front has max element index
    Deque deque = new ArrayDeque<>();
    
    for (int i = 0; i < nums.length; i++) {
        // Remove indices outside current window
        while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
            deque.pollFirst();
        }
        
        // Remove smaller elements (they can never be max)
        while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
            deque.pollLast();
        }
        
        deque.offerLast(i);
        
        // Window is complete, record max
        if (i >= k - 1) {
            result[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    
    return result;
}
// Time: O(n), Space: O(k)
// Example: nums = [1,3,-1,-3,5,3,6,7], k = 3
// Output: [3,3,5,5,6,7]
08

2D Arrays (Matrices)

Think of a spreadsheet like Excel—it has rows and columns, and you find any cell by its row number and column letter. A 2D array (also called a matrix) works exactly the same way! It's an array of arrays, commonly used to represent grids, tables, game boards (chess, sudoku), images (where each pixel is a cell), and graphs. Mastering 2D arrays is essential—they appear in 20-30% of coding interviews, especially in matrix manipulation, path finding, and dynamic programming problems.

Key Concept

2D Arrays (Matrices)

A 2D array is an array where each element is itself an array—think of it as a table with rows and columns. In Java, int[][] matrix is an array of integer arrays. The first bracket [i] picks the row, the second bracket [j] picks the column within that row.

Real-World Analogy: Apartment Building

Imagine an apartment building. building[2][5] means: go to floor 2, then find apartment 5 on that floor.

In Java: matrix[row][column] → first go to the row, then find the column. Always row first, column second!

Memory Secret: matrix[i][j] accesses row i, column j. Java stores this as an "array of arrays"—matrix[0] is the entire first row (an array itself), matrix[1] is the second row, etc. This also means rows can have different lengths (called "jagged arrays")!

Key Properties (Memorize These!)
  • matrix.length = number of rows
  • matrix[0].length = number of columns
  • Access element: matrix[row][col]
  • Total elements: rows × columns
  • Indices: rows 0 to (rows-1), cols 0 to (cols-1)
Where You'll See 2D Arrays
  • Game boards: Chess, Sudoku, Tic-Tac-Toe
  • Images: Each pixel is matrix[x][y]
  • Graphs: Adjacency matrix representation
  • DP tables: Storing subproblem solutions
  • Maps/Grids: Path finding, maze solving

Declaration and Traversal

1. Declaration and Initialization

There are two main ways to create a 2D array: specify dimensions with new, or initialize with values directly using curly braces.

// Method 1: Declare with dimensions (all values default to 0)
int[][] matrix = new int[3][4];  // 3 rows, 4 columns

// Method 2: Initialize with values (size is inferred)
int[][] grid = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};
Method 1: new int[3][4]
0 0 0 0
0 0 0 0
0 0 0 0

3 rows × 4 columns, all zeros (default)

Method 2: Inline values
1 2 3
4 5 6
7 8 9

3 rows × 3 columns, with values

2. Accessing Elements

Access any element using matrix[row][column]. Remember: indices are zero-based, so the first element is at [0][0].

int[][] grid = {
    {1, 2, 3},    // row 0
    {4, 5, 6},    // row 1
    {7, 8, 9}     // row 2
};

// Access element at row 1, column 2
int value = grid[1][2];  // Returns 6

// Modify an element
grid[0][0] = 10;  // First element is now 10
Visual Guide: For grid[1][2] → Go to row 1 (second row: 4,5,6) → Then column 2 (third element: 6)

3. Getting Dimensions

Use .length to get the number of rows and columns. Remember: rows = outer array length, columns = inner array length.

int[][] grid = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// Get dimensions
int rows = grid.length;        // 3 (number of inner arrays)
int cols = grid[0].length;     // 3 (length of first inner array)

System.out.println("Matrix is " + rows + " x " + cols);  // "Matrix is 3 x 3"
Caution: For jagged arrays (rows with different lengths), each row may have a different .length. Always use grid[i].length inside loops for safety.

4. Row-by-Row Traversal

The most common traversal pattern. The outer loop iterates through rows (i), and the inner loop iterates through columns (j). This reads left-to-right, top-to-bottom.

int[][] grid = {{1,2,3}, {4,5,6}, {7,8,9}};
int rows = grid.length;
int cols = grid[0].length;

// Row-by-row traversal (most common)
for (int i = 0; i < rows; i++) {           // For each row
    for (int j = 0; j < cols; j++) {       // For each column in row
        System.out.print(grid[i][j] + " ");
    }
    System.out.println();  // New line after each row
}

// Output:
// 1 2 3
// 4 5 6
// 7 8 9
Row-by-Row Traversal Order

Matrix View

1 2 3 4 5 6 7 8 9
Row 0 (i=0)
Row 1 (i=1)
Row 2 (i=2)
New Row

Traversal Sequence

1 2 3 4 5 6 7 8 9

5. Column-by-Column Traversal

Less common but useful for certain algorithms. Swap the loop order: outer loop for columns (j), inner loop for rows (i). This reads top-to-bottom, left-to-right.

int[][] grid = {{1,2,3}, {4,5,6}, {7,8,9}};
int rows = grid.length;
int cols = grid[0].length;

// Column-by-column traversal
for (int j = 0; j < cols; j++) {           // For each column
    for (int i = 0; i < rows; i++) {       // For each row in column
        System.out.print(grid[i][j] + " ");
    }
    System.out.println();  // New line after each column
}

// Output:
// 1 4 7  (column 0)
// 2 5 8  (column 1)
// 3 6 9  (column 2)
Column-by-Column Traversal Order

Matrix View

1 2 3 4 5 6 7 8 9
Col 0 (j=0)
Col 1 (j=1)
Col 2 (j=2)
New Column

Traversal Sequence (top-to-bottom, left-to-right)

1 4 7 2 5 8 3 6 9

6. Enhanced For-Each Loop

When you don't need indices, the for-each loop provides cleaner syntax. The outer loop gives you each row (as a 1D array), and the inner loop gives you each element.

int[][] grid = {{1,2,3}, {4,5,6}, {7,8,9}};

// For-each traversal (when indices not needed)
for (int[] row : grid) {           // Each row is an int[]
    for (int value : row) {        // Each value in the row
        System.out.print(value + " ");
    }
    System.out.println();
}

// Output: Same as row-by-row
// 1 2 3
// 4 5 6
// 7 8 9
When to use: Use for-each when you only need to read values. Use traditional for loops when you need to modify values or need the indices for calculations.

Common Matrix Operations

1. Transpose Matrix

Transposing a matrix means converting rows into columns and columns into rows. If the original matrix is m × n, the transposed matrix will be n × m. Element at position [i][j] moves to position [j][i].

Before (3×3)
1 2 3
4 5 6
7 8 9
After Transpose
1 4 7
2 5 8
3 6 9
// Transpose matrix (swap rows and columns)
int[][] transpose(int[][] matrix) {
    int rows = matrix.length;
    int cols = matrix[0].length;
    int[][] result = new int[cols][rows];  // Note: cols × rows
    
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            result[j][i] = matrix[i][j];  // Swap indices
        }
    }
    return result;
}
// Time: O(m × n), Space: O(m × n) for new matrix
Key Insight: For a square matrix, you can transpose in-place by only swapping elements above the main diagonal (j > i) to avoid swapping twice.

2. Rotate Matrix 90° Clockwise

Rotating a matrix 90° clockwise is a common interview question. The trick is to break it into two simpler steps: transpose first, then reverse each row. This avoids complex index calculations.

Original
1 2 3
4 5 6
7 8 9
After Transpose
1 4 7
2 5 8
3 6 9
After Reverse Rows
7 4 1
8 5 2
9 6 3

Step 1: Transpose the matrix - Swap elements across the main diagonal.

// Step 1: Transpose (swap across main diagonal)
// Only swap upper triangle to avoid double-swapping
void transposeInPlace(int[][] matrix) {
    int n = matrix.length;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {  // j starts at i+1
            int temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }
}
// Time: O(n²), Space: O(1) - in-place

Step 2: Reverse each row - Use two pointers to swap elements from ends toward center.

// Step 2: Reverse each row using two pointers
void reverseRows(int[][] matrix) {
    int n = matrix.length;
    for (int i = 0; i < n; i++) {
        int left = 0, right = n - 1;
        while (left < right) {
            int temp = matrix[i][left];
            matrix[i][left] = matrix[i][right];
            matrix[i][right] = temp;
            left++;
            right--;
        }
    }
}
// Time: O(n²), Space: O(1) - in-place

Complete Solution: Rotate 90° Clockwise

// Rotate matrix 90 degrees clockwise (in-place)
void rotate(int[][] matrix) {
    int n = matrix.length;
    
    // Step 1: Transpose
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            int temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }
    
    // Step 2: Reverse each row
    for (int i = 0; i < n; i++) {
        int left = 0, right = n - 1;
        while (left < right) {
            int temp = matrix[i][left];
            matrix[i][left] = matrix[i][right];
            matrix[i][right] = temp;
            left++;
            right--;
        }
    }
}
// Time: O(n²), Space: O(1)
Other Rotation Patterns:
  • 90° Counter-clockwise: Reverse each row first, then transpose
  • 180°: Reverse each row, then reverse each column (or rotate 90° twice)
  • Rotate by layer: Alternative approach that rotates elements in concentric squares

Practice: 2D Arrays / Matrix

Task: Print the primary diagonal (top-left to bottom-right) and secondary diagonal of a square matrix.

Show Solution
void printDiagonals(int[][] matrix) {
    int n = matrix.length;
    
    // Primary diagonal (top-left to bottom-right)
    System.out.print("Primary: ");
    for (int i = 0; i < n; i++) {
        System.out.print(matrix[i][i] + " ");
    }
    System.out.println();
    
    // Secondary diagonal (top-right to bottom-left)
    System.out.print("Secondary: ");
    for (int i = 0; i < n; i++) {
        System.out.print(matrix[i][n - 1 - i] + " ");
    }
}
// Time: O(n), Space: O(1)
// Example: [[1,2,3],[4,5,6],[7,8,9]]
// Primary: 1 5 9, Secondary: 3 5 7

Task: Given an m x n matrix, return all elements in spiral order (clockwise from outside to inside).

Show Solution
List spiralOrder(int[][] matrix) {
    List result = new ArrayList<>();
    if (matrix.length == 0) return result;
    
    int top = 0, bottom = matrix.length - 1;
    int left = 0, right = matrix[0].length - 1;
    
    while (top <= bottom && left <= right) {
        // Traverse right
        for (int j = left; j <= right; j++) {
            result.add(matrix[top][j]);
        }
        top++;
        
        // Traverse down
        for (int i = top; i <= bottom; i++) {
            result.add(matrix[i][right]);
        }
        right--;
        
        // Traverse left (if row still exists)
        if (top <= bottom) {
            for (int j = right; j >= left; j--) {
                result.add(matrix[bottom][j]);
            }
            bottom--;
        }
        
        // Traverse up (if column still exists)
        if (left <= right) {
            for (int i = bottom; i >= top; i--) {
                result.add(matrix[i][left]);
            }
            left++;
        }
    }
    return result;
}
// Time: O(m*n), Space: O(1) excluding output
// Example: [[1,2,3],[4,5,6],[7,8,9]] -> [1,2,3,6,9,8,7,4,5]

Task: If an element is 0, set its entire row and column to 0. Do it in-place with O(1) extra space.

Show Solution
void setZeroes(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    boolean firstRowZero = false, firstColZero = false;
    
    // Check if first row/col should be zero
    for (int j = 0; j < n; j++) {
        if (matrix[0][j] == 0) firstRowZero = true;
    }
    for (int i = 0; i < m; i++) {
        if (matrix[i][0] == 0) firstColZero = true;
    }
    
    // Use first row/col as markers
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            if (matrix[i][j] == 0) {
                matrix[i][0] = 0;  // Mark row
                matrix[0][j] = 0;  // Mark col
            }
        }
    }
    
    // Set zeros based on markers
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                matrix[i][j] = 0;
            }
        }
    }
    
    // Handle first row and column
    if (firstRowZero) {
        for (int j = 0; j < n; j++) matrix[0][j] = 0;
    }
    if (firstColZero) {
        for (int i = 0; i < m; i++) matrix[i][0] = 0;
    }
}
// Time: O(m*n), Space: O(1)

Task: Search for a target in a matrix where each row is sorted and the first element of each row is greater than the last element of the previous row.

Show Solution
boolean searchMatrix(int[][] matrix, int target) {
    int m = matrix.length, n = matrix[0].length;
    
    // Treat as 1D sorted array
    int left = 0, right = m * n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        // Convert 1D index to 2D
        int row = mid / n;
        int col = mid % n;
        int value = matrix[row][col];
        
        if (value == target) {
            return true;
        } else if (value < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return false;
}
// Time: O(log(m*n)), Space: O(1)
// Example: matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]]
// target = 3 -> true

Task: Search for target in a matrix where each row and each column is sorted in ascending order (but rows are not connected).

Show Solution
boolean searchMatrix(int[][] matrix, int target) {
    int m = matrix.length, n = matrix[0].length;
    
    // Start from top-right corner
    int row = 0, col = n - 1;
    
    while (row < m && col >= 0) {
        if (matrix[row][col] == target) {
            return true;
        } else if (matrix[row][col] > target) {
            col--;  // Move left (smaller values)
        } else {
            row++;  // Move down (larger values)
        }
    }
    return false;
}
// Time: O(m + n), Space: O(1)
// Key insight: From top-right, left is smaller, down is larger
// Example: target = 5 in:
// [[1,4,7,11],[2,5,8,12],[3,6,9,16],[10,13,14,17]]

Task: Given a binary matrix filled with 0's and 1's, find the largest rectangle containing only 1's and return its area.

Show Solution
int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0) return 0;
    
    int n = matrix[0].length;
    int[] heights = new int[n];
    int maxArea = 0;
    
    for (int i = 0; i < matrix.length; i++) {
        // Build histogram heights for this row
        for (int j = 0; j < n; j++) {
            if (matrix[i][j] == '1') {
                heights[j]++;
            } else {
                heights[j] = 0;
            }
        }
        // Find max rectangle in histogram
        maxArea = Math.max(maxArea, largestRectangleInHistogram(heights));
    }
    return maxArea;
}

int largestRectangleInHistogram(int[] heights) {
    Stack stack = new Stack<>();
    int maxArea = 0;
    
    for (int i = 0; i <= heights.length; i++) {
        int h = (i == heights.length) ? 0 : heights[i];
        
        while (!stack.isEmpty() && h < heights[stack.peek()]) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        stack.push(i);
    }
    return maxArea;
}
// Time: O(m*n), Space: O(n)
// Builds on "Largest Rectangle in Histogram" problem

Key Takeaways

O(1) Access

Arrays provide instant access by index using base address plus offset calculation

Binary Search

For sorted arrays, binary search provides O(log n) - dramatically faster than O(n) linear search

Two Pointers

Use two pointers to reduce O(n^2) brute force to O(n) for sorted array problems

Sliding Window

Maintain a window over subarrays to efficiently solve contiguous subarray problems

2D Arrays

Matrix problems use row-column indexing. Master transpose, rotation, and traversal patterns

Sorting

Arrays.sort() uses efficient O(n log n) algorithms. Sorting often simplifies complex problems

Knowledge Check

1 What is the time complexity of accessing an element by index in an array?
2 When should you use binary search instead of linear search?
3 What is the purpose of the two pointer technique?
4 What is the correct way to calculate the middle index in binary search to prevent integer overflow?
5 When is the sliding window technique most appropriate?
6 How do you access the element at row 2, column 3 in a 2D array called matrix?
Answer all questions to check your score