Lecture 1 : Binary Search on 1D Arrays¶
Search target on sorted array using binary search¶
array = {3, 4, 6, 7, 9, 12, 16, 17}
target = 6
result = 2
Approach 1 (Iterative)¶
def binary_search(nums, target):
n = len(nums)
left , right = 0, n-1
while left <= right:
mid = (left + right )//2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid-1
else :
left = mid +1
return -1
print(binary_search([3, 4, 6, 7, 9, 12, 16, 17],6))
2
Approach 2 (Recursive)¶
def binary_search(nums, target):
def helper(left,right):
if left > right:
return -1
mid = (left + right )//2
if nums[mid] == target:
return mid
if nums[mid] > target:
return helper(left,mid-1)
return helper(mid +1 , right)
n = len(nums)
return helper(0,n-1)
print(binary_search([3, 4, 6, 7, 9, 12, 16, 17],6))
2
Complexity¶
- O(log N) : N = len(nums)
- O(1)
Implement Lower Bound¶
Given a sorted array of N integers and an integer x, write a program to find the lower bound of x.
Input Format: N = 4, arr[] = {1,2,2,3}, x = 2
Result: 1
Input Format: N = 5, arr[] = {3,5,8,15,19}, x = 9
Result: 3
FORMULA¶
LOWER_BOUND = index_of(min(ELEMENT) >= TARGET)
Approach 1 (Brute Force)¶
- forward pass
- result = element >= target
def lower_bound(nums,target):
n = len(nums)
for i in range(n):
if nums[i] >= target:
return i
return n
print(lower_bound([1,2,2,3],2))
print(lower_bound([3,5,8,15,19],9))
1 3
Complexity¶
O(N) : N = len(nums)
O(1)
Approach 2 (Binary Search)¶
- even if we find the target , keep moving left
- result = right
def lower_bound(nums,target):
n = len(nums)
left ,right = 0,n-1
result = n
while left <= right:
mid = (left + right) //2
# maybe ans
if nums[mid] >= target:
result = mid
right = mid -1
else:
left = mid +1
return result
print(lower_bound([1,2,2,3],2))
print(lower_bound([3,5,8,15,19],9))
1 3
Complexity¶
O(log N) : N = len(nums)
O(1)
Implement Upper Bound¶
Given a sorted array of N integers and an integer x, write a program to find the upper bound of x.
Input Format: N = 4, arr[] = {1,2,2,3}, x = 2
Result: 3
Input Format: N = 6, arr[] = {3,5,8,9,15,19}, x = 9
Result: 4
FORMULA¶
UPPER_BOUND = index_of(min(ELEMENT) > TARGET)
Approach 1 (Brute Force)¶
- Forward pass
- result = element > target
def upper_bound(nums,target):
n = len(nums)
for i in range(n):
if nums[i] > target:
return i
return n
print(upper_bound([1,2,2,3],2))
print(upper_bound([3,5,8,9,15,19],9))
3 4
Complexity¶
O(N) : N = len(nums)
O(1)
Approach 2 (Binary Search)¶
- even if u find the same element move right
- result = right
def upper_bound(nums,target):
n = len(nums)
left , right = 0 , n-1
result = n
while left <= right:
mid = (left + right) //2
# maybe ans
if nums[mid]>target:
result = mid
right = mid -1
else:
left = mid+1
return result
print(upper_bound([1,2,2,3],2))
print(upper_bound([3,5,8,9,15,19],9))
3 4
complexity¶
O(log N) : N = len(nums)
O(1)
Search Insert Position¶
You are given a sorted array arr of distinct values and a target value x. You need to search for the index of the target value in the array.
If the value is present in the array, then return its index. Otherwise, determine the index where it would be inserted in the array while maintaining the sorted order.
Input Format: arr[] = {1,2,4,7}, x = 6
Result: 3
Input Format: arr[] = {1,2,4,7}, x = 2
Result: 1
FORMULA¶
SEARCH_INSERT_POSITION = LOWER_BOUND = index_of(min(ELEMENT)>=TARGET)
Approach 1 (Brute Force)¶
- use lower bound
- result = element >= target
def insert_pos(nums,target):
n = len(nums)
for i in range(n):
if nums[i] >= target:
return i
return n
print(insert_pos([1,2,4,7],6))
print(insert_pos([1,2,4,7],2))
3 1
Complexity¶
O(N) : N = len(nums)
O(1)
Approach 2 (Binary Search)¶
def insert_pos(nums,target):
n = len(nums)
result = n
left , right = 0,n-1
while left <= right:
mid = (left + right) //2
if nums[mid] >= target:
result = mid
right = mid-1
else:
left = mid+1
return result
print(insert_pos([1,2,4,7],6))
print(insert_pos([1,2,4,7],2))
3 1
Complexity¶
O(log N ) : N = len(nums)
O(1)
Floor and Ceil in Sorted Array¶
Example 1:
Input Format: n = 6, arr[] ={3, 4, 4, 7, 8, 10}, x= 5
Result: 4 7
Explanation: The floor of 5 in the array is 4, and the ceiling of 5 in the array is 7.
Example 2:
Input Format: n = 6, arr[] ={3, 4, 4, 7, 8, 10}, x= 8
Result: 8 8
Explanation: The floor of 8 in the array is 8, and the ceiling of 8 in the array is also 8.
FORMULA¶
FLOOR = max(ELEMENT) <=TARGET
CEIL = min(ELEMENT) >= TARGET
Approach 1 (Brute Force)¶
- for floor : use backward loop
- for ceil : use forward loop
def compute_floor_ceil(nums,target):
## floor
floor = None
for num in reversed(nums):
if num <= target:
floor = num
break
ceil = None
for num in nums:
if num >= target:
ceil = num
break
return floor,ceil
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],5))
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],8))
(4, 7) (8, 8)
Complexity¶
- N = len(nums)
- O(N)
- O(1)
Approach 2 ( Using Single pass)¶
def compute_floor_ceil(nums,target):
floor,ceil = None,None
for num in nums:
if num <= target:
floor = num
if num >= target:
ceil = num
break
return floor,ceil
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],5))
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],8))
(4, 7) (8, 8)
Approach 3 (Binary Search)¶
def compute_floor_ceil(nums,target):
floor,ceil = None,None
n = len(nums)
left , right = 0,n-1
while left <= right:
mid = (left + right) //2
if nums[mid] <= target:
floor = nums[mid]
left = mid +1
else:
right = mid-1
left , right = 0,n-1
while left <= right:
mid = (left + right) //2
if nums[mid] >= target:
ceil = nums[mid]
right = mid -1
else:
left = mid +1
return floor,ceil
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],5))
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],8))
(4, 7) (8, 8)
Complexity¶
- N = len(nums)
- O(log N)
- O(1)
Approach 4( Single Loop Binary Search)¶
def compute_floor_ceil(nums, target):
floor, ceil = None, None
n = len(nums)
left, right = 0, n - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] <= target:
floor = nums[mid]
left = mid + 1
if nums[mid] >= target:
ceil = nums[mid]
right = mid - 1
return floor, ceil
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],5))
print(compute_floor_ceil([3, 4, 4, 7, 8, 10],8))
(4, 7) (8, 8)
Last occurrence in a sorted array¶
Given a sorted array of N integers, write a program to find the index of the last occurrence of the target key. If the target is not found then return -1.
Input: N = 7, target=13, array[] = {3,4,13,13,13,20,40}
Output: 4
Input: N = 7, target=60, array[] = {3,4,13,13,13,20,40}
Output: -1
Approach 1 (brute force)¶
- backward pass
- result = index when element == target
- result = -1 when element < target
def last_occurence(nums,target):
n = len(nums)
for i in range(n-1,-1,-1):
if nums[i] == target:
return i
if nums[i] < target:
return -1
return -1
print(last_occurence([3,4,13,13,13,20,40],13))
print(last_occurence([3,4,13,13,13,20,40],60))
4 -1
complexity¶
O(N) : N = len(nums)
O(1)
Approach2 (Binary Search)¶
- when element = target , search in right
- same as upper bound
def last_occurence(nums,target):
n = len(nums)
result = -1
left , right = 0,n-1
while left <= right:
mid = (left + right ) //2
if nums[mid] == target:
result = mid
left = mid+1
elif nums[mid] < target:
left = mid +1
else:
right = mid -1
return result
print(last_occurence([3,4,13,13,13,20,40],13))
print(last_occurence([3,4,13,13,13,20,40],60))
4 -1
Complexity¶
O(log N) : N = len(nums)
O(1)
First and Last position in sorted array¶
Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]
Input: nums = [5,7,7,8,8,10], target = 6
Output: [-1,-1]
Input: nums = [], target = 0
Output: [-1,-1]
Approach 1 (Seprate 2 methods)¶
def both_positions(nums,target):
n = len(nums)
def first_pos():
result = -1
left , right = 0,n-1
while left <= right:
mid = (left + right) //2
if nums[mid] == target:
result = mid
right = mid-1
elif nums[mid] > target:
right = mid -1
else:
left = mid +1
return result
def last_pos():
result = -1
left , right = 0,n-1
while left <= right:
mid = (left + right) //2
if nums[mid] == target:
result = mid
left = mid+1
elif nums[mid] > target:
right = mid -1
else:
left = mid +1
return result
return [first_pos(),last_pos()]
print(both_positions([5,7,7,8,8,10],8))
print(both_positions([5,7,7,8,8,10],6))
print(both_positions([],0))
[3, 4] [-1, -1] [-1, -1]
Approach 2 (Clean 1 method)¶
def both_positions(nums,target):
n = len(nums)
def binary_search(find_first = True):
result = -1
left , right = 0,n-1
while left <= right:
mid = (left + right) //2
if nums[mid] == target:
result = mid
if find_first:
right = mid-1
else:
left = mid +1
elif nums[mid] > target:
right = mid -1
else:
left = mid +1
return result
return [binary_search(True),binary_search(False)]
print(both_positions([5,7,7,8,8,10],8))
print(both_positions([5,7,7,8,8,10],6))
print(both_positions([],0))
[3, 4] [-1, -1] [-1, -1]
complexity¶
O(log N) : N = len(nums)
O(1)
Count Occurrences in Sorted Array¶
Input: N = 7, X = 3 , array[] = {2, 2 , 3 , 3 , 3 , 3 , 4}
Output: 4
Input: N = 8, X = 2 , array[] = {1, 1, 2, 2, 2, 2, 2, 3}
Output: 5
Approach 1 (Brute Force)¶
def count_occurences(nums,target):
count =0
for num in nums:
if num == target:
count +=1
return count
print(count_occurences([2, 2 , 3 , 3 , 3 , 3 , 4],3))
print(count_occurences([1, 1, 2, 2, 2, 2, 2, 3],2))
4 5
Complexity¶
O(N) : N = len(nums)
O(1)
Approach 2 (Binary Search)¶
- find lower and upper index
- result = upper - lower +1
def count_occurences(nums,target):
n = len(nums)
def binary_search(find_first = True):
result = -1
left , right = 0,n-1
while left <= right:
mid = (left + right) //2
if nums[mid] == target:
result = mid
if find_first:
right = mid-1
else:
left = mid +1
elif nums[mid] > target:
right = mid -1
else:
left = mid +1
return result
lower_index ,upper_index = binary_search(True),binary_search(False)
return upper_index - lower_index +1
print(count_occurences([2, 2 , 3 , 3 , 3 , 3 , 4],3))
print(count_occurences([1, 1, 2, 2, 2, 2, 2, 3],2))
4 5
Complexity¶
O(log N) : N = len(nums)
O(1)
Find out how many times the array has been rotated¶
Given an integer array arr of size N, sorted in ascending order (with distinct values). Now the array is rotated between 1 to N times which is unknown. Find how many times the array has been rotated.
Input Format: arr = [4,5,6,7,0,1,2,3]
Result: 4
Input Format: arr = [3,4,5,1,2]
Result: 3
Approach 1 (Brute Force)¶
- Find the smallest number in the array
- result = idx
def count_rotation(nums):
n = len(nums)
small , idx = nums[0] , 0
for i in range(1,n):
if nums[i] < small:
small = nums[i]
idx = i
return idx
print(count_rotation([4,5,6,7,0,1,2,3]))
print(count_rotation([3,4,5,1,2]))
print(count_rotation([3,1,2]))
print(count_rotation([2,1]))
4 3 1 1
Complexity¶
O(N) : N = len(nums)
O(1)
Approach 2 (Binary Search)¶
- when nums[mid] > nums[right] , then move towards right and mid cannot be the ans
- otherwise move towards left but mid could be the answer
def count_rotation(nums):
left , right = 0 , len(nums)-1
while left < right:
mid = (left + right )//2
if nums[mid] > nums[right]:
left = mid+1
else:
right = mid
return left
print(count_rotation([4,5,6,7,0,1,2,3]))
print(count_rotation([3,4,5,1,2]))
print(count_rotation([3,1,2]))
print(count_rotation([2,1]))
4 3 1 1
Approach 3 (Binary Search with res)¶
def count_rotation(nums):
left , right = 0 , len(nums)-1
res = 0
while left <= right:
mid = (left + right )//2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid -1
if nums[mid] < nums[res]:
res = mid
return res
print(count_rotation([4,5,6,7,0,1,2,3]))
print(count_rotation([3,4,5,1,2]))
print(count_rotation([3,1,2]))
print(count_rotation([2,1]))
4 3 1 1
Complexity¶
O(log N ) : N = len(nums)
O(1)
Search Single Element in a sorted array¶
Given an array of N integers. Every number in the array except one appears twice. Find the single number in the array.
Input Format: arr[] = {1,1,2,2,3,3,4,5,5,6,6}
Result: 4
Input Format: arr[] = {1,1,3,5,5}
Result: 3
Approach 1 (Brute Force)¶
- not using that it is sorted
- check each element with every other element
def single_element(nums):
n = len(nums)
for i in range(n):
is_found = False
for j in range(n):
if i != j and nums[i] == nums[j]:
is_found = True
if not is_found:
return nums[i]
return -1
print(single_element([1,1,2,2,3,3,4,5,5,6,6]))
print(single_element([1,1,3,5,5]))
4 3
Complexity¶
O(N ^2) : N = len(nums)
O(1)
Approach 2 (single forward pass )¶
- forward pass with 2 increment
- result = when nums[i] != nums[j] then nums[i]
- we have to handle lots of edge cases
def single_element(nums):
n = len(nums)
## only 1 element
if n==1:
return nums[0]
# handle all elements other than first or last
for i in range(1,n-1):
if nums[i] != nums[i-1] and nums[i] != nums[i+1]:
return nums[i]
return nums[0] if nums[0]!= nums[1] else nums[n-1]
print(single_element([1,1,2,2,3,3,4,5,5,6,6]))
print(single_element([1,1,3,5,5]))
print(single_element([1,1,2]))
print(single_element([1,1,2,3,3,4,4,8,8]))
4 3 2 2
Complexity¶
O(N)
O(1)
Approach 3 (Using Xor)¶
- Cleanest but expensive that binary search
def single_element(nums):
result = 0
for num in nums:
result ^= num
return result
print(single_element([1,1,2,2,3,3,4,5,5,6,6]))
print(single_element([1,1,3,5,5]))
print(single_element([1,1,2]))
print(single_element([1,1,2,3,3,4,4,8,8]))
4 3 2 2
Complexity¶
- N = len(nums)
- O(N)
- O(1)
Approach 4(Binary search)¶
Requires Bound Check
- binary search is basically eliminating half where we know result does not exist
- if there are no single numbers pattern : even,odd will always match e.g 0-1 , 2-3 , 4-5
- when mid is odd and matches its left neighbor, we're in the left side and need to search right
- when mid is even and matches its right , we are again in the left side and need to search right
def single_element(nums):
n = len(nums)
## only 1 element
if n==1:
return nums[0]
left,right = 0 , n-1
res = nums[0]
while left <= right:
mid = (left + right) //2
# if mid is odd , 1,3,5,..n-1
if mid%2==1:
mid -=1
# now mid is always even
# when list till mid+1 is already filled
if mid < n-1 and nums[mid]==nums[mid+1]:
left = mid +2
# when list[left:mid] has the ans
else:
right = mid-1
res = nums[mid]
return res
print(single_element([1,1,2,2,3,3,4,5,5,6,6]))
print(single_element([1,1,3,5,5]))
print(single_element([1,1,2]))
print(single_element([1,1,2,3,3,4,4,8,8]))
4 3 2 2
Complexity¶
O(log N ) : N =len(nums)
O(1)