Start learning free

LESSON 273 OF 414 · BINARY SEARCH

Lower Bound in Scala: O(log n) Binary Search

Practice O(log n) Read the full explanation

Given a sorted array of integers nums, find the smallest index where we can place a given value such that the array remains sorted

Example 1:

Input: nums = [1, 2, 3, 5, 7], value = 4
Output: 3
Explanation: Placing the value 4 at index 3 we obtain nums = [1, 2, 3, 4, 5, 7]

Example 2:

Input: nums = [1, 2, 3], value = 2
Output: 1
Explanation: Placing the value 2 at index 1 we obtain nums = [1, 2, 2, 3]

Note:

Your algorithm should run in O(log n) time and use O(1) extra space.


STEP 1 OF 6
    Ctrl + Enter to check

    SCALA · BINARY SEARCH

    Lower Bound in Scala, explained

    Problem overview

    The method lowerBound receives a sorted Array[Int] named nums and an Int named value. It returns the smallest index at which value could be inserted while keeping the array sorted. Equivalently, it returns the index of the first element that is not less than value.

    In the first example, nums = [1, 2, 3, 5, 7] and value = 4. The elements 1, 2 and 3 are smaller than 4, and 5 is the first one that isn't, so the answer is index 3. In the second example, nums = [1, 2, 3] and value = 2. The value is already present at index 1, and inserting it there is still valid, so the answer is 1 and not 2.

    If every element is smaller than value, the answer is nums.length, one past the last index. If value is at most the first element, the answer is 0.

    Approach: binary search on the boundary

    A linear scan from the left would find the first element that is at least value in O(n) time. The statement asks for O(log n) time and O(1) extra space, so the scan has to go.

    The array is sorted, so the elements split into two groups: a (possibly empty) prefix of elements smaller than value, followed by a suffix of elements that are at least value. The answer is the position where the prefix ends. Looking at one element tells you which group it belongs to, and that lets you discard half of the candidates, the same trick binary search uses to find an exact value.

    The loop keeps a window [left, right] and maintains two facts:

    • every index before left holds a value smaller than value;
    • every index after right holds a value that is at least value.

    The window starts as the whole array, where both facts hold trivially. Each iteration shrinks the window while keeping both facts true. When the window is empty, right is exactly left - 1, so the two facts together say that left is the first index of the "at least value" group.

    Step by step

    1. Declare left and right with var, because both move. They start at 0 and nums.length - 1.
    2. Loop while left <= right. Using <= instead of < matters: a window holding a single element still has to be examined.
    3. Compute middle as (left + right) / 2. Dividing two Ints discards the fraction, so middle is a valid index inside the window.
    4. If nums(middle) < value, then middle and everything to its left is too small. Set left = middle + 1.
    5. Otherwise nums(middle) is at least value, so the answer is middle or something to its left. Set right = middle - 1. Index middle is not lost: it satisfies the second fact, and when the search ends left will point at it unless an even smaller index also qualifies.
    6. After the loop, left is the answer.

    The solution in Scala

    class Solution {
        def lowerBound(nums: Array[Int], value: Int): Int = {
            var left = 0
            var right = nums.length - 1
            while (left <= right) {
                val middle = (left + right) / 2
                if (nums(middle) < value) {
                    left = middle + 1
                } else {
                    right = middle - 1
                }
            }
            left
        }
    }
    

    Scala details

    Arrays are indexed with parentheses, nums(middle), not square brackets. Square brackets in Scala hold type parameters, as in Array[Int].

    The last expression of a block is its value, so the method ends with a bare left and needs no return. The if/else inside the loop is used only for its side effects, which is fine: both branches assign to a var.

    Method parameters are vals and can't be reassigned, which is why the search uses its own var variables. The midpoint is a val, because each iteration creates a fresh one.

    An Int is 32 bits and overflow wraps silently. For (left + right) / 2 to go wrong, the sum of two indices would have to exceed 2,147,483,647, which needs an array of more than a billion elements. The safer form left + (right - left) / 2 never adds the two indices, and it costs nothing, so it is a good habit. For example, with left = 2147483636 and right = 2147483646, the expression (left + right) / 2 evaluates to -7, while left + (right - left) / 2 gives 2147483641.

    Complexity

    Each iteration discards at least half of the window, so the loop runs at most about log n times (base 2, plus one) and the time is O(log n). The method stores only three Ints, so the extra space is O(1).

    Edge cases and common mistakes

    • Value larger than everything. lowerBound(Array(1, 2, 3), 9) returns 3, which is nums.length. Callers must treat that as "no element at or above value", not as a valid index.
    • Value smaller than everything. lowerBound(Array(1, 2, 3), 0) returns 0.
    • Empty array. right starts at -1, the loop never runs, and the method returns 0.
    • Duplicates. lowerBound(Array(1, 2, 2, 2, 3), 2) returns 1, the first of the equal elements. This is why the comparison is strict (<) and equal elements fall into the else branch: when nums(middle) equals value, the search keeps looking to the left for an earlier copy.
    • Returning on a match. Stopping as soon as nums(middle) == value is correct for plain binary search but wrong here, because an equal element may not be the first one.
    • Off-by-one updates. Setting left = middle or right = middle instead of moving past middle can make the window stop shrinking, and the loop never ends.

    Testing

    Try these inputs in the editor above:

    • [1, 2, 3, 5, 7] with value = 4 gives 3.
    • [1, 2, 3] with value = 2 gives 1.
    • [1, 2, 2, 2, 3] with value = 2 gives 1.
    • [1, 2, 3] with value = 9 gives 3.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free