数据结构-双指针(尺取法)

双指针有好几种,但是最常用的是尺取法,所以有的时候就说尺取法

双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。

分类有奇奇怪怪的几种,我写了三种

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;
}