数据结构-双指针(尺取法)
双指针有好几种,但是最常用的是尺取法,所以有的时候就说尺取法
双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
分类有奇奇怪怪的几种,我写了三种
1.对撞指针(除了二分没啥用)
最经典的应用是二分, 最最朴素的对撞指针就是
function fn (list) {
var left = 0;
var right = list.length - 1;
//遍历数组
while (left <= right) {
left++;
// 一些条件判断 和处理
... ...
right--;
}
}
翻转数组
void reverse(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
// swap(nums[left], nums[right])
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
2. 快慢指针
快慢指针一般都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后,巧妙解决一些链表中的问题。
判链表环(图的话用这个太慢了)
单链表的特点是每个节点只知道下一个节点,所以一个指针的话无法判断链表中是否含有环的。 如果链表中不包含环,那么这个指针最终会遇到空指针 null 表示链表到头了,这还好说,可以判断该链表不含环
boolean hasCycle(ListNode head) {
while (head != null)
head = head.next;
return false;
}
但是如果链表中含有环,那么这个指针就会陷入死循环,因为环形数组中没有 null 指针作为尾部节点。 经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。boolean hasCycle(ListNode head) {
ListNode fast, slow;
fast = slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow)
return true;
}
return false;
}
找链表上环的起点
这个问题其实不困难,有点类似脑筋急转弯,先直接看代码:
ListNode detectCycle(ListNode head) {
ListNode fast, slow;
fast = slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow)
break;
}
slow = head;
while (slow != fast) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
可以看到,当快慢指针相遇时,让其中任一个指针重新指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢? 第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。 设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。 巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点 所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。寻找链表的中点
类似上面的思路,我们还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。
ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
// slow 就在中间位置
return slow;
当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右: 寻找链表中点的一个重要作用是对链表进行归并排序。 回想数组的归并排序:求中点索引递归地把数组二分,最后合并两个有序数组。对于链表,合并两个有序链表是很简单的,难点就在于二分。 这个不能用对撞吗?? 不能,因为是单向的
寻找链表的倒数第 k 个元素
我们的思路还是使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点(为了简化,假设 k 不会超过链表长度):
ListNode slow, fast;
slow = fast = head;
while (k-- > 0)
fast = fast.next;
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
3.左右指针
两数之和
只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 可以调整 sum 的大小:
滑动窗口算法/尺取法(重要)
这也许是双指针技巧的最高境界了,如果掌握了此算法,可以解决一大类子字符串匹配的问题,我第一次见是在西南民族大学第十一届程序设计竞赛(同步赛)B-都说小镇的切糕贵看到的 输入
9
bcdddbddc
输出
3
我用的二分+朴素查找挂了好多次 最后看到大佬的标称O(n)跑出
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int l,r,ans,n,ttnum=0,used[60];
char ss[100050];
int main(){
cin>>n;
scanf("%s",ss);
memset(used,0,sizeof(used));
for(int i=0;i<n;i++){
if(!used[ss[i]-'A']){
ttnum++;
used[ss[i]-'A']=1;
}
}
l=ttnum;r=n;ans=n;
int cnt=0;
while(l<r){
cnt=0;
memset(used,0,sizeof(used));
bool flag=false;
int mid=(l+r)/2;
for(int i=0;i<mid;i++){
if(used[ss[i]-'A']==0){
cnt++;
}
used[ss[i]-'A']++;
if(cnt==ttnum){flag=true;break;}
}
for(int i=1;i<=n-mid;++i){
used[ss[i-1]-'A']--;
if(used[ss[i-1]-'A']==0) cnt--;
if(used[ss[i+mid-1]-'A']==0){
cnt++;
if(cnt==ttnum){
flag=1;
break;
}
}
used[ss[i+mid-1]-'A']++;
}
if(flag){r=mid;ans=mid;}//cout<<"OK @ "<<mid<<endl;}
else {l=mid+1;}//cout<<"ERR @ "<<mid<<endl;}
}
cout<<l;
return 0;
}