How to find the insertion point in an array using binary search?

11,345

Solution 1

This is the code from Java's java.util.Arrays.binarySearch as included in Oracles Java:

    /**
     * Searches the specified array of ints for the specified value using the
     * binary search algorithm.  The array must be sorted (as
     * by the {@link #sort(int[])} method) prior to making this call.  If it
     * is not sorted, the results are undefined.  If the array contains
     * multiple elements with the specified value, there is no guarantee which
     * one will be found.
     *
     * @param a the array to be searched
     * @param key the value to be searched for
     * @return index of the search key, if it is contained in the array;
     *         otherwise, <tt>(-(<i>insertion point</i>) - 1)</tt>.  The
     *         <i>insertion point</i> is defined as the point at which the
     *         key would be inserted into the array: the index of the first
     *         element greater than the key, or <tt>a.length</tt> if all
     *         elements in the array are less than the specified key.  Note
     *         that this guarantees that the return value will be &gt;= 0 if
     *         and only if the key is found.
     */
    public static int binarySearch(int[] a, int key) {
        return binarySearch0(a, 0, a.length, key);
    }

    // Like public version, but without range checks.
    private static int binarySearch0(int[] a, int fromIndex, int toIndex,
                                     int key) {
        int low = fromIndex;
        int high = toIndex - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            int midVal = a[mid];

            if (midVal < key)
                low = mid + 1;
            else if (midVal > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }

The algorithm has proven to be appropriate and I like the fact, that you instantly know from the result whether it is an exact match or a hint on the insertion point.

This is how I would translate this into ruby:

# Inserts the specified value into the specified array using the binary
# search algorithm. The array must be sorted prior to making this call.
# If it is not sorted, the results are undefined.  If the array contains
# multiple elements with the specified value, there is no guarantee
# which one will be found.
#
# @param [Array] array the ordered array into which value should be inserted
# @param [Object] value the value to insert
# @param [Fixnum|Bignum] from_index ordered sub-array starts at
# @param [Fixnum|Bignum] to_index ordered sub-array ends the field before
# @return [Array] the resulting array
def self.insert(array, value, from_index=0,  to_index=array.length)
  array.insert insertion_point(array, value, from_index, to_index), value
end

# Searches the specified array for an insertion point ot the specified value
# using the binary search algorithm.  The array must be sorted prior to making
# this call. If it is not sorted, the results are undefined.  If the array
# contains multiple elements with the specified value, there is no guarantee
# which one will be found.
#
# @param [Array] array the ordered array into which value should be inserted
# @param [Object] value the value to insert
# @param [Fixnum|Bignum] from_index ordered sub-array starts at
# @param [Fixnum|Bignum] to_index ordered sub-array ends the field before
# @return [Fixnum|Bignum] the position where value should be inserted
def self.insertion_point(array, value, from_index=0,  to_index=array.length)
  raise(ArgumentError, 'Invalid Range') if from_index < 0 || from_index > array.length || from_index > to_index || to_index > array.length
  binary_search = _binary_search(array, value, from_index, to_index)
  if binary_search < 0
    -(binary_search + 1)
  else
    binary_search
  end
end

# Searches the specified array for the specified value using the binary
# search algorithm.  The array must be sorted prior to making this call.
# If it is not sorted, the results are undefined.  If the array contains
# multiple elements with the specified value, there is no guarantee which
# one will be found.
#
# @param [Array] array the ordered array in which the value should be searched
# @param [Object] value the value to search for
# @param [Fixnum|Bignum] from_index ordered sub-array starts at
# @param [Fixnum|Bignum] to_index ordered sub-array ends the field before
# @return [Fixnum|Bignum] if > 0 position of value, otherwise -(insertion_point + 1)
def self.binary_search(array, value, from_index=0,  to_index=array.length)
  raise(ArgumentError, 'Invalid Range') if from_index < 0 || from_index > array.length || from_index > to_index || to_index > array.length
  _binary_search(array, value, from_index, to_index)
end

private
# Like binary_search, but without range checks.
#
# @param [Array] array the ordered array in which the value should be searched
# @param [Object] value the value to search for
# @param [Fixnum|Bignum] from_index ordered sub-array starts at
# @param [Fixnum|Bignum] to_index ordered sub-array ends the field before
# @return [Fixnum|Bignum] if > 0 position of value, otherwise -(insertion_point + 1)
def self._binary_search(array, value, from_index, to_index)
  low = from_index
  high = to_index - 1

  while low <= high do
    mid = (low + high) / 2
    mid_val = array[mid]

    if mid_val < value
      low = mid + 1
    elsif mid_val > value
      high = mid - 1
    else
      return mid # value found
    end
  end
  -(low + 1) # value not found.
end

Code returns the same values as OP provided for his test data.

Solution 2

Update 2020

Actually, the insertion problem of binary search has been well researched. There is a left insertion point and right insertion point. Code can be found on Wikipedia and Rosetta Code. For example, to find the left insertion point, the code is:

  BinarySearch_Left(A[0..N-1], value) {
      low = 0
      high = N - 1
      while (low <= high) {
          // invariants: value > A[i] for all i < low
                         value <= A[i] for all i > high
          mid = (low + high) / 2
          if (A[mid] >= value)
              high = mid - 1
          else
              low = mid + 1
      }
      return low
  }

One note is about the overflow bug, so mid really should be found as low + floor((high - low) / 2).

Earlier answer:

Actually, instead of checking for begin_index >= end_index, it can be better handled using begin_index > end_index, and the solution is much cleaner:

def binary_search_helper(arr, a, begin_index, end_index)    
  if begin_index > end_index
    return begin_index
  else
    middle_index = (begin_index + end_index) / 2
    if arr[middle_index] == a
      return middle_index
    elsif a > arr[middle_index]
      return binary_search_helper(arr, a, middle_index + 1, end_index)
    else
      return binary_search_helper(arr, a, begin_index, middle_index - 1)
    end
  end
end

# for [1,3,5,7,9], searching for 6 will return index for 7 for insertion
# if exact match is found, then return that index
def binary_search(arr, a)
  return binary_search_helper(arr, a, 0, arr.length - 1)
end

And using iteration instead of recursion may be faster and have less worry for stack overflow.

Share:
11,345
nonopolarity
Author by

nonopolarity

I started with Apple Basic and 6502 machine code and Assembly, then went onto Fortran, Pascal, C, Lisp (Scheme), microcode, Perl, Java, JavaScript, Python, Ruby, PHP, and Objective-C. Originally, I was going to go with an Atari... but it was a big expense for my family... and after months of me nagging, my dad agreed to buy an Apple ][. At that time, the Pineapple was also available. The few months in childhood seem to last forever. A few months nowadays seem to pass like days. Those days, a computer had 16kb or 48kb of RAM. Today, the computer has 16GB. So it is in fact a million times. If you know what D5 AA 96 means, we belong to the same era.

Updated on June 12, 2022

Comments

  • nonopolarity
    nonopolarity almost 2 years

    The basic idea of binary search in an array is simple, but it might return an "approximate" index if the search fails to find the exact item. (we might sometimes get back an index for which the value is larger or smaller than the searched value).

    For looking for the exact insertion point, it seems that after we got the approximate location, we might need to "scan" to left or right for the exact insertion location, so that, say, in Ruby, we can do arr.insert(exact_index, value)

    I have the following solution, but the handling for the part when begin_index >= end_index is a bit messy. I wonder if a more elegant solution can be used?

    (this solution doesn't care to scan for multiple matches if an exact match is found, so the index returned for an exact match may point to any index that correspond to the value... but I think if they are all integers, we can always search for a - 1 after we know an exact match is found, to find the left boundary, or search for a + 1 for the right boundary.)

    My solution:

    DEBUGGING = true
    
    def binary_search_helper(arr, a, begin_index, end_index)
      middle_index = (begin_index + end_index) / 2
      puts "a = #{a}, arr[middle_index] = #{arr[middle_index]}, " +
               "begin_index = #{begin_index}, end_index = #{end_index}, " +
               "middle_index = #{middle_index}" if DEBUGGING
      if arr[middle_index] == a
        return middle_index
      elsif begin_index >= end_index
        index = [begin_index, end_index].min
        return index if a < arr[index] && index >= 0  #careful because -1 means end of array
        index = [begin_index, end_index].max
        return index if a < arr[index] && index >= 0
        return index + 1
      elsif a > arr[middle_index]
        return binary_search_helper(arr, a, middle_index + 1, end_index)
      else
        return binary_search_helper(arr, a, begin_index, middle_index - 1)
      end
    end
    
    # for [1,3,5,7,9], searching for 6 will return index for 7 for insertion
    # if exact match is found, then return that index
    def binary_search(arr, a)
      puts "\nSearching for #{a} in #{arr}" if DEBUGGING
      return 0 if arr.empty?
      result = binary_search_helper(arr, a, 0, arr.length - 1)
      puts "the result is #{result}, the index for value #{arr[result].inspect}" if DEBUGGING
      return result
    end
    
    
    arr = [1,3,5,7,9]
    b = 6
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = [1,3,5,7,9,11]
    b = 6
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = [1,3,5,7,9]
    b = 60
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = [1,3,5,7,9,11]
    b = 60
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = [1,3,5,7,9]
    b = -60
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = [1,3,5,7,9,11]
    b = -60
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = [1]
    b = -60
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = [1]
    b = 60
    arr.insert(binary_search(arr, b), b)
    p arr
    
    arr = []
    b = 60
    arr.insert(binary_search(arr, b), b)
    p arr
    

    and result:

    Searching for 6 in [1, 3, 5, 7, 9]
    a = 6, arr[middle_index] = 5, begin_index = 0, end_index = 4, middle_index = 2
    a = 6, arr[middle_index] = 7, begin_index = 3, end_index = 4, middle_index = 3
    a = 6, arr[middle_index] = 5, begin_index = 3, end_index = 2, middle_index = 2
    the result is 3, the index for value 7
    [1, 3, 5, 6, 7, 9]
    
    Searching for 6 in [1, 3, 5, 7, 9, 11]
    a = 6, arr[middle_index] = 5, begin_index = 0, end_index = 5, middle_index = 2
    a = 6, arr[middle_index] = 9, begin_index = 3, end_index = 5, middle_index = 4
    a = 6, arr[middle_index] = 7, begin_index = 3, end_index = 3, middle_index = 3
    the result is 3, the index for value 7
    [1, 3, 5, 6, 7, 9, 11]
    
    Searching for 60 in [1, 3, 5, 7, 9]
    a = 60, arr[middle_index] = 5, begin_index = 0, end_index = 4, middle_index = 2
    a = 60, arr[middle_index] = 7, begin_index = 3, end_index = 4, middle_index = 3
    a = 60, arr[middle_index] = 9, begin_index = 4, end_index = 4, middle_index = 4
    the result is 5, the index for value nil
    [1, 3, 5, 7, 9, 60]
    
    Searching for 60 in [1, 3, 5, 7, 9, 11]
    a = 60, arr[middle_index] = 5, begin_index = 0, end_index = 5, middle_index = 2
    a = 60, arr[middle_index] = 9, begin_index = 3, end_index = 5, middle_index = 4
    a = 60, arr[middle_index] = 11, begin_index = 5, end_index = 5, middle_index = 5
    the result is 6, the index for value nil
    [1, 3, 5, 7, 9, 11, 60]
    
    Searching for -60 in [1, 3, 5, 7, 9]
    a = -60, arr[middle_index] = 5, begin_index = 0, end_index = 4, middle_index = 2
    a = -60, arr[middle_index] = 1, begin_index = 0, end_index = 1, middle_index = 0
    a = -60, arr[middle_index] = 9, begin_index = 0, end_index = -1, middle_index = -1
    the result is 0, the index for value 1
    [-60, 1, 3, 5, 7, 9]
    
    Searching for -60 in [1, 3, 5, 7, 9, 11]
    a = -60, arr[middle_index] = 5, begin_index = 0, end_index = 5, middle_index = 2
    a = -60, arr[middle_index] = 1, begin_index = 0, end_index = 1, middle_index = 0
    a = -60, arr[middle_index] = 11, begin_index = 0, end_index = -1, middle_index = -1
    the result is 0, the index for value 1
    [-60, 1, 3, 5, 7, 9, 11]
    
    Searching for -60 in [1]
    a = -60, arr[middle_index] = 1, begin_index = 0, end_index = 0, middle_index = 0
    the result is 0, the index for value 1
    [-60, 1]
    
    Searching for 60 in [1]
    a = 60, arr[middle_index] = 1, begin_index = 0, end_index = 0, middle_index = 0
    the result is 1, the index for value nil
    [1, 60]
    
    Searching for 60 in []
    [60]