本文选取八道算法题目,对题目进行详解,对算法原理进行图文并茂的讲解,最终附上完整答案!
目录
[283 移动零](#283 移动零)
[1089 复写零](#1089 复写零)
[202 快乐数](#202 快乐数)
[11 盛水最多的容器](#11 盛水最多的容器)
[611. 有效三角形的个数](#611. 有效三角形的个数)
[179. 和为 s 的两个数字](#179. 和为 s 的两个数字)
[18 四数之和](#18 四数之和)
283 移动零
题目描述
给定一个数组 nums,编写一个函数,将所有的 0 移动到数组的末尾,同时保存非零元素的相对顺序。注意:必须在不复制数组的情况下,原地对数组进行操作。
即:将最终要把 nums 操作为:非零元素,都在零元素之前,且非零元素的相对顺序不变。且只能在这个数组本身中进行操作。
示例 1:
输入:nums = [0, 1, 0, 3, 12]
输出:[1, 3, 12, 0, 0]
示例 2:
输入 nums = [0]
输出:[0]
算法原理
可以将这道题目抽象为一种,数组分两块的题型,即,将数组的内容,分为左右两部分

当遇到这种数组划分,数组分块的时候,我们可以考虑双指针算法来解决问题(注意:这里的双指针,指的不是 C 中的地址指针,我们这里是利用数组的下标来充当指针)

dest 和 cur 两个指针,将整个数组分成了三个区间。
0, dest\]:非零区间 \[dest + 1, cur - 1\]:0 \[cur, n - 1\]:待处理  cur 指针向后移动,直到 cur 到了 n - 1 位置,即处理完所有数据后:也就可以返回数组了\~  回到我们的题目上:举出具体的栗子:\[ 0, 1, 0, 3, 12
dest 指针的位置,是已处理区间内,非零元素的最后一个位置,cur 最开始指向第一个位置,一个元素都没有处理,且也没有非零元素,所以 dest 的位置可以先置为 -1

cur 遇到 0 元素,直接向后移动一个元素,dest 应该指向的是已处理区间中的最后一个非零元素,dest 不变

当 cur 遇到非 0 元素,则根据题意,要让这个非零元素,加入到前面的非 0 区域。此时,我们可以让 dest 后移一个位置,指向 0 元素,然后交换非 0(1) 和 0 的位置。

cur 再 ++ 即可~

接下来的流程如上面一样,cur 遇到 0 元素,则直接向后 ++,当遇到 非 0 元素时,dest 指针 ++ 指向后面的 0 元素,然后交换 cur 指向的非 0 和 dest 指向的 0。直到 cur 遍历完整个数组~


完整代码
时间复杂度为 O(n) 空间复杂度为 O(1)
java
class Solution {
public void moveZeroes(int[] nums) {
int cur = 0, dest = -1, n = nums.length;
for (cur = 0; cur < n; cur++) {
// cur找到非0元素,进行交换
if (nums[cur] != 0) {
int temp = nums[++dest]; // 注意是前置++,dest先++找到非0元素
nums[dest] = nums[cur];
nums[cur] = temp;
}
}
}
}
1089 复写零
题目描述
有一个长度固定的整数数组 arr,需将该数组中出现的每个 0 都复写一遍,并将其余的元素向右平移。
注意:不要再超过数组长度的位置写入元素,对输入的数组就地进行修改。
示例 1:
输入:arr = [1, 0, 2, 3, 0, 4, 5, 0]
输出:[1, 0, 0, 2, 3, 0, 0, 4]
示例 2:
输入:arr = [1, 2, 3]
输出:[1, 2, 3]
算法原理
根据题目描述,如果没有下面的就地修改的注意事项,我们很容易想到再开辟一个数组,用双指针来进行异地操作:

开辟一个新的数组,cur 用来扫描元素,dest 用来指向最终的位置,然后 cur 进行判断
cur 指向非 0,dest 直接将元素添加进入新的数组,cur 指向 0,dest 向右添加两个 0,直到 dest 超出数组的长度,cur 结束判断

但由于题目要求是就地 操作,我们可以将上面的异地双指针操作 ,优化为,双指针的就地操作。

如果 dest 和 cur 都从数组最左侧开始遍历,因为题目要求的是复写 0,就会导致元素覆盖:
当 cur 指向 0 时候,dest 要向右复写两个 0,导致元素 2 被覆盖,当 cur 再向右移动,就找不到 2 了

可以从右向左移动 cur 和 dest,cur 最初指向最后一个复写的数据(4)的位置

这样从右向左复写,就可以实现任务

那么问题来了,如何找到最后一个复写的数据的位置呢? ==》 双指针算法

双指针算法中,dest 的位置,我们在上一题已经反复强调了,应该是已处理区域内的非 0 区间内的最后一个位置。最开始不知道这最后一个位置在哪里,所以先定义为 -1

cur 向后遍历数组,来决定 dest 向后一步还是两步


按照这样的步骤,cur 向后遍历,直到 dest 到结束位置,此时,cur 指向的就是最后一个复写的元素,此时,dest 指向了要开始抄写的位置:
补充:这里还会存在一种特例情况发生,如下:

如果数据为上面这样,当我们按照双指针算法向后寻找的时候,最终的结果是 cur 的位置正确,依然能找到最后一个复写的元素,但是 dest 的位置已经越界访问了~

即,这里需要处理一下,当最后一个 cur 为 0 的时候,dest 可能出现的越界情况
当 cur 指向的最后一个复写的元素为 0 时候,向左复写,dest 要将自己的位置,和 dest - 1 的位置,都复写为 0

但 dest 此时已经越界,指向的是 n 位置,我们仅仅需要 n - 1 位置变为0,然后让 cur--,dest -= 2 即可,就相当于,在这里提前处理了一位复写 0 的操作

总结步骤:
- 先用双指针算法,找到最后一个复写的元素
1.1 先判断 cur 位置的值
1.2 决定 dest 向后移动一步还是两步
1.3 判断 dest 是否到结束位置
1.4 cur++
处理一下边界情况:当最后一个 cur 为 0 的时候,dest 可能出现越界情况,此时仅需让 n - 1位置变为 0,然后 cur--,dest -= 2 即可~
- 从后向前,完成复写操作
完整代码
java
class Solution {
public void duplicateZeros(int[] arr) {
int cur = 0, dest = -1, n = arr.length;
// 1. 先找到最后一个需要复写的数
while (cur < n) {
if (arr[cur] == 0) {
dest += 2;
} else {
dest++;
}
if (dest >= n - 1) break;
cur++;
}
// 2. 处理一下边界情况
if (n == dest) {
arr[n - 1] = 0;
cur--;
dest -= 2;
}
// 3. 从后向前进行复写操作
while (cur >= 0) {
if (arr[cur] != 0) {
arr[dest--] = arr[cur--];
} else {
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
}
202 快乐数
题目描述
编写一个算法来判断一个数 n 是否是快乐数
快乐数定义:
对于一个正整数,每一次将该数替换为它每个位置上数字的平方和
然后重复这个过程直到这个数变为 1,也有可能是无限循环,但始终变不到 1
如果这个过程结果为 1,那么这个数就是快乐数
如果 n 是快乐数就返回 true,不是则返回 false
示例 1:
输入:n = 19
输出:true
解释:n
19 -> 1 * 1 + 9 * 9 = 82
82 -> 8 * 8 + 2 * 2 = 68
68 -> 6 * 6 + 8 * 8 = 100
100 -> 1 * 1 + 0 * 0 + 0 * 0 = 1

示例 2:
输入:n = 2
输出:false
2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 -> 16(数字 16 重复,则会陷入循环,且最终结果肯定不会是 1)

为了方便叙述,我们将题目中" 对于一个正整数,每一次将该数替换为它每个位置上数字的平方和"这个操作,称为 x 操作。
题目告诉我们,当我们不断重复 x 操作的时候,计算结果一定会"死循环",死的方式有两种:
情况 1:一直在 1 中死循环 1 -> 1 -> 1 -> 1 -> 1
情况 2:在历史的数据中死循环,但始终变不到 1
由于上述两种情况只会出现一种,因此,只要我们能确定,循环是在情况 1 出现,还是在情况 2 出现,就一定能得到结果。
算法原理
由题目描述理解,我们发现,这道题关键是要判断链表是否有环,我们已经由题目得知,所给的数据一定可以成环,重点研究的就是,成环是情况 1 还是情况 2。
解法:快慢双指针
-
定义快慢指针
-
慢指针每次向后移动一步,快指针每次向后移动两步
-
判断相遇时候的值即可
这里补充一个鸽巢原理, 来说明一下,为什么一定会成环。

总结:
由题目分析和鸽巢原理,我们可知,当重复执行 x 操作的时候,数据一定会陷入到一个循环之中。而快慢指针,有一个特性,就是在一个圆圈中,快指针,总是会追上慢指针的,也就是说,他们总会在一个位置上相遇。如果相遇的位置是 1,那么这个数就是快乐数,如果相遇位置不是 1,那么就不是快乐数。
补充:如果求数 n 每个位置上的数字的平方和:
- 把 n 的每一位都提取出来
1.1 int t = n % 10 // 提取一位
1.2 n /= 10 // 干掉一位
直到 n 的值变为 0
- 提取每一位的时候,用一个变量 sum来记录这一位的平方与之前提取位数的平方和
sum += t * t
完整代码
java
class Solution {
public int bitSum(int n) {
int sum = 0;
while (n != 0) {
int t = n % 10;
sum += t * t;
n /= 10;
}
return sum;
}
public boolean isHappy(int n) {
int slow = n, fast = bitSum(n);
while (slow != fast) {
slow = bitSum(slow);
fast = bitSum(bitSum(fast));
}
return slow == 1;
}
}
11 盛水最多的容器
题目描述
给定一个长度为 n 的整数数组 height,有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。
找出其中的两条线,使得他们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
示例 1:

输入:1,8,6,2,5,4,8,3,7
输出:49
解释:图中垂直线为数组[1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49(7 * 7)
示例 2:
输入:height = [1,1]
输出:1
注意:题目要求的是容器能够容纳水的体积,即底部的长度 * 高的长度。在示例 1 中

两个标红数据之间的长度为 8 - 1 = 7 高度为短的那一边 7 ==》 7 * 7 = 49
算法原理
解法一:暴力枚举,即分别求取每两个的数据之间的体积,然后比较~

解法二:利用单调性,使用双指针来解决问题
首先我们先理解单调性:

如上图,left 指向高度 1,right 指向高度 7,容积是 8 * 1 = 8
如果移动 right(较高的柱子),容积只会更小或者不变。
原因:容器的容积由两个因素决定:两根柱子之间的高度(底边长),两根柱子中较矮的那个高度(高度)
两根柱子的距离一定会缩小(指针向内)。
right 移动后,如果遇到的是比 right 还高的柱子,高度受较矮的柱子影响,不会变化,底边长减小,容积减小。
如果遇到的是比 right 还低的柱子,容积肯定也会减小
如果遇到的是和 right 一样高的柱子,因为底边长减小了,所以容积也一定会减小
因此,我们可以移动较矮的柱子,移动较矮的柱子,虽然底边长还是会缩小,但是较矮的柱子,可能遇到更高的柱子,从而让矮的高度增加,最终使得整体容积增大
核心思路:
-
初始化两个指针,分别指向数组的左右两端
-
计算当前两个指针所能形成的容积
2.1 容积 = 底边长(right - left)* 高度(较矮柱子的高度)
-
保留最大值
-
移动指针:为了获得更大的容积,由单调性可得,应该移动较矮的柱子
-
重复 2 - 4 直到两个指针相遇
时间复杂度为 O(n),只有一个循环,指针移动的总次数不会超过 n 次
空河复杂度为O(1)
完整代码
java
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length - 1, ret = 0, h = 0, v = 0;
while (left < right) {
h = Math.min(height[left], height[right]);
v = (right - left) * h;
ret = Math.max(v,ret);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return ret;
}
}
611. 有效三角形的个数
题目描述
给定一个包含非负整数的数组 nums,返回其中可以组成三角形三条边的三元组个数
示例 1:
输入: nums = 2, 2,3,4
输出:3
解释:有效的组合是:2,3,4(使用第一个 2);2,3,4(使用第二个 2);2,2,3
示例 2:
输入:nums = 4,2,3,4
输出:4
解释:4,2,3;4,2,4;4,3,4;2,3,4
算法原理
构成三角形:任意两边之和大于第三边
但我们可以进行小小优化,即先对整个数组排序,然后三条边中的较小的两条边之和大于第三条边即可~
解法一:暴力枚举
三层 for 循环,枚举出所有的三元组 ==》 超时~
解法二:利用单调性,使用双指针算法来解决问题
可以先固定一个最长边,然后在比这条边小的有序数组中,找到一个二元组,使得这个二元组之和大于这个最长边(因为有序数组,且我们最开始先固定了一个最长边,所以二元组都比最长边短),这样就找到一组三角形。由于数组是有序的,我们遍历数组即可~
举例:
nums = 2, 2,3,4,5,9,10
可以先对整个数组进行排序,最长边就是数组最后面的数据。固定 c 为 10,left 指向 2,right 指向 10

第一种情况:left + right > c ,则 left 之后的元素,都比 left 大,与 right 相加,都比 c 大,都可以与 right,c 组成三角形,加上 left ,个数共有 right - left 个,然后 right--

第二种情况:left + right <= c,right 之前的元素,都比 right 小,与 left 相加,仍然都比 c 小,无法与 left,c 组成三角形,只能 left++。
核心思路:
-
数组排序
-
固定住最大的数
-
在最大的数的左区间中,使用双指针算法,快速统计出符合要求的三元组个数
时间复杂度为 O(n^2)排序占 O(n log n)双重循环(外层 n 次,内层总共 n 次)
空间复杂度 O(n log n),主要来自排序操作需要的栈空间
完整代码
java
class Solution {
public int triangleNumber(int[] nums) {
// 1. 排序
Arrays.sort(nums);
// 2. 利用双指针解决问题
int ret = 0;
int n = nums.length;
for (int i = n - 1; i >= 2; i--) { // 先固定最大的数
// 利用双指针快速统计出符合要求的三元组的个数
int left = 0, right = i - 1;
while (left < right) {
if (nums[left] + nums[right] > nums[i]) {
ret += right - left;
right--;
} else {
left++;
}
}
}
return ret;
}
}
179. 和为 s 的两个数字
LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)
题目描述
输入一个递增排序的数组,和一个数字 s,在数组中查找两个数,使得它们的和正好是 s。如果有多对数组的和为 s,则输出任意一对即可。
示例 1:
输入:nums = 2,7,11,15, target = 9
输出:2,7 或者 7,2
算法原理
解法一:暴力枚举 O(n^2)
两层 for 循环嵌套,循环出所有组合,看和是否为 target,是的话直接返回
解法二:利用单调性,使用双指针算法解决问题
举例:2,7,11,15,19,21 t = 30
left 指向 2,right 指向 21,记 left + right 为 sum
会有三种情况:
1: sum < t

此时,left 指向的元素 2,left 与 right 中间的元素 7,11,15,19,left 指向的元素 2 与最大的 right 指向的 21,相加之和都不满足 t,则 left 与中间的元素相加之和,更无法满足 t,所以可以放心的将 left 指向的元素 2 删除 ==》 left++
- sum > t

此时,right 指向的元素 21,left 与 right 中间的元素 15,19,right 指向的元素 21 与最小的 left 指向的 11,相加之和都大于 t,则 right 与中间的元素相加之和,更大于 t,所以可以将 right 指向的元素 21 删除 ==》 right--
- sum == t:直接返回结果即可~ 注意这里返回的是 nums[left] 和 nums[right]
时间复杂度:O(n)
空间复杂度:O(1)
完整代码
java
class Solution {
public int[] twoSum(int[] price, int target) {
int left = 0, right = price.length - 1;
while (left < right) {
int sum = price[left] + price[right];
if (sum < target) {
left++;
} else if (sum > target) {
right--;
} else {
return new int[]{price[left], price[right]};
}
}
return new int[]{0};
}
}
15.三数之和
题目描述
一个整数数组 nuns,判断是否存在三元组 nums[i] nums[j] nums[k] 满足 i j k 三者互不相同,同时满足,nums[i] nums[j] nums[k] == 0。返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组
示例 1:
输入:nums = -1,0,1,2,-1,-4
输出:-1,-1,2; -1,0,1
注意:输出的顺序和三元组的顺序并不重要
示例 2:
输入:nums = 0,1,1
输出:[ ]
解释:唯一可能的三元组的和不为0
算法原理
解法 1:排序 + 暴力枚举 + set 去重
解法 2:排序 + 双指针
-
对数组进行排序
-
固定一个数 a。此处有一个优化:此处固定的 a 必须是小于等于 0 的数。当 a 大于 0 的时候, -a 就是小于 0 的数,在后面的区间,都是大于 0 的数,不可能有和为小于 0 的数了
-
在该数 a 后面的区间内,利用双指针算法,快速找到两个和为 -a 的数据

但这道题,还需要处理不重 不漏 这两个细节问题:
- 不重:当找到一种结果后,left 和 right 两个指针,要跳过重复元素
当使用完一次双指针算法后,i 也需要跳过重复元素
- 不漏:找到一种结果之后,不要停,缩小区间,继续寻找
时间复杂度:O(n^2)
空间复杂度:O(1)
完整代码
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 1. 排序
Arrays.sort(nums);
// 2. 利用双指针解决问题
List<List<Integer>> ret = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n; ) { // 固定 a
if (nums[i] > 0) break; // 小优化
int taregt = -nums[i];
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum < taregt) {
left++;
} else if (sum > taregt) {
right--;
} else {
ret.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right])));
right--; left++; // 缩小区间继续寻找
// 去重 left right 且防止越界
while(left < right && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
}
}
// 对 i 去重
i++;
while (i < n && nums[i] == nums[i - 1]) {
i++; // 去重之后,i 已经指向了我们的元素 a 则在 for 循环中不需要最后 i++ 了~
}
}
return ret;
}
}
18 四数之和
题目描述
有一个由 n 个整数组成的数组 nums,和一个目标值 target。请找出并返回满足下述条件,且不重复的四元组:nums[a] nums[b] nums[c] nums[d]
0 <= a b c d <= n
a b c d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
示例 1:
输入:nums = 1,0,-1,0,-2,2 target = 0
输出:-2,-1,1,2; -2,0,0,2; -1,0,0,1
示例 2 :
输入:nums = 2,2,2,2 ,2 target = 8
输出:2,2,2,2
算法原理
解法:排序 + 双指针
-
依次固定一个数 a
-
在 a 后面的区间内,利用"三数之和",找到三个数,使得这三个数的和为 target - a
2.1 依次固定一个数 b
2.2 在 b 后面的区间内,利用双指针,找到两个数,使得这两个数的和为 target - a - b

在这个问题中,也需要处理 不重 和 不漏问题

完整代码
java
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ret = new ArrayList<>();
// 1. 排序
Arrays.sort(nums);
// 2. 利用双指针解决问题
int n = nums.length;
for (int i = 0; i < n; ) { // 固定数 a
// 3. 在 a 后面的区间,利用三数之和,找到三个数,
for (int j = i + 1; j < n; ) { // 固定数 b
// 在 b 后面的区间,利用"双指针" 找到两个数
// 使这两个数字的和等于 target - a - b
int left = j + 1, right = n - 1;
long aim = (long)target - nums[i] - nums[j];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum > aim) {
right--;
} else if (sum < aim) {
left++;
} else {
ret.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
left++; right--;
// 去重 1
while (left < right && nums[left] == nums[left - 1]) left++;
while (left < right && nums[right] == nums[right + 1]) right--;
}
}
// 去重 j
j++;
while (j < n && nums[j] == nums[j - 1]) j++;
}
// 去重 i
i++;
while(i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
}