二分查找
题目描述 : 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
思路:将数组的中间值与 target 进行比较,如果 target 大于中间值,就去右半部分寻找,如果 target 小于中间值,就去左半部分寻找。
边界处理问题
整个数组的空间可以有左闭右开、左闭右闭的形式,首先以左闭右开为例
int left = 0;
int right = nums.size(); // 这里要指向最后一个元素的下一个位置,因为右边开区间无意义
左闭右开区间,while 判断条件为 left < right ,因为 left = right 无意义
区间的更新
当 target 在 mid 左侧时,nums[mid] 不等于 target,因为是左闭右开,要保证 mid - 1 在区间中,所以 right = mid
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid;
}
else if(nums[mid] < target){
left = mid + 1;
}
else{
return mid;
}
}
return -1;
}
};
同理,左闭右闭区间,right = nums.size() - 1(区间右端点有意义)
更新区间时,当 target 在 mid 左侧,nums[mid] 不等于 target,right = mid - 1即可,因为mid - 1在此区间中。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] > target){
right = mid - 1;
}
else if(nums[mid] < target){
left = mid + 1;
}
else{
return mid;
}
}
return -1;
}
};
移除元素
题目描述 : 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
若不考虑原地移动,暴力解法:遍历数组,遇到值等于 val 的,将 val 后面的元素依次向前移动一个位置(两层 for 循环)每次移动完,数组大小减一
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size(); // size 会变化,所以事先定义
for(int i = 0; i < size; i++){
if(nums[i] == val){
for(int j = i + 1; j < size; j++){
nums[j - 1] = nums[j];
}
size--;
i--; // i 下标以后的元素都向前移动了一位,i 也要向前移动
}
}
return size;
}
};
双指针法
通过一个快指针和一个慢指针在一个 for 循环中完成两个 for 循环做的事情
- 快指针遍历数组,寻找新数组的元素
- 慢指针决定将谁保留在新数组中
fast 指针遍历数组,如果找到的值 nums[fast] == val,只让 fast 向前移动,否则两者都会移动
而且要在移动过程中完成赋值,将 fast 遍历的旧数组的值赋给 slow 所指的新数组
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
for(int fast = 0; fast < nums.size(); fast++){
if(nums[fast] != val){
nums[slow++] = nums[fast];
}
}
return slow;
}
};
有序数组的平方
题目描述:给你一个按非递减顺序排序的整数数组 nums,返回 每个数字的平方组成的新数组,要求也按非递减顺序排序
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
暴力解法:遍历数组,求每个元素的平方,最后再进行排序
双指针法(反向双指针):因为数组是有序的,所以对数组中元素平方后,负数可能变为最大值,最大值会出现在数组两端
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
vector<int> result(nums.size(), 0); // 保存最终的结果
int k = nums.size() - 1;
while(left <= right) // 含等号,因为要处理最后两个元素
{
if(nums[left] * nums[left] < nums[right] * nums[right])
{
result[k--] = nums[right] * nums[right];
right--;
}
else
{ // 大于等于的情况,如果 left = right 情况,会走到这里
result[k--] = nums[left] * nums[left];
left++;
}
}
return result;
}
};
长度最小的子数组
题目描述:给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组
[numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
暴力解法:两个for循环,然后不断的寻找符合条件的子序列。
第一个 for 循环即遍历子数组的起点位置,第二个 for 循环确定的是子数组的终止位置
每次遍历,记录下子序列的长度并求和,最终判断最小的子序列,每轮新的子序列都要将子序列和 sum 清零
// leetcode 会超时
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = INT_MAX;
int sublength = 0; // 子序列的长度
int sum = 0; // 子序列和
for(int i = 0; i < nums.size(); i++) // 遍历,寻找符合条件的子序列
{
sum = 0;
for(int j = i; j < nums.size(); j++)
{
sum += nums[j];
if(sum >= target)
{
sublength = j - i + 1;
result = result < sublength ? result : sublength;
break;
}
}
}
return result == INT32_MAX ? 0 : result;
}
};
滑动窗口
滑动窗口采用一个 for 循环去表示子序列的起始和终止位置
通过上面的动态图可以知道,窗口内是满足 sum 大于等于 target 的最小连续子数组,当总和大于等于 target 了,记录下此长度,起始位置就可以向前移动了,窗口的结束位置就是遍历数组的指针,就是for循环里的索引
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = INT_MAX;
int i = 0; // 滑动窗口的起始位置
int subLength = 0; // 子数组的长度
int sum = 0; // 子数组的大小
for(int j = 0; j < nums.size(); j++){
sum += nums[j];
while(sum >= target)
{
subLength = j - i + 1;
result = result < subLength ? result : subLength; // 更新结果
// 更新滑动窗口的起点 (滑动窗口的精髓)
sum -= nums[i++];
}
}
return result == INT_MAX ? 0 : result;
}
};
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)