一、写在前面的话
笔者在面一家非常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)
【本题条件】
- 有序数组
- 其他元素都出现两次
- 待查元素只出现一次
/*************************************************
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中等号的选取,使用的时候,脑子要保持清醒不要慌不要慌不要慌重要的事情说三遍 [捂脸][捂脸][捂脸]。。。
最后还是那句话:梦想还是要有的,万一实现了呢~~~~ヾ(◍°∇°◍)ノ゙~~~