力扣刷题之 数组

本文详细探讨了数组的各种操作,包括遍历数组以查找最大连续1的个数、计算中毒状态的总时长、找到数组中第三大的数、找到缺失的第一个正数等。还介绍了如何处理数组的改变和移动,如旋转数组、移动零以及处理二维数组的旋转和重塑。此外,文章还讨论了特定顺序遍历二维数组的方法,如螺旋矩阵的旋转和区域和检索。文章强调了在解决数组问题时寻找数学规律、利用空间和时间复杂度优化的重要性。
摘要由CSDN通过智能技术生成

前言

从2021年9月28开始正式刷的题,到今天也有10天了,我的任务不多,就是一天3道题,所以说到今天正好30道题,全部都是数组题,是根据这篇博客来进行刷题练习的。
写这篇博客的目的就是,将前10天这30道数组题有一个很好的归纳与分析,也算是自己的一遍复习。

数组题目分类

一、数组的遍历

数组的遍历对应题目编号:485 、495 、 414 、628

485. 最大连续 1 的个数

给定一个二进制数组, 计算其中最大连续 1 的个数。
在这里插入图片描述
方法:一次遍历
为了得到数组中最大连续1的个数,需要遍历数组,无可厚非,同时要记录已经遍历过的最大的连续1的个数和 当前的连续1的个数。
具体而言:从数组下标0开始,如果当前的元素是1,则连续1的个数count++,否则的话,就要使用之前的连续1的个数更新最大的连续1的个数:maxCount=max(count,maxCount),同时要将count置为0,因为此时1已经不再连续。
遍历结束之后,需要再次使用当前的连续1的个数更新最大的连续1的个数,因为数组的最后一个元素可能是1,并且最长连续1的子数组可能出现在数组的末尾,如果遍历数组结束之后不更新最大的连续1的个数,会出错。

int findMaxConsecutiveOnes(vector<int> &nums){
	int n=nums.size();
	int conut=0;
	int maxCount=0;
	for(int i=0;i<n;i++){
		if(nums[i]==1){
			count++;
		}
		else{
			maxCount=max(maxCount,count);
			count=0;
		}
	}
	maxCount=max(maxCount,count);
	return maxCount;
}

这道题目就是很简单的遍历问题,但是我从中仍然学到了东西,比如说当遍历结束之后,还是要将当前连续1的个数和之前连续1的个数对比,要不然的话,如果最后一个是1,那就只能count++,但是maxCount仍然是上一步的maxCount,所以在遍历结束之后,还是需要比较,我觉得这一点适用于所有的循环遍历思想,就是说当你遍历一个数据结构的时候,尤其是更改某些东西的时候,一定要注意开头和结尾的处理。

495. 提莫攻击

描述:

在《英雄联盟》的世界中,有一个叫 “提莫” 的英雄,他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。现在,给出提莫对艾希的攻击时间序列和提莫攻击的中毒持续时间,你需要输出艾希的中毒状态总时长。
在这里插入图片描述

这一道题仍然是一个需要遍历数组的题目,但不同于上一题的是这里有一个中毒持续时间,也就是说如果在某一个时刻+中毒持续时间>它的下一个时刻,这个时候总时长就不是简单的某一时刻+中毒持续时间了,所以在遍历的时候,有判断的条件。
考虑相邻两个攻击时间点A[i]和A[i+1]以及中毒持续时间t,如果A[i]+t<=A[i+1],那么在第i+1次攻击时,第i次攻击造成的中毒的持续时间已经结束,即持续时间为t,但是如果A[i]+t>A[i+1],那么在第i+1次攻击时,第i次攻击的中毒仍然在持续,由于中毒状态不可以叠加,因此持续的时间为A[i+1]-A[i]
仍然是采用一次遍历的思想:

int findPoisonedDuration(vector<int> &timeSeries,int duration){
	int n=timeSeries.size();
	int res=0;
	for(int i=0;i<n-1;i++){
		res=res+min(duration,timeSeries[i+1]-timeSeries[i]);
	}
	res=res+duration;
	//这里还是上一题的结尾思想,虽然遍历结束了,但是在最后一个元素的时候仍然是有中毒持续时间的。
	return res;
}

414. 第三大的数

描述

给你一个非空数组,返回此数组中 第三大的数 。如果不存在,则返回数组中最大的数。
在这里插入图片描述

这一题,我在写的时候,想的就是直接对数组进行排序,然后输出数组中的倒数第三个元素,但是要对数组去重。

//去重函数:iterator unique(iterator it_1,iterator it_2);返回的是去重后不重复序列的最后一个元素的下一个元素
//unique函数的去重过程实际上就是不停的把后面不重复的元素移到前面来,也可以说是用不重复的元素占领重复元素的位置。去重前:13345667  去重后:13456367 一定不要忘记的是,unique函数在使用前需要对容器中的元素进行排序
//unique函数并不是真正意义上将元素删去了,所以我们还有用到erase函数,将后面的元素删除
 int thirdMax(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        nums.erase(unique(nums.begin(),nums.end()),nums.end());
        if(nums.size()<3) return nums[nums.size()-1];
        else return nums[nums.size()-3];
    }

当然,这个思路不是最佳的,因为它的时间复杂度是O(nlogn),主要来自排序的时间,控件复杂度是O(logn),排序需要的栈空间为O(logn)。但n很大的时候,上面的时间复杂度和空间复杂度都不是很理想。
优化:
仍然用一次遍历的思想:

我们可以遍历数组,并用三个变量a,b,c来维护数组中的最大值、次大值和第三大值,我们将其均初始化为小于数组最小值的元素,视为无穷小
从数组下标为0的元素开始遍历,如果数组元素>a,那么最大值就要发生变化,同样第二大值和第三大值都要变化,a要变为数组元素的值,b要变为a的值,c要变为b的值(代码顺序正好相反);如果a>数组元素>b,那么就最大值a不变,将b替换为数组元素,c替换为b;如果b>数组元素>c,类似的,a和b都不用变,将c替换为num即可。

int thirdMax(vector<int> &nums){
	long a=LONG_MIN,b=LONG_MIN,c=LONG_MIN;
	for(long num:nums){//其实我觉得在c++中遍历,用这个形式就很好
		if(num>a){
		 c=b;b=a;a=num;
		 }
		 else if(a>num && num>b){
		 	c=b;b=num;
		 }
		 else if(b>num && num>c){
		 	c=num;
		 }
	}
    return c==LONG_MIN?a:c;
}

628. 三个数的最大乘积

描述:给你一个整型数组 nums ,在数组中找出由三个数组成的最大乘积,并输出这个乘积。

我可能是个小垃圾,所以遇见那种什么在数组中最大,最小的啥的时候第一反应就是排序,但是,排序算法的空间复杂度是O(logn),但n很大的时候,显然这种算法是不可行的,那就乖乖看题解吧!
一般说来,如果要通过排序来解决的话,那我们可以用变量来存放排序的结果,这样的话空间复杂度就会降低.
方法:线性扫描(仍然是一次遍历的思想)
因为这道题目中,可能会出现数组元素是复数的情况,所以说,如果用变量代替排序,那就需要用5个变量:数组中最大的三个数,以及最小的两个数。(有点类似于上一题)

int maximumProduct(vector<int> &nums){
	int min1=INT_MAX,min2=INT_MAX;
	int max1=INT_MIN,max2=INT_MIN,max3=INT_MIN;
	for(int x:nums){
		if(x<min1){
			min2=min1;
			min1=x;
		}
		else if(x<min2){
			min2=x;
		}
		if (x > max1) {
                max3 = max2;
                max2 = max1;
                max1 = x;
            } else if (x > max2) {
                max3 = max2;
                max2 = x;
            } else if (x > max3) {
                max3 = x;
            }
	}
	return max(min1 * min2 * max1, max1 * max2 * max3);
}

时间复杂度:O(N)O(N),其中 NN 为数组长度。我们仅需遍历数组一次。
空间复杂度:O(1)O(1)。

小总结:在涉及到数组的遍历的题目时,如果题目要求空间复杂度的话,我们可以用变量去代替我们要找的排序后的元素。而且涉及到数组的题目,在遍历完之后一定看遍历结束的条件,需不需要特殊处理,一般是需要的。

二、统计数组中的元素

645. 错误的集合(easy)

方法一:排序
这应该是很多人都能想到的,将数组排序之后,比较每对相邻的元素,即可找到错误的集合。题目要求:返回重复的数字;返回丢失的数字
重复的数字很好找,遍历数组,如果相邻的两个元素相等,那么这个元素就是重复的数字。
寻找丢失的数字相对复杂,可能会有三种情况:
(1)丢失的是1:(2,2,3,4)
(2)丢失的是n:(1,2,3,3)
(3)丢失的是比1大比n小的数。(1,2,2,4)
那么为了寻找丢失的数字,我们需要在遍历已经排好序数组的同时记录上一个元素,然后计算当前元素与上一个元素的差,因为可能丢失的数字是1,所以初始化上一个元素应该为0.

vector<int> findErrorNums(vector<int> &nums){
	vector<int> errorNums(2);
	int n=nums.size();
	sort(nums.begin(),nums.end());
	int prev=0;
	for(int i=0;i<n;i++){
		int curr=nums[i];
		if(curr==prev){
			errorNums[0]=prev;//找到重复元素
		}else if(curr-prev>1){  
			errorNums[1]=prev+1;
		}
		//除了上述两种情况,那就是正常的情况了,就往后遍历
		prev=curr;	
	}
	if(nums[n-1]!=n){
		errorNums[1]=n;
	}
	return errorNums;
	}

时间复杂度:O(nlogn),其中 n 是数组 nums 的长度。排序需要 O(nlogn) 的时间,遍历数组找到错误的集合需要 O(n) 的时间,因此总时间复杂度是O(nlogn)。
空间复杂度:O(logn),其中 n 是数组nums 的长度。排序需要 O(logn) 的空间。

优化:
方法二:哈希表
重复的数字在数组中出现2次,丢失的数字在数组中出现0次,其余的每个数字在数组中出现1次,因此可以使用哈希表记录每一个元素在数组中出现的次数,然后遍历从1到n的每一个数字,分别找到出现2次和出现0次的数字,即为重复的数字和丢失的数字。

vector<int> findErrorNums(vector<int> &nums>){
	vector<int> errorNums(2);
	int n=nums.size();
	unordered_map<int,int> mp;
	for(auto &num:nums){
		mp[num]++;	
	}
	for(int i=1;i<=n;i++){
		int count=mp[i];
		if(count==2){
			errorNums[0]=i;
		}else if(count==0){
			errorNums[1]=i;
		}
	}
	return errorNums;
}

时间复杂度:O(n),需要遍历数组填入哈希表
空间复杂度:O(n),需要创建大小为n的哈希表
再优化:
方法三:位运算
(今天不想写,因为饿了,,,,为什么南方的学校食堂没饭这么早…)

697.数组的度

描述: 给定一个非空且只包含非负数的整数数组 nums,数组的度的定义是指数组里任一元素出现频数的最大值。你的任务是在 nums 中找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度。

方法一:哈希表
记原数组中出现次数最多的数为x,那么和原数组的度相同的最短连续子数组,必然包含了原数组中的全部x,并且两段恰好是x第一次出现和最后一次出现的位置。
因为符合条件的x可能有很多个,即多个不同的数在原数组中出现的次数相同,所以为了找到这个子数组,我们需要统计每一个数出现的次数,同时还需要统计每一个数第一次出现和最后一次出现的位置。

class Solution {
public:
    int findShortestSubArray(vector<int>& nums) {
        //借助三个unordered_map分别来保存,出现次数最多元素的初始位置、最终位置,以及各个元素出现的次数
        unordered_map<int,int> left,right,counter;
        int degree=0;
        for(int i=0;i<nums.size();i++){
            if(!left.count(nums[i])){
                left[nums[i]]=i;
            }
            right[nums[i]]=i;
            counter[nums[i]]++;
            degree=max(degree,counter[nums[i]]);
        }
        int res=nums.size();
        for(auto kt:counter){
            if(kt.second==degree){
            res=min(res,right[kt.first]-left[kt.first]+1);
            }
        }
        return res;
    }
};

448. 找到所有数组中消失的数字

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

方法:原地修改
我们可以用一个哈希表记录数组nums中的数字,由于数字范围均在[1,n]中,记录数字后我们再利用哈希表检查;1,n]中的每一个数是否出现,从而找到缺失的数字。
由于数字范围在[1,n]中,我们也可以用一个长度为n的数组来代替哈希表,但是这一做法的空间复杂度是O(n),我们的目标是优化空间复杂度到O(1)。注意到nums的长度恰好也是n,能否让nums充当哈希表呢?由于nums的数字范围均在[1,n]中,我们可以利用这一范围之外的数字,来表达【是否存在】的含义。
具体来说,遍历nums,每遇到一个数x,就让nums[x-1]增加n。由于nums中所有的数均在[1,n]中,增加以后,这些数必然大于n。最后我们遍历nums,如果nums[i]中的数并没有大于n,那就说明没有遇到过数i+1。
注意,当我们遍历到某个位置时,其中的数可能已经被增加过,因此需要对 nn 取模来还原出它本来的值。

vector<int> findDisappearedNumbers(vector<int> &nums){
	int n=nums.size();
	for(auto &num:nums){
		int x=(num-1) %n;
		nums[x]+=n;
	}
	vector<int> res;
	for(int i=0;i<n;i++){
		if(nums[i]<n){
		res.push_back(i+1);
	}
	}
	return res;
}

442.数组中重复的数据

给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次。
找到所有出现两次的元素。
你可以不用到任何额外空间并在O(n)时间复杂度内解决这个问题吗?

41. 缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

方法:原地哈希:
int firstMissingPositive(vector& nums) {

    int n=nums.size();
    //分析一下题目,缺失的第一个正数它的取值范围应该是[1,n+1]
    //所以说当我们利用原地哈希将数组遍历一遍,发现元素在它该有的位置上,那么缺失的数字就是n+1
    //现在进行原地哈希(也就是说将数组本身当做哈希表来考虑,通俗一点来讲就是“一个萝卜一个坑",)
    //将数组的值与它的下标联系起来
    for(int i=0;i<n;i++){
    //遍历数组,如果发现数组的值是在[1,n+1]范围之内的,那么就让其与它应有的下标对应在一起。
    //但是除了上述条件之外 ,还有一个就是说原来的位置上如果值和下标是对应的,那么就不动。
        while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
            swap(nums[nums[i] - 1], nums[i]);
        }
    }
    //这样遍历下去会发现 ,在原来数组中会有下标和值不匹配的,那个就是我们要找到的
    //现在再次遍历数组,给这个数找出来即可
    for(int i=0;i<n;i++){
        if(nums[i]!=i+1){
            return i+1;
        }
    }
    return n+1;
}

274. H 指数

给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。
h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。且其余的 n - h 篇论文每篇被引用次数 不超过 h 次。
例如:某人的 h 指数是 20,这表示他已发表的论文中,每篇被引用了至少 20 次的论文总共有 20 篇。
提示:如果 h 有多种可能的值,h 指数 是其中最大的那个。

方法:排序
首先我们可以将初始的H指数h设为0,然后将引用次数排序,并且对排序后的数组从小到大遍历。
根据H指数的定义,如果当前H指数为h并且在遍历过程中找到当前值citations[i]>h,则说明我们找到了一篇被引用了至少h+1次的论文,所以将现有的h值加1.继续遍历直到h无法继续增大。

class Solution {
    public int hIndex(int[] citations) {
        Arrays.sort(citations);
        int h = 0, i = citations.length - 1; 
        while (i >= 0 && citations[i] > h) {
            h++; 
            i--;
        }
        return h;
    }
}

时间复杂度:O(nlogn),其中 n为数组citations 的长度。即为排序的时间复杂度。
空间复杂度:O(logn),其中 n 为数组 citations 的长度。即为排序的空间复杂度。

三、数组的改变、移动

453. 最小操作次数使数组元素相等

给定一个长度为 n 的 非空 整数数组,每次操作将会使 n - 1 个元素增加 1。找出让数组所有元素相等的最小操作次数。

方法:数学法

int minMoves(vector<int>& nums) {
    //每次操作可以使数组里n - 1个元素+1,最后让数组元素相等。
    //简单思考一下,就能得出让数组n - 1个元素+1,实际上就是让剩下的那个元素-1
    //我们可以贪心的找出最小的元素,让所有元素不断-1和它相同即可
    int min=*min_element(nums.begin(),nums.end()); 找出最小元素
    int k=0;
    for(auto num:nums){
        k=k+num-min;   // //每个元素和最小元素的差值就是它们的操作次数
    }
    return k;
    }

665. 非递减数列

给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。
我们是这样定义一个非递减数列的: 对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。

方法:贪心
本题目是说要维持一个非递减的数列,所以说当遇到递减的情况nums[i]>nums[i+1]的时候,要么将前面的元素缩小,要么将后面的元素放大。
但是本题的易错点就在这:
如果将nums[i]缩小,可能会导致其无法融入前面已经遍历过的非递减子序列。
如果将nums[i+1]放大,可能会导致其后续的继续出现递减。
所以有下面的解决方法:

bool checkPossibility(vector<int> & nums){
	int n=nums.size();
	if(n==1) return true;
	bool flag=nums[0]<=nums[1]?true:false;
	for(int i=1;i<n-1;i++){
		if(nums[i]>nums[i+1])  //出现递减了
		{
			if(flag){
				//flag为真,就说明还有修改的机会
				//修改的方式有两种,因为我们是要看3个元素才能来正确的做出判断的
				//当nums[i+1]>nums[i-1]的时候,也就是说只有i和i+1是不满足递增的,比如【2,3,4,6,5】,那么这个时候将i位置上的值改为i+1位置上的就可以,而不是单纯的缩小或者放大。
				//这样既不会破坏i+1后面的又不破坏i-1前面的
				if(nums[i+1]>=nums[i-1]){
					nums[i]=nums[i+1];
				}
				else{
					//如果这个时候nums[i+1]<nums[i-1],比如[2,3,4,6,3],这个时候就算将i位置上的值换成了i+1的,也不满足递增,因为i-1位置上的数比i+1位置上的数大,这个是时候应该将i+1位置上的值变为i上面的
					nums[i+1]=nums[i];
				}
				flag=false;//没有修改机会了
			}
			else{
				return false;
			}		
		}
	}
	return true;
}

283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
输入: [0,1,0,3,12] 输出: [1,3,12,0,0]

方法:双指针
我们考虑使用双指针,一个指针用来遍历数组,另一个指针用来判断当前元素是否为非零元素。

void moveZeroes(vector<int> &nums){
	//i指针用来遍历元素,j指针用来判断当前元素是否为0
	int j=0;
	for(int i=0;i<nums.size();i++){
		if(nums[i]!=0){
		//当i指向的元素是非零元素的时候,j也往后移动,如果i指向的元素为0,那么j不动,表示此时j指向的元素为0,但是i继续往后遍历,当i移动到不是0的地方,就要发生交换行为:将i指向的非零元素和j指向的0元素交换位置。同时j++。
			int temp;
			temp=nums[i];
			nums[i]=nums[j];
			nums[j]=temp;
			j++;
		}
	}
}

四、二维数组以及滚动数组

118. 杨辉三角

给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在这里插入图片描述

方法:数学思路
每个数字等于上一行的左右两个数字之和,可用此性质写出整个杨辉三角。
即第 n 行的第 i个数等于第 n−1 行的第 i−1 个数和第 i个数之和。

vector<vector<int>> generate(int numRows){
	vector<vector<int>> res(numRows);
	for(int i=0;i<numRows;i++){
		res[i].resize(i+1);//第0层有一个元素,第一层有两个元素...
		res[i][0]=1;//每一行的第一个元素都是1
		//下面就是数学规律的代码实现了
		for(int j=1;j<i;j++){
		//第 n 行的第 i个数等于第 n−1 行的第 i−1 个数和第 i个数之和
			res[i][j]=res[i-1][j-1]+res[i-1][j];
		}	
		res[i][i]=1;
	}
	return res;
}

119. 杨辉三角 II

给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。
输入: rowIndex = 3 输出: [1,3,3,1]

方法:数学思路

vector<int> getRow(int rowIndex) {
        int numRows=rowIndex+1;
        vector<vector<int>> res(numRows);
        for(int i=0;i<numRows;i++){
            res[i].resize(i+1);//第0层有一个元素,第1层有两个元素....
            res[i][0]=1;
            //这里就是重点了,就是当前行的元素是其上一行元素左上角和右上角之和
            for(int j=1;j<i;j++){
                res[i][j]=res[i-1][j-1]+res[i-1][j];
            }
            res[i][i]=1;
        }
        return res[rowIndex];
 }

661. 图片平滑器

包含整数的二维矩阵 M 表示一个图片的灰度。你需要设计一个平滑器来让每一个单元的灰度成为平均灰度 (向下舍入) ,平均灰度的计算是周围的8个单元和它本身的值求平均,如果周围的单元格不足八个,则尽可能多的利用它们。

方法:遍历矩阵
我们可以很简单的想到的是遍历这个二维矩阵,然后取某一点的八个方向上的值求和再求平均值

vector<vector<int>> imageSmoother(vector<vector<int>> &img){
	//我们可以很简单的想到的是遍历这个二维矩阵,然后取某一点的八个方向上的值求和再求平均值
	//所以第一步要做的就是先整8个方向坐标,然后再判断某一点所有方向坐标是否存在,存在就加入sum值,不存在就不加入
	int m=img.size();
	int n=img[0].size();
	vector<vector<int>> ans(m,vector<int>(n));
	int x[8]={1,1,1,0,0,-1,-1,-1};
	int y[8]={1,0,-1,1,-1,1,0,-1};
	for(int i=0;i<m;i++){
		for(int j=0;j<n;j++){
			int sum=img[i][j];
			int num=1;
			//遍历8个方向
			for(int k=0;k<8;k++){
				int a=i+x[k];
				int b=j+y[k];
				if(a>=0 && a<m &&b>=0 &&b<n){
					sum=sum+img[a][b];
					num++;
				}
			}
			ans[i][j]=sum/num;
		}
	}
	return ans;
} 

在这里插入图片描述

598. 范围求和 II

给定一个初始元素全部为 0,大小为 m*n 的矩阵 M 以及在 M 上的一系列更新操作。
操作用二维数组表示,其中的每个操作用一个含有两个正整数 a 和 b 的数组表示,含义是将所有符合 0 <= i < a 以及 0 <= j < b 的元素 M[i][j] 的值都增加 1。
在执行给定的一系列操作后,你需要返回矩阵中含有最大整数的元素个数。

解读题意:输入3,3 (3行3列的数组) operations=[[2,2],[3,3]]
有两个操作,操作1是【2,2】意思是将二维数组中所有符合0<=i<2以及0<=j<2的元素M[i][j]的值都增加1,经过一系列操作之后,你需要返回的是矩阵中含有最大整数的元素个数。

方法:一次遍历

int maxCount(int m,int n,vector<vector<int>> &ops){
	 //也是没想到自己能那么笨无语住了,本来还想的是把a,b取出来,一一的进行操作
        //但其实这后面是有数学规律的,这就再一次证明,关于数组的很多题,甚至一些其他的算法题,都要找背后的数学原理,然后转换为代码
        //这道题,求经过操作之后矩阵含有的最大整数的元素个数,可以举个例子,然后会发现,返回的就是一系列操作中的最小行与最小列的乘积
		int num=ops.size();
		for(int i=0;i<num;i++){
			m=min(m,ops[i][0]);
			n=min(n,ops[i][1]);
		}
		return m*n;
}

419. 甲板上的战舰

给定一个二维的甲板,请计算其中有多少艘战舰,战舰用‘X’表示,空位用‘.’表示,你需要遵守以下规则:
给你一个有效的甲板,仅由战舰或者空位组成
战舰智能水平或者垂直放置,换句话说,战舰只能由1N(1行N列)组成,或者N1(N行1列)组成,其中N可以是任意大小。
两艘战舰之间至少有一个水平或者垂直的空位分隔。即没有相邻的战舰

方法:一次扫描算法

int countBattleships(vector<vector<char>> &board){
	//再来读一遍题目:意思是说战舰只能是横着放或者是竖着放的,即战舰是不可能不在一行或者不在一列的
      //怎么在代码中实现这个要求呢,其实很简单,就是遍历每一行的时候固定某一列
        //同时两艘战舰之间至少有一个水平方向或者是垂直的空位,那么我们可以遍历甲板的时候,如果某一战舰的左边或者上边都没有战舰,那说明这个是有效的战舰
        int m=board.size();
        int n=board[0].size();
        int ans=0;
        for(int i=0;i<m;i++){
			for(int j=0;j<n;j++){
				if(board[i][j]='X'){
				 	//如果遍历出某一行中的第j列出现了X战舰,那么,了解下题目含义比较重要,按顺序放时,从左到右 ,从上到下,所以只需要考虑,上,左有无空位即可,
				 	//当战舰位于第一行的时候,我们只需要判断它的左边有没有战舰,即board[i][j-1]时候由战舰即可,同理,当战舰位于第一列的时候,我们只需要判断它的上边有没有战舰即可
				 	if(i==0 || board[i-1][j]!='X' && j==0 || board[i][j-1]!='X'){
				 	 	ans++;
				 	 }
				 }
			}
		}
		return ans;

}

五、数组的旋转

189. 旋转数组

给定一个数组,将数组中的元素向右移动k个位置,其中k是非负数。
尽可能的想出更多的解决方案,至少有三种不同的方法可以解决这个问题
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

方法一:使用额外数组
我们可以使用额外的数组来将每一个元素放置在正确的位置,用n表示数组的长度,我们遍历原数组,将原数组下标为i的元素放至新数组下标(i+k)mod n的位置,
比如(1,2,3,4,5) k=2,那么移动后的数组应该是(4,5,1,2,3),我们发现4原本的位置是3,也就是i=3,移动之后变为了0,0=(3+2)mod 5.最后将新数组拷贝到原数组中即可。

void rotate(vector<int> &nums,int k){
	int n=nums.size();
	vector<int> newArr(n);
	for(int i=0;i<n;i++){
	 	newArr[(i+k) % n]=nums[i];
	 }
	 nums.assign(newArr.begin(),newArr.end());
}

时间复杂度: O(n),其中 n 为数组的长度。空间复杂度: O(n)。
方法二:数组翻转
该方法基于如下的事实:当我们将数组的元素向右移动k次后,尾部k mod n个元素会移动到数组头部,其余元素向后移动k mod n个位置。
该方法为数组的翻转,我们可以先将所有的元素翻转,这样尾部k mod n个元素就被移动到数组的头部了,然后我们再翻转[0,k mod n-1]区间的元素和[k mod n,n-1]区间的元素即能得到最后的答案。

void reverse(vector<int> &nums,int start,int end){
	while(start<end){
		swap(nums[start],nums[end]);
		start+=1;
		end-=1;
	}
}
void rotate(vector<int> &nums,int k){
	k %= nums.size();
	reverse(nums,0,nums.size()-1);
	reverse(nums,0,k-1);
	reverse(nums,k,nums.size()-1);
}

时间复杂度:O(n),其中 n 为数组的长度。每个元素被翻转两次,一共 n 个元素,因此总时间复杂度为 O(2n)=O(n)。空间复杂度:O(1)O(1)。

396. 旋转函数

给定一个长度为n的整数数组A。假设Bk是数组A顺时针旋转k个位置后的数组,我们定义A的旋转函数F为:
在这里插入图片描述

方法:数学思路

int maxRotateFunction(vector<int>& nums) {
        //找规律,求出F(k)和F(k-1)之间的关系式,然后递归求最大值
        int n= nums.size();
        int sumA=0;
        int presumF=0;//递归以F(0)为初始值
        for(int i=0;i<n;i++){
            sumA += nums[i];
            presumF += i*nums[i];
        }
        int ans=presumF;
        for(int i=1;i<n;i++){
            //递归求F(x):F(k)=F(k-1)+sumA-n*nums[n-i]
            int sumF=presumF+sumA-n*nums[n-i];
            ans=max(ans,sumF);
            presumF=sumF;
        }
        return ans;
    }

六、特定顺序遍历二维数组

54. 螺旋矩阵

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

在这里插入图片描述
方法:按层模拟
可以将矩阵看成若干层,首先输出最外层的元素,其次输出次外层的元素,直到输出最内层的元素。
对于每一层,从左上方开始以顺时针的顺序遍历所有元素,假设当前层的左上角位于(top,left),右下角位于(bottom,right),按照如下顺序遍历当前层的元素。
1、从左到右遍历上侧元素,依次为(top,left)到(top,right)
2、从上到下遍历右侧元素,依次为(top+1,right)到(bottom,right)
3、如果left<right并且top<bottom,则从右到左遍历下侧元素,依次为(bottom,right-1)到(bottom,left+1),以及从下到上遍历左侧元素,依次为(bottom,left)到(top+1.left)
遍历完当前层的元素之后,将left和top分别增加1,将right和bottom分别减少1,进入下一层继续遍历,直到遍历完所有元素为止。

vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if (matrix.size() == 0 || matrix[0].size() == 0) {
            return {};
        }
        int rows=matrix.size();
        int columns=matrix[0].size();
        int left=0;
        int right=columns-1;
        int top=0;
        int bottom=rows-1;
        vector<int> order;
        while(left<=right && top<=bottom){
            for(int column=left;column<=right;column++){
                order.push_back(matrix[top][column]);
            }
            for(int row=top+1;row<=bottom;row++){
                order.push_back(matrix[row][right]);
            }
            if(left<right && top<bottom){
                //从右往左遍历
                for(int column=right-1;column>left;column--){
                    order.push_back(matrix[bottom][column]);
                }
                for(int row=bottom;row>top;row--){
                    order.push_back(matrix[row][left]);
                }
            }
                left++;
                right--;
                top++;
                bottom--;
            }
        return order;
    }

59. 螺旋矩阵 II

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
在这里插入图片描述

方法:按层模拟
和上一题一样的思想

vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> matrix(n,vector<int>(n));
        int left=0,right=n-1,top=0,num=1,bottom=n-1;
        while(left<=right && top<=bottom){
            for(int column=left;column<=right;column++){
                //matrix[top][column]=column+1;
                matrix[top][column]=num;
                num++;
            }
            for(int row=top+1;row<=bottom;row++){
                matrix[row][right]=num;
                num++;
            }
            if(left<right && top<bottom){
                for(int column=right-1;column>=left;column--){
                    matrix[bottom][column]=num;
                    num++;
                }
                for(int row=bottom-1;row>top;row--){
                    matrix[row][left]=num;
                    num++;
                }
            }
            left++;
            right--;
            top++;
            bottom--;
        }
        return matrix;
    }

498. 对角线遍历

给你一个大小为 m x n 的矩阵 mat ,请以对角线遍历的顺序,用一个数组返回这个矩阵中的所有元素。
在这里插入图片描述

方法:模拟对角线路线
按照题目的要求,模拟在数组中的行走路线,得到想要的遍历顺序,为了实现此模拟,必须清楚数组内部的行走策略。对每条对角线需要明确两件事情:
1、知道该对角线的行走方向(朝上还是朝下)
2、确定对角线的起点元素,这取决于对角线的行走方向。

vector<int> findDiagonalOrder(vector<vector<int>>& mat) {
        //首先判断对角线应该朝上走还是朝下走:数组下标和对2取余==0朝上,否则朝下
        int n=mat.size();//n行
        int m=mat[0].size();//m列
        vector<int> res(n*m);
        if(n==0 || m==0){
            return {};
        }
        if(n==1){
            return mat[0];
        }
        if(m==1){
            for(int i=0;i<n;i++){
                res[i]=mat[i][0];
            }
            return res;
        }

        int index=0;//res的下标
        int i=0;//res的行坐标
        int j=0;//res的列坐标

        while(i<n && j<m){
            res[index++]=mat[i][j];//每一次循环之前先将这个值赋值给res
            if((i+j)%2==0){
                //对角线方向朝上,这个时候会有3种情况
                //(1)如果是第一行的元素 同时并不是最后一列,这个时候直接列+1就行
                if(i==0 && j!=m-1){
                    j++;
                }
                //(2)如果此时是最后一列,直接行加一
                else if(j==m-1){
                    i++;
                }
                else{
                    i--;
                    j++;
                }
            }
            else{
                //对角线方向朝下,这个时候依然会有3种情况
                //(1)如果是第一列的元素,但不是最后一行,这个时候直接行+1
                if(j==0 && i!=n-1){
                    i++;
                }
                else if(i==n-1){
                    j++;
                }else{
                    i++;
                    j--;
                }
            }
        }
        return res;
    }

七、二维数组变换

566. 重塑矩阵

在matlab中,有一个非常有用的函数reshape,它可以将一个mn矩阵重塑为另外一个大小不同的新矩阵(rc),但是保留其原始数据。
给你一个由二维数组mat表示的m*n矩阵,以及两个正整数r和c,分别表示想要的重构的矩阵的行数和列数。
重构后的矩阵需要将原始矩阵的所有元素以相同的 行遍历顺序填充。如果具有给定参数的 reshape 操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。
在这里插入图片描述

方法:二维数组的一维表示
对于一个行数为m。列数为n,行列下标都从0开始编号的二维数组,我们可以通过下面的方式,将其中的每一个元素(i,j)映射到整数域内,并且它们按照行优先的顺序一一对应着[0,mn)中的每一个整数。形象化地来说,我们把这个二维数组【排扁】成了一个一维数组。
这样的映射即为:
(i,j)---->i*n+j
同样的,我们可以将整数x映射回其在矩阵中的下标,即
i=x/n; j=x%n;
其中 / 表示整数除法,%表示取模运算
那么题目需要我们做的事情相当于:
将二维数组nums映射为一个一维数组;
将这个一维数组映射回r行c列的二维数组。
设nums本身为m行n列,如果mn!=rc,那么二者包含的元素个数不相同,因此无法进行重塑。
否则,对于x∈[0,mn),第x个元素在nums中对应的下标为(x/n,x%n),而在新的重塑矩阵中对应的下标为(x/c,x%c),我们直接进行赋值即可。

vector<vector<int>> matrixReshape(vector<vector<int>>& mat, int r, int c) {
        int m=mat.size();
        int n=mat[0].size();
        //m行n列的矩阵
        vector<vector<int>> res(r,vector<int>(c));
        if(m*n != r*c){
            return mat;
        }
        for(int i=0;i<m*n;i++){
            res[i/c][i%c]=mat[i/n][i%n];
        }
        return res;
    }

48. 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。
在这里插入图片描述

void rotate(vector<vector<int>>& matrix) {
        //先水平翻转,再对角线翻转
        int n=matrix.size();
        for(int i=0;i<n/2;i++){
            for(int j=0;j<n;j++){
                swap(matrix[i][j],matrix[n-i-1][j]);
            }
        }

        for(int i=0;i<n;i++){
            for(int j=0;j<i;j++){
                swap(matrix[i][j],matrix[j][i]);
            }
        }
    }

73. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。

方法一:使用标记数组
我们可以使用两个标记数组分别记录每一行进而每一列是否有零出现。
具体的,我们首先遍历该数组一次,如果某一个元素为0,那么就将该元素所在的行和列所对应的标记数组的位置标记为true。最后我们再次遍历该数组,用标记数组更新原数组即可。

void setZeroes(vector<vector<int>> &matrix){
	int m=matrix.size();
	int n=matrix[0].size();
	vector<int> row(m),col(n);
	for(int i=0;i<m;i++){
	 for(int j=0;j<n;j++){
	  	if(!matrix[i][j]){
  		 	row[i]=col[j]=true;
  		 }
	  }
	 }
	 for(int i=0;i<m;i++){
	 for(int j=0;j<n;j++){
		if(row[i] || col[j]){
		 matrix[i][j]=0;
		 }
	}
	}
}

时间复杂度:O(mn),其中 m是矩阵的行数,n 是矩阵的列数。我们至多只需要遍历该矩阵两次。空间复杂度:O(m+n),其中 m 是矩阵的行数,n 是矩阵的列数。我们需要分别记录每一行或每一列是否有零出现。

方法二:使用两个标记变量

void setZeroes(vector<vector<int>>& matrix) {
        //根据题解,如果是O(m+n)的空间复杂度,那么可以直接用两个标记数组
        //但是现在需要用常数级的空间,所以我们将第一行和第一列作为标记数组,在此之前先记录第一行和第一列中是否含有0 
        int flag_row0=false;
        int flag_col0=false;
        int m=matrix.size();//m行
        int n=matrix[0].size();//n列
        for(int i=0;i<n;i++){
            //查看第一行是否有0
            if(matrix[0][i]==0){
                //如果有0
                flag_row0=true;
            }
        }
        for(int i=0;i<m;i++){
            if(matrix[i][0]==0){
                flag_col0=true;
            }
        }
        //接下来就从【1,1】开始遍历,如果某一元素【i,j】为0,那么所对应的第一行和第一列都应该为0
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(matrix[i][j]==0){
                    matrix[i][0]=0;
                    matrix[0][j]=0;
                }
            }
        }

        //修改
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(matrix[i][0]==0 || matrix[0][j]==0){
                    matrix[i][j]=0;
                }
            }
        }
        if(flag_row0){
            for(int i=0;i<n;i++){
                matrix[0][i]=0;
            }
        }
        if(flag_col0){
            for(int i=0;i<m;i++){
                matrix[i][0]=0;
            }
        }
    }

289.生命游戏

根据 百度百科 ,生命游戏,简称为生命,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态:1 即为活细胞(live),或 0 即为死细胞(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。
在这里插入图片描述

这个问题看起来很简单,但有一个陷阱,如果你直接根据规则更新原始数组,那么就做不到题目中说的 同步 更新。假设你直接将更新后的细胞状态填入原始数组,那么当前轮次其他细胞状态的更新就会引用到当前轮已更新细胞的状态,但实际上每一轮更新需要依赖上一轮细胞的状态,是不能用这一轮的细胞状态来更新的。

方法:使用额外的状态
题目中每个细胞只有两种状态 live(1) 或 dead(0),但我们可以拓展一些复合状态使其包含之前的状态。举个例子,如果细胞之前的状态是 0,但是在更新之后变成了 1,我们就可以给它定义一个复合状态 2。这样我们看到 2,既能知道目前这个细胞是活的,还能知道它之前是死的。
算法

  • 遍历 board 中的细胞。
  • 根据数组的细胞状态计算新一轮的细胞状态,这里会用到能同时代表过去状态和现在状态的复合状态。
  • 具体的计算规则如下所示:
    规则 1:如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡。这时候,将细胞值改为 -1,代表这个细胞过去是活的现在死了;
    规则 2:如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活。这时候不改变细胞的值,仍为 1;
    规则 3:如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡。这时候,将细胞的值改为 -1,代表这个细胞过去是活的现在死了。可以看到,因为规则 1 和规则 3 下细胞的起始终止状态是一致的,因此它们的复合状态也一致;
    规则 4:如果死细胞周围正好有三个活细胞,则该位置死细胞复活。这时候,将细胞的值改为 2,代表这个细胞过去是死的现在活了。
  • 根据新的规则更新数组;现在复合状态隐含了过去细胞的状态,所以我们可以在不复制数组的情况下完成原地更新;
  • 对于最终的输出,需要将 board 转成 0,1 的形式。因此这时候需要再遍历一次数组,将复合状态为 2 的细胞的值改为 1,复合状态为
    -1 的细胞的值改为 0。
void gameOfLife(vector<vector<int>>& board) {
        //先定义8个坐标表示细胞的周围
        int x[8]={1,1,1,0,0,-1,-1,-1};
        int y[8]={1,0,-1,1,-1,1,0,-1};
        //遍历细胞板
        int rows=board.size();
        int cols=board[0].size();
        for(int i=0;i<rows;i++){
            for(int j=0;j<cols;j++){
                int livenums=0;
                //遍历8个方向
                for(int k=0;k<8;k++){
                    int r=i+x[k];
                    int c=j+y[k];
                    if(r>=0 && r<rows && c>=0 && c<cols ){
                        if(abs(board[r][c])==1)
                        livenums+=1;
                    }
                }
            //规则1  或者 规则3
            if((board[i][j]==1) && (livenums<2 || livenums>3)){
                board[i][j]=-1;
            }
            if(board[i][j]==0 && livenums==3){
                board[i][j]=2;
            }
            }
        }
        for(int i=0;i<rows;i++){
            for(int j=0;j<cols;j++){
                if(board[i][j]>0){
                    board[i][j]=1;
                }
                else {
                    board[i][j]=0;
                }
            }}
    }

八、前缀和 数组

303. 区域和检索 - 数组不可变

在这里插入图片描述
方法:前缀和
最朴素的想法是存储数组 nums 的值,每次调用 sumRange 时,通过循环的方法计算数组 nums 从下标 ii 到下标 jj 范围内的元素和,需要计算 j−i+1 个元素的和。由于每次检索的时间和检索的下标范围有关,因此检索的时间复杂度较高,如果检索次数较多,则会超出时间限制。
由于会进行多次检索,即多次调用 sumRange,因此为了降低检索的总时间,应该降低 sumRange 的时间复杂度,最理想的情况是时间复杂度 O(1)。为了将检索的时间复杂度降到 O(1),需要在初始化的时候进行预处理。

public:
    vector<int> sums;

    NumArray(vector<int>& nums) {
        int n = nums.size();
        sums.resize(n + 1);
        for (int i = 0; i < n; i++) {
            sums[i + 1] = sums[i] + nums[i];
        }
    }

    int sumRange(int i, int j) {
        return sums[j + 1] - sums[i];
    }
};

238. 除自身以外数组的乘积

给你一个长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除nums[i] 之外其余各元素的乘积。

方法二:空间复杂度 O(1) 的方法
思路
尽管上面的方法已经能够很好的解决这个问题,但是空间复杂度并不为常数。
由于输出数组不算在空间复杂度内,那么我们可以将 L 或 R 数组用输出数组来计算。先把输出数组当作 L 数组来计算,然后再动态构造 R 数组得到结果。让我们来看看基于这个思想的算法。

  1. 初始化 answer 数组,对于给定索引 i,answer[i] 代表的是 i 左侧所有数字的乘积。
  2. 构造方式与之前相同,只是我们试图节省空间,先把 answer 作为方法一的 L 数组
  3. 这种方法的唯一变化就是我们没有构造 R 数组。而是用一个遍历来跟踪右边元素的乘积。并更新数组answer[i]=answer[i]∗R。然后 R 更新为 ]R=R∗nums[i],其中变量 R 表示的就是索引右侧数字的乘积

    vector<int> productExceptSelf(vector<int>& nums) {
        int n=nums.size();
        vector<int> ans(n);
        ans[0]=1;
        for(int i=1;i<n;i++){
            ans[i]=ans[i-1]*nums[i-1];
        }
       
        // R 为右侧所有元素的乘积
        // 刚开始右边没有元素,所以 R = 1
        int R = 1;
        for (int i = n - 1; i >= 0; i--) {
            // 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R
            ans[i] = ans[i] * R;
            // R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上
            R *= nums[i];
        }
        return ans;

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值