二分查找
-
每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。
-
可以用更加数学的方式定义二分查找。给定一个在 [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 不在这个数组内。
-
按照个人习惯,区间的选定为左闭右开
求开方
题目:给定一个非负整数,求它的开方,向下取整。
Input: 8
Output: 2
一、二分查找法
化为数学公式为满足x2<8的最大整数。可以对[0,8)区间的数进行二分,然后判断轴点的平方与8的大小关系.
若大于8,取[lo,mi),若小于或等于8,取[mi,hi),经过不断的二分。最后返回lo即可。这里注意为了防止int超上界,使用long储存数据
public int mySqrt(int x) {
long lo = 0;
long hi = x;
while (lo<hi){
long mi =(hi + lo) >> 1;
if (x < mi * mi){
hi = mi;
} else {
lo = mi+1;
}
}
return (int)--lo;
}
这里会发现在边界处,即0、1处有问题,直接把特例列出来即可
public int mySqrt(int x) {
if (x == 1 || x == 0) return x;
long lo = 0;
long hi = x;
while (lo<hi){
long mi =(hi + lo) >> 1;
if (x < mi * mi){
hi = mi;
} else {
lo = mi+1;
}
}
return (int)--lo;
}
二、袖珍计算器
通过转化为其他计算方法得到,一般不使用这个方法
public int mySqrt(int x) {
if (x == 0) {
return 0;
}
int ans = (int)Math.exp(0.5 * Math.log(x));
return (long)(ans + 1) * (ans + 1) <= x ? ans + 1 : ans;
}
三、牛顿方法
化为数学公式,即求F(x)= x2 - C = 0时,x的解,根据微积分化为,xi =0.5(x0+c/x0),然后比较x0与xi的大小,直至满足要求
public int mySqrt4(int x) {
if (x == 0) {
return 0;
}
double C = x, x0 = x;
while (true) {
double xi = 0.5 * (x0 + C / x0);
if (Math.abs(x0 - xi) < 1e-7) {
break;
}
x0 = xi;
}
return (int) x0;
}
查找区间
有序数组中值的位置
给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置
Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]
一、二分查找法
经常使用的二分查找,返回的是不大于查找值的最大位置,我们用find(x)、find(x-1)+1找到的便是最后和最初出现的位置
特殊情况:数组中不含有该元素,单独列出来
public int[] searchRange(int[] nums, int target) {
long tar = target;
int a = find(nums,tar-1)+1;
int b = find(nums,tar);
if (a == nums.length || nums[a] != target){
return new int[]{-1,-1};
}
return new int[]{a,b};
}
public int find(int[] nums,long e){
int lo = 0;
int hi = nums.length;
while (lo < hi){
int mi = lo + ((hi - lo) >> 1);
//取得两者的中点
if (e < nums[mi]){
//mi处值大于e
hi = mi;
} else {
lo = mi+1;
}
}
return --lo;
}
有序数组中单一元素
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
一、二分查找法
数学上,简化为由一系列点组成的函数,其中只有一个是单独的,我们利用二分查找的办法,轴处的mi(这里不是比较大小)而是判断(mi-0)%2 == 0,以及与左一位和右一位作比较,这样共有四种情况,判断即可,最后都不满足就是找到这个数,返回即可
public int singleNonDuplicate1(int[] nums) {
int lo = 0;
int hi = nums.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
Boolean halvesAreEven = (hi - mid) % 2 == 0;
if (nums[mid + 1] == nums[mid]) {
if (halvesAreEven) {
lo = mid + 2;
} else {
hi = mid - 1;
}
} else if (nums[mid - 1] == nums[mid]) {
if (halvesAreEven) {
hi = mid - 2;
} else {
lo = mid + 1;
}
} else {
return nums[mid];
}
}
return nums[lo];
}
二、二分查找(只对偶数索引进行判断)
沿用上面的思路,每次取得的轴点为偶数(奇数的话减一)(对应的是奇数个元素),判断其与后一位的数是否相等,相等的话,独数在后面,不等的话独数在前面,最后返回lo处元素即可
public int singleNonDuplicate2(int[] nums) {
int lo = 0;
int hi = nums.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (mid % 2 == 1) mid--;
if (nums[mid] == nums[mid + 1]) {
lo = mid + 2;
} else {
hi = mid;
}
}
return nums[lo];
}
旋转数组查找数字
旋转排序数组判断值是否存在
一个原本增序的数组被首尾相连后按某个位置断开(如 [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
一、二分查找法
数学上,将其看作一个分段区间,其中区间的左边界要高于或等于右边界。进行二分查找时,轴点mi处数值与左边界作比较
但大于时,其位于左半边,小于时,其位于右半边;相等时比较特殊,把左端点右移一位,继续进行二分查找
public Boolean search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return false;
}
int lo = 0;
int hi = nums.length-1;
while (lo < hi){
int mi = (lo+hi)/2;
if (nums[mi] == target){
return true;
}
if (nums[mi] == nums[hi]){
--hi;
} else if (nums[mi] < nums[hi]){
if (target > nums[mi] && target <= nums[hi]){
lo = mi+1;
} else {
hi = mi-1;
}
} else {
if (target < nums[mi] && target >= nums[lo]){
hi = mi-1;
} else {
lo = mi + 1;
}
}
}
return false;
}
旋转排序数组中最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。请找出其中最小的元素。注意数组中可能存在重复的元素。
输入: [1,3,5]
输出: 1
一、二分查找法
数学上,将其看作一个分段区间,其中区间的左边界要高于或等于右边界。进行二分查找时,轴点mi处数值与左边界作比较
但大于时,其位于左半边,小于时,其位于右半边;相等时比较特殊,把左端点右移一位,继续进行二分查找
public int findMin1(int[] nums) {
int low = 0;
int high = nums.length - 1;
while (low < high) {
int pivot = low + (high - low) / 2;
if (nums[pivot] < nums[high]) {
high = pivot;
} else if (nums[pivot] > nums[high]) {
low = pivot + 1;
} else {
high -= 1;
}
}
return nums[low];
}