相向双指针的应用:
双指针的工作原理:
降低时间复杂度的原理:
简单例子:
有序数组中的两数之和
移除数组中的重复元素
判断回文串
双指针的优势:
双指针的局限性:
查找问题
原地操作问题
字符串处理
区间问题
// 同向双指针基本模板
void twoPointersSameDirection(vector<int>& nums) {
int n = nums.size();
int left = 0; // 左指针初始位置
int right = 0; // 右指针初始位置(也可以是其他位置)
while (right < n) {
// 更新状态或进行计算
// ...
// 移动右指针
right++;
// 根据条件移动左指针
while (left < right && /* 某些条件 */) {
// 可能需要在移动前更新状态
// ...
left++;
}
// 处理当前窗口的结果
// ...
}
// 返回最终结果
// ...
}
// 相向双指针基本模板
void twoPointersOppositeDirection(vector<int>& nums) {
int n = nums.size();
int left = 0; // 左指针初始位置
int right = n - 1; // 右指针初始位置
while (left < right) {
// 计算或比较当前指针指向的元素
// ...
// 根据条件移动指针
if (/* 某些条件 */) {
left++;
} else {
right--;
}
// 可能需要跳过重复元素
while (left < right && nums[left] == nums[left - 1]) left++;
while (left < right && nums[right] == nums[right + 1]) right--;
}
// 返回最终结果
// ...
}
// 快慢指针模板(以链表为例)
ListNode* fastSlowPointers(ListNode* head) {
if (!head || !head->next) return head;
ListNode* slow = head; // 慢指针
ListNode* fast = head->next; // 快指针
// 快指针每次移动两步,慢指针每次移动一步
while (fast && fast->next) {
slow = slow->next; // 慢指针移动一步
fast = fast->next->next; // 快指针移动两步
// 根据问题处理特定情况
// 例如:检测环
if (slow == fast) {
// 找到环
// ...
break;
}
}
// 返回结果(如链表中点、环的入口等)
// ...
return slow; // 或其他结果
}
// 滑动窗口模板
int slidingWindow(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = 0;
int sum = 0; // 或其他状态变量
int result = 0; // 或其他结果变量
while (right < n) {
// 扩展窗口
sum += nums[right]; // 更新状态
right++;
// 根据条件收缩窗口
while (left < right && /* 窗口需要收缩的条件 */) {
sum -= nums[left]; // 更新状态
left++;
}
// 更新结果
// ...
}
return result;
}
// 有序数组中的双指针模板(以两数之和为例)
vector<int> twoSum(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
return {left, right}; // 找到目标
} else if (sum < target) {
left++; // 和太小,增加左指针
} else {
right--; // 和太大,减小右指针
}
}
return {-1, -1}; // 未找到目标
}
// 链表中的双指针模板(以删除倒数第N个节点为例)
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* first = dummy;
ListNode* second = dummy;
// 先让第一个指针前进n+1步
for (int i = 0; i <= n; i++) {
first = first->next;
}
// 两个指针同时前进,直到第一个指针到达末尾
while (first) {
first = first->next;
second = second->next;
}
// 此时second指向要删除节点的前一个节点
ListNode* temp = second->next;
second->next = second->next->next;
delete temp;
ListNode* result = dummy->next;
delete dummy;
return result;
}
// 字符串中的双指针模板(以判断回文串为例)
bool isPalindrome(string s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
// 跳过非字母数字字符
while (left < right && !isalnum(s[left])) {
left++;
}
while (left < right && !isalnum(s[right])) {
right--;
}
// 比较字符(忽略大小写)
if (tolower(s[left]) != tolower(s[right])) {
return false;
}
left++;
right--;
}
return true;
}
双指针的常见错误与注意事项
- 边界条件处理:忘记检查数组为空或只有一个元素的情况。
- 指针越界:没有正确检查指针是否超出数组范围,导致访问无效内存。
- 指针移动规则:指针移动逻辑错误,导致无法覆盖所有情况或陷入死循环。
- 终止条件:设置不正确的循环终止条件,导致提前退出或无限循环。
- 重复元素处理:在需要去重的场景中,没有正确处理重复元素。
- 状态更新:在移动指针前没有正确更新状态变量或结果。
- 特殊情况:没有考虑问题中的特殊情况,如空字符串、负数等。
- 算法选择:使用双指针解决不适合的问题,导致算法复杂或低效。
问题描述:
给定一个已按照升序排列的整数数组numbers,请找出两个数,使得它们相加之和等于目标数target。函数应该返回这两个数的下标(从1开始计数),而且下标1必须小于下标2。
分析:
// 两数之和(有序数组)
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0;
int right = numbers.size() - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
// 题目要求返回的下标从1开始
return {left + 1, right + 1};
} else if (sum < target) {
// 和太小,增加左指针
left++;
} else {
// 和太大,减小右指针
right--;
}
}
// 题目保证有唯一解,所以不会到达这里
return {-1, -1};
}
代码解析:
left为0,右指针right为数组末尾复杂度分析:
优势:
问题描述:
给定一个数组nums和一个值val,你需要原地移除所有数值等于val的元素,并返回移除后数组的新长度。不要使用额外的数组空间,必须在原地修改输入数组并使用O(1)额外空间的条件下完成。
分析:
slow指向当前需要填入新元素的位置fast用于遍历数组fast指向的元素不等于val时,将其复制到slow位置,并增加slowslow的值就是新数组的长度// 移除元素
int removeElement(vector<int>& nums, int val) {
int n = nums.size();
int slow = 0; // 慢指针,指向当前需要填入新元素的位置
for (int fast = 0; fast < n; fast++) {
// 如果当前元素不等于val,则保留
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
// 如果当前元素等于val,则跳过(fast增加,slow不变)
}
// slow指向的位置就是新数组的长度
return slow;
}
代码解析:
slow指针表示新数组的当前位置fast指针遍历原数组的每个元素fast指向的元素不等于要移除的值时,将其复制到slow位置slow向前移动一步fast指向的元素等于要移除的值时,只移动fast,不复制元素复杂度分析:
优势:
问题描述:
给定一个长度为n的整数数组height,有n条垂线,第i条线的两个端点是(i, 0)和(i, height[i])。找出其中的两条线,使得它们与x轴共同构成的容器可以容纳最多的水。
分析:
// 盛最多水的容器
int maxArea(vector<int>& height) {
int n = height.size();
int left = 0;
int right = n - 1;
int maxWater = 0;
while (left < right) {
// 计算当前容器的面积
int h = min(height[left], height[right]);
int w = right - left;
int area = h * w;
// 更新最大面积
maxWater = max(maxWater, area);
// 移动较短的那条线对应的指针
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
}
代码解析:
复杂度分析:
贪心策略:
问题描述:
给定一个包含n个整数的数组nums,判断nums中是否存在三个元素a, b, c,使得a + b + c = 0?找出所有满足条件且不重复的三元组。
分析:
// 三数之和
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> result;
// 排序
sort(nums.begin(), nums.end());
for (int i = 0; i < n - 2; i++) {
// 跳过重复的第一个数
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int target = -nums[i];
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
// 找到一组解
result.push_back({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 < target) {
left++;
} else {
right--;
}
}
}
return result;
}
代码解析:
复杂度分析:
优化:
问题描述:
给定一个链表,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。
分析:
进阶:找到环的入口点
// 检测链表中的环
bool hasCycle(ListNode *head) {
if (!head || !head->next) {
return false;
}
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next; // 慢指针移动一步
fast = fast->next->next; // 快指针移动两步
if (slow == fast) {
return true; // 指针相遇,存在环
}
}
return false; // 快指针到达末尾,不存在环
}
代码解析:
slow和快指针fast都指向链表头复杂度分析:
Floyd's算法原理:
问题描述:
给定一个数组nums和滑动窗口的大小k,请找出所有滑动窗口里的最大值。
分析:
// 滑动窗口最大值
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
vector<int> result;
deque<int> dq; // 存储索引的双端队列
for (int i = 0; i < n; i++) {
// 移除离开窗口的元素
if (!dq.empty() && dq.front() == i - k) {
dq.pop_front();
}
// 移除队列中所有小于当前元素的值
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
// 将当前元素的索引加入队列
dq.push_back(i);
// 当窗口形成后,记录最大值
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
代码解析:
dq存储元素的索引复杂度分析:
优势:
[POJ 2739] Sum of Consecutive Prime Numbers
[HDU 1392] Surround the Trees
[CSP-J 2019] 数列
[USACO 2016 Feb Silver] Load Balancing
[NOIP 2015] 子串
[CSES 1640] Sum of Two Values
[CSP-S 2019] 树的重心
[POJ 3061] Subsequence
[HDU 3549] Flow Problem
[NOIP 2016] 换教室
[USACO 2019 Jan Gold] Sleepy Cow Sorting
[IOI 2015] Teams
双指针的核心要点:
基本思想:
主要类型:
常见应用:
实践建议:
编码前:
编码时:
调试时:
双指针的扩展:
多指针技术:
双指针与其他算法结合:
高维双指针:
并行双指针:
实际应用中的注意事项:
在使用双指针之前,先考虑:
优化建议:
记住:

这个讲义使用了Awesome Marp主题的蓝色风格。 演讲者备注不会在演示模式中显示给观众,仅供演讲者参考。 每个关键页面都添加了备注,帮助演讲者更好地讲解内容。
封面页: - 欢迎学生,简要介绍本次课程的主题和目标 - 强调双指针是算法设计中的基础技巧,也是竞赛和面试中的常见题型 - 可以简单提及本课程将从基础概念到实际应用,全面讲解双指针的思想和技巧
目录页: - 简要介绍本次讲座的结构和内容安排 - 可以提及讲座的逻辑流程:从基础概念到具体应用,再到实践建议 - 强调我们将通过多个例子来深入理解双指针的应用 - 提醒学生关注每个部分之间的联系,以及如何将理论应用到实践中
转场页: - 这是一个简单的转场页,用于引入双指针概述部分 - 可以简单提及双指针是算法设计中的重要策略,有着广泛的应用
算法复杂度概念简介: - 介绍时间复杂度的基本概念 - 解释为什么需要优化算法复杂度 - 说明双指针算法如何优化时间复杂度
指针的含义: - 解释双指针算法中"指针"的概念 - 说明这里的指针与C++中实际内存指针的区别 - 介绍指针在算法中的作用
双指针概述: - 介绍双指针的基本概念和核心思想 - 强调双指针的高效性和应用场景 - 简要提及双指针的历史和发展
双指针的应用场景: - 介绍双指针的主要应用场景 - 讨论双指针在不同问题中的应用 - 强调双指针的重要性和普遍性
双指针的基本思想: - 详细解释双指针的工作原理 - 讨论如何通过双指针降低时间复杂度 - 提供一些简单的例子来说明双指针的思想
转场页: - 引入双指针的特点部分 - 可以简单提及理解这些特点有助于更好地应用双指针
双指针的特点: - 详细介绍双指针的主要特点 - 强调这些特点如何帮助识别和解决双指针问题 - 解释双指针的优势和局限性
双指针的优势与局限性: - 分析双指针的优势和局限性 - 讨论何时应该选择双指针 - 提供一些优化双指针的思路
双指针的适用场景: - 详细介绍双指针适用的各种场景 - 提供一些实际应用的例子 - 讨论如何识别可以使用双指针的问题
转场页: - 引入双指针的模板代码部分 - 可以简单提及掌握这些模板有助于快速实现双指针算法
同向双指针基本模板: - 提供同向双指针的基本代码模板 - 解释模板中的关键部分 - 强调指针移动规则的重要性
相向双指针基本模板: - 提供相向双指针的基本代码模板 - 解释与同向双指针的区别 - 强调初始位置和终止条件的设置
快慢指针模板: - 提供快慢指针的代码模板 - 解释快慢指针的特点和应用 - 强调快慢指针在链表操作中的重要性
滑动窗口模板: - 提供滑动窗口的代码模板 - 解释滑动窗口与同向双指针的关系 - 强调窗口大小调整的策略
双指针在有序数组中的应用模板: - 提供在有序数组中使用双指针的代码模板 - 解释有序性如何帮助优化算法 - 强调这种模板在查找问题中的应用
双指针在链表中的应用模板: - 提供在链表中使用双指针的代码模板 - 解释链表特性如何影响双指针的使用 - 强调这种模板在链表操作中的应用
双指针在字符串中的应用模板: - 提供在字符串中使用双指针的代码模板 - 解释字符串特性如何影响双指针的使用 - 强调这种模板在字符串处理中的应用
双指针的常见错误与注意事项: - 列举双指针实现中的常见错误 - 提供避免这些错误的方法 - 强调细节的重要性
转场页: - 引入实例分析部分 - 可以简单提及我们将通过几个经典例子来深入理解双指针
两数之和问题: - 详细介绍两数之和问题的描述和分析 - 解释如何使用双指针解决这个问题 - 强调有序性在问题解决中的重要性
两数之和问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
移除元素问题: - 详细介绍移除元素问题的描述和分析 - 解释如何使用双指针解决这个问题 - 强调原地操作的实现方法
移除元素问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
盛最多水的容器问题: - 详细介绍盛最多水的容器问题的描述和分析 - 解释如何使用双指针解决这个问题 - 强调贪心思想在问题解决中的应用
盛最多水的容器问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
三数之和问题: - 详细介绍三数之和问题的描述和分析 - 解释如何使用双指针解决这个问题 - 强调排序和去重的重要性
三数之和问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
链表中环的检测问题: - 详细介绍链表中环的检测问题的描述和分析 - 解释如何使用快慢指针解决这个问题 - 强调Floyd's Tortoise and Hare算法的原理
链表中环的检测问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
滑动窗口最大值问题: - 详细介绍滑动窗口最大值问题的描述和分析 - 解释如何使用滑动窗口和双指针解决这个问题 - 强调数据结构选择的重要性
滑动窗口最大值问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
转场页: - 引入练习题推荐部分 - 可以简单提及练习是掌握双指针的关键
基础练习题: - 提供一系列适合初学者的双指针练习题 - 简要介绍每道题的内容和难度 - 提供题目链接或参考资料
中级练习题: - 提供一系列适合有一定基础的学习者的双指针练习题 - 简要介绍每道题的内容和难度 - 提供题目链接或参考资料
高级练习题: - 提供一系列适合有较强基础的学习者的双指针练习题 - 简要介绍每道题的内容和难度 - 提供题目链接或参考资料
综合应用题: - 提供一系列需要综合运用多种算法技巧的双指针练习题 - 简要介绍每道题的内容和难度 - 提供题目链接或参考资料
转场页: - 引入总结部分 - 可以简单提及总结将回顾本次讲座的主要内容
总结: - 回顾双指针的核心概念和特点 - 强调双指针的应用场景和解题思路 - 提供一些学习和实践的建议
进阶方向: - 介绍双指针的进阶内容和优化技巧 - 提供一些实践建议和学习资源 - 鼓励学生继续深入学习和实践
封底页: - 感谢学生的参与 - 提供联系方式或其他资源 - 鼓励学生继续学习和实践