当前位置: 首页 > article >正文

C++.双指针算法(1.1目录修正)

C++.双指针算法

  • 1. 双指针算法概述
    • 1.1 双指针算法的定义
    • 1.2 双指针算法的应用场景
      • 1.2.1 数组中的两数之和问题
      • 1.2.2 链表中的环检测问题
      • 1.2.3 滑动窗口问题
      • 1.2.4 有序数组的合并问题
  • 2. 双指针算法的实现基础
    • 2.1 指针的基本概念
    • 2.2 指针的运算操作
  • 3. 双指针算法的常见类型及示例
    • 3.1 快慢指针
      • 3.1.1 快慢指针的原理
      • 3.1.2 快慢指针的示例:链表中环的检测
    • 3.2 左右指针
      • 3.2.1 左右指针的原理
      • 3.2.2 左右指针的示例:二分查找
    • 3.3 对撞指针
      • 3.3.1 对撞指针的原理
      • 3.3.2 对撞指针的示例:两数之和
  • 4. 双指针算法的优化技巧
    • 4.1 指针移动的策略
    • 4.2 指针初始位置的选择
  • 5. 双指针算法的复杂度分析
    • 5.1 时间复杂度分析
      • 5.1.1 快慢指针
      • 5.1.2 左右指针
      • 5.1.3 对撞指针
      • 5.1.4 总结
    • 5.2 空间复杂度分析
      • 5.2.1 快慢指针
      • 5.2.2 左右指针
      • 5.2.3 对撞指针
      • 5.2.4 总结
  • 6. 双指针算法的实践案例
    • 6.1 案例一:滑动窗口问题
      • 问题描述
      • 算法思路
      • 示例代码
      • 示例分析
      • 时间复杂度
      • 空间复杂度
    • 6.2 案例二:字符串匹配问题
      • 问题描述
      • 算法思路
      • 示例代码
      • 示例分析
  • 7. 双指针算法的注意事项
    • 7.1 指针越界问题
    • 7.2 指针空指针问题

1. 双指针算法概述

1.1 双指针算法的定义

双指针算法是一种高效的算法思想,通常用于处理数组或链表等线性数据结构。其核心思想是使用两个指针(通常称为慢指针和快指针)来遍历数据结构,通过控制指针的移动速度和方向,实现对数据的高效处理。双指针算法的关键在于如何定义指针的移动规则,以及如何根据问题的需求来调整指针之间的关系。

1.2 双指针算法的应用场景

双指针算法在多种编程问题中都有广泛的应用,以下是一些常见的应用场景及示例代码:

1.2.1 数组中的两数之和问题

问题描述:给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。

  • 算法思路:使用双指针法,首先对数组进行排序,然后使用两个指针,一个指向数组的起始位置,另一个指向数组的末尾。通过比较两个指针所指向的元素之和与目标值的大小关系,来决定移动哪个指针。
  • 示例代码
#include <vector>
#include <algorithm>
using namespace std;vector<int> twoSum(vector<int>& nums, int target) {vector<pair<int, int>> nums_with_index;for (int i = 0; i < nums.size(); ++i) {nums_with_index.push_back({nums[i], i});}sort(nums_with_index.begin(), nums_with_index.end());int left = 0, right = nums.size() - 1;while (left < right) {int sum = nums_with_index[left].first + nums_with_index[right].first;if (sum == target) {return {nums_with_index[left].second, nums_with_index[right].second};} else if (sum < target) {++left;} else {--right;}}return {};
}

1.2.2 链表中的环检测问题

问题描述:给定一个链表,判断链表中是否存在环。

  • 算法思路:使用快慢指针法,快指针每次移动两步,慢指针每次移动一步。如果链表中存在环,快指针和慢指针最终会相遇;如果不存在环,快指针会先到达链表的末尾。
  • 示例代码
#include <iostream>
using namespace std;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->next;while (slow != fast) {if (!fast || !fast->next) return false;slow = slow->next;fast = fast->next->next;}return true;
}

1.2.3 滑动窗口问题

问题描述:给定一个数组和一个整数k,找出数组中所有长度为k的子数组的最大值。

  • 算法思路:使用双指针表示滑动窗口的左右边界,通过移动右指针扩展窗口,移动左指针收缩窗口,同时维护一个数据结构(如队列)来存储窗口内的最大值。
  • 示例代码
#include <vector>
#include <deque>
using namespace std;vector<int> maxSlidingWindow(vector<int>& nums, int k) {vector<int> result;deque<int> window;for (int i = 0; i < nums.size(); ++i) {// 移除窗口外的元素while (!window.empty() && window.front() <= i - k) {window.pop_front();}// 移除窗口内小于当前元素的值while (!window.empty() && nums[window.back()] < nums[i]) {window.pop_back();}window.push_back(i);// 当窗口大小达到k时,记录最大值if (i >= k - 1) {result.push_back(nums[window.front()]);}}return result;
}

1.2.4 有序数组的合并问题

问题描述:给定两个有序数组nums1nums2,将它们合并为一个有序数组。

  • 算法思路:使用双指针分别指向两个数组的起始位置,比较两个指针所指向的元素大小,将较小的元素添加到结果数组中,并移动对应的指针。重复此过程,直到其中一个数组遍历完成,然后将另一个数组的剩余部分添加到结果数组中。
  • 示例代码
#include <vector>
using namespace std;vector<int> merge(vector<int>& nums1, vector<int>& nums2) {vector<int> result;int i = 0, j = 0;while (i < nums1.size() && j < nums2.size()) {if (nums1[i] < nums2[j]) {result.push_back(nums1[i++]);} else {result.push_back(nums2[j++]);}}while (i < nums1.size()) {result.push_back(nums1[i++]);}while (j < nums2.size()) {result.push_back(nums2[j++]);}return result;
}

双指针算法通过巧妙地使用两个指针来解决问题,避免了暴力解法中不必要的重复计算,大大提高了算法的效率。

2. 双指针算法的实现基础

2.1 指针的基本概念

在C++中,指针是一种变量,它存储了另一个变量的内存地址。指针的基本概念是理解双指针算法的关键。以下是关于指针的一些重要概念:

  • 指针的声明:指针的声明格式为类型 *指针名;,例如int *p;表示声明了一个指向整数的指针p
  • 指针的初始化:指针可以通过取地址运算符&来初始化,例如int a = 10; int *p = &a;,此时p存储了变量a的地址。
  • 指针的解引用:通过解引用运算符*可以访问指针所指向的变量的值,例如*p将返回a的值。
  • 指针与数组的关系:数组名本身就是一个指向数组首元素的指针。例如,对于数组int arr[5];arr&arr[0]是等价的,都表示数组首元素的地址。

指针的基本概念是双指针算法实现的基础,通过指针可以高效地操作内存中的数据。

2.2 指针的运算操作

指针的运算操作主要包括指针的加减运算、指针的比较运算以及指针的自增自减运算。这些运算操作在双指针算法中非常重要,以下是详细介绍:

  • 指针的加减运算

    • 指针的加法运算:p + n表示将指针p向后移动n个元素的位置。例如,对于int arr[5]; int *p = arr;p + 2将指向arr[2]
    • 指针的减法运算:p - n表示将指针p向前移动n个元素的位置。例如,p - 2将指向arr[-2](注意:这可能会导致越界错误,需要谨慎使用)。
    • 指针的差值运算:p2 - p1表示两个指针之间的元素个数。例如,对于int *p1 = arr; int *p2 = arr + 3;p2 - p1的结果是3
  • 指针的比较运算

    • 指针可以使用比较运算符(==!=><>=<=)进行比较。比较的结果取决于指针所指向的内存地址的大小。例如,p1 < p2表示p1所指向的地址小于p2所指向的地址。
  • 指针的自增自减运算

    • 指针的自增运算:p++++p表示将指针p向后移动一个元素的位置。例如,p++将使p指向下一个元素。
    • 指针的自减运算:p----p表示将指针p向前移动一个元素的位置。例如,p--将使p指向前一个元素。

这些运算操作在双指针算法中被广泛应用,通过合理地移动指针,可以高效地处理数组或链表中的数据。例如,在链表中的环检测问题中,通过快慢指针的移动,可以判断链表是否存在环。

3. 双指针算法的常见类型及示例

3.1 快慢指针

3.1.1 快慢指针的原理

快慢指针是一种特殊的双指针技术,其中一个指针(快指针)的移动速度是另一个指针(慢指针)的两倍。这种技术主要用于链表结构,用于检测环的存在或寻找链表的中点等。快指针每次移动两个节点,而慢指针每次移动一个节点。如果链表中存在环,快指针和慢指针最终会在环内相遇;如果不存在环,快指针会先到达链表的末尾。

3.1.2 快慢指针的示例:链表中环的检测

问题描述:给定一个链表,判断链表中是否存在环。

算法思路:使用快慢指针法,快指针每次移动两步,慢指针每次移动一步。如果链表中存在环,快指针和慢指针最终会相遇;如果不存在环,快指针会先到达链表的末尾。

示例代码

#include <iostream>
using namespace std;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->next;while (slow != fast) {if (!fast || !fast->next) return false;slow = slow->next;fast = fast->next->next;}return true;
}

3.2 左右指针

3.2.1 左右指针的原理

左右指针通常用于数组或字符串等线性数据结构。左右指针分别从数组或字符串的两端开始,向中间移动。通过比较左右指针所指向的元素,可以实现各种操作,如查找目标值、计算最大值或最小值等。左右指针的移动规则根据具体问题的需求而定,通常在满足某些条件时移动左指针或右指针。

3.2.2 左右指针的示例:二分查找

问题描述:给定一个有序数组nums和一个目标值target,请你在该数组中查找目标值的位置。如果目标值不存在于数组中,返回-1

算法思路:使用左右指针表示数组的搜索范围,初始时左指针指向数组的起始位置,右指针指向数组的末尾。通过比较中间位置的元素与目标值的大小关系,决定移动左指针或右指针,逐步缩小搜索范围,直到找到目标值或搜索范围为空。

示例代码

#include <vector>
using namespace std;int binarySearch(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {return mid;} else if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}return -1;
}

3.3 对撞指针

3.3.1 对撞指针的原理

对撞指针是一种特殊的左右指针技术,通常用于处理数组或字符串中的对称问题或需要从两端向中间逐步逼近的问题。对撞指针的初始位置分别位于数组或字符串的两端,通过逐步移动指针,逐步缩小范围,直到两个指针相遇。对撞指针的移动规则根据具体问题的需求而定,通常在满足某些条件时移动左指针或右指针。

3.3.2 对撞指针的示例:两数之和

问题描述:给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。

算法思路:使用对撞指针法,首先对数组进行排序,然后使用两个指针,一个指向数组的起始位置,另一个指向数组的末尾。通过比较两个指针所指向的元素之和与目标值的大小关系,来决定移动哪个指针。

示例代码

#include <vector>
#include <algorithm>
using namespace std;vector<int> twoSum(vector<int>& nums, int target) {vector<pair<int, int>> nums_with_index;for (int i = 0; i < nums.size(); ++i) {nums_with_index.push_back({nums[i], i});}sort(nums_with_index.begin(), nums_with_index.end());int left = 0, right = nums.size() - 1;while (left < right) {int sum = nums_with_index[left].first + nums_with_index[right].first;if (sum == target) {return {nums_with_index[left].second, nums_with_index[right].second};} else if (sum < target) {++left;} else {--right;}}return {};
}

4. 双指针算法的优化技巧

4.1 指针移动的策略

指针移动的策略是双指针算法优化的关键。合理的移动策略可以减少不必要的计算,提高算法的效率。以下是一些常见的优化策略:

  • 根据问题需求调整移动速度:在快慢指针中,快指针的移动速度通常是慢指针的两倍,但根据具体问题,可以调整移动速度。例如,在链表中寻找环的入口时,快指针和慢指针相遇后,将其中一个指针移回链表头部,然后两个指针以相同的速度移动,它们再次相遇的位置即为环的入口。这种调整可以避免重复遍历,提高算法效率。
  • 动态调整指针位置:在处理数组或字符串问题时,指针的移动不仅取决于当前指针所指向的值,还可能与数组或字符串的其他部分有关。例如,在滑动窗口问题中,右指针用于扩展窗口,左指针用于收缩窗口。当窗口内的数据不满足条件时,左指针需要根据窗口内的数据动态调整位置,以找到满足条件的最小窗口。
  • 减少指针回溯:在某些情况下,指针回溯会增加算法的时间复杂度。通过合理设计指针的移动规则,可以尽量减少指针回溯。例如,在二分查找中,通过比较中间值与目标值的大小关系,直接决定左指针或右指针的移动,避免了不必要的回溯操作,从而将时间复杂度降低到O(log n)。

4.2 指针初始位置的选择

指针的初始位置对双指针算法的性能也有重要影响。选择合适的初始位置可以减少不必要的遍历,提高算法的效率。以下是一些常见的选择策略:

  • 根据数据结构的特点选择初始位置:对于数组或字符串,左右指针通常分别从两端开始。例如,在两数之和问题中,左右指针分别从数组的起始位置和末尾位置开始,这样可以充分利用数组的有序性,快速找到满足条件的两个数。对于链表,快慢指针通常从链表头部开始,这样可以方便地检测链表中是否存在环。
  • 根据问题的目标选择初始位置:在某些问题中,指针的初始位置需要根据问题的具体目标来选择。例如,在滑动窗口问题中,右指针从数组的起始位置开始,用于扩展窗口,而左指针在窗口满足条件时才开始移动,用于收缩窗口。这种初始位置的选择可以确保窗口的大小和位置符合问题的要求,从而提高算法的效率。
  • 考虑边界条件选择初始位置:在选择指针初始位置时,还需要考虑边界条件。例如,在处理数组或字符串时,指针不能超出数组或字符串的范围。在链表中,指针不能指向空节点。因此,在选择初始位置时,需要根据数据结构的边界条件进行调整,以避免潜在的错误。

5. 双指针算法的复杂度分析

5.1 时间复杂度分析

双指针算法的时间复杂度主要取决于指针的移动次数和每次移动的操作复杂度。以下是对几种常见双指针算法的时间复杂度分析:

5.1.1 快慢指针

  • 链表中环的检测
    • 时间复杂度:O(n)。快指针每次移动两步,慢指针每次移动一步。在最坏的情况下,快指针会遍历整个链表一次,慢指针会遍历链表的一半。因此,总的时间复杂度为O(n)。
    • 示例:假设链表长度为n,快指针和慢指针相遇时,快指针最多移动2n步,慢指针最多移动n步,总的时间复杂度为O(n)。

5.1.2 左右指针

  • 二分查找

    • 时间复杂度:O(log n)。左右指针每次将搜索范围缩小一半,直到找到目标值或搜索范围为空。因此,时间复杂度为O(log n)。
    • 示例:假设数组长度为n,每次将搜索范围缩小一半,经过log2(n)次操作后,搜索范围为空,因此时间复杂度为O(log n)。
  • 两数之和

    • 时间复杂度:O(n)。左右指针从数组的两端开始,每次移动一个指针,直到找到满足条件的两个数或指针相遇。因此,时间复杂度为O(n)。
    • 示例:假设数组长度为n,左右指针最多移动n次,每次移动的操作复杂度为O(1),因此总的时间复杂度为O(n)。

5.1.3 对撞指针

  • 两数之和

    • 时间复杂度:O(n)。对撞指针从数组的两端开始,每次移动一个指针,直到找到满足条件的两个数或指针相遇。因此,时间复杂度为O(n)。
    • 示例:假设数组长度为n,对撞指针最多移动n次,每次移动的操作复杂度为O(1),因此总的时间复杂度为O(n)。
  • 滑动窗口问题

    • 时间复杂度:O(n)。右指针用于扩展窗口,左指针用于收缩窗口。每个元素最多被访问两次(一次被右指针访问,一次被左指针访问),因此总的时间复杂度为O(n)。
    • 示例:假设数组长度为n,右指针最多移动n次,左指针最多移动n次,每次移动的操作复杂度为O(1),因此总的时间复杂度为O(n)。

5.1.4 总结

双指针算法的时间复杂度通常为O(n)或O(log n),具体取决于指针的移动方式和问题的性质。通过合理设计指针的移动规则,可以显著提高算法的效率,避免不必要的重复计算。

5.2 空间复杂度分析

双指针算法的空间复杂度主要取决于额外使用的空间。以下是对几种常见双指针算法的空间复杂度分析:

5.2.1 快慢指针

  • 链表中环的检测
    • 空间复杂度:O(1)。快指针和慢指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
    • 示例:在链表中环的检测问题中,只使用了两个指针变量,没有使用额外的数据结构,因此空间复杂度为O(1)。

5.2.2 左右指针

  • 二分查找

    • 空间复杂度:O(1)。左右指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
    • 示例:在二分查找问题中,只使用了两个指针变量和一个中间变量,没有使用额外的数据结构,因此空间复杂度为O(1)。
  • 两数之和

    • 空间复杂度:O(1)。左右指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
    • 示例:在两数之和问题中,只使用了两个指针变量,没有使用额外的数据结构,因此空间复杂度为O(1)。

5.2.3 对撞指针

  • 两数之和

    • 空间复杂度:O(1)。对撞指针只使用了常数级别的额外空间,因此空间复杂度为O(1)。
    • 示例:在两数之和问题中,只使用了两个指针变量,没有使用额外的数据结构,因此空间复杂度为O(1)。
  • 滑动窗口问题

    • 空间复杂度:O(k)。滑动窗口问题通常需要一个额外的数据结构(如队列)来存储窗口内的元素,其大小为窗口的长度k。因此,空间复杂度为O(k)。
    • 示例:在滑动窗口问题中,使用了一个队列来存储窗口内的元素,队列的大小为窗口的长度k,因此空间复杂度为O(k)。

5.2.4 总结

双指针算法的空间复杂度通常为O(1)或O(k),具体取决于是否使用了额外的数据结构。大多数双指针算法只使用了常数级别的额外空间,因此空间复杂度较低。在某些情况下,如滑动窗口问题,可能需要使用额外的数据结构来存储窗口内的元素,此时空间复杂度会稍高。

通过合理设计双指针算法,可以在保证时间效率的同时,尽量减少空间的使用,从而实现高效的算法设计。

6. 双指针算法的实践案例

6.1 案例一:滑动窗口问题

滑动窗口问题是双指针算法的经典应用场景之一,主要用于处理数组或字符串中的连续子序列问题。以下是一个具体的实践案例,详细讲解如何使用双指针算法解决滑动窗口问题。

问题描述

给定一个整数数组nums和一个整数k,找出数组中所有长度为k的子数组的最大值。

算法思路

使用双指针表示滑动窗口的左右边界,通过移动右指针扩展窗口,移动左指针收缩窗口,同时维护一个数据结构(如队列)来存储窗口内的最大值。具体步骤如下:

  1. 初始化

    • 定义两个指针leftright,分别表示滑动窗口的左边界和右边界,初始时均指向数组的起始位置。
    • 使用一个双端队列window来存储窗口内的元素索引,队列的前端始终存储当前窗口的最大值的索引。
  2. 扩展窗口

    • 移动右指针right,将新元素的索引加入队列。在加入之前,需要移除队列中所有小于当前元素的索引,以确保队列的前端始终存储最大值的索引。
  3. 收缩窗口

    • 如果队列的前端索引小于左边界left,说明该索引已经不在当前窗口范围内,需要从队列中移除。
    • 当窗口大小达到k时,记录当前窗口的最大值(队列前端的值)。
  4. 移动窗口

    • 每次移动右指针后,如果窗口大小超过k,则移动左指针,收缩窗口。

示例代码

#include <vector>
#include <deque>
using namespace std;vector<int> maxSlidingWindow(vector<int>& nums, int k) {vector<int> result;deque<int> window;for (int right = 0; right < nums.size(); ++right) {// 移除窗口外的元素while (!window.empty() && window.front() <= right - k) {window.pop_front();}// 移除窗口内小于当前元素的值while (!window.empty() && nums[window.back()] < nums[right]) {window.pop_back();}window.push_back(right);// 当窗口大小达到k时,记录最大值if (right >= k - 1) {result.push_back(nums[window.front()]);}}return result;
}

示例分析

假设输入数组nums = {1, 3, -1, -3, 5, 3, 6, 7},窗口大小k = 3,则算法的执行过程如下:

  1. 初始状态

    • left = 0, right = 0
    • window为空
  2. 扩展窗口

    • right = 0nums[0] = 1,加入队列,window = {0}
    • right = 1nums[1] = 3,移除队列中小于3的索引,window = {1}
    • right = 2nums[2] = -1,移除队列中小于-1的索引,window = {1, 2}
  3. 记录最大值

    • right = 2时,窗口大小达到3,记录最大值nums[1] = 3,结果为{3}
  4. 继续扩展窗口

    • right = 3nums[3] = -3,移除队列中小于-3的索引,window = {1, 2, 3}
    • right = 4nums[4] = 5,移除队列中小于5的索引,window = {4}
  5. 收缩窗口

    • left = 1,移除队列中索引小于left的值,window = {4}
  6. 记录最大值

    • right = 4时,窗口大小达到3,记录最大值nums[4] = 5,结果为{3, 5}
  7. 重复上述过程,最终结果为{3, 3, 5, 5, 6, 7}

时间复杂度

  • 时间复杂度:O(n)。每个元素最多被加入和移除队列一次,因此总的时间复杂度为O(n)。

空间复杂度

  • 空间复杂度:O(k)。队列的大小最多为窗口的大小k

6.2 案例二:字符串匹配问题

字符串匹配问题是双指针算法的另一个经典应用场景,主要用于在主字符串中查找模式字符串的出现位置。以下是一个具体的实践案例,详细讲解如何使用双指针算法解决字符串匹配问题。

问题描述

给定两个字符串text(主字符串)和pattern(模式字符串),找出patterntext中的所有出现位置。

算法思路

使用双指针分别指向主字符串和模式字符串,通过逐字符比较来判断模式字符串是否在主字符串中出现。具体步骤如下:

  1. 初始化

    • 定义两个指针ij,分别指向主字符串text和模式字符串pattern的起始位置。
    • 定义一个结果数组result,用于存储模式字符串在主字符串中的所有出现位置。
  2. 逐字符比较

    • 如果text[i] == pattern[j],则两个指针同时向右移动。
    • 如果text[i] != pattern[j],则将主字符串的指针i回退到上次匹配失败的位置的下一个位置,模式字符串的指针j回退到模式字符串的起始位置。
  3. 记录匹配位置

    • 当模式字符串的指针j到达模式字符串的末尾时,说明找到了一个匹配的子串,将主字符串的指针i - j的位置加入结果数组。
    • 重置指针ij,继续查找下一个匹配的子串。

示例代码

#include <vector>
#include <string>
using namespace std;vector<int> strStr(string text, string pattern) {vector<int> result;int i = 0, j = 0;while (i <= text.size() - pattern.size()) {if (text[i + j] == pattern[j]) {++j;if (j == pattern.size()) {result.push_back(i);i = i + j;j = 0;}} else {i = i + j;j = 0;}}return result;
}

示例分析

假设主字符串text = "ababcabcab",模式字符串pattern = "abc",则算法的执行过程如下:

  1. 初始状态

    • i = 0, j = 0
    • result为空
  2. 逐字符比较

    • i = 0, j = 0text[0] == 'a'pattern[0] == 'a',匹配成功,j = 1
    • i = 0, j = 1text[1] == 'b'pattern[1] == 'b',匹配成功,j = 2
    • i = 0, j = 2text[2] == 'a'pattern[2] == 'c',匹配失败,i = 2j = 0
  3. 继续比较

    • i = 2, j = 0text[2] == 'a'pattern[0] == 'a',匹配成功,j = 1
    • i = 2, j = 1text[3] == 'b'pattern[1] == 'b',匹配成功,j = 2
    • i = 2, j = 2text[4] == 'c'pattern[2] == 'c',匹配成功,j = 3
  4. 记录匹配位置

    • j = 3时,模式字符串匹配成功,记录位置i = 2,结果为{2}
    • 重置指针i = 5j = 0
  5. **

7. 双指针算法的注意事项

7.1 指针越界问题

指针越界是双指针算法中常见的问题之一,它可能导致程序崩溃或产生不可预测的结果。指针越界通常发生在指针移动超出数据结构的边界时。以下是几种常见的指针越界情况及其解决方法:

  • 数组越界

    • 问题描述:在处理数组时,指针可能移动到数组的边界之外。例如,在使用左右指针时,如果指针移动规则设计不当,可能会导致指针指向数组的负索引或超出数组的最大索引。
    • 解决方法:在移动指针之前,必须检查指针是否在数组的有效范围内。例如,在二分查找中,确保左指针不超过右指针,且指针的值始终在数组的索引范围内。可以通过添加边界条件检查来避免越界问题。例如:
      if (left < 0 || right >= nums.size()) {// 处理越界情况
      }
      
  • 链表越界

    • 问题描述:在处理链表时,指针可能移动到链表的末尾之外。例如,在使用快慢指针检测环时,如果链表中不存在环,快指针可能会先到达链表的末尾,此时继续移动快指针会导致空指针异常。
    • 解决方法:在移动指针之前,必须检查指针是否为空。例如,在链表环检测中,每次移动快指针之前,需要检查快指针是否为空:
      if (!fast || !fast->next) {return false;
      }
      

7.2 指针空指针问题

空指针问题是指指针未初始化或指向了一个无效的内存地址。在双指针算法中,空指针问题可能导致程序崩溃或产生错误的结果。以下是几种常见的空指针情况及其解决方法:

  • 未初始化的指针

    • 问题描述:在使用指针之前,如果没有正确初始化指针,指针可能会指向一个随机的内存地址,导致不可预测的行为。
    • 解决方法:在声明指针时,必须确保指针被正确初始化。例如,可以将指针初始化为nullptr,并在使用之前检查指针是否为空:
      int *p = nullptr;
      if (p) {// 使用指针
      }
      
  • 链表中的空指针

    • 问题描述:在处理链表时,如果链表为空或指针移动到链表的末尾,可能会导致空指针异常。例如,在链表中删除节点时,如果链表为空,直接访问链表的头节点会导致空指针异常。
    • 解决方法:在操作链表之前,必须检查链表是否为空。例如,在链表中删除节点时,需要先检查链表是否为空:
      if (!head) {return; // 链表为空
      }
      
  • 数组中的空指针

    • 问题描述:在处理数组时,如果数组为空或指针移动到数组的边界之外,可能会导致空指针异常。例如,在数组中查找元素时,如果数组为空,直接访问数组的元素会导致空指针异常。
    • 解决方法:在操作数组之前,必须检查数组是否为空。例如,在数组中查找元素时,需要先检查数组是否为空:
      if (nums.empty()) {return; // 数组为空
      }
      

相关文章:

C++.双指针算法(1.1目录修正)

C.双指针算法 1. 双指针算法概述1.1 双指针算法的定义1.2 双指针算法的应用场景1.2.1 数组中的两数之和问题1.2.2 链表中的环检测问题1.2.3 滑动窗口问题1.2.4 有序数组的合并问题 2. 双指针算法的实现基础2.1 指针的基本概念2.2 指针的运算操作 3. 双指针算法的常见类型及示例…...

『uniapp』添加桌面长按快捷操作 shortcuts(详细图文注释)

目录 手机环境适配说明安卓效果图代码 iOS(暂未实测,没有水果开发者)总结 欢迎关注 『uniapp』 专栏&#xff0c;持续更新中 欢迎关注 『uniapp』 专栏&#xff0c;持续更新中 手机环境适配说明 个别手机系统可能需要进行特别的权限设置,否则会无法使用 桌面快捷方式: 已知的有…...

【LLM vs Agent】从语言模型到智能体,人工智能迈出的关键一步

目录 一、什么是 LLM&#xff1f;语言的天才&#xff0c;思维的起点 ✅ 特点小结&#xff1a; 二、什么是 Agent&#xff1f;智能的执行者&#xff0c;自主的决策者 ✅ 特点小结&#xff1a; 三、LLM 与 Agent 的关系&#xff1a;是工具&#xff0c;更是大脑 四、案例实战…...

【看到哪里写到哪里】C的指针-3(函数指针)

//定义四个函数 加减乘数 int add(int a, int b) {return a b; } int subtract(int a, int b) {return a - b; } int multiply(int a, int b) {return a * b; } int divide(int a, int b) {if (b 0){printf("Error: devision by ZERO!");return 0;}return a / b; }…...

麦克风和电脑内播放声音实时识别转文字软件FunASR整合包V5下载

我基于FunASR制作的实时语音识别转文字软件当前更新到V5版本。软件可以实时识别麦克风声音和电脑内播放声音转为文字。 FunASR软件介绍 FunASR 是一款基础语音识别工具包和开源 SOTA 预训练模型&#xff0c;支持语音识别、语音活动检测、文本后处理等。 我使用FunASR制作了一…...

PyTorch——卷积层(3)

conv_arithmetic/README.md at master vdumoulin/conv_arithmetic GitHub out_channel1 out_channel2...

(面试)OkHttp实现原理

OkHttp 是一个高效的 HTTP 客户端&#xff0c;被广泛应用于 Android 和 Java 应用中。它提供了许多强大的特性&#xff0c;例如连接池、透明的 GZIP 压缩、HTTP/2 支持等。理解 OkHttp 的实现原理有助于更好地使用和调试它。 以下是 OkHttp 的一些核心实现原理&#xff1a; 1…...

从 PyTorch 到 TensorFlow Lite:模型训练与推理

一、方案介绍 研发阶段&#xff1a;利用 PyTorch 的动态图特性进行快速原型验证&#xff0c;快速迭代模型设计。 灵活性与易用性&#xff1a;PyTorch 是一个非常灵活且易于使用的深度学习框架&#xff0c;特别适合研究和实验。其动态计算图特性使得模型的构建和调试变得更加直…...

C++ 17 正则表达式

正则表达式不是C语言的一部分&#xff0c;这里仅做简单的介绍。 将这项技术引进&#xff0c;在 』的讨论 正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求&#xff1a; 1,检查一个串是否包含某种形式的子串&#xff1b; 2,将匹配的子串替换&a…...

【存储基础】存储设备和服务器的关系和区别

文章目录 1. 存储设备和服务器的区别2. 客户端访问数据路径场景1&#xff1a;经过服务器处理场景2&#xff1a;客户端直连 3. 服务器作为"中转站"的作用 刚开始接触存储的时候&#xff0c;以为数据都是存放在服务器上的&#xff0c;服务器和存储设备是一个东西&#…...

kernel内核和driver驱动的区别

“kernel”和“driver”虽然都跟操作系统和硬件有关&#xff0c;但它们指的是不同的东西。 1. Kernel&#xff08;内核&#xff09; 定义&#xff1a;操作系统的核心组件&#xff0c;是操作系统中负责管理系统资源和硬件的最底层软件。 职责&#xff1a; 管理CPU调度&#xff…...

5.29打卡

浙大疏锦行 DAY 38 Dataset和Dataloader类 知识点回顾&#xff1a; 1. Dataset类的__getitem__和__len__方法&#xff08;本质是python的特殊方法&#xff09; 2. Dataloader类 3. minist手写数据集的了解 作业&#xff1a;了解下cifar数据集&#xff0c;尝试获取其中一张图…...

【黑马程序员uniapp】项目配置、请求函数封装

黑马程序员前端项目uniapp小兔鲜儿微信小程序项目视频教程&#xff0c;基于Vue3TsPiniauni-app的最新组合技术栈开发的电商业务全流程_哔哩哔哩_bilibili 参考 有代码&#xff0c;还有app、h5页面、小程序的演示 小兔鲜儿-vue3ts-uniapp-一套代码多端部署: 小兔鲜儿-vue3ts-un…...

ios tableview吸顶

由于项目需要实现一个上滑吸顶的效果&#xff0c;网上也看到有很多种方式实现&#xff0c;但是如果加上下拉刷新的功能会导致界面异常&#xff0c;还有第三方库实现方式库&#xff0c;太繁琐了&#xff0c;下面是我的实现方式&#xff0c;效果如下&#xff1a; tablevie滑动吸顶…...

PyTorch——DataLoader的使用

batch_size, drop_last 的用法 shuffle shuffleTrue 各批次训练的图像不一样 shuffleFalse 在第156step顺序一致...

【Python 进阶2】抽象方法和实例调用方法

抽象方法和实例调用方法 对比表格&#xff1a; 特性抽象方法 (forward)实例调用方法 (call)定义方式abc.abstractmethod 装饰器特殊方法名 __call__调用方式不能直接调用&#xff0c;必须通过子类实现可以直接调用对象&#xff1a;controller(attn, ...)实现要求必须由子类实…...

第1章:走进Golang

第1章&#xff1a;走进Golang 一、Golang简介 Go语言&#xff08;又称Golang&#xff09;是由Google的Robert Griesemer、Rob Pike及Ken Thompson开发的一种开源编程语言。它诞生于2007年&#xff0c;2009年11月正式开源。Go语言的设计初衷是为了在不损失应用程序性能的情况下…...

Predixy的docker化

概述 当前已有一套redis cluster的集群&#xff0c;但是fs中的hiredis只能配置单实例redis。 AI了一下方案&#xff0c;可以使用redis的proxy组件来实现从hiredis到redis cluster的互通。 代码地址&#xff1a;https://github.com/joyieldInc/predixy Predixy特性介绍&…...

C++ 之 多态 【虚函数表、多态的原理、动态绑定与静态绑定】

目录 前言 1.多态的原理 1.1虚函数表 1.2派生类中的虚表 1.3虚函数、虚表存放位置 1.4多态的原理 1.5多态条件的思考 2.动态绑定与静态绑定 3.单继承和虚继承中的虚函数表 3.1单继承中的虚函数表 3.2多继承(非菱形继承)中的虚函数表 4.问答题 前言 需要声明的&#x…...

【JavaWeb】Maven、Servlet、cookie/session

目录 5. Maven6. Servlet6.1 Servlet 简介6.2 HelloServlet6.3 Servlet原理6.4 Mapping( **<font style"color:rgb(44, 44, 54);">映射 ** )问题6.5 ServletContext6.6 HttpServletResponse<font style"color:rgb(232, 62, 140);background-color:rgb(…...

[蓝桥杯]阶乘求值【省模拟赛】

问题描述 给定 nn&#xff0c;求 n!n! 除以 10000000071000000007 的余数。 其中 n!n! 表示 nn 的阶乘&#xff0c;值为从 11 连乘到 nn 的积&#xff0c;即 n!123…nn!123…n。 输入格式 输入一行包含一个整数 nn。 输出格式 输出一行&#xff0c;包含一个整数&#xff…...

鸿蒙OSUniApp微服务架构实践:从设计到鸿蒙部署#三方框架 #Uniapp

UniApp微服务架构实践&#xff1a;从设计到鸿蒙部署 引言 在最近的一个大型跨平台项目中&#xff0c;我们面临着一个有趣的挑战&#xff1a;如何在UniApp框架下构建一个可扩展的微服务架构&#xff0c;并确保其在包括鸿蒙在内的多个操作系统上流畅运行。本文将分享我们的实践…...

Rust 编程实现猜数字游戏

文章目录 编程实现猜数字游戏游戏规则创建新项目默认代码处理用户输入代码解析 生成随机数添加依赖生成逻辑 比较猜测值与目标值类型转换 循环与错误处理优化添加循环优雅处理非法输入​ 最终完整代码核心概念总结 编程实现猜数字游戏 我们使用cargo和rust实现一个经典编程练习…...

关于神经网络中的激活函数

这篇博客主要介绍一下神经网络中的激活函数以及为什么要存在激活函数。 首先&#xff0c;我先做一个简单的类比&#xff1a;激活函数的作用就像给神经网络里的 “数字信号” 加了一个 “智能阀门”&#xff0c;让机器能学会像人类一样思考复杂问题。 没有激活i函数的神经网络…...

CentOS_7.9 2U物理服务器上部署系统简易操作步骤

近期单位网站革新&#xff0c;鉴于安全加固&#xff0c;计划将原有Windows环境更新到Linux-CentOS 7.9&#xff0c;这版本也没的说&#xff08;绝&#xff09;了&#xff08;版&#xff09;官方停止更新&#xff0c;但无论如何还是被sisi的牵挂着这一大批人&#xff0c;毕竟从接…...

第十三篇:MySQL 运维自动化与可观测性建设实践指南

本篇重点介绍 MySQL 运维自动化的关键工具与流程&#xff0c;深入实践如何构建高效可观测体系&#xff0c;实现数据库系统的持续稳定运行与故障快速响应。 一、为什么需要 MySQL 运维自动化与可观测性&#xff1f; 运维挑战&#xff1a; 手动备份容易遗漏或失败&#xff1b; …...

短视频平台差异视角下开源AI智能名片链动2+1模式S2B2C商城小程序的适配性研究——以抖音与快手为例

摘要 本文以抖音与快手两大短视频平台为研究对象&#xff0c;从用户群体、内容生态、推荐逻辑三维度分析其差异化特征&#xff0c;并探讨开源AI智能名片链动21模式与S2B2C商城小程序在平台适配中的创新价值。研究发现&#xff0c;抖音的流量中心化机制与优质内容导向适合品牌化…...

HTTP 如何升级成 HTTPS

有一个自己的项目需要上线&#xff0c;域名解析完成后&#xff0c;发现只能使用 http 协议&#xff0c;这在浏览器上会限制&#xff0c;提示用户不安全&#xff0c;所以需要把 HTTP 升级成 HTTPS 协议&#xff0c;但又不想花钱。 前提条件&#xff1a; 已经配置好 Nginx 服务器…...

【笔记】Windows 下载并安装 ChromeDriver

以下是 在 Windows 上下载并安装 ChromeDriver 的笔记&#xff1a; ✅ Windows 下载并安装 ChromeDriver 1️⃣ 确认 Chrome 浏览器版本 打开 Chrome 浏览器 点击右上角 ︙ → 帮助 → 关于 Google Chrome 记下版本号&#xff0c;例如&#xff1a;114.0.5735.199 2️⃣ 下载…...

Spark-Core Project

RDD转换算子总结 RDD转换算子分为Value类型、双Value类型和Key - Value类型。 1、Value类型 map&#xff1a;对数据逐条映射转换&#xff0c;可改变数据类型或值。如 dataRDD.map(num > num * 2 运行结果&#xff1a; 2&#xff09;mapPartitions&#xff1a;以分区为单位处…...