力扣刷题- 双指针的一些经典题目


前言

刷过算法题的都知道,双指针技巧在数组和字符串的相关题目中运用得非常多,但是我在一开始学习的时候很茫然,即使看了提示要用双指针也无从下手,写这篇博客的目的是记录经典双指针题目,以便以后及时回顾。


目录

前言

一、经典题目

二、总结


一、经典题目

本文节选的经典题目都是参考的公众号 【代码随想录】 上的刷题攻略,在此感谢大佬指点迷津。

力扣icon-default.png?t=L9C2https://leetcode-cn.com/problems/remove-element/

1. 【移除元素】

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

        解题思路

        我们都知道数组在内存中是连续的地址空间,本题暴力解法就是遍历数组,当前元素等于 val 时,只能将后面的元素整体向前移动一位,并且用一个变量记录移除val后的数组新长度。

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

显然,暴力解法的时间复杂度为:o(n^2),空间复杂度为:o(1),这不符合原地移除的要求!

接下来,我将谈一谈双指针。本题中双指针就是用两个指针(这里不是c语音中的指针变量,而是用来遍历数组的变量)一个用于遍历数组,一个用于更新数组。通俗地讲就是用两个指针在一个循环内完成两个for循环的工作

 

我第一次看了【代码随想录】的思路写的代码:

public int removeElement(int[] nums, int val) {
        int i = 0, j = 0;
        // 先找到第一个 val 所在位置 i
        while (i < nums.length) {
            if (nums[i] == val)
                break;
            i++;
        }

        // 从索引 i+1 开始遍历数组,将!= val的值按照顺序依次赋值给nums[i],这样就完成了原地移除
        j = i + 1;
        while (j < nums.length) {
            if (nums[j] != val)
                nums[i++] = nums[j];
            j++;
        }

        return i;
    }

说实话,写得一点都不简洁了,可能这就是屎一样的代码,哈哈。其实仔细观察上面的动图思考下就知道,在用 j 遍历的数组的时候遇到不等于 val 的元素时只要按照顺序赋值给 nums[i] 即可。

public int removeElement(int[] nums, int val) {
        // 双指针法原地移除元素
        int i = 0;
        for (int j = 0; j < nums.length; j++) {
            if (nums[j] != val) 
                nums[i++] = nums[j];
        }
        return i;
    }

2. 【移动0】
力扣icon-default.png?t=L9C2https://leetcode-cn.com/problems/move-zeroes/

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

思路与【移除元素】一模一样

// 双指针移除0,最后剩余元素全部置为0即可
    public void moveZeroes(int[] nums) {
        int i = 0, j = 0;
        // 先找到第一个为0的元素位置
        while (i < nums.length) {
            if (nums[i] == 0)
                break;
            i++;
        }

        // 从第一个为0的元素后面一位开始遍历,遇到不等于0的元素就覆盖当前i处的元素
        j = i + 1;
        while (j < nums.length) {
            if (nums[j] != 0)
                nums[i++] = nums[j];
            j++;
        }

        // 最终 i 表示去除掉所有0后数组长度,原数组最后 nums.length - i 个元素全部置0即可
        while (i < nums.length)
            nums[i++] = 0;
    }

3. 比较含退格的字符串

给定 st 两个字符串,当它们分别被输入到空白的文本编辑器后,请你判断二者是否相等。# 代表退格字符。如果相等,返回 true ;否则,返回 false

注意:如果对空文本输入退格字符,文本继续为空。

输入:s = "ab#c", t = "ad#c"
输出:true
解释:S 和 T 都会变成 “ac”。

根据上述两题的思路,可以写出如下代码:

public boolean backspaceCompare(String s, String t) {
        char[] chars = s.toCharArray();
        char[] chart = t.toCharArray();
        int size1 = size(chars);
        int size2 = size(chart);
        if(size1 != size2) {
            return false;
        }else {
            for (int i = 0; i < size1; i++) {
                if(chars[i] != chart[i]) {
                    return false;
                }
            }
            return true;
        }
    }

    public int size(char[] chars) {
        int j = 0;
        for (int i = 0; i < chars.length; i++) {
            if(chars[i] != '#') {
                chars[j++] = chars[i];
            }else {
                if(j >= 1) {
                    j--;
                }
            }
        }
        return j;
    }

上述代码一个关键点就是 size 方法中遍历字符数组的时候,遇到 “#” 的时候 j 需要回退一步。上述代码因为要将字符串转换为字符数组,比较麻烦,并不是最优解法。下面介绍官方的双指针解法,很巧妙。

public boolean backspaceCompare(String s, String t) {
        int i = s.length() - 1, j = t.length() - 1;
        int skipS = 0, skipT = 0;
        while (true) {
            // 从后往前遍历找到s中的待匹配字符
            while (i >= 0) {
                if (s.charAt(i) == '#')
                    skipS++;
                else {
                    if (skipS > 0)
                        skipS--;
                    else
                        break;
                }
                i--;
            }

            // 从后往前遍历找到中的待匹配字符
            while (j >= 0) {
                if (t.charAt(j) == '#')
                    skipT++;
                else {
                    if (skipT > 0)
                        skipT--;
                    else
                        break;
                }
                j--;
            }

            // 在两个字符串均没有到头的情况下,比较待匹配字符
            if (i >=0 && j >= 0) {
                if (s.charAt(i) != t.charAt(j))
                    return false;
            }

            // 如果两个字符串其中有一个越界到头了,就退出循环
            if (i < 0 || j < 0)
                break;
            i--;
            j--;
        }

        // 当s和t最终都到头了且前面没有返回false的时候才返回true
        return (i == -1 && j == -1);
    }

整体思路:# 只对前面的字符产生影响,对后面的字符没有影响。所以可以用双指针分别后序遍历字符串 s 和 t ,用两个变量 skipS 和 skipT 记录当前需要跳过的字符个数。算法步骤如下:

  • 后序遍历字符串 s,遇到 # 就 skipS++ ,遇到其他字符时,如果 skipS>0,就表示当前字符不是待匹配字符,是需要被回退删除的,此时 skipS--。否则,就表示当前字符就是待匹配字符,跳出当前循环
  • 后续遍历字符串 t,同样的操作找到待匹配字符,跳出循环
  • 第三步有个细节,就是有可能字符串 s 或者 t,没有待匹配字符了(都被 # 回退删除了),那么下标就会存在越界异常,所以要在 s 和 t 都没有到头的情况下判断待匹配字符是否相同,不相同直接返回 false
  • 如果下标 i 和 j 有一个越界了,就跳出最外层的 while 循环
  • 上述操作没有返回false,说明最终的字符串有可能是相同的。但是最后需要判断两个字符串是否都到头了,都到头了才返回 true

4. 力扣icon-default.png?t=L9C2https://leetcode-cn.com/problems/squares-of-a-sorted-array/【977 有序数组的平方】

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

这个题目很简单,暴力解法就是平方后排序即可,时间复杂度为 o(nlogn + n),nlogn 是排序的时间,n 是遍历取平方的时间。如果面试官要求实现 o(n) 的时间复杂度呢?此时可以使用双指针。具体思路:数组中存在负数且是非递减的,负数平方后有可能比正数平方大,所以最大值一定是在最左端或者最右端取到。用两个指针,l 指向索引0,r 指向最后一位,同时新建一个存储结果的数组,从最后一位往前开始填充。具体见代码。

public int[] sortedSquares(int[] nums) {
        int[] ret = new int[nums.length];
        int l = 0, r = nums.length - 1, k = r;

        while ( l <= r) {
            if (nums[r] * nums[r] >= nums[l] * nums[l]) {
                ret[k--] = nums[r] * nums[r];
                r--;
            }else {
                ret[k--] = nums[l] * nums[l];
                l++;
            }
        }

        return ret;
    }

注意 l==r 的时候是有意义的,此时的 nums[l] * nums[l] 就是 ret[0]

二、总结

双指针是解决数组和字符串相关问题的一种使用技巧,可以用一次遍历达到两个for循环的效果,降低了时间复杂度(双指针的时间复杂度一般是 o(n)。双指针我目前遇到的无非就是两种,一种是快慢指针,都是从索引 0 开始,但是快指针走的快点。一种是两个指针一个从头开始,一个从尾开始(二分查找其实也可以认为就是这种双指针)。总之,没有捷径,只能通过不断地刷题总结才可能掌握技巧,唯手熟尔。编程改变世界,我改变混乱的逻辑

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值