1. 算法解釋
雙指針主要用於遍歷數組,兩個指針指向不同的元素,從而協同完成任務。也可以延伸到多個數組的多個指針。
若兩個指針指向同一數組,遍歷方向相同且不會相交,則也稱為滑動窗口(兩個指針包圍的區域即為當前的窗口),經常用於區間搜索。
若兩個指針指向同一數組,但是遍歷方向相反,則可以用來進行搜索,待搜索的數組往往是排好序的。
對於 C++ 語言,指針還可以玩出很多新的花樣。一些常見的關於指針的操作如下。
1.1 指針與常量
1.2 指針函數與函數指針
static string Blog_Adress = "https://www.cnblogs.com/wanghongyang/"
// 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);
}
int main() {
// minus是函數指針,指向函數的指針
int (*minus)(int, int) = subtraction;
int* m = addition(1, 2);
int n = operation(3, *m, minus);
cout << "*m: " << *m << " " << "n: " << n << endl;
return 0;
}
函數指針,需要大家了解
運行結果為
2. 兩數之和
167. 兩數之和 II - 輸入有序數組
給定一個已按照 升序排列 的整數數組 numbers ,請你從數組中找出兩個數滿足相加之和等於目標數 target 。
函數應該以長度為 2 的整數數組的形式返回這兩個數的下標值。numbers 的下標 從 1 開始計數 ,所以答案數組應當滿足 1 <= answer[0] < answer[1] <= numbers.length 。
你可以假設每個輸入只對應唯一的答案,而且你不可以重復使用相同的元素。
示例 1:
輸入:numbers = [2,7,11,15], target = 9
輸出:[1,2]
解釋:2 與 7 之和等於目標數 9 。因此 index1 = 1, index2 = 2 。
示例 2:
輸入:numbers = [2,3,4], target = 6
輸出:[1,3]
示例 3:
輸入:numbers = [-1,0], target = -1
輸出:[1,2]
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers 按 遞增順序 排列
-1000 <= target <= 1000
僅存在一個有效答案
題解
因為數組已經排好序,我們可以采用方向相反的雙指針來尋找這兩個數字,一個初始指向最小的元素,即數組最左邊,向右遍歷;一個初始指向最大的元素,即數組最右邊,向左遍歷。
- 如果兩個指針指向元素的和等於給定值,那么它們就是我們要的結果。
- 如果兩個指針指向元素的和小於給定值,我們把左邊的指針右移一位,使得當前的和增加一點。
- 如果兩個指針指向元素的和大於給定值,我們把右邊的指針左移一位,使得當前的和減少一點。
證明
可以證明,對於排好序且有解的數組,雙指針一定能遍歷到最優解。證明方法如下:假設最
優解的兩個數的位置分別是 l 和 r。我們假設在左指針在 l 左邊的時候,右指針已經移動到了 r;
此時兩個指針指向值的和小於給定值,因此左指針會一直右移直到到達 l。同理,如果我們假設
在右指針在 r 右邊的時候,左指針已經移動到了 l;此時兩個指針指向值的和大於給定值,因此
右指針會一直左移直到到達 r。所以雙指針在任何時候都不可能處於 (l,r) 之間,又因為不滿足條
件時指針必須移動一個,所以最終一定會收斂在 l 和 r。
代碼
static string Blog_Adress = "https://www.cnblogs.com/wanghongyang/"
class Solution {
public:
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;
}
}
// 這里是因為題目要求下標從1開始
return vector<int>{l + 1, r + 1};
}
};
執行結果
3. 合並兩個有序數組
88. 合並兩個有序數組
給你兩個有序整數數組 nums1 和 nums2,請你將 nums2 合並到 nums1 中,使 nums1 成為一個有序數組。
初始化 nums1 和 nums2 的元素數量分別為 m 和 n 。你可以假設 nums1 的空間大小等於 m + n,這樣它就有足夠的空間保存來自 nums2 的元素。
示例 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]
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[i] <= 109
題解
因為這兩個數組已經排好序,我們可以把兩個指針分別放在兩個數組的末尾,即 nums1 的 m − 1 位和 nums2 的 n − 1 位。每次將較大的那個數字復制到 nums1 的后邊,然后向前移動一位。
因為我們也要定位 nums1 的末尾,所以我們還需要第三個指針,以便復制。
在以下的代碼里,我們直接利用 m 和 n 當作兩個數組的指針,再額外創立一個 pos 指針,起始位置為 m +n−1。每次向前移動 m 或 n 的時候,也要向前移動 pos。這里需要注意,如果 nums1的數字已經復制完,不要忘記把 nums2 的數字繼續復制;如果 nums2 的數字已經復制完,剩余nums1 的數字不需要改變,因為它們已經被排好序。
注意 這里我們使用了
++ 和--的小技巧:a++ 和 ++a 都是將 a 加 1,但是 a++ 返回值為 a,而++a 返回值為 a+1。如果只是希望增加 a 的值,而不需要返回值,則推薦使用 ++a,其運行速度會略快一些。
代碼
static string Blog_Adress = "https://www.cnblogs.com/wanghongyang/"
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
// 這里m--返回的是m,但是m實際上已經減小了
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--];
}
}
};
執行結果
4. 快慢指針
142. 環形鏈表 II
給定一個鏈表,返回鏈表開始入環的第一個節點。 如果鏈表無環,則返回 null。
為了表示給定鏈表中的環,我們使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該鏈表中沒有環。注意,pos 僅僅是用於標識環的情況,並不會作為參數傳遞到函數中。
說明:不允許修改給定的鏈表。
進階:
你是否可以使用 O(1) 空間解決此題?
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:返回索引為 1 的鏈表節點
解釋:鏈表中有一個環,其尾部連接到第二個節點。
示例 2:
輸入:head = [1,2], pos = 0c
輸出:返回索引為 0 的鏈表節點
解釋:鏈表中有一個環,其尾部連接到第一個節點。
示例 3:
輸入:head = [1], pos = -1
輸出:返回 nullc
解釋:鏈表中沒有環。
提示:
鏈表中節點的數目范圍在范圍 [0, 104] 內
-105 <= Node.val <= 105c
pos 的值為 -1 或者鏈表中的一個有效索引
題解
結論
對於鏈表找環路的問題,有一個通用的解法—─快慢指針(Floyd判圈法)。給定兩個指針,分別命名為 slow和 fast,起始位置在鏈表的開頭。每次fast 前進兩步,slow前進一步。如果fast可以走到盡頭,那么說明沒有環路;如果fast 可以無限走下去,那么說明一定有環路,且一定存在一個時刻slow和fast相遇。當slow和 fast第一次相遇時,我們將fast重新移動到鏈表開頭,並讓 slow和fast每次都前進一步。當slow和 fast第二次相遇時,相遇的節點即為環路的開始點。
詳解
static string Blog_Adress = "https://www.cnblogs.com/wanghongyang/"
原理:首先初始化快指針 fast = head.next.next 和 slow = head.next,
此時快指針走的路長為2, m慢指針走的路長為1,之后令快指針每次走兩步,
慢指針每次走一步,這樣快指針走的路長始終是慢指針走的路長的兩倍,
若不存在環,直接返回None,
若存在環,則 fast 與 slow 肯定會在若干步之后相遇;
性質1:
設從head需要走 a 步到達環的入口,如果環存在的話,
再走 b 步可以再次到達該入口(即環的長度為b),
如果存在環的話,上述描述的 快指針 fast 和
慢指針slow 必然會相遇,且此時slow走的路長
小於 a + b(可以自行證明),設其為 a + x,
當快慢指針相遇時,快指針已經至少走完一圈環了,
不妨設相遇時走了完整的m圈(m >= 1),有:
快指針走的路長為 a + mb + x
慢指針走的路長為 a + x
由於快指針fast 走的路長始終是慢指針的 2倍,所以:
a + mb + x = 2(a + x)
化簡可得:
a = mb - x ------------- (*)
當快指針與慢指針相遇時,由於 <性質1> 的存在,
可以在鏈表的開頭初始化一個新的指針,
稱其為 detection, 此時 detection 距離環的入口的距離為 a,
此時令 detection 和 fast 每次走一步,
會發現當各自走了 a 步之后,兩個指針同時到達了環的入口,理由分別如下:
detection不用說了,走了a步肯定到剛好到入口
fast已經走過的距離為 a + mb + x,當再走 a 步之后,
fast走過的總距離為 2a + mb + x,帶入性質1的(*)式可得:
2a + mb + x = a + 2mb,會發現,fast此時剛好走完了
整整 2m 圈環,正好處於入口的位置。
基於此,我們可以進行循環,直到 detection 和
fast 指向同一個對象,此時指向的對象恰好為環的入口。
代碼
/**
* 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) {
// 1.判斷是否有環
if(head==NULL){
return NULL;
}
// 創建兩個快慢指針
ListNode* fast = head;
ListNode* slow = head;
// 進行循環判斷
while(fast!=NULL && fast->next!=NULL){
// fast走兩步
fast = fast->next->next;
// slow走一步
slow = slow->next;
// 當fast=slow時,證明有環,跳出循環
if(fast==slow){
break;
}
}
// 將無環的情況返回NULL
if(fast==NULL||fast->next==NULL){
return NULL;
}
// 如果有環,則將fast返回到頭節點
fast = head;
while(fast!=slow){
// fast與slow都一步一個節點
fast = fast->next;
slow = slow->next;
}
// 當fast與slow相遇時,則為入環的第一個節點
return fast;
}
};
還有一種寫法
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
// 判斷是否存在環路
do {
if (!fast || !fast->next) return nullptr;
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
// 如果存在,查找環路節點
fast = head;
while (fast != slow){
slow = slow->next;
fast = fast->next;
}
return fast;
}
執行結果
5. 平方數之和
633. 平方數之和
思路
兩種方法
1)雙指針
由於a,b兩個數的范圍在0到根號C之間,因此,我們用兩個指針指向左邊和右邊
如果
- 平方和大於c,則右邊的指針減1
- 平方和小於c,則左邊的指針加1
- 平方和等於c,則返回true
代碼
class Solution {
public:
bool judgeSquareSum(int c) {
long r = 0, l = (int)sqrt(c), sum;
while (r <= l) {
sum = r * r + l * l;
if (sum == c) {
return true;
}
if (sum < c) {
++r;
}
else {
--l;
}
}
return false;
}
};
執行結果
2)枚舉法
簡單來說就是先找一個數a,然后另外一個數就是根號下C-a*a,然后把這個數取整,計算平方和,等於c就返回true,否則返回false
下面放上官方的代碼,很好理解
class Solution {
public:
bool judgeSquareSum(int c) {
for (long a = 0; a * a <= c; a++) {
double b = sqrt(c - a * a);
if (b == (int)b) {
return true;
}
}
return false;
}
};
6. 驗證回文字符串
680. 驗證回文字符串 Ⅱ
題解
題目要求最多刪一個字符,所以情況比較簡單,我們可以用雙指針把刪除一個字符后出現的兩種情況寫出來就好。
雙指針分別為head 和 tail。 head從左往右遍歷,tail從右往左遍歷。
遇到s[head] != s[tail]時,就分化為兩種情況即
- head = head + 1, tail = tail
- head = head, tail = tail - 1
- 分別從這兩種情況進行判斷即可,如果還有不等情況,那么就返回false。
代碼
static string Blog_Adress = "https://www.cnblogs.com/wanghongyang/"
class Solution {
public:
bool validPalindrome(string s) {
if (s.empty() || s.size() == 1)
return true;
int length = s.size();
int head = 0, tail = length - 1;
while (head < tail) //正常雙指針判斷回文字符串
{
if (s[head] == s[tail])
{
++head;
--tail;
}
else
break; //從分歧點退出
}
// 因為如果是正常退出,即head>=tail
if (head >= tail) //如果是正常退出
return true;
//情況1
int new_head = head + 1, new_tail = tail;
int flag1 = true, flag2 = true;
while (new_head < new_tail)
{
if (s[new_head] == s[new_tail])
{
++new_head;
--new_tail;
}
else
{
flag1 = false;
break;
}
}
//情況2
new_head = head;
new_tail = tail - 1;
while (new_head < new_tail)
{
if (s[new_head] == s[new_tail])
{
++new_head;
--new_tail;
}
else
{
flag2 = false;
break;
}
}
//由於對兩種情況進行遍歷,所以只要有一種能滿足回文,那就可以!
return flag1 || flag2;
}
};
執行結果
7. 刪除字母匹配到字典里最長單詞
524. 通過刪除字母匹配到字典里最長單詞
題解
雙指針,但是在使用雙指針前需要對被查找集合做排序
1,根據題目要求,先將dictionary的字符串按照字符串的長度從大到小排序,且字符串符合字典序,進行排序,目的是為了接下查找時,dictionary中第一個符合條件字符串的即為題目要求的答案。
2,定義並初始化,字符串s的長度s_len,dictionary的長度d_len,dictionary中字符串的長度ds_len,指向字符串s的指針s_ptr,指向dictionary中第i個字符串的指針ds_ptr。
3,for循環遍歷dictionary中所以字符串,獲取當前dictionary中第i個的字符串的長度
4,while循環使用雙指針,比較字符串s是否包含當前第i個dictionary中的字符串,
如果包含,則d_ptr遍歷到dictionary中第i個的字符串的末尾,即d_ptr == ds_len - 1,返回dictionary[i]即為答案,即返回長度最長且字典序最小的字符串。
如果不包含,則d_ptr未遍歷到dictionary中第i個的字符串的末尾,且s_ptr遍歷到字符串s的末尾
5,退出當前while循環,即將遍歷dictionary中的第i+1個字符串,雙指針歸零為下一個while循環做准備
6,如果退出for循環,則表示答案不存在,則返回空字符串。
代碼
class Solution {
public:
string findLongestWord(string s, vector<string>& dictionary) {
//字符串的長度從大到小排序,且字符串符合字典序
auto cmp = [&](string& a, string& b)
{
if (a.size() == b.size()) {
return a < b;
}
return a.size() > b.size();
};
sort(dictionary.begin(), dictionary.end(), cmp);
int s_len = s.size(), d_len = dictionary.size(), ds_len = 0;
int s_ptr = 0, d_ptr = 0;
//雙指針方法,遍歷字典
for (int i = 0; i < d_len; ++i)
{
ds_len = dictionary[i].size(); //當前字典的字符串的長度
while (s_ptr < s_len && d_ptr < ds_len)
{
if (s[s_ptr] == dictionary[i][d_ptr]) //存在相等的字母
{
if (d_ptr == ds_len - 1) //且已經到達當前字符串的末尾,即存在,因為已經排序,所以第一個符合條件的即為答案
{
return dictionary[i];
}
//當前字典的字符串的下一個字母
++d_ptr;
}
//匹配被查找字符串的下一個字母
++s_ptr;
}
//比較字典的下一個字符串,被查找字符串的s_ptr歸零
s_ptr = 0;
//進行字典的下一個字符串比較,d_ptr歸零
d_ptr = 0;
}
return "";
}
};
運行結果
8. 總結
這個系列讓我了解到雙指針的一些題目場景,了解了雙指針的使用,然后雙指針的部分就到這里,下期開始寫二分查找