LeetCode704. 二分查找
https://leetcode-cn.com/problems/binary-search/
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
思路
这道题目中的数组是有序数组,这也是使用二分查找的基础条件。只要题目中的数组是有序数组,都可以考虑能否使用二分法。
二分查找最重要的是处理好边界条件。
- 首先,我们确定一个左闭右闭的区间,也就是[left, right] 。
- 然后,我们确定 int left = 0, right = sz -1,保证一个左闭右闭的区间,也就是[left, right] 。
- 最后,循环结束条件为 while (left <= right),因为当left==right,区间[left, right]依然有效。
代码
class Solution
{
public:
int searchInsert(vector<int>& nums, int target)
{
int size = nums.size();
if (size == 0) return -1;
//保证左闭右闭的区间,也就是[left, right]
int left = 0,right = size - 1;
//当left==right,区间[left, right]依然有效
while(left <= right)
{
//防止溢出,等同于(left + right)/2
int mid = left + (right - left) / 2;
//命中
if (target == nums[mid])
{
return mid;
}
//target在右区间,所以[mid + 1, right]
else if (target > nums[mid])
{
left = mid + 1;
}
//target在左区间,所以[left, mid - 1]
else
{
right = mid - 1;
}
}
return -1;
}
};
LeetCode35. 搜索插入位置
https://leetcode-cn.com/problems/search-insert-position/
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
输入: [1,3,5,6], 5
输出: 2
输入: [1,3,5,6], 2
输出: 1
思路
本题与LeetCode704. 二分查找基本相同,唯一的区别是当目标值不存在于数组中,本题返回它将会被按顺序插入的位置,而LeetCode704. 二分查找返回-1。
按照LeetCode704. 二分查找的思路,如果目标值不存在与数组中,那么目标值将会被按顺序插入的位置是left。
代码
class Solution
{
public:
int searchInsert(vector<int>& nums, int target)
{
int size = nums.size();
if (size == 0) return -1;
//保证左闭右闭的区间,也就是[left, right]
int left = 0,right = size - 1;
//当left==right,区间[left, right]依然有效
while(left <= right)
{
//防止溢出,等同于(left + right)/2
int mid = left + (right - left) / 2;
//命中
if (target == nums[mid])
{
return mid;
}
//target在右区间,所以[mid + 1, right]
else if (target > nums[mid])
{
left = mid + 1;
}
//target在左区间,所以[left, mid - 1]
else
{
right = mid - 1;
}
}
return left;
}
};
LeetCode34. 在排序数组中查找元素的第一个和最后一个位置
https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
思路
本题大体思路与LeetCode704. 二分查找相同。
在LeetCode704. 二分查找的基础上,主要要解决两个问题:
- 如何搜索左侧边界
关键在于对于 nums[mid] == target 这种情况的处理:
if (nums[mid] == target)
right = mid-1;
找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid-1] 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
注意越界检查:由于 while 的退出条件是 left == right + 1,所以当 target 比 nums 中所有元素都大时,left会被加到到 size,所以需要在最后防止越界。
- 如何搜索右侧边界
同理,关键点还是这里:
if (nums[mid] == target)
left = mid + 1;
当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,在区间[mid+1,right]中继续搜索,使得区间不断向右收缩,达到锁定右侧边界的目的。
注意越界检查:由于 while 的退出条件是 left == right + 1,当 target 比所有元素都小时,right 会被减到 -1,所以需要在最后防止越界。
代码
class Solution
{
private:
int findFirstPosition(vector<int> &nums, int target)
{
int size = nums.size();
int left = 0;
int right = size - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (target == nums[mid])
{
// 不可以直接返回,应该继续向左边找,即在区间 [left, mid - 1] 里找
right = mid - 1;
}
else if (target > nums[mid])
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
//当target比nums中所有元素都大时, left会越界, 所以要做越界判断
if (left != size && nums[left] == target)
{
return left;
}
return -1;
}
int findLastPosition(vector<int> &nums, int target)
{
int size = nums.size();
int left = 0;
int right = size - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (target == nums[mid])
{
// 不可以直接返回,应该继续向左边找,即在区间 [mid + 1, right] 里找
left = mid + 1;
}
else if (target > nums[mid])
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
if (right != -1 && nums[right] == target)
{
return right;
}
return -1;
}
public:
vector<int> searchRange(vector<int> &nums, int target)
{
if (nums.size() == 0)
{
return vector<int>{-1, -1};
}
int firstPosition = findFirstPosition(nums, target);
// 如果第 1 次出现的位置都找不到,肯定不存在最后 1 次出现的位置
if (firstPosition == -1)
{
return vector<int>{-1, -1};
}
int lastPosition = findLastPosition(nums, target);
return vector<int>{firstPosition, lastPosition};
}
};
LeetCode410. 分割数组的最大值
https://leetcode-cn.com/problems/split-array-largest-sum/
给定一个非负整数数组 nums
和一个整数 m
,你需要将这个数组分成 m
个非空的连续子数组。
设计一个算法使得这 m
个子数组各自和的最大值最小。
输入:nums = [7,2,5,10,8], m = 2
输出:18
解释:
一共有四种方法将 nums 分割为 2 个子数组。 其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
思路
一个数组分为m段,有不同分法。要找到不同分法中各段和最大值的最小值,设最小值为x。
首先明确本题最佳解法为二分查找:即我们要在一个范围内,查找我们想要的这个值x。
1.二分查找的范围
要进行二分查找,首先要找到一个范围
这个x最小能多小?肯定大于等于整个数组的最大值
这个x最大能多大?肯定小于等于整个数组的和
所以二分查找的范围是[max(数组), sum(数组)]
2.进行二分查找
二分查找本质上就是每次测试范围的中点是大了还是小了,从而缩小查找的范围。
取中点mid,假设mid就是不同分法中各段和最大值的最小值,那么各段和必定 <= mid。
使用mid值,对数组进行分割:一旦当前段 > mid,就结束该段,开启一个新段。
如果段数 > m,则mid取小了,取右半边继续查找;
如果段数 < m,则mid取大了,取左半边继续查找。
当查找范围只剩一个数的时候,结束二分查找。
3.注意
如果段数已经等于m了, 此时若查找范围仍然不止一个数,查找范围还是会继续收敛,且取的是左半边,目的是让我们能最终找到一个确切的值,这个值恰好就是取得了最大值的那个数组的和。
代码
class Solution
{
public:
int splitArray(vector<int>& nums, int m)
{
int size = nums.size();
int left = *max_element(nums.begin(), nums.end());
int right = accumulate(nums.begin(), nums.end(), 0);
while (left <= right)
{
int mid = left + (right - left) / 2;
//计数器初始化为1,因为无法记录到最后一段
int count = 1;
int sum = 0;
for (int i = 0; i < size; ++i)
{
sum = sum + nums[i];
if (sum > mid)
{
++count;
sum = 0;
--i;
}
}
if (count > m)
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
return left;
}
};