算法解释

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

对于 C++ 语言,指针还可以玩出很多新的花样。一些常见的关于指针的操作如下:

指针与常量

int x;
int *p1 = &x; // 指针可以被修改,值也可以被修改
const int *p2 = &x; // 指针可以被修改,值不可以被修改(const int)
int *const p3 = &x; // 指针不可以被修改(*const),值可以被修改
const int *const p4 = &x; // 指针不可以被修改,值也不可以被修改

指针函数与函数指针

// addition是指针函数,一个返回类型是指针的函数
int* addition(int a, int b) {
int *sum = new int(a + b);
return sum;
}
int subtraction(int a, int b) {
return a - b;
}
int operation(int x, int y, int (*func)(int, int)) {
return (*func)(x,y);
}

// minus是函数指针,指向函数的指针
int (*minus)(int, int) = subtraction;
int *m = addition(1, 2);
int n = operation(3, *m, minus);

Two Sum

两数之和II

给你一个下标从 1 开始的整数数组 numbers,该数组已按非递减顺序排列,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2],则 1 <= index1 < index2 <= numbers.length。

以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和index2。

你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。你所设计的解决方案必须只使用常量级的额外空间。

示例 1:

输入:numbers = [2,7,11,15], target = 9
输出:[1,2]

示例 2:

输入:numbers = [2,3,4], target = 6
输出:[1,3]

示例 3:

输入:numbers = [-1,0], target = -1
输出:[1,2]

采用方向相反的双指针遍历数组,因为已经排好序,所以如果两个数之和小于目标值,则左指针右移,若大于目标值,则右指针左移。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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 和 nums2,另有两个整数 m 和 n,分别表示nums1 和 nums2 中的元素数目。

请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0,应忽略。nums2 的长度为 n。

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]

示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]

示例 3:

输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
注意:因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的 m−1 位和 nums2 的 n−1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。
因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针 pos,以便复制。

注意:如果 nums1 的数字已经复制完,不要忘记把 nums2 的数字继续复制;如果 nums2 的数字已经复制完,剩余 nums1 的数字不需要改变,因为它们已经被排好序。

代码
1
2
3
4
5
6
7
8
9
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

给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

链表定义如下:

1
2
3
4
5
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};

不允许修改链表。

示例:

输入:示例
head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点

对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd 判圈法)。给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast 可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
do {
if (!fast || !fast->next)
return NULL;
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
fast = head;
while (fast != slow){
slow = slow->next;
fast = fast->next;
}
return fast;
}

滑动窗口

最小覆盖子串

给你一个字符串 s、一个字符串 t。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “”。s,t只由英文字母组成。

注意:对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = “ADOBECODEBANC”, t = “ABC”
输出:”BANC”

示例 2:

输入:s = “a”, t = “a”
输出:”a”

示例 3:

输入: s = “a”, t = “aa”
输出: “”

本题使用滑动窗口求解,即两个指针 l 和 r 都是从最左端向最右端移动,且 l 的位置一定在 r 的左边或重合。先统计 t 中字符数量,滑动窗口至包含所有字符,l 左移,得到最短子串。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
string minWindow(string S, string T) {
vector<int> chars(128, 0);
vector<bool> flag(128, false);
for (char t : T) {
flag[t] = true;
chars[t]++;
}
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]]) {
if (--chars[S[r]] >= 0) {
cnt++;
}
while (cnt == T.size()) {
if (r - l - 1 < min_size) {
min_l = l;
min_size = r - l + 1;
}
if (flag[S[l]] && ++chars[S[l]] > 0) {
cnt--;
}
l++;
}
}
}
return min_size>S.size() ? "" : S.substr(min_l, min_size);
}

练习

平方数之和

给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a^2^ + b^2^ = c 。

示例 1:

输入:c = 5
输出:true

示例 2:

输入:c = 3
输出:false

双指针,一个指向0,另一个指向根号c取整,左指针向右移或者右指针向左移,就可以避免双重循环解决问题。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool judgeSquareSum(int c) {
long l = 0, r = sqrt(c) + 1;
long ans = 0;
while(l <= r) {
ans = l * l + r * r;
if (ans == c)
return true;
if (ans < c)
l++;
else
r--;
}
return false;
}

验证回文字符串II

给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。

示例 1:

输入: s = “aba”
输出: true

示例 2:

输入: s = “abca”
输出: true

示例 3:

输入: s = “abc”
输出: false

双指针,一个在开头,一个在末尾,相向移动,不符合的字符记录下来即可。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool checkPalindrome(const string &s, int low , int high) {
for(int i = low, j = high; i < j; i++,j--) {
if(s[i] != s[j])
return false;
}
return true;
}
bool validPalindrome(string s) {
int l = 0;
int r = s.size() - 1;
while(l < r) {
if(s[l] == s[r]) {
l++;
r--;
}
else {
return checkPalindrome(s, l+1, r) || checkPalindrome(s, l, r-1);
}
}
return true;
}

删除字母匹配字符串

给你一个字符串 s 和一个字符串数组 dictionary,找出并返回 dictionary 中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。均只由小写英文字母组成。

如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。

示例 1:

输入:s = “abpcplea”, dictionary = [“ale”,”apple”,”monkey”,”plea”]
输出:”apple”

示例 2:

输入:s = “abpcplea”, dictionary = [“a”,”b”,”c”]
输出:”a”

双指针,一个用于遍历 s,另一个遍历 d 中的字符串,记录满足题目要求的字符串即可。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string findLongestWord(string s, vector<string>& dictionary) {
string ans = "";
for (string d : dictionary) {
int m = 0, n = 0;
if (ans.size() < d.size()
|| (ans.size() == d.size() && ans.compare(d) > 0)) {
while (m < s.size() && n < d.size()) {
if (s[m] == d[n])
n++;
m++;
}
if (n == d.size()) {
ans = d;
}
}
}
return ans;
}

至多包含 k 个不同字符的最长子串

给定一个字符串 s,找出至多包含 k 个不同字符的最长子串 T 的长度。

示例 1:

输入:s = “eceba”, k = 2
输出:3

示例 2:

输入:s = “aa”, k = 1
输出:2

滑动窗口。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int lengthOfLongestSubstringKDistinct(string s, int k) {
unordered_map<char,int> m;
int maxlen = 0;
for(int i = 0, j = 0; i < s.size(); ++i) {
if(m.size() <= k)
m[s[i]]++;
while(m.size() > k) {
if(--m[s[j]] == 0)
m.erase(s[j]);
j++;
}
maxlen = max(maxlen, i-j+1);
}
return maxlen;
}

因为这题是付费的,所以我并不确定是否完全AC,仅供参考