相关博客
283. 移动零
看到题目最简单的想法应该是按顺序找出全部的非零元素,然后填充在数组的前面,最后剩下的全部赋值0。想到这种解法的原因是27. 移除元素,有异曲同工之妙。
public void moveZeroes(int[] nums) {
int index = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[index] = nums[i];
index++;
}
}
while (index < nums.length) {
nums[index] = 0;
index++;
}
}
提交当然也是成功的,但是翻了一下题解之后,发现一个很有意思的解法叫滚雪球解法。
public void moveZeroes(int[] nums) {
int snowBall = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == 0) {
snowBall++;
} else if (snowBall > 0) {
nums[i - snowBall] = nums[i];
nums[i] = 0;
}
}
}
把 0 想象成一个雪球,0 和 0 相遇的时候就会越滚越大,0 聚在一起会形成一个数组,而这个 snowBall 就是这个雪球数组的长度,i 是当前指针所在的地方。每次交换雪球中第一个0和雪球后面第一个非零元素,让雪球一直往后滚动,最后所有的0就会滚动到数组最后面。
11. 盛最多水的容器
这一题求两个柱之间的最大容量,看图很容易理解,就是短一点的柱子高度乘上两个柱子之间的距离。相当于是求两个数的最大乘积,暴力解法也就是双重for循环,但是 O(n²) 的时间复杂度很容易就超时了。
public int maxArea(int[] height) {
int length = height.length;
int result = 0;
for (int i = 0; i < length; i++) {
int tempArea = 0;
int lineHeight = height[i];
for (int j = i + 1; j < length; j++) {
tempArea = Math.min(lineHeight, height[j]) * (j - i);
result = Math.max(result, tempArea);
}
}
return result;
}
所以需要对解法进行优化,我们并不需要把每个柱子有可能的容器全求一遍,参考下面的图片,有两种移动方案
- 移动短边,面积变化不定,可大可小可不变
- 移动长边,面积一定变小
有了这两种移动方案的结果,我们只需要维护两个指针,短的指针向内移动,长指针不动,就可以写出时间复杂度为 O(n)
的解法
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int result = 0;
while (left < right) {
result = Math.max(result, Math.min(height[left], height[right]) * (right - left));
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return result;
}
可以简化一下代码
public int maxArea(int[] height) {
int i = 0, j = height.length - 1, res = 0;
while(i < j) {
res = height[i] < height[j] ?
Math.max(res, (j - i) * height[i++]):
Math.max(res, (j - i) * height[j--]);
}
return res;
}
15. 三数之和
题目需要找到三个数的组合,让他们的和为 0,最简单的解法就是三重for循环,但是这个解法时间复杂度太高了,不用写都知道会超时。如果我们先固定住一个指针,剩下两个指针就可以用上一题的思路进行移动,这样就是一个O(n²)
的时间复杂度。
其中,题目里最关键的一个点就是结果集不能重复,所以我们需要进行去重。但是数组是无序的,这就让去重的难度变高了,所以可以先对题目数组进行一个排序,这样也给双指针移动的规则简单了许多。
public List<List<Integer>> threeSum(int[] nums) {
// 结果集
List<List<Integer>> result = new ArrayList<>();
// 排序
Arrays.sort(nums);
// 如果排序后 第一个元素大于0,或者最后一个元素小于0,那么就不用继续执行了
if (nums[0] > 0 || nums[nums.length - 1] < 0) {
return result;
}
// 进入 for 循环
for (int i = 0; i < nums.length; i++) {
// 左指针从 i 后一个元素开始,右指针永远是最后一个元素
int left = i + 1;
int right = nums.length - 1;
// 如果 当前元素 和 上一个元素 相等,去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 定位剩下两个指针
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
// 由于现在数组有序,所以求和大于 0 就把右指针变小;求和小于 0 就把左指针变大
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
// 如果找到了符合条件的结果,加入结果集
result.add(List.of(nums[i], nums[left], nums[right]));
// 进行去重,移动右指针,跳过重复元素
while (right > left && nums[right] == nums[right - 1]) {
right--;
}
// 同样进行去重
while (right > left && nums[left] == nums[left + 1]) {
left++;
}
// 去重结束,正常移动指针
left++;
right--;
}
}
}
return result;
}
42. 接雨水
这是一道困难题,看到题目这张图片,和上一题乍一看挺像的,所以实际思路也差不多。
这一题并不是求最大的储水量,而是需要求这些柱子中间能存多少水,能储水的格子就是有左右两边都有高度差的位置,具体储水多少就是看左右两边谁更低一些,有了上一题的铺垫,理解题目应该会容易许多。
这一题有很多解法,目前就仿照上一题使用双指针的思路来解题,参考下面官方题解中提供的图解。
- 首先需要两个左右指针。两个指针慢慢向中间移动,当指针相遇的时候就结束循环。
- 那么怎么计算某一个位置的储水量呢?
- 首先这个位置的左右两边都要有一个比这个位置更高的柱子
- 储水量需要根据两边更高柱子中,矮一些的柱子高度减去当前指针的高度
- 如何移动指针呢?
- 两个指针分别指向头和尾,向中间移动
- 和上一题类似,优先移动短边,因为移动短边有概率让储水量变大
public int trap(int[] height) {
int result = 0;
int left = 0, right = height.length - 1;
int leftHeight = 0, rightHeight = 0;
while (left < right) {
// 更新左右两边的最高高度
if (height[left] > leftHeight) {
leftHeight = height[left];
}
if (height[right] > rightHeight) {
rightHeight = height[right];
}
// 统计结果
result += (leftHeight - height[left]) + (rightHeight - height[right]);
// 移动指针
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return result;
}