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);
        }        
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值