题目一:移动零
问题分析
将数组中的所有零移动到末尾,保持非零元素的相对顺序,要求原地操作。
双指针解法
java
class Solution {
public void moveZeroes(int[] nums) {
int cur = 0; // 遍历指针
int dest = -1; // 非零元素放置位置
while (cur < nums.length) {
if (nums[cur] != 0) {
// 遇到非零元素,交换到dest位置
dest++;
int tmp = nums[cur];
nums[cur] = nums[dest];
nums[dest] = tmp;
}
cur++;
}
}
}
优化版本(减少交换次数)
java
class Solution {
public void moveZeroes(int[] nums) {
int nonZeroIndex = 0;
// 将所有非零元素移到前面
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[nonZeroIndex++] = nums[i];
}
}
// 将剩余位置填充为零
while (nonZeroIndex < nums.length) {
nums[nonZeroIndex++] = 0;
}
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
题目二:复写零
问题分析
将数组中的每个零复写一次,其余元素右移,超过数组长度的部分丢弃。
双指针解法
java
class Solution {
public void duplicateZeros(int[] arr) {
int n = arr.length;
int dest = -1;
int cur = 0;
// 第一步:模拟复写过程,找到原数组中最后保留的元素位置
while (dest < n - 1) {
if (arr[cur] == 0) {
dest += 2; // 零需要两个位置
} else {
dest++; // 非零只需要一个位置
}
if (dest < n - 1) {
cur++;
}
}
// 处理边界情况:最后一个元素是零且复写后刚好超出数组
if (dest == n) {
arr[n - 1] = 0;
dest -= 2;
cur--;
}
// 第二步:从后往前进行实际复写
while (cur >= 0) {
if (arr[cur] == 0) {
arr[dest] = 0;
arr[dest - 1] = 0;
dest -= 2;
} else {
arr[dest] = arr[cur];
dest--;
}
cur--;
}
}
}
详细步骤说明
- 模拟阶段:计算复写后需要的位置,确定哪些元素会被保留
- 边界处理:处理最后一个零导致数组溢出的特殊情况
- 实际复写:从后往前填充,避免覆盖未处理的元素
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
题目三:快乐数
问题分析
判断一个数经过各位平方和变换后是否能最终变为1,或者进入循环。
快慢指针解法
java
class Solution {
// 计算数字各位的平方和
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
public boolean isHappy(int n) {
int slow = n;
int fast = getNext(n);
// 快慢指针判断循环
while (fast != 1 && slow != fast) {
slow = getNext(slow); // 慢指针走一步
fast = getNext(getNext(fast)); // 快指针走两步
}
return fast == 1;
}
}
哈希表解法
java
class Solution {
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n);
n = getNext(n);
}
return n == 1;
}
}
复杂度分析
- 时间复杂度:O(log n)
- 空间复杂度:O(log n)
题目四:盛水最多的容器
问题分析
找出两条垂线,使得它们与x轴构成的容器能容纳最多的水。
双指针解法
java
class Solution {
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxArea = 0;
while (left < right) {
// 计算当前面积
int currentHeight = Math.min(height[left], height[right]);
int currentWidth = right - left;
int currentArea = currentHeight * currentWidth;
// 更新最大面积
maxArea = Math.max(maxArea, currentArea);
// 移动较短的边
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
}
证明正确性
移动较短边的证明:
- 面积由较短边的高度和宽度决定
- 移动较长边不会增加最小高度,但会减少宽度
- 移动较短边有可能找到更高的边,从而增加面积
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
题目五:有效三角形的个数
问题分析
统计数组中能组成三角形的三元组个数。
排序+双指针解法
java
class Solution {
public int triangleNumber(int[] nums) {
Arrays.sort(nums);
int count = 0;
int n = nums.length;
// 固定最大的边c
for (int c = n - 1; c >= 2; c--) {
int left = 0;
int right = c - 1;
while (left < right) {
// 三角形条件:a + b > c
if (nums[left] + nums[right] > nums[c]) {
// 所有left到right-1的数都与right、c能组成三角形
count += (right - left);
right--;
} else {
left++;
}
}
}
return count;
}
}
暴力解法(超时)
java
class Solution {
public int triangleNumber(int[] nums) {
int count = 0;
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
for (int j = i + 1; j < n - 1; j++) {
for (int k = j + 1; k < n; k++) {
if (nums[i] + nums[j] > nums[k] &&
nums[i] + nums[k] > nums[j] &&
nums[j] + nums[k] > nums[i]) {
count++;
}
}
}
}
return count;
}
}
复杂度分析
- 时间复杂度:O(n²)
- 空间复杂度:O(1)(排序使用O(log n))
题目六:两数之和
问题分析
在数组中找到两个数,使它们的和等于目标值。
哈希表解法
java
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> numToIndex = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
// 检查补数是否在哈希表中
if (numToIndex.containsKey(complement)) {
return new int[]{numToIndex.get(complement), i};
}
// 将当前数和索引存入哈希表
numToIndex.put(nums[i], i);
}
return new int[0]; // 题目保证有解,这里不会执行
}
}
双指针解法(数组有序时)
java
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
return new int[]{left, right};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[0];
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
题目七:三数之和
问题分析
找到所有不重复的三元组,使得三个数之和为0。
排序+双指针解法
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
// 跳过重复的a
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = n - 1;
int target = -nums[i]; // b + c = -a
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 跳过重复的b和c
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return result;
}
}
四数之和扩展
java
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 3; i++) {
// 跳过重复的a
if (i > 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < n - 2; j++) {
// 跳过重复的b
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
int left = j + 1;
int right = n - 1;
long remaining = (long)target - nums[i] - nums[j];
while (left < right) {
long sum = (long)nums[left] + nums[right];
if (sum == remaining) {
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
// 跳过重复的c和d
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
} else if (sum < remaining) {
left++;
} else {
right--;
}
}
}
}
return result;
}
}
复杂度分析
- 三数之和:O(n²)
- 四数之和:O(n³)
通用解题模板
双指针模板
java
// 通用双指针框架
public void twoPointerTemplate(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
// 根据条件移动指针
if (condition) {
left++;
} else {
right--;
}
}
}
哈希表模板
java
// 通用哈希表框架
public void hashMapTemplate(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])) {
// 找到解
return;
}
map.put(nums[i], i);
}
}
算法技巧总结
1. 双指针适用场景
- 对撞指针:有序数组的两数之和、盛水容器
- 快慢指针:判断循环、找中点
- 滑动窗口:子数组问题
2. 哈希表适用场景
- 快速查找:两数之和、重复元素检测
- 频率统计:字符频率、元素出现次数
- 映射关系:随机链表复制
3. 排序预处理
- 很多问题在排序后变得简单
- 注意排序的时间复杂度
- 考虑稳定性要求
4. 边界条件处理
- 空数组和单元素数组
- 重复元素处理
- 整数溢出(特别是四数之和)
这些题目涵盖了双指针和哈希表的核心应用,掌握这些技巧能够解决大多数相关的算法问题。