算法 - Day2
双指针简介
双指针一般有两种用法。
1、在一个序列中一个指针从首端向后移动,另一个指针从尾端向前移动。
2、两个指针都从首端向后移动,但是移动速度不一样,一个移动的快,一个移动的慢(在这里是指 快指针在前“探路”,当符合某种条件时慢指针再向前移动)。
双指针的应用
下面我们通过一道题来更好的认识一下双指针的应用。
题目连接:977.有序数组的平方
这道题的意思是给你一个非递减数组,让你将其中的数平方后的数组仍然是非递减的,这道如果不知道双指针也是很容易写出来的,我们可以将数组中的每个数遍历平方后,在使用sort排序就可以了。下面给大家写一下暴力求解是如何写出来的。
vector<int> sortedSquares(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++)
{
nums[i] *= nums[i];
}
sort(nums.begin(), nums.end());
return nums;
}
我们使用暴力求解算法虽然代码简洁,但是时间复杂度却很高,时间复杂度为O(N + Nlog(N)), 因为for循环中代码执行了N次,而sort底层使用的快速排序的思想,快速排序的时间复杂度为O(Nlog(N))。
接下来我们分析一下这道题的特点,首先这到题说数组中的数是非递减排序的,我们可以假设如果数组中的数非负那么,平方之后数组中数的排序和原数组相同,但是如果有负数的话平方之后的数可能比原来正数平方后要大,所以我们可以知道,最大值一定在数组的最左端或者在数组的最右端,我们可以建立一个新数组,然后通过比较原数组中前端和后端值的平方,将大的那个放到新数组的结尾,以此类推,直到结束。
下面我们利用双指针来实现上面的思路。
vector<int> sortedSquares(vector<int>& nums) {
int k = nums.size() - 1;
vector<int> ret(nums.size(), 0);
for(int i = 0, j = nums.size() - 1; i <= j;)
{
if(nums[i] * nums[i] < nums[j] * nums[j])
{
ret[k--] = nums[j] * nums[j];
j--;
}
else
{
ret[k--] = nums[i] * nums[i];
i++;
}
}
return nums;
}
该题的思路演示如下图所示
滑动窗口的概念
滑动窗口顾名思义可以滑动的窗口,滑动窗口通常用于字符串和数组中作为字符串或者数组的一个子集来解决问题。
从字面意思上来理解滑动窗口,滑动窗口可分为滑动和窗口两部分。
滑动:说明这个窗口是可以移动的,也就是移动是按照一定方向来的。
窗口:窗口大小并不是固定的,可以不断扩容直到满足一定的条件;也可以不断缩小,直到找到一个满足条件的最小窗口;当然也可以是固定大小。
上面这张图片形象的表现了固定滑动窗口的演示流程,通过不断移动窗口,减少循环嵌套的次数,这就是滑动窗口的优点之一。
滑动窗口的应用
下面我们通过一道题来进一步了解滑动窗口及其相对于朴素解法的优势。
题目连接:209.长度最小的子数组
首先我们来分析一下这道题,这道的意思是让我们找出连续的子数组的和为target的最小子数组,如果我们没有学过滑动窗口,我们可能会使用两层循环来遍历数组,第一层循环数组的起始位置,第二层数组循环数组的位置,直到找到目标值target就停止内层循环,换一个起始位置继续寻找,直到数组起始位置遍历到数组结尾,就结束循环,找到最小的子数组的长度并返回。
下面用朴素算法来实现该题。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) { //暴力算法会超出时间限制,但是暴力解法同样也得会。
int ret = INT32_MAX;
//先将最小子数组长度设为最大值,后面比较得到最小值
int sum = 0;
int sublength = 0; //每次满足条件的数组长度
for(int i = 0; i < nums.size(); i++)
{
sum = 0;
for(int j = i; j < nums.size(); j++)
{
sum += nums[j];
//这里使用if而不是while因为从前向后找找到的第一个一定是长度最小的
if(sum >= target)
{
sublength = j - i + 1;
ret = ret < sublength ? ret : sublength;
break;
}
}
//如果ret == INT32_MAX 代表数组中就没有满足条件的子数组故返回0
return ret == INT32_MAX ? 0 : ret;
}
};
写完朴素算法后我们来看下滑动窗口是怎实现的,我们仔细观察可以发现朴素算法是通过先确定起始位置后在通过移动终止位置来寻找满足条件的子数组,这样的作法会浪费很多时间,做了很多重复的工作,而滑动窗口则不一样它是通过先确定结束位置后,通过移动起始位置来找到满足条件的子数组
下面我们来实现一下滑动窗口的代码。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int ret = INT32_MAX;
int sum = 0;
int sublength = 0;
int i = 0;
for(int j = 0; j < nums.size(); j++)
{
sum += nums[j];
while(sum >= target)
{
sublength = j - i + 1;
ret = ret < sublength ? ret : sublength;
sum -= nums[i++];
}
}
return ret == INT32_MAX ? 0 : ret;
}
};
以上就是滑动窗口的解决办法,下面我们通过一个具体的实例来模拟一下滑动窗口。本例给出数组{2,3,1,2,4,3} ,寻找最短的连续子数组要求子数组的和为7。
首先通过挪动结尾指针,找到和大于等于目标值的数组然后通过挪动起始指针来寻找最小的满足条件的子数组,然后继续向后挪动结尾指针重复之前的动作直到结尾指针移动到数组最后位置结束挪动并返回最小子数组的长度。
循环不变量巧妙解决螺旋数组问题
我们来看下这道螺旋数组的题目。
题目链接:59.螺旋数组 II
所谓螺旋数组就是在一个n x n的矩阵中,将1到n^2个数按照螺旋形态填入数组, 这时我们可以想到之前学过的循环不变量来解决这个问题,因为数组是一个n x n的矩阵所以每行每列都需要按照相同的循环规则来填充数据, 这里采用开区间的方法来进行填充, 并根据n的大小来判断需要填充的次数,如果n是奇数的话最中间是需要填数值的。
下面看下具体的代码:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ret(n, vector<int>(n, 0));
int startx = 0, starty = 0;
int loop = n / 2;//计算需要填充的次数
int mid = n / 2;//数组终中间位置
int count = 1;//每次需要填充的数
int offset = 1;//每次循环的偏移量
int i, j;
while(loop--)
{
i = startx;
j = starty;
for(j = starty; j < n - offset; j++)//先从第一行开始填充
{
ret[i][j] = count++;
}
for(i = startx; i < n - offset; i++)//再从最后一列开始填充
{
ret[i][j] = count++;
}
for(; j > starty; j--)//最后一行开始填充
{
ret[i][j] = count++;
}
for(; i > startx; i--)//第一列开始填充
{
ret[i][j] = count++;
}
startx++;
starty++;
offset++;
}
if(n % 2) ret[mid][mid] = count;//如果是奇数,填充最中间的位置
return ret;
}
};
以上就是第二节的全部内容了,喜欢的朋友们可以一键三连啊!!!