今日主要内容:数组基础(977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II)
前置准备
1:复习昨天的博客
based on C++ 学习依据代码随想录:)
内容学习
1:简单复习
感觉写代码的时候头脑并不很清楚,就有时候写到下面不知道自己上面那一段是怎么想的了,然后就出现很逆天的代码,今日计划写代码的时候注意注释的写。
题目1
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
首先能想到暴力解法,首先将每个元素进行平方,不改变其位置。然后再对元素们进行比较排序,虽然肉眼可见的复杂,但感觉基本可以实现。
另一种写法可能考虑双指针,毕竟才学习的思想,先想一想快慢指针分别会代表什么比较合适。(好吧完全没想到这里会以什么方式使用双指针,先尝试一下暴力方法) 先写代码尝试一下结果:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++){
nums[i] *= nums[i];
}//这部分首先把每个元素平方,不改变其具体位置
int a = 0; // 方便一会交换元素位置
for (int i = 0; i < nums.size(); i++){
for (int j = i+1; j < nums.size(); j++){
if (nums[i] >= nums[j]){
a = nums[j];
nums[j] = nums[i];
nums[i] = a;
//这里使用双重循环,如果当前元素大于后面的任意一个
//元素,就把他们交换一下
//疑点:if否可以取等号,第二重循环是否有可能越界
}
}
}
return nums;
}
};
运行时通过了,但是提交时用时过长了,悲从中来;
肉眼可见的使用了大量的资源,但是从132/137的通过率感觉,应该大体上的问题不是很大,但是还有一些显而易见的疑点需要处理:if否可以取等号,第二重循环是否有可能越界(以及算法的复杂度过高了,应该使用快速排序算法改善暴力求解的速度)
视频学习
双指针思路
双指针也即取两个指针,从头和尾分别逼近中心元素。新建一个数组。其实看到这里思路就已经很明确了,于是暂停视频回来写代码,一次通过。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; i++){
nums[i] *= nums[i];
}//这部分首先把每个元素平方,不改变其具体位置
vector<int> result = vector(n,0); // 定义新的数组来排序
for (int i = 0,j = nums.size()-1 ; i <= j ; ){
//这里控制i和j从两个方向逼近。在i和j相遇的时候结束
if (nums[i] >= nums[j]){
result[n-1] = nums[i];
i++;
n--;
//谁大谁就排到后面去,然后这边的指针向前(后)移动,
//这里新数组的n-1位置已经被占用了,要往后走一步,下同
}
else {
result[n-1] = nums[j];
j--;
n--;
}
}
return result;
}
};
其中有两个小地方,写的时候竟然想清楚了,帅的一:
for (int i = 0,j = nums.size()-1 ; i <= j ; ) 第一部分是这里,i<=j,如果去掉等号的话会丢掉最后相等时的一个元素,显然就错误了。其次是例如i++,j--这些内容都是经过判断时候才会去做的,因此不写在for循环里。主要就是这两个问题。
快速排序
再次补充一下快速排序的内容:
class Solution {
public:
vector<int> sortedSquares(vector<int>& 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)。但是这里也没有说明快速排序的具体流程,因此我再去寻找一下资源:
基本思想:
采用“分治”的思想,对于一组数据,选择一个基准元素(base),通常选择第一个或最后一个元素,通过第一轮扫描,比base小的元素都在base左边,比base大的元素都在base右边,再有同样的方法递归排序这两部分,直到序列中所有数据均有序为止。
一趟快速排序的算法步骤是:
1.设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2.以第一个数组元素作为关键数据(通常是第一个),赋值给key,即key=A[0];
3.从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]的值交换; (进行交换的时候i, j指针位置不变)
4.从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换; (进行交换的时候i, j指针位置不变)
5.重复第3、4步,直到i==j;
注意:
在3, 4步中,没找到符合条件的值,即3中A[j]不小于key, 4中A[i]不大于key的时候也要改变j、i的值,使得j--,i++,直至找到为止。找到符合条件的值。
另外,i==j这一过程一定正好是i++或j--完成的时候,此时令循环结束。
代码如下
void QuickSort(int a[],int low,int high)
{
if(low>=high) return;//此时已经完成排序,直接返回
int i = low;
int j = high;
int key = a[low];
while(i<j)//实现第一趟排序
{
while(i<j&&key<a[j]) j--;//从右向左找比key小的值
a[i] = a[j];
while(i<j&&key>a[i]) i++;//从左向右找比key大的值
a[j] = a[i];
}
a[i] = key;//将关键数据填入low=high的位置
QuickSort(a,low,i-1);//左边子序列递归排序
QuickSort(a,i+1,high);//右边子序列递归排序
}
int main()
{
int a[20];
QuickSort(a,0,19);
for(int i = 0;i<20;i++)
cout<<a[i]<<" ";
return 0;
题目2
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3]
是该条件下的长度最小的子数组。
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
我日你哥,看到题目一脸懵b,啥想法没有啊?
也不能说完全没有吧,首先可以知道如果不存在符合条件的子数组,返回 0
。这个也即遍历数组,计算其总加和,如果总加和都不及target,可以直接返回0。那么如何计算最小的子数组呢,到这里想到可能是根据总和挨个去除一些元素?比如把所有的元素从小到大排序,用总和依次减去累加和,当差值小于target时,再加上刚刚去掉的那个元素就够了。
(呆逼,审题哦,连续子数组你还在这重新排序还在这问答案为什么不对,笑拥了。)
想到这好像能解释这个问题了,尝试写一下代码吧。(错误代码)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int sum = 0;
int n = nums.size();
int r = 0; //用来输出结果
for(int i = 0 ; i < n ; i++){
sum += i;
}//计算加和
if(sum<target){
r=0;//不够直接返回0
}
else if(sum == target){
r=n;//相等直接返回总数
}
else{
sort(nums.begin(), nums.end()); //不相同先排序
for (int i=0;i<n;i++){
sum -= nums[i];
if(sum<target){
r=n-i;
break;
}//如果小了,说明刚刚那个只要不减去就够了,
//一共n个,去掉了i+1个元素,再加一
else if(sum == target){
r=n-i;
break;//如果相等,说明现在正好够了,
//一共n个,去掉了i+1个元素
}
}
}
return r;
}
};
但是结果是错的,为什么呢,我感觉写的还蛮好的呢(不是)。还是让我们来看看具体该如何解决吧。(你是弱智吧还写得好好钩子)
视频学习
超级暴力解决
啊这个也没想到吗===
超级暴力方法,使用for循环,写出所有的区间可能性并加和比较,选出其中最小的一个。(又写了个错的代码,为什么呢?) 后面再看,先暂缓一下。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result;
int n = nums.size();
int sum = 0;
for (int i=0;i<n;i++){
sum+=nums[i];
}
if (sum<target){
return 0;
}
// else if (sum == target){
// return n;
// }
for (int i=0;i<n;i++){
int sum = 0;
for(int j=i;j<n;j++){
sum += nums[j];
if(sum >= target){
result = j-i+1;
break;
}
}
}
return result;
}
};
正确代码 :(虽然正确但是也会有部分会超时)
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); 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 == INT32_MAX ? 0 : result;
双指针法(滑动窗口思想)
主要就是将j指针初始作为最终位置(不是起始就在最后,for循环遍历得到的),然后移动处在起始位置的i指针来调整位置,调整区间大小。条件是:目前的区间元素和已经大于等于s了,就要移动i。整体思想比较好理解,也是一些小地方需要处理好。
首先,在判断sum是否大于等于target时,由于要不断地进行判断推进i的位置,应该使用while而不是if;其次,在最后那里需要判断,原始集合是否足够大,若本来就不够,没有进入循环并操作,就应该直接输出0。这两个地方需要注意。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
int i = 0;
for(int j = 0; j < nums.size();j++){
sum += nums[j];
while(sum >= target){
subLength = j-i+1;
// result = min(result,subLength);
result = result < subLength ? result : subLength;
sum -= nums[i];
i++;
}
}
return result == INT32_MAX ? 0 : result;
// return result;
}
};
实际上,在return的时候也可以提前判断是否参与了循环,也即:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
**
for(int i = 0;i<nums.size();i++){
sum += nums[i];
}
if (sum < target){
return 0;
}
else{
sum = 0;
}
int i = 0;
**
for(int j = 0; j < nums.size();j++){
sum += nums[j];
while(sum >= target){
subLength = j-i+1;
result = min(result,subLength);
// result = result < subLength ? result : subLength;
sum -= nums[i];
i++;
}
}
// return result == INT32_MAX ? 0 : result;
return result;
}
};
题目3
给你一个正整数 n
,生成一个包含 1
到 n2
所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
。
示例 1:
输入:n = 3 输出:[[1,2,3],[8,9,4],[7,6,5]]
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
重击我的大脑反正是:(((((((
感觉只有一些零散的想法吧:1~n²这些数一起可以写成一个矩阵,考虑i和j分别作为横纵坐标,先横着打印,然后如果i到达n,就换成j继续行动,j到达n就换成i继续行动这样,然后把输出的结果放入数组中即可。但是个人感觉好像编程能力很难达到这个水平。快进到学习视频,就不胡乱尝试了。
视频学习
学完了,实际上思路还是比较简单,把握好每一次读哪些结束就可以了。然后用来控制每次跑多远的就设计成全局变量,随着一圈的完成增加就可以了。这里在思路上注意一点,画矩阵要想着二维数组里是【1】【1】→【1】【2】的增长,而不是类似于x轴坐标的增长,第一次写错主要就是这个原因。另外,for循环中如果没有要调整的元素,就空着不写,不要写个变量在里面,类似于这样
for(i;i<n;i++);
以下是正确的代码:这个题目反而比第二个要好想不少:(
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int x = 0;
int y = 0;
int cha = 1;
int time = n/2;
vector<vector<int>> res(n, vector<int>(n, 0));//定义二维数组
int count = 1;
while(time--){
int i;
int j;
for (j = y; j < n - cha; j++) {
res[x][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = x; i < n - cha; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > y; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > x; i--) {
res[i][j] = count++;
}
x+=1;
y+=1;
cha+=1;
}
if(n%2!=0){
res[x][y] = count;
}
return res;
}
};
总结
数组的经典题目
在面试中,数组是必考的基础数据结构。
其实数组的题目在思想上一般比较简单的,但是如果想高效,并不容易。
之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。
#二分法
这道题目呢,考察数组的基本操作,思路很简单,但是通过率在简单题里并不高,不要轻敌。
可以使用暴力解法,通过这道题目,如果追求更优的算法,建议试一试用二分法,来解决这道题目
- 暴力解法时间复杂度:O(n)
- 二分法时间复杂度:O(logn)
在这道题目中我们讲到了循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力。
个人认为,二分法算比较基础的想法,只要弄清楚边界(也很好想,记不得的时候用数学定义就能很好的理解),基本上实现比较容易。
#双指针法
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
- 暴力解法时间复杂度:O(n^2)
- 双指针时间复杂度:O(n)
这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点:
- 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
- C++中vector和array的区别一定要弄清楚,vector的底层实现是array,封装后使用更友好。
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
个人认为,双指针法主要要清楚每一个指针的具体作用是什么,这样就很容易理解每一个步骤要做什么,自己写的时候才能写清楚。
#滑动窗口
本题介绍了数组操作中的另一个重要思想:滑动窗口。
- 暴力解法时间复杂度:O(n^2)
- 滑动窗口时间复杂度:O(n)
本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。
这部分内容掌握的还不是很好,甚至暴力解法也没完全理解在result的取值方面的问题,准备借助文本资料和代码注释再理解一下,之后复现。
#模拟行为
模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。
相信大家有遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,拆了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点。
这个实际上难度并不大,就是搞明白边界,然后把用来调整循环次数、循环长度的全局变量控制好就行了。
总体来说,数组方面入门确实不是很难,但一些新的思想还是很值得多次学习反复思考的。其次,数组的操作也没有很熟练,这一定程度上也影响到了做题的速度和效率,例如在二维数组那里,我还是用array来处理,无法输出。马上要开始链表的题目,而链表我的理解和认识,以及对基本语法的掌握还处于很低的水平,因此今晚还要把链表的知识多看一看,多写一些代码热热身。
这两天也挺高强度的学习和了解算法的这样一些比较有趣的思想,提升认识之余也对后面的学习满怀希望,虽然目前是个残旧的半成品,写出来的代码常常依托沟狮不能运行,但我们说:“没有任何一件事情是一定怎么怎么样的。”在工作闲暇时间好好学吧。