Searching Algorithms
Explores different searching techniques such as linear search, binary search, and hash tables. Includes analysis of their time complexity and trade-offs.
Binary Search Algorithm
What is Binary Search?
Binary search is a highly efficient search algorithm used to find the position of a target value within a sorted array. It works by repeatedly dividing the search interval in half. If the target value matches the middle element of the array, its index is returned. If the target value is less than the middle element, the search continues in the left half. If the target value is greater than the middle element, the search continues in the right half. This process continues until the target value is found or the search interval is empty, indicating the target value is not present in the array.
Key Prerequisite: The data must be sorted. Binary search absolutely relies on the sorted order of the elements. If the data is not sorted, the algorithm will likely return incorrect results or fail entirely.
In-Depth Explanation
How it Works
- Initialization: Start with the entire sorted array as the search interval. Define two pointers,
low
andhigh
, representing the lower and upper bounds of the search interval. Initially,low
points to the first element (index 0) andhigh
points to the last element (index n-1, where n is the number of elements in the array). - Find the Middle Element: Calculate the middle index
mid
using the formulamid = low + (high - low) / 2
. This formula avoids potential integer overflow issues that could occur with(low + high) / 2
when dealing with very large arrays. - Comparison: Compare the target value with the element at the middle index
array[mid]
:- Match: If
target == array[mid]
, the target value is found! Return the indexmid
. - Target is Smaller: If
target < array[mid]
, the target value, if present, must be in the left half of the array. Update thehigh
pointer tomid - 1
. - Target is Larger: If
target > array[mid]
, the target value, if present, must be in the right half of the array. Update thelow
pointer tomid + 1
.
- Match: If
- Iteration/Recursion: Repeat steps 2 and 3 with the narrowed search interval (either the left half or the right half) until one of the following conditions is met:
- Target Found: The target value is found, and its index is returned.
- Search Interval Empty:
low > high
. This indicates that the target value is not present in the array. Return a value indicating "not found" (e.g., -1).
Why Sorted Data is Essential
Binary search relies heavily on the ordered nature of the data. Each comparison with the middle element allows the algorithm to eliminate half of the remaining search space. This is only possible because we know that all elements to the left of the middle element are smaller (or equal to) and all elements to the right are larger (or equal to) than the middle element. Without this guarantee, we would not be able to confidently discard half of the array in each iteration, making the search no more efficient than a linear search.
Imagine trying to use binary search on an unsorted array. After comparing the target with the middle element, there's no way to know whether the target is more likely to be in the "left half" or the "right half." You would essentially have to check every element individually, which defeats the purpose of binary search.
Implementation
Iterative Implementation (Python)
def binary_search_iterative(arr, target):
low = 0
high = len(arr) - 1
while low <= high:
mid = low + (high - low) // 2 # Prevent potential overflow
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1 # Target not found
Recursive Implementation (Python)
def binary_search_recursive(arr, target, low, high):
if low > high:
return -1 # Target not found
mid = low + (high - low) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, high)
else:
return binary_search_recursive(arr, target, low, mid - 1)
# Example Usage (Recursive)
# arr = [2, 3, 4, 10, 40]
# target = 10
# result = binary_search_recursive(arr, target, 0, len(arr) - 1)
Time Complexity Analysis
Binary search offers significant performance advantages over linear search due to its logarithmic time complexity. Here's a breakdown of its time complexity in different scenarios:
- Best-Case Time Complexity: O(1)
The best-case scenario occurs when the target value is found at the middle index of the array in the very first comparison. In this case, the algorithm terminates immediately, requiring only one comparison. This is a constant-time operation.
- Worst-Case Time Complexity: O(log n)
The worst-case scenario occurs when the target value is not present in the array or is located at the very edge of the search space, requiring the algorithm to repeatedly divide the search interval in half until it is empty. The number of times the array can be divided in half before reaching a single element (or an empty interval) is proportional to the logarithm base 2 of the array's size (log2 n). Therefore, the worst-case time complexity is O(log n).
- Average-Case Time Complexity: O(log n)
On average, binary search will perform slightly more than the theoretical minimum number of comparisons (which is log2 n). However, the average-case time complexity is still logarithmic, O(log n). The small constant factor difference is usually negligible, especially for large input sizes.
Space Complexity:
- Iterative: O(1) The iterative implementation uses a constant amount of extra space, regardless of the input size. It only requires a few variables to store pointers and the middle index.
- Recursive: O(log n) The recursive implementation has a space complexity of O(log n) due to the call stack. Each recursive call adds a new frame to the call stack, and in the worst case, the depth of the recursion is proportional to log n.
Examples
Example 1: Finding a Value
Let's say we have the sorted array arr = [2, 5, 7, 8, 11, 12]
and we want to find the target value 13
.
low = 0
,high = 5
,mid = 2
,arr[mid] = 7
. Since13 > 7
, we updatelow = mid + 1 = 3
.low = 3
,high = 5
,mid = 4
,arr[mid] = 11
. Since13 > 11
, we updatelow = mid + 1 = 5
.low = 5
,high = 5
,mid = 5
,arr[mid] = 12
. Since13 > 12
, we updatelow = mid + 1 = 6
.- Now,
low = 6
andhigh = 5
. The conditionlow <= high
is false. We return -1, indicating that the target value 13 is not found.
Example 2: Value Found
Using the same array arr = [2, 5, 7, 8, 11, 12]
, let's search for 11
.
low = 0
,high = 5
,mid = 2
,arr[mid] = 7
. Since11 > 7
, we updatelow = mid + 1 = 3
.low = 3
,high = 5
,mid = 4
,arr[mid] = 11
. Since11 == 11
, we returnmid = 4
. The target value is found at index 4.
Practice Problems
- Search in a Rotated Sorted Array: A sorted array is rotated at some pivot unknown to you beforehand. Search for a target element in this rotated array. Example: `[4, 5, 6, 7, 0, 1, 2]` might be rotated from `[0, 1, 2, 4, 5, 6, 7]`.
- Find First and Last Position of Element in Sorted Array: Given a sorted array and a target value, find the index of the first and last occurrence of the target in the array. If the target is not present in the array, return `[-1, -1]`.
- Peak Index in a Mountain Array: An array arr is a mountain if the following properties hold:
- arr.length >= 3
- There exists some i with 0 < i < arr.length - 1 such that:
- arr[0] < arr[1] < ... < arr[i-1] < arr[i]
- arr[i] > arr[i+1] > ... > arr[arr.length - 1]
- Implement `sqrt(x)`: Given a non-negative integer x, compute and return the square root of x. Since the return type is an integer, the decimal digits are truncated, and only the integer part of the result is returned.