对双指针的理解和总结
前言
双指针在这周的题目中频繁出现,于是我尝试总结并归纳目前我所见到的双指针
简介
双指针是一种常用于解决数组、链表等线性结构问题的算法技巧。它通过使用两个指针在同一结构上操作,以减少时间复杂度,优化算法性能。双指针分为快慢指针和对撞指针,在做题时需要根据题目需要,判断双指针类型
快慢指针
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
用户评测:
评测机将使用以下代码测试您的解决方案:
int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
// 它以不等于 val 的值排序。
int k = removeElement(nums, val); // 调用你的实现
assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i < actualLength; i++) {
assert nums[i] == expectedNums[i];
}
如果所有的断言都通过,你的解决方案将会 通过。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,_,_]
解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,_,_,_]
解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
提示:
-
0 <= nums.length <= 100
-
0 <= nums[i] <= 50
-
0 <= val <= 100
思路
这是一道很经典的快慢指针在数组中的操作
快指针负责遍历数组中所有元素并与所需值作比较
慢指针负责写入新的处理好的数组
代码实现
int removeElement(int* nums, int numsSize, int val) {
int fast = 0,slow = 0;
for (; fast < numsSize; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/)
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内 -105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
**进阶:**你是否可以使用 O(1)
空间解决此题?
思路
使用快慢指针确定链表中是否有环
在快慢指针相遇后,将其中一个指针调回链表的头节点,再同步移动指针,两指针相遇的位置即为链表进入环的入口位置
代码实现
struct ListNode* detectCycle(struct ListNode* head) {
struct ListNode* fast = head;
struct ListNode* slow = head;
// 检测是否有环
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
// 相遇,表示有环
if (fast == slow) {
fast = head; // 将fast指针移到链表头部
while (fast != slow) { // fast和slow一起前进,直到相遇
fast = fast->next;
slow = slow->next;
}
return fast; // 返回环的入口节点
}
}
return NULL;
}
滑动窗口
滑动窗口是双指针中比较特殊的用法
不断调节左右指针,遍历这个窗口,判断子数组是否符合条件,输出
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度**。**如果不存在符合条件的子数组,返回 0
。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 104
进阶:
- 如果你已经实现
O(n)
时间复杂度的解法, 请尝试设计一个O(n log(n))
时间复杂度的解法。
思路
创建滑动窗口,检测窗口中的元素是否符合要求,纪录窗口长度。
滑动窗口本质是根据窗口的数据,不断调整左右指针的位置,从而调整窗口大小,进行下一轮判断
代码实现
int minSubArrayLen(int target, int* nums, int numsSize) {
int fast = 0, slow = 0;
int sum = 0;
int len = numsSize + 1;
for (;fast < numsSize; fast++) {
sum += nums[fast];
while (sum >= target) {
sum -= nums[slow];
slow++;
if (len > fast - slow + 1) {
len = fast - slow + 1;
}
}
}
if (len != numsSize + 1) {
return len;
}else {
return 0;
}
}
双指针对撞
双指针一个指向头,一个指向尾,然后向中间相向而行,直到双指针相遇,此时,遍历完成整个数组
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须**原地修改输入数组**、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
提示:
1 <= s.length <= 105
s[i]
都是 ASCII 码表中的可打印字符
思路
典型的对撞双指针,建立双指针,分别指向边界
指针向中间移动,交换两指针指向的值,直到两指针相遇
代码实现
void reverseString(char* s, int sSize) {
int i,j;
i = 0;
j = sSize - 1;
//直到两指针相遇
while (i <= j) {
//交换指针指向的值
char temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
}
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
已按 非递减顺序 排序
进阶:
- 请你设计时间复杂度为
O(n)
的算法解决本问题
思路
题目要求有序数组的平方,但是要求平方后任然有序
不难想到实际上数组两边的数平方后是最大的数
建立双指针,一个指向头一个指向尾
平方后比较大小,倒序插入数组中
代码实现
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
*returnSize = numsSize;
//双指针指向数组两边
int left = 0;
int right = numsSize - 1;
int* result = (int* )malloc(sizeof(int) * numsSize);
//指向要插入的位置
int p = numsSize - 1;
while (left <= right) {
int squarel = nums[left] * nums[left];
int squarr = nums[right] * nums[right];
//判断平方后的数组那个大,并插入数组
if (squarel > squarr) {
result[p] = squarel;
left++;
p--;
} else {
result[p] = squarr;
right--;
p--;
}
}
return result;
}
总结
双指针其实是代替双循环从而降低时间复杂度,双指针的本质是通过两个指针在同一个数据结构上高效地并行操作,利用它们的相对位置关系来优化问题的求解过程。它通过减少不必要的重复计算,提高程序效率