C++.双指针算法(1.1目录修正)
C++.双指针算法
- 1. 双指针算法概述
- 1.1 双指针算法的定义
- 1.2 双指针算法的应用场景
- 1.2.1 数组中的两数之和问题
- 1.2.2 链表中的环检测问题
- 1.2.3 滑动窗口问题
- 1.2.4 有序数组的合并问题
- 2. 双指针算法的实现基础
- 2.1 指针的基本概念
- 2.2 指针的运算操作
- 3. 双指针算法的常见类型及示例
- 3.1 快慢指针
- 3.1.1 快慢指针的原理
- 3.1.2 快慢指针的示例:链表中环的检测
- 3.2 左右指针
- 3.2.1 左右指针的原理
- 3.2.2 左右指针的示例:二分查找
- 3.3 对撞指针
- 3.3.1 对撞指针的原理
- 3.3.2 对撞指针的示例:两数之和
- 4. 双指针算法的优化技巧
- 4.1 指针移动的策略
- 4.2 指针初始位置的选择
- 5. 双指针算法的复杂度分析
- 5.1 时间复杂度分析
- 5.1.1 快慢指针
- 5.1.2 左右指针
- 5.1.3 对撞指针
- 5.1.4 总结
- 5.2 空间复杂度分析
- 5.2.1 快慢指针
- 5.2.2 左右指针
- 5.2.3 对撞指针
- 5.2.4 总结
- 6. 双指针算法的实践案例
- 6.1 案例一:滑动窗口问题
- 问题描述
- 算法思路
- 示例代码
- 示例分析
- 时间复杂度
- 空间复杂度
- 6.2 案例二:字符串匹配问题
- 问题描述
- 算法思路
- 示例代码
- 示例分析
- 7. 双指针算法的注意事项
- 7.1 指针越界问题
- 7.2 指针空指针问题
1. 双指针算法概述
1.1 双指针算法的定义
双指针算法是一种高效的算法思想,通常用于处理数组或链表等线性数据结构。其核心思想是使用两个指针(通常称为慢指针和快指针)来遍历数据结构,通过控制指针的移动速度和方向,实现对数据的高效处理。双指针算法的关键在于如何定义指针的移动规则,以及如何根据问题的需求来调整指针之间的关系。
1.2 双指针算法的应用场景
双指针算法在多种编程问题中都有广泛的应用,以下是一些常见的应用场景及示例代码:
1.2.1 数组中的两数之和问题
问题描述:给定一个整数数组nums
和一个目标值target
,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。
- 算法思路:使用双指针法,首先对数组进行排序,然后使用两个指针,一个指向数组的起始位置,另一个指向数组的末尾。通过比较两个指针所指向的元素之和与目标值的大小关系,来决定移动哪个指针。
- 示例代码:
#include <vector>
#include <algorithm>
using namespace std;vector<int> twoSum(vector<int>& nums, int target) {vector<pair<int, int>> nums_with_index;for (int i = 0; i < nums.size(); ++i) {nums_with_index.push_back({nums[i], i});}sort(nums_with_index.begin(), nums_with_index.end());int left = 0, right = nums.size() - 1;while (left < right) {int sum = nums_with_index[left].first + nums_with_index[right].first;if (sum == target) {return {nums_with_index[left].second, nums_with_index[right].second};} else if (sum < target) {++left;} else {--right;}}return {};
}
1.2.2 链表中的环检测问题
问题描述:给定一个链表,判断链表中是否存在环。
- 算法思路:使用快慢指针法,快指针每次移动两步,慢指针每次移动一步。如果链表中存在环,快指针和慢指针最终会相遇;如果不存在环,快指针会先到达链表的末尾。
- 示例代码:
#include <iostream>
using namespace std;struct ListNode {int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {}
};bool hasCycle(ListNode *head) {if (!head || !head->next) return false;ListNode *slow = head;ListNode *fast = head->next;while (slow != fast) {if (!fast || !fast->next) return false;slow = slow->next;fast = fast->next->next;}return true;
}
1.2.3 滑动窗口问题
问题描述:给定一个数组和一个整数k
,找出数组中所有长度为k
的子数组的最大值。
- 算法思路:使用双指针表示滑动窗口的左右边界,通过移动右指针扩展窗口,移动左指针收缩窗口,同时维护一个数据结构(如队列)来存储窗口内的最大值。
- 示例代码:
#include <vector>
#include <deque>
using namespace std;vector<int> maxSlidingWindow(vector<int>& nums, int k) {vector<int> result;deque<int> window;for (int i = 0; i < nums.size(); ++i) {// 移除窗口外的元素while (!window.empty() && window.front() <= i - k) {window.pop_front();}// 移除窗口内小于当前元素的值while (!window.empty() && nums[window.back()] < nums[i]) {window.pop_back();}window.push_back(i);// 当窗口大小达到k时,记录最大值if (i >= k - 1) {result.push_back(nums[window.front()]);}}return result;
}
1.2.4 有序数组的合并问题
问题描述:给定两个有序数组nums1
和nums2
,将它们合并为一个有序数组。
- 算法思路:使用双指针分别指向两个数组的起始位置,比较两个指针所指向的元素大小,将较小的元素添加到结果数组中,并移动对应的指针。重复此过程,直到其中一个数组遍历完成,然后将另一个数组的剩余部分添加到结果数组中。
- 示例代码:
#include <vector>
using namespace std;vector<int> merge(vector<int>& nums1, vector<int>& nums2) {vector<int> result;int i = 0, j = 0;while (i < nums1.size() && j < nums2.size()) {if (nums1[i] < nums2[j]) {result.push_back(nums1[i++]);} else {result.push_back(nums2[j++]);}}while (i < nums1.size()) {result.push_back(nums1[i++]);}while (j < nums2.size()) {result.push_back(nums2[j++]);}return result;
}
双指针算法通过巧妙地使用两个指针来解决问题,避免了暴力解法中不必要的重复计算,大大提高了算法的效率。
2. 双指针算法的实现基础
2.1 指针的基本概念
在C++中,指针是一种变量,它存储了另一个变量的内存地址。指针的基本概念是理解双指针算法的关键。以下是关于指针的一些重要概念:
- 指针的声明:指针的声明格式为
类型 *指针名;
,例如int *p;
表示声明了一个指向整数的指针p
。 - 指针的初始化:指针可以通过取地址运算符
&
来初始化,例如int a = 10; int *p = &a;
,此时p
存储了变量a
的地址。 - 指针的解引用:通过解引用运算符
*
可以访问指针所指向的变量的值,例如*p
将返回a
的值。 - 指针与数组的关系:数组名本身就是一个指向数组首元素的指针。例如,对于数组
int arr[5];
,arr
和&arr[0]
是等价的,都表示数组首元素的地址。
指针的基本概念是双指针算法实现的基础,通过指针可以高效地操作内存中的数据。
2.2 指针的运算操作
指针的运算操作主要包括指针的加减运算、指针的比较运算以及指针的自增自减运算。这些运算操作在双指针算法中非常重要,以下是详细介绍:
-
指针的加减运算:
- 指针的加法运算:
p + n
表示将指针p
向后移动n
个元素的位置。例如,对于int arr[5]; int *p = arr;
,p + 2
将指向arr[2]
。 - 指针的减法运算:
p - n
表示将指针p
向前移动n
个元素的位置。例如,p - 2
将指向arr[-2]
(注意:这可能会导致越界错误,需要谨慎使用)。 - 指针的差值运算:
p2 - p1
表示两个指针之间的元素个数。例如,对于int *p1 = arr; int *p2 = arr + 3;
,p2 - p1
的结果是3
。
- 指针的加法运算:
-
指针的比较运算:
- 指针可以使用比较运算符(
==
、!=
、>
、<
、>=
、<=
)进行比较。比较的结果取决于指针所指向的内存地址的大小。例如,p1 < p2
表示p1
所指向的地址小于p2
所指向的地址。
- 指针可以使用比较运算符(
-
指针的自增自减运算:
- 指针的自增运算:
p++
或++p
表示将指针p
向后移动一个元素的位置。例如,p++
将使p
指向下一个元素。 - 指针的自减运算:
p--
或--p
表示将指针p
向前移动一个元素的位置。例如,p--
将使p
指向前一个元素。
- 指针的自增运算:
这些运算操作在双指针算法中被广泛应用,通过合理地移动指针,可以高效地处理数组或链表中的数据。例如,在链表中的环检测问题中,通过快慢指针的移动,可以判断链表是否存在环。
3. 双指针算法的常见类型及示例
3.1 快慢指针
3.1.1 快慢指针的原理
快慢指针是一种特殊的双指针技术,其中一个指针(快指针)的移动速度是另一个指针(慢指针)的两倍。这种技术主要用于链表结构,用于检测环的存在或寻找链表的中点等。快指针每次移动两个节点,而慢指针每次移动一个节点。如果链表中存在环,快指针和慢指针最终会在环内相遇;如果不存在环,快指针会先到达链表的末尾。
3.1.2 快慢指针的示例:链表中环的检测
问题描述:给定一个链表,判断链表中是否存在环。
算法思路:使用快慢指针法,快指针每次移动两步,慢指针每次移动一步。如果链表中存在环,快指针和慢指针最终会相遇;如果不存在环,快指针会先到达链表的末尾。
示例代码:
#include <iostream>
using namespace std;struct ListNode {int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {}
};bool hasCycle(ListNode *head) {if (!head || !head->next) return false;ListNode *slow = head;ListNode *fast = head->next;while (slow != fast) {if (!fast || !fast->next) return false;slow = slow->next;fast = fast->next->next;}return true;
}
3.2 左右指针
3.2.1 左右指针的原理
左右指针通常用于数组或字符串等线性数据结构。左右指针分别从数组或字符串的两端开始,向中间移动。通过比较左右指针所指向的元素,可以实现各种操作,如查找目标值、计算最大值或最小值等。左右指针的移动规则根据具体问题的需求而定,通常在满足某些条件时移动左指针或右指针。
3.2.2 左右指针的示例:二分查找
问题描述:给定一个有序数组nums
和一个目标值target
,请你在该数组中查找目标值的位置。如果目标值不存在于数组中,返回-1
。
算法思路:使用左右指针表示数组的搜索范围,初始时左指针指向数组的起始位置,右指针指向数组的末尾。通过比较中间位置的元素与目标值的大小关系,决定移动左指针或右指针,逐步缩小搜索范围,直到找到目标值或搜索范围为空。
示例代码:
#include <vector>
using namespace std;int binarySearch(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {return mid;} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}return -1;
}
3.3 对撞指针
3.3.1 对撞指针的原理
对撞指针是一种特殊的左右指针技术,通常用于处理数组或字符串中的对称问题或需要从两端向中间逐步逼近的问题。对撞指针的初始位置分别位于数组或字符串的两端,通过逐步移动指针,逐步缩小范围,直到两个指针相遇。对撞指针的移动规则根据具体问题的需求而定,通常在满足某些条件时移动左指针或右指针。
3.3.2 对撞指针的示例:两数之和
问题描述:给定一个整数数组nums
和一个目标值target
,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。
算法思路:使用对撞指针法,首先对数组进行排序,然后使用两个指针,一个指向数组的起始位置,另一个指向数组的末尾。通过比较两个指针所指向的元素之和与目标值的大小关系,来决定移动哪个指针。
示例代码:
#include <vector>
#include <algorithm>
using namespace std;vector<int> twoSum(vector<int>& nums, int target) {vector<pair<int, int>> nums_with_index;for (int i = 0; i < nums.size(); ++i) {nums_with_index.push_back({nums[i], i});}sort(nums_with_index.begin(), nums_with_index.end());int left = 0, right = nums.size() - 1;while (left < right) {int sum = nums_with_index[left].first + nums_with_index[right].first;if (sum == target) {return {nums_with_index[left].second, nums_with_index[right].second};} else if (sum < target) {++left;} else {--right;}}return {};
}
4. 双指针算法的优化技巧
4.1 指针移动的策略
指针移动的策略是双指针算法优化的关键。合理的移动策略可以减少不必要的计算,提高算法的效率。以下是一些常见的优化策略:
- 根据问题需求调整移动速度:在快慢指针中,快指针的移动速度通常是慢指针的两倍,但根据具体问题,可以调整移动速度。例如,在链表中寻找环的入口时,快指针和慢指针相遇后,将其中一个指针移回链表头部,然后两个指针以相同的速度移动,它们再次相遇的位置即为环的入口。这种调整可以避免重复遍历,提高算法效率。
- 动态调整指针位置:在处理数组或字符串问题时,指针的移动不仅取决于当前指针所指向的值,还可能与数组或字符串的其他部分有关。例如,在滑动窗口问题中,右指针用于扩展窗口,左指针用于收缩窗口。当窗口内的数据不满足条件时,左指针需要根据窗口内的数据动态调整位置,以找到满足条件的最小窗口。
- 减少指针回溯:在某些情况下,指针回溯会增加算法的时间复杂度。通过合理设计指针的移动规则,可以尽量减少指针回溯。例如,在二分查找中,通过比较中间值与目标值的大小关系,直接决定左指针或右指针的移动,避免了不必要的回溯操作,从而将时间复杂度降低到O(log n)。
4.2 指针初始位置的选择
指针的初始位置对双指针算法的性能也有重要影响。选择合适的初始位置可以减少不必要的遍历,提高算法的效率。以下是一些常见的选择策略:
- 根据数据结构的特点选择初始位置:对于数组或字符串,左右指针通常分别从两端开始。例如,在两数之和问题中,左右指针分别从数组的起始位置和末尾位置开始,这样可以充分利用数组的有序性,快速找到满足条件的两个数。对于链表,快慢指针通常从链表头部开始,这样可以方便地检测链表中是否存在环。
- 根据问题的目标选择初始位置:在某些问题中,指针的初始位置需要根据问题的具体目标来选择。例如,在滑动窗口问题中,右指针从数组的起始位置开始,用于扩展窗口,而左指针在窗口满足条件时才开始移动,用于收缩窗口。这种初始位置的选择可以确保窗口的大小和位置符合问题的要求,从而提高算法的效率。
- 考虑边界条件选择初始位置:在选择指针初始位置时,还需要考虑边界条件。例如,在处理数组或字符串时,指针不能超出数组或字符串的范围。在链表中,指针不能指向空节点。因此,在选择初始位置时,需要根据数据结构的边界条件进行调整,以避免潜在的错误。
5. 双指针算法的复杂度分析
5.1 时间复杂度分析
双指针算法的时间复杂度主要取决于指针的移动次数和每次移动的操作复杂度。以下是对几种常见双指针算法的时间复杂度分析:
5.1.1 快慢指针
- 链表中环的检测:
- 时间复杂度:O(n)。快指针每次移动两步,慢指针每次移动一步。在最坏的情况下,快指针会遍历整个链表一次,慢指针会遍历链表的一半。因此,总的时间复杂度为O(n)。
- 示例:假设链表长度为
n
,快指针和慢指针相遇时,快指针最多移动2n
步,慢指针最多移动n
步,总的时间复杂度为O(n)。
5.1.2 左右指针
-
二分查找:
- 时间复杂度:O(log n)。左右指针每次将搜索范围缩小一半,直到找到目标值或搜索范围为空。因此,时间复杂度为O(log n)。
- 示例:假设数组长度为
n
,每次将搜索范围缩小一半,经过log2(n)
次操作后,搜索范围为空,因此时间复杂度为O(log n)。
-
两数之和:
- 时间复杂度:O(n)。左右指针从数组的两端开始,每次移动一个指针,直到找到满足条件的两个数或指针相遇。因此,时间复杂度为O(n)。
- 示例:假设数组长度为
n
,左右指针最多移动n
次,每次移动的操作复杂度为O(1),因此总的时间复杂度为O(n)。
5.1.3 对撞指针
-
两数之和:
- 时间复杂度:O(n)。对撞指针从数组的两端开始,每次移动一个指针,直到找到满足条件的两个数或指针相遇。因此,时间复杂度为O(n)。
- 示例:假设数组长度为
n
,对撞指针最多移动n
次,每次移动的操作复杂度为O(1),因此总的时间复杂度为O(n)。
-
滑动窗口问题:
- 时间复杂度:O(n)。右指针用于扩展窗口,左指针用于收缩窗口。每个元素最多被访问两次(一次被右指针访问,一次被左指针访问),因此总的时间复杂度为O(n)。
- 示例:假设数组长度为
n
,右指针最多移动n
次,左指针最多移动n
次,每次移动的操作复杂度为O(1),因此总的时间复杂度为O(n)。
5.1.4 总结
双指针算法的时间复杂度通常为O(n)或O(log n),具体取决于指针的移动方式和问题的性质。通过合理设计指针的移动规则,可以显著提高算法的效率,避免不必要的重复计算。
5.2 空间复杂度分析
双指针算法的空间复杂度主要取决于额外使用的空间。以下是对几种常见双指针算法的空间复杂度分析:
5.2.1 快慢指针
- 链表中环的检测:
- 空间复杂度:O(1)。快指针和慢指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
- 示例:在链表中环的检测问题中,只使用了两个指针变量,没有使用额外的数据结构,因此空间复杂度为O(1)。
5.2.2 左右指针
-
二分查找:
- 空间复杂度:O(1)。左右指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
- 示例:在二分查找问题中,只使用了两个指针变量和一个中间变量,没有使用额外的数据结构,因此空间复杂度为O(1)。
-
两数之和:
- 空间复杂度:O(1)。左右指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
- 示例:在两数之和问题中,只使用了两个指针变量,没有使用额外的数据结构,因此空间复杂度为O(1)。
5.2.3 对撞指针
-
两数之和:
- 空间复杂度:O(1)。对撞指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
- 示例:在两数之和问题中,只使用了两个指针变量,没有使用额外的数据结构,因此空间复杂度为O(1)。
-
滑动窗口问题:
- 空间复杂度:O(k)。滑动窗口问题通常需要一个额外的数据结构(如队列)来存储窗口内的元素,其大小为窗口的长度
k
。因此,空间复杂度为O(k)。 - 示例:在滑动窗口问题中,使用了一个队列来存储窗口内的元素,队列的大小为窗口的长度
k
,因此空间复杂度为O(k)。
- 空间复杂度:O(k)。滑动窗口问题通常需要一个额外的数据结构(如队列)来存储窗口内的元素,其大小为窗口的长度
5.2.4 总结
双指针算法的空间复杂度通常为O(1)或O(k),具体取决于是否使用了额外的数据结构。大多数双指针算法只使用了常数级别的额外空间,因此空间复杂度较低。在某些情况下,如滑动窗口问题,可能需要使用额外的数据结构来存储窗口内的元素,此时空间复杂度会稍高。
通过合理设计双指针算法,可以在保证时间效率的同时,尽量减少空间的使用,从而实现高效的算法设计。
6. 双指针算法的实践案例
6.1 案例一:滑动窗口问题
滑动窗口问题是双指针算法的经典应用场景之一,主要用于处理数组或字符串中的连续子序列问题。以下是一个具体的实践案例,详细讲解如何使用双指针算法解决滑动窗口问题。
问题描述
给定一个整数数组nums
和一个整数k
,找出数组中所有长度为k
的子数组的最大值。
算法思路
使用双指针表示滑动窗口的左右边界,通过移动右指针扩展窗口,移动左指针收缩窗口,同时维护一个数据结构(如队列)来存储窗口内的最大值。具体步骤如下:
-
初始化:
- 定义两个指针
left
和right
,分别表示滑动窗口的左边界和右边界,初始时均指向数组的起始位置。 - 使用一个双端队列
window
来存储窗口内的元素索引,队列的前端始终存储当前窗口的最大值的索引。
- 定义两个指针
-
扩展窗口:
- 移动右指针
right
,将新元素的索引加入队列。在加入之前,需要移除队列中所有小于当前元素的索引,以确保队列的前端始终存储最大值的索引。
- 移动右指针
-
收缩窗口:
- 如果队列的前端索引小于左边界
left
,说明该索引已经不在当前窗口范围内,需要从队列中移除。 - 当窗口大小达到
k
时,记录当前窗口的最大值(队列前端的值)。
- 如果队列的前端索引小于左边界
-
移动窗口:
- 每次移动右指针后,如果窗口大小超过
k
,则移动左指针,收缩窗口。
- 每次移动右指针后,如果窗口大小超过
示例代码
#include <vector>
#include <deque>
using namespace std;vector<int> maxSlidingWindow(vector<int>& nums, int k) {vector<int> result;deque<int> window;for (int right = 0; right < nums.size(); ++right) {// 移除窗口外的元素while (!window.empty() && window.front() <= right - k) {window.pop_front();}// 移除窗口内小于当前元素的值while (!window.empty() && nums[window.back()] < nums[right]) {window.pop_back();}window.push_back(right);// 当窗口大小达到k时,记录最大值if (right >= k - 1) {result.push_back(nums[window.front()]);}}return result;
}
示例分析
假设输入数组nums = {1, 3, -1, -3, 5, 3, 6, 7}
,窗口大小k = 3
,则算法的执行过程如下:
-
初始状态:
left = 0, right = 0
window
为空
-
扩展窗口:
right = 0
,nums[0] = 1
,加入队列,window = {0}
right = 1
,nums[1] = 3
,移除队列中小于3的索引,window = {1}
right = 2
,nums[2] = -1
,移除队列中小于-1的索引,window = {1, 2}
-
记录最大值:
- 当
right = 2
时,窗口大小达到3,记录最大值nums[1] = 3
,结果为{3}
- 当
-
继续扩展窗口:
right = 3
,nums[3] = -3
,移除队列中小于-3的索引,window = {1, 2, 3}
right = 4
,nums[4] = 5
,移除队列中小于5的索引,window = {4}
-
收缩窗口:
left = 1
,移除队列中索引小于left
的值,window = {4}
-
记录最大值:
- 当
right = 4
时,窗口大小达到3,记录最大值nums[4] = 5
,结果为{3, 5}
- 当
-
重复上述过程,最终结果为
{3, 3, 5, 5, 6, 7}
时间复杂度
- 时间复杂度:O(n)。每个元素最多被加入和移除队列一次,因此总的时间复杂度为O(n)。
空间复杂度
- 空间复杂度:O(k)。队列的大小最多为窗口的大小
k
。
6.2 案例二:字符串匹配问题
字符串匹配问题是双指针算法的另一个经典应用场景,主要用于在主字符串中查找模式字符串的出现位置。以下是一个具体的实践案例,详细讲解如何使用双指针算法解决字符串匹配问题。
问题描述
给定两个字符串text
(主字符串)和pattern
(模式字符串),找出pattern
在text
中的所有出现位置。
算法思路
使用双指针分别指向主字符串和模式字符串,通过逐字符比较来判断模式字符串是否在主字符串中出现。具体步骤如下:
-
初始化:
- 定义两个指针
i
和j
,分别指向主字符串text
和模式字符串pattern
的起始位置。 - 定义一个结果数组
result
,用于存储模式字符串在主字符串中的所有出现位置。
- 定义两个指针
-
逐字符比较:
- 如果
text[i] == pattern[j]
,则两个指针同时向右移动。 - 如果
text[i] != pattern[j]
,则将主字符串的指针i
回退到上次匹配失败的位置的下一个位置,模式字符串的指针j
回退到模式字符串的起始位置。
- 如果
-
记录匹配位置:
- 当模式字符串的指针
j
到达模式字符串的末尾时,说明找到了一个匹配的子串,将主字符串的指针i - j
的位置加入结果数组。 - 重置指针
i
和j
,继续查找下一个匹配的子串。
- 当模式字符串的指针
示例代码
#include <vector>
#include <string>
using namespace std;vector<int> strStr(string text, string pattern) {vector<int> result;int i = 0, j = 0;while (i <= text.size() - pattern.size()) {if (text[i + j] == pattern[j]) {++j;if (j == pattern.size()) {result.push_back(i);i = i + j;j = 0;}} else {i = i + j;j = 0;}}return result;
}
示例分析
假设主字符串text = "ababcabcab"
,模式字符串pattern = "abc"
,则算法的执行过程如下:
-
初始状态:
i = 0, j = 0
result
为空
-
逐字符比较:
i = 0, j = 0
,text[0] == 'a'
,pattern[0] == 'a'
,匹配成功,j = 1
i = 0, j = 1
,text[1] == 'b'
,pattern[1] == 'b'
,匹配成功,j = 2
i = 0, j = 2
,text[2] == 'a'
,pattern[2] == 'c'
,匹配失败,i = 2
,j = 0
-
继续比较:
i = 2, j = 0
,text[2] == 'a'
,pattern[0] == 'a'
,匹配成功,j = 1
i = 2, j = 1
,text[3] == 'b'
,pattern[1] == 'b'
,匹配成功,j = 2
i = 2, j = 2
,text[4] == 'c'
,pattern[2] == 'c'
,匹配成功,j = 3
-
记录匹配位置:
- 当
j = 3
时,模式字符串匹配成功,记录位置i = 2
,结果为{2}
- 重置指针
i = 5
,j = 0
- 当
-
**
7. 双指针算法的注意事项
7.1 指针越界问题
指针越界是双指针算法中常见的问题之一,它可能导致程序崩溃或产生不可预测的结果。指针越界通常发生在指针移动超出数据结构的边界时。以下是几种常见的指针越界情况及其解决方法:
-
数组越界:
- 问题描述:在处理数组时,指针可能移动到数组的边界之外。例如,在使用左右指针时,如果指针移动规则设计不当,可能会导致指针指向数组的负索引或超出数组的最大索引。
- 解决方法:在移动指针之前,必须检查指针是否在数组的有效范围内。例如,在二分查找中,确保左指针不超过右指针,且指针的值始终在数组的索引范围内。可以通过添加边界条件检查来避免越界问题。例如:
if (left < 0 || right >= nums.size()) {// 处理越界情况 }
-
链表越界:
- 问题描述:在处理链表时,指针可能移动到链表的末尾之外。例如,在使用快慢指针检测环时,如果链表中不存在环,快指针可能会先到达链表的末尾,此时继续移动快指针会导致空指针异常。
- 解决方法:在移动指针之前,必须检查指针是否为空。例如,在链表环检测中,每次移动快指针之前,需要检查快指针是否为空:
if (!fast || !fast->next) {return false; }
7.2 指针空指针问题
空指针问题是指指针未初始化或指向了一个无效的内存地址。在双指针算法中,空指针问题可能导致程序崩溃或产生错误的结果。以下是几种常见的空指针情况及其解决方法:
-
未初始化的指针:
- 问题描述:在使用指针之前,如果没有正确初始化指针,指针可能会指向一个随机的内存地址,导致不可预测的行为。
- 解决方法:在声明指针时,必须确保指针被正确初始化。例如,可以将指针初始化为
nullptr
,并在使用之前检查指针是否为空:int *p = nullptr; if (p) {// 使用指针 }
-
链表中的空指针:
- 问题描述:在处理链表时,如果链表为空或指针移动到链表的末尾,可能会导致空指针异常。例如,在链表中删除节点时,如果链表为空,直接访问链表的头节点会导致空指针异常。
- 解决方法:在操作链表之前,必须检查链表是否为空。例如,在链表中删除节点时,需要先检查链表是否为空:
if (!head) {return; // 链表为空 }
-
数组中的空指针:
- 问题描述:在处理数组时,如果数组为空或指针移动到数组的边界之外,可能会导致空指针异常。例如,在数组中查找元素时,如果数组为空,直接访问数组的元素会导致空指针异常。
- 解决方法:在操作数组之前,必须检查数组是否为空。例如,在数组中查找元素时,需要先检查数组是否为空:
if (nums.empty()) {return; // 数组为空 }