【一刷】代码随想录算法训练营Day2|数组篇:有序数组的平方,长度最小的子数组(滑动窗口),螺旋矩阵(模拟行为),区间和(前缀和),开发商购买土地,数组总结篇【Rikka】

目录

有序数组的平方

文章讲解

视频讲解

现学现练:Leetcode977

暴力解法

双指针法(重点掌握)

长度最小的子数组(滑动窗口)

文章讲解

视频讲解

现学现练:Leetcode209

暴力解法

滑动窗口(重点掌握)

螺旋矩阵(模拟行为)

文章讲解

视频讲解

现学现练:Leetcode59

区间和(前缀和)

文章讲解

现学现练

暴力解法

前缀和法

开发商购买土地

文章讲解

现学现练(使用前缀和与不使用前缀和的两种方法)

方法一(使用前缀和)

方法二(不用前缀和)

数组总结篇

文章讲解(卡哥的网站总结的超级详细)

飞雷神标记(提醒一下,留给二刷)

长度最小的子数组(滑动窗口)

螺旋矩阵(模拟行为)

今日心得


有序数组的平方

文章讲解

数组4.有序数组的平方

视频讲解

双指针法经典题目 | LeetCode:977.有序数组的平方

现学现练:Leetcode977

题目链接

暴力解法

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) //这是一个函数,返回的类型为vector<int>,就和int类型的函数返回的是一个int型是一样的,写return的时候要注意
    {
        for(int i=0;i<nums.size();i++)
        {
            nums[i]=nums[i]*nums[i];
        }
        sort(nums.begin(),nums.end());//数组排序
        return nums;
    }
};

代码详解

1.思路:暴力解法就是对数组的每个元素进行平方构成新数组,然后再对这个新数组进行排序

2.时间复杂度:O(n+nlogn),也可以记作O(nlogn),为了便于与接下来的双指针法进行对比,就记为O(n+nlogn)

3.这个解法的缺陷:太粗暴以至于时间复杂度较高,也没有利用到题目中数组nums是非递减顺序的条件。

4.有个小马虎稍稍注意一下,for循环里面写i=0的时候别忘了如果没定义要加上int,别把int漏掉了实在不行你就在循环外面先定义int i防止自己遗忘。

双指针法(重点掌握)

class Solution {
public:
//同样是双指针,代码随想录把这个单独列出来了,这个双指针和快慢有所不同了,是一个左边一个右边往中间逼近
    vector<int> sortedSquares(vector<int>& nums) {
     //用vector单独创建一个数组
     //创建数组大小为nums.size()
     vector<int> rikka(nums.size(),0);
     int i=0;
     int j=nums.size()-1;
     int k=nums.size()-1;
     //因为原来的数组是非递减的,所以平方最大的数要么在左边,要么在最右边【学会分析并利用题目条件!】
     while(i<=j)
     {
     if(nums[i]*nums[i]<=nums[j]*nums[j])
     {
        rikka[k--]=nums[j]*nums[j];
        j--;
     }
     else
     {
        rikka[k--]=nums[i]*nums[i];
        i++;
     }
     }
     return rikka;
    }
};

代码详解

1.题目中明确提到了整数数组nums是按照非递减顺序的,比如我随便写一个数组{-3,-2,-1,0,1,2,5},他要求每个数字的平方构成的新数组也是按照非递减顺序的,那么原来的数组一定可以确定一个平方最大的数在最左端或者最右端,然后把它放在新数组的最右端。新数组的数是从右往左慢慢填满的。

2.根据上述分析,就可以设置双指针(之前我们学的叫快指针和慢指针,这里有所不同是两个指针在两端向中间逼近的,我喜欢叫它们左指针和右指针),一个在原数组最左边一个在原数组最右边来保证每一次遍历都能找到一个符合要求的数来填入新数组,填入之后再移动其中一个指针,另一个指针不动,按照要求构造循环即可。

3.自己需要额外建立一个新数组不要忘记哦

4.前面我们利用左指针和右指针找到了其中一个符合条件的元素,我们也建立了一个大小为nums.size()的新数组,要把这个元素放入新数组也就是要找到对应的下标,这个下标随着元素一个个放入慢慢向左边逼近也就是自减了,所以我们设置了索引k。

5.时间复杂度:O(n)

长度最小的子数组(滑动窗口)

文章讲解

数组5.长度最小的子数组

视频讲解

拿下滑动窗口! | LeetCode 209 长度最小的子数组

现学现练:Leetcode209

题目链接

暴力解法

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result=INT32_MAX;
        int sublength=0;
        for(int i=0;i<nums.size();i++)
        {
            int 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;
    }
};

代码详解

1.最暴力方法就是用两个for循环不断遍历数组来寻找符合条件的子数组,再从这些符合条件的子数组中找到长度最小的那一个并将长度输出。

2.这个int sum=0的位置不要粗心大意,是写在最外面的那层for循环外面还是里面呢?因为每一次i++之后j都要刷新到i的位置,sum会重新变化不再是之前记录的值,所以要把int sum=0写在循环里面。

3.我们要不断将符合条件的子数组的长度进行比较并输出长度最小的那一个,那么就有一个问题:怎么比较?比如有我手头有三个符合条件的子数组A,B,C,现在我又要得到一个符合条件的子数组D想得到最小值,我是将D的长度依次与A,B,C的长度比较还是与A,B,C中长度的最小值进行比较?结果显而易见是后者。那也就是说随着我得到子数组,我要不断更新我已经得到的最小值。这个值与sublength比较之后得到新的最小值存储在result当中并与后面得到的sublength比较,这个时候就可以用到条件操作符 exp1 ? exp2 : exp3了。

4.如果能找到符合条件的子数组就输出result,不能就输出0。

5.时间复杂度:O(n^2),空间复杂度O(1)

6.INT_MAX 和 INT_MIN 是 C++ 的两个宏,代表了整型变量能够存储的最大正整数和最小负整数,分别为 2147483647 和 -2147483648,这两个宏在头文件 <limits.h> 中定义。

ps:这个解法放在leetcode中超时了,不用担心代码错误。

滑动窗口(重点掌握)

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result=INT32_MAX;//是C++的一个宏,代表了整型变量能存储的最大整数
        //result代表了我要返回的长度,如果存在就给他赋值,如果不存在就仍然是INT32_MAX
        int sum=0;
        int i=0;
        int length=0;//滑动窗口长度
        for(int j=0;j<nums.size();j++)
        {
            sum+=nums[j];
            while(sum>=target)
            {
                length=j-i+1;//记录,然后与后面满足条件的length比较,取小的
                result=result>length?length:result;
                sum-=nums[i];
                i++;
                //给result在满足条件的循环里面,不存在就赋值不了
            }
        }
        //易错,赋值和等于别搞混
        return result==INT32_MAX?0:result;
    }
};

代码详解

1.暴力解法需要使用两个for循环,滑动窗口只使用一个for循环来进行优化。二者都是不断调节子数组的起始位置和终止位置来获得新数组并判断这个新数组是否符合要求。

2.既然只允许用一个for循环,那么对滑动窗口来说这个for循环应该移动终止位置,因为如果是移动起始位置,那就又要一个一个往后找终止位置,那就和暴力解法本质上没有什么区别了。

3.既然已经决定for循环是拿来移动终止位置的,那么起始位置如何移动?这是解决本题的关键!

窗口内的子数组是要满足其总和>=target的,因为题目明确说明这个数组里面的元素都是正整数,所以可以将这个窗口的起始位置向右边移动一格,得到一个新的窗口(子数组),这个子数组的和以及长度是一定小于上一个子数组的,如果仍然能满足条件就可以将长度记录下来。

4.滑动窗口的巧妙之处就在于根据当前子序列大小的情况,不断调节子序列的起始位置。

5.时间复杂度:O(n)【理解成O(2*n)更好理解】,空间复杂度O(1)

6.滑动窗口本质上也是一种双指针法,是在充分理解题目的情况下对暴力解法的优化。这道题目在使用滑动窗口时,终止位置在向后移动,而在这个过程中起始位置是“不用回头”的,不用再从0开始。而暴力解法中随着起始位置向后移动,终止位置都要重新从起始位置开始移动。

卡哥精华:滑动窗口的本质:是满足了单调性,即左右指针只会往一个方向走且不会回头。收缩的本质即去掉不再需要的元素。

螺旋矩阵(模拟行为)

文章讲解

数组6.螺旋矩阵

视频讲解

一入循环深似海 | LeetCode:59.螺旋矩阵II

现学现练:Leetcode59

题目链接

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> ans(n, vector<int>(n));//创建一个行与列均为n的二维数组
        int startx=0;
        int starty=0;
        int offset=1;
        int count=1;
        int loop=n/2;
        while(loop--)
        {
            int i=startx;
            int j=starty;
            for( ;j<n-offset;j++)
            {
                ans[i][j]=count++;
            }
            for( ;i<n-offset;i++)
            {
                ans[i][j]=count++;
            }
            for( ;j>starty;j--)
            {
                ans[i][j]=count++;
            }
            for( ;i>startx;i--)
            {
                ans[i][j]=count++;
            }
            startx++;
            starty++;
            offset++;
        }
        if(n%2!=0)
        {
            ans[n/2][n/2]=n*n;
        }
        return ans;
    }
};

代码详解

1.首先要利用vector自己创建一个二维数组,再把数count放进去。

2.分析要转几圈以及中心点怎么处理:已经创建了一个n行n列的二维数组(矩阵),首先可以判断要循环的圈数为n/2,比如一个4行4列(n为偶数)的二维数组要循环2圈;一个3行3列(n为奇数)的二维数组要循环1圈,但此时会发现中心会额外空出一个元素,这个中心[n/2][n/2]要单独作判断并对其进行赋值,值可以是count的最终值也可以是n*n。这里我犯了一个小错误,我想表示n的平方一开始写的是n^2,因为数学用多了就这么认为了,但是c语言不一样,c语言中^并不是数学中的平方而是异或运算符。

3.分析每一圈:每一圈按照顺时针顺序有上,右,下,左四条边,每一条边我们要按统一规则进行处理,也就是二分查找中提到的循环不变量法则,不然做的过程中非常容易混乱。每一条边我们按照左闭右开的原则进行处理。

比如最上面这条边,我们只处理[0][0]和[0][1]这两个元素,而[0][2]这个元素我们交给右边这条边处理。

4.对于每一个圈,要确定圈的起始位置(圈的左上角)。在处理完第一圈处理下一圈时,同样是最上面的那条边,起始位置和终止位置也是不一样的,而且我们在遍历每一条边时选择的起点也是由圈的起始位置决定的。这个时候就要用到startx和starty

比如说这个红线圈,它的起始位置就是[0][0],而到了绿色圈起始位置就是[1][1],这也就意味着我们不要忘记了startx++和starty++的操作。

5.遍历每一条边:以i和j表示遍历过程中在边内所在的位置。遍历一条边的时候i和j只有一个是变化的,这个变化的范围怎么判断呢?以红圈的最上面的边举例,i=0(由起始位置决定),j取值为0~3,故j<4=5-1;在绿圈的最上面的边中,i=1(由起始位置决定),j取值为1~2,故j<3=5-2;所以我们还要设置一个变量offset来控制一条边遍历的长度,每一圈循环结束后offset都要加1来达到边界收缩的效果:起始位置向后移,终止位置向前移。

6.思考一下int i=startx,int j=starty写在循环内与循环外有什么区别?

区间和(前缀和)

文章讲解

数组7.区间和

现学现练

题目链接

暴力解法

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    int n,a,b;
    cin>>n;
    vector<int> Array(n);//建立一个长度为n的数组Array
    for(int i=0;i<n;i++)
    {
        cin>>Array[i];//输入数组Array的元素
    }
    while(cin>>a>>b)
    {
        int sum=0;
        for(int j=a;j<=b;j++)
        {
            sum=sum+Array[j];
        }
        cout<<sum<<endl;
    }
}

前缀和法

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    int n,a,b;
    int sum=0;
    cin>>n;//设置数组的长度
    vector<int> Array(n);
    vector<int> preArray(n);
    for(int i=0;i<n;i++)
    {
        cin>>Array[i];
        sum=sum+Array[i];
        preArray[i]=sum;
    }
    while(cin>>a>>b)
    {
        if(a!=0)
        {
            cout<<preArray[b]-preArray[a-1]<<endl;;
        }
        else
        {
            cout<<preArray[b]<<endl;
        }
    }
    return 0;
}

代码详解

1.题目提到了算区间和,而且这个区间是连续的,那么就可以应用前缀和的思想。

2.前缀和的思想是重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。

3.高中学数列有个思想有点像前缀和,比如说前n+3项的和减去前n项的和:Sn+3-Sn=an+1+an+2+an+3。关于前缀和的具体细节卡哥的代码随想录网站中解释的已经很详细了,此处不再赘述,可以点击上方链接。要理解核心点就是

4.特别注意: 在使用前缀和求解的时候,要特别注意求解区间。如果我们要求 区间下标 [2, 5] 的区间和,那么应该是 p[5] - p[1],而不是 p[5] - p[2]。

5.如果区间的起始端是0,那么直接算p[b]而不是p[b]-p[a-1],因为a-1此时为-1.

开发商购买土地

文章讲解

数组8.开发商购买土地

现学现练(使用前缀和与不使用前缀和的两种方法)

题目链接

方法一(使用前缀和)

#include <iostream>
#include <vector>
using namespace std;
int main () {
    int n, m;
    cin >> n >> m;
    int sum = 0;
    vector<vector<int>> vec(n, vector<int>(m, 0)) ;//构造一个n行m列的矩阵初始化为0
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> vec[i][j];//给矩阵内元素赋值
            sum += vec[i][j];//计算矩阵所有元素之和
        }
    }
    // 统计横向
    vector<int> horizontal(n, 0);
    //构造一个一维数组,有n个元素对应n行,每一个元素的值是对应行内所有元素之和
    for (int i = 0; i < n; i++) {
        for (int j = 0 ; j < m; j++) {
            horizontal[i] += vec[i][j];
        }
    }
    // 统计纵向
    vector<int> vertical(m , 0);
    //构造一个一维数组,有m个元素对应m列,每一个元素的值是对应列内所有元素之和
    for (int j = 0; j < m; j++) {
        for (int i = 0 ; i < n; i++) {
            vertical[j] += vec[i][j];
        }
    }
    int result = INT32_MAX;
    int horizontalCut = 0;
    //将区域按横向分块
    for (int i = 0 ; i < n; i++) {
        horizontalCut += horizontal[i];
        result = min(result, abs((sum - horizontalCut)-horizontalCut));
        //以i=0举例子
        //horizontalCut的值是下标为0的行内所有元素之和
        //sum-horizontalCut的值是下标为1与2的行内所有元素之和
        //二者相减再用abs函数取绝对值是求分块后两个子区域内土地总价值之间的差距
    }
    int verticalCut = 0;
    //将区域按纵向分块
    for (int j = 0; j < m; j++) {
        verticalCut += vertical[j];
        result = min(result, abs((sum - verticalCut)-verticalCut));
        //以j=0举例子
        //verticalCut的值是下标为0的列内所有元素之和
        //sum-verticalCut的值是下标为1与2的列内所有元素之和
        //二者相减再用abs函数取绝对值是求分块后两个子区域内土地总价值之间的差距,此时再用min会与按横向分块得到的结果进行比较得到最小的结果。
    }
    cout << result << endl;
}

时间复杂度:O(n^2)

方法二(不用前缀和)

#include <iostream>
#include <vector>
using namespace std;
int main () {
    int n, m;
    cin >> n >> m;
    int sum = 0;
    vector<vector<int>> vec(n, vector<int>(m, 0)) ;//建立一个n行m列的二维数组初始化为0
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> vec[i][j];
            sum += vec[i][j];
        }
    }

    int result = INT32_MAX;
    int count = 0; // 统计遍历过的行
    for (int i = 0; i < n; i++) {
        for (int j = 0 ; j < m; j++) {
            count += vec[i][j];
            // 遍历到行末尾,即j==m-1时开始统计
            if (j == m - 1) result = min (result, abs(sum - count - count));

        }
    }

    count = 0; // 统计遍历过的列
    for (int j = 0; j < m; j++) {
        for (int i = 0 ; i < n; i++) {
            count += vec[i][j];
            // 遍历到列末尾,即i==n-1时开始统计
            if (i == n - 1) result = min (result, abs(sum - count - count));
        }
    }
    cout << result << endl;
}

时间复杂度也为O(n^2)

数组总结篇

文章讲解(卡哥的网站总结的超级详细)

数组9.总结篇

这一章主要内容为:基础知识、二分查找、双指针法、滑动窗口、模拟行为、前缀和。双指针法是重点!

飞雷神标记(提醒一下,留给二刷)

长度最小的子数组(滑动窗口)

Leetcode904.水果成篮

Leetcode76.最小覆盖子串

螺旋矩阵(模拟行为)

Leetcode54.螺旋矩阵

剑指Offer29.顺时针打印矩阵

今日心得

我是刚学完数据结构与算法就开始一刷代码随想录,刷每一道算法题都显得比其他录友吃力,但是一遍又一遍告诉自己你一定要坚持下来啊岂可修!其实最难的环节并不是刷代码随想录,而是刷完之后总结博客。我在分享自己感悟的时候对自己要求很高,仿佛回到了以前做MAD的感觉,就和泛式做赛马娘MAD一样,这个MAD/博客可能是我最后一次做它了,就像做一个人物志一样,我就一定要把它给做好,做到让自己满意,让观众满意!这是 盛大演出!我的进度也许会显得很慢,但绝不会缺席!

我是Rikka,一只普通的私宅大学生,谢谢你能看我的博客!૮(˶ᵔ ᵕ ᵔ˶)ა

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Takanashi_Rikka_2004

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值