day2:第一章 数组 Part02

今日任务(2024/05/09)

  • 977.有序数组的平方
  • 209.长度最小的子数组
  • 59.螺旋矩阵II
  • 总结

977.有序数组的平方

题目建议

  • 本题关键在于理解双指针思想

题目:977. 有序数组的平方 - 力扣(LeetCode)

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums 已按 非递减顺序 排序

进阶:

  • 请你设计时间复杂度为 O(n) 的算法解决本问题

暴力解法(求平方+排序)[ 用时: 5 m 19 s ]

思路

  1. 对每个元素求平方,替代原元素
  2. 对整个数组进行排序(如果是排序,应该用vector容器)

代码

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> res(nums.size());
        int i = 0;
        for (auto& n : nums) {
            res[i++] = n * n;
        }
        sort(res.begin(), res.end());
        return res;
    }
};
  • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

    • 计算平方的遍历:: O ( n ) O(n) O(n)
    • 排序操作:: O ( n l o g n ) O(nlogn) O(nlogn)
  • 空间复杂度: O ( n ) O(n) O(n)

分析不足

  • 排序操作:增加了时间复杂度
  • 额外使用一个数组:增加了空间复杂度

区间边界法[ 用时: 18 m 30 s ]

这里应该可以称作双指针法,为了和Carl的思路区分,我又起了个名字,哈哈

思路

分析问题,发现存在三种情况:

  1. 如果数组里没有负数,则可以直接在原数组位置求平方(空间复杂度仍是 O ( 1 ) O(1) O(1)
  2. 如果数组里全是正数,则可以求平方后倒序存放(存到新数组会比较方便,但空间复杂度 O ( n ) O(n) O(n)
  3. 如果有正有负,不确定最左端和最右端的元素谁更大,还是存到新数组会更加方便
    • 设置两个下标指针lr,分别指向原始数组的左右两端(也可以理解为未处理的数据区间)
    • 倒序遍历新数组,选择大的元素填充到新数组里,更新下标指针(未处理区间)

其中,情况3的思路可以统一处理情况1、2。因此可以得到一个通用的处理思路:

  • 初始化未处理的数组区间(l, r),定义新数组res
  • 倒序遍历新数组res(每次循环填充一个元素res[i]
    • 比较区间边界元素的绝对值大小(比较绝对值可以减少运算量)
    • 将绝对值大的元素处理为平方,填充到res[i]
    • 更新区间边界
  • 输出数组

代码

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> res(nums.size());
        // 未处理区间的边界
        int l = 0, r = nums.size() - 1;
        // 倒序遍历新数组
        for (int i = res.size() - 1; i >= 0; i--) {
            if (abs(nums[l]) > abs(nums[r])) {
                res[i] = nums[l] * nums[l];
                l++;    // 更新左边界
            } else {
                res[i] = nums[r] * nums[r];
                r--;    // 更新右边界
            }
        }
        return res;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

Carl思路

[代码随想录 (programmercarl.com)](代码随想录 (programmercarl.com))

看了Carl的思路后,发现对上面的两种方法都可以进一步优化,下面列出优化后的代码:

  • 暴力解法:

    • 优化1:可以直接在原始数组上求平方,空间复杂度可以降低到 O ( 1 ) O(1) O(1)
    for (auto& n : nums) {
        n *= n;
    }
    
  • 双指针法:

    • 优化1:将左右边界定义为循环内的局部变量,空间使用有些许减少
    • 注意:我这里使用新数组的个数i来判断是否处理完,等同于判断条件l <= r
    // 倒序遍历新数组,未处理区间的边界是(l, r)
    for (int i = res.size() - 1, l = 0, r = nums.size() - 1; i >= 0; i--) {...}
    

209.长度最小的子数组

题目建议

  • 本题关键在于理解滑动窗口
  • 这个滑动窗口看文字讲解 还挺难理解的,建议大家先看视频讲解。
  • 拓展题目可以先不做。

题目:209. 长度最小的子数组 - 力扣(LeetCode)

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

进阶:

  • 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

我的思路

思路

  • 设置两个下标指针作为窗口边界(l, r)l = r = 0

  • 设置最小窗口长度len = INT_MAX

  • 遍历数组:

    • 没超过目标值:扩张右边界
    • 超过目标值:探寻最大左边界
      • 计算当前窗口大小r - l + 1,更新len
      • 缩小左边界,直至总和不满足目标
  • len == INT_MAX,输出0;否则,输出len

代码

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int len = INT32_MAX, sum = 0;
        for (int l = 0, r = 0; r < nums.size();) {
            sum += nums[r];
            // 超过目标值:探寻最大左边界
            while (l <= r && sum >= target) {
                if (r - l + 1 < len) len = r - l + 1;
                sum -= nums[l];
                l++;
            }
            // 没超过目标值:扩张右边界
            if (sum < target) r++;
        }
        return (len == INT_MAX) ? 0 : len;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)

    • 窗口移动: O ( n ) O(n) O(n)
    • 窗口收缩: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

Carl思路

[代码随想录 (programmercarl.com)](代码随想录 (programmercarl.com))

  • 数组操作中另一个重要的方法:滑动窗口
    • 怎么滑动?不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

相关题目

59.螺旋矩阵II

题目建议

  • 本题关键还是在转圈的逻辑
  • 在二分搜索中提到的区间定义,在这里又用上了。

题目:59. 螺旋矩阵 II - 力扣(LeetCode)

给你一个正整数 n ,生成一个包含 1n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

示例 1:

螺旋矩阵Ⅱ 示例1

输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

示例 2:

输入:n = 1
输出:[[1]]

提示:

  • 1 <= n <= 20

我的思路 [ 用时: 34 m 26 s ]

思路

要实现的是一个正方形矩阵,观察其几何位置可以发现以下特点:

  • 若边长是偶数,则绕圈数是n/2
  • 若边长是奇数,绕圈数也可以表示为n/2,但是**n/2圈后会剩余一个数字没有填到最中间的空格中**。
  • 每一圈的起始位置在左上角,其坐标特点是:(x, x)
  • 每一圈到方形边界的距离都是一样的,当圈数增加1,偏移的距离也增加1

根据上面的特点来思考,我们可以按照圈数来遍历,给出每圈的边界,而每圈中按照顺时针遍历4条边:

  1. 设定起始坐标(sta, sta),这里的sta其实与偏移量offset相等,且都为0**(因为第一圈相当于偏移0)**

  2. 设置边界:xy都在[offset, n - offset - 1]范围内***(注意:这里最大值必须要减一)***

  3. 设置初值填充值cnt = 1,遍历4条边,逐个填充(注意,为了防止重复,我们需要先确定每条边的形式,这里采用的是左闭右开,如图):

    • 上:x = stay[offset, n - offset)递增
    • 右:y = n - stax[offset, n - offset)递增
    • 下:x = n - stay(offset, n - offset]递减
    • 左:y = n - stax(offset, n - offset]递减

    四条边的遍历方式

  4. 遍历完一圈后更新偏移量offset和每圈起始位置(sta, sta)(加一!)

  5. 若边长是奇数,还需要填充中心

代码

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n));
        int sta = 0, offset = 0, cnt = 1;
        for (int i = 0; i < n/2; i++) {
            int x = sta, y = sta;
            // 上
            for (; y < n - offset - 1; y++) {
                res[x][y] = cnt++;
            }
            // 右
            for (; x < n - offset - 1; x++) {
                res[x][y] = cnt++;
            }
            // 下
            for (; y > sta; y--) {
                res[x][y] = cnt++;
            }
            // 左
            for (; x > sta; x--) {
                res[x][y] = cnt++;
            }
            sta++;
            offset++;
        }
        // 填充中心
        if (n % 2) res[n/2][n/2] = cnt;
        return res;
    }
};
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)

    • 因为输入的n是边长,而我们这里填充了 n 2 n^2 n2
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)

    • 因为我们创建了一个 n ⋅ n n\cdot n nnvector

Carl思路

代码随想录 (programmercarl.com)

  • 我觉得是空间复杂度是 O ( n 2 ) O(n^2) O(n2)呀,因为我们不是创建了一个 n ⋅ n n\cdot n nnvector吗?为啥Carl说是 O ( 1 ) O(1) O(1)呢?感觉这里可能有一点小问题~

相关题目

收获

这几天一直在忙,可能是太高强度了,突然有些消沉,没有心情去完成这边的任务,所以今天重新来完善一下。

关于数组专题的总结,我打算将相关的推荐题目全部完成后再做整理,先立个flag!

今日任务的难点在于双指针法以及其衍生的滑动窗口

    1. 有序数组的平方
    • 这一题的双指针法比较基础,只是按部就班地挪动区间的左右边界,逻辑比较清晰
    1. 长度最小的子数组
    • 这一题其实也是双指针法的思想,只是双指针的使用更加灵活,但是其实我觉得还蛮难理解的
    • 但是***”滑动窗口“***这个名字就非常形象(大家可以看Carl的动图感受一下),这让我觉得和溜冰的一个场景很像:
      • 假若两人手牵手,刚开始前一个人奋力向前(后一个人不动),此时两人的距离会拉大;
      • 当距离不能再大的时候(胳膊都拉直啦),前一个人的力量会带动着后一个人前进,直至距离不能再小
      • 然后前一个人再奋力前进(后一个人不动)…
      • 如此循环,直到两人滑倒冰场一端
    • 我感觉真的和这个场景很像,不知道我的语言有没有表达清楚,如果有人能get到就太好啦

    滑动窗口

    • 没超过目标值:扩张右边界
  • 超过目标值:探寻最大左边界

    1. 螺旋矩阵Ⅱ
    • 这题是我接触的第一个模拟类型的题目,个人感觉模拟是很看重场景的构建的,要找到当前场景中各个参数之间的关系是基础(比如说位置坐标的数学关系),重点的逻辑就是”模拟“本身(我的理解可能还不是很完善)
    • 有一些要注意的地方:
      1. 每圈循环内要分别处理四条边,这是要单独处理的
      2. 每条边上还得注意边界(左闭右开),否则就会重复填充
      3. 要理解”每圈偏移量“的概念,意思是这一圈离最外圈的距离

后续工作

  • 完成推荐习题
  • 完成数组专题总结
  • 本文若存在侵权,烦请指出,本人会立马删除相关内容;
  • 本文内容若有不正确或不规范指出,请大家不吝赐教~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值