二分查找法

113 阅读3分钟

704. 二分查找

思路

这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?

大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。

写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。

下面我用这两种区间的定义分别讲解两种不同的二分写法。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    let left = 0;
    let right = nums.length -1;
    while(left <= right) {
        const middle = (left + right) >> 1;
        if (nums[middle] > target) {
            right = middle - 1;
        }
        if (nums[middle] < target) {
            left = middle + 1;
        }
        if (nums[middle] ===target) {
            return middle;
        }
    }
    return -1;
};

35. 搜索插入位置

思路说明:

  1. 定义两个指针 left 和 right,分别指向数组的开始和结尾。
  2. 在循环中,计算 mid 值: (left + right) / 2。
  3. 检查 nums[mid] 是否等于目标值 target。如果是,则返回 mid,表示找到了目标值的索引。
  4. 如果 nums[mid] 小于目标值 target,说明目标值在 mid 的右边。更新 left 为 mid + 1。
  5. 如果 nums[mid] 大于目标值 target,说明目标值在 mid 的左边。更新 right 为 mid - 1。
  6. 循环继续直到找到目标值或者 left > right。
  7. 如果最后没有找到目标值,返回 left。因为 left 会指向目标值插入的位置。

这种解法的时间复杂度是O(log n),其中 n 是数组的长度。因为每次循环都会将查找区间缩小一半,直到找到目标值或者区间为空。如果目标值存在于数组中,那么时间复杂度是O(log n)。如果目标值不存在于数组中,那么时间复杂度也是O(log n),因为最后会找到插入位置。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var searchInsert = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        const middle = (left + right) >> 1;
        if (nums[middle] > target) {
            right = middle - 1;
        }
        if (nums[middle] < target) {
            left = middle + 1;
        }
        if (nums[middle] === target) {
            return middle
        }
    }
    return left;
};

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

const binarySearch = (nums, target, lower) => {
    let left = 0, right = nums.length - 1, ans = nums.length;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        if (nums[mid] > target || (lower && nums[mid] >= target)) {
            right = mid - 1;
            ans = mid;
        } else {
            left = mid + 1;
        }
    }
    return ans;
}

var searchRange = function(nums, target) {
    let ans = [-1, -1];
    const leftIdx = binarySearch(nums, target, true);
    const rightIdx = binarySearch(nums, target, false) - 1;
    if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
        ans = [leftIdx, rightIdx];
    } 
    return ans;
}

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/solution/zai-pai-xu-shu-zu-zhong-cha-zhao-yuan-su-de-di-3-4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

69. x 的平方根

/**
 * @description: 模拟法   TC:O(n)  SC:O(1)
 * @author: JunLiangWang
 * @param {*} x 给定非负整数 x
 * @return {*}
 */
function simulation(x){
    /**
     * 本方案使用模拟的方式,由于仅需要返回算术平方
     * 根整数部分,因此我们只需要从1开始逐步递增找
     * 到第一个其平方大于x的数,然后将该数-1即为答
     * 案
     */

    let root=1;
    // 从1开始逐步递增,找到第一个其平方大于x的数
    while(root*root<=x)root++;
    // 将该数-1即为答案
    return root-1;
}


/**
 * @description: 二分法  TC:O(logn)  SC:O(1)
 * @author: JunLiangWang
 * @param {*} x 给定非负整数 x
 * @return {*}
 */
function binary(x){
    /**
     * 本方案使用二分法,上述模拟法是通过将根逐步递增1来逼近答案的,
     * 因此存在优化空间,我们可以利用二分法不断在分割区间并做选择,
     * 使其2倍增或2倍减的方式逼近答案
     */

    // 初值化左指针为0
    let left=0,
    // 初值化右指针为x
    right=x,
    // 初始化中间值为0
    middle=0
    // 当左指针超出了右指针,遍历完成
    while(left<=right){
        // 计算中间值
        middle=Math.floor((left+right)/2);
        // 如果中间值的平方等于x,则为答案直接返回
        if(middle*middle==x)return middle;
        // 如果中间值的平方大于x,证明[middle,right]区间
        // 的平方都是大于x的,因此舍去该区间,并重置为
        // [left,middle-1]
        else if(middle*middle>x)right=middle-1
        // 如果中间值的平方小于x,证明[left,middle]区间
        // 的平方都是小于x的,因此舍去该区间,并重置为
        // [middle+1,right]
        else left=middle+1
    }
    // 当遍历完成都未找到平方与x相等的答案,
    // 此时返回left-1即可
    return left-1;
}

/**
 * @description: 牛顿迭代法   TC:O(logn)  SC:O(1)
 * @author: JunLiangWang
 * @param {*} x
 * @return {*}
 */
function newtonIteration(x){
    /**
     * 该方案使用牛顿迭代法,我们不断用(x,f(x))的切线来逼近x^2-a=0的根。
     * 根号a其实就是x^2-a=0的一个正实根,这个函数的导数为2x。也就是说
     * 函数上任意一点(x,f(x))的切线斜率为2x。那么,x-f(x)/(2x)就是一
     * 个比x更接近的近似值。法如f(x)=x^2-a得到x-(x^2-a)/(2x),也就是
     * (x+a/x)/2。
     *  */ 
    
    if (x == 0) {
        return 0;
    }

    let C = x, x0 = x;
    while (true) {
        let xi = 0.5 * (x0 + C / x0);
        if (Math.abs(x0 - xi) < 1e-7) {
            break;
        }
        x0 = xi;
    }
    return Math.floor(x0);
}

367. 有效的完全平方数

你可以使用二分查找的方法来解决这个问题。

首先,对于给定的整数num,你可以在范围[1, num]内进行二分查找,找到一个整数mid。然后,判断mid的平方是否等于num。如果是,则num是一个完全平方数,返回true。如果不是,判断mid的平方是否大于num,如果大于,则在范围[1, mid-1]内继续进行二分查找。如果小于,则在范围[mid+1, num]内继续进行二分查找。重复这个过程,直到找到一个完全平方数或者确定不存在完全平方数。

以下是使用JavaScript实现该算法的代码:

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

console.log(isPerfectSquare(16)); // 输出 true
console.log(isPerfectSquare(14)); // 输出 false