Showdown front-end algorithm - Search Search Algorithm - binary search

Keywords: Front-end TypeScript Algorithm data structure

Index of this series

Binary search principle

Binary search is an algorithm applied to sequential sequences. In the process of search, we continuously narrow the search range on the premise of ensuring that the target value is in the search range, and finally find the target value

Firstly, the simplest binary search algorithm is introduced. This algorithm is used to locate the given target value. If it cannot be found, it returns - 1

First, set the two pointers at the beginning and end, and then compare them through the intermediate value (head + tail) > > 1. If the intermediate value is equal to the target value, the target value will be returned directly. Otherwise, perform the following operations

{ a r r [ m i d ] < x , h e a d = m i d + 1 a r r [ m i d ] > x , t a i l = m i d − 1 \begin{cases} arr[mid]<x,head=mid+1 \\ arr[mid] > x,tail = mid - 1 \end{cases} {arr[mid]<x,head=mid+1arr[mid]>x,tail=mid−1​

The operation here is easy to understand. The goal is to ensure that the target is within the search range and narrow the search range

function binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h <= t) {
        mid = (t + h) >> 1
        if (nums[mid] === target) return mid
        if (nums[mid] < target) h = mid + 1
        else t = mid - 1
    }
    return -1
}

01 model

00001111

This model is usually used to find the last 0 and the first 1

The last 0 here can be understood as the last value less than or equal to x;
The first 1 can be understood as the first value greater than or equal to x.

Here, take finding the first value greater than or equal to x as an example for reasoning

// 01-1 core part
while (h < t) {
	// Middle pointer
    mid = (h + t) >> 1
    // Narrow selection
    if (nums[mid] <= target) h = mid
    else t = mid - 1
}

// 01-1 core code
function b_binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = (h + t) >> 1
        // mid = h + ((t - h) >> 1)
        if (nums[mid] >= target) t = mid
        else h = mid + 1
    }
    return h
}

Similarly, the 01-0 code is obtained

// 01-0 core code
function s_binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = h + ((t - h) >> 1) + 1  // In any case, one to the right
        // parseInt(`${(h + t + 1) / 2} `) / / find the middle value for odd numbers and the middle right for even numbers
        if (nums[mid] <= target) h = mid
        else t = mid - 1
    }
    return h
}

Bias of middle pointer

In the above code, you can find that the mid of 01-0 and 01-1 are slightly different

In the search sequence, if the number of elements is odd, the middle value they locate is the same; However, if it is an even number, 01-0 is positioned to the right of the middle, while 01-1 is positioned to the left of the middle

If it is right, there are two cases

  1. As mentioned above, odd numbers are positioned in the middle, and even numbers are positioned in the right of the middle
    mid = parseInt(`${(h + t + 1) / 2}`)
    
  2. The pointer is one bit to the right
    mid = ((h + t) >> 1) + 1
    mid = h + ((t - h) >> 1) + 1 // In static languages, this way of writing can prevent overflow
    

Both cases work

Binary search questions

Square root of leetCode 69 x


This problem can be transformed into finding the last value less than or equal to x

function mySqrt(x: number): number {
    let h = 0, t = x, mid: number
    while (h < t) {
        mid = ((h + t) >> 1) + 1
        if (mid ** 2 <= x) h = mid
        else t = mid - 1
    }
    return h
}

leetCode 35 search insertion location


Find the first position greater than or equal to x

function searchInsert(nums: number[], target: number): number {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = (h + t) >> 1
        if (nums[mid] >= target) t = mid
        else h = mid + 1
    }
    return (h === nums.length - 1 && nums[h] < target) ? h + 1 : h
}

leetCode 1 sum of two numbers


In this problem, we use simple two-point search to accurately find the target value

First traverse the array, and then find it later t a r g e t − x target - x Value of target − x


Note that the premise of binary search is for ordered sequences, so we have to sort the original array first. Here we use the merge sort we learned before

type dataType = [val: number, ind: number]
function binarySearch (nums: dataType[], l: number, target: number) {
    let h = l, t = nums.length - 1, mid: number
    while (h <= t) {
        mid = (h + t) >> 1
        if (nums[mid][0] === target) return nums[mid][1]
        if (nums[mid][0] < target) h = mid + 1
        else t = mid - 1
    }
    return -1
}
function twoSum(nums: number[], target: number): number[] {
    const data: dataType[] = [], ret: number[] = []
    let ind: number
    nums.forEach((val, i) => data.push([val, i]))
    mergeSort(data, 0, nums.length - 1)
    for (let i = 0; i < data.length - 1; i++) {
        ind = binarySearch(data, i + 1, target - data[i][0])
        if (ind > -1) ret.push(data[i][1], ind)
    }
    return ret
}
//*Merge sort
function mergeSort (data: dataType[], l: number, r: number) {
    if (l >= r) return
    const mid = (l + r) >> 1
    mergeSort(data, l, mid)
    mergeSort(data, mid + 1, r)
    const temp: dataType[] = []
    let p1 = l, p2 = mid + 1
    while (p1 <= mid || p2 <= r) {
        if (p2 > r || (p1 <= mid && data[p1][0] < data[p2][0])) temp.push(data[p1++])
        else temp.push(data[p2++])
    }
    temp.forEach((v, i) => data[l + i] = v)
}


Of course, if C + + is used here, the efficiency of using sort through symbolic overloading will be higher

leetCode 34 finds the first and last positions of elements in a sorted array


This problem is a typical example of the application of model 01

The first position of the element is 01-1, and the last position is 01-0

//* 01-0
function s_binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = ((h + t) >> 1) + 1
        if (nums[mid] <= target) h = mid
        else t = mid - 1
    }
    //*When looking for the second value, it is certain that there must be a target value on the field
    return h
}
//* 01-1
function b_binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = (h + t) >> 1
        if (nums[mid] >= target) t = mid
        else h = mid + 1
    }
    return nums[h] === target ? h : -1
}
function searchRange(nums: number[], target: number): number[] {
    let fir: number, sec: number
    fir = b_binarySearch(nums, target)
    if (fir === -1) return [-1, -1]
    sec = s_binarySearch(nums, target)
    return [fir, sec]
}

The idea of the main program is also very simple. If the first occurrence position of the target value cannot be found, directly return [- 1, - 1], otherwise continue to find the last occurrence position (the two can coincide)

leetCode 1658 minimum operand to reduce x to 0


A very intuitive solution to this problem is to first count the prefix sum in the left and right directions. When traversing the left prefix sum, use dichotomy to act on the right prefix and array to find x - sums[l]

But this problem only needs to calculate the prefix sum from left to right once

Here we emphasize that the interval and of our previous study are as follows:
s [ 4 ] − s [ 2 ] = n [ 2 ] + n [ 3 ] Namely s [ i ] − s [ j ] = ∑ n = j n = i − 1 a n s[4] - s[2] = n[2]+n[3] \ \ i.e. s[i]-s[j]=\sum_{n=j}^{n=i-1}a_n s[4] − s[2]=n[2]+n[3], i.e. s[i] − s[j]=n=j Σ n=i − 1 an

And the one used here n [ i ] − s [ j ] n[i]-s[j] n[i] − s[j] is equivalent to statistics ( j , i ) (j, i) Number of elements between (j,i)


So when i and j are found, the result is c n t = n u m s . l e n g t h − ( i − j ) cnt = nums.length - (i - j) cnt=nums.length−(i−j)

function binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h <= t) {
        mid = (h + t) >> 1
        if (nums[mid] === target) return mid
        if (nums[mid] < target) h = mid + 1
        else t = mid - 1
    }
    return -1
}
function minOperations(nums: number[], x: number): number {
    //*Building prefixes and arrays
    const sums = [0]
    nums.forEach((n, i) => sums.push(n + sums[i]))
    //*Left and right counting
    let j: number, cnt: number, ans = -1
    for (let i = nums.length, s = 0; i >= 0; --i, s += nums[Math.max(i, 0)]) {
        j = binarySearch(sums, x - s)
        if (j === -1) continue
        cnt = nums.length - (i - j)
        if (cnt > nums.length) continue // This happens when I < = J
        if (ans === -1 || cnt < ans) ans = cnt
    }
    return ans
}

The main function strategy here is to see whether there is a target value (x - s) from the prefix and in each cycle. If not, continue to the next cycle

leetCode 475 heater


The idea of this problem is very simple. First, sort the address of the heater (the premise of binary search), then traverse the house, find the location of the nearest heater in the house and record the distance

Finally, the maximum distance is the minimum range

//* 01-0
function s_binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = ((h + t) >> 1) + 1
        if (nums[mid] <= target) h = mid
        else t = mid - 1
    }
    return nums[h]
}
//* 01-1
function b_binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = (h + t) >> 1
        if (nums[mid] >= target) t = mid
        else h = mid + 1
    }
    return nums[h]
}
function findRadius(houses: number[], heaters: number[]): number {
    heaters = heaters.sort((i, j) => i - j)
    let maxRadius = -1, recentHeater: number
    houses.forEach(house => {
        recentHeater = Math.min(Math.abs(house - s_binarySearch(heaters, house)), Math.abs(b_binarySearch(heaters, house) - house))
        maxRadius = Math.max(maxRadius, recentHeater)
    })
    return maxRadius
}


The practice here is to use two structures of 01 model, but if you master 01 model deeply enough, just use one

function b_binarySearch (nums: number[], target: number) {
    let h = 0, t = nums.length - 1, mid: number
    while (h < t) {
        mid = (h + t) >> 1
        if (nums[mid] >= target) t = mid
        else h = mid + 1
    }
    return h
}
function findRadius(houses: number[], heaters: number[]): number {
    heaters = heaters.sort((i, j) => i - j)
    let maxRadius = -1, a: number, b: number, ind: number
    houses.forEach(house => {
        ind = b_binarySearch(heaters, house)
        a = Math.abs(heaters[ind] - house)
        b = Math.abs(ind ? heaters[ind - 1] - house : a + 1)
        maxRadius = Math.max(maxRadius, Math.min(a, b))
    })
    return maxRadius
}

The idea here is to first find the heater (01-1) behind the current house. If the heater is not the first, compare it with the previous one. Who is closer to the house

Through this question, we can find that if the 01 model cannot find the result (for example, 01-1 cannot find a value greater than or equal to the target value), the first element subscript will be returned


You can see that the result is also correct

leetCode 81 search rotation sort array II


This problem is very complicated to read, but the actual situation is shown in the figure below

Each time the middle value is taken, it is only compared with the head / tail. If it is in the non shaded area, just keep the pointers on both sides in the middle

Because of the particularity of this problem, four pointers are required

Two are fixed at the head and tail, and the other two are gradually narrowing intervals

function search(nums: number[], target: number): boolean {
    if (!nums.length) return false
    if (nums[0] === target || nums[nums.length - 1] === target) return true
    //*Set the head and tail of the two sections
    let l = 0, r = nums.length - 1
    while (l < r && nums[l] === nums[0]) l++
    while (l < r && nums[r] === nums[nums.length - 1]) r--
    const head = l, tail = r
    //*Positioning element
    let mid: number
    while (l <= r) {
        mid = (l + r) >> 1
        if (nums[mid] === target) return true
        //*If it's in the right section
        if (nums[mid] < nums[tail]) {
            if (nums[mid] < target && target <= nums[tail]) l = mid + 1
            else r = mid - 1
        } else {
            //*In the left section
            if (nums[head] <= target && target < nums[mid]) r = mid - 1
            else l = mid + 1
        }
    }
    return false
}

leetCode 4 finds the median of two positively ordered arrays


To solve this problem, we must first have a consensus

With this consensus, we can start coding

function findK (nums1: number[], nums2: number[], l1: number, l2: number, k: number): number {
    //*Boundary condition judgment
    if (l1 === nums1.length) return nums2[l2 + k - 1]
    if (l2 === nums2.length) return nums1[l1 + k - 1]
    if (k === 1) return nums1[l1] < nums2[l2] ? nums1[l1] : nums2[l2]
    //*Processing of getting values from two arrays
    let cnt1 = Math.min(k >> 1, nums1.length - l1)
    let cnt2 = Math.min(k - cnt1, nums2.length - l2)
    //*Narrow down
    if (nums1[l1 + cnt1 - 1] < nums2[l2 + cnt2 - 1]) {
        return findK(nums1, nums2, l1 + cnt1, l2, k - cnt1)
    }
    return findK(nums1, nums2, l1, l2 + cnt2, k - cnt2)
}
function findMedianSortedArrays(nums1: number[], nums2: number[]): number {
    const cnt = nums1.length + nums2.length, mid = (nums1.length + nums2.length + 1) >> 1
    const a = findK(nums1, nums2, 0, 0, mid)
    if (cnt % 2) return a
    const b = findK(nums1, nums2, 0, 0, mid + 1)
    return (a + b) / 2
}

First, set a function that can find the k-th element in the two arrays. If the length of the two operation arrays is an odd number, the middle value is the middle number; If it is an even number, you need to find the number to the right and average it

The boundary conditions here are also well understood. When one array is empty, it is equivalent to directly operating on the other array; When k is 1, you only need to compare the header elements of the two arrays to see who is smaller

When taking values from two arrays, it should be noted that the length of the array may not be enough k 2 \frac{k}{2} 2k, so you need to do some compatibility processing

The final narrowing operation is based on the consensus just mentioned

Although the coding of this problem is not like the traditional binary search, it is the same in thought

Binary search determines the target value through the comparison of intermediate values, and here the target value is determined through three boundary conditions;
Binary search reduces the range through left and right pointers. Here, it reduces the range through counting and head pointers.

Xi Wei~

Posted by simenss on Tue, 21 Sep 2021 02:40:42 -0700