Skip to content

Latest commit

 

History

History
841 lines (816 loc) · 28.8 KB

双指针算法.md

File metadata and controls

841 lines (816 loc) · 28.8 KB

数组的删除操作

理论基础

数组删除元素的原理,并不是真正意义的删除,而是将需要删除的元素用其它元素覆盖,时间复杂度为 $O(n)$

数组的删除操作系列题型

leetcode.27 移除元素

  • 链接https://leetcode.cn/problems/remove-element/
  • 解题思路:双指针
    新指针 k 用来存新数组的下标
    旧指针 i 用来遍历原来的数组
    元素存入新数组条件:只要原数组中的元素不是要删除的元素都要存入新数组
  • leetcode解题代码
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int k = 0;
        for (int i = 0; i < nums.size(); i ++){
            if (nums[i] != val)
                nums[k ++] = nums[i];
        }
        return k;
    }
};

leetcode.26 删除有序数组中的重复项

  • 链接https://leetcode.cn/problems/remove-duplicates-from-sorted-array/
  • 解题思路:双指针
    新指针 k 用来存新数组的下标
    旧指针 i 用来遍历原来的数组
    元素存入新数组条件:第一个元素要存入新数组,从第二个元素开始,所有和上一个元素不相等的元素都要存入新数组
  • leetcode解题代码
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int k = 0;
        for (int i = 0; i < nums.size(); i ++){
            if (i == 0 || nums[i] != nums[i - 1])
                nums[k ++] = nums[i];
        }
        return k;
    }
};

leetcode.283 移动零

  • 链接https://leetcode.cn/problems/move-zeroes/
  • 解题思路:双指针
    新指针 k 用来存新数组的下标
    旧指针 i 用来遍历原来的数组
    元素存入新数组条件:只要原数组中的元素不是 0,就存入新数组
    将新数组其余的位赋值为 0 即可
  • leetcode解题代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int k = 0;
        for (int i = 0; i < nums.size(); i ++){
            if (nums[i] != 0)
                nums[k ++] = nums[i];
        }
        
        while (k < nums.size()) nums[k ++] = 0;
    }
};

leetcode.844 比较含退格的字符串

  • 链接https://leetcode.cn/problems/backspace-string-compare/
  • 解题思路:双指针
    新指针 k 用来存新数组的下标
    旧指针 i 用来遍历原来的数组
    元素存入新数组条件:只要原数组中的元素不是 #,就存入新数组,如果是 #,就将新数组的下标前移一位(新数组的下标大于 0)
    resize 函数调整新数组的尺寸
  • leetcode解题代码
class Solution {
public:
    void get(string &s){
        int k = 0;
        for (int i = 0; i < s.size(); i ++){
            if (s[i] != '#')
                s[k ++] = s[i];
            else 
                if (k > 0)
                    k --;
        }
        s.resize(k);
    }

    bool backspaceCompare(string s, string t) {
        get(s);
        get(t);
        return s == t;
    }
};

leetcode.977 有序数组的平方

  • 链接https://leetcode.cn/problems/squares-of-a-sorted-array/
  • 解题思路:双指针
  • 注意:由于数组是有序的,最大值只可能出现在原来数组的两端,所以新数组也从最末端开始存
    新指针 k 用来存新数组的下标
    旧指针 i j 用来遍历原来的数组,i 从前往后遍历,j 从后往前遍历
    元素存入新数组条件:比较下标 i 和下标 j 元素的平方,新数组存较大的那个值
  • leetcode解题代码
class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int n = nums.size();
        vector<int> res(n);

        for (int i = 0, j = n - 1, k = n - 1; i <= j;){
            if (nums[i] * nums[i] > nums[j] * nums[j]){
                res[k] = nums[i] * nums[i];
                k --, i ++;
            }
            else {
                res[k] = nums[j] * nums[j];
                k --, j --;
            }
        }
        return res;
    }
};

n 数之和系列题型

leetcode.15 三数之和

  • 链接https://leetcode.cn/problems/3sum/
  • 解题思路:双指针
  • 注意:将数组排序用于去重和指针移动
    指针 i l r 分别用于找三个符合要求的元素
    i 遍历原数组
    当 i 遍历原数组的时候定义 l 和 r,l 位于 i 的右侧 r 位于数组末尾
    当三数之和大于 0 则 r 指针向前移动
    小于 0 则 l 指针向后移动
    等于 0 输出答案数组,并且 l 向后移动同时 r 向前移动,同时 l 和 r也需要进行去重操作
  • 难点:三个指针都需要进行去重操作,注意边界条件
  • leetcode解题代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        vector<vector<int>> res;

        for (int i = 0; i < n; i ++){
            // 对 i 指针去重
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            int l = i + 1, r = n - 1;
            while (l < r){
                if (nums[i] + nums[l] + nums[r] > 0) r --;
                else if (nums[i] + nums[l] + nums[r] < 0) l ++;
                else {
                    res.push_back({nums[i], nums[l], nums[r]});
                    // 对 l,r 指针去重
                    while (l < r && nums[l] == nums[l + 1]) l ++;
                    while (l < r && nums[r] == nums[r - 1]) r --;
                    l ++, r --;
                }
            }
        }
        return res;
    }   
};

leetcode.18 四数之和

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector<vector<int>> res;

        for (int k = 0; k < n; k ++){
            if (k > 0 && nums[k] == nums[k - 1]) continue;
            for (int i = k + 1; i < n; i ++){
                if (i > k + 1 && nums[i] == nums[i - 1]) continue;
                int l = i + 1, r = n - 1;
                while (l < r){
                    if ((long)nums[k] + nums[i] + nums[l] + nums[r] > target) r --;
                    else if ((long)nums[k] + nums[i] + nums[l] + nums[r] < target) l ++;
                    else {
                        res.push_back({nums[k], nums[i], nums[l], nums[r]});
                        while (l < r && nums[l] == nums[l + 1]) l ++;
                        while (l < r && nums[r] == nums[r - 1]) r --;
                        l ++, r --;
                    }
                }
            }
        }
        return res;
    }
};

滑动窗口系列

leetcode.209 长度最小的子数组

  • 链接https://leetcode.cn/problems/minimum-size-subarray-sum/
  • 解题思路:双指针(滑动窗口)
    i 指针用来遍历原数组,k 指针用来更新窗口大小
    当窗口内元素总和大于目标值,尝试删除 k 指针的元素,如果删除 k 指针的元素总和依然大于目标值,那么 k 指针向后移动,直到无法删除k 指针的元素,更新窗口内的最少元素个数
  • leetcode解题代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int sum = 0, res = INT_MAX;
        int k = 0;
        for (int i = 0; i < nums.size(); i ++){
            sum += nums[i];
            while (sum - nums[k] >= target){
                sum -= nums[k];
                k ++;
            }
            if (sum >= target) res = min(res, i - k + 1);
        }

        if (res == INT_MAX) return 0;
        return res;
    }
};

leetcode.904 水果成篮

  • 链接https://leetcode.cn/problems/fruit-into-baskets/
  • 解题思路:双指针(滑动窗口) + 哈希表
    i 指针用来遍历原数组,k 指针用来更新窗口大小
    题目要求不能采摘超过两种水果,所以需要用哈希表记录水果出现的次数,type 表示水果的种类,当水果出现的次数为 0,则水果种类需要 -1
    当水果种类大于 2,则窗口大小需要更新
  • leetcode解题代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int res = 0, type = 0;
        unordered_map<int, int> cnt;

        int k = 0;
        for (int i = 0; i < fruits.size(); i ++){
            cnt[fruits[i]] ++;
            if (cnt[fruits[i]] == 1) type ++;// 如果字符第一次出现说明是一种新的水果
            while (type > 2){
                cnt[fruits[k]] --;
                if (cnt[fruits[k]] == 0) type --;// 如果字符删除之后出现的次数为 0,说明少了一种水果
                k ++;
            }
            res = max(res, i - k + 1);
        }
        return res;
    }
};

leetcode.3 无重复字符的最长子串

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int res = 0;
        unordered_map<char, int> hs;

        int k = 0;
        for (int i = 0; i < s.size(); i ++){
            hs[s[i]] ++;
            while (hs[s[i]] > 1){
                hs[s[k]] --;
                k ++;
            }
            res = max(res, i - k + 1);
        }
        return res;
    }
};

leetcode.76 最小覆盖子串

  • 链接https://leetcode.cn/problems/minimum-window-substring/
  • 解题思路:双指针(滑动窗口)
    i 指针用来遍历原数组,k 指针用来更新窗口大小
    题目要求找到覆盖 t 的最小子串,所以需要用哈希表记录子串 t 中每个字符出现的次数,同时记录 s 中每个字符出现的次数
    当 s 中每个字符出现的次数小于等于 t 中字符出现的次数,说明当前字符是有效的,当 s 中字符出现的次数大于 t 中字符出现的次数,说明需要更新窗口大小
    当有效字符数量和子串 t 中字符数量相等,则更新答案
  • leetcode解题代码
class Solution {
public:
    string minWindow(string s, string t) {
        string res;
        int cnt = 0;// 有效字符的数量
        unordered_map<char, int> hs, ht;
        for (auto c: t) ht[c] ++;

        int k = 0;
        for (int i = 0; i < s.size(); i ++){
            hs[s[i]] ++;
            if (hs[s[i]] <= ht[s[i]]) cnt ++;// 是一个有效字符
            while (hs[s[k]] > ht[s[k]]){
                hs[s[k]] --;
                k ++;
            }
            if (cnt == t.size()){
                if (res.empty() || i - k + 1 < res.size())
                    res = s.substr(k, i - k + 1);
            }
        }
        return res;
    }
};

leetcode.567 字符串的排列

  • 链接https://leetcode.cn/problems/permutation-in-string/
  • 解题方法:哈希表 + 滑动窗口
    i 指针用来遍历原数组,k 指针用来更新窗口大小
    题目要求找到s2 是否包含 s1 的排列,与上一题不同之处在于不能有其它的字符
    用哈希表记录子串 s1 中每个字符出现的次数,同时记录 s2 中每个字符出现的次数
    与上一题区别在于,字符出现的次数需要完全相等,所以在指针移动之前需要更新有效字符的数量
  • leetcode解题代码
class Solution {
public:
    unordered_map<char, int> h1, h2;
    bool check(char c){
        if (h1.count(c) && h1[c] == h2[c])
            return true;
        return false;
    }

    bool checkInclusion(string s1, string s2) {
        for (auto c: s1) h1[c] ++;
        
        int cnt = 0;// 有多少个字符满足要求
        int k = 0;// 滑动窗口左端点
        for (int i = 0; i < s2.size(); i ++){
            if (check(s2[i])) cnt --;// 如果当前字符满足要求,添加一个字符就会使其不满足要求
            h2[s2[i]] ++;
            if (check(s2[i])) cnt ++;
            // 当窗口大小大于短串的长度,右端点开始移动
            if (i - k + 1 > s1.size()){
                if (check(s2[k])) cnt --;
                h2[s2[k]] --;
                if (check(s2[k])) cnt ++;
                k ++;
            }

            if (cnt == h1.size()) return true;
        }
        return false;
    }
};

leetcode.438 找到字符串中所有字母异位词

class Solution {
public:
    unordered_map<char, int> hs, hp;    
    bool check(char c){
        if (hs.count(c) && hs[c] == hp[c])
            return true;
        return false;
    }
    vector<int> findAnagrams(string s, string p) {
        vector<int> res;
        for (auto c: p) hp[c] ++;
        
        int cnt = 0;// 有多少个字符满足要求
        int k = 0;// 滑动窗口左端点
        for (int i = 0; i < s.size(); i ++){
            if (check(s[i])) cnt --;// 如果当前字符满足要求,添加一个字符就会使其不满足要求
            hs[s[i]] ++;
            if (check(s[i])) cnt ++;
            // 当窗口大小大于短串的长度,右端点开始移动
            if (i - k + 1 > p.size()){
                if (check(s[k])) cnt --;
                hs[s[k]] --;
                if (check(s[k])) cnt ++;
                k ++;
            }

            if (cnt == hp.size()) res.push_back(k);
        }
        return res;
    }
};

反转字符串系列题型

leetcode.344 翻转字符串

  • 链接https://leetcode.cn/problems/reverse-string/
  • 解题思路:双指针
    i 指针 j 指针分别指向字符串首尾两端,每次交换两个字符,交换完成之后两个指针都向中间移动一位
  • leetcode解题代码
class Solution {
public:
    void reverseString(vector<char>& s) {
        int i = 0, j = s.size() - 1;
        while (i < j){
            swap(s[i], s[j]);
            i ++, j --;
        }
    }
};

leetcode.541 反转字符串 II

  • 链接https://leetcode.cn/problems/reverse-string-ii/
  • 解题方法:双指针
    定义维护的区间 [l, r],每次维护 2k 个数
    如果区间不足 k 个数,r 指针指向区间结尾
    如果区间超过 k 个数,r 指针指向区间第 k 个数
  • leetcode解题代码
class Solution {
public:
    string reverseStr(string s, int k) {
        int n = s.size();
        for (int i = 0; i < n - 1; i += k * 2){
            int l = i, r = min(i + k, n);
            reverse(s.begin() + l, s.begin() + r);
        }
        return s;
    }
};

leetcode.557 反转字符串中的单词 III

  • 链接https://leetcode.cn/problems/reverse-words-in-a-string-iii/
  • 解题思路:双指针
    i 指针用来找每个单词的首位两个字符的下标,k 指针用来找每个单词中间的空格
    当 k 遍历到空格时,说明找到了一个完整的单词,反转 i k 指针之间的单词
  • leetcode解题代码
class Solution {
public:
    string reverseWords(string s) {
        for (int i = 0; i < s.size(); i ++){
            int k = i;
            while (k < s.size() && s[k] != ' ') k ++;
            reverse(s.begin() + i, s.begin() + k); // reverse区间[)左闭右开
            i = k;
        }
        return s;
    }
};

leetcode.151 反转字符串中的单词

  • 链接https://leetcode.cn/problems/reverse-words-in-a-string/
  • 解题思路:双指针
    反转字符串中的单词:反转整个字符串的顺序 + 反转每个单词的顺序
    需要去除前导和尾随空格,还要去除每个单词中间多余的空格
  • leetcode解题代码
class Solution {
public:
    string reverseWords(string s) {
        reverse(s.begin(), s.end());
        while (s.back() == ' ') s.pop_back();
        reverse(s.begin(), s.end());
        while (s.back() == ' ') s.pop_back();

        reverse(s.begin(), s.end());
        for (int i = 0; i < s.size(); i ++){
            int k = i;
            while (k < s.size() && s[k] != ' ') k ++;
            reverse(s.begin() + i, s.begin() + k);
            while (k < s.size() && s[k + 1] == ' ') s.erase(s.begin() + k);
            i = k;
        }
        return s;
    }
};

链表双指针系列题型

leetcode.203 移除链表元素

  • 链接https://leetcode.cn/problems/remove-linked-list-elements/
  • 解题方法:双指针
    cur 指针指向新链表的最后一个节点
    p 指针从 head 开始遍历链表,如果 p 指针元素不是要删除的元素则将 cur 指针指向 p,并更新 cur 指针
    最后 cur 指针是无重复元素链表的最后一个节点,要将其指向空
  • leetcode解题代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        auto dummy = new ListNode(-1);
        dummy->next = head;

        auto cur = dummy;
        for (auto p = head; p; p = p->next){
            if (p->val != val)
                cur = cur->next = p;
        }
        cur->next = nullptr;
        return dummy->next;
    }
};

leetcode.83 删除排序链表中的重复元素

  • 链接https://leetcode.cn/problems/remove-duplicates-from-sorted-list/
  • 解题方法:双指针
    cur 指针指向无重复元素链表的最后一个节点
    p 指针从 head->next 开始遍历链表(第一个节点一定添加到新链表中),如果 p 指针元素和 cur 指针元素不相等则将 cur 指针指向 p,并更新 cur 指针
    最后 cur 指针是无重复元素链表的最后一个节点,记得将其指向空
  • leetcode解题代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head) return head;
        auto cur = head;
        for (auto p = head->next; p; p = p->next)
            if (p->val != cur->val)
                cur = cur->next = p;
        cur->next = nullptr;
        return head;
    }
};

leetcode.82 删除排序链表中的重复元素 II

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        auto dummy = new ListNode(-1);
        dummy->next = head;

        auto cur = dummy;
        while (cur->next){
            auto next = cur->next;
            while (next && cur->next->val == next->val) next = next->next;
            if (next == cur->next->next) cur = cur->next;
            else cur->next = next;
        }
        return dummy->next;
    }
};

leetcode.206 反转链表

  • 链接https://leetcode.cn/problems/reverse-linked-list/
  • 解题思路:双指针
    a 指向第一个节点,b 指向第二个节点
    当第二个节点存在时,每次将第二个节点的指针指向第一个节点,并且将 a 指针和 b 指针都向后移动一位,最后将头节点指向空
  • leetcode解题代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head) return nullptr;
        
        auto a = head, b = head->next;
        while (b){
            auto c = b->next;
            b->next = a;
            a = b, b = c;
        }
        head->next = nullptr;
        return a;
    }
};

leetcode.92 反转链表 II

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        auto dummy = new ListNode(-1);
        dummy->next = head;

        auto p = dummy;
        for (int i = 0; i < left - 1; i ++) p = p->next;

        auto a = p->next, b = a->next;
        for (int i = 0; i < right - left; i ++){
            auto c = b->next;
            b->next = a;
            a = b, b = c;
        }

        p->next->next = b;
        p->next = a;
        return dummy->next;
    }
};

leetcode.160 相交链表

  • 链接https://leetcode.cn/problems/intersection-of-two-linked-lists/
  • 解题思路:双指针
    a链表长度 + 公共长度 + b链表长度 = b链表长度 + 公共长度 + a链表长度
    a 指向第一个链表的头节点,b 指向第二个链表的头节点,当 a 指针和 b 指针未相遇时,a 和 b 都向后移动,当 a 指向空了,将其指向第二个链表的头节点,同理当 b 指向空了,将其指向第一个链表的头节点,一定会在相交节点相遇
  • leetcode解题代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        auto a = headA, b = headB;
        while (a != b){
            if (a) a = a->next;
            else a = headB;
            if (b) b = b->next;
            else b = headA;
        }
        return a;
    }
};

链表快慢指针系列题型

leetcode.876 链表的中间结点

  • 链接https://leetcode.cn/problems/middle-of-the-linked-list/
  • 解题思路:双指针
    快指针每次移动两位,慢指针每次移动一位,当快指针指向链表末尾,慢指针刚好指向链表的中间节点
  • 注意:题目要求如果有两个中间节点,则返回第二个中间节点,如果要求返回第一个中间节点,代码需要改成while (f && f->next && f->next->next)
  • leetcode解题代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        auto f = head, s = head;
        while (f && f->next){
            s = s->next, f = f->next->next;
        }
        return s;
    }
};

leetcode.2095 删除链表的中间节点

  • 链接https://leetcode.cn/problems/delete-the-middle-node-of-a-linked-list/
  • 解题思路:双指针
    快指针每次移动两位,慢指针每次移动一位,当快指针指向链表末尾,慢指针刚好指向链表的中间节点
    由于我们需要找到中间节点的前驱节点,所以将快慢指针都前移一位到虚拟头节点,当快指针指向链表末尾,慢指针刚好指向链表中间节点的前驱节点
  • leetcode解题代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* deleteMiddle(ListNode* head) {
        auto dummy = new ListNode(-1);
        dummy->next = head;

        auto f = dummy, s = dummy;
        while (f && f->next && f->next->next){
            s = s->next, f = f->next->next;
        }
        s->next = s->next->next;
        return dummy->next;
    }
};

leetcode.141 环形链表

class Solution {
public:
    bool hasCycle(ListNode *head) {
        auto fast = head, slow = head;
        
        while(fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
            if (slow == fast) return true;
        }
        return false;
    }
};

leetcode.142 环形链表II

  • 链接https://leetcode.cn/problems/linked-list-cycle-ii/
  • 解题思路:双指针
    判断是否有环和上一题相同,当两个指针相遇时,将其中一个指针指向链表头节点,另一个指针的位置不变,将快指针和慢指针都改为每次移动一位,当两个节点再次相遇时,就是环的入口节点,数学证明略
  • leetcode解题代码
/**
 * 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) {
        if (!head || !head->next) return NULL;

        auto slow = head, fast = head;
        while (fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
            if (slow == fast){
                slow = head;
                while (slow != fast){
                    slow = slow->next;
                    fast = fast->next;
                }
                return slow;
            }
        }
        return NULL;
    }
};