查找数组中每个大小为k的窗口的最大值

时间:2011-11-07 01:41:32

标签: algorithm

给定一个大小为n和k的数组,如何找到每个大小为k的连续子数组的最大值?

例如

arr = 1 5 2 6 3 1 24 7
k = 3
ans = 5 6 6 6 24 24

我正在考虑使用一个大小为k的数组,每一步都将最后一个元素逐出,并添加新元素并在其中找到最大值。它导致O(nk)的运行时间。有更好的方法吗?

24 个答案:

答案 0 :(得分:109)

你听说过使用dequeue在O(n)中做这件事。

这是O(n)中这个问题的一个众所周知的算法。

我说的方法很简单,需要动态编程,时间复杂度为O(n)。

Your Sample Input:
n=10 , W = 3

10 3
1 -2 5 6 0 9 8 -1 2 0

Answer = 5 6 6 9 9 9 8 2

概念:动态编程

<强>算法:

  1. N是数组中元素的数量,W是窗口大小。因此,窗口编号= N-W + 1
  2. 现在从索引1开始将数组分成W块。

    这里分成大小为'W'= 3的块。 对于您的样本输入:

    divided blocks

  3. 我们已经分为块,因为我们将以2种方式计算最大值A.)从左到右遍历B.)从右到左遍历。 但是如何??

  4. 首先,从左到右穿越。对于块中的每个元素ai,我们将找到最大值,直到元素ai从块的START开始到该块的END。 所以在这里,

    LR

  5. 其次,从右到左穿越。对于块中的每个元素'ai',我们将找到最大值,直到元素'ai'从块的END开始到该块的START。 所以,

    RL

  6. 现在我们必须找到大小为'W'的每个子阵列或窗口的最大值。 所以,从index = 1到index = N-W + 1。

    <强> max_val[index] = max(RL[index], LR[index+w-1]);

    LR + RL

     for index=1: max_val[1] = max(RL[1],LR[3]) = max(5,5)= 5
    
  7.   

    Simliarly,对于所有索引i(i<=(n-k+1))RL[i]LR[i+w-1]的值   比较,这两个中的最大值是该子阵列的答案。

    所以最终答案:5 6 6 9 9 9 8 2

    时间复杂度:O(n)

    实施代码:

    // Shashank Jain
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    
    #define LIM 100001 
    
    using namespace std;
    
    int arr[LIM]; // Input Array
    int LR[LIM]; // maximum from Left to Right
    int RL[LIM]; // maximum from Right to left
    int max_val[LIM]; // number of subarrays(windows) will be n-k+1
    
    int main(){
        int n, w, i, k; // 'n' is number of elements in array
                        // 'w' is Window's Size 
        cin >> n >> w;
    
        k = n - w + 1; // 'K' is number of Windows
    
        for(i = 1; i <= n; i++)
            cin >> arr[i];
    
        for(i = 1; i <= n; i++){ // for maximum Left to Right
            if(i % w == 1) // that means START of a block
                LR[i] = arr[i];
            else
                LR[i] = max(LR[i - 1], arr[i]);        
        }
    
        for(i = n; i >= 1; i--){ // for maximum Right to Left
            if(i == n) // Maybe the last block is not of size 'W'. 
                RL[i] = arr[i]; 
            else if(i % w == 0) // that means END of a block
                RL[i] = arr[i];
            else
                RL[i] = max(RL[i+1], arr[i]);
        }
    
        for(i = 1; i <= k; i++)    // maximum
            max_val[i] = max(RL[i], LR[i + w - 1]);
    
        for(i = 1; i <= k ; i++)
            cout << max_val[i] << " ";
    
        cout << endl;
    
        return 0;
    }  
    

    Running Code Link


    我会尝试证明:(来自@ johnchen902)

    如果k % w != 1k不是阻止的开头)

    Let k* = The begin of block containing k
    ans[k] = max( arr[k], arr[k + 1], arr[k + 2], ..., arr[k + w - 1])
           = max( max( arr[k],  arr[k + 1],  arr[k + 2],  ..., arr[k*]), 
                  max( arr[k*], arr[k* + 1], arr[k* + 2], ..., arr[k + w - 1]) )
           = max( RL[k], LR[k+w-1] )
    

    否则(k是块的开头)

    ans[k] = max( arr[k], arr[k + 1], arr[k + 2], ..., arr[k + w - 1])
           = RL[k] = LR[k+w-1]
           = max( RL[k], LR[k+w-1] )
    

答案 1 :(得分:15)

Shashank Jain非常巧妙地解释了动态编程方法。我想用dequeue解释如何做同样的事情。 关键是要将max元素保持在队列顶部(对于一个窗口)并丢弃无用元素,我们还需要丢弃当前窗口索引之外的元素。 /> 无用的元素 = 如果当前元素大于队列的最后一个元素,那么队列的最后一个元素是无用的。
注意:我们将索引存储在队列中而不是元素本身。代码本身会更清楚。
1.如果Current元素大于队列的最后一个元素,则队列的最后一个元素是无用的。我们需要删除最后一个元素。 (并继续删除,直到队列的最后一个元素小于当前元素) 2.如果current_index - k&gt; = q.front()表示我们要离开窗口,那么我们需要从队列前面删除该元素。

vector<int> max_sub_deque(vector<int> &A,int k)
{
    deque<int> q;
    for(int i=0;i<k;i++)
    {
        while(!q.empty() && A[i] >= A[q.back()])
            q.pop_back();
        q.push_back(i);
    }
    vector<int> res;
    for(int i=k;i<A.size();i++)
    {
        res.push_back(A[q.front()]);
        while(!q.empty() && A[i] >= A[q.back()] )
            q.pop_back();
        while(!q.empty() && q.front() <= i-k)
            q.pop_front();
        q.push_back(i); 
    }
    res.push_back(A[q.front()]);
    return res;
}


由于每个元素排队并且最多出现1次,因此复杂度为:O(n + n)= O(2n)= O(n)。
并且队列的大小不能超过限制k。所以空间复杂度= O(k)。

答案 2 :(得分:5)

您需要一个快速的数据结构,可以在少于O(n)的时间内添加,删除和查询max元素(如果可以接受O(n)或O(nlogn),则可以使用数组)。您可以使用堆,平衡二进制搜索树,跳过列表或在O(log(n))中执行这些操作的任何其他已排序数据结构。

好消息是,大多数流行语言都有一个已实现的排序数据结构,可以为您支持这些操作。 C ++有std::setstd::multiset(你可能需要后者),Java有TreeSet

答案 3 :(得分:3)

使用堆(或树),您应该能够在O(n * log(k))中执行此操作。我不确定这是否会更好。

答案 4 :(得分:3)

这是java实现

public static Integer[] maxsInEveryWindows(int[] arr, int k) {
    Deque<Integer> deque = new ArrayDeque<Integer>();
    /* Process first k (or first window) elements of array */
    for (int i = 0; i < k; i++) {
        // For very element, the previous smaller elements are useless so
        // remove them from deque
        while (!deque.isEmpty() && arr[i] >= arr[deque.peekLast()]) {
            deque.removeLast(); // Remove from rear
        }
        // Add new element at rear of queue
        deque.addLast(i);
    }
    List<Integer> result = new ArrayList<Integer>();
    // Process rest of the elements, i.e., from arr[k] to arr[n-1]
    for (int i = k; i < arr.length; i++) {
        // The element at the front of the queue is the largest element of
        // previous window, so add to result.
        result.add(arr[deque.getFirst()]);
        // Remove all elements smaller than the currently
        // being added element (remove useless elements)
        while (!deque.isEmpty() && arr[i] >= arr[deque.peekLast()]) {
            deque.removeLast();
        }
        // Remove the elements which are out of this window
        while (!deque.isEmpty() && deque.getFirst() <= i - k) {
            deque.removeFirst();
        }
        // Add current element at the rear of deque
        deque.addLast(i);
    }
    // Print the maximum element of last window
    result.add(arr[deque.getFirst()]);

    return result.toArray(new Integer[0]);
}

以下是相应的测试用例

@Test
public void maxsInWindowsOfSizeKTest() {
    Integer[] result = ArrayUtils.maxsInEveryWindows(new int[]{1, 2, 3, 1, 4, 5, 2, 3, 6}, 3);
    assertThat(result, equalTo(new Integer[]{3, 3, 4, 5, 5, 5, 6}));

    result = ArrayUtils.maxsInEveryWindows(new int[]{8, 5, 10, 7, 9, 4, 15, 12, 90, 13}, 4);
    assertThat(result, equalTo(new Integer[]{10, 10, 10, 15, 15, 90, 90}));
}

答案 5 :(得分:2)

方法1:O(n)时间,O(k)空间
我们使用双端队列(它像一个列表,但两端都有固定的插入和删除时间)来存储有用元素的 index
当前最大值的索引保留在双端队列的最左侧。双端队列的最右边元素最小。 在下文中,为了便于说明,我们说数组中的元素在双端队列中,而实际上该元素的索引在双端队列中。

假设{5,3,2}已经在双端队列中了(同样,如果事实上它们的索引在)。

  • 如果我们从数组中读取的下一个元素大于5(请记住,双端队列的最左边的元素保留最大值),则说7:我们删除双端队列并创建一个新的其中只有7的元素(之所以这样做,是因为当前元素没有用,我们找到了一个新的最大值。

  • 如果下一个元素小于2(这是双端队列的最小元素),请说1:我们将其添加到右侧({5,3,2,1})

  • 如果下一个元素大于2但小于5,则说4:我们从右边删除小于该元素的元素,然后从右边添加元素({5,4})。

  • p>
  • 我们也仅保留当前窗口的元素(由于存储索引而不是元素,我们可以在固定时间内执行此操作)。

from collections import deque

def max_subarray(array, k):
  deq = deque()

  for index, item in enumerate(array):

    if len(deq) == 0:
      deq.append(index)

    elif index - deq[0] >= k:  # the max element is out of the window
      deq.popleft()

    elif item > array[deq[0]]:  # found a new max
      deq = deque()
      deq.append(index)

    elif item < array[deq[-1]]:  # the array item is smaller than all the deque elements
      deq.append(index)

    elif item > array[deq[-1]] and item < array[deq[0]]:
      while item > array[deq[-1]]:
        deq.pop()
      deq.append(index)

    if index >= k - 1:  # start printing when the first window is filled
      print(array[deq[0]])

O(n)时间的证明:我们需要检查的唯一部分是while循环。 在代码的整个运行过程中,while循环总共最多可以执行O(n)个操作。原因是while循环从双端队列弹出元素,并且由于在代码的其他部分中,我们最多对双端队列进行O(n)插入,因此while循环总共不能超过O(n)个操作。因此,总运行时间为O(n)+ O(n)= O(n)


方法2:O(n)时间,O(n)空间
这是对S Jain建议的方法的解释(正如他的帖子中提到的那样,该方法不适用于数据流,大多数滑动窗口问题都是针对此数据流而设计的)。

使用以下示例说明方法起作用的原因:

array = [5, 6, 2, 3, 1, 4, 2, 3]
k = 4

    [5, 6, 2, 3   1, 4, 2, 3 ]
LR:  5  6  6  6   1  4  4  4 
RL:  6  6  3  3   4  4  3  3  
     6  6  4  4   4  

要获取窗口[2,3,1,4]的最大值, 我们可以获得[2,3]的最大值和[1,4]的最大值,然后返回两者中的较大者。 [2,3]的最大值在RL传递中计算,[1,4]的最大值在LR传递中计算。

答案 6 :(得分:0)

100% 工作测试 (Swift)

 func maxOfSubArray(arr:[Int],n:Int,k:Int)->[Int]{
        var lenght = arr.count
        var resultArray = [Int]()
        for i in 0..<arr.count{
            if lenght+1 > k{
           let tempArray =  Array(arr[i..<k+i])
                resultArray.append(tempArray.max()!)
            }
            lenght = lenght - 1
        }
        print(resultArray)
    return resultArray
    }

这样我们就可以使用:

maxOfSubArray(arr: [1,2,3,1,4,5,2,3,6], n: 9, k: 3)

结果:

[3, 3, 4, 5, 5, 5, 6]

答案 7 :(得分:0)

创建一个大小为 k 的 TreeMap。将前 k 个元素作为键放入其中并分配任何值,例如 1(无关紧要)。 TreeMap 具有根据键对元素进行排序的属性,因此现在,地图中的第一个元素将是 min,最后一个元素将是 max 元素。然后从映射中删除 arr 中索引为 i-k 的 1 个元素。在这里,我已经考虑到 Input 元素是在数组 arr 中获取的,我们正在从该数组中填充大小为 k 的映射。因为,我们不能对 TreeMap 内部发生的排序做任何事情,因此这种方法也需要 O(n) 时间。

答案 8 :(得分:0)

我们可以使用Python进行切片来解决它。

def sliding_window(a,k,n):
  max_val =[]
  val =[]
  val1=[]

for i in range(n-k-1):
    if i==0:
        val = a[0:k+1]
        print("The value in val variable",val)
        val1 = max(val)
        max_val.append(val1)
    else:
        val = a[i:i*k+1]
        val1 =max(val)
        max_val.append(val1)
return max_val

驱动程序代码

a = [15,2,3,4,5,6,2,4,9,1,5]
n = len(a)
k = 3
sl=s liding_window(a,k,n)


print(sl)

答案 9 :(得分:0)

您可以使用Deque数据结构来实现此目的。 Deque具有独特的功能,您可以从队列的两端插入和删除元素,而传统的队列只能从一端插入并从另一端删除。

以下是上述问题的代码。

    public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    int[] maxInWindow = new int[n - k + 1];
    Deque<Integer> dq = new LinkedList<Integer>();
    int i = 0;
    for(; i<k; i++){
        while(!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]){
            dq.removeLast();
        }
        dq.addLast(i);
    }
    for(; i <n; i++){
        maxInWindow[i - k] = nums[dq.peekFirst()];
        while(!dq.isEmpty() && dq.peekFirst() <= i - k){
            dq.removeFirst();
        }
        
        while(!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]){
            dq.removeLast();
        }
        dq.addLast(i);
    }
    maxInWindow[i - k] = nums[dq.peekFirst()];
    return maxInWindow;
}

结果数组将具有n-k + 1个元素,其中n是给定数组的长度,k是给定的窗口大小。

答案 10 :(得分:0)

这是我想到的天真的(条件)嵌套循环方法的优化版本,它快得多,并且不需要任何辅助存储或数据结构 。 当程序从一个窗口移到另一个窗口时,开始索引和结束索引向前移动1。换句话说,两个连续的窗口具有相邻的开始索引和结束索引。

对于第一个大小为W的窗口,内部循环查找索引为(0到W-1)的元素的最大值。 (因此,在代码的第4行中,i == 0位于if中)。 现在,不用计算仅包含一个新元素的第二个窗口,因为我们已经计算了索引0到W-1的元素的最大值,所以我们只需要将此最大值与新元素中唯一的新元素进行比较索引为W的窗口

但是,如果0处的元素是最大值,这是不属于新窗口的唯一元素,则我们需要使用内部循环从1到W的内部循环再次计算最大值(因此第二个条件{{ 1}}(如果在第4行中),否则只需比较前一个窗口的最大值和新窗口中唯一的新元素。

maxm == arr[i-1]

答案 11 :(得分:0)

两种方法。

  1. 段树O(nlog(n-k))
    • 构建maximum段树。
    • 在[i,i + k)之间进行查询

类似..

public static void printMaximums(int[] a, int k) {
    int n = a.length;
    SegmentTree tree = new SegmentTree(a);
    for (int i=0; i<=n-k; i++) System.out.print(tree.query(i, i+k));
}
  1. 双端队列O(n)
    • 如果下一个元素大于rear element,请移除后部元素。
    • 如果双端队列front中的元素不在窗口内,请删除前面的元素。
public static void printMaximums(int[] a, int k) {
     int n = a.length;
     Deque<int[]> deck = new ArrayDeque<>();
     List<Integer> result = new ArrayList<>();
     for (int i=0; i<n; i++) {
         while (!deck.isEmpty() && a[i] >= deck.peekLast()[0]) deck.pollLast();
         deck.offer(new int[] {a[i], i});
         while (!deck.isEmpty() && deck.peekFirst()[1] <= i - k) deck.pollFirst();
         if (i >= k - 1) result.add(deck.peekFirst()[0]);
     }
     System.out.println(result);
}

答案 12 :(得分:0)

arr = [1, 2, 3, 1, 4, 5, 2, 3, 6]
k = 3
for i in range(len(arr)-k):
  k=k+1
  print (max(arr[i:k]),end=' ') #3 3 4 5 5 5 6

答案 13 :(得分:0)

这是O(1)中的Python实现...感谢@Shahshank Jain。

from sys import stdin,stdout
from operator import *
n,w=map(int , stdin.readline().strip().split())
Arr=list(map(int , stdin.readline().strip().split()))
k=n-w+1           # window size = k
leftA=[0]*n
rightA=[0]*n
result=[0]*k
for i in range(n):
    if i%w==0:
        leftA[i]=Arr[i]
    else:
        leftA[i]=max(Arr[i],leftA[i-1])
for i in range(n-1,-1,-1):
    if i%w==(w-1) or i==n-1:
        rightA[i]=Arr[i]
    else:
        rightA[i]=max(Arr[i],rightA[i+1])

for i in range(k):
    result[i]=max(rightA[i],leftA[i+w-1])
print(*result,sep=' ')

答案 14 :(得分:0)

arr = 1 5 2 6 3 1 24 7

我们必须找到子数组的最大值,对吗? 那么,什么是子数组? SubArray = Partial set,它应该是有序的和连续的。

从上面的数组 {1,5,2} {6,3,1} {1,24,7}都是子数组示例

n = 8 // Array length
k = 3 // window size

要找到最大值,我们必须遍历数组,并找到最大值。 在窗口大小k中,

{1,5,2} = 5 is the maximum
{5,2,6} = 6 is the maximum
{2,6,3} = 6 is the maximum
and so on..
ans = 5 6 6 6 24 24

它可以被评估为 n-k + 1 因此,8-3 + 1 = 6 正如我们所见,答案的长度是6。

我们现在如何解决这个问题? 当数据从管道中移出时,首先想到的是数据结构是 Queue

但是,相反,我们在这里讨论不多,我们直接跳到 deque

Thinking Would be: 
Window is fixed and data is in and out 
Data is fixed and window is sliding 
EX: Time series database 

While (Queue is not empty and arr[Queue.back() < arr[i]] { 
Queue.pop_back(); 
Queue.push_back(); 

其余:

打印队列的前面

// purged expired element 
While (queue not empty and queue.front() <= I-k) { 
  Queue.pop_front(); 
  While (Queue is not empty and arr[Queue.back() < arr[i]] { 
  Queue.pop_back(); 
  Queue.push_back(); 
  }
}

答案 15 :(得分:0)

    static void countDistinct(int arr[], int n, int k) 
    { 
        System.out.print("\nMaximum integer in the window : ");
        // Traverse through every window
        for (int i = 0; i <= n - k; i++) {
               System.out.print(findMaximuminAllWindow(Arrays.copyOfRange(arr, i, arr.length), k)+ " ");
           }
    } 

    private static int findMaximuminAllWindow(int[] win, int k) {
        // TODO Auto-generated method stub
        int max= Integer.MIN_VALUE;

        for(int i=0; i<k;i++) {
            if(win[i]>max)
                max=win[i];
        }

        return max;
    }

答案 16 :(得分:0)

package com;
public class SlidingWindow {

    public static void main(String[] args) {

        int[] array = { 1, 5, 2, 6, 3, 1, 24, 7 };
        int slide = 3;//say
        List<Integer> result = new ArrayList<Integer>();

        for (int i = 0; i < array.length - (slide-1); i++) {
            result.add(getMax(array, i, slide));

        }
        System.out.println("MaxList->>>>" + result.toString());
    }

    private static Integer getMax(int[] array, int i, int slide) {

        List<Integer> intermediate = new ArrayList<Integer>();
        System.out.println("Initial::" + intermediate.size());
        while (intermediate.size() < slide) {
            intermediate.add(array[i]);
            i++;
        }
        Collections.sort(intermediate);
        return intermediate.get(slide - 1);
    }
}

答案 17 :(得分:0)

class MaxFinder
{
    // finds the max and its index
    static int[] findMaxByIteration(int arr[], int start, int end)
    {
        int max, max_ndx; 

        max = arr[start];
        max_ndx = start;
        for (int i=start; i<end; i++)
        {
            if (arr[i] > max)
            {
                max = arr[i];
                max_ndx = i;
            }    
        }

        int result[] = {max, max_ndx};

        return result;
    }

    // optimized to skip iteration, when previous windows max element 
    // is present in current window
    static void optimizedPrintKMax(int arr[], int n, int k)
    {
        int i, j, max, max_ndx;

        // for first window - find by iteration.    
        int result[] = findMaxByIteration(arr, 0, k);

        System.out.printf("%d ", result[0]);

        max = result[0];
        max_ndx = result[1];   

         for (j=1; j <= (n-k); j++)
         {
            // if previous max has fallen out of current window, iterate and find
            if (max_ndx < j)  
            {
                result = findMaxByIteration(arr, j, j+k);
                max = result[0];
                max_ndx = result[1];   
            } 
            // optimized path, just compare max with new_elem that has come into the window 
            else 
            {
                int new_elem_ndx = j + (k-1);
                if (arr[new_elem_ndx] > max)
                {
                    max = arr[new_elem_ndx];
                    max_ndx = new_elem_ndx;
                }      
            }

            System.out.printf("%d ", max);
         }   
    }     

    public static void main(String[] args)
    {
        int arr[] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
        //int arr[] = {1,5,2,6,3,1,24,7};
        int n = arr.length;
        int k = 3;
        optimizedPrintKMax(arr, n, k);
    }
}    

答案 18 :(得分:0)

很抱歉,这应该是评论,但我暂时不能发表评论。 @leo和@Clay Goddard 您可以免于重新计算storing both maximum and 2nd maximum of the window in the beginning的最大值 (仅当初始窗口中有两个最大值时,第二个最大值才是最大值)。如果最大滑出窗口,您仍然有下一个与新条目进行比较的最佳候选者。所以你得到O(n),否则如果你再次允许整个重新计算,最坏的情况顺序是O(nk),k就是窗口大小。

答案 19 :(得分:0)

使用Fibonacci堆,您可以在O(n + (n-k) log k)中执行此操作,对于小O(n log k),等于k,对于kn接近O(n) n

算法:实际上,你需要:

  • n-k插入堆
  • n-k删除
  • O(1) findmax's

How much these operations cost in Fibonacci heaps? Insert和findmax被O(log n)摊销,删除被O(n + (n-k) log k + (n-k)) = O(n + (n-k) log k) 摊销。所以,我们有

{{1}}

答案 20 :(得分:-2)

请注意,您只需在新窗口中找到以下内容: *窗口中的新元素小于前一个元素(如果它更大,那么肯定是这个元素)。 要么 *刚刚弹出窗口的元素是当前更大的元素。

在这种情况下,请重新扫描窗口。

答案 21 :(得分:-3)

摊销常数O(1)复杂性的完整工作解决方案。 https://github.com/varoonverma/code-challenge.git

答案 22 :(得分:-3)

有多大k?对于合理大小的k。你可以创建k k大小的缓冲区,只是遍历数组,跟踪缓冲区中的最大元素指针 - 不需要数据结构,并且是O(n)k ^ 2预分配。

答案 23 :(得分:-6)

比较前k个元素并找到最大值,这是你的第一个数字

然后将下一个元素与之前的最大值进行比较。如果下一个元素更大,那就是下一个子数组的最大值,如果它等于或小于,则该子数组的最大值是相同的

然后转到下一个数字

max(1 5 2) = 5
max(5 6) = 6
max(6 6) = 6
... and so on
max(3 24) = 24
max(24 7) = 24

它只比你的回答略胜一筹