LeetCode解題記錄(雙指標專題)

進擊的汪sir發表於2021-07-20

1. 演算法解釋

雙指標主要用於遍歷陣列,兩個指標指向不同的元素,從而協同完成任務。也可以延伸到多個陣列的多個指標。

若兩個指標指向同一陣列,遍歷方向相同且不會相交,則也稱為滑動視窗(兩個指標包圍的區域即為當前的視窗),經常用於區間搜尋。

若兩個指標指向同一陣列,但是遍歷方向相反,則可以用來進行搜尋,待搜尋的陣列往往是排好序的。

對於 C++ 語言,指標還可以玩出很多新的花樣。一些常見的關於指標的操作如下。

1.1 指標與常量

image-20210719094743103

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;
}

函式指標,需要大家瞭解

執行結果為

image-20210719131318749

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
僅存在一個有效答案

題解

因為陣列已經排好序,我們可以採用方向相反的雙指標來尋找這兩個數字,一個初始指向最小的元素,即陣列最左邊,向右遍歷;一個初始指向最大的元素,即陣列最右邊,向左遍歷。

  • 如果兩個指標指向元素的和等於給定值,那麼它們就是我們要的結果。
  • 如果兩個指標指向元素的和小於給定值,我們把左邊的指標右移一位,使得當前的和增加一點。
  • 如果兩個指標指向元素的和大於給定值,我們把右邊的指標左移一位,使得當前的和減少一點。

證明

可以證明,對於排好序且有解的陣列,雙指標一定能遍歷到最優解。證明方法如下:假設最

優解的兩個數的位置分別是 lr。我們假設在左指標在 l 左邊的時候,右指標已經移動到了 r

此時兩個指標指向值的和小於給定值,因此左指標會一直右移直到到達 l。同理,如果我們假設

在右指標在 r 右邊的時候,左指標已經移動到了 l;此時兩個指標指向值的和大於給定值,因此

右指標會一直左移直到到達 r。所以雙指標在任何時候都不可能處於 (l,r) 之間,又因為不滿足條

件時指標必須移動一個,所以最終一定會收斂在 lr

程式碼

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};
	}
};

執行結果

image-20210719151033717

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 的末尾,所以我們還需要第三個指標,以便複製。

在以下的程式碼裡,我們直接利用 mn 當作兩個陣列的指標,再額外創立一個 pos 指標,起始位置為 m +n−1。每次向前移動 mn 的時候,也要向前移動 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--];
        }

    }
};

執行結果

image-20210719221147773

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;
}

執行結果

image-20210719231219006

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;
	}
};

執行結果

image-20210720113743112

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;
    }
};

執行結果

image-20210720125017983

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 "";
	}
};

執行結果

image-20210720131457191

8. 總結

這個系列讓我瞭解到雙指標的一些題目場景,瞭解了雙指標的使用,然後雙指標的部分就到這裡,下期開始寫二分查詢

相關文章