二分查找刷题
Posted 乐学乐知
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分查找刷题相关的知识,希望对你有一定的参考价值。
二分查找刷题
现在数组已经排序完成 一个重要的使用场景就是二分查找 所以这篇文章引入二分查找
我们依然从leetcode
的一个题目引入
二分查找
给定一个
n
个元素有序的(升序)整型数组nums
和一个目标值target
,写一个函数搜索nums
中的target
,如果目标值存在返回下标,否则返回-1
。示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1思路: 很明显数组已经排序好了 对于一个有序的数组的查找 我们最优的方法肯定是二分 也就是对半查找
这个题目比较简单 就直接实现了
public int search(int[] nums, int target) {
int len = nums.length;
if (len == 0) return -1;
int start = 0, end = len - 1;
//这里需要取等于的原因是 当end = start + 1的时候 中间数一直是start
while (start <= end) {
//防止溢出
int mid = start + (end - start) / 2;
if (nums[mid] == target) return mid;
//中间值偏大 我们从左侧找
if (nums[mid] > target) end = mid - 1;
else start = mid + 1;
}
//额外说一句 目前的start就是target 应在的索引
return -1;
}额外说一句: 最后的
start
就是target
没有找到下的索引值 这个十分有用 你去观察java
的api
实现会注意到 对最后的插入点 做了一个偏移 主要是考虑到 0 的负数还是 0 那么就无法区分是找到了还是没有找到 就像上一篇推文的桶排序一样 需要做偏移-start-1
而且不会溢出
二分查找
二分查找是十分简单的 但是应用确实十分广泛 有时候刷leetcode的时候都意识不到 它是一个二分查找的题目
拓展练习:
第 N 个神奇数字
寻找两个有序数组的中位数
搜索插入位置
寻找旋转排序数组中的最小值
寻找旋转排序数组中的最小值 II
搜索旋转排序数组
搜索旋转排序数组 II
我们来找几个题目实现下 练练手
寻找旋转排序数组中的最小值 II
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组
[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思路: 主要是定位区间 mid在哪个区间 然后根据区间进行不同操作
public int findMin(int[] nums) {
int len = nums.length;
int start = 0, end = len - 1;
//这里不能取相等的原因是 一旦相等说明已经找到了 在进入循环的话 start会自加
while (start < end) {
int mid = start + (end - start) / 2;
//如果两者相等 我们无法判断区间 所以将start++
if (nums[start] == nums[end]) start++;
//如果mid>end了 说明在左区间 我们之间可以将start=mid+1
else if (nums[mid] > nums[end]) {
start = mid + 1;
}
//这种情况下 说明在右区间 注意可能正好是最小值 我们不能mid-1
else if (nums[mid] < nums[start]) {
end = mid;
}
//正常提增序列 我们之间break
else break;
}
return nums[start];
}
再加一个练习:
搜索旋转排序数组 II
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组
[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思路: 我发现对于这种旋转的二分搜索思路都是类似的 就是需要定位好mid的区间
public boolean search(int[] nums, int target) {
int len = nums.length;
if (len == 0) return false;
int start = 0, end = len - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) return true;
if (nums[start] == nums[end]) start++;
else if (nums[start] <= nums[mid]) {
//这个if的条件是在左递增区间上 这个时候 end可以之间等于mid-1
if (nums[start] <= target && nums[mid] > target) end = mid - 1;
//否则的话 我们知道目标一定在mid+1...end之间 我们之间将start = mid+1
else start = mid + 1;
} else if (nums[end] >= nums[mid]) {
if (nums[end] >= target && nums[mid] < target) start = mid + 1;
else end = mid - 1;
}
}
return false;
}
我们来个难一点的应用题
最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。说明:
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
思路: 基础版本的题目显然可以用
动态规划搞定
我们用dp[8]
的int
数组来储存比当前索引小的值个数 比如示例我们生成的dp{1,1,1,2,2,4,7,7}
那么转移方程是什么样子呢?dp[i] = max(dp[0...i-1])+1
其中0...i-1
索引能参与计算的 需要满足arr[idx]<arr[i]
显然是n^2
的时间复杂度我们试着做优化:
idx=0, val=10
这时候明显len=1
我们取第一个元素 {10}
idx=1, val=9
9<10 长度依然是len=1
这个时候我们需要在9
和10
之间做一个取舍 需要使得最后的序列最长 那么我们优先保留 较小的数字 这个时候我们取值数组变成了{9}....
以此类推 那么当我们维护的数组长度达到了
n
的时候 怎么定位一个新进来的数字呢 可以使用二分查找的方式public int lengthOfLIS(int[] nums) {
int len = nums.length;
if (len == 0) return 0;
int res = 1;//第一个idx=0是我们维护的数组的长度
for (int i = 1; i < len; i++) {
//新进来的数字大于了 我们维护的最大数字 长度加一
if (nums[i] > nums[res - 1]) nums[res++] = nums[i];
//小于了 我们去做替换 保持维护的数组最优
else if (nums[i] < nums[res - 1]) {
//二分查找 很容易实现 见上面的实现
int idx = Arrays.binarySearch(nums, 0, res, nums[i]);
//api 做了偏移
//return -(low + 1); // key not found.
if (idx < 0) idx = -idx - 1;
nums[idx] = nums[i];
}
}
return res;
}这是一个
最长递增子序列
的题目 很常见拓展练习
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
俄罗斯套娃信封问题 (思路一样 不过要先排序 比如长度排序后需要注意宽度倒序 然后转化成
LIS
问题)
再来一个难一点的题目
寻找两个有序数组的中位数
给定两个大小为 m 和 n 的有序数组
nums1
和nums2
。请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设
nums1
和nums2
不会同时为空。示例 1:
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5思路: 有序数组逃不开二分查找...
长度是
(m+n)
比如4 我们找到 在 m数组和n数组分别切一刀 使得切点前的元素个数是2个 那么中位数就是(max(l1,l2) + min(r1,r2))/2
切点需要满足(L1<=R2 && L2<=R1)
m: 1(L1) <(cut)>(R1) 4
n: 2 (L2)<(cut)>(R2) 5
我们来实现下:
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len1 = nums1.length, len2 = nums2.length;
//我们二分查找小的 减少检索
if (len1 > len2) return findMedianSortedArrays(nums2, nums1);
int len = (len1 + len2 + 1) / 2;
int start = 0, end = len1 - 1;
do {
int cut1 = end == -1 ? -1 : start + (end - start) / 2;
int cut2 = len - (cut1 + 1) - 1;
//判断越界
int L1 = cut1 >= 0 && cut1 < len1 ? nums1[cut1] : Integer.MIN_VALUE;
int R1 = cut1 + 1 < len1 ? nums1[cut1 + 1] : Integer.MAX_VALUE;
int L2 = cut2 >= 0 && cut2 < len2 ? nums2[cut2] : Integer.MIN_VALUE;
int R2 = cut2 + 1 < len2 ? nums2[cut2 + 1] : Integer.MAX_VALUE;
if (L1 <= R2 && L2 <= R1) {
if ((len1 + len2) % 2 == 0) return (Math.max(L1, L2) + Math.min(R1, R2)) / 2.0;
return Math.max(L1, L2);
}
//切点位置太大 我们往左切一点
else if (L1 > R2) end = cut1 - 1;
//切点位置太小 我们往右切一点
else if (R1 < L2) start = cut1 + 1;
} while (true);
}
以上是关于二分查找刷题的主要内容,如果未能解决你的问题,请参考以下文章