Appearance
数组与双指针
数组是面试中最基础也最高频的数据结构,双指针技巧则是解决数组问题的核心武器。本模块涵盖两数之和、三数之和、接雨水等经典题目,掌握快慢指针、左右指针、滑动窗口等核心思想,能帮你高效解决大多数数组类问题。
Q1: 两数之和 「🟢 初级」
题目描述:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的两个整数,返回它们的数组下标。你可以假设每种输入只会对应一个答案,且同一个元素不能使用两次。
考察点:哈希表的应用、数组遍历、空间换时间思想。
解题思路:
- 方法1:暴力解法,两层循环遍历所有数对(时间O(n²),空间O(1))
- 方法2:哈希表,一次遍历,用哈希表存储已访问的数字及其索引(时间O(n),空间O(n))- 最优解
代码实现:
java
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i);
}
return new int[]{-1, -1};
}复杂度分析:时间复杂度 O(n),仅遍历一次数组;空间复杂度 O(n),哈希表最多存储 n 个元素。
变体扩展:
- 变体1:三数之和
- 变体2:四数之和
- 变体3:两数之和II(有序数组,双指针解法)
- 变体4:两数之和III(数据结构设计)
Q2: 三数之和 「🟡 中级」
题目描述:给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k,同时还满足 nums[i] + nums[j] + nums[k] == 0。请你返回所有和为 0 且不重复的三元组。
考察点:排序 + 双指针、去重技巧、三指针思想。
解题思路:
- 方法1:暴力三层循环(时间O(n³),空间O(1))- 超时
- 方法2:排序 + 双指针,固定一个数,剩下两数用双指针查找(时间O(n²),空间O(1))- 最优解
代码实现:
java
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
if (nums[i] > 0) break;
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) left++; // 去重
while (left < right && nums[right] == nums[right - 1]) right--; // 去重
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return res;
}复杂度分析:时间复杂度 O(n²),排序 O(nlogn) + 双层遍历 O(n²);空间复杂度 O(1)(忽略排序的栈空间)。
变体扩展:
- 变体1:最接近的三数之和
- 变体2:四数之和
- 变体3:较小的三数之和
Q3: 移除元素 「🟢 初级」
题目描述:给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
考察点:快慢指针、原地修改数组。
解题思路:
- 方法1:快慢指针,慢指针指向新数组末尾,快指针遍历(时间O(n),空间O(1))- 最优解
- 方法2:双指针优化(当要删除的元素很少时),从两端向中间遍历
代码实现:
java
public int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}复杂度分析:时间复杂度 O(n),遍历一次数组;空间复杂度 O(1),原地修改。
变体扩展:
- 变体1:删除有序数组中的重复项
- 变体2:删除有序数组中的重复项II(最多保留2个)
- 变体3:移动零
Q4: 删除有序数组中的重复项 「🟢 初级」
题目描述:给你一个升序排列的数组 nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致。
考察点:快慢指针、有序数组特性、原地修改。
解题思路:
- 方法1:快慢指针,慢指针指向不重复部分的末尾,快指针遍历(时间O(n),空间O(1))- 最优解
代码实现:
java
public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int slow = 0;
for (int fast = 1; fast < nums.length; fast++) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}复杂度分析:时间复杂度 O(n),遍历一次数组;空间复杂度 O(1),原地修改。
变体扩展:
- 变体1:删除有序数组中的重复项II(每个元素最多出现2次)
- 变体2:移除元素
- 变体3:有序链表删除重复节点
Q5: 合并两个有序数组 「🟢 初级」
题目描述:给你两个按非递减顺序排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。
考察点:双指针从后向前遍历、原地合并。
解题思路:
- 方法1:先合并再排序(时间O((m+n)log(m+n)),空间O(1))
- 方法2:双指针从前往后,需要额外数组(时间O(m+n),空间O(m))
- 方法3:双指针从后往前,原地合并(时间O(m+n),空间O(1))- 最优解
代码实现:
java
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1, j = n - 1, k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}复杂度分析:时间复杂度 O(m+n),最多遍历 m+n 个元素;空间复杂度 O(1),原地修改。
变体扩展:
- 变体1:合并两个有序链表
- 变体2:合并k个有序链表
- 变体3:有序数组的平方
Q6: 接雨水 「🔴 高级」
题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
考察点:双指针、单调栈、动态规划、空间优化。
解题思路:
- 方法1:暴力解法,每个位置找左右最大高度(时间O(n²),空间O(1))
- 方法2:动态规划,预处理左右最大高度数组(时间O(n),空间O(n))
- 方法3:双指针,左右指针向中间移动(时间O(n),空间O(1))- 最优解
- 方法4:单调栈
代码实现:
java
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0;
int res = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
res += leftMax - height[left];
}
left++;
} else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
res += rightMax - height[right];
}
right--;
}
}
return res;
}复杂度分析:时间复杂度 O(n),左右指针各遍历一次;空间复杂度 O(1),仅使用常数额外空间。
变体扩展:
- 变体1:盛最多水的容器
- 变体2:柱状图中最大的矩形
- 变体3:接雨水II(二维)
Q7: 盛最多水的容器 「🟡 中级」
题目描述:给定一个长度为 n 的整数数组 height。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。
考察点:双指针、贪心思想。
解题思路:
- 方法1:暴力解法,枚举所有数对(时间O(n²),空间O(1))
- 方法2:双指针,左右指针向中间移动,每次移动较短的一边(时间O(n),空间O(1))- 最优解
代码实现:
java
public int maxArea(int[] height) {
int left = 0, right = height.length - 1;
int maxArea = 0;
while (left < right) {
int area = Math.min(height[left], height[right]) * (right - left);
maxArea = Math.max(maxArea, area);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}复杂度分析:时间复杂度 O(n),左右指针各遍历一次;空间复杂度 O(1),常数额外空间。
变体扩展:
- 变体1:接雨水
- 变体2:装最多水的容器III
Q8: 下一个排列 「🟡 中级」
题目描述:整数数组的一个排列就是将其所有成员以序列或线性顺序排列。给你一个整数数组 nums,找出 nums 的下一个排列。必须原地修改,只允许使用额外常数空间。
考察点:双指针、排列规律、原地反转。
解题思路:
- 方法:从后向前找第一个下降的位置i,再从后向前找第一个比nums[i]大的位置j,交换i和j,然后反转i之后的部分(时间O(n),空间O(1))- 最优解
代码实现:
java
public void nextPermutation(int[] nums) {
int i = nums.length - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
int j = nums.length - 1;
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j);
}
reverse(nums, i + 1, nums.length - 1);
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
swap(nums, start, end);
start++;
end--;
}
}复杂度分析:时间复杂度 O(n),最多遍历三次数组;空间复杂度 O(1),原地修改。
变体扩展:
- 变体1:上一个排列
- 变体2:全排列
- 变体3:第k个排列
Q9: 旋转数组 「🟡 中级」
题目描述:给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。你必须使用空间复杂度为 O(1) 的原地算法解决这个问题。
考察点:三次翻转法、环状替换、原地旋转。
解题思路:
- 方法1:使用额外数组(时间O(n),空间O(n))
- 方法2:三次翻转法,先翻转整体,再翻转前k个,再翻转后n-k个(时间O(n),空间O(1))- 最优解
- 方法3:环状替换
代码实现:
java
public void rotate(int[] nums, int k) {
k %= nums.length;
reverse(nums, 0, nums.length - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.length - 1);
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}复杂度分析:时间复杂度 O(n),每个元素被翻转两次;空间复杂度 O(1),原地修改。
变体扩展:
- 变体1:旋转链表
- 变体2:旋转图像
- 变体3:搜索旋转排序数组
Q10: 除自身以外数组的乘积 「🟡 中级」
题目描述:给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。题目数据保证数组 nums 之中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。请不要使用除法,且在 O(n) 时间复杂度内完成此题。
考察点:前缀积 + 后缀积、空间优化、双指针思想。
解题思路:
- 方法1:左右乘积数组,分别计算前缀积和后缀积(时间O(n),空间O(n))
- 方法2:空间优化,用输出数组存储前缀积,再从右向左乘后缀积(时间O(n),空间O(1),输出数组不计入)- 最优解
代码实现:
java
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] res = new int[n];
// 前缀积
res[0] = 1;
for (int i = 1; i < n; i++) {
res[i] = res[i - 1] * nums[i - 1];
}
// 后缀积
int right = 1;
for (int i = n - 1; i >= 0; i--) {
res[i] *= right;
right *= nums[i];
}
return res;
}复杂度分析:时间复杂度 O(n),两次遍历数组;空间复杂度 O(1)(输出数组不计入空间复杂度)。
变体扩展:
- 变体1:乘积最大子数组
- 变体2:和为K的子数组
- 变体3:除自身以外数组的和