解析滑动窗口

524 阅读4分钟

最近为了备战面试,一直在刷LeetCode,虽然我自认为我刷题速度还挺快的,但没想到题目增加速度也挺快的。想刷遍所有题目是不可能的了。所幸在思考的过程中,我发现很多题目有些共同点,而这些共同点往往到最后指引我使用类似的解题方法。有了这些规律,即便出现再多的没见过的题目,我都不慌张,只要冷静,题目总能以这样那样的方式转化成自己熟悉的题目求解。

今天我就来跟大家聊聊我遇到的最多的题型之一——滑动窗口。

在有些题目里面,给定一个数组或者链表,我们要找到或者计算出关于一个给定大小的连续子集的某些东西,比如给一个数组,求所有连续K个元素构成的子集的平均数。我们通过一个实际的例子来理解这道题:

Array: [1, 3, 2, 6, -1, 4, 1, 8, 2], K=5

在这里,我们要计算出这个数组所有连续5个元素构成的子集的平均数,我们来列举一下:

  1. 对于从索引0-4这5个元素,平均数是:(1+3+2+6−1)/5=>2.2
  2. 接下来5个数的平均数(索引1-5):(3+2+6−1+4)/5=>2.8
  3. 再接下来(索引2-6): (2+6-1+4+1)/5 => 2.4 ... 最终我们就可以得到如下结果:
[2.2, 2.8, 2.4, 3.6, 2.8]

那么代码该怎么写呢,我们先来尝试一下暴力解法:

public static double[] findAverages(int K, int[] arr) {
        double[] result = new double[arr.length - K + 1];
        for (int i = 0; i <= arr.length - K; i++) {
            // 找到下K个连续元素之和
            double sum = 0;
            for (int j = i; j < i + K; j++)
                sum += arr[j];
            result[i] = sum / K; // 计算平均数
        }
        return result;
    }

我们稍微分析一下这个解法的复杂度,我们要迭代每一个元素,每迭代一个元素就要计算从它开始的K个元素之和,那我们这里时间复杂度就是O(N*K),这个N是输入数组的长度。

通常暴力方法都是可以被优化的,一般通过减少重复跟避免低效计算等手段。我们仔细观察一下,我们在计算K个数之和的时候,每次都把这K个数相加,比如上面的例子,K=5,1+3+2+6−1与3+2+6−1+4,这样会有一个问题,前5个数跟当前5个数其实只有1个数字不一样,而我们把那些重叠的部分计算了两次!

重叠部分

如果想利用两次计算之间重叠的部分,我们只需要把前面计算结果减去1,然后加上4,就能得到当前需要计算的结果了。这其实就是滑动窗口的一个基本思想了,我们减1加4就是为了保证计算的是符合要求的K的元素的和。我们想象一个大小为K的窗口,它在给定数组上依次移动,不管怎么移动我们只能有K个元素,每次移动会导致移动前窗口内的第一个元素被踢出去,而移动前窗口后面的第一个元素就会被加进来。这样我们就可以利用前面计算的结果了,同时也因为不用再用循环迭代这K个数计算和,我们把时间复杂度优化到了一个线性的阶段:O(n)。

我们来看看优化后的代码:

public static double[] findAverages(int K, int[] arr) {
        double[] result = new double[arr.length - K + 1];
        double windowSum = 0;
        int windowStart = 0;
        for (int windowEnd = 0; windowEnd < arr.length; windowEnd++) {
            windowSum += arr[windowEnd]; // 加上这个元素
            // 如果还没达到K个元素这个要求我们就不用向后滑动
            if (windowEnd >= K - 1) {
                result[windowStart] = windowSum / K; // 计算平均数
                windowSum -= arr[windowStart]; // 减去踢掉的元素
                windowStart++; // 将窗口向后滑动
            }
        }
        return result;
    }

好了,这就是滑动窗口的基本原理,这是一种自然而然的思想。这个窗口的大小也不都是固定的,有时候会按情况增大或减小(比如判断给定字符串中不包含重复字符的最长子串)。一般来说,当题目需要我们对数组或者链表的一些连续元素构成的子集做些什么的话,那基本上我们就可以判断用滑动窗口的思想来解决问题。只要根据题目要求滑动窗口,踢出加入元素,就能得解。

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