这里将对刷题笔记一文末提及的几道推荐二分法进阶题目进行说明介绍。一道简单题加了一定的文字修饰,一道中等题巧用二分查找,以下为刷题笔记一链接,题目链接在文末提供。
题目一:搜索插入位置
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
测试用例
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0
示例 5:
输入: nums = [1], target = 0
输出: 0
提示:
- 1 <= nums.length <= 104
- -104 <= nums[i] <= 104
- nums 为无重复元素的升序排列数组
- -104 <= target <= 104
解题思路
由题目提示中目标数组为升序有序无重复元素的数组且题目要求为寻找目标值以及对于时间复杂度的要求,可以清楚地认识到本题使用二分法解决问题。
解题步骤:
- 对目标数组进行二分查找,找到目标值,返回对应下标,否则,进行下一步。
- 左右全闭:
i. 对于目标值在数组所有元素之前 [0, -1]
ii. 对于目标值插入数组中的位置 [left, right],return right + 1
iii. 对于目标值在数组所有元素之后的情况 [left, right], return right + 1 - 左闭右开:
i. 目标值在数组所有元素之前 [0,0)
ii. 目标值插入数组中的位置 [left, right) ,return right 即可
iii. 目标值在数组所有元素之后的情况 [left, right),return right 即可
Java程序代码:
- 左右全闭
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0 ;
int right = nums.length-1;
int middle = left + (right-left)/2;
if(nums[left]>target){
return 0;
}
if(nums[right]<target){
return right + 1;
}
while(left<=right){
if(nums[middle]<target){
left=middle+1;
middle = left + (right-left)/2;
}
else if(nums[middle]>target){
right=middle-1;
middle = left + (right-left)/2;
}
else{
return middle;
}
}
return right+1;
}
}
- 左闭右开
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0 ;
int right = nums.length;
int middle = left + (right-left)/2;
if(nums[left]>target){
return 0;
}
if(nums[right]<target){
return right;
}
while(left < right){
if(nums[middle]<target){
left=middle+1;
middle = left + (right-left)/2;
}
else if(nums[middle]>target){
right=middle-1;
middle = left + (right-left)/2;
}
else{
return middle;
}
}
return right;
}
}
题目二:在排序数组中查找元素的第一个和最后一个位置
题目描述
给定一个按照升序排列的整数数组 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
解题思路一
题目要求的是取得目标值 target 出现的闭区间(包含且等于 target ),即左边界为第一次小于 target 值的下标加一,右边界为第一次大于目标值 target 的下标减一,如若为 {5,7,7,8,8,10}, target = 8 ,返回 {3,4}。
左右边界的获取方法可以说是一致,但需要判断方法有所不同:
- 左边界:从左到右二分查找小于目标值 target 下标
- 右边界:从左到右二分查找大于目标值 target 下标
根据题目要求寻找target在数组里的左右边界后,可分为如下几种情况:
-
target 在数组范围之外,即在数组范围的右边或者左边。
例如数组 {3, 4, 5},target 为 2 或者数组 {3, 4, 5},target 为 6,此时应该返回 {-1, -1} -
target 在数组范围中,分两种情况:tareget存在数组之中,不存在数组之中。如下:
-
数组中不存在 target。
例如数组 {3,6,7} ,target 为 5,此时应该返回 {-1, -1} -
数组中存在 target。左边界加一,右边界减一,得到区间。
例如数组 {3,6,7} ,target 为 6,此时应该返回 {1, 1}
-
了解完基本思路,我们就要去实现代码来寻找左右边界,且如果对于二分法足够清晰,可以采用同时查找左右边界的方式,如果,还只是初学者,建议先从一个边界值下手,再来处理另一个边界值。
Java 程序代码
- 分别寻找左右边界,再根据三种情况讨论结果。
class Solution {
int[] searchRange(int[] nums, int target) {
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 数组中不存在 target。
if (leftBorder == -2 || rightBorder == -2) {
return new int[]{-1, -1};
}
// 数组中存在 target
if (rightBorder - leftBorder > 1) {
return new int[]{leftBorder + 1, rightBorder - 1};
}
// target 在数组范围之外
return new int[]{-1, -1};
}
int getRightBorder(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
}
else { // 寻找右边界,nums[middle] == target的时候更新left
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
}
else {
left = middle + 1;
}
}
return leftBorder;
}
}
解题思路二
- 先采用二分法寻找 target 值在数组是否存在。
- 如存在,分别将 left 与 right 赋值为中间值,在分别移动判断是否存在重复值,直至不存在重复值得到下标返回 {left,right} 。
- 不存在,返回 {-1,-1}
注意:
-
左右边界值移动时,切忌数组下标越界,即下标小于零或大于数组长度-1,必须先判断下标不越界,才能对比值,顺序不可调换。对于条件的选择顺序应为:
-
left - 1 >= 0 && nums[left - 1] == nums[index]
-
right + 1 < nums.length && nums[right + 1] == nums[index]
-
class Solution {
public int[] searchRange(int[] nums, int target) {
int index = binarySearch(nums, target); // 二分查找
if (index == -1) { // nums 中不存在 target,直接返回 {-1, -1}
return new int[] {-1, -1}; // 匿名数组
}
// nums 中存在 targe,则左右滑动指针,来找到符合题意的区间
int left = index;
int right = index;
// 向左滑动,找左边界
while (left - 1 >= 0 && nums[left - 1] == nums[index]) { // 防止数组越界。逻辑短路,两个条件顺序不能换
left--;
}
// 向左滑动,找右边界
while (right + 1 < nums.length && nums[right + 1] == nums[index]) { // 防止数组越界。
right++;
}
return new int[] {left, right};
}
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 不变量:左闭右闭区间
while (left <= right) { // 不变量:左闭右闭区间
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1; // 不变量:左闭右闭区间
}
}
return -1; // 不存在
}
}