解析双指针

399 阅读4分钟

上次我们一起分析了滑动窗口这个常用的算法技巧,使用俩指针即可维护满足条件的窗口,我也跟大家说过,双指针也是算法中重要的工具,很多题目因为引入了双指针的思想变得异常简单。

一开始我在做题的时候,最喜欢用的就是暴力无脑循环,但是很多时候得到的算法复杂度都很高,后来我就发现一个规律,凡是题目说给我们一个排好序的数组或者链表,要我们从中找到满足条件的一系列元素时,往往在提示我们用双指针解题,这是为什么呢,我们接着往下看。

先看一道题,给定一个排序好的数组跟一个数字,找出和跟这个数字相等的一对数字,返回他们的索引。这道题目,我一眼就看出暴力循环,有木有?!

写起来贼简单,这活儿我熟,我来写:)

public static int[] search(int[] arr, int targetSum) {
        for (int i = 0; i < arr.length) {
            for (int j = i + 1; j < arr.length; j++) {
                if (targetSum - arr[j] == arr[i]) {
                    return new int[]{i, j};
                }
            }
        }
        return new int[]{-1, -1};
    }

在这里我们依次迭代剩余元素来寻找符合条件的两个数,由于两层循环,时间复杂度为O(n^2)。这肯定不是最好的办法,因为题目给我们的排序好的数组这个属性完全没有用到,题目肯定不会给我们无用的信息,我们来看看排序好的数组有什么不一样。

对于排序好的数组来说,从前往后越来越大,这是一个很重要的属性,那我们就可以分别在数组前后各放置一个指针,start跟end,指着两个数,然后我们可以做两件事:

  1. 如果这两个数之和大于target,那意味着我们需要两数之和小一点,那我们就把end往前移动。
  2. 要是粮食之和小于target,那说明我们得把start往后移动了。 这就是二分搜索的思想,我们来实现一下代码:
public static int[] search(int[] arr, int targetSum) {
        int start = 0;
        int end = arr.length - 1;
        while (start < end) {
            if (arr[start] + arr[end] == targetSum) {
                return new int[]{start, end};
            }

            if (arr[start] + arr[end] > targetSum) {
                end--;
            } else {
                start++;
            }
        }
        return new int[]{-1, -1};
    }

核心思想就是这么简单,用俩指针在排序好的数组里快速迭代,但是双指针的绝不仅仅是用来二分搜索,我们再来看一道题。 给定一个排序好的数组,去掉重复元素,返回去重后的新长度,不准用额外的空间。也就是说,如果这个数组是

[2, 3, 3, 3, 6, 9, 9]

这个情况下我们得返回4,而且不能使用额外的数据结果来帮助去重。乍一看不用额外空间似乎没法做,这可咋整,那我们只能在原数组上进行修改了。但是好在这个数组是排序好的,那也就是说,重复元素都是相邻的,但数组不像List一样可以直接删掉某个元素,我们只能想办法把数组中,重复的元素扔到一边。

那其实我们这里就可以使用两个指针,一个指着下一个非重复元素应该放置的位置,一个迭代整个数组寻找下一个非重复元素的位置,这么说可能比较拗口,我们直接来看代码就能理解了:

public static int remove(int[] arr) {
        int nextNonDuplicate = 0; // 记录放置非重复元素的位置
        for (int i = 1; i < arr.length; i++) {
            if (arr[nextNonDuplicate] != arr[i]) {
                arr[nextNonDuplicate + 1] = arr[i];
                nextNonDuplicate++;
            }
        }
        return nextNonDuplicate + 1;
    }

我们不管数组里有多少个重复元素,在也不管哪里有重复元素,还拿[2, 3, 3, 3, 6, 9, 9]这个数组来说,去重是去掉后面出现的一样的元素,nextNonDuplicate 指向索引0,它肯定是第一次出现的,i指向位置为1的元素,然后判断nextNonDuplicate 位置的元素(即2)跟i指向的元素(即3)是否相等,当然不等,那i指向的元素放在nextNonDuplicate后面就不重复,那nextNonDuplicate+1 就该设置为i指向的元素(即3)(其实本来也就是3),但是在接着往下迭代,nextNonDuplicate 还指向索引为1的位置,而i继续寻找下一个不重复元素的索引,已经到了索引为3的位置,(因为nextNonDuplicate跟i指向的元素在这期间一直相等),直到i=4,再次出现不等的情况,我们给nextNonDuplicate+1所指的位置赋新值。,因为i>=nextNonDuplicate,我们不会漏掉任何元素,这样我们就能保证nextNonDuplicate指向的值,一定是第一次出现的元素。到最后结束,所有第一次出现的元素都被移动到了数组的左边,nextNonDuplicate指示的位置一定是当前最远的无重复元素的索引。

这两个例子应该能促使大家对双指针的技巧有所思考,大家千万要记住凡是题目中出现排序好的数组等字眼的,都可以试着用双指针去解决。

关注我,跟我一起讨论吧!