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

第一章 数组part02

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

参考链接:代码随想录

977.有序数组的平方 

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

思路:可以分为暴力解法和双指针法(重点)

题目里面这个非递减要理解它的含义,也就递增

暴力排序:

最直观的想法,莫过于:每个数平方之后,排个序,代码如下:

class Solution {
public:
    vector sortedSquares(vector& A) {
        for (int i = 0; i < A.size(); i++) {
            A[i] *= A[i];
        }
        sort(A.begin(), A.end()); // 快速排序
        return A;
    }
};

这个时间复杂度是 O(n + nlogn), 可以说是O(nlogn)的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 O(n + nlog n)。

双指针法:

数组其实是有序的, 只不过负数平方之后可能成为最大数了。

那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。

此时可以考虑双指针法了,i指向起始位置,j指向终止位置。

定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。

如果A[i] * A[i] < A[j] * A[j] 那么result[k--] = A[j] * A[j]; 。

如果A[i] * A[i] >= A[j] * A[j] 那么result[k--] = A[i] * A[i]; 。

过程如图示所示:

代码随想录算法训练营day02||977.有序数组的平方,209.长度最小的子数组,59.螺旋矩阵II ;数组总结_第1张图片

此时的时间复杂度为O(n),相对于暴力排序的解法O(n + nlog n)还是提升不少的。

Java语言的代码如下:

class Solution {
    public int[] sortedSquares(int[] nums) {
        //双指针的思想
        int left = 0;
        int right = nums.length - 1;
        int[] result = new int[nums.length];
        int k = nums.length - 1;//新结果集数组的索引,因为非递减,所以从后往前插入
        while(left <= right){
            if(nums[left]*nums[left] > nums[right]*nums[right]){
            // 正数的相对位置是不变的, 需要调整的是负数平方后的相对位置!!!
               result[k] = nums[left]*nums[left];
                --k;
            //还可以合并写:(注:++在前先自加再赋值,++在后先赋值再自加)
            //result[k--] = nums[left] * nums[left];
                ++left;  
            }else {
                result[k] = nums[right]*nums[right];
                --k;
            //result[index--] = nums[right] * nums[right];
                --right;
            }  
        }
        return result;
    }
}

补充一下:自增、自减操作还可以写到数组里面去,简化写法,这种方法要会

  result[k--] = nums[left] * nums[left++];

  result[k--] = nums[right] * nums[right--];

* ++、--可以出现在变量前面或者后面.无论是前面还是后面,变量值都会自加1 

(1) ++出现在变量后:先进行赋值运算,再进行变量自加1

(2) ++出现在变量前:  先进行变量自加1,再进行赋值运算

 209.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

思路:暴力解法就是两个for循环,然后不断的寻找符合条件的子序列,也即数组元素和 ≥ s 的子数组,全部找出来,再找出长度最小的,返回该长度,时间复杂度很明显是O(n^2)。

暴力解法:
class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        //暴力解法
        int result = Integer.MAX_VALUE; //最终的结果,先赋一个最大的整数值上限
        int sum = 0; // 子序列的数值之和
        int subLength = 0; // 子序列的长度
        for (int i = 0; i < nums.length; i++) { // 设置子序列起点为i
            sum = 0;
            for (int j = i; j < nums.length; j++) { // 设置子序列终止位置为j
                sum += nums[j];
                if (sum >= s) { // 一旦发现子序列和超过了s,更新result
                    subLength = j - i + 1; // 取子序列的长度
                    result = result < subLength ? result : subLength;
                    break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件
                          //就break
                }
            }
        }
        // 如果result没有被赋别的值的话,就返回0,说明没有符合条件的子序列
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

(后面力扣更新了数据,暴力解法已经超时了。)

滑动窗口:

接下来就开始介绍数组操作中另一个重要的方法:滑动窗口

思路分析:所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。所以时间复杂度是 O(n^2)。

首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。

如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?

此时难免再次陷入 暴力解法的怪圈。

所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置

那么问题来了, 滑动窗口的起始位置如何移动呢?

 这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:

代码随想录算法训练营day02||977.有序数组的平方,209.长度最小的子数组,59.螺旋矩阵II ;数组总结_第2张图片

代码随想录算法训练营day02||977.有序数组的平方,209.长度最小的子数组,59.螺旋矩阵II ;数组总结_第3张图片

 最后找到 4,3 是最短距离。

其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。

在本题中实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。

解题的关键在于 窗口的起始位置如何移动,如图所示:

代码随想录算法训练营day02||977.有序数组的平方,209.长度最小的子数组,59.螺旋矩阵II ;数组总结_第4张图片

可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。 

注:一些朋友可能会疑惑为什么时间复杂度是O(n)

不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        //滑动窗口
        int left = 0;
        //int right =0;
        int sum = 0;
        int result = Integer.MAX_VALUE;//表示int数据类型的最大取值数:2 147 483 647
        for(int right = 0;right < nums.length;right++){
            sum += nums[right];
            while(sum >= target){
                int newlength = right - left + 1;
                result = Math.min(result,newlength);
                //合并写法:result = Math.min(result,right - left + 1);
                sum -= nums[left];
                ++left;
                //合并写法:sum -= num[left++];
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}
一、Integer.MAX_VALUE的含义

在Java中,一共有8种基本数据类型:

整数型:int , short , long , byte 。
浮点型:float , double 。
字符类型:char 。
表示真值的类型:boolean 。

(String属于Java中的字符串类型,也是一个引用类型,并不属于基本的数据类型)

代码随想录算法训练营day02||977.有序数组的平方,209.长度最小的子数组,59.螺旋矩阵II ;数组总结_第5张图片

Integer.MAX_VALUE表示int数据类型的最大取值数:2 147 483 647
Integer.MIN_VALUE表示int数据类型的最小取值数:-2 147 483 648

对应:
Short.MAX_VALUE 为short类型的最大取值数 32 767
Short.MIN_VALUE 为short类型的最小取值数 -32 768**

补充:Integer.MAX_VALUE+1=Integer.MIN_VALUE

0111 1111 1111 1111 1111 1111 1111 1111+1=1000 0000 0000 0000 0000 0000 0000 0000

二、Math.min()返回零个或更多个数值的最小值

1.返回给定数值中最小的数。如果任一参数不能转换为数值,则返回NaN。如果没有参数,结果为Infinity(无穷大)。
参数有效值:整数,浮点数,数字字符串。
参数无效值:非数字字符串,空变量,空数组(非数字)。

Math.min(-1, 4, 6, 12);// -1

三、Math.max()返回一组数中的最大值

1.返回给定的一组数字中的最大值。如果给定的参数中至少有一个参数无法被转换成数字,则会返回 NaN。如果没有参数,则结果为 - Infinity。

Math.max(10, 20); // 20 Math.max(-10, -20); // -10

59.螺旋矩阵II 

给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

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

思路:本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。

前面在用到二分法的时候讲究:一定要坚持循环不变量原则

而求解本题依然是要坚持循环不变量原则。

模拟顺时针画矩阵的过程:

  • 填充上行从左到右
  • 填充右列从上到下
  • 填充下行从右到左
  • 填充左列从下到上

这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是一进循环深似海,难以自拔了

代码随想录算法训练营day02||977.有序数组的平方,209.长度最小的子数组,59.螺旋矩阵II ;数组总结_第6张图片

这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。

这也是坚持了每条边左闭右开的原则。

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] res = new int[n][n];//定义一个 二维数组
        int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
        int loop = 0;//loop 控制循环次数,循环次数由n值决定,分奇数、偶数 
        //int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
        int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
        int count = 1; // 用来给矩阵中每一个空格赋值
        int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
        int i,j;
        while (loop++ <= n/2) {
            i = startx;
            j = starty;

            // 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (j = starty; j < n - offset; j++) {
                res[startx][j] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (i = startx; i < n - offset; i++) {
                res[i][j] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (; j > starty; j--) {
                res[i][j] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)
            for (; i > startx; i--) {
                res[i][j] = count++;
            }

            // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
            startx++;
            starty++;

            // offset 控制每一圈里每一条边遍历的长度
            offset += 1;
        }

        // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n % 2 == 1) {
            res[mid][mid] = count;
        }
        return res;
    }
}
  • 时间复杂度 O(n^2): 模拟遍历二维矩阵的时间
  • 空间复杂度 O(1)

其他版本的大神解答代码参考: 

整个过程示意图:有助于理解(这里则用的是 左开右闭的方法,第一行例外)

代码随想录算法训练营day02||977.有序数组的平方,209.长度最小的子数组,59.螺旋矩阵II ;数组总结_第7张图片

class Solution {
    public int[][] generateMatrix(int n) {
        //大神解法的解读
        int left = 0, right = n-1, top = 0, bottom = n-1;
        int count = 1, target = n * n; //初始值从1开始,一共到迭代到n*n
        int[][] res = new int[n][n];//new一个最终的 结果数组
        //for循环中变量定义成i或j的细节:按照通常的思维,i代表行,j代表列
        //这样,就可以很容易区分出来变化的量应该放在[][]的第一个还是第二个
        //对于变量的边界怎么定义:
            //从左向右填充:填充的列肯定在[left,right]区间
            //从上向下填充:填充的行肯定在[top,bottom]区间
            //从右向左填充:填充的列肯定在[right,left]区间
            //从下向上填充:填充的行肯定在[bootom,top]区间
        //通过上面的总结会发现边界的起始和结束与方向是对应的
        while(count <= target){
            //不是(l= left; j--) res[bottom][j] = count++;
            //缩小下边界
            bottom--;
            //从下向上填充,相当于缩小左边界
            for(int i = bottom; i >= top; i--) res[i][left] = count++;
            //缩小左边界
            left++;
        }
        return res;
    }
}

你可能感兴趣的:(代码随想录,算法,java,leetcode,数据结构)