Note : https://gist.github.com/carefree-ladka/58c72ce26f079ffea973bbd2e1dec60e
- Introduction to Binary Search
- Basic Binary Search
- Lower Bound and Upper Bound
- Binary Search on Rotated Arrays
- Peak Finding
- Binary Search on Answer
- Binary Search in 2D Matrix
- Binary Search with Precision
- Advanced Patterns
- Time and Space Complexity
- Common Pitfalls and Best Practices
Binary Search is a highly efficient algorithm for finding an element in a sorted array. It works by repeatedly dividing the search interval in half, achieving O(log n) time complexity.
Key Requirements:
- Array must be sorted (or have some monotonic property)
- Random access to elements (array/list structure)
Core Concept: Instead of checking every element linearly, we eliminate half of the remaining elements at each step by comparing with the middle element.
The fundamental binary search implementation to find if a target exists in a sorted array.
/**
* Basic Binary Search - Find target in sorted array
* @param {number[]} arr - Sorted array
* @param {number} target - Element to find
* @return {number} - Index of target, or -1 if not found
*/
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
// Prevent integer overflow: use Math.floor((left + right) / 2)
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // Target not found
}
// Example usage
const arr = [1, 3, 5, 7, 9, 11, 13, 15];
console.log(binarySearch(arr, 7)); // Output: 3
console.log(binarySearch(arr, 6)); // Output: -1Recursive Implementation:
function binarySearchRecursive(arr, target, left = 0, right = arr.length - 1) {
if (left > right) {
return -1;
}
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
return binarySearchRecursive(arr, target, mid + 1, right);
} else {
return binarySearchRecursive(arr, target, left, mid - 1);
}
}Finds the first position where we can insert the target while maintaining sorted order, or the first occurrence of the target.
/**
* Lower Bound - Find first position >= target
* @param {number[]} arr - Sorted array
* @param {number} target - Target value
* @return {number} - Index of lower bound
*/
function lowerBound(arr, target) {
let left = 0;
let right = arr.length;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid; // Could be the answer, keep searching left
}
}
return left;
}
// Find first occurrence of target
function findFirstOccurrence(arr, target) {
const idx = lowerBound(arr, target);
return (idx < arr.length && arr[idx] === target) ? idx : -1;
}
// Example
const arr1 = [1, 2, 2, 2, 3, 4, 5];
console.log(lowerBound(arr1, 2)); // Output: 1 (first occurrence)
console.log(findFirstOccurrence(arr1, 2)); // Output: 1
console.log(lowerBound(arr1, 6)); // Output: 7 (insertion point)Finds the last position where the target can exist, or the position after the last occurrence.
/**
* Upper Bound - Find first position > target
* @param {number[]} arr - Sorted array
* @param {number} target - Target value
* @return {number} - Index of upper bound
*/
function upperBound(arr, target) {
let left = 0;
let right = arr.length;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
// Find last occurrence of target
function findLastOccurrence(arr, target) {
const idx = upperBound(arr, target) - 1;
return (idx >= 0 && arr[idx] === target) ? idx : -1;
}
// Count occurrences using bounds
function countOccurrences(arr, target) {
const first = lowerBound(arr, target);
const last = upperBound(arr, target);
return last - first;
}
// Example
const arr2 = [1, 2, 2, 2, 3, 4, 5];
console.log(upperBound(arr2, 2)); // Output: 4
console.log(findLastOccurrence(arr2, 2)); // Output: 3
console.log(countOccurrences(arr2, 2)); // Output: 3A sorted array that has been rotated at some pivot point.
/**
* Search in Rotated Sorted Array
* @param {number[]} nums - Rotated sorted array
* @param {number} target - Target to find
* @return {number} - Index of target or -1
*/
function searchRotated(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums[mid] === target) {
return mid;
}
// Determine which half is sorted
if (nums[left] <= nums[mid]) {
// Left half is sorted
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// Right half is sorted
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
// Example
const rotated = [4, 5, 6, 7, 0, 1, 2];
console.log(searchRotated(rotated, 0)); // Output: 4
console.log(searchRotated(rotated, 3)); // Output: -1/**
* Find Minimum in Rotated Sorted Array
* @param {number[]} nums - Rotated sorted array
* @return {number} - Minimum element
*/
function findMinRotated(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
// If mid element is greater than right, minimum is in right half
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
// Minimum is in left half (including mid)
right = mid;
}
}
return nums[left];
}
// Example
console.log(findMinRotated([4, 5, 6, 7, 0, 1, 2])); // Output: 0
console.log(findMinRotated([3, 4, 5, 1, 2])); // Output: 1/**
* Find Maximum in Rotated Sorted Array
* @param {number[]} nums - Rotated sorted array
* @return {number} - Maximum element
*/
function findMaxRotated(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
// If mid is greater than right, max could be at mid or left
if (nums[mid] > nums[right]) {
left = mid;
// Need to break if left and right are adjacent
if (right - left === 1) break;
} else {
right = mid - 1;
}
}
return nums[left];
}
// Example
console.log(findMaxRotated([4, 5, 6, 7, 0, 1, 2])); // Output: 7A peak element is an element that is strictly greater than its neighbors.
/**
* Find Peak Element
* @param {number[]} nums - Array of numbers
* @return {number} - Index of peak element
*/
function findPeakElement(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
// If middle element is less than next, peak is on right
if (nums[mid] < nums[mid + 1]) {
left = mid + 1;
} else {
// Peak is on left (including mid)
right = mid;
}
}
return left;
}
// Example
console.log(findPeakElement([1, 2, 3, 1])); // Output: 2
console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // Output: 5A mountain array increases up to a peak and then decreases.
/**
* Find Peak in Mountain Array
* @param {number[]} arr - Mountain array
* @return {number} - Index of peak
*/
function peakIndexInMountainArray(arr) {
let left = 0;
let right = arr.length - 1;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] < arr[mid + 1]) {
// Ascending, peak is to the right
left = mid + 1;
} else {
// Descending, peak is to the left (or at mid)
right = mid;
}
}
return left;
}
// Search in Mountain Array
function searchInMountain(arr, target) {
const peak = peakIndexInMountainArray(arr);
// Search in ascending part
let result = binarySearch(arr.slice(0, peak + 1), target);
if (result !== -1) return result;
// Search in descending part (reverse binary search)
let left = peak + 1;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
right = mid - 1; // Reversed because descending
} else {
left = mid + 1;
}
}
return -1;
}
// Example
console.log(peakIndexInMountainArray([0, 1, 0])); // Output: 1
console.log(peakIndexInMountainArray([0, 2, 1, 0])); // Output: 1
console.log(peakIndexInMountainArray([0, 10, 5, 2])); // Output: 1Binary search on answer is used when:
- The answer lies in a range [min, max]
- We can check if a particular value is valid in O(n) or better
- If answer x is valid, then all values in one direction are also valid (monotonic property)
Pattern:
- Define the search space (min and max possible answer)
- Write a check function to validate if a value works
- Binary search on the range to find optimal answer
1. Minimum Capacity / Maximum Minimum
/**
* Split Array Largest Sum
* Split array into m subarrays to minimize the largest sum
* @param {number[]} nums - Array of numbers
* @param {number} m - Number of subarrays
* @return {number} - Minimum largest sum
*/
function splitArray(nums, m) {
// Check if we can split with max subarray sum <= maxSum
function canSplit(maxSum) {
let count = 1;
let currentSum = 0;
for (const num of nums) {
if (currentSum + num > maxSum) {
count++;
currentSum = num;
if (count > m) return false;
} else {
currentSum += num;
}
}
return true;
}
let left = Math.max(...nums); // Minimum possible answer
let right = nums.reduce((a, b) => a + b, 0); // Maximum possible answer
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (canSplit(mid)) {
right = mid; // Try smaller maximum
} else {
left = mid + 1;
}
}
return left;
}
// Example
console.log(splitArray([7, 2, 5, 10, 8], 2)); // Output: 182. Koko Eating Bananas
/**
* Minimum eating speed to finish all bananas in h hours
* @param {number[]} piles - Banana piles
* @param {number} h - Hours available
* @return {number} - Minimum eating speed
*/
function minEatingSpeed(piles, h) {
function canFinish(speed) {
let hours = 0;
for (const pile of piles) {
hours += Math.ceil(pile / speed);
}
return hours <= h;
}
let left = 1;
let right = Math.max(...piles);
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (canFinish(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// Example
console.log(minEatingSpeed([3, 6, 7, 11], 8)); // Output: 43. Allocate Books / Painter's Partition
/**
* Allocate minimum pages to students
* @param {number[]} books - Pages in each book
* @param {number} students - Number of students
* @return {number} - Minimum maximum pages
*/
function allocateBooks(books, students) {
if (students > books.length) return -1;
function isValid(maxPages) {
let studentCount = 1;
let currentPages = 0;
for (const pages of books) {
if (pages > maxPages) return false;
if (currentPages + pages > maxPages) {
studentCount++;
currentPages = pages;
if (studentCount > students) return false;
} else {
currentPages += pages;
}
}
return true;
}
let left = Math.max(...books);
let right = books.reduce((a, b) => a + b, 0);
let result = -1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (isValid(mid)) {
result = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return result;
}
// Example
console.log(allocateBooks([12, 34, 67, 90], 2)); // Output: 1134. Find Kth Smallest in Sorted Matrix
/**
* Kth Smallest Element in Sorted Matrix
* @param {number[][]} matrix - n x n matrix sorted row and column wise
* @param {number} k - Find kth smallest
* @return {number} - Kth smallest element
*/
function kthSmallest(matrix, k) {
const n = matrix.length;
function countLessEqual(mid) {
let count = 0;
let col = n - 1;
for (let row = 0; row < n; row++) {
while (col >= 0 && matrix[row][col] > mid) {
col--;
}
count += (col + 1);
}
return count;
}
let left = matrix[0][0];
let right = matrix[n - 1][n - 1];
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (countLessEqual(mid) < k) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
// Example
const matrix = [
[1, 5, 9],
[10, 11, 13],
[12, 13, 15]
];
console.log(kthSmallest(matrix, 8)); // Output: 13/**
* Search in row-wise and column-wise sorted matrix
* @param {number[][]} matrix - Sorted matrix
* @param {number} target - Target to find
* @return {boolean} - True if found
*/
function searchMatrix2D(matrix, target) {
if (!matrix.length || !matrix[0].length) return false;
let row = 0;
let col = matrix[0].length - 1;
// Start from top-right corner
while (row < matrix.length && col >= 0) {
if (matrix[row][col] === target) {
return true;
} else if (matrix[row][col] > target) {
col--; // Move left
} else {
row++; // Move down
}
}
return false;
}
// Example
const matrix1 = [
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
];
console.log(searchMatrix2D(matrix1, 5)); // Output: true
console.log(searchMatrix2D(matrix1, 20)); // Output: falseWhen the matrix is fully sorted (each row is sorted, and first element of next row is greater than last element of previous row).
/**
* Search in fully sorted matrix (treat as 1D array)
* @param {number[][]} matrix - Fully sorted matrix
* @param {number} target - Target to find
* @return {boolean} - True if found
*/
function searchFullySortedMatrix(matrix, target) {
if (!matrix.length || !matrix[0].length) return false;
const m = matrix.length;
const n = matrix[0].length;
let left = 0;
let right = m * n - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
// Convert 1D index to 2D coordinates
const row = Math.floor(mid / n);
const col = mid % n;
const midVal = matrix[row][col];
if (midVal === target) {
return true;
} else if (midVal < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
// Example
const matrix2 = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 60]
];
console.log(searchFullySortedMatrix(matrix2, 3)); // Output: true
console.log(searchFullySortedMatrix(matrix2, 13)); // Output: false/**
* Calculate square root with given precision
* @param {number} n - Number to find square root of
* @param {number} precision - Decimal places
* @return {number} - Square root
*/
function sqrtWithPrecision(n, precision = 6) {
if (n < 0) return NaN;
if (n === 0 || n === 1) return n;
let left = 0;
let right = n;
const epsilon = Math.pow(10, -precision);
while (right - left > epsilon) {
const mid = (left + right) / 2;
const square = mid * mid;
if (Math.abs(square - n) < epsilon) {
return parseFloat(mid.toFixed(precision));
} else if (square < n) {
left = mid;
} else {
right = mid;
}
}
return parseFloat(((left + right) / 2).toFixed(precision));
}
// Integer part first, then precision
function sqrtOptimized(n, precision = 6) {
if (n < 0) return NaN;
if (n === 0 || n === 1) return n;
// Find integer part using binary search
let left = 0;
let right = n;
let intPart = 0;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (mid * mid === n) {
return mid;
} else if (mid * mid < n) {
intPart = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
// Find decimal part
let answer = intPart;
let increment = 0.1;
for (let i = 0; i < precision; i++) {
while (answer * answer <= n) {
answer += increment;
}
answer -= increment;
increment /= 10;
}
return parseFloat(answer.toFixed(precision));
}
// Examples
console.log(sqrtWithPrecision(50, 4)); // Output: 7.0711
console.log(sqrtOptimized(2, 6)); // Output: 1.414214
console.log(sqrtOptimized(10, 3)); // Output: 3.162/**
* Find cube root with precision
* @param {number} n - Number
* @param {number} precision - Decimal places
* @return {number} - Cube root
*/
function cubeRoot(n, precision = 6) {
let left = n < 0 ? n : 0;
let right = n < 0 ? 0 : n;
if (n > -1 && n < 1) {
left = -1;
right = 1;
}
const epsilon = Math.pow(10, -precision);
while (right - left > epsilon) {
const mid = (left + right) / 2;
const cube = mid * mid * mid;
if (Math.abs(cube - n) < epsilon) {
return parseFloat(mid.toFixed(precision));
} else if (cube < n) {
left = mid;
} else {
right = mid;
}
}
return parseFloat(((left + right) / 2).toFixed(precision));
}
// Example
console.log(cubeRoot(27, 4)); // Output: 3.0000
console.log(cubeRoot(8, 6)); // Output: 2.000000
console.log(cubeRoot(-27, 4)); // Output: -3.0000/**
* Find median of two sorted arrays
* @param {number[]} nums1 - First sorted array
* @param {number[]} nums2 - Second sorted array
* @return {number} - Median
*/
function findMedianSortedArrays(nums1, nums2) {
// Ensure nums1 is smaller for optimization
if (nums1.length > nums2.length) {
[nums1, nums2] = [nums2, nums1];
}
const m = nums1.length;
const n = nums2.length;
let left = 0;
let right = m;
while (left <= right) {
const partition1 = Math.floor((left + right) / 2);
const partition2 = Math.floor((m + n + 1) / 2) - partition1;
const maxLeft1 = partition1 === 0 ? -Infinity : nums1[partition1 - 1];
const minRight1 = partition1 === m ? Infinity : nums1[partition1];
const maxLeft2 = partition2 === 0 ? -Infinity : nums2[partition2 - 1];
const minRight2 = partition2 === n ? Infinity : nums2[partition2];
if (maxLeft1 <= minRight2 && maxLeft2 <= minRight1) {
// Found the correct partition
if ((m + n) % 2 === 0) {
return (Math.max(maxLeft1, maxLeft2) + Math.min(minRight1, minRight2)) / 2;
} else {
return Math.max(maxLeft1, maxLeft2);
}
} else if (maxLeft1 > minRight2) {
right = partition1 - 1;
} else {
left = partition1 + 1;
}
}
throw new Error("Arrays are not sorted");
}
// Example
console.log(findMedianSortedArrays([1, 3], [2])); // Output: 2
console.log(findMedianSortedArrays([1, 2], [3, 4])); // Output: 2.5/**
* Minimum capacity to ship packages in given days
* @param {number[]} weights - Package weights
* @param {number} days - Days available
* @return {number} - Minimum ship capacity
*/
function shipWithinDays(weights, days) {
function canShip(capacity) {
let daysNeeded = 1;
let currentWeight = 0;
for (const weight of weights) {
if (currentWeight + weight > capacity) {
daysNeeded++;
currentWeight = weight;
if (daysNeeded > days) return false;
} else {
currentWeight += weight;
}
}
return true;
}
let left = Math.max(...weights);
let right = weights.reduce((a, b) => a + b, 0);
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (canShip(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// Example
console.log(shipWithinDays([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5)); // Output: 15/**
* Find k closest elements to x
* @param {number[]} arr - Sorted array
* @param {number} k - Number of elements
* @param {number} x - Target value
* @return {number[]} - K closest elements
*/
function findClosestElements(arr, k, x) {
let left = 0;
let right = arr.length - k;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
// Compare distances: which window is better?
if (x - arr[mid] > arr[mid + k] - x) {
left = mid + 1;
} else {
right = mid;
}
}
return arr.slice(left, left + k);
}
// Example
console.log(findClosestElements([1, 2, 3, 4, 5], 4, 3)); // Output: [1, 2, 3, 4]
console.log(findClosestElements([1, 2, 3, 4, 5], 4, -1)); // Output: [1, 2, 3, 4]| Operation | Time Complexity | Notes |
|---|---|---|
| Basic Binary Search | O(log n) | Halves search space each iteration |
| Rotated Array Search | O(log n) | Still divides space in half |
| 2D Matrix Search | O(m + n) or O(log(m*n)) | Depends on approach |
| Binary Search on Answer | O(n * log(range)) | n for validation, log for search |
| Precision Search | O(log(n/ε)) | ε is precision |
| Implementation | Space Complexity | Notes |
|---|---|---|
| Iterative | O(1) | Constant extra space |
| Recursive | O(log n) | Call stack depth |
| With auxiliary structures | O(n) | Depends on data structures used |
For an array of size n:
- Linear Search: n comparisons in worst case
- Binary Search: log₂(n) comparisons in worst case
Example:
- Array size: 1,000,000
- Linear Search: Up to 1,000,000 comparisons
- Binary Search: Maximum 20 comparisons (log₂(1,000,000) ≈ 20)
Problem:
// Bad: Can overflow for large numbers
const mid = Math.floor((left + right) / 2);Solution:
// Good: Prevents overflow
const mid = Math.floor(left + (right - left) / 2);Problem:
// Can cause infinite loop when left and right are adjacent
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (condition) {
left = mid; // Dangerous!
} else {
right = mid - 1;
}
}Solution:
// Option 1: Use left + 1 for mid
const mid = Math.floor(left + (right - left + 1) / 2);
// Option 2: Ensure progress
if (condition) {
left = mid + 1;
} else {
right = mid;
}Always check:
- Empty array
- Single element array
- Target at boundaries (first/last element)
- Target not in array
function safeBinarySearch(arr, target) {
// Handle edge cases
if (!arr || arr.length === 0) return -1;
if (arr.length === 1) return arr[0] === target ? 0 : -1;
let left = 0;
let right = arr.length - 1;
// Rest of implementation...
}// Use left <= right when you want to check every element
while (left <= right) {
// ...
}
// Use left < right when you want to find boundary/position
while (left < right) {
// ...
}For finding exact match:
let left = 0;
let right = arr.length - 1; // Inclusive
while (left <= right) { /* ... */ }For finding boundary/position:
let left = 0;
let right = arr.length; // Exclusive
while (left < right) { /* ... */ }Before applying binary search, ensure the property you're searching on is monotonic:
// Good: Array is sorted
const sorted = [1, 2, 3, 4, 5];
// Bad: Not monotonic, binary search won't work
const notSorted = [1, 3, 2, 4, 5];
// Good: Function is monotonic (checking feasibility)
function canFinishInTime(time) {
// Returns true for all times >= minimum
// Returns false for all times < minimum
}Always test your binary search with:
- [ ] Empty array
- [ ] Single element
- [ ] Two elements
- [ ] Target at start
- [ ] Target at end
- [ ] Target in middle
- [ ] Target not present
- [ ] Duplicate elements (if applicable)
- [ ] All same elements (if applicable)
- Always prevent integer overflow in mid calculation
- Clearly define search space boundaries (inclusive/exclusive)
- Use appropriate loop condition (
<=vs<) - Test edge cases thoroughly
- Document your invariants (what remains true after each iteration)
- Verify monotonicity before applying binary search
- Choose iterative over recursive for space efficiency
- Use meaningful variable names (left/right instead of i/j)
Binary Search is a powerful technique that extends far beyond simple array searching:
Core Concepts:
- Requires sorted/monotonic property
- O(log n) time complexity
- Can be applied to answer spaces, not just arrays
Key Patterns:
- Basic Search: Find exact element
- Bounds: Lower/upper bound for insertions
- Rotated Arrays: Handle sorted but rotated data
- Peaks: Find local maxima
- Answer Search: Optimize over a range of answers
- 2D Search: Extend to matrices
- Precision: Handle floating-point searches
When to Use Binary Search:
- Data is sorted or has monotonic property
- Need O(log n) complexity
- Can validate answers efficiently
- Search space is bounded and ordered
Practice Problems:
- LeetCode: 704, 35, 33, 34, 162, 410, 875, 1011, 74, 378
- Focus on understanding the pattern, not memorizing solutions
Master these patterns, and you'll be able to recognize and solve binary search problems efficiently!