leetcode——二分查找
二分查找也常称为二分法或折半查找。对于一个长度为O(n)的数组,二分查找的时间复杂度为O(logn)。
编写此类问题的时候注意两个地方:
1.编写习惯,如左闭右闭或左闭右开等,尝试熟练一种方式。
2.思考区间只剩最后一个数或两个数的时候,是否会陷入死循环。
1.x的平方根(69)
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
题解:
我们采用左闭右闭的方法,对[0,8]之间的数进行二分查找。当mid^2<=x时,它就有可能是满足x平方根的最大整数。将其赋值给ans,然后继续尝试,直到 l > r 结束查找。
此题还有一种另一种更快的算法——牛顿迭代法,有兴趣可以自己去了解。
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
2.在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
- 0 <= nums.length <= 105
- -109 <= nums[i] <= 109
- nums 是一个非递减数组
- -109 <= target <= 109
题解:
解法为labuladong所写。
public int[] searchRange(int[] nums, int target) {
int[] res=new int[2];
if(nums.length==0||nums==null) return new int[]{-1,-1};
res[0] = findFirst(nums, target);
res[1] = findLast(nums, target);
return res;
}
private int findFirst(int [] nums, int target){
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1;
}
}
// 最后要检查 left 越界的情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
private int findLast(int [] nums, int target){
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1;
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target)
return -1;
return right;
}
3.搜索旋转排序数组Ⅱ(81)
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
示例 1:
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
示例 2:
输入: nums = [2,5,6,0,0,1,2], target = 3
输出: false
进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
题解:
因为这种旋转是把前半部分接到后边,所以nums数组中,第一个值一定是大于等于最后一个值,如题目给定序列[2,5,6,0,0,1,2] 。假设 l = 0, r = nums.length-1, mid = l + (r - l)/2, 当nums[mid] <= nums[right],则说明mid右边是增序的(非递减)。(mid这个位置一定是在后半段增序的序列里,因为左半段的任何一个值都一定大于等于第一个值,也就大于等于该数组的最后一个值。)同理,当nums[mid] > nums[right],则说明左边是增序的。
注意:当nums[l] == nums[mid] 时,是无法判断哪个区间增序的。因为我们已经判断了nums[mid] != target 所以 nums[l] != target, 我们跳过它再往前读一位进行判断即可。
class Solution {
public boolean search(int[] nums, int target) {
int l = 0, r = nums.length - 1, mid;
while(l <= r) {
mid = l + (r - l) / 2;
if(nums[mid] == target) {
return true;
}
if(nums[l] == nums[mid]) {
l++;
}else if(nums[mid] <= nums[r]) {
//右边增序(非递减)
if(target > nums[mid] && target <= nums[r]) {
l = mid + 1;
}else {
r = mid - 1;
}
}else {
//左边增序
if(target >= nums[l] && target < nums[mid]) {
r = mid - 1;
}else {
l = mid + 1;
}
}
}
return false;
}
}
4.寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素。
示例 1:
输入: [1,3,5]
输出: 1
示例 2:
输入: [2,2,2,0,1]
输出: 0
说明:
这道题是 寻找旋转排序数组中的最小值 的延伸题目。
允许重复会影响算法的时间复杂度吗?会如何影响,为什么?
题解:
有了上一道题的基础后,我们知道当 nums[mid] > nums[r] 时,左边是增序的,且其任意一个值一定大于等于数组的最后一个值,所以最小值在右边找。 当 nums[mid] < nums[r] 时, 我们知道右边是增序的,**所以右边最小的值就是nums[mid],将查找的右边界换为mid。**而当 nums[mid] == nums[r] 时, 右边的值都等于mid,但它有可能不是最小的值,mid - 1的值可能比mid还小。所以r – 后继续查找。
public int findMin(int[] nums) {
int l = 0;
int r = nums.length - 1;
while(l < r) {
int mid = l + (r - l) / 2;
if(nums[mid] > nums[r]) {
l = mid + 1;
}else if(nums[mid] < nums[r]) {
r = mid;
}else
r--;
}
return nums[l];
}
5.有序数组中的单一元素(540)
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: [3,3,7,7,10,11,11]
输出: 10
注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
题解:
分析测试数据:
如数组1 1 2 3 3 4 4 8 8
下标 0 1 2 3 4 5 6 7 8
步骤:
①单独的数2前面的同样值的下标顺序是偶奇 如1 1,下标是0 1,后面是奇偶 如 3 3,下标识3,4。
②用二分法
当mid是偶数,如果它的值和后一个相等,即偶奇,由①知,单独的数在mid右边。如果不等,则单独的数在mid左边。
当mid是奇数,如果它的值和后一个相等,即奇偶,由①知,单独的数在mid左边。如果不等,则单独的数在mid右边。
③有一种特殊情况,比如 1 1 2 3 3,mid刚好就是这个单独的数,②中不等的情况就不成立,这个时候我们再加一个条件判断它是否与前一个也不等即可。
class Solution {
public int singleNonDuplicate(int[] nums) {
int l = 0 , r = nums.length - 1, mid;
while(l < r) {
mid = l + (r - l) / 2;
if(mid % 2 == 0) {
if(nums[mid] == nums[mid + 1]) {
l = mid + 1;
}else {
if(mid > 0 && nums[mid] != nums[mid - 1])
return nums[mid];
r = mid - 1;
}
}else {
if(nums[mid] == nums[mid + 1]) {
r = mid - 1;
}else {
if(mid > 0 && nums[mid] != nums[mid - 1] )
return nums[mid];
l = mid + 1;
}
}
}
return nums[l];
}
}
6.寻找两个正序数组的中位数(4)
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。
进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
示例 3:
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000
示例 4:
输入:nums1 = [], nums2 = [1]
输出:1.00000
示例 5:
输入:nums1 = [2], nums2 = []
输出:2.00000
提示:
- nums1.length == m
- nums2.length == n
- 0 <= m <= 1000
- 0 <= n <= 1000
- 1 <= m + n <= 2000
- -106 <= nums1[i], nums2[i] <= 106
题解:
来自leetcode评论,id Wait想念
这道题让我们求两个有序数组的中位数,而且限制了时间复杂度为O(log (m+n)),看到这个时间复杂度,自然而然的想到了应该使用二分查找法来求解。那么回顾一下中位数的定义,如果某个有序数组长度是奇数,那么其中位数就是最中间那个,如果是偶数,那么就是最中间两个数字的平均值。这里对于两个有序数组也是一样的,假设两个有序数组的长度分别为m和n,由于两个数组长度之和 m+n 的奇偶不确定,因此需要分情况来讨论,对于奇数的情况,直接找到最中间的数即可,偶数的话需要求最中间两个数的平均值。为了简化代码,不分情况讨论,我们使用一个小trick,我们分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个,然后求其平均值即可,这对奇偶数均适用。加入 m+n 为奇数的话,那么其实 (m+n+1) / 2 和 (m+n+2) / 2 的值相等,相当于两个相同的数字相加再除以2,还是其本身。
这里我们需要定义一个函数来在两个有序数组中找到第K个元素,下面重点来看如何实现找到第K个元素。首先,为了避免产生新的数组从而增加时间复杂度,我们使用两个变量i和j分别来标记数组nums1和nums2的起始位置。然后来处理一些边界问题,比如当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了。还有就是如果K=1的话,那么我们只要比较nums1和nums2的起始位置i和j上的数字就可以了。难点就在于一般的情况怎么处理?因为我们需要在两个有序数组中找到第K个元素,为了加快搜索的速度,我们要使用二分法,对K二分,意思是我们需要分别在nums1和nums2中查找第K/2个元素,注意这里由于两个数组的长度不定,所以有可能某个数组没有第K/2个数字,所以我们需要先检查一下,数组中到底存不存在第K/2个数字,如果存在就取出来,否则就赋值上一个整型最大值。如果某个数组没有第K/2个数字,那么我们就淘汰另一个数字的前K/2个数字即可。有没有可能两个数组都不存在第K/2个数字呢,这道题里是不可能的,因为我们的K不是任意给的,而是给的m+n的中间值,所以必定至少会有一个数组是存在第K/2个数字的。最后就是二分法的核心啦,比较这两个数组的第K/2小的数字midVal1和midVal2的大小,如果第一个数组的第K/2个数字小的话,那么说明我们要找的数字肯定不在nums1中的前K/2个数字,所以我们可以将其淘汰,将nums1的起始位置向后移动K/2个,并且此时的K也自减去K/2,调用递归。反之,我们淘汰nums2中的前K/2个数字,并将nums2的起始位置向后移动K/2个,并且此时的K也自减去K/2,调用递归即可。
关于为什么要赋最大值的问题:
赋予最大值的意思只是说如果第一个数组的K/2不存在,则说明这个数组的长度小于K/2,那么另外一个数组的前K/2个我们是肯定不要的。给你举个例子,加入第一个数组长度是2,第二个数组长度是12,则K为7,K/2为3,因为第一个数组长度小于3,则无法判断中位数是否在其中,而第二个数组的前3个肯定不是中位数!故当K/2不存在时,将其置为整数型最大值,这样就可以继续下一次循环。
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int left = (m + n + 1) / 2;
int right = (m + n + 2) / 2;
return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
}
//i: nums1的起始位置 j: nums2的起始位置
public int findKth(int[] nums1, int i, int[] nums2, int j, int k){
if( i >= nums1.length) return nums2[j + k - 1];//nums1为空数组
if( j >= nums2.length) return nums1[i + k - 1];//nums2为空数组
if(k == 1){
return Math.min(nums1[i], nums2[j]);
}
int midVal1 = (i + k / 2 - 1 < nums1.length) ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
int midVal2 = (j + k / 2 - 1 < nums2.length) ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
if(midVal1 < midVal2){
return findKth(nums1, i + k / 2, nums2, j , k - k / 2);
}else{
return findKth(nums1, i, nums2, j + k / 2 , k - k / 2);
}
}