注:自2023下半年开始,力扣里"剑指 Offer"前缀统一改成了"LCR"
目录
[283. 移动零 - 力扣(LeetCode)](#283. 移动零 - 力扣(LeetCode))
[1089. 复写零 - 力扣(LeetCode)](#1089. 复写零 - 力扣(LeetCode))
[202. 快乐数 - 力扣(LeetCode)](#202. 快乐数 - 力扣(LeetCode))
[11. 盛最多水的容器 - 力扣(LeetCode)](#11. 盛最多水的容器 - 力扣(LeetCode))
[611. 有效三角形的个数 - 力扣(LeetCode)](#611. 有效三角形的个数 - 力扣(LeetCode))
[1. 两数之和 - 力扣(LeetCode)](#1. 两数之和 - 力扣(LeetCode))
[LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)](#LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode))
[LCR 007. 三数之和 - 力扣(LeetCode)](#LCR 007. 三数之和 - 力扣(LeetCode))
[18. 四数之和 - 力扣(LeetCode)](#18. 四数之和 - 力扣(LeetCode))
283. 移动零 - 力扣(LeetCode)

🍉算法逻辑:
①定义两个变量des、cur,起始位置分别为-1、0

②规定cur在遇到非零元素时,交换"des+1"位置的元素,des++
cur在遇到零元素时不做处理

③这样一来就达到了以下图示的样子:

🌰代码演示:
java
class Solution1 {
public void moveZeroes(int[] nums) {
int destination = -1;
for (int cur = 0; cur < nums.length; cur++) {
if(nums[cur] != 0){
swap(nums,destination+1,cur);
destination++;
}
}
}
public void swap(int[]array,int x,int y){
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
}
1089. 复写零 - 力扣(LeetCode)

🍉算法逻辑:
① 初始阶段演示:
如果
cur指向非零元素 :cur和des各向前走一步
如果
cur指向零元素 :cur向前走一步,des向前走两步(因为要复写一个零)

⬇️

推演一遍之后,可以看到复写完成后的数组应该是这个样子:

des 大多数情况下会超出数组范围,只有少数特例恰好停在末尾(比如现在这个😆)。
因此用 des >= n - 1 作为终止条件,可以同时覆盖"到达末尾"和"超出范围"两种情况。
②解决完寻找复写位置的问题,就需要解决一个特殊情景:
即des刚好处于数组末尾后一个位置(n),说明数组末尾位置(n-1)需要被复写为 0
直接手动设置,并将指针前移两位,避免后续循环中访问 arr[destination] 时(destination = n)发生数组越界

③ 上述两步处理完成后,从 destination 指向的位置开始,按既定规则从后向前复写元素,直到数组全部填满
🌰代码演示:
java
public void duplicateZeros(int[] arr) {
int destination = -1;
int cur = 0;
int n = arr.length;
//1、寻找最终cur跟des的位置
for (; cur < n; cur++) {
if (arr[cur] == 0) {
destination += 2;
} else {
destination++;
}
if (destination >= n - 1) {
break;
}
}
//2、解决边界情况
if(destination == n){
arr[n - 1] = 0;
cur--;
destination -= 2;
}
//3、从复写位置开始后移
while (cur >= 0){
if(arr[cur] != 0){
arr[destination] = arr[cur];
destination--;
}else {
arr[destination] = arr[cur];
arr[destination - 1] = arr[cur];
destination -= 2;
}
cur--;
}
}
202. 快乐数 - 力扣(LeetCode)

🍉算法逻辑:
①根据题目红线描述,一个数最终只有两种结果:
1、变成 1(快乐数)
2、进入循环(非快乐数)
②因此循环的终止条件就是:
要么当前数为 1
要么检测到环(快慢指针相遇)

③使用快慢指针时,fast 的初始值应该比 slow 快一步(即先计算一次平方和),否则两者起点相同,循环无法进入。
④另外要注意:每次更新指针时,必须用指针自身的值 来计算下一步,而不能一直用原始的 n,否则指针永远不会移动,导致死循环。
🌰代码演示:
java
class Solution3{
//计算数字各位的平方和
public int getSquare(int n){
int sum = 0;
while (n > 0){
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
public boolean isHappy(int n){
int slow = n;
int fast = getSquare(n);
while (fast != 1 && slow != fast){
slow = getSquare(slow); // 用slow更新slow
fast = getSquare(getSquare(fast)); // 用fast更新fast
}
return fast == 1;
}
}
这个是采用存放元素的形式,没有使用双指针的写法:
java
class Solution3 {
public boolean isHappy(int n) {
LinkedList<Integer> x = new LinkedList<>();
//不断拿到新的n值并判断
while (n != 1 && !x.contains(n)){
x.add(n);
n = getSquare(n);
}
return n == 1;
}
//计算数字各位的平方和
public int getSquare(int n){
int sum = 0;
while (n > 0){
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
}
11. 盛最多水的容器 - 力扣(LeetCode)

🍉算法逻辑:
① 这道题代码实现很简单,但背后的思想值得注意:
它利用了单调性原理,定义左右双指针从两端向中间移动。
每次比较两端高度,移动较矮的那一侧指针,因为容器的宽度一直在减小,只有放弃当前较短的边,才可能遇到更高的边来弥补宽度的损失。如此重复直到指针相遇,即可得到最大面积
🌰代码演示:
java
public int maxArea(int[] height) {
int left = 0;
int right = height.length-1;
int max = 0;
while (left != right){
int area = Math.min(height[left],height[right]) * (right-left);
max = Math.max(area,max);
if(height[left] >= height[right]){
right--;
}else {
left++;
}
}
return max;
}
611. 有效三角形的个数 - 力扣(LeetCode)

🍉算法逻辑:
① 这题逻辑上也很简单,排序后,只要两小边之和 ≤ 最大边,就不能构成三角形(单调性原理)

② 所以先排序,然后从右往左枚举最大边。每一轮用双指针:若 nums[left] + nums[right] > max,则累加 (right - left) 并将右指针左移(尝试更小的右边);否则将左指针右移(尝试更大的左边)。最后累加所有结果即可

🌰代码演示:
java
public int triangleNumber(int[] nums) {
//1、排序
//Java内置排序
Arrays.sort(nums);
int maxIndex = nums.length - 1;
int sum = 0;
//2、每一轮最大值都在换
while (maxIndex >= 2) {
//保证left与right每次循环都更新
int left = 0;
int right = maxIndex - 1;
while (left < right) {
if (nums[left] + nums[right] > nums[maxIndex]) {
//累加
sum += right - left;
right--;
} else {
left++;
}
}
maxIndex--;
}
return sum;
}
1. 两数之和 - 力扣(LeetCode)
LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)

🍉算法逻辑:
① 两数之和(LeetCode 1)本质是无序数组的暴力枚举。
而第二题(LCR 179 / 剑指 Offer 57)虽然也可以用暴力枚举,但由于数组有序,可以利用单调性提前结束循环,比无序数组的写法更优。
这里直接采用最优解法---双指针
② 利用单调性,总和大于target则right--,总和小于target则left++
🌰代码演示:
暴力枚举:
java
//暴力枚举无序数组
public int[] twoSum2(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if(nums[i] + nums[j] == target){
return new int[]{i,j};
}
}
}
//照顾编译器
return new int[]{};
}
双指针:
java
public int[] subarraySum(int[] nums, int target) {
//1、排序
Arrays.sort(nums);
int left = 0;
int right = nums.length - 1;
while (left < right){
if( nums[left] + nums[right] == target ){
return new int[] { nums[left], nums[right] };
}else if( nums[left] + nums[right] > target ){
right--;
}else {
left++;
}
}
//照顾编译器
return new int[]{};
}
LCR 007. 三数之和 - 力扣(LeetCode)

🍉算法逻辑:
①三数之和,又相当于双指针解决两数之和的延申。
数组排序
固定一个数 nums[i] 作为 target
在当前target之后,用双指针寻找满足条件的两数之和

②需要着重注意的两点:不漏、去重
不漏:当找到一组符合条件的组合时,左右指针继续移动 (左指针右移、右指针左移),继续寻找当前 target 下的其他解,而不是直接 break
去重:1、左边去重、右边去重、target去重 2、要考虑到去重时的边界情况

🌰代码演示:
暴力枚举+set去重(两个deBuff):
java
//写法一:暴力枚举
public List<List<Integer>> threeSum(int[] nums) {
//1、排序
Arrays.sort(nums);
//2、利用Set去重
Set<List<Integer>> result = new HashSet<>();
//3、三重循环枚举所有三元组
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
for (int k = j + 1; k < nums.length; k++) {
if(nums[i] + nums[j] + nums[k] == 0){
//4、将满足条件的三元组转换成List放入Set
result.add(Arrays.asList(nums[i],nums[j],nums[k]));
}
}
}
}
return new ArrayList<>(result);
}
双指针+手动去重:
java
//写法二:双指针
public List<List<Integer>> threeSum2(int[] nums) {
//1、排序
Arrays.sort(nums);
List<List<Integer>> list = new LinkedList<>();
//2、双指针
int left;
int right;
int i = 0;
int n = nums.length - 2;
while (i < n){
//小优化
if(nums[i] > 0){
break;
}
left = i + 1;
right = nums.length - 1;
while (left < right){
if(nums[left] + nums[right] + nums[i] == 0){
list.add(Arrays.asList(nums[i],nums[left],nums[right]));
//找到了也不能停下
//此处可结合单调性理解
left++;
right--;
//分别对左右去重
//每次去重时都检查下是否越界了
while (left < right && nums[left] == nums[left - 1]){
left++;
}
while (left < right && nums[right] == nums[right + 1]){
right--;
}
}else if(nums[left] + nums[right] + nums[i] > 0){
right--;
}else {
left++;
}
}
i++;
//对i去重
//为啥不写成i<left? 因为left不固定,一直在移动
while (i < n && nums[i] == nums[i - 1]){
i++;
}
}
return list;
}
18. 四数之和 - 力扣(LeetCode)
🍉算法逻辑:
①比三数之和多固定一个数 ,因此外层多一层循环
②每多固定一个数,就多一次去重操作
③最内层依然使用双指针解决两数之和
🌰代码演示:
java
public List<List<Integer>> fourSum(int[] nums, int target) {
//1、排序
Arrays.sort(nums);
List<List<Integer>> list = new LinkedList<>();
//2、双指针
int left;
int right;
int x = 0;
int i = 0;
int n = nums.length - 2;
while (x < n - 1){
i = x + 1;
while (i < n){
left = i + 1;
right = nums.length - 1;
while (left < right){
long sum = (long) nums[x] + nums[left] + nums[right] + nums[i];
if(sum == target){
list.add(Arrays.asList(nums[x],nums[i],nums[left],nums[right]));
left++;
right--;
while (left < right && nums[left] == nums[left - 1]){
left++;
}
while (left < right && nums[right] == nums[right + 1]){
right--;
}
}else if(sum > target){
right--;
}else {
left++;
}
}
i++;
//对i去重
while (i < n && nums[i] == nums[i - 1]){
i++;
}
}
x++;
//对x去重
//注意这里的边界
while (x < n - 1 && nums[x] == nums[x - 1]){
x++;
}
}
return list;
}
本专题完

