def bisect(list_, x, begin=0, end=None):
    """
    Return the insertion point for x to maintain sorted order, in logarithmic 
    time.
    
    list_ -- the sorted list of items; we assume ascending order by default
    x -- the item we search for
    begin -- the first index of the range we want to search in
    end -- the index past end of the range we want to search in
    
    Search inside list_[begin:end]. 
    
    The result is an index i such as if we insert the item there, the list
    remains sorted.
    
    Time complexity: O(log(N)), where N is the length of the search range,
                     i.e. `N = end - begin`
    """
    # check input parameters
    if end is None:
        end = len(list_)
    if not 0 <= begin <= end <= len(list_):
        raise ValueError('0 <= begin <= end <= len(list_) must hold')
    if begin == end:
        return begin
    # we don't want to implement this recursively so that we won't have to
    # worry about Python's max recursion depth 
    # loop while the search range includes two or more items
    while end - begin > 1:
        # find the midpoint 
        mid = begin + (end - begin) // 2
        # "halve" the search range and continue searching 
        # in the appropriate half
        if x <= list_[mid]:
            end = mid
        else:
            begin = mid + 1
    # we are down to a search range of zero or one items; either:
    # - begin == end, which can happen if we search for an item that's bigger
    #   than every item in the list, or
    # - begin is the index of the only left item; end is the next index
    if begin == end or x <= list_[begin]:
        return begin
    else:
        return end