一 前言
接下来我们来到了双指针的题目。双指针算法是常见且高效的算法思想,主要用于在数组,字符串,链表等有序结构中高效地解决问题。本篇讲解的三道题目都属于双指针算法的题目。 三道题目难度从低到高,属于双指针算法中的快慢指针、分组思想的题目。三道题目在leetcode100里面,可以用来练习和学习双指针算法。
二 双指针算法
-
定义:双指针就是用两个变量同时在一个序列上移动,通过不同的移动策略(快慢、相向、同向)来降低时间复杂度。
-
常见场景是:
- 遍历时避免重复计算
- 用两个指针代替双重循环
- 保持窗口结构,做滑动窗口问题
-
为什么双指针高效?
- 传统做法:往往需要两层循环,复杂度 O(n²)
- 双指针:每个指针最多移动 n 次,整体复杂度 O(n)
- 思路:通过两个变量控制范围,避免重复遍历。
三 题目
3.1 移动0
题目描述 : 给定一个数组 nums
,编写一个函数将所有的 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例 : 输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
题目分析: 题目要求把所有的0移动到数组的末尾,同时保持非零元素的相对顺序。那么也就是说要把非零元素放到前半部分。 我们可以用快慢指针来解决这个问题。快指针用来遍历数组,慢指针用来指向当前可以交换的位置。当快指针遍历到一个非0的元素时,我们就将它交换到当前可以交换的位置,然后将当前可以交换的位置向后移动一位。当快指针遍历完数组时,慢指针后面的元素就是0了。 再思考下,如果不用快慢指针,我们该如何解决这个问题?最暴力的方式就是每次遇到非零元素就把它放到数组的前面,然后把后面的元素都往后移动一位。但是这样的时间复杂度是O(n²)。所以通过双指针减少重复的挪位和记录之前的状态可以把时间复杂度降到O(n)。
Java代码:
java
class Solution {
public void moveZeroes(int[] nums) {
int slow = 0;
for(int fast = 0; fast < nums.length; fast++){
if(nums[fast] !=0){
int temp = nums[fast];
nums[fast] = nums[slow];
nums[slow] = temp;
slow++;
}
}
}
}
3.2 颜色分类(荷兰国旗问题)
题目描述 : 给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,将它们原地排序,使得相同颜色的元素相邻,顺序为红色(0)→白色(1)→蓝色(2)。
示例 : 输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
题目分析: 这道题目是稍微复杂一点的双指针问题。题目要求把数组分成三部分,分别是0,1,2。我们可以用三个指针来解决这个问题。 首先一定需要一个指针来进行遍历。还需要一个指针来标记0的位置,一个指针来标记2的位置。遍历指针遇到0就和0指针交换,遇到2就和2指针交换。 另外要注意的是,遍历指针遇到2时,不能直接交换,因为交换过来的元素可能是0,需要继续判断。 遇到1的时候直接跳过,因为1是中间的元素,不需要交换。只需要把0和2处理好,1就会自动到中间。 通过这种方式时间复杂度是O(n),空间复杂度是O(1)。
Java代码:
java
class Solution {
public void sortColors(int[] nums) {
int left = 0, right = nums.length - 1;
int i = 0;
while (i <= right) {
if (nums[i] == 0) {
swap(nums, i, left);
i++;
left++;
} else if (nums[i] == 2) {
swap(nums, i, right);
right--;
} else {
i++;
}
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
3.3 数组中的第k个最大元素
题目描述 : 给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。 注意:你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
示例 : 输入: nums = [3,2,1,5,6,4], k = 2
输出: 5
题目分析: 这道题目可以用多种方法来解决,比如排序,堆,快速选择等。这里我们用快速选择来解决这个问题。 我们可以看到快速选择三路分区代码和颜色分类的代码很相似,都是用三个指针来解决问题。当学会颜色分类的题目后,这道题目就很简单了。 快速选择的核心原理是:
- 每次选择一个 pivot 元素,将数组分成三部分:小于 pivot 的元素,等于 pivot 的元素,大于 pivot 的元素。
- 如果 pivot 元素的位置等于 k,那么 pivot 元素就是第 k 大的元素。
- 如果 pivot 元素的位置大于 k,那么第 k 大的元素在 pivot 元素的左边。
- 如果 pivot 元素的位置小于 k,那么第 k 大的元素在 pivot 元素的右边。
- 递归地对左边或右边的子数组进行快速选择,直到找到第 k 大的元素。 时间复杂度:O(n),空间复杂度:O(1)。
Java代码(快速选择 QuickSelect 实现):
java
class Solution {
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, nums.length - k, 0, nums.length -1);
}
public int quickSelect(int[] nums, int index, int left, int right){
if(left >= right){
return nums[left];
}
int i = left;
int j = right;
int cur = left;
int pivot = nums[right];
while(cur <= j){
if(nums[cur] > pivot){
swap(nums, cur, j);
j--;
} else if(nums[cur] < pivot){
swap(nums, cur, i);
i++;
cur++;
} else {
cur++;
}
}
if(index >= i && index <= j){
return nums[index];
} else if(index < i){
return quickSelect(nums, index, left, i - 1);
} else {
return quickSelect(nums, index, j + 1, right);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
四 总结
双指针算法是一种高效的算法思想,能够有效降低时间复杂度。很多遍历和问题都可以通过双指针算法来减少时间复杂度。 通过这三道题目,我们学习了双指针算法的基本思想和应用场景。这三道题目的核心算法和代码非常类似,所以把他们放到一起讲解。后面会继续讲解其他的双指针算法。比如滑动窗口算法,快慢指针算法,首尾指针算法等。