二分题目汇总

120 阅读5分钟

1. 简单二分

二分查找常用来在有序数组中某个值的下标或者是某个值应该插入的位置。因为数组是有序(或局部有序)的,每次都可以将数组平分,故二分的最坏时间复杂度和平均时间复杂度都为O(logN)。具体的二分查找分为两种。

1.1 左闭右开

当查找区间是左闭右开的时候,形如下面的形式:

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

可能初学者会奇怪在这里left和right的修改是不一样的。这是因为既然我们确定查找区间是左闭右开的,那么区间的修改就必须保持一致,同时循环的结束条件也要保持一致。即查找区间[left, right)left == right查找就应该结束。

1.2 左闭右闭

另外一种则是左右搜索区间都是闭区间的时候,形式如下:

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

可以看到,随着查找区间的改变。循环的结束条件和区间的修改策略都是不一样的。

1.3 Leetcode 35.搜索插入位置

image.png 在排序数组中查找目标值并返回索引,若值不存在则返回该值插入的位置。解法如下:

    int searchInsert(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        while(left < right) {
            int mid = left + (right - left)/2;
            if(nums[mid] == target) return mid;
            else if(nums[mid] > target) right = mid;
            else left = mid + 1;
        }
        return left;
    }

若采用左闭右闭区间搜索,则为:

    int searchInsert(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        while(left <= right) {
            int mid = left + (right - left)/2;
            if(nums[mid] == target) return mid;
            else if(nums[mid] > target) right = mid - 1;
            else left = mid + 1;
        }
        return left;
    }

这里我并不想再多加讨论这两种解法,我想搞清楚的是。由于查找区间不同,所以循环结束条件也是不同的,那么为什么最后都是返回left呢?

  1. 区间左闭右开的情况下。若目标值存在,则最后区间收缩到目标值,且此时left == right导致循环结束,返回索引;若目标值不存在,例如数组为[2, 4], target = 3,因为求取mid是向下取整的,此时nums[mid]比软会小于目标值,然后更新left = mid + 1导致循环结束,此时的left就是目标值该被插入的位置。
  2. 区间左闭右闭的情况与上面类似,不做过多阐述,大家可以自行举例子理解。

2. 二维二分

image.png 二维二分本质上和一维二分并没有太大区别。我们在实际二分查找的时候需要手动mid转化为行列值。

    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(), n = matrix[0].size();
        int left = 0, right = m * n;
        while(left < right) {
            int mid = left + (right - left)/2;
            //转化mid为行列值
            int tmp = matrix[mid/n][mid%n];
            if(tmp == target) return true;
            else if(tmp < target) left = mid + 1;
            else right = mid;
        }
        return false;
    }

3.查找指定数字的边界

image.png 当数组为有序的时候,我们都可以考虑用二分是否能解决。以本题为例,当我们查找左边界的时候。
若当前元素不等于目标值,我们正常按照二分来收缩边界;若当前元素等于目标值,记录当前下标,同时收缩右边界(具体的收缩规则需要考虑到二分的查找区间)

class Solution {
public:
    //左闭右闭,循环结束条件和更新边界需要保持一致
    int searchLeft(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
        int leftBorder = -1;
        while(left <= right) {
            int mid = left + (right - left)/2;
            if(nums[mid] == target) {
                right = mid - 1;
                leftBorder = mid;
            } 
            else if(nums[mid] > target) right = mid - 1;
            else left = mid + 1;
        }
        return leftBorder;
    }

    //左闭右开
    int searchRight(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        int rightBorder = -1;
        while(left < right) {
            int mid = left + (right - left)/2;
            if(nums[mid] == target) {
                rightBorder = mid;
                left = mid + 1;
            } 
            else if(nums[mid] > target) right = mid;
            else left = mid + 1;
        }
        return rightBorder;
    }
    vector<int> searchRange(vector<int>& nums, int target) {
        return {searchLeft(nums, target), searchRight(nums, target)};
    }
};

4. 搜索旋转排序数组

image.png 一个数组经过旋转后变成了两段局部有序的数组。这里的核心思想就是:每次数组二分之后,必然有一段是有序的,而另一段则不是,我们只需在有序的部分继续二分即可

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
        while(left <= right) {
            int mid = left + (right-left)/2;
            if(nums[mid] == target) return mid;
            //[left, mid]局部有序
            else if(nums[left] <= nums[mid]) {
                if(target >= nums[left] && target <= nums[mid]) {
                    right = mid - 1;
                }
                else {
                    left = mid + 1;
                }
            }
            //[mid, right]局部有序
            else {
                if(target >= nums[mid] && target <= nums[right]) {
                    left = mid + 1;
                }
                else {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }
};

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

image.png 搜索旋转排序数组中的最小值。我们需要想办法怎么利用它的特性来压缩搜索空间。与搜索指定值不同,我们没有明显的比较条件。但可以确定初始时候最小值也就是目标值就在搜索区间[left, right]内。当nums[mid] < nums[right]时,说明nums[mid]在最小值右边;反之则在目标值左边,我们可以利用这一点来压缩搜索空间。因为这里需要用到nums[right],则必须是左闭右闭区间。

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        while(left <= right) {
            int mid = left + (right-left)/2;
            //当右边有序时,mid有可能就是目标值,故right = mid
            if(nums[mid] < nums[right]) {
                right = mid;
            }
            else {
                left = mid + 1;
            }
        }
        return nums[right];
    }
};

6. 查找只出现一个的数字

若一个数组有序,且只有一个数字出现一次,其它的都是出现两次。查找只出现一次的数字?
本题看似只能遍历查找,但实际上仍然有规律可循。

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int left = 0;
        int right = nums.size() - 1;

        while (left < right) {
            int mid = left + (right - left) / 2;
            //若当前为偶数下标,则考虑nums[mid]与nums[mid+1]
            if (mid % 2 == 0) {
                if (nums[mid] == nums[mid + 1]) {
                    left = mid + 2;
                } else {
                    right = mid;
                }
            } else {
                if (nums[mid] == nums[mid - 1]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return nums[left];
    }
};

7. 两个有序数组的中位数