【算法】双指针

双指针算法解析

双指针算法是一种高效的数组或链表处理技巧,主要用于在一次遍历中解决某些特定问题。通过使用两个指针,我们可以在单次遍历中实现复杂的逻辑操作

使用场景

双指针算法通常是为了解决划分数组的问题:
题目给定我们一个数组,让我们按照某个规则把这个数组划分成若干区间(链表也类似)

  • 数组中我们利用数组下标来充当“指针”,因为数组有个特性:可以通过数组下标来索引数组中的元素。因此没有必要真正定义一个传统意义上的指针
  • 链表中我们只能通过定义节点指针来实现

核心思想

核心思想是通过设计不同速度、不同间距、或不同方向的两个(或多个)指针对目标集合进行操作
不同速度:快慢指针

  • 一个指针移动速度较快,另一个指针移动速度较慢,通过比较快慢指针的位置关系来解决问题。
  • 典型应用:判断链表是否有环、寻找链表的中间节点

不同间距:同向指针(也称滑动窗口)

  • 两个指针从同一端开始,一个指针固定或移动较慢,另一个指针移动较快,形成一个窗口,通过调整窗口的大小和位置来解决问题。
  • 典型应用:寻找满足条件的子数组或子序列。

不同方向:相向指针(也称左右指针、对撞指针)

  • 两个指针分别从数组或链表的两端开始向中间移动,通过对指针的位置进行调整来解决问题。
  • 典型应用:寻找有序数组中两个数之和等于给定值的组合。

总结来说,双指针算法的核心在于如何移动指针以及指针间的关系

双指针分类

快慢指针

通过两个指针以不同的速度遍历,可以有效地检测到一些特殊情况,例如链表是否有环、找到链表的中间节点等。快慢指针的优势在于它的实现相对简单且时间复杂度较低

快慢指针的核心思想
快指针(Fast Pointer):每次移动两步或多步。
慢指针(Slow Pointer):每次移动一步或多步。
通过这种方式,快指针会比慢指针更快地到达链表的末尾,或者在有环的情况下,快指针会在环中追上慢指针。

常见应用场景:

  • 检测链表是否有环:如果链表中存在环,快指针最终会在环中追上慢指针。
  • 寻找链表的中间节点:当快指针到达链表末尾时,慢指针刚好到达链表的中间。
  • 寻找环的起点:在检测到链表有环之后,可以找到环的起点。

检测链表是否有环

#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

bool hasCycle(ListNode* head) {
    if (!head || !head->next) return false;
    
    ListNode* slow = head;
    ListNode* fast = head;
    
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        
        if (slow == fast) {
            return true;
        }
    }
    
    return false;
}

int main() {
    ListNode* head = new ListNode(3);
    head->next = new ListNode(2);
    head->next->next = new ListNode(0);
    head->next->next->next = new ListNode(-4);
    head->next->next->next->next = head->next;  // 形成环

    std::cout << (hasCycle(head) ? "Cycle detected" : "No cycle") << std::endl;

    // 释放内存(这里是循环链表,实际应用中需要避免内存泄漏)
    return 0;
}

寻找链表的中间节点

#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode* findMiddle(ListNode* head) {
    if (!head) return nullptr;
    
    ListNode* slow = head;
    ListNode* fast = head;
    
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    return slow;
}

int main() {
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(3);
    head->next->next->next = new ListNode(4);
    head->next->next->next->next = new ListNode(5);

    ListNode* middle = findMiddle(head);
    if (middle) {
        std::cout << "Middle node value: " << middle->val << std::endl;
    } else {
        std::cout << "The list is empty." << std::endl;
    }

    // 释放内存
    ListNode* tmp;
    while (head) {
        tmp = head;
        head = head->next;
        delete tmp;
    }

    return 0;
}

寻找环的起点

#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode* detectCycle(ListNode* head) {
    if (!head || !head->next) return nullptr;
    
    ListNode* slow = head;
    ListNode* fast = head;
    
    // 检测环的存在
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        
        if (slow == fast) {
            ListNode* ptr = head;
            while (ptr != slow) {
                ptr = ptr->next;
                slow = slow->next;
            }
            return ptr;  // 环的起点
        }
    }
    
    return nullptr;
}


int main() {
    ListNode* head = new ListNode(3);
    head->next = new ListNode(2);
    head->next->next = new ListNode(0);
    head->next->next->next = new ListNode(-4);
    head->next->next->next->next = head->next;  // 形成环

    ListNode* cycleStart = detectCycle(head);
    if (cycleStart) {
        std::cout << "Cycle starts at node with value: " << cycleStart->val << std::endl;
    } else {
        std::cout << "No cycle detected." << std::endl;
    }

    // 释放内存(这里是循环链表,实际应用中需要避免内存泄漏)
    return 0;
}

滑动窗口

滑动窗口(Sliding Window),特别适用于处理一维数据结构如数组或字符串的问题。滑动窗口算法的核心思想是通过维护一个窗口(通常是一个子数组或子字符串),根据具体问题的需要动态调整窗口的大小和位置,从而高效地找到满足条件的子数组或子字符串。

核心思想
主要分为两种类型:

  • 固定大小的滑动窗口:窗口的大小固定,通过移动窗口的位置来找到满足条件的子数组或子字符串。
  • 动态调整大小的滑动窗口:窗口的大小根据具体问题的需要动态调整,以找到满足条件的最优解。

常见应用场景:

  • 固定大小窗口的最大值或最小值:如在一个数组中找到所有大小为k的子数组的最大值。
  • 最长不重复子串:在字符串中找到最长的不含重复字符的子串。
  • 和为某个值的子数组:找到数组中和为给定值的最长或最短子数组。

滑动窗口适用于解决涉及子数组或子字符串的问题。通过维护一个窗口并动态调整其大小和位置,可以在一次线性扫描中高效地找到满足条件的子数组或子字符串。固定大小的滑动窗口适用于需要在固定大小窗口内进行操作的问题,而动态调整大小的滑动窗口适用于需要灵活调整窗口大小以满足特定条件的问题。

固定大小的滑动窗口最大值

#include <vector>
#include <deque>
#include <iostream>

std::vector<int> maxSlidingWindow(std::vector<int>& nums, int k) {
    std::deque<int> deq;
    std::vector<int> result;

    for (int i = 0; i < nums.size(); ++i) {
        // 移除不在滑动窗口范围内的元素
        if (!deq.empty() && deq.front() == i - k) {
            deq.pop_front();
        }

        // 移除队列中所有小于当前元素的元素
        while (!deq.empty() && nums[deq.back()] < nums[i]) {
            deq.pop_back();
        }

        // 将当前元素添加到队列
        deq.push_back(i);

        // 当窗口大小达到k时,记录最大值
        if (i >= k - 1) {
            result.push_back(nums[deq.front()]);
        }
    }

    return result;
}

int main() {
    std::vector<int> nums = {1, 3, -1, -3, 5, 3, 6, 7};
    int k = 3;

    std::vector<int> result = maxSlidingWindow(nums, k);

    std::cout << "Sliding window maximums: ";
    for (int val : result) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    return 0;
}

最长不重复子串

#include <iostream>
#include <unordered_set>
#include <string>

int lengthOfLongestSubstring(const std::string& s) {
    std::unordered_set<char> charSet;
    int left = 0, right = 0, maxLength = 0;

    while (right < s.size()) {
        if (charSet.find(s[right]) == charSet.end()) {
            charSet.insert(s[right]);
            maxLength = std::max(maxLength, right - left + 1);
            ++right;
        } else {
            charSet.erase(s[left]);
            ++left;
        }
    }

    return maxLength;
}

int main() {
    std::string s = "abcabcbb";

    int result = lengthOfLongestSubstring(s);

    std::cout << "Length of the longest substring without repeating characters: " << result << std::endl;

    return 0;
}

和为某个值的最长子数组

#include <vector>
#include <unordered_map>
#include <iostream>

int maxSubArrayLen(std::vector<int>& nums, int k) {
    std::unordered_map<int, int> sumIndexMap;
    sumIndexMap[0] = -1;  // 辅助处理从索引0开始的情况
    int sum = 0, maxLength = 0;
    
    for (int i = 0; i < nums.size(); ++i) {
        sum += nums[i];
        if (sumIndexMap.find(sum - k) != sumIndexMap.end()) {
            maxLength = std::max(maxLength, i - sumIndexMap[sum - k]);
        }
        if (sumIndexMap.find(sum) == sumIndexMap.end()) {
            sumIndexMap[sum] = i;
        }
    }

    return maxLength;
}

int main() {
    std::vector<int> nums = {1, -1, 5, -2, 3};
    int k = 3;

    int result = maxSubArrayLen(nums, k);

    std::cout << "Length of the longest subarray with sum " << k << ": " << result << std::endl;

    return 0;
}

左右指针

有序数组的两数之和

#include <vector>

std::vector<int> twoSum(const std::vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;
    while (left < right) {
        int sum = nums[left] + nums[right];
        if (sum == target) {
            return {left, right};
        } else if (sum < target) {
            ++left;
        } else {
            --right;
        }
    }
    return {};
}

验证回文字符串

#include <string>

bool isPalindrome(const std::string& s) {
    int left = 0, right = s.size() - 1;
    while (left < right) {
        while (left < right && !isalnum(s[left])) ++left;
        while (left < right && !isalnum(s[right])) --right;
        if (tolower(s[left]) != tolower(s[right])) {
            return false;
        }
        ++left;
        --right;
    }
    return true;
}

三数之和问题

#include <vector>
#include <algorithm>

std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {
    std::sort(nums.begin(), nums.end());
    std::vector<std::vector<int>> result;
    int n = nums.size();
    
    for (int i = 0; i < n - 2; ++i) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        
        int left = i + 1, right = n - 1;
        while (left < right) {
            int total = nums[i] + nums[left] + nums[right];
            if (total < 0) {
                ++left;
            } else if (total > 0) {
                --right;
            } else {
                result.push_back({nums[i], nums[left], nums[right]});
                while (left < right && nums[left] == nums[left + 1]) ++left;
                while (left < right && nums[right] == nums[right - 1]) --right;
                ++left;
                --right;
            }
        }
    }
    return result;
}

左右指针是一种简单而高效的双指针技术,特别适用于有序数组或字符串等线性数据结构。通过分别从数据结构的两端开始,向中间移动,逐步缩小搜索范围,可以有效地解决一系列问题。关键在于通过比较指针指向的元素来决定如何移动指针,从而在一次遍历中达到目标。

定义三个指针的情况

有些问题需要定义三个指针来解决。一个典型的例子是“三数之和”问题。在这个问题中,我们需要在一个数组中找到所有的三元组,使得它们的和为零。可以使用三个指针来解决这个问题:第一个指针遍历数组,另外两个指针一个从当前指针的下一个位置开始,另一个从数组的末尾开始,然后这两个指针向中间移动。

三数之和

#include <vector>
#include <algorithm>

std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {
    std::vector<std::vector<int>> result;
    std::sort(nums.begin(), nums.end());  // 将数组排序
    
    for (int i = 0; i < nums.size(); ++i) {
        // 避免重复结果
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        
        int target = -nums[i];
        int left = i + 1;
        int right = nums.size() - 1;
        
        while (left < right) {
            int sum = nums[left] + nums[right];
            if (sum == target) {
                result.push_back({nums[i], nums[left], nums[right]});
                while (left < right && nums[left] == nums[left + 1]) ++left;  // 跳过重复的元素
                while (left < right && nums[right] == nums[right - 1]) --right;  // 跳过重复的元素
                ++left;
                --right;
            } else if (sum < target) {
                ++left;
            } else {
                --right;
            }
        }
    }
    
    return result;
}

解释
排序:首先对数组进行排序,这是为了方便后续使用双指针进行查找。
遍历:使用第一个指针遍历整个数组。在每次迭代中,固定当前元素,目标是找到另外两个元素使它们的和为当前元素的相反数。
双指针查找:对于每个固定的元素,使用左右指针从剩下的数组中查找另外两个元素。左指针从固定元素的下一个位置开始,右指针从数组的末尾开始。通过调整左指针和右指针的位置来寻找满足条件的三元组。
去重:在查找过程中,跳过重复的元素以避免重复的结果。
这种方法的时间复杂度为 𝑂(𝑛2),其中 𝑛是数组的长度。通过排序和双指针的结合,这种方法在性能和简洁性上都表现良好。

四数之和

可以在三数之和的基础上再添加一个外层循环,用四个指针解决四数之和问题。

#include <vector>
#include <algorithm>

std::vector<std::vector<int>> fourSum(std::vector<int>& nums, int target) {
    std::vector<std::vector<int>> result;
    std::sort(nums.begin(), nums.end());  // 将数组排序
    
    for (int i = 0; i < nums.size(); ++i) {
        // 避免重复结果
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        
        for (int j = i + 1; j < nums.size(); ++j) {
            // 避免重复结果
            if (j > i + 1 && nums[j] == nums[j - 1]) continue;
            
            int new_target = target - nums[i] - nums[j];
            int left = j + 1;
            int right = nums.size() - 1;
            
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum == new_target) {
                    result.push_back({nums[i], nums[j], nums[left], nums[right]});
                    while (left < right && nums[left] == nums[left + 1]) ++left;  // 跳过重复的元素
                    while (left < right && nums[right] == nums[right - 1]) --right;  // 跳过重复的元素
                    ++left;
                    --right;
                } else if (sum < new_target) {
                    ++left;
                } else {
                    --right;
                }
            }
        }
    }
    
    return result;
}

链表的归并排序

在链表排序中,也可能需要使用三个指针来拆分和合并链表:

#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

// 归并两个有序链表
ListNode* merge(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    
    while (l1 && l2) {
        if (l1->val < l2->val) {
            tail->next = l1;
            l1 = l1->next;
        } else {
            tail->next = l2;
            l2 = l2->next;
        }
        tail = tail->next;
    }
    
    if (l1) tail->next = l1;
    if (l2) tail->next = l2;
    
    return dummy.next;
}

// 使用快慢指针将链表分为两部分
ListNode* split(ListNode* head) {
    ListNode* slow = head;
    ListNode* fast = head->next;
    
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    ListNode* mid = slow->next;
    slow->next = nullptr;
    return mid;
}

// 归并排序主函数
ListNode* mergeSort(ListNode* head) {
    if (!head || !head->next) return head;
    
    ListNode* mid = split(head);
    ListNode* left = mergeSort(head);
    ListNode* right = mergeSort(mid);
    
    return merge(left, right);
}

// 打印链表
void printList(ListNode* head) {
    while (head) {
        std::cout << head->val << " ";
        head = head->next;
    }
    std::cout << std::endl;
}

// 测试函数
int main() {
    ListNode* head = new ListNode(4);
    head->next = new ListNode(2);
    head->next->next = new ListNode(1);
    head->next->next->next = new ListNode(3);

    std::cout << "Original list: ";
    printList(head);
    
    head = mergeSort(head);

    std::cout << "Sorted list: ";
    printList(head);

    // 释放内存
    ListNode* tmp;
    while (head) {
        tmp = head;
        head = head->next;
        delete tmp;
    }

    return 0;
}

  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

徐徐同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值