第4章 二分查找
4.1 算法解释
二分查找也常被称为二分法或者折半查找,每次查找时通过将带查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度尾O(n)的数组,二分查找的事件复杂度尾O(log n)。
举例来说,给定一个排好序的数组[3, 4, 5, 6, 7],我们希望查找4在不在这个数组内。第一次折半时考虑中位数5,因为5大于4,所以如果4存在于这个数组,那么其必定存在于 5左边这一半。于是我们的查找区间变成了[3, 4, 5]。第二次折半时考虑新的中位数4,正好是我们需要查找的数字。于是我们发现,对于一个长度为5的数组,我们只进行了2次查找。如果是遍历数组,最坏的情况则需要查找5次。
我们也可以用更加数学的方式定义二分查找。给定一个在[a, b]区间内的单调函数f(x),若f(a)和f(b)正负性相反,那么必定存在一个解c,使得f(c)=0。在上个例子中,f(x)是离散函数f(x)=x+2,查找4是否存在等价于求f(x)-4=0是否有离散解。因为f(1)-4=3-4=-1<0,f(5)-4=7-4=3>0,且函数在区间内单调递增,因此我们可以利用二分法查找求解。如果最后二分到不能再分的情况,如果只剩一个数字,且剩余区间里不存在满足条件的解,则说明不存在离散解,即4不在这个数组内。
具体到代码上,二分查找时区间的左右端取开区间或闭区间绝大多数时候都可以,建议左闭右开。
4.2 求开方
-
Sqrt(x) (Easy)
题目描述
给定要给非负整数,求它的开方,向下取整。
输入输出样例
input: 8 output: 2
8开方的结果是2.82842...,向下取整既是2。
题解
这道题可以理解为给定一个非负整数a,求f(x)=X^2 - a=0的解。因为只考虑x>=0,所以在定义域上是单调递增的。由于f(0)=-a<=0,f(a)=a^2 -a >=0,对区间[0, a]内使用二分法找到f(x)=0的解。
为了防止除以0,a=0单独考虑,然后对区间[1, a]进行二分查找。
private static int mySqrt1(int a){ if(a==0) return 0; int left=1, right=a, mid, sqrt; while(left<=right){ mid = left + (right-left)/2; sqrt = a/mid; if(sqrt==mid){ return mid; }else if(mid>sqrt){ right = mid-1; }else{ left = mid+1; } } return right; }
还有一种更快的算法——牛顿迭代法,其公式为Xn+1 = Xn - f(Xn)/f`(Xn)。给定f(x) = x^2 - a = 0,这里的迭代公式为Xn+1 = (Xn + a/Xn)/2:
private static int mySqrt2(int a){ long x = a; while(x*x > a){ x = (x + a/x)/2; } return (int)x; }
4.3 查找区间
-
Find First and Last Position of Element in Sorted Array(Medium)
题目描述
给定一个增序的整数数组 和一个值,查找该值第一次和最后一次出现的位置。
输入输出样例
输入是一个数组和一个值,输出为该值第一次出现的位置和最后一次出现的位置(从0开始),如果不存在则返回-1。
input: nums = [5, 7, 7, 8, 8, 10], target = 8 output: [3, 4]
数字8在索引 3第一次出现,索引4最后一次出现。
题解
感觉这道题像是java中的indexOf()和lastIndexOf(),但是使用的算法不同。
private static int[] searchRange(int[] nums, int target){ if(nums.length==0) return new int[]{-1, -1}; int first = first_index(nums, target); int last = last_index(nums, target) - 1; if(first==nums.length||nums[first]!=target){ return new int[]{-1, -1}; } return new int[]{first, last}; } private static int first_index(int[] nums, int target){ int left=0, right=nums.length, mid; while (left<right){ mid = (left+right)/2; if(nums[mid]>=target){ right = mid; }else{ left = mid+1; } } return left; } private static int last_index(int[] nums, int target){ int left=0, right=nums.length, mid; while (left<right){ mid = (left+right)/2; if(nums[mid]>target){ right = mid; }else{ left = mid+1; } } return left; }
4.4 旋转数组查找数字
-
Search in Rotated Sorted Array II(Medium)
题目描述
一个原本增序的数组被首尾相连后按某个位置断开(如[1,2,2,3,4,5] -> [2,3,4,5,,1,2],在第一位和第二位断开),我们称其为旋转数组。给定一个值和一个旋转数组,判断这个值是否存在于这个旋转数组中。
输出输出样例
输入是一个数组和一个值,输出是一个布尔值,表示数组中是否存在该值。
input: nums = [2, 5, 6, 0, 0, 1, 2], target = 0 output: true
题解
即使数组被旋转过,我们仍然可以利用这个数组的递增性(其仍为单调递增),使用二分查找。对于当前的中点,如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对另一半区间继续二分查找。
注意,因为数组存在重复数字,如果中点和左端的数字相同,我们不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。
private static boolean search(int[] nums, int target){ int start=0, end=nums.length-1, mid; while (start<=end){ mid = (start+end)/2; if(nums[mid]==target){ return true; } if(nums[mid]==nums[start]){ // 无法判断哪个区间是增序的 start++; }else if(nums[mid]<=nums[end]){ // 右区间是增序的 if(target<nums[mid] && nums[end]<=nums[mid]){ start = mid + 1; }else{ end = mid - 1; } }else{ // 左区间是增序的 if(nums[start]<=target &&target<nums[mid]){ end = mid - 1; }else{ start = mid + 1; } } } return false; }
4.5 练习
基础难度
-
Find Minimum in Rotated Sorted Array II(Medium)
题目描述
旋转数组的变形题之一。
假设以升序排序的数组在事先未知的某个轴上旋转。
(即[0,1,2,4,5,6,7]可能会变成[4,5,6,7,0,1,2])。
找到最小的元素。
该数组可能包含重复项。
输入输出样例:
input: nums=[4,5,6,7,0,1,2] output: 0
题解
private static int minimumInRotatedArray(int[] nums){ int left=0, right=nums.length-1, mid=-1; while (left<=right){ mid = (left+right)/2; if(nums[mid]==nums[left]){ // 无法判断哪个区间是递增的 left++; }else if(nums[mid]<=nums[right]){ // 右侧区间是递增的 left = mid; }else{ // 左侧区间是递增的 right = mid; } } return Math.min(nums[left], nums[right]); }
-
Single Element in a Sorted Array(Medium)
题目描述
在出现独立数之前和之后,奇偶位数的值发生了什么变化?
给定一个仅由整数组成的排好序的数组,其中每个元素仅出现两次,只有一个元素除外。
找到只出现一次的这个元素。
提示:您的解决方案应在O(log n)时间和O(1)空间中运行。
样例输入输出
Input: nums = [1,1,2,3,3,4,4,8,8] Output: 2
题解
private static int singleNonDuplicate(int[] nums){ int left=0, right=nums.length-1, mid; while(left < right){ mid = (left+right)/2; // 在右侧 if ((nums.length-1-mid)%2==1){ if(nums[mid]==nums[mid + 1]){ right = mid; }else{ left = mid+1; } // 在左侧 } else{ if(nums[mid]!=nums[mid + 1]){ right = mid; }else{ left = mid+1; } } } return nums[left]; }
进阶难度
-
Median of Two Sorted Arrays(Hard)
题目描述
需要对两个数组同时进行二分搜索。