滑动窗口1:双指针的基本思想

1.双指针的基本思想

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

换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算。

不过这里的指针并不是真正意义的指针,只是两个遍历数组的游标罢了。在链表中也可以使用双指针,定义了两个引用,也不算真正的指针。所以双指针只是一种思想的统称,没必要追究其名字。

双指针有两种基本的策略,一个称为快慢指针,一个称为对撞指针。快慢指针是从一头开始,一个走的快一个走的慢,一般的快是用来选择满足条件的参数,而慢的则是已经固定好的元素,该场景我们在数组和链表章节介绍了很多,而冒泡排序也属于这种思想。另一种是对撞型的,两个指针分别从两侧向中间走,很多场景不能一次就移动完,可能要多次执行才可以,典型的例子就是快速排序。

两个指针看似简单,但是能解决非常多的复杂问题。

2.快慢指针

快慢指针是两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)慢指针(slow),两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如 fast 每次增长两个,slow 每次增长一个等等。

在基本算法中典型的例子就是对数组进行增加或者删除元素的时候,为了防止大量移动元素,我们会采用两个游标来辅助操作,一个是当前正访问的对象(fast),我们要判断该元素是否满足我们的要求,不满足会跳过继续向前走。第二个用来标记已经调整好的(slow),当fast找到符合要求的元素时就会将其添加到slow位置。

典型的题目有:

1.LeetCode26 给你一个有序数组 nums ,请你原地删除重复出现的元素,使每个元素只出现一次 ,返回删除后数组的新长度。

2.剑指offer题目:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

3.链表问题,LeetCode141判断链表中是否有环。

4.寻找链表中间结点。

我们来看一下做法。LeetCode26是一个常规的题目,使用双指针最方便,一个指针负责责数组遍历,一个指向有效数组的最后一个位置。当两个指针的值不一样时,才将指向有效位的向下移动。

 public int removeDuplicates1(int[] nums) {
        int n = nums.length;
        //j用来标记有效位
        int j = 0;
        for (int i = 0; i < n; i++) {
            if (nums[i] != nums[j]) {
                nums[++j] = nums[i];
            }
        }
        return j + 1;
    }

剑指offer字符串替换的问题,这个题比较特别的一点是移动的时候要先计算一遍空格的数量,算出调整之后的总长度,然后从后向前开始移动。这样做的好处同样是为了每次替换空格都要进行大量的元素移动。

 public String replaceSpace(StringBuffer str) {
    	String res="";
        for(int i=0;i<str.length();i++){
            char c=str.charAt(i);
            if(c==' ')
                res += "%20";
            else
                res += c;
        }
        return res;
     }

对于链表,快慢指针同样适用,而且应用还非常广。

LeetCode141,给定一个链表,判断链表中是否有环。一个快指针(一次走两步),一个慢指针(一次走一步)。如果快的能到达表尾就不会有环,否则如果存在圈,则慢指针一定会在某个位置与快指针相遇。这就像在操场长跑,一个人快一个人慢,只要时间够,快的一定能在某个时候再次追上慢的人(也就是所谓的套圈)。代码就是这样:

public boolean hasCycle(ListNode head) {
       if(head==null || head.next==null){
           return false; 
       }
        ListNode fast=head;
        ListNode slow=head;
        while(fast!=null && fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(fast==slow)
                return true;
        }
        return false;
    }

而寻找链表的中点更容易,就是上面题目的一部分,还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。

ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
    fast = fast.next.next;
    slow = slow.next;
}
// slow 就在中间位置
return slow;

3.对撞指针

对撞指针是指在数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。

对撞数组适用于连续数组和字符串,也就是说当你遇到题目给定连续数组和字符串问题时,应该第一时间想想对撞指针行不行。

这个最典型的例子就是快速排序、以及数组的奇偶移动等等。

例如LeetCode905 输入一个整数数组,通过一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。

这个题最直接的方式是使用一个临时数组,第一遍查找并将所有的偶数复制给新数组,第二遍查找并复制所有的奇数给数组。代码如下:

class Solution {
    public int[] sortArrayByParity(int[] A) {
        int[] ans = new int[A.length];
        int t = 0;

        for (int i = 0; i < A.length; ++i)
            if (A[i] % 2 == 0)
                ans[t++] = A[i];

        for (int i = 0; i < A.length; ++i)
            if (A[i] % 2 == 1)
                ans[t++] = A[i];

        return ans;
    }
}

# 4. 两种指针的移动场景

双指针的应用非常广,那怎么判断使用哪种双指针呢?这个问题比较难回答,如果思考的角度不同就可能有不同的解答,甚至两种指针方式都可以用,例如leetcode27 给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。

在删除的时候,从删除位置开始的所有元素都要向前移动下,所以这题的关键就是如果很多值为val的元素,如何避免反复向前移动。 有两种处理方式,为了开拓思路,我们都看一下:

(1)第一种方法:快慢指针

将数组分成前后两段,定义两个指针i和j,初始值都是0。

i之前的位置都是有效部分,j表示当前要访问的元素。

这样遍历的时候,j不断向后移动:

如果array[j]的值不为val,则将其移动到array[++i]处。

如果array[j]的值为val,则j继续向前移动

这样,前半部分是有效部分,后半部分是无效部分。

其实只可以借助for的语法进一步简化实现:

    public int removeElement(int[] nums, int val) {
        int ans = 0;
        for(int num: nums) {
            if(num != val) {
                nums[ans] = num;
                ans++;
            }
        }
        //最后剩余元素的数量
        return ans;
    }

这里虽然只有一个变量ans,但是for循环的写法帮助我们减少了一个定义。

(2)第二种方式:对撞双指针

这里还有一种解法,有的地方叫做交换移除,思路就是我们还是从两端开始向中间遍历,left遇到num[i]=val的时候停下来,右侧继续。当右侧遇到num[j]!=val的位置的时候,将num[j]交换或者直接覆盖num[i]。之后i继续向右走。

   public int removeElement(int[] nums, int val) {
        int ans = nums.length;
        for (int i = 0; i < ans;) {
            if (nums[i] == val) {
                nums[i] == nums[ans - 1];
                ans--;
            } else {
                i++;
            }
        }
        return ans;
    }

这个思路其实和快速排序中执行一轮的过程非常类似。快速排序就是标定一个元素,比它小的全在其左侧,比它大的全在右侧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

纵横千里,捭阖四方

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

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

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

打赏作者

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

抵扣说明:

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

余额充值