C语言二分查找详解 二分算法入门与进阶

1  二分查找概念

二分查找也称折半查找,是一种在有序数组中查找某一特定元素的搜索算法。我们可以从定义可知,运用二分搜索的前提是数组必须是有序的,这里需要注意的是,我们的输入不一定是数组,也可以是数组中某一区间的起始位置和终止位置。

二分查找可以优化代码的时间复杂度,在面试过程中比较常见,更能锻炼逻辑思维能力。在追求代码极致性能时,二分查找比普通遍历时间复杂度要小。

2 基础二分例题

2.1  简单的二分查找,从数组nums中查找指定的target,若查到返回其索引,查不到返回-1

int FindIndexOfTarget(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] == target){
            return mid;
        }
        if (nums[mid] < target){ // target在右半区间或者不存在
            left = mid + 1;
        }
        if (nums[mid] > target){ // target在左半区间或者不存在
            right = mid - 1;
        }
    }
    return -1;
}

以上代码有3个注意点

(1)计算 mid 时 ,不能使用 (left + right )/ 2,否则有可能会导致溢出;

(2)while (left < = right) { } 注意括号内为 left <= right ,而不是 left < right ,如果我们设置条件为 left < right 则当我们执行到最后一步时,则我们的 left 和 right 重叠时,则会跳出循环,返回 -1,区间内不存在该元素,但 left 和 right 此时指向的可能就是目标元素 ;

(3)left = mid + 1,right = mid - 1 而不是 left = mid 和 right = mid。否则可能会进入死循环。

2.2 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。假设数组中无重复元素

思路:如果在数组中找到目标值,返回mid即可,十分容易理解;

           如果在数组中找达不到目标值,那么最后一次循环left==right,如果nums[left]==nums[right]==nums[mid]right跳出循环,可以发现此时left就是应该插入的位置;如果nums[left]==nums[right]==nums[mid]>target,那么执行right=mid-1(其实就是right--),由于left>right跳出循环,此时的left也是target应该插入的位置。

int FindIndexOfTarget(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] == target){
            return mid;
        }
        if (nums[mid] < target){ // target在右半区间或者不存在
            left = mid + 1;
        }
        if (nums[mid] > target){ // target在左半区间或者不存在
            right = mid - 1;
        }
    }
    return left;
}

2.3 在排序数组中查找元素的第一个索引位置,例如数组为[1,2,2,2,2,3,6,55],查找2的首个索引。(假设数组存在target)

思路:如果nums[mid]==target时,不再返回mid,而在左半区间继续查找,即right=mid-1,这样有希望找到target的首个索引位置。那么问题来了,即使定位到了首个索引,由于下一步是right=mid-1,right索引对应的数据小于target(此时right+1才是首个索引)。[left,right]区间内的值均小于target,于是在最新的[left,right]区间不断向右查找,最终left=right=mid,此时nums[left]==nums[mid]==num[right]right,跳出循环,返回left即为结果。

int FindFirstIndexOfTarget(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] < target){ // 在左半区间查找
            left = mid + 1;
        }
        if (nums[mid] >= target){ // 在右半区间查找
            right = mid - 1;
        }
    }
    return left;
}

问题来了,如果数组内不存在target,以上代码怎么修改呢?其实也很简单,不能再直接返回left,要判断一下nums[left]是否等于target,如果不等于就返回-1。

还有种极端情况,数组内不但不存在target,target还要大于数组内所有元素,所以循环跳出后left还在向右移位,最后left==numsSize,left已经超出了数组索引最大值(numsSize-1),结合这两点就知道什么情况下返回left了。

int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] < target){ // 在左半区间查找
            left = mid + 1;
        }
        if (nums[mid] >= target){ // 在右半区间查找
            right = mid - 1;
        }
    }
    if(left < numsSize && nums[left] == target)
    {
        return left;
    }
    return -1;

2.4 在排序数组中查找元素的最后一个索引位置,例如数组为[1,2,2,2,2,3,6,55],查找2的末位索引。(假设数组存在target)

思路:和前面分析类似,如果nums[mid]==target时,不再返回mid,而在右半区间查找,即left=mid+1,这样有希望找到target的末位索引,当mid定位到target的最后一个索引时,由于执行left=mid+1,导致[left,right]区间内的值均大于target,此时target末位索引是left-1。于是不断向左区间查询,直至left==right也没有查到,因为此时nums[left]==nums[right]==nums[mid]>target,便执行right=mid-1,此时right就是target的末位索引了,由于left>right也跳出了循环。

int FindLastIndexOfTarget(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] <= target){ // 在右半区间查找
            left = mid + 1;
        }
        if (nums[mid] > target){ // 在左半区间查找
            right = mid - 1;
        }
    }
    return right;
}

2.5 找出第一个大于目标元素的索引

思路:显然应该分为两种情况,一种情况是nums[mid]<=target,肯定要left=mid+1,向右区间查询;

          另一种情况是nums[mid]>target,这种相对复杂,要不要返回mid??但此时mid可能不是首个大于target的索引。如果mid==left(区间内的首位),或者nums[mid-1]<=target,此时mid必然是首个大于target的索引了,返回mid即可,否则就可以继续向左查询了,执行right=mid-1即可。

int FindFirstIndexOfBigTarget(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] <= target){ // 在右半区间查找
            left = mid + 1;
        }
        if (nums[mid] > target){ 
            if(mid == left || nums[mid - 1] <= target){
                return mid;
            }else{
                right = mid - 1;
            }
        }
    }
    return -1;
}

2.6 找出最后一个小于目标元素的索引

思路:与前面类似,分两种情况,一种情况是nums[mid]>=target,肯定要在左区间继续搜寻,即right=mid-1

另外一种情况是nums[mid]=target时,mid就是区间内最后一个小于目标元素的索引,返回mid即可。否则由于mid不是最后一个,向右区间继续搜寻即可,即left=mid+1。

int FindLastIndexOfSmallTarget(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] >= target){ // 右区间均>=target,不符合条件
            right = mid - 1;
        }
        if (nums[mid] < target){ // 此时的mid可能是答案,但可能继续在右半区间
            if(mid == right || nums[mid + 1] >= target){
                return mid;
            }else{
                left = mid + 1;
            }
        }
    }
    return -1;
}

3 二分进阶例题

3.1 一个从小到大的有序数组经过旋转,变成不完全有序数组nums,查找target的索引。(例如[7,8,9,2,3,6])。假设数组内不含有重复元素。

思路:一次遍历很简单,但其复杂度高于二分算法,二分算法往往是解题精髓。nums数组被mid索引一分为二,其中一半肯定有序,另外一半肯定无序。有序部分再一分为二,两部分都有序;无序部分再一分为二,一部分肯定有序,一部分肯定无序...........规律便出来了

  • 采用二分法实现,旋转排序数组一分为二,其中一定有一个是有序的,另一个可能是有序,也能是部分有序。
  • 此时有序部分用二分法查找。
  • 无序部分再一分为二,其中一个一定有序,另一个可能有序,可能无序。
  • 每次缩小一半搜索区间,实现查找

(0)如果nums[mid]==target,直接返回mid即可。否则执行以下两步,不断减小搜索空间
(1)如果nums[left]

(2)如果左半区间有序,并且nums[left] <= target && target < nums[mid],这说明target刚好在左半区间,否则就在右半区间

如果右半区间有序,并且target > nums[mid] && target <= nums[right],这说明target刚好落在右半区间,否则就在左半区间

int FindIndexOfTarget(int *nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //写法比(left+right)/2要好,防止溢出int
        if (nums[mid] == target){
            return mid;
        }
        if (nums[mid] > nums[left]){ //左半部分有序
            if (nums[left] <= target && target < nums[mid]){//target刚好落在左半部分
                right = mid - 1;
            }else{//target不在左半部分,必然在右半区间
                left = mid + 1;
            }
        }
        else{ //右半部分有序
            if (target > nums[mid] && target <= nums[right]){//target刚好在右半部分
                left = mid + 1;
            }else{//target不再右半部分,必然在左半区间
                right = mid - 1;
            }
        }
    }
    return -1;
}

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

思路:可以完全转化为一维有序数组二分查找的思想,只是初始left为0,初始right为二维数组总元素个数减1,nums[mid]变为了nums[mid/col][mid%col]。代码如下。

bool searchMatrix(int **matrix, int matrixSize, int *matrixColSize, int target)
{
    int row = matrixSize;//二维数组有多少行
    int col = matrixColSize[0];//二维数组每行有多少个元素,即有多少列
    int left = 0;
    int right = row * col - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        int num = matrix[mid / col][mid % col];
        if(num == target){
            return true;
        }
        if (num < target){
            left = mid + 1;
        }
        if (num > target){
            right = mid - 1;
        }
    }
    return false;
}

4 总结

二分查找在学习过程,可能会有不同模板,导致同一例题写法多种多样,不能死记硬背,二分查找核心是不断减小搜索空间,最终查到target。实现过程中要注意死循环问题和数组越界问题,意识到这两个边界问题,能减少不少麻烦。也可参考上述例题二分查找的写法,理解后能解决大部分的二分问题。

你可能感兴趣的:(C语言算法实现,算法,数据结构)