优算
1. 双指针
1.1 移动零

题目链接:移动零
其可以归类为 数组划分/数组分块 问题,一般使用 双指针算法 进行解题。
针对这道题,假定两个指针 cur / dest
作用分别是
cur: 从左向右遍历数组;dest: 表示已处理区间内( [0,cur - 1] ),某一特征元素的最后一个位置。
这两个指针将数组划分成了三个部分,
[0, dest] 非零数 [dest + 1, cur - 1] 零 [cur, n -1] 待处理数据
在 cur 从左向右遍历的过程中,
遇到 0: cur++;
遇到非 0: dest++(因为又有新的非零元素了);交换;cur++;
当 cur 遍历完数组,三个区域就只剩两个区域,即 dest及其左边的非零数;它右边的非零数。
java
class Solution {
public void moveZeroes(int[] nums) {
int len = nums.length;
for (int cur = 0, dest = -1; cur < len; cur++) {
if (nums[cur] != 0) {
dest++;
int tmp = nums[cur];
nums[cur] = nums[dest];
nums[dest]= tmp;
}
}
}
}
这个双指针进行数组分块,同样是 快速排序 的核心步骤。只不过 dest 表示 目标值
1.2 复写零

题目链接: 1089.复写零
同样使用双指针。
先完成 "异地" 操作,然后优化成双指针下的 "就地" 操作。
假定你整出了个新数组(题目要求在原数组的基础上操作),

这是 "异地" ,那么 "就地" 中,同样是这样移动指针。
设想如果从前开始复制,那么就会导致原数组元素被覆盖,后续全都是错的。像这样:

原数组中的 2 ,直接被覆盖了。
那从后先前试一试,假定 cur 指向 4 (即复写完成后,数组末尾值),dest 值为 n (数组长度),发现可以完成。
那么,找到最后一个值就很关键了。这里同样使用双指针来完成,不过不对数组元素进行操作。
四个步骤:
-
先看 cur 的值
-
dest 走几步
-
dest 是否到达末尾
-
cur++
找到之后,"从后向前"完成复写操作。
这里有边界条件需要处理,如果给定的数组是 【1,0,2,3,0,4】,那么当 cur 走到原数组最后一个 0 时,dest 走两步就会超出数组长度。
针对这种情况,这样处理:
-
将 n - 1 处的置为 0
-
dest -= 0
-
cur--
java
class Solution {
public void duplicateZeros(int[] arr) {
int cur = 0, dest = -1, n = arr.length;
while(cur < n) {
if (arr[cur] != 0) {
dest++;
} else {
dest += 2;
}
if (dest >= n - 1) break;
cur++;
}
if (dest == n) {
arr[n - 1] = 0;
cur--;
dest -= 2;
}
while (cur >= 0) {
if (arr[cur] != 0) {
arr[dest--] = arr[cur];
} else {
arr[dest--] = 0;
arr[dest--] = 0;
}
cur--;
}
}
}
1.3 快乐数

题目链接: 202. 快乐数
由题目 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1 知存在下面这种过程。

看起来是两种,但是可以抽象成一种:

快乐数圈上全是 1 。
发现和链表是否有环 这一问题图形一样。其实,原理也是一样的。使用快慢双指针 来解决。当快慢指针相遇时,通过判断相遇点是否为一来判断是否为快乐数。这里没有指针,所以就把 数作为指针 ,而题目的变换关系 f f f 作为 .next 操作。
为什么一定成环? -- 鸽巢原理
n 的最大值为 2,147,483,647. 这里为了好算,记为 9,999,999,999.一共十个九;
那么n 经过 f f f 变换 最大值即为 81 × 10 = 810 . 取值范围 [0, 810].
假定经过了 811次变化, 那么至少存在一个数出现了两次(即成环了).
java
class Solution {
public int getSum(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 = n;
do {
slow = getSum(slow);
fast = getSum(getSum(fast));
} while (slow != fast);
return slow == 1;
}
}
1.4 盛水最多的容器

题目链接: 11. 盛水最多的容器
方法一: 暴力枚举; 但是该题会超时。
方法二: 利用单调性,双指针解决。
先从示例一中取一小段。
此时较大值即为 两端中的较小值 × 长度。移动指向最小值的指针,则又可以确定一个体积值。这一连串的体积值中的最大值即为所求。
java
class Solution {
public int maxArea(int[] height) {
int max = 0, width = 0, high = 0;
int left = 0, right = height.length - 1;
while (left < right) {
high = Math.min(height[left],height[right]);
width = right - left;
if (max < width * high) {
max = width * high;
}
if (height[left] > height[right]) {
right--;
} else {
left++;
}
}
return max;
}
}
1.5 有效三角形的个数

题目链接: 611. 有效三角形的个数
解法一:暴力枚举
使用三层循环嵌套,完成。时间复杂度为 O( n 3 n^3 n3) 。
解法二:利用单调性,使用双指针解题
先对数组进行排序。这时只需要比较一次即可 例如 a < = b < = c a <= b <= c a<=b<=c ; 只要证明 a + b > = c a + b >= c a+b>=c 即可。暴力枚举则要比较三次。
对数组排序后,就可以利用它的单调性解题。
-
锚定最大值
-
在最大值左侧区域,使用双指针,快速统计出符合要求的三元组的个数

时间复杂度为 O( n 2 n^2 n2);
解法二的完整时间复杂度为 n l o g n + n 2 nlogn + n^2 nlogn+n2 ,相较于解法一的 (3n³) (常系数 3 通常来源于三角形不等式的三个条件检查) ,有了质的飞跃。
java
class Solution {
public int triangleNumber(int[] nums) {
Arrays.sort(nums);
int len = nums.length,count = 0;
for (int i = len - 1; i >= 2; i--) {
int left = 0, right = i - 1;
while(left < right) {
if (nums[left] + nums[right] > nums[i]) {
count += (right - left);
right--;
} else {
left++;
}
}
}
return count;
}
}
1.6 和为 s 的两个数

题目链接: LCR179. 查找总价格为目标值的两个商品
解法一:暴力解法 使用两个循环嵌套。 时间复杂度 O( n 2 n^2 n2)
解法二:利用单调性,使用双指针解题
时间复杂度 O( n n n) ;解题思想同前题相同。
java
class Solution {
public int[] twoSum(int[] price, int target) {
int left = 0, right = price.length - 1;
while (left < right) {
int sum = price[right] + price[left];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
return new int[] {price[left],price[right]};
}
}
return new int[2];
}
}
1.7 三数之和

题目链接: 15. 三数之和
通过题目值要对结果进行去重。
可以先得到结果在排序去重,也可以先排序再去重。
解法一:排序 + 暴力枚举 + set特性去重
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums); // 排序,方便去重和剪枝
Set<List<Integer>> result = new HashSet<>(); // 利用Set自动去重
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
if (nums[i] > 0) break; // 提前终止,因为数组已排序,后续不可能有解
for (int j = i + 1; j < n - 1; j++) {
for (int k = j + 1; k < n; k++) {
if (nums[i] + nums[j] + nums[k] == 0) {
// 将满足条件的三元组排序后存入Set(去重关键)
List<Integer> triplet = Arrays.asList(nums[i], nums[j], nums[k]);
result.add(triplet);
}
}
}
}
return new ArrayList<>(result); // 转换为List返回
}
}
解法二:排序 + 双指针
-
排序
-
固定一个数 a;因为已经排过序,所以 a <= 0。否则,三数之和不可能为零。
-
在 a 后面的区间内运用双指针,找到两数和为 -a。
细节问题:
- 去重
找到一种结果后,left 和 right 指针要跳过重复元素。完成一次双指针运算后,i也要跳过重复元素。注意:不要越界
- 不漏
找到一种结果后,left++, right-- 缩小区间,继续寻找。
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
int len = nums.length;
List<List<Integer>> ret = new ArrayList<>();
for (int i = 0; i < len - 2; ) {
if(nums[i] > 0) break;
int left = i + 1, right = len - 1, target = -nums[i];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
ret.add(new ArrayList<Integer> (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--;
}
}
i++;
while (i < len - 2 && nums[i] == nums[i - 1]) i++;
}
return ret;
}
}
1.8 四数之和

题目链接: 18. 四数之和
解法一:同 1.7 但是是四层嵌套
解法二:仍是 排序+双指针
-
先固定一个 a,再在右侧区域固定一个 b。
-
然后在 b 右侧区域用双指针找到 两数之和 为 target - a - b
-
同样,要做到不重(这里a,b都要跳过重复元素),不漏
注意事项:如果是target是负数时 target - a - b 该计算可能超出范围。所以要强转为 long ,转一个数就行了编译器会自动转其他数,不要转结果因为结果已经是超限的结果。
java
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
int len = nums.length;
for (int a = 0; a < len - 3;) {
for (int b = a + 1; b < len - 2; ) {
int left = b + 1, right = len - 1;
long aim = (long)target - nums[a] - nums[b];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum < aim) {
left++;
} else if (sum > aim) {
right--;
} else {
list.add(Arrays.asList(nums[a],nums[b],nums[left],nums[right]));
left++;
right--;
while (left < right && nums[left] == nums[left - 1]) left++;
while (left < right && nums[right] == nums[right + 1]) right--;
}
}
b++;
while (b < len - 2 && nums[b] == nums[b - 1]) b++;
}
a++;
while (a < len - 3 && nums[a] == nums[a - 1]) a++;
}
return list;
}
}