双指针算法简介
双指针是一种常用的算法技巧,它通过使用两个指针在数据结构上进行操作,通常用来优化时间复杂度,解决数组或链表中的一些特定问题。双指针技巧一般用于“有序”数据结构(如排序后的数组或链表),但也可以扩展到一些特殊的无序问题。
双指针的基本思想
双指针算法通过两个指针同时遍历数组或链表,指针之间根据特定规则相互移动,从而达到解决问题的目的。双指针的经典应用场景包括:
- 寻找两数之和(例如给定一个有序数组,查找是否存在两个数之和等于目标值)
- 反转字符串或数组
- 滑动窗口问题(例如求最大子数组和、最小子数组长度等)
- 合并两个有序数组等
双指针有两种常见的移动方式:
- 快慢指针:一个指针每次移动两步,另一个指针每次移动一步,用于检测环形链表、寻找中间节点等问题。
- 左右指针:两个指针分别从序列的两端开始,向中间逼近,常用于有序数组、字符串等的处理。
1.经典问题:两数之和(Two Sum)
测试链接:https://leetcode.cn/problems/two-sum/description/
假设给定一个有序数组,要求找到两个数的和等于目标值。我们可以用双指针技巧高效地解决这个问题。
问题描述:
给定一个有序数组nums
和一个目标值target
,请你在数组中找出两个数,使得它们的和等于目标值,并返回它们的数组下标。
思路:
- 初始化两个指针:一个指向数组的开头(
left
),另一个指向数组的末尾(right
)。 - 计算当前指针所指元素的和:
- 如果和等于目标值,返回这两个指针所指的元素的下标。
- 如果和小于目标值,说明需要更大的值,将
left
指针向右移动。 - 如果和大于目标值,说明需要更小的值,将
right
指针向左移动。
- 重复这个过程,直到两个指针相遇。
Java代码示例:
public class TwoSum {
public static int[] twoSum(int[] nums, int target) {
int left = 0, 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--; // 需要更小的值,移动右指针
}
}
throw new IllegalArgumentException("No solution found");
}
public static void main(String[] args) {
int[] nums = {2, 7, 11, 15};
int target = 9;
int[] result = twoSum(nums, target);
System.out.println("Indices: [" + result[0] + ", " + result[1] + "]");
}
}
代码解释:
left
指针从数组的开始位置出发,right
指针从数组的末尾出发。while (left < right)
确保两个指针没有交叉。- 根据
sum
与target
的比较结果,决定移动哪一个指针。若sum == target
,则返回当前指针位置的下标。
时间复杂度:
时间复杂度为O(n)
,其中n
是数组的长度。由于每个元素最多只被访问一次,因此时间复杂度为线性时间。
空间复杂度:
空间复杂度为O(1)
,只用了常数空间。
其他应用场景
除了“两数之和”问题,双指针还可以解决许多其他问题。以下是一些常见的应用场景:
- 反转数组或字符串:双指针从两端向中间靠拢交换元素。
- 无重复元素的两数之和:在无重复的情况下,可以使用双指针找到目标值的两数之和。
- 滑动窗口:利用双指针来调整窗口大小,解决如最长子串、最小子数组等问题。
- 合并两个有序数组:通过双指针同时遍历两个数组,按顺序合并成一个新的有序数组。
2.反转字符串代码示例:
public class ReverseString {
public static void reverse(char[] s) {
int left = 0, right = s.length - 1;
while (left < right) {
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
public static void main(String[] args) {
char[] s = {'h', 'e', 'l', 'l', 'o'};
reverse(s);
System.out.println(s); // 输出: "olleh"
}
}
测试链接:https://leetcode.cn/problems/reverse-string/description/
经典问题复盘:三数之和
测试链接:https://leetcode.cn/problems/3sum/description/
问题描述:
给定一个包含n
个整数的数组nums
,判断nums
中是否存在三个元素a, b, c
,使得a + b + c = 0
。请找出所有满足条件的三元组。
思路:
- 首先,将数组排序,排序后的数组使得能够通过双指针技术有效查找符合条件的三元组。
- 遍历数组,固定第一个元素
nums[i]
,然后通过双指针在剩余的部分寻找符合条件的两个数。 - 若当前三元组和为0,保存结果,并移动指针跳过重复元素。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ThreeSum {
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums); // 先排序
for (int i = 0; i < nums.length - 2; i++) {
// 去除重复的元素
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.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 result;
}
public static void main(String[] args) {
int[] nums = {-1, 0, 1, 2, -1, -4};
List<List<Integer>> result = threeSum(nums);
for (List<Integer> triplet : result) {
System.out.println(triplet);
}
}
}
代码解释:
- 数组先排序,便于后续的双指针查找。
- 外层循环固定一个元素
nums[i]
,然后用双指针left
和right
在剩余部分寻找满足和为0的两个数。 - 为了避免重复的三元组,在每次找到有效三元组时,需要跳过重复的元素。
时间复杂度:
- 排序的时间复杂度是
O(n log n)
,外层循环为O(n)
,内层的双指针为O(n)
,因此整体时间复杂度为O(n^2)
。
空间复杂度:
O(1)
,只用了常数空间(不考虑输出结果)。
2. 滑动窗口与双指针:最小子数组之和
测试链接:
问题描述:
给定一个含有n
个正整数的数组nums
和一个目标值target
,请找出该数组中和大于或等于target
的最小子数组的长度。如果没有符合条件的子数组,返回0
。
思路:
- 使用滑动窗口的方法,双指针
left
和right
分别代表窗口的左右边界。 - 初始时,
right
指针逐步扩大窗口,直到窗口中的元素和大于等于target
。 - 然后,移动
left
指针收缩窗口,尽可能地减小窗口的大小,并同时保持和大于等于target
。
Java代码实现:
public class MinSubArrayLen {
public static int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int minLength = Integer.MAX_VALUE;
int sum = 0;
int left = 0;
for (int right = 0; right < n; right++) {
sum += nums[right];
while (sum >= target) {
minLength = Math.min(minLength, right - left + 1);
sum -= nums[left++];
}
}
return minLength == Integer.MAX_VALUE ? 0 : minLength;
}
public static void main(String[] args) {
int[] nums = {2, 3, 1, 2, 4, 3};
int target = 7;
System.out.println("Minimum length subarray: " + minSubArrayLen(target, nums)); // Output: 2
}
}
代码解释:
- 使用双指针
left
和right
表示当前滑动窗口的左右边界。 - 逐步扩大
right
指针,直到窗口和满足条件,然后尝试缩小窗口(移动left
指针)以寻找最小长度。
时间复杂度:
- 由于每个元素最多被
left
和right
指针访问一次,因此时间复杂度为O(n)
。
空间复杂度:
O(1)
,只用了常数空间。
3. 合并两个有序数组
测试链接:https://leetcode.cn/problems/merge-sorted-array/description/
问题描述:
给定两个已排序的数组nums1
和nums2
,请将nums2
合并到nums1
中,使得nums1
仍然是一个有序数组。
思路:
- 使用双指针
i
和j
分别从nums1
和nums2
的末尾开始,比较两个数组的当前元素,将较大的元素放到nums1
的末尾。这个方法可以避免额外的空间开销。
Java代码实现:
public class MergeSortedArray {
public static 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--];
}
}
public static void main(String[] args) {
int[] nums1 = new int[6];
int[] nums2 = {1, 2, 3};
nums1[0] = 2;
nums1[1] = 5;
nums1[2] = 6;
merge(nums1, 3, nums2, 3);
System.out.println("Merged array: " + Arrays.toString(nums1));
}
}
代码解释:
- 使用两个指针
i
和j
分别指向nums1
和nums2
的最后元素,通过比较两个元素的大小将较大的元素放到nums1
的末尾。 - 当
nums2
还有剩余元素时,直接将其拷贝到nums1
中。
时间复杂度:
O(m + n)
,其中m
和n
分别是nums1
和nums2
的长度。
空间复杂度:
O(1)
,只使用了常数空间。