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

87 阅读7分钟

977.有序数组的平方

问题分析

给一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。换而言之,就是比较数组中各个元素的平方值进行重新排序。

不难理解,当数据的绝对值越大的时候,那么平方值也就越大,而提供的数据本身就是有序的,因此绝对值更大的数据必定会集中在数组的两侧,我们可以利用这个特点来构思解决办法。

解决方案

已知绝对值更大的数据必定会集中在数组的两侧,那么我们可以直接依次比较未排序的数据中位于最两侧的两个数据的平方值,选择两者之间的更大的一个,就必定是当前数据中平方值最大的数据了。为了从两侧遍历向中间遍历,我们可以参考二分法设置两个前后两个指针。

设置start和end两个指针分别从左侧和右侧向nums中间位置遍历,remainCount记录目前剩余的处理数据数量,result[]未返回的目标数组:

  1. 初始状态start = 0(最左元素),end = numSize - 1(最右元素),remainCount = numSize;
  2. 如果nums[start]的平方大于nums[end]的平方,说明目前平方和最大的是start指向的数据,将该数据平方值填入result[remainCount-1],然后start加一,移动到下一个待比较的数据位置,remainCount减一
  3. 如果nums[start]的平方小于等于nums[end]的平方,说明目前平方和最大的是end指向的数据,将该数据平方值填入result[remainCount-1],然后end减一,移动到下一个待比较的数据位置,remainCount减一
  4. 当start大于end时,说明nums遍历完毕,算法结束。

代码实现

/// 977. 有序数组的平方
/// - Parameters:
///   - nums: 待处理数组
///   - numsSize: 数组长度
///   - returnSize: 返回长度
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
    int *result = (int *)malloc(sizeof(int) * numsSize);                //为返回的数组申请空间
    int start = 0;                                                       //左指针,初始值为0
    int end = numsSize - 1;                                           //右指针,初始值n-1
    int remainCount = numsSize;                                         //剩余数据数,初始值n
    *returnSize = numsSize;                                             //返回数组的长度,值等于n
    while (start <= end) {
        int comp1 = nums[start];
        int comp2 = nums[end];
        if(comp1 * comp1 > comp2 * comp2){                              //如果start平方更大
            start++;                                                     //start向右移
            result[--remainCount] = comp1 * comp1;                      //start元素的值写入返回数组
        }else{                                                          //如果end平方更大
            end--;                                                    //end向右移
            result[--remainCount] = comp2 * comp2;                      //end元素的值写入返回数组
        }
    }
    return  result;
}

image-20231228001235391

209.长度最小的子数组

问题分析

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

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

由于确定数组元素都是正数,因此每个子数组末尾位置向后扩展时,新的子数组的元素总和一定会增加;每个子数组起始位置向后收缩时,新的子数组的元素总和一定会减少。想要求长度最短的连续子数组,需要理解这个特性。

解决方案

比较容易想到的是两轮嵌套遍历的方法,把所有起始位置的满足条件子数组的长度都记录下来,选择最小的一个输出。但仔细想一下,其实可能会出现很多无用的统计和累加。例如下面的情况:

image-20231228125100698

如果Target取k,则无论start从哪里开始比较子数组的元素和,都需要end走到最后一个元素才会满足条件,记元素总数为n,那么第1趟累加和移动的次数为n-1,同理,第i趟为n-i,最后一趟为1次,则总次数为n*(n-1)/2,显然为**O(n^2)**级别。

这个时候,我们使用滑动窗口更加合适。滑动窗口是一种数组和字符串问题的常用算法,它可以用来解决一系列的子数组或子字符串问题。其基本思想是维护一个窗口,这个窗口可以增大或减小,以满足特定的条件。

处理流程如下:

  1. 初始化:定义两个指针startend,分别代表子数组(窗口)的左右边界。初始化start为0,end从0开始遍历数组,以及一个变量sum来存储窗口内元素的总和,初始化为0。
  2. 扩展窗口:逐步移动end指针(外层循环),将nums[end]加到sum中。这个过程是在寻找一个窗口,其总和大于等于s
  3. 收缩窗口:一旦找到了一个总和大于等于s的窗口,记录当前窗口的长度(end - start + 1),并尝试通过移动start指针(内层循环)来缩小窗口,并更新sumsum -= nums[start]),直到窗口的总和小于s。这一步是为了找到最小长度的子数组。
  4. 更新结果:每次找到一个总和大于等于s的窗口时,更新结果result为当前窗口长度和之前result的最小值。
  5. 返回结果:如果找到了符合条件的子数组,返回result;如果没有找到,则返回0。

这个方案的时间复杂度分析

  • 每个元素被end指针访问一次。
  • 每个元素最多被start指针访问一次(当它被移除窗口时)。

因此,每个元素最多被两次操作,总操作数是2n,这给出了一个线性时间复杂度O(n)。

代码实现

/// 209. 长度最小的子数组
/// - Parameters:
///   - target: 目标长度
///   - nums: 数组
///   - numsSize: 数组长度
int minSubArrayLen(int target, int* nums, int numsSize) {
    if(numsSize <= 0){
        return 0;
    }
    int minlength = INT_MAX;                                            //最小子数组长度纪录,初始应该为最大值
    int start = 0;                                                      //子数组首元素指针
    int end = 0;                                                        //子数组尾元素指针
    int sum = 0;                                                        //子数组元素和,初始为0
    while (end < numsSize) {
        sum += nums[end];                                               //元素和累加尾元素值
        while (sum >= target) {                                         //如果此时累加和大于等于目标值,此时有了可以纪录的方案
            int currentLength = end - start + 1;
            minlength = currentLength < minlength ? currentLength : minlength;
            sum -= nums[start];                                         //start尝试向前收缩
            start++;
        }
        end++;
    }
    if(minlength < INT_MAX){                                            //如果有找到过可行方案,则返回
        return minlength;
    }
    return 0;
}

59.螺旋矩阵II

问题分析

这是一个代码循环的控制问题,由于都是正方形,因此我们可以把绘制分为一层一层的方框的绘制问题。

解决方案

设置4个变量来分别记录当前尚未填写的部分的上(top)下(bottom)左(left)右(righ)四个边界。其中右边界和下边界为开区间。

因此初始状态下,top和left取0,bottom和right取n(因为是开区间)。以4阶矩阵为例,填写过程如下:

image-20231229013600937

可以总结出四个边界的调整规则:

  1. 左到右的行填入完毕,top要向下一格(增加1);
  2. 上到下的列填入完毕,right要向左一格(减少1);
  3. 右到左的行填入完毕,bott要向上一格(减少1);
  4. 下到上的列填入完毕,left要向右一格(增加1)。

代码实现

int** generateMatrix(int n, int* returnSize, int** returnColumnSizes) {
    *returnSize = n;
    *returnColumnSizes = malloc(sizeof(int) * n);
    int **matrix = malloc(sizeof(int*) * n);
    for (int i = 0; i < n; i++) {
        (*returnColumnSizes)[i] = n;
        matrix[i] = malloc(sizeof(int) * n);
    }
    int target = n * n;
    int currentNum = 1;
    int top = 0;
    int bottom = n;
    int left = 0;
    int right = n;
    while (currentNum <= target) {
        // 从右到左的行写入
        for (int i = left; i < right; i++) {
            matrix[top][i] = currentNum++;
        }
        top++;                                                  //从左到右的行写入完毕,top要向下一格
        // 从上到下的列写入
        for (int j = top; j < bottom; j++) {
            matrix[j][right - 1] = currentNum++;
        }
        right--;                                                //从上到下的列写入完毕,right要向左一格
        // 从右到左的行写入
        for (int k = right - 1; k >= left; k--) {
            matrix[bottom - 1][k] = currentNum++;
        }
        bottom--;                                               //从右到左的行写入完毕,bottom要向下一格
        // 从下到上的列写入
        for (int l = bottom - 1; l >= top; l--) {
            matrix[l][left] = currentNum++;
        }
        left++;                                                 //从下到上的列写入完毕,left要向右一格
    }
    return matrix;
}

image-20231229014937606