双指针
双指针主要指的是在解题时采用两个速度不同或位置不同的指针来遍历对象,解决问题。
以下是问题的类别以及采用的方法。
1.二分查找
首先在采用二分查找前,应关注对象是否有序,二分查找针对有序数组。
解题思路:将循环判定条件设为两指针是否相等,与中间点相比较,未找到则缩小范围,否则执行需要的操作。
通过两个前后指针来表示头和尾,每次遍历找中间值,比较后缩小一半遍历的距离。
例题1(704):给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
int search(vector<int>& nums, int target) {
int low=0,high=nums.size()-1;
while(low<=high){
int mid=(high-low)/2+low;
if(nums[mid]==target){
return mid;
}
else if(nums[mid]<target){
low=mid+1;
}
else{
high=mid-1;
}
}
return -1;
}
例题2(278):你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
int firstBadVersion(int n) {
int left = 1, right = n;
while (left < right) { // 循环直至区间左右端点相同
int mid = left + (right - left) / 2; // 防止计算时溢出
if (isBadVersion(mid)) {
right = mid; // 答案在区间 [left, mid] 中
} else {
left = mid + 1; // 答案在区间 [mid+1, right] 中
}
}
// 此时有 left == right,区间缩为一个点,即为答案
return left;
2.前后指针排序(归并)
例题1(977):给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
解题思路:首先拥有条件数组已经有序,那么可以找到负数和正数的中间点,左边为负数,右边为正数,接下来得到了两个有序的子数组,可以采用归并排序的方法。具体地,使用两个指针分别指向位置neg和neg+1,每次比较两个指针对应的数,选择较小的那个放入答案并移动指针。当某一指针移至边界时,将另一指针还未遍历到的数依次放入答案。
vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size();
vector<int> ans(n);
for (int i = 0, j = n - 1, pos = n - 1; i <= j;) {
if (nums[i] * nums[i] > nums[j] * nums[j]) {
ans[pos] = nums[i] * nums[i];
++i;
}
else {
ans[pos] = nums[j] * nums[j];
--j;
}
--pos;
}
return ans;
}
3.判断链表
当不知道列表长度,同时要取列表中某一位的数的时候,就可以采用前后指针方法。
例题(19):给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。(尝试用一次扫描)
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
解题思路:删除倒数第n个节点,也就是删除最后一个节点的第前n-1个节点,若我们能用两个指针相差为n,当快节点到达尾部,满节点的next指向的就是要删除的节点。
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy=new ListNode(0,head);
ListNode* fast=head;
ListNode* slow=dummy;
for(int i=0;i<n;i++){
fast=fast->next;
}
while(fast){
fast=fast->next;
slow=slow->next;
}
slow->next=slow->next->next;
ListNode* ans=dummy->next;
delete dummy;
return ans;
}
4.滑动窗口
属于双指针难度较高的一种应用,涉及到动态规划,在字符串中查找某一符合要求的子串。
例题(567):
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
解题思路:从s2中取子串,只有长度等于s1的长度时,才符合条件,创建哈希表cnt来存储s1的字符,设定滑动窗口,每次左指针加1,从表中去除该数字,右指针加1,判断是否满足哈希表,满足则返回。对s1的cnt减1,s2逐个加。
优化思路,还可以在保证 cnt 的值不为正的情况下,去考察是否存在一个区间,其长度恰好为 n。
bool checkInclusion(string s1, string s2) {
int n=s1.length(),m=s2.length();
if(n>m)
return false;
vector<int> cnt(26);
for(int i=0;i<n;i++)
{
--cnt[s1[i]-'a'];
}
int left=0;
for(int right=0;right<m;right++)
{
int x=s2[right]-'a';
++cnt[x];
while(cnt[x]>0)
{
cnt[s2[left]-'a']--;
left++;
}
if(right-left+1==n)
return true;
}
return false;
}
部分细节未在文章中给出,仅供参考使用,想要更全面学习可以在Leetcode上查看。