有很多方法可以解决这个问题。这里有几个选项。接下来,我将让 n 表示输入数组中的元素个数,w 表示窗口大小。
选项 1:一个简单的 O(n log w) 时间算法
一种选择是维护一个平衡的二叉搜索树,其中包含当前窗口中的所有元素,包括重复项。向这个 BST 中插入一些东西需要时间 O(log w),因为窗口中只有 w 个元素,出于同样的原因,删除一个元素也需要时间 O(log w)。这意味着将窗口滑动一个位置需要时间 O(log w)。
要在窗口中查找第二大元素,您只需要应用 standard algorithm for finding the second-largest element in a BST,这在具有 w 个元素的 BST 中需要时间 O(log w)。
这种方法的优势在于,在大多数编程语言中,编写这个代码相当简单。它还利用了许多众所周知的标准技术。缺点是运行时不是最优的,我们可以对其进行改进。
选项 2:O(n) 前缀/后缀算法
这是一个相对容易实施的线性时间解决方案。在高层次上,该解决方案通过将数组拆分为一系列块来工作,每个块的大小为 w。例如,考虑以下数组:
31 41 59 26 53 58 97 93 23 84 62 64 33 83 27 95 02 88 41 97
假设 w = 5。我们将数组拆分为大小为 5 的块,如下所示:
31 41 59 26 53 | 58 97 93 23 84 | 62 64 33 83 27 | 95 02 88 41 97
现在,想象一下在这个数组的某处放置一个长度为 5 的窗口,如下所示:
31 41 59 26 53 | 58 97 93 23 84 | 62 64 33 83 27 | 95 02 88 41 97
|-----------------|
请注意,此窗口将始终由一个块的后缀和另一个块的前缀组成。这很好,因为它允许我们解决一个稍微简单的问题。想象一下,不知何故,我们可以有效地确定任何块的任何前缀或后缀中的两个最大值。然后我们可以在任何窗口中找到第二个最大值,如下所示:
- 找出窗口对应的块的前缀和后缀。
- 从每个前缀和后缀中获取前两个元素(如果窗口足够小,则仅获取前一个元素)。
- 在这(最多)四个值中,确定第二大的值并返回。
通过一点预处理,我们确实可以设置我们的窗口来回答“每个后缀中最大的两个元素是什么?”形式的查询。和“每个前缀中最大的两个元素是什么?”您可以将其视为一个动态规划问题,设置如下:
- 对于任何长度为 1 的前缀/后缀,将单个值存储在该前缀/后缀中。
- 对于长度为 2 的任何前缀/后缀,前两个值是两个元素本身。
- 对于任何较长的前缀或后缀,可以通过将较小的前缀或后缀扩展单个元素来形成该前缀或后缀。要确定该较长前缀/后缀的前两个元素,请将用于扩展范围的元素与前两个元素进行比较,然后选择该范围之外的前两个。
请注意,填写每个前缀/后缀的前两个值需要时间 O(1)。这意味着我们可以在 O(w) 时间内填写任何窗口,因为有 w 个条目要填写。此外,由于总共有 O(n / w) 个窗口,因此填写这些条目所需的总时间是 O (n),所以我们的整体算法运行时间为 O(n)。
至于空间使用:如果您急切地计算整个数组中的所有前缀/后缀值,则需要使用空间 O(n) 来保存所有内容。但是,由于在任何时候我们只关心两个窗口,因此您也可以只在需要时计算前缀/后缀。这将只需要 O(w) 空间,这非常非常好!
选项 3:使用智能数据结构的 O(n) 时间解决方案
最后一种方法与上述方法完全等价,但框架不同。
可以build a queue that allows for constant-time querying of its maximum element。这个队列背后的想法 - 从a stack that supports efficient find-max 开始,然后在双栈队列构造中使用它 - 可以很容易地推广到构建一个队列,该队列提供对第二大元素的恒定时间访问。为此,您只需调整堆栈结构以存储每个时间点的前两个元素,而不仅仅是最大的元素。
如果你有一个这样的队列,在任何窗口中找到第二个最大值的算法都非常快:用前 w 个元素加载队列,然后重复出队一个元素(将一些东西移出窗口)并将下一个元素排入队列(将某些东西移入窗口)。这些操作中的每一个都需要平均 O(1) 时间才能完成,因此总体上需要 O(n) 时间。
有趣的事实 - 如果您查看此队列实现在此特定用例中的实际作用,您会发现它完全等同于上述策略。一个栈对应上一个块的后缀,另一个对应下一个块的前缀。
最后一个策略是我个人最喜欢的,但不可否认,这只是我自己的数据结构偏见。
希望这会有所帮助!