一、写在前面的话
笔者在面一家非常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中等号的选取,使用的时候,脑子要保持清醒不要慌不要慌不要慌重要的事情说三遍 [捂脸][捂脸][捂脸]。。。
最后还是那句话:梦想还是要有的,万一实现了呢~~~~ヾ(◍°∇°◍)ノ゙~~~