【二分查找】原型和变种以及相关思考

一、写在前面的话

笔者在面一家非常NB的互联网公司时,面到了二分查找的变种题,回来后对这个看似简单的二分查找做了深入的思考,可能也不算深入,但至少比之前是更加领教了它的厉害。每一次面试都会带来不一样的思维启迪,面完这家公司带给我最大的启迪就是学算法不止学它本身,应该看到它的边界和可扩展性,经常想想这些好的经典算法,让它融入到自己潜意识里,才算真正掌握。话不多说,开始上干货。

二、二分查找原型

/*********************************************** 
Author:tmw 
date:2018-3-24 
************************************************/  
#include <stdio.h>  
#include <stdlib.h>  
  
/**二分查找**/  
int binary_search(int array[] , int array_number , int target_data )  
{  
    int left = 0;  
    int right = array_number - 1;  
  
    while(left<=right)  
    {  
        int mid = left + ( right - left ) / 2;  
  
        if( target_data > array[mid] ) //目标值大于中间值,则说明在中间值以右查找--变更left值  
            left = mid + 1;  
        else if( target_data < array[mid]) //目标值小于中间值,说明在中间值以左查找--变更right值  
            right = mid - 1;  
        else  
            return mid;  
    }  
    return -1; //没查找到则返回-1  
}  

二分查找原型代码使用的前提是:1、数组是有序的;2、不考虑重复元素

二分查找时间复杂度O(logn),空间复杂度O(1)

因此,当这两个条件发生改变时,就需要在原有二分查找的基础上,做些小改动

 

三、二分查找变种1 ----- 出现重复元素的情况

【例如】在一个有序数组,求一个数字k,找出它在该数组中第一次出现的位置和最后一次出现的位置,数组内可能有重复元素------【注意:此题的解法也可作为求:数字在排序数组中出现的次数!!!】。

    此题已经确认了数组是个递增序列,因此在二分查找时只需要考虑重复元素的处理即可
特殊情况无非是,当array[mid]==target时,不能直接返回位置,得往前探索(找第一次出现的位置)或往后探索(找最后一次出现的位置)
    因此,
        1)在找第一次出现的位置中,当array[mid] == target时,由于不确定array[mid-1]?=array[mid],所以不要随意就return mid,而是先用一个变量暂存mid值,让right=mid-1(外层的while循环left<=right,一定要取等号才是right=mid-1,如果不取等号就是right=mid)

        2)在找最后一次出现的位置中,当array[mid] == target时,由于不确定array[mid+1]?=array[mid],所以也不要随意就return mid,也是先用一个变量暂存mid值,让left=mid+1(外层的while循环left<=right,一定要取等号才是left=mid+1,如果不取等号就是left=mid)

代码如下:

/*************************************************
Author:tmw
date:2018-3-24
*************************************************/
#include <stdio.h>
#include <stdlib.h>

/**
变种:
1、序列递增条件不变
2、元素可能会重复出现----改动情况
**/

/**找上界**/
int Binary_find_FirstPosition(int* array, int len, int target)
{
    int left = 0;
    int right = len-1;
    int ans = -1;
    while( left<=right )
    {
        int mid = left+(right-left)/2;
        if( target == array[mid] )
        {
            ans = mid;
            /**
                由于上面的while循环取了等号,所以在找第一次出现的位置时,用:
                right = mid-1;
            **/
            right = mid-1;
        }
        else if( target > array[mid] )
            left = mid + 1;
        else
            right = mid - 1;
    }
    return ans;
}

int Binary_find_LastPosition(int* array, int len, int target)
{
    int left = 0;
    int right = len-1;
    int ans = -1;
    while( left <= right )
    {
        int mid = left + (right-left)/2;
        if( target == array[mid] )
        {
            ans = mid;
            /**
                由于上面的while循环取了等号,所以在找最后一次出现的位置时,用:
                left = mid+1;
            **/
            left = mid + 1;
        }
        else if( target > array[mid] )
            left = mid + 1;
        else
            right = mid - 1;
    }
    return ans;
}

四、二分查找变种2 ----旋转排序数组,mid切分使得一部分有序另一部分无序

【例如】在旋转排序数组中搜索目标值target,如果在数组中发现它返回其索引,否则返回-1。数组元素无重复

 

这道题跟普通的排好序的序列不同,因为它是旋转排序的 4 5 6 7 0 1 2。依旧采用二分查找,但是需要对边界进行改进:

1、当mid的位置切到数组左边部分内部或边缘

    1)target值若在左边,则左边必是单调递增,即array[left]<=target<array[mid],让right游标放到mid处

    2)target值若在右边,则右边不一定单调递增,left游标放到mid+1继续观望

2、当mid的位置切到右边部分内部

    1)target值若在右边:则右边必定是单调递增,即array[right]>=target>array[mid],让left = mid+1

    2)target值若在左边,则左边不一定单调递增,right游标放到mid继续观望

/*************************************************
Author:tmw
date:2018-3-23
*************************************************/
#include <stdio.h>
#include <stdlib.h>

/**

二分查找变种:
1、序列有一部分确定一定递增,有一部分不一定递增----变动情况
2、序列中无重复元素

**/

int search( int* array, int len, int target )
{
    int left = 0;
    int right = len-1;
    int mid;

    while( left < right )
    {
        mid = left+(right-left)/2;
        if( array[mid] == target )
            return mid;

        /**当mid的位置切到数组左边部分内部或边缘**/
        if( array[left]<=array[mid] )
        {
            /**
                target值若在左边:则左边必是单调递增,即array[left]<=target<array[mid]
                注意:target<array[mid]不取等号是因为这种情况在上一个if直接考虑了。
            **/
            if( target<array[mid] && target>=array[left])
                right = mid;
            /**target值在右边的情况**/
            else
                left = mid+1;
        }
        /**当mid的位置切到右边部分内部**/
        else
        {
            /**
                target值若在右边:则右边必定是单调递增,即array[right]>=target>array[mid]
            **/
            if( array[right]>=target && target>array[mid] )
                left = mid+1;
            else
                right = mid;
        }
    }
    return -1; //没找到
}
/**method 2: 先考虑target的位置在左边还是右边,貌似比上个方法更容易理解**/
int rotated_array_binary_search(int array[], int len, int target)
{
    int left = 0;
    int right = len - 1;
    int mid = 0;

    while( left <= right )
    {
        mid = (left + right) / 2;
        //在左半支
        if(target > array[left])
        {
            //mid的位置在右半支
            if(array[mid] < array[left])
                right = mid;
            //mid在左半支
            else
            {
                if(target > array[mid])
                    left = mid + 1;
                else if(target < array[mid])
                    right = mid - 1;
                else
                    return mid;
            }
        }
        //在右半支
        else if(target < array[left])
        {
            //mid切分在左半支
            if(array[mid] > array[right])
                left = mid;
            //mid切分在右半支
            else
            {
                if(target > array[mid])
                    left = mid + 1;
                else if(target < array[mid])
                    right = mid - 1;
                else
                    return mid;
            }
        }
        else
            return left;
    }
    return -1;
}

五、二分查找变种3 ----旋转排序数组,mid切分使得一部分有序另一部分无序,同时有重复元素

【例1】在旋转排序数组中搜索目标值target,如果在数组中发现它返回其索引,否则返回-1。数组元素允许重复

    允许重复元素,则上一题中如果array[mid]>=array[left], 那么[left,mid] 为递增序列的假设就不能成立了,比如[1,3,1,1,1]。

    如果array[mid]>=array[left] 不能确定递增,那就把它拆分成两个条件:

        1)若array[mid]>array[left],则区间[left,mid] 一定递增

        2)若array[mid]==array[left] 确定不了,可能是[1,3,1,1,1]这种非定增,也可能是[3,3,3,1,1]这种递增,那就left++,往下看一步即可。

/*************************************************
变种:
1、序列不一定递增(与mid位置划分和元素重复出现有关)   -----改动情况
2、元素可能会重复出现                                 ----改动情况

Author:tmw
date:2018-3-23
*************************************************/
#include <stdio.h>
#include <stdlib.h>

/**时间复杂度O(logn),空间复杂度O(1)**/
int search_pro( int array[], int len, int key )
{
    int left = 0;
    int right = len-1;

    while( left <= right )
    {
        int mid = left+(right-left)/2;

        /**当切到中间**/
        if( key == array[mid] )
            return mid;

        /**当切到左部分递增序列内**/
        if( array[mid] > array[left] )
        {
            /**
                当key在左部分时,由于有重复元素,左边不一定是单调递增的序列
                比如:3,3,3,1,1
            **/
            if( array[left]<=key && key < array[mid] )
                right=mid;
            else
                left=mid+1;
        }
        /**当切到右部分递增序列内**/
        else if( array[mid] < array[left] )
        {
            /**
                当key在右部分时,由于有重复元素,右边不一定是单调递增序列
                比如:1,3,1,1,1
            **/
            if( array[mid]<key && key<=array[right] )
                left = mid+1;
            else
                right = mid;
        }
        /**
            当切到的点使得左边或者右边其中一边并不是递增序列
            则left++再看一步
        **/
        else
            left++;

    }
    return -1;//没找着
}

 

【例2】在旋转排序数组中找最小值并输出,数组元素允许重复

int findMinInOrder(int* array, int left, int right)
{
    int min_number = array[left];
    int i;
    for(i=left+1; i<=right; i++)
        if(min_number > array[i])
            min_number = array[i];
    return min_number;
}
/**寻找旋转排序数组的最小数字,数组里有重复元素。
例如1,0,1,1,1和1,1,1,0,1都是0,1,1,1,1的旋转数组
**/
int findSmallestNumber(int* array, int len)
{
    int left = 0;
    int right = len-1;
    int mid;

    while(left < right)
    {
        mid = (left+right)/2;

        if(array[left] == array[mid] && array[right] == array[mid])
            return findMinInOrder(array, left, right);

        if(array[mid]>=array[left])
            left = mid;
        if(array[mid]<=array[right])
            right = mid;

        if(right-left==1)
            return array[right];
    }
    return -1;
}

六、二分查找变种4 ----有序数组,数组中只有一个元素出现了一次,其他元素均出现两次,要求找到这个只出现了一次的元素的位置

【解题思路】

1)对数组做二分得到中间下标index
2)当index为奇数时,说明它在数组的偶数位(数组下标从0开始),则将它与前一位数做比较array[index]?=array[index-1]
    a)若相等,说明只出现一次的数在右边,更新left=mid+1;
    b)若不等,说明只出现一次的数在左边,更新right=mid-1;
3)当index为偶数时,说明它在数组的奇数位(数组下标从0开始),则将它与后一位数做比较array[index]?=array[index+1]
    a)若相等,说明只出现一次的数在右边,更新left=mid+2;
    b)若不等,说明只出现一次的数在左边,更新right=mid;
查找的时间复杂度为O(nlogn)

【本题条件】

  1. 有序数组
  2. 其他元素都出现两次
  3. 待查元素只出现一次
/*************************************************
author:tmw
date:2018-8-29
*************************************************/
#include <stdio.h>
#include <stdlib.h>

int findNumberAppearOnce_binarySearch( int* array, int left, int right )
{
    if( array == NULL || left > right ) return -1;
    while( left <= right )
    {
        int mid = (left+right)/2;

        /**当mid为偶数,则在奇数位,与后一个元素比较**/
        if( mid%2==0 )
        {
            /**target值在右边**/
            if( array[mid] == array[mid+1] )
                left = mid+2;
            else /**target值在左边**/
                right = mid;
        }
        /**当mid为奇数,则在偶数位,与前一个元素比较**/
        else
        {
            /**target值在右边**/
            if( array[mid] == array[mid-1] )
                left = mid+1;
            else /**target值在左边**/
                right = mid-1;
        }

        if(left==right)
            return left;
    }
    return -1;
}

高能预警--接下来的变种题型类似,请保持头脑清醒,最好在稿纸上用测试例想一遍

七、二分查找变种5 ----有序数组,元素有重复,查找最后一个小于等于target的元素

/****************************************
author:tmw
date:2018-8-30
*****************************************/
#include <stdio.h>
#include <stdlib.h>

/** 查找最后一个小于等于target的元素 **/
int findLastEqualOrSmaller(int* array, int left, int right, int target) {

    if( array==NULL ) return -1;
    /*** 这里必须是 <=  因为当测试例为{1,2,2,2,3,3,5}target=4时,会检测到5比4大,返回3***/
    while (left <= right) {
        int mid = (left + right) / 2;

        if( array[mid] <= target )
            left = mid + 1;
        else
            right = mid - 1; //一旦大于,则right回退
    }
    return right;
}

八、二分查找变种6 ----有序数组,元素有重复,查找最后一个小于target的元素

/** 查找最后一个小于target的元素 **/
int findLastSmaller(int* array, int left, int right, int target)
{
    if( array == NULL ) return -1;

    while( left <= right )
    {
        int mid = (left+right)/2;

        if( array[mid] < target )
            left = mid+1;
        else
            right = mid-1;
    }
    return right;
}

九、二分查找变种7 ----有序数组,元素有重复,查找第一个大于等于target的元素

/** 查找第一个大于等于target的元素 **/
int findFirstLargerAndEqual( int* array, int left, int right, int target )
{
    if( array == NULL ) return -1;

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

十、二分查找变种8 ----有序数组,元素有重复,查找第一个大于target的元素

/** 查找第一个大于target的元素 **/
int findFirstLarger( int* array, int left, int right, int target )
{
    if( array == NULL ) return -1;

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

 

不论做什么样的变种,在二分查找中,要么是对判断条件(边界)的增加;要么是对游标left和right变化情况的改动,还有注意一点就是,while循环里的left<=right里等号是否取也会影响下面各个if中等号的选取,使用的时候,脑子要保持清醒不要慌不要慌不要慌重要的事情说三遍 [捂脸][捂脸][捂脸]。。。

 

 

最后还是那句话:梦想还是要有的,万一实现了呢~~~~ヾ(◍°∇°◍)ノ゙~~~

 

 

 

 

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值