Start learning free

LESSON 249 OF 348 · BINARY SEARCH

Lower Bound in TypeScript: 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 on the 4th index 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 on the 1st index 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

    TYPESCRIPT · BINARY SEARCH

    Lower Bound in TypeScript, explained

    Problem overview

    The method lowerBound receives a sorted array of integers, nums, and a value. It returns the smallest index at which value could be inserted so that the array stays sorted. Equivalently, it is the index of the first element that is not less than value, or nums.length if every element is smaller.

    For nums = [1, 2, 3, 5, 7] and value = 4 the answer is 3: inserting 4 at index 3 gives [1, 2, 3, 4, 5, 7]. For nums = [1, 2, 3] and value = 2 the answer is 1. The value already exists at index 1, and inserting at the earliest valid place puts the new 2 in front of the old one.

    Approach: binary search that keeps going after a match

    The statement asks for O(log n) time and O(1) extra space, so a left-to-right scan for the first element that is large enough, which is O(n), is too slow. The array is sorted, which is what binary search needs.

    Ordinary binary search stops at a match. Here you want the first position that qualifies, so you never stop early. Each step looks at the middle element and asks one question: is it smaller than value?

    • If yes, the middle element and everything before it are too small to be the answer. Move left past middle.
    • If no, the middle element is at least value, so it could be the answer, but something earlier might also be. Move right to just before middle and keep looking on the left.

    The loop runs until the range is empty. At that point, right is one less than left. Every index below left holds a value smaller than value, and every index from left on holds a value that is at least value. So left is the first index of the second kind, which is the answer.

    On [1, 2, 3, 5, 7] with value = 4: left = 0, right = 4, middle = 2, and nums[2] = 3 is smaller, so left = 3. Next middle = 3, and nums[3] = 5 is not smaller, so right = 2. Now left > right, and the answer is left = 3.

    Step by step

    1. Set the window. left and right are declared in one let, starting at 0 and nums.length - 1, so the window covers the whole array.
    2. Loop while the window is not empty. The condition is left <= right. With <=, a window of one element is still examined.
    3. Compute the middle. Math.floor((left + right) / 2). Division of numbers can produce a fraction, as in 3 / 2, and an index must be whole.
    4. Too small? If nums[middle] < value, the answer is to the right, so left = middle + 1.
    5. Otherwise, the middle element is at least value, so the answer is middle or somewhere to its left: right = middle - 1.
    6. Return left. It is the first index whose value is at least value.

    The solution in TypeScript

    class Solution {
        lowerBound(nums: number[], value: number): number {
            let left = 0, right = nums.length - 1;
            while (left <= right) {
                let middle = Math.floor((left + right) / 2);
                if (nums[middle] < value) {
                    left = middle + 1;
                } else {
                    right = middle - 1;
                }
            }
            return left;
        }
    }
    • One let, two variables. let left = 0, right = nums.length - 1; declares both. Each is inferred as number.
    • Math.floor instead of integer division. There is no integer division operator. 7 / 2 is 3.5, so the result must be rounded down explicitly.
    • No overflow worry. left + right is an ordinary double sum, and array indices are far below the point where doubles lose precision.
    • The return value can be nums.length. When every element is smaller, left ends one past the last index. That is a valid insertion position, though not a valid index to read.
    • Types are checked at compile time only. A sorted array is not something number[] can express. On unsorted data the method returns an answer with no meaning.

    Using the result, and the upper-bound twin

    The returned index is exactly where Array.prototype.splice should insert the value. After nums.splice(lowerBound(nums, 4), 0, 4) on [1, 2, 3, 5, 7], the array is [1, 2, 3, 4, 5, 7]. The middle argument 0 means "remove nothing", and splice itself moves the later elements, so that step costs O(n) even though finding the position was O(log n).

    Changing one comparison gives the related function that finds the position after the last equal element:

    function upperBound(nums: number[], value: number): number {
        let left = 0, right = nums.length - 1;
        while (left <= right) {
            let middle = Math.floor((left + right) / 2);
            if (nums[middle] <= value) {
                left = middle + 1;
            } else {
                right = middle - 1;
            }
        }
        return left;
    }

    On [1, 2, 2, 2, 3] with 2 it returns 4, while lowerBound returns 1. The difference between the two answers, 4 - 1 = 3, is how many times the value occurs.

    Complexity

    Time: O(log n). Each iteration discards at least half of the window, so the loop runs at most about log₂(n) + 1 times.

    Space: O(1). Only left, right and middle are stored.

    Edge cases and common mistakes

    • Empty array. right is -1, the loop does not run and the answer is 0.
    • Value smaller than everything. [1, 2, 3] with 0 gives 0.
    • Value larger than everything. [1, 2, 3] with 9 gives 3, which equals the length.
    • Duplicates. [1, 2, 2, 2, 3] with 2 gives 1, the first copy. Returning at the first match would give an index in the middle of the run, which is wrong for this problem.
    • Using <= in the comparison. nums[middle] <= value finds the position after the last equal element, a different function often called the upper bound.
    • Returning right. It ends one position too low. The answer is left.
    • Skipping Math.floor. A fractional index such as 1.5 reads undefined from the array.

    Testing

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