Problem Statement
QUESTION 1: Alice has some cards with numbers written on them. She arranges the cards in decreasing order, and lays them out face down in a sequence on a table. She challenges Bob to pick out the card containing a given number by turning over as few cards as possible. Write a function to help Bob locate the card.
Input
- cards: A list of numbers sorted in decreasing order. E.g.- [13, 11, 10, 7, 4, 3, 1, 0]
- query: A number, whose position in the array is to be determined. E.g.- 7
Output
- position: The position of- queryin the list- cards. E.g.- 3in the above case (counting from- 0)
Solution:
Our function should be able to handle any set of valid inputs we pass into it. Here’s a list of some possible variations we might encounter:
- The number queryoccurs somewhere in the middle of the listcards.
- queryis the first element in- cards.
- queryis the last element in- cards.
- The list cardscontains just one element, which isquery.
- The list cardsdoes not contain numberquery.
- The list cardsis empty.
- The list cardscontains repeating numbers.
- The number queryoccurs at more than one position incards.
We will assume that our function will return -1 in case cards does not contain query.
In the case where query occurs multiple times in cards, we’ll expect our function to return the first occurrence of query.
Linear Search Algorithm:
1
2
3
4
5
6
7
def locate_card(cards, query):
    position = 0
    while position < len(cards):
        if cards[position] == query:
            return position
        position += 1
    return -1
The time complexity of linear search is O(N) and its space complexity is O(1).
Binary Search:
- Binary search to find the index of any occurence of query:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def locate_card(cards, query):
    lo, hi = 0, len(cards) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        mid_number = cards[mid]
        if mid_number == query:
            return mid
        elif mid_number < query:
            hi = mid - 1
        elif mid_number > query:
            lo = mid + 1
    return -1
- Binary search to find out first occurence of query:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def test_location(cards, query, mid):
    if cards[mid] == query:
        if mid-1 >= 0 and cards[mid-1] == query:
            return 'left'
        else:
            return 'found'
    elif cards[mid] < query:
        return 'left'
    else:
        return 'right'
def locate_card(cards, query):
    lo, hi = 0, len(cards) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        result = test_location(cards, query, mid)
        if result == 'found':
            return mid
        elif result == 'left':
            hi = mid - 1
        elif result == 'right':
            lo = mid + 1
    return -1
Complexity of Algorithm:
If we start out with an array of N elements, then each time the size of the array reduces to half for the next iteration, until we are left with just 1 element.
Initial length - N
Iteration 1 - N/2
Iteration 2 - N/4 i.e. N/2^2
Iteration 3 - N/8 i.e. N/2^3
…
Iteration k - N/2^k
Since the final length of the array is 1, we can find the
N/2^k = 1
Rearranging the terms, we get
N = 2^k
Taking the logarithm
k = log N
Where log refers to log to the base 2. Therefore, our algorithm has the time complexity O(log N). This fact is often stated as: binary search runs in logarithmic time. You can verify that the space complexity of binary search is O(1).
Binary search runs c * N / log N times faster than linear search, for some fixed constant c. Since log N grows very slowly compared to N, the difference gets larger with the size of the input. Here’s a graph showing how the comparing common functions for running time of algorithms:
Generic Binary Search:
1
2
3
4
5
6
7
8
9
10
11
12
def binary_search(lo, hi, condition):
    """TODO - add docs"""
    while lo <= hi:
        mid = (lo + hi) // 2
        result = condition(mid)
        if result == 'found':
            return mid
        elif result == 'left':
            hi = mid - 1
        else:
            lo = mid + 1
    return -1
We can now rewrite the locate_card function more succinctly using the binary_search function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def locate_card(cards, query):
    def condition(mid):
        if cards[mid] == query:
            if mid > 0 and cards[mid-1] == query:
                return 'left'
            else:
                return 'found'
        elif cards[mid] < query:
            return 'left'
        else:
            return 'right'
    return binary_search(0,len(cards)-1,condition)
Question:
Given an array of integers nums sorted in ascending order, find the starting and ending position of a given number.
Answer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def first_position(nums, target):
    def condition(mid):
        if nums[mid] == target:
            if mid > 0 and nums[mid-1] == target:
                return 'left'
            return 'found'
        elif nums[mid] < target:
            return 'right'
        else:
            return 'left'
    return binary_search(0, len(nums)-1, condition)
def last_position(nums, target):
    def condition(mid):
        if nums[mid] == target:
            if mid < len(nums)-1 and nums[mid+1] == target:
                return 'right'
            return 'found'
        elif nums[mid] < target:
            return 'right'
        else:
            return 'left'
    return binary_search(0, len(nums)-1, condition)
def first_and_last_position(nums, target):
    return first_position(nums, target), last_position(nums, target)
Problems for Practice
Here are some resources to learn more and find problems to practice.
 
 
