双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
(1)167. Two Sum II - Input array is sorted (Easy)
题目描述
在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。
本人思路
这个题目由于是做过的,所以很容易想到了双指针。从左边和右边分别遍历,如果小了就左移,大了就右移。
代码展示
vector<int> twoSum(vector<int>& numbers, int target) {
int l = 0, r = numbers.size() - 1, sum;
while (l < r) {
sum = numbers[l] + numbers[r];
if (sum == target) {
break;
}
if (sum < target) {
++l;
} else {
--r;
}
}
return vector<int>{l + 1, r + 1};
}
(2)88. Merge Sorted Array (Easy)
题目描述
给定两个有序数组,把两个数组合并为一个。
本人思路
第一反应是想到了算法中学的合并排序,但是想了一会没想起来当时的排序细节了。思考了一下能想到的就是一步步移,但是效率太低了,如何利用双指针来解决这个问题呢?
题解思路
因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。在以下的代码里,我们直接利用 m 和 n 当作两个数组的指针,再额外创立一个 pos 指针,起始位置为 m +n−1。每次向前移动 m 或 n 的时候,也要向前移动 pos。这里需要注意,如果 nums1的数字已经复制完,不要忘记把 nums2 的数字继续复制;如果 nums2 的数字已经复制完,剩余nums1的数字不需要改变,因为它们已经被排好序。
代码展示
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int pos = m+ n- 1;
m--;
n--;
while (m >= 0 && n >= 0) {
nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];
}
while (n >= 0) {
nums1[pos--] = nums2[n--];
}
}
(3)142. Linked List Cycle II (Medium)
题目描述
给定一个链表,如果有环路,找出环路的开始点。
本人思路
完全没什么思路,没遇到过这种类型的题目,思考了一会直接看题解了。
题解
对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd 判圈法) 。给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。
当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。
代码展示
ListNode* detectCycle(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
// 判断是否存在环路
do {
if (!fast || !fast->next) {
return nullptr;
}
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
// 如果存在,查找环路节点
fast = head;
while (fast != slow) {
slow = slow->next;
fast = fast->next;
}
return fast;
}
(4)76. Minimum Window Substring (Hard)
题目描述
给定两个字符串 S 和 T,求 S 中包含 T 所有字符的最短连续子字符串的长度,同时要求时间
复杂度不得超过 O(n)。
本人思路
这题可以很容易的想到滑动窗口,但还有一个潜在的难点是如何去“计数”,记录存在哪些字符以及相应的数目,可以用哈希表或者数组存储都行,这里采用的是数组。以及需要注意一下边界的处理,我在移动左边界的时候写错了左边界移动的时机导致出现了段错误。
代码展示
string minWindow(string S, string T) {
vector<int> chars(128, 0);
vector<bool> flag(128, false);
// 先统计T中的字符情况
for (int i = 0; i < T.size(); ++i) {
flag[T[i]] = true;
++chars[T[i]];
}
// 移动滑动窗口,不断更改统计数据
int cnt = 0, l = 0, min_l = 0, min_size = S.size() + 1;
for (int r = 0; r < S.size(); ++r) {
if (flag[S[r]]) {
if (--chars[S[r]] >= 0) {
++cnt;
}
// 若目前滑动窗口已包含T中全部字符,
// 则尝试将l右移,在不影响结果的情况下获得最短子字符串
while (cnt == T.size()) {
if (r - l + 1 < min_size) {
min_l = l;
min_size = r - l + 1;
}
if (flag[S[l]] && ++chars[S[l]] > 0) {
--cnt;
}
++l;
}
}
}
return min_size > S.size() ? "" : S.substr(min_l, min_size);
}
总结
今天的学习内容是双指针。学到的新知识主要有两点:一个是快慢指针的运用,另外一个是滑动窗口。快慢指针可以用于确认链表是否有环、链表的中位数、判断链表长度是否为偶数;而滑动窗口则是一种新的方法,可以用于解决区间问题。