'''
Binary search

Binary search offers very fast search for a particular item
in a sequence of sorted(!) items.
An usual, so called linear, search needs a number of checks
comparable to the length of the sequence. When sequence is long,
that number may reach millions and more checks, to find a single item.
In binary search, most of items in the sequence are skipped and
not checked.
When a length of a sequence is about 1 million, or 10**6,
the number of checks performed in binary search is about just 20,
or, to be more precise, binary logarithm of 10**6 rounded
to nearest bigger integer.
'''

def binarySearch( value, arr ):
    print( 'Looking for',  value )
    # search range is [low..high], in other words, arr[low:high+1]

    # start search with the max possible range
    low = 0
    high = len(arr)-1

    while low < high:    # while length of search range is at least 2


        # middle position in the search range
        mid = (low + high) // 2

        # limit search range to only the left or only the right half
        # of the search range:
        if arr[mid] < value: low  = mid+1    # right half
        else:                high = mid      # left half
        # end of while

    # now search range contains just one item, check it:
    if arr[low] == value:
       return low           # found or
    return -1               # not found

'''


Note 1
  When the value is not found, there is no standard which would say
what value(s) should be returned or whether maybe an exception should be raised, etc.
Thus, it mostly depends on the module/library authors' attitudes.

Note 2
  Binary search requires the array to be sorted. However, the search does
not verify if the array is sorted, that would destroy its effectivity.

Note 3
  There is a very useful application of the so-called "lazy approach" paradigm
in the implementation of binary sort. Note that the deciding check
 -- item found/not found -- is postponed (procrastinated) to the very end
of the function. The values encountered during the search at positions
[low], [mid], [high] are never checked whether they are maybe already equal
to the searched value. But that would allow - sometimes - to locate
the searched value even earlier, somebody might say.
Yes, but the percentage of cases when *that* would happen is
negligibly small. In most cases it massively pays off to omit
any intermediate checks for the searched value.

Exercise 1.
  Change binarySearch() function, so that it searches in an array sorted
in non-ascending order.

Exercise 2.
  When there are multiple occurences of the queried value in the array,
binarySearch() function returns the index of the leftmost occurence.
Change binarySearch() function, so that it returns the index of the rightmost occurence.
'''


# In the following variant of binary search,
# it is not important whether the given value is present in the
# array.  Note the subtle(!) CHANGES to the binarySearch() function.

def indexOfClosestBiggerValueTo( value, arr ):
    # search range is [low..high], in other words, arr[low:high-1]

    # start search with the max possible range
    low = 0
    high = len(arr)-1

    while low < high:    # while length of search range is at least 2

        # middle position in the search range
        mid = (low + high) // 2

        # limit search range to only the left or only the right half
        # of the search range:
        if arr[mid] <= value: low  = mid+1  # CHANGE: '<=' cut off the unpromissing left part
        else:                 high = mid

        # end of while

    # now search range contains just one item, check it:
    if arr[low] > value:                   # CHANGE: '>' accept only a bigger value
       return low           # found or
    return -1               # not found

'''
Note
When we search for a particular given value,
it may be even more effective to store the values
in a dictionary or in a set (internally hash tables),
their expected serach time is even shorter than that of binary search.
However, when the search is based on a value(s)  NOT stored in the sequence
(in a dictionary, set, etc.), like finding the clossest bigger value via
function indexOfClosestBiggerValueTo(), binary search or its modification
may be the only available option of a simple and effective search.


Exercise
  Change closestBiggerValueTo() function, so that it searches in a array sorted
in non-ascending order.
'''

# ----------------- END OF MODULE ----------------------------------------------


# ==============================================================================
# anything to run when NOT used as a library module

if __name__ == '__main__':
    a = [ 2, 2, 4, 6, 7, 10, 11, 11, 11, 12, 12, 14, 20, 20 ]
    # success
    print( binarySearch ( 2, a ) )
    print( binarySearch ( 4, a ) )
    print( binarySearch ( 11, a ) )
    print( binarySearch ( 20, a ) )
    # fail
    print( binarySearch ( -1, a ) )
    print( binarySearch ( 3, a ) )
    print( binarySearch ( 13, a ) )
    print( binarySearch ( 21, a ) )


    # success
    for x in range( -1, a[-1] ):
        print( x, indexOfClosestBiggerValueTo( x, a ), a[indexOfClosestBiggerValueTo(x,a)]  )
    # fail
    print( indexOfClosestBiggerValueTo ( 20, a ) )
    print( indexOfClosestBiggerValueTo ( 21, a ) )
    print( indexOfClosestBiggerValueTo ( 33, a ) )





