双指针技巧是一种常用的算法优化方法,主要用于处理数组或链表等线性结构的问题。它通过定义两个指针(或索引),并以不同的策略移动这两个指针,来达到减少时间复杂度、简化问题解决过程的目的。根据具体问题和解题策略的不同,双指针技巧大致可以分为以下几类:
# 快慢指针
快慢指针主要用于解决链表中的问题,如检测环、寻找环的入口、寻找链表的中点等。在这种策略中,两个指针从同一位置出发,一个以快速移动(例如,每次移动两步),另一个以慢速移动(例如,每次移动一步)。通过比较快慢指针的相对速度,我们可以得到一些有用的信息。
检测链表中的环
#include <iostream> | |
using namespace std; | |
// 定义链表节点类 | |
class ListNode { | |
public: | |
int val; // 节点存储的值 | |
ListNode *next; // 指向下一个节点的指针 | |
// 构造函数,初始化节点值和指针 | |
ListNode(int x) : val(x), next(NULL) {} | |
}; | |
// 检测链表中是否存在环 | |
bool hasCycle(ListNode *head) { | |
// 如果链表为空或者只有一个节点,则不可能形成环 | |
if (head == NULL || head->next == NULL) return false; | |
// 使用两个指针,slow 每次移动一步,fast 每次移动两步 | |
ListNode *slow = head, *fast = head->next; | |
// 循环直到两个指针相遇 | |
while (fast != slow) { | |
// 如果 fast 指针到达链表末尾(NULL),说明链表中没有环 | |
if (fast == NULL || fast->next == NULL) return false; | |
// 移动指针,slow 移动一步,fast 移动两步 | |
slow = slow->next; | |
fast = fast->next->next; | |
} | |
// 如果 while 循环结束,说明 fast 和 slow 相遇了,链表中存在环 | |
return true; | |
} |
左右指针
左右指针主要用于处理数组或字符串问题,特别是那些涉及到对元素进行配对、查找特定条件下的元素组合等问题。在这种策略中,一个指针从数组(或字符串)的左端开始,另一个从右端开始,根据问题的需要,逐步向中间移动,直到满足特定条件或两指针相遇。
反转字符串
#include <iostream> | |
#include <string> | |
using namespace std; | |
void reverseString(string& s) { | |
int left = 0, right = s.size() - 1; | |
while (left < right) { | |
swap(s[left], s[right]); | |
left++; | |
right--; | |
} | |
} | |
int main() { | |
string str = "hello"; | |
reverseString(str); | |
cout << str << endl; // 输出 olleh | |
return 0; | |
} |
# 滑动窗口
滑动窗口是一种特殊的双指针应用,主要用于解决数组或字符串上的子区间问题,如最长无重复字符的子串、包含所有字符的最短子串等。在这种策略中,两个指针定义了一个窗口的边界,通过移动这两个指针的位置来扩大或缩小窗口的大小,以此来找到满足特定条件的最优解。
最长无重复字符的子串长度
#include <iostream> | |
#include <string> | |
#include <unordered_set> | |
using namespace std; | |
int lengthOfLongestSubstring(string s) { | |
// 使用一个 unordered_set 来存储当前考虑的子串中的字符。 | |
//unordered_set 是一个容器,能够快速判断一个值是否存在于集合中。 | |
unordered_set<char> charSet; | |
// 定义两个指针 l 和 r,分别表示当前考虑的子串的左边界和右边界。 | |
// 初始化结果 res 为 0,用于记录遍历过程中发现的最长子串的长度。 | |
int l = 0, res = 0; | |
// 开始遍历字符串 s,r 作为右边界指针,从头到尾移动。 | |
for (int r = 0; r < s.size(); r++) { | |
// 如果当前字符 s [r] 已经存在于 charSet 中,说明遇到了重复字符。 | |
// 需要移动左边界 l,直到 s [r] 不再 charSet 中为止。 | |
while (charSet.find(s[r]) != charSet.end()) { | |
// 从 charSet 中移除左边界字符,然后将左边界向右移动。 | |
charSet.erase(s[l]); | |
l++; | |
} | |
// 将当前字符加入到 charSet 中,因为它不在当前考虑的子串中。 | |
charSet.insert(s[r]); | |
// 更新结果 res,当前子串的长度为 r - l + 1。 | |
//max 函数用于比较并保留更大的值。 | |
res = max(res, r - l + 1); | |
} | |
// 返回最终计算出的最长无重复字符子串的长度。 | |
return res; | |
} | |
int main() { | |
// 测试字符串 | |
string str = "abcabcbb"; | |
// 调用函数并输出结果 | |
cout << lengthOfLongestSubstring(str) << endl; // 输出 3 | |
return 0; | |
} |
# 注意事项:
- 初始化:正确初始化两个指针的位置。
- 边界条件:在移动指针时,要注意检查边界条件,避免访问超出数组或链表的范围。
- 移动策略:根据问题的特性,决定是同时移动两个指针还是交替移动;以及如何更新指针的位置。
- 停止条件:明确循环或迭代的停止条件,确保算法能够在正确的时候终止。
双指针技巧通过减少不必要的计算和优化遍历过程,往往能将算法的时间复杂度从 O (n^2) 降低到 O (n),在处理一些特定问题时非常有效。
# 题目
【算法 2-2】常见优化技巧 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)