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

一、写在前面的话

笔者在面一家非常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; //没找到
}

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

【例如】在旋转排序数组中搜索目标值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;//没找着
}

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


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


猜你喜欢

转载自blog.csdn.net/qiki_tangmingwei/article/details/79678240