目录
一、数组
题目35.搜索插入位置
总结:
题目27.移除元素
1.暴力解法
代码如下
2.双指针法
代码如下
总结:
题目15.三数之和
题目209.长度最小的子数组
1.暴力解法
2.滑动窗口解法
C++滑动窗口代码
题目59.螺旋矩阵II
C++代码
数组总结:
一、数组理论基础
数组的经典题目
二分法
双指针法
滑动窗口
模拟行为
1.二分查找第一种写法
class Solution {
public:
int searchInsert(vector& nums, int target) {
int n = nums.size(); //step1,获得数组长度
int left = 0;
int right = n - 1; //定义target在左闭右闭的区间里,[left,right]
while(left <= right){ //因为我们定义的左闭右闭的区间,所以left = right时仍然有效
int middle = (left + right)/2; //确定middle的位置,基本语句1
if(nums[middle] > target){
right = middle - 1; //target在左区间
}else if(nums[middle] < target){
left = middle + 1;
}else{
return middle;
}
}
//处理四种情况
//target在所有元素之前
//target等于某个元素 return middle
//target插入数组中 return right + 1
//target在数组所有元素之后 return right + 1
return right + 1;
}
};
时间复杂度: 设基本语句1执行次数为x1,则有2的x1次方等于数组长度n,所以x1=log以2为底n的对数,所以时间复杂度为O(logn)
空间复杂度: O(1):?
2.二分查找第二种写法
class Solution {
public:
int searchInsert(vector& nums, int target) {
int n = nums.size();
int left = 0;
int right = n; // 定义target在左闭右开的区间里,[left, right) target
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
int middle = left + ((right - left) >> 1); //'>> 1'相当于 / 2
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0,0)
// 目标值等于数组中某一个元素 return middle
// 目标值插入数组中的位置 [left, right) ,return right 即可
// 目标值在数组所有元素之后的情况 [left, right),return right 即可
return right;
}
};
时间复杂度和空间复杂度同上一种方法
在使用二分查找法时,关键是要确定区间的定义,确定要查找的区间是双闭还是一闭一开,这是变化过程中的不变量,然后在【二分查找的循环中,坚持循环不变量的原则】,其次是对问题的刨解,问题有几种可能,分别怎么解决等等
这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。
删除过程如下:
class Solution {
public:
int removeElement(vector& nums, int val) {
int size = nums.size();
for(int i = 0;i < size;i++){
if(nums[i] == val){ //发现需要移除的元素,就将i之后的元素集体移动
for(int j = i + 1;j < size;j++){
nums[j-1] = nums[j];
}
i--; //因为下表为i以后的数值都向前移动了一位,所以i也应该向前移动一位,再进行外层for循环的时候,才能比较现在i位置的数值和val的关系
size--;
}
}
return size;
}
};
时间复杂度:O(n^2)
双指针法(快慢指针法):「通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。」
class Solution {
public:
int removeElement(vector& nums, int val) {
int slowIndex = 0;
int size = nums.size();
for(int firstIndex = 0;firstIndex < size;firstIndex++){
if(nums[firstIndex] != val){
nums[slowIndex++] = nums[firstIndex];
}
}
return slowIndex;
}
};
时间复杂度:O(n)
双指针法在数组和链表的操作中非常常见,它的精髓在于将两个for循环幻化为一个for循环,利用一个快指针和一个慢指针,省去了内层for循环的替换赋值操作
大体思想如下:
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下表0的地方开始,同时定一个下表left 定义在i+1的位置上,定义下表right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i] b = nums[left] c = nums[right]。
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下表就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
时间复杂度:O(n^2)。
双指针法C++代码:
class Solution {
public:
vector> threeSum(vector& nums) {
vector> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
} else {
result.push_back(vector{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
class Solution {
public:
int minSubArrayLen(int target, vector& nums) {
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 >= target){ //一旦sum 超过或等于target,更新result
subLength = j - i + 1;
result = result < subLength ? result : subLength;
break; //因为找的是最短的子序列,所以一旦找到就退出内层循环,此时的最短,是以i为起点的最短
}
}
}
//如果result没有被赋值,则返回0,意思是没有满足条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
时间复杂度:O(n^2)
空间复杂度:O(1)
所谓滑动窗口,「就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果」。
这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:
最后找到 4,3 是最短距离。
其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。
在本题中实现滑动窗口,主要确定如下三点:
窗口内是什么?
如何移动窗口的起始位置?
如何移动窗口的结束位置?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。
解题的关键在于 窗口的起始位置如何移动,如图所示:
class Solution {
public:
int minSubArrayLen(int target, vector& nums) {
int result = INT32_MAX;
int sum = 0; //滑动窗口数值之和
int subLength = 0; //滑动窗口长度之和
int i = 0; //子序列的起点
for(int j = 0;j < nums.size();j++){ //j是滑动窗口的终端
sum += nums[j];
//这里使用while,判断子序列是否符合条件并更新i的值
while(sum >= target){
subLength = (j - i + 1);
result = result < subLength ? result : subLength;
sum -= nums[i++]; //滑动窗口的精髓,不断变更i
}
}
//如果result没有被赋值,则返回0,意思是没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
而求解本题依然是要坚持循环不变量原则。
模拟顺时针画矩阵的过程:
填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
由外向内一圈一圈这么画下去。
可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是「一进循环深似海,从此offer是路人」。
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。
那么我按照左闭右开的原则,来画一圈,大家看一下:
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。
这也是坚持了每条边左闭右开的原则。
一些同学做这道题目之所以一直写不好,代码越写越乱。
就是因为在画每一条边的时候,一会左开又闭,一会左闭右闭,一会又来左闭右开,岂能不乱。
class Solution {
public:
vector> generateMatrix(int n) {
vector> res(n,vector(n,0)); //使用vector定义一个二维数组
int startx = 0,starty = 0; //循环开始的位置
int loop = n / 2; //循环的次数,如n等于3,则循环的次数为2
int count = 1; //用来给矩阵的每一个格赋值
int mid = n / 2; //如果n是奇数,需要单独给中间的值进行赋值
int offest = 1; //控制遍历的列表的长度
int i,j;
while(loop--){
i = startx;
j = starty;
//以下四个for循环,分别对应了四个方向
//上行,从左到右
for(;j < starty + n - offest;j++){
res[i][j] = count++;
}
//右列,从上到下
for(;i < startx + n - offest;i++){
res[i][j] = count++;
}
//下行,从右到左
for(;j > starty;j--){
res[i][j] = count++;
}
//左列,从下到上
for(;i > startx;i--){
res[i][j] = count++;
}
//第二圈开始的时候,起始位置各自加一,例如第一圈是(0,0),第二圈就是(1,1)
startx++;
starty++;
//offest控制遍历的长度
offest += 2;
}
//如果n是奇数,则需要单独给矩阵中间位置的值赋值
if(n % 2){
res[mid][mid] = count;
}
return res;
}
};
首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题
「数组是存放在连续内存空间上的相同类型数据的集合。」
数组可以方便的通过下表索引的方式获取到下表下对应的数据。
举一个字符数组的例子,如图所示:
需要两点注意的是:
「数组下表都是从0开始的。」
「数组内存空间的地址是连续的」
正是「因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。」
例如删除下表为3的元素,需要对下表为3的元素后面的所有元素都要做移动操作,如图所示:
而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
「数组的元素是不能删的,只能覆盖。」
那么二维数组直接上图,大家应该就知道怎么回事了
「那么二维数组在内存的空间地址是连续的么?」
我们来举一个例子,例如:int[][] rating = new int[3][4];
, 这个二维数据在内存空间可不是一个 3*4
的连续地址空间
看了下图,就应该明白了:
所以「二维数据在内存中不是 3*4
的连续地址空间,而是四条连续的地址空间组成!」
在面试中,数组是必考的基础数据结构。
其实数据的题目在思想上一般比较简单的,但是如果想高效,并不容易。
我们之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。
数组:每次遇到二分法,都是一看就会,一写就废
这道题目呢,考察的数据的基本操作,思路很简单,但是在通过率在简单题里并不高,不要轻敌。
可以使用暴力解法,通过这道题目,如果要求更优的算法,建议试一试用二分法,来解决这道题目
暴力解法时间复杂度:O(n)
二分法时间复杂度:O(logn)
在这道题目中我们讲到了「循环不变量原则」,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
「二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力」。
数组:就移除个元素很难么?
双指针法(快慢指针法):「通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。」
暴力解法时间复杂度:O(n^2)
双指针时间复杂度:O(n)
这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点:
数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
C++中vector和array的区别一定要弄清楚,vector的底层实现是array,所以vector展现出友好的一些都是因为经过包装了。
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
数组:滑动窗口拯救了你
本题介绍了数组操作中的另一个重要思想:滑动窗口。
暴力解法时间复杂度:O(n^2)
滑动窗口时间复杂度:O(n)
本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
「滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。」
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。
数组:这个循环可以转懵很多人!
模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
在这道题目中,我们再一次介绍到了「循环不变量原则」,其实这也是写程序中的重要原则。
相信大家又遇到过这种情况:感觉题目的边界调节超多,一波接着一波的判断,找边界,踩了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实「真正解决题目的代码都是简洁的,或者有原则性的」,大家可以在这道题目中体会到这一点。
说明:本篇主要来自《代码回忆录》,不用于商业,纯属个人积累