Day 2 - 数组

本文详细解析了LeetCode中的977.有序数组的平方问题,介绍了双指针法和暴力法的解题思路,以及209.长度最小的子数组的滑动窗口解决方案。同时讨论了螺旋矩阵Ⅱ的模拟方法,展示了在不同算法问题上的时间复杂度和空间复杂度分析。
摘要由CSDN通过智能技术生成

代码随想录算法训练营第二天 | 977. 有序数组的平方、209. 长度最小的子数组、59. 螺旋矩阵 Ⅱ

977. 有序数组的平方

文档讲解:代码随想录 | 数组 | 有序数组的平方
视频讲解:双指针法经典题目 | LeetCode:977. 有序数组的平方
状态:这个暴力法贼简单!不过双指针法确实完全没想到

题目链接:977. 有序数组的平方
解题思路-1:双指针法

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 n u m s nums nums 的长度
  • 空间复杂度: O ( 1 ) O(1) O(1),除了存储答案的数组以外,只需要维护常量空间

解题思路-2:暴力法

  • 时空间复杂度这里取决于所用的排序方法:快速排序
  • 时间复杂度: O ( n l o g ⁡ n ) O(nlog⁡n) O(nlogn),其中 n n n 是数组 n u m s nums nums 的长度;实际上是 O ( n + n l o g ⁡ n ) O(n + nlog⁡n) O(n+nlogn) ,因为快速排序前的 for 循环时间复杂度为 O ( n ) O(n) O(n)
  • 空间复杂度: O ( l o g n ) O(logn) O(logn),除了存储答案的数组以外,我们需要 O ( l o g n ) O(logn) O(logn) 的栈空间进行排序

双指针法

思路

由于负数的存在,负数平方之后可能就会成为最大数,因此数组平方后的最大值就应该在数组的两端,不是最左边就是最右边,不可能是中间
因此使用两个指针 leftright 分别指向数组的第一位最后一位 ,每次比较两个指针对应的数,选择较大的那个逆序放入答案并移动指针
在这里插入图片描述

代码

C++ 代码如下:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int left = 0;
        int right = nums.size() - 1;
        int maxIndex = nums.size() - 1;
        vector<int> result(nums.size(), 0);
        while (left <= right) {
            if (nums[left] * nums[left] > nums[right] * nums[right]) {
                result[maxIndex--] = nums[left] * nums[left];
                left = left + 1;
            } else {
                result[maxIndex--] = nums[right] * nums[right];
                right = right - 1;
            }
        }
        return result;
    }
};

Python 代码如下:

# (版本一)双指针法
class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        l, r, i = 0, len(nums)-1, len(nums)-1
        res = [float('inf')] * len(nums) # 需要提前定义列表,存放结果
        while l <= r:
            if nums[l] ** 2 < nums[r] ** 2: # 左右边界进行对比,找出最大值
                res[i] = nums[r] ** 2
                r -= 1 # 右指针往左移动
            else:
                res[i] = nums[l] ** 2
                l += 1 # 左指针往右移动
            i -= 1 # 存放结果的指针需要往前平移一位
        return res

暴力法

思路

最直观的想法,序列中每个数平方后直接快速排序

代码

C++ 代码如下:

class Solution {
public:
    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;
    }
};

Python 代码如下:

# (版本二)暴力排序法
class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        for i in range(len(nums)):
            nums[i] *= nums[i]
        nums.sort()
        return nums
# (版本三)暴力排序法+列表推导法
class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        return sorted(x*x for x in nums)

注意要点

  1. 两个语法知识点
    • 快速排序:sort(nums.begin(), nums.end())
    • vector 定义:vector<int> result(nums.size(), 0)
  2. 易错点: 题目要求升序排序,而原数组平方后的最大值就在数组两端,而升序排列要求最大值应存放在结果数组最右端,因此逆序放入答案并移动指针

相关题目推荐

待补充

209. 长度最小的子数组

文档讲解:代码随想录 | 数组 | 长度最小的子数组
视频讲解:拿下滑动窗口! | LeetCode 209 长度最小的子数组
状态:轻松拿下!

题目链接:209. 长度最小的子数组
解题思路-1:滑动窗口

  • 前提:滑动窗口中不会加入负数!
  • 优势:滑动窗口的时间复杂度只能说遥遥领先!
  • 时间复杂度: O ( n ) O(n) O(n) ,其中 n n n 为数组长度。指针 leftright 最多各移动 n n n
  • 空间复杂度: O ( 1 ) O(1) O(1)

解题思路-2:暴力法

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n 为数组长度。需要遍历每个下标作为子数组的开始下标,对于每个开始下标,需要遍历其后面的下标得到长度最小的子数组
  • 空间复杂度: O ( 1 ) O(1) O(1)

滑动窗口

思路

滑动窗口:本质是满足了单调性,即左右指针只会往一个方向走且不会回头。收缩的本质即去掉不再需要的元素。也就是做题我们可以先固定移动右指针,判断条件是否可以收缩左指针算范围

  1. 初始化一个 sum 来记录子数组的和
  2. sum >= target 说明窗口内存在子数组的和大于 target ,先扩张窗口右边界找到子数组,再缩减窗口左边界找到长度最小的子数组
    • 当窗口右边界向右扩张,统计窗口内的元素和 sum
    • 当窗口左边界向右缩减,统计窗口内的元素和 sum

以示例 1 为例:

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

滑动窗口实现示意图如下:
在这里插入图片描述

思考:双指针和滑动窗口有什么区别,感觉双指针也是不断缩小的窗口。这道题,想用两头取值的双指针,结果错了?

因为两头指针走完相当于最多只把整个数组遍历一遍,会漏掉很多情况。滑动窗口实际上是双层遍历的优化版本,而双指针其实只有一层遍历,只不过是从头尾开始遍历的
滑动窗口的原理是右边先开始走,然后直到窗口内值的总和大于target,此时就开始缩圈,缩圈是为了找到最小值,只要此时总和还大于target,我就一直缩小,缩小到小于target为止在这过程中不断更新最小的长度值,然后右边继续走,如此反复,直到右边碰到边界。这样就保证了可以考虑到最小的情况

代码

C++ 代码如下:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        if (nums.size() == 0) {
            return 0;
        }
        int minLen = INT32_MAX; // 最终的结果,目标为最小值常初始化设置为最大值
        int sum = 0; // 滑动窗口数值之和
        int left = 0, right = 0; // 滑动窗口的起始位置和终止位置
        while (right < nums.size()) {
            sum += nums[right++];
            // 注意这里使用 while,每次更新 left(起始位置),并不断比较子序列是否符合条件
            while (sum >= target) {
                minLen = minLen < right - left ? minLen : right - left;
                sum -= nums[left++]; // 这里体现出滑动窗口的精髓之处,不断变更 left(子序列的起始位置)
            }
        }
        // 如果 minLen 没有被赋值的话,就返回 0,说明没有符合条件的子序列
        return minLen != INT32_MAX ? minLen : 0;
    }
};

Python 代码如下:

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        if not nums:
            return 0
        
        n = len(nums)
        ans = n + 1
        sums = [0]
        for i in range(n):
            sums.append(sums[-1] + nums[i])
        
        for i in range(1, n + 1):
            target = s + sums[i - 1]
            bound = bisect.bisect_left(sums, target)
            if bound != len(sums):
                ans = min(ans, bound - (i - 1))
        
        return 0 if ans == n + 1 else ans

暴力法

思路

最直接的想法:2 层 for 循环进行遍历:第一层 for 循环控制区间的起始位置,第二层 for 循环控制区间的终止位置

  1. 把数组的所有区间情况都遍历出来,然后找到 >= target 的最小区间长度
  2. 在这个区间里面不断搜索,把所有区间情况都枚举出来,然后判断 >= target 的最小长度是多少,最后返回这个最小长度
代码

C++ 代码如下:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        if (nums.size() == 0) {
            return 0;
        }
        int minLen = INT32_MAX; // 最终的结果,目标为最小值常初始化设置为最大值
        for (int left = 0; left < nums.size(); left++) { // 设置子序列起点为 left
            int sum = 0; // 子序列的数值之和
            for (int right = left; right < nums.size(); right++) { // 设置子序列终点为 right
                sum += nums[right];
                if (sum >= target) { // 一旦发现子序列和超过了 target,更新 minLen
                    minLen = minLen < right - left + 1 ? minLen : right - left + 1;
                    break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就 break 跳出内层循环
                }
            }
        }
        // 如果 minLen 没有被赋值的话,就返回0,说明没有符合条件的子序列
        return minLen != INT32_MAX ? minLen : 0;
    }
};

Python 代码如下:

# (版本二)暴力法
class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        if not nums:
            return 0
        
        n = len(nums)
        ans = n + 1
        for i in range(n):
            total = 0
            for j in range(i, n):
                total += nums[j]
                if total >= s:
                    ans = min(ans, j - i + 1)
                    break
        
        return 0 if ans == n + 1 else ans

注意要点

  1. 滑动窗口前提: 滑动窗口中不会加入负数!
  2. LeetCode 给出的代码均考虑到 nums.size() == 0 的情况,确实会更严谨一些,需要注意一下
  3. 本题目目标结果是得到满足某条件(条件可替换)的最小连续子数组的长度 int minLen = INT32_MAX ,这种目标为最小值常初始化设置为最大值的方式以后会很常见,反之亦然
  4. 子序列长度取 r i g h t − l e f t right - left rightleft 还是 r i g h t − l e f t + 1 right - left + 1 rightleft+1 需要仔细考虑,这一点会受 left++right++ 代码位置影响,视情况而定
  5. 滑动窗口 方法中需要连续判定 sum >= target 以此来缩减左边界,相当于多个连续 if 判断语句,因此需要用 while 替代
  6. 其他一些小细节
    • 滑动窗口的外循环条件 while (right < nums.size()) ,最开始考虑用 for (int right = 0; right < nums.size(); right++) ,其实回头来看效果是一样的,但感觉前者其实更好理解
    • 如果 minLen 没有被赋值的话,就返回 0,说明没有符合条件的子序列:return minLen != INT32_MAX ? minLen : 0; 一行代码解决可以节省代码量

相关题目推荐

待补充

59. 螺旋矩阵 Ⅱ

文档讲解:代码随想录 | 数组 | 螺旋矩阵 II
视频讲解:一入循环深似海 | LeetCode:59.螺旋矩阵 II
状态:一遍过!!!

题目链接:59. 螺旋矩阵 II
解题思路-1:模拟法

  • 循环不变量原则:左闭右开
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是给定的正整数。矩阵的大小是 n × n n×n n×n,需要填入矩阵中的每个元素
  • 空间复杂度: O ( 1 ) O(1) O(1),除了返回的矩阵以外,空间复杂度是常数

模拟法

思路
  1. 可以将矩阵看成若干层,首先输出最外层的元素,其次输出次外层的元素,直到输出最内层的元素
  2. 对于每层,从左上方开始以顺时针顺序遍历所有元素,由外向内一圈一圈这么画下去
    • 填充上行从左到右
    • 填充右列从上到下
    • 填充下行从右到左
    • 填充左列从下到上

左闭右开原则示意图:
在这里插入图片描述
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画!

代码

C++ 代码如下:

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
        int offset = 0; // 外层循环次数
        int count = 1; // 用来给矩阵中每一个空格赋值
        // 每循环一圈填充两个边,因此循环次数为 n / 2,但若 n 为奇数中间的值需要单独处理
        while (offset < n / 2) {
            
            // 下面开始的四个 for 就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (int x = offset, y = offset; y < n - 1 - offset; y++) {
                res[x][y] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (int x = offset, y = n - 1 - offset; x < n - 1 - offset; x++) {
                res[x][y] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (int x = n - 1 - offset, y = n - 1 - offset; y > offset; y--) {
                res[x][y] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)            
            for (int x = n - 1 - offset, y = offset; x > offset; x--) {
                res[x][y] = count++;
            }
            
            offset++;
        }
        
        // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n % 2) {
            res[n / 2][n / 2] = n * n;
        }
        return res;
    }
};

Python 代码如下:

class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        nums = [[0] * n for _ in range(n)]
        startx, starty = 0, 0               # 起始点
        loop, mid = n // 2, n // 2          # 迭代次数、n为奇数时,矩阵的中心点
        count = 1                           # 计数

        for offset in range(1, loop + 1) :      # 每循环一层偏移量加1,偏移量从1开始
            for i in range(starty, n - offset) :    # 从左至右,左闭右开
                nums[startx][i] = count
                count += 1
            for i in range(startx, n - offset) :    # 从上至下
                nums[i][n - offset] = count
                count += 1
            for i in range(n - offset, starty, -1) : # 从右至左
                nums[n - offset][i] = count
                count += 1
            for i in range(n - offset, startx, -1) : # 从下至上
                nums[i][starty] = count
                count += 1                
            startx += 1         # 更新起始点
            starty += 1

        if n % 2 != 0 :			# n为奇数时,填充中心点
            nums[mid][mid] = count 
        return nums

注意要点

  1. 模拟过程处理边界条件时应注意循环不变量原则:左闭右开
  2. 易错点: n n n 为奇数需要单独给矩阵最中间的位置赋值,判断语句条件 n % 2 不需要用 n % 2 == 1,下标索引直接 n / 2 即可,会自动取整

相关题目推荐

待补充

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值