双指针
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多
个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的
区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是
排好序的。
TWO SUM
167. 两数之和 II - 输入有序数组
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
思路:
因为数组已经排好序,我们可以采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。
如果两个指针指向元素的和等于给定值,那么它们就是我们要的结果。如果两个指针指向元素的和小于给定值,我们把左边的指针右移一位,使得当前的和增加一点。如果两个指针指向元素的和大于给定值,我们把右边的指针左移一位,使得当前的和减少一点。
(前后夹,没什么好讲的)
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int first = 0, last = numbers.size() - 1, sum;
while (first <= last) {
sum = numbers[first] + numbers[last];
if (sum > target)
last--;
else {
if (sum == target)
return { first+1,last+1 };
first++;
}
}
return {0,0};
}
};
归并有序数组
88. 合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
思路:
因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的 m − 1 位和 nums2 的 n − 1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。
因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。
(ps:注意nums1的数分配完后不能再判断了,否则越界,直接全部复制nums2,而nums2的数分配完后就不用分配了,因为数组本就已排序的原因,剩下的都已经分配好了)
(因为是数组,还有一个有空位,直接从后往前插,每次比较最大的,注意好边界条件就行)
代码:
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int pos = nums1.size() - 1;
while (pos>-1) {
if (n < 1)
return;
if (m < 1||nums2[n - 1] > nums1[m - 1])
nums1[pos--] = nums2[n-- - 1];
else
nums1[pos--] = nums1[m-- - 1];
}
return;
}
};
快慢指针
142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:
你是否可以使用 O(1) 空间解决此题?
思路:
对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd 判圈法)。给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点(设非环处长度为m,环长度为n,那么第一次相遇fast走了m+An+k,slow走了m+Bn+k,两者相减为(A-B)*n发现是n的倍数,证明两者相遇时总共走了n圈环,设(A-B)*n为S,因为fast是slow的两倍速,则slow走了S,此时把fast指到头,两者同等速度走m步到环的起点时,slow走的总长度为S+m)
代码:
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* fast = head, * slow = head;
if (head == NULL)
return NULL;
while (true) {
if (fast->next == NULL || fast->next->next == NULL)
return NULL;
fast = fast->next->next, slow = slow->next;
if (fast == slow)
break;
}
fast = head;
while (fast != slow)
fast = fast->next, slow = slow->next;
return fast;
}
};\
滑动窗口
76. 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
思路:
开两个数组分别记录t内字符在s出现的频率和标志位(t中出现的字符设置标志位为1,未出现的字符设为0),左右指针从头开始,右指针不断往右遍历,出现一次t中字符,频率数组对应字符频率减1,如果频率大于等于0,说明t中该字符在当前字符串未溢出,这时我们设置一个初始为0的变量cnt,加1,记录当前字符串t的字符总共出现了多少个,如果频率小于0,说明该字符超出了需求,cnt就不做处理,然后每次右指针遍历到了t内字符,我们先判断该字符是否溢出,从而决定加不加cnt,然后把左指针一直右移到下一个属于t内字符,而且不溢出的字符出现的地方,然后判断cnt,看是否t内字符都出现了,是的话若左指针所指字符溢出或不是指向t内字符,右移到下一个t内字符,然后判断当前字符串是否最小,记录,如果右指针遍历时没有发现t内字符,就不做处理继续右移。
(一开始的思路有些复杂,现在重新缕一缕,首先哈希记录t的字符频率是肯定的,然后从头遍历也是肯定的,一个数记录t的所有字符是否出现也是肯定的,那么现在的问题就是,第一次所有字符齐了以后,记录最小长度,为减小长度,我们肯定先得把左指针往右移啊,因为已经满足条件了,右指针右移只会增加长度,那么左指针什么时候停呢?自然是遇到下一个t内字符为止,而如果这个字符已经溢出了t所需求的,自然还可以往右移,直到遇到不溢出的,此时再把右指针右移,直到下一次满足条件)
代码:
class Solution {
public:
string minWindow(string s, string t) {
vector<int> tcnt(58, 0), find(58, 0);
string re;
int scnt = 0, min = s.size() + 1, mlast = -1, mfirst = -1;
for (string::iterator it = t.begin(); it != t.end(); it++)
tcnt[*it - 'A']++, find[*it - 'A'] = 1;
for (int l = 0, r = 0; l < s.size() && r < s.size(); r++) {
if (find[s[r] - 'A']) {//只对t内字符处理后续就方便多了
if (tcnt[s[r] - 'A']-- > 0) {
if (++scnt == t.size()) {
if(min > r - l)
min = r - l, mfirst = l, mlast = r;
scnt--, tcnt[s[l++] - 'A']++;
}
}
}
while (l < s.size() && (!find[s[l] - 'A'] || tcnt[s[l] - 'A'] < 0))
tcnt[s[l++] - 'A']++;
}
return mlast < 0 ? re = "" : re.assign(s.begin() + mfirst, s.begin() + mlast + 1);
}
};
练习
633. 平方数之和
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a^2 + b^2 = c 。
思路:
类似167,从0-c设置头尾两指针往中间靠
(这里有减少时间的捷径,既然要找平方和等于它的,尾指针就从它的平方根开始,因为平方根后的数自己平方都大于它了,还搜索什么呢?)
代码:
class Solution {
public:
bool judgeSquareSum(int c) {
long r = sqrt(c), l = 0, sum = 0;
while (l<=r) {
sum = pow(r, 2) + pow(l, 2);
if (sum < c)
l++;
else if (sum > c)
r--;
else
return true;
}
return false;
}
};
680. 验证回文字符串 Ⅱ
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
思路:
头尾两个指针,若指向字符相同,一起往中间靠一步,若不同,flag加1,分别尝试头指针和尾指针向中间靠,看是否相等,都不等就证明无法最多删除一个字符成为回文字符串,若相等,那就按照相等的做法单独移动头或尾指针(若移动头或尾都相等,那么先按其中一种情况分析,一旦失败,就换另一种),接下来回到开头步骤不断重复,一旦flag大于1,则失败
(这种找对称就是头尾开始找,如果不同,为判断是哪边多余了,左右各往内走一下,如果都不同,肯定不能靠删一次解决问题了,那么如果都相等呢?按照每种情况再往内走一次。
一种易错的情况:
[1,1…2,1] 看上去判断很麻烦,不知道左边哪个1是多余的,但是仔细分析发现,只要删去一个1,不在意是第几个,所以只要左右指针相同的话,直接左右往中间靠一步就行了)
代码:
class Solution {
public:
int cnt = 0;
bool validPalindrome(string s) {
int l = 0, r = s.size()-1;
string a;
while (l <= r) {
if (s[l] == s[r])
l++, r--;
else {
if (cnt++)
return false;
return validPalindrome(a.assign(s.begin() + l + 1, s.begin() + r + 1)) || validPalindrome(a.assign(s.begin() + l, s.begin() + r));
}
}
return true;
}
};
524. 通过删除字母匹配到字典里最长单词
给你一个字符串 s 和一个字符串数组 dictionary 作为字典,找出并返回字典中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。
如果答案不止一个,返回长度最长且字典序最小的字符串。如果答案不存在,则返回空字符串。
思路:
类似88,而且因为如果答案不止一个,返回长度最长且字典序最小的字符串,那么匹配到第一个词后,字典内长度不大于该词的就可以直接pass了。
那么言归正传,每次选取一个字典内词,两个指针分别从两个字符串匹配…
代码:
class Solution {
public:
string findLongestWord(string s, vector<string>& dictionary) {
int maxlen = 0, l, r;
string a = "";
for (int i = 0; i < dictionary.size(); i++) {
l = r = 0;
if (dictionary[i].size() < maxlen || (a != "" && dictionary[i].size() == maxlen && dictionary[i].compare(a) >= 0))
continue;
while (l<s.size()&&r< dictionary[i].size()) {
if (s[l] == dictionary[i][r])
r++;
l++;
}
if(r == dictionary[i].size())
maxlen = dictionary[i].size(), a = dictionary[i];
}
return a;
}
};