双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
两数之和
思路:
- 双指针法,已知数组有序,两数之和小于目标值时,左指针++;两数之和小于目标值时,右指针--;
- 哈希表,遍历数组,查找数组中是否存在目标元素减当前i。如果存在,我们需要找出它的索引。而保持数组中的每个元素与其索引相互对应的最好方法是哈希表。
哈希表法:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> res;
unordered_map<int, int> hash;//由于unorder_map速度要比map快所以选择无序哈希表
for(int i=0; i < nums.size();++i){
int another = target - nums[i];
if(hash.count(another)){
res = vector<int>({hash[another], i});
return res;
}
hash[nums[i]] = i;
}
return res;
}
};
双指针法:
vector<int> twoSum(vector<int>& numbers, int target) {
int l = 0, r = numbers.size() - 1, sum;
while (l < r) {
sum = numbers[l] + numbers[r];
if (sum == target) break;
if (sum < target) ++l;
else --r;
}
return vector<int>{l + 1, r + 1};
}
归并两个有序数组
思路:双指针,从前往后遍历需要把其他元素存放在其他位置,需要使用额外空间,所以我们采用从后往前改写nums1的值。
因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的 m−1位和nums2的 n−1位。每次将较大的那个数字复制到nums1的后边,然后向前移动一位. 因为我们也要定位nums1的末尾,所以我们还需要第三个指针,以便复制,每次向前移动m或n的时候,也要向前移动第三个指针.
结束条件以nums2全都插入进去为止(因为nums1本身是有序的)
- a++ 和 ++a 都是将 a 加1,但是 a++ 返回值为 a,而 ++a返回值为 a+1。如果只是希望增加 a的值,而不需要返回值,则推荐使用++a,因为其运行速度会略快一些。
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int pos = m-- + n-- - 1;
while (m >= 0 && n >= 0) {
nums1[pos--] = nums1[m] > nums2[n]? nums1[m--]: nums2[n--];
}
while (n >= 0) {
nums1[pos--] = nums2[n--];
}
}
};
环形链表II
思路:
对于链表找环路的问题,通用的解法为快慢指针。给定两个指针,分别命名为slow和fast,起始位置在链表的开头。每次fast前进两步,slow前进一步。如果fast可以走到尽头,那么说明没有环路;如果fast可以无限走下去,那么说明一定有环路,且一定存在一个时刻slow和fast相遇。当slow和fast第一次相遇时,我们将fast重新移动到链表开头,并让slow和fast每次都前进一步。当slow和fast第二次相遇时,相遇的节点即为环路的开始点。
- 以下内容取自leetcode 作者:carlsun-2
如果有环,如何找到这个环的入口
假设从头结点到环形入口节点的节点数为x
环形入口节点到fast指针与slow指针相遇节点节点数为y
从相遇节点再到环形入口节点节点数为z
如图所示:
相遇时,slow指针走过的节点数为: x + y
fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数
因为fast指针是一步走两个节点,slow指针一步走一个节点,所以fast指针走过的节点数 = slow指针走过的节点数 * 2
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为我们要找环形的入口,那么要求的是x,因为x表示头结点到环形入口节点的的距离。
所以我们要求x ,将x单独放在左面:x = n (y + z) - y
在从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针
这个公式说明什么呢
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了slow指针了
当n为1的时候,公式就化解为 x = z
这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2
让index1和index2同时移动,每次移动一个节点,那么他们相遇的地方就是环形入口的节点
n如果大于1,就是fast指针在环形转n圈之后才遇到slow指针
其实这种情况和n为1的时候效果是一样的,一样可以通过这个方法找到环形的入口节点,只不过index1指针在环里多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点
代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
滑动窗口
思路:
采用类似滑动窗口的思路,即用两个指针表示窗口左端left和右端right。 向右移动right,保证left与right之间的字符串足够包含需要包含的所有字符, 而在保证字符串能够包含所有需要的字符条件下,向右移动left,保证left的位置对应为需要的字符,这样的窗口才有可能最短,此时只需要判断当期窗口的长度是不是目前来说最短的,决定要不要更新L和R(这两个 变量用于记录可能的最短窗口的端点)
搞清楚指针移动的规则之后,需要确定当前窗口包含所有需要的字符,以及怎么确定left的 位置对应的是需要的字符。 这里我们用一个数组chars其中chars表示目前每个字符缺少的数量,flag表示每个字符是否在T中存在。 只要我们在向右移动right的时候,碰到t中的一个字符,对应chars的数量就减一,那么当chars这些元素的值都不大于0的时候, 我们的窗口里面就包含了所有需要的字符;但判断chars这些元素的值都不大于0并不能在O(1)时间内实现,因此我们要用一个变量来记录我们遍历过字符数目,记为cnt,当我们遍历s的时候,碰到chars中存在的字符且对应计数大于0,就说明我们还没有找到足够的字符,那么就要继续向右移动r,此时cnt+=1;直到t_len变为T.size(),就说明此时已经找到足够的字符保证窗口符合要求了。
所以接下来就是移动left。我们需要移动left,直到找到目标字符串中的字符,同时又希望窗口尽可能短,因此我们就希望找到的left使得窗口的开头就是要求的字符串中的字符,同时整个窗口含有所有需要的字符数量。注意到,前面我们更新chars的时候, 比如字符"A",如果我们窗口里面有10个A,而目标字符串中有5个A,那此时chars中A对应的计数就是-5,那么我要收缩窗口又要保证窗口能够包含所需的字符,那么我们就要在收缩窗口的时候增加对应字符在chars的计数,直到我们找到某个位置的字符A,此时字典中的计数为0,就不可以再收缩了(如果此时继续移动left,那么之后的窗口就不可能含有A这个字符了),此时窗口为可能的最小窗口,比较更新记录即可。
代码如下:
class Solution {
public:
string minWindow(string S, string T) {
vector<int> chars(128, 0);
vector<bool> flag(128, false); // 先统计T中的字符情况
for (int i = 0; i < T.size(); i++) {
flag[T[i]] = true;
chars[T[i]]++;
}
// 移动滑动窗口,不断更改统计数据
int cnt = 0, l = 0, min_l = 0, min_size = S.size() + 1;
for (int r = 0; r < S.size(); ++r) {
if (flag[S[r]]) {
chars[S[r]]--;
if (chars[S[r]] >= 0) {
cnt++;
}
// 若目前滑动窗口已包含T中全部字符,
// 则尝试将l右移,在不影响结果的情况下获得最短子字符串
while (cnt == T.size()) {
if (r - l + 1 < min_size) {
min_l = l;
min_size = r - l + 1;
}
chars[S[l]]++;
if (flag[S[l]] && chars[S[l]] > 0) {
cnt--;
}
l++;
}
}
}
return min_size > S.size()? "": S.substr(min_l, min_size);
}
};