题目:977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II ,数组总结
参考链接:代码随想录
给你一个按 非递减顺序 排序的整数数组 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];
。
过程如图示所示:
此时的时间复杂度为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,再进行赋值运算
给定一个含有 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;
}
}
(后面力扣更新了数据,暴力解法已经超时了。)
接下来就开始介绍数组操作中另一个重要的方法:滑动窗口。
思路分析:所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。所以时间复杂度是 O(n^2)。
首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。
如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?
此时难免再次陷入 暴力解法的怪圈。
所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。
那么问题来了, 滑动窗口的起始位置如何移动呢?
这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:
最后找到 4,3 是最短距离。
其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。
在本题中实现滑动窗口,主要确定如下三点:
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
解题的关键在于 窗口的起始位置如何移动,如图所示:
可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将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中的字符串类型,也是一个引用类型,并不属于基本的数据类型)
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
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
思路:本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。
前面在用到二分法的时候讲究:一定要坚持循环不变量原则。
而求解本题依然是要坚持循环不变量原则。
模拟顺时针画矩阵的过程:
这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是一进循环深似海,难以自拔了。
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。
这也是坚持了每条边左闭右开的原则。
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;
}
}
其他版本的大神解答代码参考:
整个过程示意图:有助于理解(这里则用的是 左开右闭的方法,第一行例外)
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;
}
}