双指针是用两个指针去遍历数组,协同完成检索任务。一般是利用两个指向元素的关系,决定之后指针的移动操作,找到目标或完成任务,如
当两个指针指向同一数组,并且同向移动时,可以形成滑动窗口,快慢指针等;
当两个指针指向同一数组,并且反向移动时,可以对有序数组形成检索。
1. 两数之和 II - 输入有序数组(167)
使用反向指针,检索数组。
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int p1=0,p2=numbers.size()-1;
while(p1!=p2){
if(numbers[p1]+numbers[p2]<target){
p1++;
}
else if(numbers[p1]+numbers[p2]>target){
p2--;
}
else{
break;
}
}
// return{p1+1,p2+1};
vector<int> ans;
ans.push_back(p1+1);
ans.push_back(p2+1);
return ans;
}
};
语法:可以直接return{p1+1,p2+1}; 这是C++11新特性:“返回用大括弧初始化的该函数对象(返回的类型和函数类型一样)。即返回用p1+1和p2+1初始化后的twosum对象。
2. 合并两个有序数组(88)
用双指针合并两个有序数组是常见用法,但是本题要求不能使用额外空间,而是直接合并到数组1中。如果从nums1和nums2的头部开始比较,则需要移动nums1的后面元素。因此我们需要从尾部开始比较,把确定位置的元素插入nums1的尾部空闲部分。
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int p1 = m-1,p2 = n-1,p3 = m+n-1;
while(p1 >= 0 &&p2 >= 0){
if(nums1[p1] < nums2[p2]){
nums1[p3--] = nums2[p2];
p2--;
}
else{
nums1[p3--] = nums1[p1];
p1--;
}
}
// 若nums2中还有剩余元素
if(p2>=0){
while(p2>=0){
nums1[p3--] = nums2[p2];
p2--;
}
}
}
};
3. 环形链表 II(142)
快慢指针可以用于判断链表中是否存在环。让快指针每次走两个结点,慢指针每次走一个结点。如果存在环,则进入环后,快指针最终将追上慢指针。
本题还要求判断入环结点的位置,这需要一定的数学推导。首先,慢指针在进入环的第一圈就会被追上,因为两个指针距离范围为0-S-1(设环长为S),每走一次快指针靠近慢指针一步,所以在S步内就会被追上。注意:由于每次是接近一步,所以只会恰好相遇,而不会出现快指针经过慢指针的情况。
如下图所示,设链表中环外部分的长度为 a。slow 指针进入环后,又走了 b 的距离与 fast 相遇。此时,fast 指针已经走完了环的 nn 圈,因此它走过的总距离为 a+n(b+c)+b = a+(n+1)b+nc。
根据题意,任意时刻,\textit{fast}fast 指针走过的距离都为 \textit{slow}slow 指针的 22 倍。因此,我们有
a+(n+1)b+nc=2(a+b) ⟹ a=c+(n−1)(b+c)
现在把fast移动回起点,让fast和slow每次都移动一格。则fast走过a距离时到达入环结点,slow则位于 b+a = n(b+c),正好也回到入环结点。即第二次在入环结点处相遇。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *fast = head,*slow = head;
do{
// 出现空指针,说明遍历到头,没有环
if(fast==nullptr || fast->next==nullptr) return nullptr;
fast = fast->next->next;
slow = slow->next;
}while(fast!=slow);
fast = head;
// 第二次相遇处,即为入环的第一个结点
while(fast!=slow){
fast = fast->next;
slow = slow->next;
}
return fast;
}
};
方法二:利用哈希表,当出现某个结点被第二次访问时,说明存在环,且该结点就是入环结点。
用unordered_set存储指针,当指针已存在说明是环。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// unordered_set是只去重不进行内部排序的set
unordered_set<ListNode *> visited;
while (head != nullptr) {
// count()函数时查找集合中是否存在该元素
if (visited.count(head)) {
return head;
}
visited.insert(head);
head = head->next;
}
return nullptr;
}
};
除了判断环,快慢指针还可以用来寻找链表中倒数第n个结点(让快指针先走n步)。
4. 最小覆盖子串(76)
本题要从字符串s中找到包含字符串t所有字符的最小字串,可以用双指针形成滑动窗口检索,具体思路如下:
(1)对s长度小于t长度,直接返回“”;
(2)遍历t,使用map统计各个字符的出现次数;
(3)遍历s,先找到一个包含t的字串,方法是:固定p1在s[0],先移动p2,碰到t中字符,就使其对应map次数-1,直到map所有字符次数<=0,就找到了一个包含t的字串;如果找不到,返回"";
(4)确定最小字串,使p1前移,只要p1经过的字符没在t中,或者滑动窗口该字符个数有多,就可以前移,缩小滑动窗口左边,之后更新子串长度;p2继续右移,直到抵达一个t中有的字符,并更新map。循环上述过程,直到p2到达s末端,就会找到所有的子串,并比较出最短子串。
class Solution {
public:
unordered_map<char,int> mp;
bool isFind(){
for(auto it=mp.begin();it!=mp.end();it++){
if(it->second>0) return false;
}
return true;
}
string minWindow(string s, string t) {
// s长度小于t长度直接返回""
int slen = s.length(),tlen = t.length();
if(slen<tlen){
return "";
}
// 统计t中字符数量
for(int i=0;i<tlen;i++){
auto it = mp.find(t[i]);
if(it!=mp.end()){
mp[t[i]]++;
}else{
mp[t[i]]=1;
}
}
// 先找到一个从s[0]开始的包含t的字串
int p1=0, p2=0,subStr = slen;
bool flag = false;
while(p2<slen){
auto it = mp.find(s[p2]);
if(it!=mp.end()){
mp[s[p2]]--;
}
if(isFind()){//找到包含t的子串
flag =true;
break;
}
++p2;
}
if(flag == false) return "";
// 寻找最小字串
int idx1=p1,idx2=p2;//记录下标
while(p2<slen){
// 先前移p1,缩小滑动窗口的范围
while(p1<=p2){
auto it = mp.find(s[p1]);
if(it != mp.end()){
// p1再往前移,就会有元素没有被包含到
if(it->second==0) break;
else ++(it->second);
}
p1++;
}
//更新子串长度
if(p2-p1+1 < subStr){
subStr = p2-p1+1;
idx1 = p1;
idx2 = p2;
}
// p2前移继续寻找
while(p2<slen){
auto it = mp.find(s[++p2]);
if(it != mp.end()){
--(it->second);
break;
}
}
}
string ans;
for(int i=idx1;i<=idx2;i++){
ans+=s[i];
}
return ans;
//return s.substr(idx1,subStr);
}
};
语法:(1)使用unordered_map比map更快,因为它不需要排序。unordered_map是用哈希表实现的,map是用红黑树实现的;
(2)返回子串可以用substr(pos,len)函数,pos为开始位置,len为子串长度。
本题的映射只需要在字符和int间建立,使用map的话大量find很不方便,实际上可以自己建立数组用于哈希,操作起来更方便。
class Solution {
public:
string minWindow(string s, string t) {
vector<int> need(128,0);
int count = 0;
for(char c : t)
{
need[c]++;
}
count = t.length();
int l=0, r=0, start=0, size = INT_MAX;
while(r<s.length())
{
char c = s[r];
if(need[c]>0)
count--;
need[c]--; //先把右边的字符加入窗口
if(count==0) //窗口中已经包含所需的全部字符
{
while(l<r && need[s[l]]<0) //缩减窗口
{
need[s[l++]]++;
} //此时窗口符合要求
if(r-l+1 < size) //更新答案
{
size = r-l+1;
start = l;
}
need[s[l]]++; //左边界右移之前需要释放need[s[l]]
l++;
count++;
}
r++;
}
return size==INT_MAX ? "" : s.substr(start, size);
}
};
语法:(1)INT_MAX、INT_MIN表示整型的最大、最小整数;
(2)for(char c : str)是C++11借鉴Python的一种遍历用法,即让c等于字符串的每个字符进行遍历操作;其它容器也可做类似操作,如对double类型数组a,可以这样遍历for(double x:a),x逐一等于a的每一个元素。
5. 平方数之和(633)
判断C是否能表示为两个整数的平方和,类似于两数之和,双指针反向寻找:
(1)先确定范围,[0,(int)sqrt(c)];
(2)令p1,p2分别等于0和(int)sqrt(c),根据p1和P2平方和进行移动。
class Solution {
public:
bool judgeSquareSum(int c) {
int p1 = 0, p2 = int(pow(c,0.5));
while(p1<=p2){
int sum = pow(p1,2)+pow(p2,2);
if(sum == c) return true;
else if(sum < c) --p2;
else ++p1;
}
return false;
}
};
注意:sum在c很大时,可能会超出Int界限,所以改用了long long。在官方解答中使用了long类型。int 和 long的区别如下:
int long longlong在不同平台上长度可能不一致,但必须遵循:int不少于16位(2字节),Long不少于32位,long long不少于64位,且sizeof(int) <= sizeof(long) <= sizeof(long long)。在某些平台上,int和long可能都是4字节,32位,这时它们是一样的。
6. 验证回文字符串 Ⅱ(680)
只删除一个元素情况下,能否使字符串变成回文串。同样使用双指针检验,当出现不一致时,考察删除p1或p2指向元素,只要有一种情况下能子串是回文串即可。
class Solution {
public:
//判断字串是否是回文字符串
bool check(string s,int i,int j){
while(i < j){
if(s[i] == s[j]){
++i;
--j;
}
else return false;
}
return true;
}
bool validPalindrome(string s) {
int p1 = 0, p2 = s.length()-1;
bool flag = false;
while(p1 <= p2){
if(s[p1] == s[p2]){
++p1;
--p2;
}
else {
if(check(s,p1+1,p2)||check(s,p1,p2-1)) return true;
else return false;
}
}
return true;
}
};
开始我犯了一个错误,认为当出现不一致时,只要s[p1+1] == s[p2],就一定是按照删除p1。但碰到如:string a = "cuppucu";的情况,只有删除末尾的u才能成立。因此必须严格地对两种删除情况都做检查,只要有一个成立就行。
7. 通过删除字母匹配到字典里最长单词(524)
(1)先用双指针判断单词是否是s的子序列:如果比较后,单词字符串被遍历完,则说明是子序列;
(2)更新子序列;
预先对字典进行排序,可能可以更快一点,
class Solution {
public:
string findLongestWord(string s, vector<string>& dictionary) {
string ans="";
for(string str:dictionary){
int p1=0,p2=0;
while(p1<s.length()&&p2<str.length()){
if(s[p1]==str[p2]) {
++p1;
++p2;
}
else{
++p1;
}
}
// 本单词不是s子串
if(p2<str.length()) continue;
if(str.length()>ans.length() ||(str.length()==ans.length() && str<ans)) ans = str;
}
return ans;
}
};
Python版
class Solution:
# ->返回注释,说明返回的类型;self代表当前对象的地址。self能避免非限定调用造成的全局变量
def findLongestWord(self, s: str, dictionary: List[str]) -> str:
ans = ""
for t in dictionary:
i=j=0
while i<len(s) and j<len(t):
if s[i]==t[j]:
j+=1
i+=1
# t被遍历完,说明是s的子序列
if j==len(t):
if len(t)>len(ans) or len(t)==len(ans) and t<ans:
ans = t
return ans