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.
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, aString[]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
ArrayListwhich does this automatically). - Zero-Indexed: The first element is at index
0, not1. 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 |
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.
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.
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 |
.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
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.
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.
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) timeSearching
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) timeTraversing 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
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]
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
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.
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.
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
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.
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}}
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
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:
- Compare the first two elements. If the first is greater, swap them.
- Move to the next pair (2nd and 3rd elements) and repeat.
- Continue until end of array—now the largest element is at the end.
- Repeat the process for remaining unsorted portion (excluding last element).
- 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
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:
- Start at position 0. Assume this is where the minimum should go.
- Scan the entire unsorted portion to find the actual minimum element.
- Swap the minimum with the element at position 0.
- Move to position 1 and repeat—find minimum from remaining unsorted portion.
- 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
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:
- Start from index 1 (element at index 0 is already "sorted" by itself).
- Pick the current element (call it "key") and remember it.
- Compare key with elements to its left. Shift larger elements one position right.
- Insert key into the gap created by shifting.
- 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!
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!
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
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!
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?"
- Expand: Move
rightpointer → add element to window - Check: Is the condition satisfied?
- Shrink: While condition is satisfied, move
leftpointer → try to minimize window - 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]
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.
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 rowsmatrix[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]
3 rows × 4 columns, all zeros (default)
Method 2: Inline values
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
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"
.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
Traversal Sequence
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
Traversal Sequence (top-to-bottom, left-to-right)
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
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)
After Transpose
// 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
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
After Transpose
After Reverse Rows
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)
- 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