写二分查找不得不注意的细节!!!说三遍

1,184 阅读3分钟

Problem: 367. 有效的完全平方数 problem: 704.二分查找

[TOC]

思路

二分查找的整体思路

二分查找模板(左闭右闭)
int search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size()-1; // 定义target在左闭右闭的区间里,即:[left, right]
    while (left <= right) { // 因为left == right的时候,在[left, right)是有效的空间,所以使用 <=
        int middle = left + ((right - left) >> 1);
        if nums[middle] == target{
            return middle; // 数组中找到目标值,直接返回下标
        }else if(nums[middle] > target) {
            right = middle-1; // target 在左区间,在[left, middle]中
        } else if (nums[middle] < target) {
            left = middle + 1; // target 在右区间,在[middle + 1, right]中
        }
    }
    // 未找到目标值
    return -1;
}

二分查找模板(左闭右开)
int search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
    while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
        int middle = left + ((right - left) >> 1);
        if nums[middle] == target{
            return middle; // 数组中找到目标值,直接返回下标
        }else if(nums[middle] > target) {
            right = middle; // target 在左区间,在[left, middle)中
        } else if (nums[middle] < target) {
            left = middle + 1; // target 在右区间,在[middle + 1, right)中
        }
    }
    // 未找到目标值
    return -1;
}

二分查找思路不难,就是在有序无重复元素中,通过一个left和一个right,确定一个middle,从而将数据一分为二 二分1.png 再将midddle与 目标值target进行比较。

  1. 如果middle等于target ,显然我们要就返回对应middle下标。
  2. 如果middle大于target ,显然我们要将left右移。(注意!我们这里的数据是升序的!!!)
  3. 如果middle小于于target ,显然我们要将left左移。

再更新middle值,在目标值所在区间继续进行二分查找,不断循环下去

这里主要的是一些细节大家容易出错。

  1. 循环的条件 在while( left ? right ) 里面用 > 还是 >= 呢?
  2. left right移动的位置 if( 表达式 ){ right = ? } 里面是middle 还是 midlle-1/middle-1 呢?

要想解决这样的细节问题,最重要的就是先解决区间问题

(一)区间问题 right取值

一般常用的就是两种区间

  1. 左闭右闭 [left,right]
  2. 左闭右开 [left,right)

在区间中,想要查找到每个元素,就要先想想自己用的是 左闭右闭 还是 左闭右开? 其实不管是左闭右闭还是左闭右开,都需要明确,我们要查询过每一个要查元素。

开始,我们比较的是始末元素和中值。这里就需要注意哪个right=nums.size()-1哪个是nums.size()

左闭右闭 二分5.png

如果是左闭右闭,由于左右边界我们都可以取到,我们的leftright开始就应该在数组的始末。所以right=nums.size()-1

左闭右开 二分4.png

如果是左闭右闭,由于右边界我们取不到,我们的right开始应该在数组的末尾的后一位。所以right=nums.size()

(二)left right移动的位置

解决了开始的 right取值,下面解释left right移动的位置就方便多了 还是刚才的图 , 由于每一次middle都会被查找过, 所以我们下次的查找的范围都应该是从leftmiddle-1或者middle+1right 二分5-2.png 二分4-1.png 因为开区间right取不到自己,所以范围相当于从middle+1right-1,而我们之前初始化的时候已经给 right加过1了,所以范围也是middle+1right

左闭右闭 二分5.png

由于是左闭右闭,左右边界leftright我们都可以取到,所以找到目标区间后,我们的leftright应该跳过middle, 从middle-1或者middle+1开始查找,即right=nums.size()-1 / left = middle+1 二分5-1.png

左闭右开 二分4.png 由于是左闭右闭,右边界right我们不可以取到,所以找到目标区间后,我们的left应该跳过middle,即left = middle+1 ,而right应该等于middle,从而使查找的范围,也是从leftmiddle-1,middle+1right,即right=middle / left = middle+1 二分4-2.png

(三)循环条件

知道了left right移动的位置,然后就该讨论下一个细节问题,循环条件。

在之前都没有找到目标值的情况下,最后一次循环结束的条件应该是,我们把目标区间里的数,也就是最后一次的leftright里的数都应该查询了一遍。

左闭右闭

(本例为假设) 如果target = 5,上次left = 4 right = 5 middle = 4。 由于nums[middle] < target,left = middle+1,所以left = right = 5 这时left = right,我们该不该继续循环呢? 二分10.png 答案时肯定的,因为我们还没有找到5,意味着middle还没有取到5,所以我们应该继续循环让middle=5. 二分6.png 此时,middle刚好时5,结束返回下标结束。 但是,如果这时不是5,我们又取完了区间内所有的数,下一次就会结束循环,也就是left>right时,结束循环,所以循环条件是left<=right 二分7.png (还有一种情况就是left右移,也会结束循环)

左闭右开 (本例为假设) 如果target = 5,上次left = 5 right = 7(下标) middle = 6(下标)。 由于nums[middle] < target,right = middle 二分11.png 这时middle = left,由于我们没有取过left对应元素,所以循环应该继续。

再循环一次,由于nums[middle] < target,right = middle,所以 left = right = middle = 5 二分6.png 由于这时已经取过了所有区间内元素,所以left = right时就应该结束循环,所以循环的条件是left<right

解题方法

掌握了二分查找就好解决了

  1. 区间选择左边右闭。
  2. 循环条件left <= right
  3. left = middle+1 , right =middle-1

只是把比较的middle 变成了 middle*middle,要注意类型转换,防止数据溢出。

复杂度

  • 时间复杂度:

O(logn)O(logn)

Code


class Solution {
public:
    bool isPerfectSquare(int num) {
        int left = 0, right = num;
        while(left <= right){
            int middle = left + (right - left)/2;
            long square = (long)middle * middle; //要强制类型转换为 long 类型
            if( square == num){
                return true;     
            }else if(square > num){
                right = middle - 1;
            }else if(square < num){
                left = middle + 1;
            }
        }

            return false;
        
    }
};