在滑动窗口中查找第二大元素

时间:2019-06-23 01:49:05

标签: algorithm dynamic-programming sliding-window

因此,给定一个数组和一个窗口大小,我需要在每个窗口中找到第二大窗口。蛮力解决方案非常简单,但是我想使用动态编程找到一种有效的解决方案

当我在大型阵列上尝试时,蛮力解决方案会超时,因此我需要找到一个更好的解决方案。我的解决方案是通过对每个滑动窗口进行排序并获取第二个元素来找到第二大窗口,我知道某些数据结构可以更快地排序,但是我想知道是否有更好的方法。

4 个答案:

答案 0 :(得分:2)

有很多方法可以解决此问题。这里有几个选择。在接下来的内容中,我将用n表示输入数组中的元素数,并且w是窗口大小。

选项1:一种简单的O(n log w)时间算法

一种选择是维护一个平衡的二进制搜索树,其中包含当前窗口中的所有元素,包括重复元素。由于此窗口中总共只有w个元素,因此在此BST中插入某些内容将花费时间O(log 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)时间。

有趣的事实-如果您查看此队列实现在此特定用例中实际执行的操作,则会发现它与上述策略完全等效。一个堆栈对应于前一个块的后缀,另一个对应于下一个块的前缀。

这最后一个策略是我个人的最爱,但是请记住,这只是我自己的数据结构偏见。

希望这会有所帮助!

答案 1 :(得分:1)

因此,只需像设置那样按顺序存储数据的数据结构即可。 就像您在集合上存储4 2 6一样,它将存储为2 4 6。

那么算法是什么

让我们

Array = [12,8,10,11,4,5] 窗口大小= 4

第一个窗口= [12,8,10,11] 设置= [8,10,11,12]

如何获得第二高的成绩:
  -从集合中删除最后一个元素,并将其存储在容器中。 set = [8,10,11],contaniner = 12
  -移除后,集合中的当前最后一个元素是当前窗口的第二大元素。
  -再次将存储在容器中的已删除元素放入集合set = [8,10,11,12]
现在移开窗户,    -从集合中删除12,然后添加4。
   -现在您将获得新窗口并进行设置。
   -检查类似过程。
在集合中删除和添加元素的复杂度约为log(n)。

一个技巧:

如果您始终要按降序存储数据,则可以将数据乘以-1来存储数据。弹出数据时,将其乘以-1就可以使用它。

答案 2 :(得分:1)

我们可以将双头队列用于O(n)解决方案。队列的前部将具有较大的元素(并且较早出现):

Knapsack { 


static int max(int a, int b)  { return (a > b) ? a : b; } 


static int knapSack(int W, int wt[], int val[], int n) 
{ 
    int i, w; 
    int K[][] = new int[n + 1][W + 1]; 

    // Build table K[][] in bottom up manner 
    for (i = 0; i<= n; i++) { 
        for (w = 0; w<= W; w++) { 
            if (i == 0 || w == 0) 
                K[i][w] = 0; 
            else if (wt[i - 1]<= w) 
                K[i][w] = max(val[i - 1] + K[i - 1][w - wt[i - 1]], K[i - 1][w]); 
            else
                K[i][w] = K[i - 1][w]; 
        } 
    } 

    return K[n][W]; 
} 


public static void main(String args[]) 
{ 
    int val[] = new int[] { 60, 100, 120 }; 
    int wt[] = new int[] { 10, 20, 30 }; 
    int W = 50; 
    int n = val.length; 
    System.out.println(knapSack(W, wt, val, n)); 

     } 

  } 

“弹出”和“从前面删除”分别为 0 1 2 3 4 5 {12, 8,10,11, 4, 5} window size: 3 i queue (stores indexes) - ----- 0 0 1 1,0 2 2,0 (pop 1, then insert 2) output 10 remove 0 (remove indexes not in the next window from the front of the queue.) 3 3 (special case: there's only one smaller element in queue, which we need so keep 2 as a temporary variable.) output 10 4 4,3 output 10 remove 2 from temporary storage 5 5,3 (pop 4, insert 5) output 5 while A[queue_back] <= A[i](尽管队列中只剩下一个较小元素的复杂性)。我们从队列的前面输出由第二个元素索引的数组元素(尽管我们的前面可能也有一个特殊的临时朋友,该朋友也曾经在前面;该特殊朋友一旦代表了一个位于外部的元素,就会被转储小于窗口的大小或小于从前面的第二个队列元素索引的元素)。双头队列的复杂度O(1)要从正面或背面移除。我们只在后面插入。

每个templatetypedef在注释中的请求:“如何确定要使用的队列操作?在每次迭代中,使用索引while queue_front is outside next window,在将其插入队列之前,我们(1)从队列的后面弹出表示数组中小于或等于i的元素的每个元素, (2)从队列的开头移除当前窗口之外的索引中的每个元素。 (如果在(1)期间只剩下一个较小或相等的元素,由于它是当前的第二大元素,我们将其保存为临时变量。)

答案 3 :(得分:0)

有一个相对简单的动态编程O(n^2)解决方案: 构建经典的金字塔结构,以在子集(将下面对中的值组合成上面的每一步的位置)上汇总值,在其中跟踪最大的2个值(及其位置),然后仅保留最大的2个值4个组合值中的1个(实际上由于重叠而较少),请使用位置来确保它们实际上是不同的。然后,您只需从具有正确滑动窗口大小的图层中读取第二个最大值即可。