力扣283. 移动零
Problem: 283. 移动零
思路
首先我们来讲一下本题的思路
- 本题主要可以归到【数组划分/数组分块】这一类的题型。我们将一个数组中的所有元素划分为两段区间,左侧是非零元素,右侧是零元素

- 那解决这一类的题我们首先想到的就是【双指针算法】,学习过C语言的同学应该就可以知道指针是比较繁琐和复杂,如果有兴趣学习的同学可以看看我的这篇文章 链接
- 不过在这里呢我们不需要去使用
int*
这种指针,而是直接使用数组下标来充当指针即可
好,那我们就来看看这个双指针到底是怎样的,要如何去使用
- 两个指针的作用
- 【cur】: 从左往右扫描数组,遍历数组
- 【dest】:已处理的区间内,非零元素的最后一个位置
- 可以看到,
cur
是我们用来遍历数组的,从[cur, n - 1]
就是还未处理的元素;那么从[0, cur]
就是已经处理过的元素,但是呢本题的要求是我们要划分出【零元素】与【非零元素】,所以呢前面的区间我们可以再度划分为[0, dest]
和[dest + 1, cur - 1]

小结一下:
[0, dest] [dest + 1, cur - 1] [cur, n - 1]
[0, dest]
------ 非零元素[dest + 1, cur - 1]
------ 零元素[cur, n - 1]
------ 未处理元素
算法图解分析
接下去我们就通过画算法图解的形式来模拟一下解题的过程
- 我们就以题目中所给出的第一个示例为例来进行讲解,因为在一开始我们还没处理过任何的非零元素,所以对于
[0, cur - 1]
这段区间是没有任何数据的,所以在一开始我们可以将【dest】这个指针置于-1的位置

- 因为我们需要将非0元素移动到前面,所以呢如果遇到了0元素的话,
cur++
即可,将其留在这个位置上

- 那当我们遇到非0元素时,就需要将其交换到前面去,那我们
[0, dest]
这个区间就是用来存放非0元素的,此时多了一个元素的话那dest
就要加1,原本其是指向-1这个位置,那我们可以使用++dest
来完成

- 接下去,当数据交换过来后,我们可以去对照上面的这三个区间,可以发现最左侧是非0元素,中间是0元素,右侧呢则是待处理的元素。接下去我们又碰到了0元素,所以
cur++

cur
再后移之后呢,我们又碰到了非0元素,继续让dest
上来然后交换二者位置上的元素

- 那现在我们再来看这三个区间,左侧还是保持为【非0元素】,中间为【0元素】,右侧的话则是【待处理的元素】

- 然后碰到非0元素后,继续让
++dest
,然后做交换

- 最后的话我们来看看这个处理完后的整个区间元素:非0元素都在前面,而0元素则都在后面,
[cur, n - 1]
的这段区间也不存在了,说明已经没有待处理元素了

复杂度
接下去我们来分析一下本题的时空复杂度
- 时间复杂度:
本算法的核心思路参考的是【快速排序】的区间划分,我们这里就是在不断遍历数组的过程中,以中间的0作为分割,然后左侧是非0元素,右侧是未处理的元素。在处理的过程中我们只是遍历了一次这个数组,所以复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)
- 空间复杂度:
在本题中我们并没有去开出额外的空间,所以复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)
Code
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
for(int dest = -1, cur = 0; cur < nums.size(); ++cur)
{
if(nums[cur] != 0)
{
swap(nums[++dest], nums[cur]);
}
}
}
};
力扣1089. 复写零
Problem: 1089. 复写零
题目解析
首先我们来分析一下本题的题目意思
- 可以看到题目中给到了一个数组,意思是让我们将数组中的零元素都复写一遍,然后将其余的元素向后平移

- 光就上面这样来看还是不太形象,我们通过画图来分析一下,通过下图我们可以看到,凡是0的都复写了两遍,凡不是0的都复写了一遍

- 但是呢题目中很明显地讲到只能让我们在数组上进行就地操作,但是就我们上面的操作而言则是在另外开辟了一块数组的空间
那在下面我们就去考虑一下在数组原地的操作
- 可以看到在下面我使用到了双指针的操作,若是
cur
遍历到0的话就进行两次的复写操作,不过呢大家可以看到在第一次的复写操作完成之后,【2】被覆盖了,但是这个【2】是我们需要的,那也就造成了一定的问题

💬 那么反应快的同学可以意识到,如果要进行覆盖操作的话就需要 从后往前 进行遍历操作才可以
算法原理分析
好,接下去呢我们就来分析一下解决本题的思路
找到最后一个复写的位置
- 上面说到是要从后往前开始做复写操作,那么第一步我们所要做的就是找到最后一个复写的位置,即让这个
dest
指向最后的0

那要怎么去找呢?(头一次尝试幻灯片≧ ﹏ ≦)
可以分为以下几步:
- 判断cur位置的值,决定dest走一步还是两步
- 判断dest是否到达末尾,决定cur是否++
<,
,
,
,
,
,
>
但是呢,就上面这样的逻辑去走的话其实是不对的,因为我们还未考虑到特殊的边界情况
- 即下面的这种情况,当测试用例的倒数第二个数为0的时候,此时
dest
又刚好到这个位置,那么就需要向后移动两步,此时就造成了越界问题

所以此时我们应该要考虑处理一下这个边界问题
- 因为倒数第二个数为0,那么对其进行复写操作的话,最后一个也是0,我们将其做一个修改即可,不过呢两个指针
cur
和dest
也需要去做一个变化,cur
前移一位即可,dest
因为做了复写操作,所以需要前移两位

从后往前进行复写操作
上面呢,我们已经找到了需要复写的最后一个位置,那接下去我们就要正式开始复写操作了
- 这一块的话就不做动画演示了,读者可以试着自己去手动模拟一下,也就是从我们上面所找到的
cur
位置开始,慢慢地向前遍历然后去做复写操作即可,将数一一地复写到dest
所在的位置,如果arr[cur]
为0的话,那我们就需要考虑复写两次了

代码展示
最后来展示一下整体的代码
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
// 1.找到复写的最后一个位置
// (1) 判断cur位置的值,决定dest走一步还是两步
// (2) 判断dest是否到达末尾,决定cur是否++
int dest = -1;
int cur = 0;
int sz = arr.size();
while(dest < sz)
{
if(arr[cur]) dest++;
else dest += 2;
if(dest >= sz - 1)
break;
cur++;
}
// 2.判断边界的情况
if(dest == sz)
{
arr[dest - 1] = 0;
cur--;
dest -= 2;
}
// 3.从右往左复写0
while(cur >= 0)
{
if(arr[cur]) arr[dest--] = arr[cur--];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
};
下面是运行后的结果

力扣11. 盛最多水的容器
Problem: 11. 盛最多水的容器
题目解析
首先我们来解析一下本题
题目中说到,要找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
- 那我们现在来看最外侧的两根,一个高度为8,一个则为7,那我们肯定选择高度为7的,如果选择8的话就会出现溢出的问题 ;我们这里要求解的是可以容纳多少的水分,所以便要计算的是【容量】,那从第一根柱子到第八根的距离是多少呢?即
8 - 1 = 7
- 所以最后的容量即为
7 * 7 = 49

- 那我们再来看一个,高度取小的那个为6,宽度则取4,所以最后的容积为
4 * 6 = 24
,比49
要来的小,但我们要取的是最大的那一个容量,所以还是取 49

💬 所以对于本题来说,我们初步的想法就是不断地去找两根柱子,然后计算出这两根柱子之间的所围成的容积大小,最后我们所要的则是最大的那一个容积
算法原理讲解
接下去呢我们再来讲解一下本题的算法原理
- 首先的话来讲解一下第一种方法,那就是我们同学最喜欢使用的【暴力枚举】,因为我们是不断地一一比较,所以直接使用双层for循环去进行实现即可。不过呢这种写法我试了一下是会超时的,所以立马放弃❌

- 接下去第二种,也是我要进行重点讲解的,那利用单调性,然后使用【双指针】来进行求解。因为我们在对两根柱子不断进行比较的时候,数字都会不停地发生变化,那么这里就会有两个情况:
- 第一种呢是比较的数字开始出现缩减的情况,即
w
变小;而且距离也开始缩减,即h
变小,那w
和h
都进行缩减的话,最后的乘积[v]
也会变得小 - 第二种的话则是所计算的数据不变,新的数据发生了放大,所以呢
h
不会缩小,不过距离的话还是会发生缩减,此时整体[v]
也会变得小
- 第一种呢是比较的数字开始出现缩减的情况,即

那根据上面的分析,我们呢可以使用双指针去模拟遍历两个x轴的数据
- 看到下面我们直接从两侧开始进行计算,那么在计算完得出第一个容量
v1
后,我们便可以直接舍弃这个【1】,因为其再与任何结合计算都会比【1】与【7】要来得小,原因在于距离会发生一个缩减

- 那接下去还是一样的思路,我们在使用双指针进行遍历的时候,只需要去判断二者的大小即可,左侧小的话就右移,右侧小的话就左移 ,然后记录下每一个容量
v1
、v2
、v3
,最后的话再去做一个比较即可

复杂度
- 时间复杂度:
对于时间复杂度而言,因为我们就是使用左右指针在遍历原先的数组,所以呢复杂度即为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)
- 空间复杂度:
因为没开辟多余的空间,所以空间复杂度, 示例: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)
Code
以下是代码展示,读者可以根据我上面所分析的思路,自行去书写一下代码
- 可以看到,我在这里定义两个左右指针
left
和right
,然后呢通过循环去遍历并计算它们两个位子上的数,计算的方法就是我们上面所讲,记住要去不断地更新最大值 - 当一轮计算完成之后不要忘记去更新
left
和right
。最后当这个循环结束再去返回计算出来的最大值即可。
cpp
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int ret = 0;
while(left < right)
{
int v = min(height[left], height[right]) * (right - left);
ret = max(v, ret); // 更新最大值
if(height[left] < height[right]) left++;
else right--;
}
return ret;
}
};
