二分查找算法详解

433 阅读8分钟

定义

       二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

查找过程

        首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

二分查找场景

        二分查找常用的场景有: 寻找一个数、寻找左侧边界、寻找右侧边界, 下面对这些场景的细节做一些介绍,然后列进一些leetcode的上的题目。

二分查找的框架伪代码如下:

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

二分查找常见题

二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

来源: leetcode-cn.com/problems/bi…

var search = function(nums, target) {
    let start = 0, end = nums.length - 1, mid = 0;
    while(start <= end) {
        mid = start + Math.floor((end - start) / 2);
        if(nums[mid] === target) {
            return mid;
        } else if(target < nums[mid]) {
            end = mid - 1;
        } else if(target  > nums[mid]) {
           start = mid + 1;
        }
    }
    return -1;
};

有效的完全平方数

给定一个正整数num,编写一个函数,如果num是一个完全平方数,则返回 True,否则返回 False。

输入:16
输出:True

来源: leetcode-cn.com/problems/va…

var isPerfectSquare = function(num) {
    if(num === 1) return true;
    let left = 0, right = num;
    while(left <= right) {
        let mid = left + Math.floor((right - left) / 2);
        let tmp = mid * mid;
        if(tmp === num) {
            return true;
        } else if(tmp < num) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return false;
};

两数之和 II - 输入有序数组

给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 说明: 返回的下标值(index1 和 index2)不是从零开始的。 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。  

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2

来源: leetcode-cn.com/problems/tw…

var twoSum = function(numbers, target) {
    let left = 0, right = numbers.length - 1;
    while(left <= right) {
        if(numbers[left] + numbers[right] < target) {
            left++;
        } else if(numbers[left] + numbers[right] > target) {
            right--;
        } else if(numbers[left] + numbers[right] === target) {
            return [left + 1, right + 1];
        }
    }
};

x 的平方根

实现 int sqrt(int x) 函数。 计算并返回 x 的平方根,其中 x 是非负整数。 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。  

输入: 4
输出: 2

来源: leetcode-cn.com/problems/sq…

var mySqrt = function(x) {
    if (x < 2) return x
     let left = 1, mid, right = Math.floor(x / 2);
     while (left <= right) {
        mid = Math.floor(left + (right - left) / 2)
        if (mid * mid === x) return mid
        if (mid * mid < x) {
            left = mid + 1
        }else {
            right = mid - 1
        }
     }
     return right;
};

排序矩阵查找

给定M×N矩阵,每一行、每一列都按升序排列,请编写代码找出某元素。

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

给定 target = 5,返回 true

来源: leetcode-cn.com/problems/so…

var searchMatrix = function(matrix, target) {
    if(matrix.length === 0) return false;
    let row = matrix.length, col = matrix[0].length;
    for (let i = 0, j = col - 1; i >= 0 && i < row && j >= 0 && j < col;) {
        if(matrix[i][j] > target) {
            j--;
        } else if(matrix[i][j] < target) {
            i++;
        } else {
            return true;
        }
    }
    return false;
};

搜索旋转排序数组

给你一个升序排列的整数数组 nums ,和一个整数 target 。 假设按照升序排序的数组在预先未知的某个点上进行了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。 

输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

来源: leetcode-cn.com/problems/se…

//如果中间的数小于最右边的数,则右半段是有序的,若中间数大于最右边数,则左半段是有序的,
//我们只要在有序的半段里用首尾两个数组来判断目标值是否在这一区域内,这样就可以确定保留哪半边了
var search = function(nums, target) {
    if(nums.length === 0) return -1;
    let left = 0, right = nums.length - 1;
    while(left <= right) {
        let mid = (left + right) >> 1;
        if(nums[mid] === target) {
            return mid;
        } else if(nums[mid] < nums[right]) {
            if(nums[mid] < target && nums[right] >=target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        } else {
            if(nums[left] <= target && nums[mid] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
    }
    return -1;
};

搜索旋转排序数组 II

假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。 编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。  

输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true

来源: leetcode-cn.com/problems/se…

var search = function(nums, target) {
    if(nums.length == 0) return false;
    let left = 0, right = nums.length - 1;
    while(left <= right) {
        while(left < nums.length - 1 && nums[left] == nums[left + 1])
            ++left;
        while(right > 0 && nums[right] == nums[right - 1])
            --right;
        let mid = (left + right) >>> 1;
        if(nums[mid] == target) {
            return true;
        } else if (nums[mid] < nums[right]) {
            if(nums[mid] < target && nums[right] >= target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        } else {
            if(nums[left] <= target && nums[mid] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
    }
    return false;
};

搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性: 每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 3
输出:true

来源: leetcode-cn.com/problems/se…

var searchMatrix = function(matrix, target) {
    if(matrix.length === 0) return false;
    let row = 0;
    let col = matrix[0].length - 1;
    let length = matrix.length;
    while(row < length && col >= 0) {
        if(matrix[row][col] == target) {
            return true;
        } else if(matrix[row][col] < target) {
            row++;
        } else {
            col--;
        }
    }
    return false;
};

山脉数组的峰顶索引

我们把符合下列属性的数组 A 称作山脉:

  • A.length >= 3
  • 存在 0 < i < A.length - 1 使得A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1]  

给定一个确定为山脉的数组,返回任何满足 A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1] 的 i 的值。 

输入:[0,2,1,0]
输出:1

来源: leetcode-cn.com/problems/pe…

var peakIndexInMountainArray = function(A) {
    let l = 1, r = A.length - 2;
    while(l <= r) {
        let mid = (l + r) >> 1;
        if(A[mid] > A[mid + 1]) {
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    return l;
};

寻找两个正序数组的中位数

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的中位数。

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

来源: leetcode-cn.com/problems/me…

// i 为分割线在第1个数组右边的第一个元素下标 = 割线在第1个数组左边的元素个数
// j 为分割线在第2个数组右边的第一个元素下标 = 割线在第2个数组左边的元素个数
var findMedianSortedArrays = function(nums1, nums2) {
    if(nums1.length > nums2.length) {
        let tmp = nums1;
        nums1 = nums2;
        nums2 = tmp;
    }
    let m = nums1.length, n = nums2.length;
    let totalL = ((m + n + 1) >> 1);
    let l = 0, r = m;
    while(l < r) {
        let i = l + Math.floor((r - l + 1) / 2);
        let j = totalL - i;
        if(nums1[i - 1] > nums2[j]) {
            r = i - 1;
        } else {
            l = i;
        }
    }
    let i = l;
    let j = totalL - i;
    let nums1LMax = i === 0 ? -Infinity : nums1[i - 1];
    let nums1RMin = i === m ? Infinity : nums1[i];
    let nums2LMax = j === 0 ? -Infinity : nums2[j - 1];
    let nums2RMin = j === n ? Infinity : nums2[j];

    if((m + n) % 2 === 1) {
        return Math.max(nums1LMax, nums2LMax);
    } else {
        return (Math.max(nums1LMax, nums2LMax) + Math.min(nums1RMin, nums2RMin)) / 2;
    }
};

猜数字大小

猜数字游戏的规则如下:

  • 每轮游戏,我都会从 1n随机选择一个数字。 请你猜选出的是哪个数字。
  • 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-110):

  • -1:我选出的数字比你猜的数字小 pick < num

  • 1:我选出的数字比你猜的数字大 pick > num

  • 0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num  。

    输入:n = 10, pick = 6 输出:6

    输入:n = 1, pick = 1 输出:1

来源: leetcode-cn.com/problems/gu…

/** 
 * Forward declaration of guess API.
 * @param {number} num   your guess
 * @return 	            -1 if num is lower than the guess number
 *			             1 if num is higher than the guess number
 *                       otherwise return 0
 * var guess = function(num) {}
 */

/**
 * @param {number} n
 * @return {number}
 */
var guessNumber = function(n) {
    let low = 1;
    let high = n;     
    while(true){        
        let mid = Math.floor((low+high)/2);        
        let result = guess(mid);       
        //他的数字比较小,就是我猜的大了
        if(result==-1){
            high = mid-1;
        }else if(result == 1){
            low = mid+1;
        }else{
            return mid;
        }        
    }
};

山脉数组中查找目标值

输入:array = [1,2,3,4,5,3,1], target = 3
输出:2
解释:3 在数组中出现了两次,下标分别为 2 和 5,我们返回最小的下标 2。

来源: leetcode-cn.com/problems/fi…

var findInMountainArray = function(target, mountainArr) {
    let peek = findPeek(mountainArr);
    let index = search(mountainArr, target, 0, peek, v => v);
    if(index !== -1) {
        return index
    } else {
        return search(mountainArr, target, peek + 1, mountainArr.length() - 1, v => -v)
    }
};

function findPeek(mountainArr) {
    let l = 0, r = mountainArr.length() - 1;
    while(l < r) {
        let m = Math.floor((l + r) / 2);
        if( mountainArr.get(m) < mountainArr.get(m + 1)) {
            l = m + 1;
        } else {
            r = m;
        }
    }
    return l;
}

function search(arr, target, l, r, fn) {
    target = fn(target);
    while(l <= r) {
        let mid = Math.floor((l + r) / 2);
        let cur = fn(arr.get(mid));
        if(cur === target) {
            return mid
        } else if(cur < target) {
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return -1;
}

寻找比目标字母大的最小字母

给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。

输入:
letters = ["c", "f", "j"]
target = "a"
输出: "c"

输入:
letters = ["c", "f", "j"]
target = "c"
输出: "f"

输入:
letters = ["c", "f", "j"]
target = "d"
输出: "f"

来源: leetcode-cn.com/problems/fi…

var nextGreatestLetter = function(letters, target) {
    let low = 0, high = letters.length;
    while(low <= high) {
        let mid = low + Math.floor((high - low) / 2);
        if(letters[mid] > target) {
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }
    return low > letters.length ? letters[0] : letters[low];
};

寻找峰值

峰值元素是指其值大于左右相邻值的元素。

输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5 
解释: 你的函数可以返回索引 1,其峰值元素为 2;
     或者返回索引 5, 其峰值元素为 6。

来源: leetcode-cn.com/problems/fi…

var findPeakElement = function(nums) {
    let l = 0, r = nums.length - 1;
    for(;l < r;) {
        let mid = l + (Math.floor((r - l) / 2));
        if(nums[mid] > nums[mid + 1]) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    return l;
};

寻找旋转排序数组中的最小值

假设按照升序排序的数组在预先未知的某个点上进行了旋转。例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] 。 请找出其中最小的元素。  

输入:nums = [3,4,5,1,2]
输出:1

來源:leetcode-cn.com/problems/fi…

var findMin = function(nums) {
    if(nums.length  === 1) return nums[0];
    let left = 0, right= nums.length - 1;
    if(nums[right] > nums[0]) return nums[0]
    while(right >=  left) {
        let mid = left + Math.floor((right - left)  / 2);
        if(nums[mid]  > nums[mid + 1]) {
            return nums[mid + 1]
        }
        if(nums[mid - 1] >  nums[mid]) {
            return nums[mid]
        }
        if(nums[mid] > nums[0]) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
};

寻找旋转排序数组中的最小值 II

假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 请找出其中最小的元素。 注意数组中可能存在重复的元素。  

输入: [2,2,2,0,1]
输出: 0

来源: leetcode-cn.com/problems/fi…

var findMin = function(nums) {
    let low = 0;
    let high = nums.length - 1;
    while(low < high) {
        let pivot = low + Math.floor((high - low) / 2);
        if(nums[pivot] < nums[high]) {
            high = pivot;
        }
        else if(nums[pivot] > nums[high]) {
            low = pivot + 1;
        } else {
            high -= 1;
        }
    }
    return nums[low]
};

在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 你的算法时间复杂度必须是 O(log n) 级别。 如果数组中不存在目标值,返回 [-1, -1]。  

输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]

来源: leetcode-cn.com/problems/fi…

var searchRange = function(nums, target) {
    let left = 0, right = nums.length - 1, mid;
    while(left <= right) {
        mid = (left + right) >> 1;
        if(nums[mid] === target) break;
        if(nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    if(left > right) return [-1, -1];
    let i = mid, j = mid;
    while(nums[i] === nums[i-1]) i--;
    while(nums[j] === nums[j+1]) j++;
    return [i, j];
};

统计有序矩阵中的负数

给你一个 m * n 的矩阵 grid,矩阵中的元素无论是按行还是按列,都以非递增顺序排列。请你统计并返回 grid负数 的数目。

输入:grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]]
输出:8
解释:矩阵中共有 8 个负数。

来源: leetcode-cn.com/problems/co…

var countNegatives = function(grid) {
    let len1 = grid.length, len2 = grid[0].length;
    let count = 0, i = 0, j = len2 - 1;
    while(i < len1 && j >= 0) {
        if(grid[i][j] < 0) {
            count += len1 - i;
            j--;
        } else {
            i++;
        }
    }
    return count;
};