算法-双指针

264 阅读6分钟

什么是双指针?

顾名思义,就是俩变量,特点是一个跑的快,一个跑得慢。通过一个快指针和一个慢指针在一个for循环下完成两个for循环的工作。

先来看看什么场景可以使用双指针

  • 过滤字符串/数组,找出符合条件的元素

双指针使用方式

双指针使用方式一: 只定义一个指针slow,将i作为快指针(正常指针)

这种指针适用于什么场景呢?通过我少量的钻研,总结:不需要排序,单纯的把不想要的元素替换或者挪位或者删除。且看下面题 1. 示🌰1:去除不想要的后数组的长度(力扣27题)

  • 题目:给你一个数组 nums 和一个值 val,你需要 原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。
  • 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
  • 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]

解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。

你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案

先来分析一波:要求原地移除,不能创建新数组。把不想要的数据干走行,那不就是不需要排序嘛,然后把不要的东西挪到后面或者改掉他。(元素x不要&无排序)因此:咱们优先选择slow+i的双指针模式

时间复杂度,一个循环,所以O(n),空间复杂度O(1)

var removeElement = function(nums, val) {
  let slow = 0;// 身为绿茶,我要跟你i保持一致先让你知道我的好
  for(let i=0;i<nums.length;i++) {
    if(nums[i]!=val) {
      // slow开始装了说,你真棒,我要向你靠近,同时把你给我的钱给我守护的人(i哭了,原来我是舔狗)
      nums[slow] = nums[i];
      slow++;
    }
  }
  // 那么新长度是多少呢?看s前进了多少步就好了,因为符合条件,才++
  return slow
}

总结 这种题的思想就是我不想要你啊val,你不要过来啊。然后诱惑i去看看数据里有没有val,没有那i太棒了,我要向你前进一步,并且让我(slow)守护的人nums[slow]拿到你的好东西nums[i]

2. 示🌰2将不想要的移动到末尾(力扣283题)

  • 题目:移动0

  • 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

  • 请注意 ,必须在不复制数组的情况下原地对数组进行操作。

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

元素x不要&无排序&移位)因此:咱们优先选择slow+i的双指针模式。通过你对上面题目的分析,你会不会有如下代码的想法

var moveZeroes = function(nums) {
    let slow = 0;
    for(let i=0;i<nums.length;i++) {
        if(nums[i]!=0) {
            nums[slow] = nums[i]
            slow++
        }
    }
    for(let j=slow;j<nums.length;j++) {
        nums[j] = 0
    }
    return nums
};

但是这样不讲武德,题目是移动,不是让你在那替换

你说WechatIMG1305.jpeg 可我想说 WechatIMG1307.jpeg

但迫于生活压力,我不得不正儿八经移动(圈起来要考)

移动到后面,其实就是在交换位置,我想要的我都拿来,但是因为不能丢,只能先交给i保管一下

于是正儿八经分析:如果i的值合我心意,身为绿茶的我(slow)还是会像你靠近,只是这个i对不起了,你先委屈一下;因为你太安全了,我必须要把我守护的人的值给你,你把你的优秀的值给我,因为0不能扔,我必须得把0给你。呵,女人。最后i走过终点,0也走向了终点。

可以看到指针slow都是在符合条件的时候(i!=x)向i靠近,那意味着我们下次遇到这种位移或者替换操作时,可以对slow进行绿茶的初步设定

其实和上面的题区别在于挪动不符合条件的,这样我们就可以将符合条件的(i!=x)给slow的同时,将nums[i]变成nums[slow]

var moveZeroes = function(nums) {
    let slow = 0,fast=0,right=0;
    for(let i=0;i<nums.length;i++) {
        if(nums[i]!=0) {
            let temp = nums[slow];
            nums[slow] = nums[i];
            nums[i] = temp
            slow++;
        }
    }
    return nums
};

3. 示🌰3:去除#字符前面的字符(力扣844题)

  • 题目:给定 st 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true # 代表退格字符。
  • 注意:如果对空文本输入退格字符,文本继续为空。
输入: s = "ab#c", t = "ad#c"
输出: true
解释: s 和 t 都会变成 "ac"

分析

  • 删除的是#前面的和#,后面的是不变的,所以我们倒序遍历;
  • 肯定不能遍历完整个s后,和t后再进行st的比较,不符合算法的快准狠;
  • 肯定是s掏出来一个可比的,t掏出来一个可比的进行比较;
  • 什么是可比的呢?
    • 该元素是不会被#删除的元素
  • 什么是不可比的呢?
    • 该元素会被#删除
  • 由此可见,我们需要知道元素是否需要被删除,于是给st分别整一个变量存储需要删除的元素的个数,有的话,不拿这个来比,没有的话,上比较板子
  • 什么时候结束比较?当两个都比较到了头
   var backspaceCompare = function(s, t) {
    let i = s.length - 1, skipS=0;
    let j = t.length - 1, skipT=0;
    
    while(i>=0||j>=0) {
        if(s[i]=='#') {
            skipS++;
            i--;
            continue;
        } else if(skipS>0) {
            skipS--;
            i--;
            continue;
        } else {
            // 拿去比较
        }
        if(t[j]=='#') {
            skipT++;
            j--;
            continue;
        } else if(skipT>0) {
            skipT--;
            j--;
            continue;
        } else {
            // 拿去比较
        }
        if(s[i]!=t[j]) {
            return false
        } else {
            i--;
            j--;
        }
    }
    return true
};

双指针使用方式二: 定义leftright指针,i照样循环(循环的目的是改变i的值)

这种指针适用于什么场景呢?通过我少量的钻研,总结:需要排序,并且当前给到的数组是有排序规律的。且看下面题 1. 示🌰1:有序数组的平方(力扣977题)

  • 题目:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

分析

  • 数据依次递增,只有存在负数才可能会有挪位的可能,也就是最左边和最右边绝对有一个更大,所以我们左右比。这个特点是本题的关键,因为只有左比右大的可能,所以我们在左右比之后就能拿到第一大、第二大的值,避免了其他情况我们还要把一次比较后的大值再次拿出来进行比较
  • 左右比有一个特点,就像二分法一样缩小了比较次数,因为一次比较拿到两个值,那么意味着我们可以有快的方法只比较大概数组长度1半的次数就能完成这道题
  • 照着这个思路,那循环条件一定不能是i的循环啦,如二分法一样,我们将循环条件弄成left<=right
  • 两两比较必有一大,谁大谁能够满足i想要的,所以这里的i我们从nums.length-1开始。大的值给到i,之后谁大谁就有资格向i靠近,于是left++,right--意味着靠近。
var sortedSquares = function(nums) {
 let left=0,right=nums.length-1,i=nums.length-1,arr=[];
 while(left<=right) {
     let leftNum = Math.pow(nums[left],2);
     let rightNum = Math.pow(nums[right],2);
     if(leftNum<rightNum) { 
         arr[i] =rightNum; 
         right--; 
     } else {
         arr[i] = leftNum; 
         left++;
     }
     i--;
 }
 return arr
};

看了下leetcood底下的评论,我也想说俺也一样。 image.png

总结

  • 双指针的使用场景有很多,如寻找一个数组中满足条件的最短子数组(滑动窗口);他们终究是通过两个变量的挪动去解题,关键在于如何利用双指针去解题
  • 不需要排序,单纯的把不想要的元素替换或者挪位或者删除——i+slow指针
  • 需要排序或者不能改变原数组的考虑使用left+right
  • 滑动窗口的自己琢磨吧,也是left+right,就是left++, right++/--需要好好想想