面试算法:用队列计算滑动窗口内的最大网络流量

更详细的讲解和代码调试演示过程,请参看视频
如何进入google,算法面试技能全面提升指南

如果你对机器学习感兴趣,请参看一下链接:
机器学习:神经网络导论

在网络流量的控制过程中,有时候需要找到从给定的某个时间点开始,往前倒退若干个时段内的最大网络流量m(t, w). 其中 t 是给定的某个时间节点,w就是滑动窗口大小,于是m表示的就是在时间段[t - w, t] 之间的最大网络流量。

举个例子,假设知道的滑动窗口大小w 为6,某个具体时刻的网络流量用(t,v)来表示,t表示时间点,v表示当时的网络流量,于是(3, 5) 就表示在时间点3时,网络流量是5个单位。如果有下面一系列的流量记录:

(1,10), (3,1), (5,4), (7,8),(9,3),(12,9)

那么m(12, 6) 就等于9, m(9, 6) 就等于8. 因为如果在时刻12,窗口大小为6时,落入这个时段的流量点有 (7,8), (9,3), (12,9) ,这三个流量点中,流量最大的就是时刻12,流量为9,所以m(12,6) 就等于9. 如果时刻点是9, 那么落入窗口大小为6的流量点有: (3,1), (5,4), (7,8), (9,3) 其中流量最大的时刻点是7,流量大小为8,所以m(9,6) 就等于8.

我们注意,流量记录中的时间点总是递增排列的。

问题是,给定流量点记录的数组A, 里面有n个流量点记录,同时给定滑动窗口的大小w, A[i] 的格式就是上面所说的流量记录点(t,v), A所存储的流量点,其中的时间点都是按升序排列的。要求设计一个有效算法,计算A的中每个时间点在滑动窗口范围内的最大网络流量。

解决这个问题的关键有两点:

一是,给定一个具体时间点和窗口大小,我们要快速找到这个范围内的所有时间点记录。

二是:在所有时间点记录中,快速找到流量最大的那个时间点。

根据以上两点,我们先看看最简单的做法,例如给定时间点是(12,9),根据第一步,我们找到在窗口范围内的时间点是(7,8), (9,3), (12,9). 根据第二点,我们得到其中网络流量最大的点是12,流量为9.

如果给定时间点是(9,3), 那么可以找到的处于窗口范围内的时间点是(3,1), (5,4), (7,8),(9,3), 于是我们找到流量最大的点是7,流量为8.

如果每个点我们都这么找,那么我们可以解决问题,但这么做效率显然不高,因为你在每个给定时间点时,总得通过遍历去找到给定范围内的时间点,然后再从中找到流量最大的那个点。

如果给定的记录点是数组A中的第n个元素,那么我们回退去寻找窗口期内的记录点,可能就得遍历n个元素,如果给定的记录点是数组A中第n-1个元素,那么我们需要往回遍历n-1个元素,于是上面做法的时间复杂度就是n + n-1 +…+1 = O(n^2)。

算法设计的根本问题是:有没有更快的做法?

我们在以前讨论过一个算法题叫:
计算堆栈当前元素的最大值

我们将依赖于当时研究过的算法,来解决本题。

对于给定时间点(12,9) 我们得到的窗口内时间点是(7,8), (9,3), (12,9),此时最大流量的时间点是12,流量是9.

给定时间点(9,3), 我们得到窗口内时间点是(3,1), (5,4), (7,8),(9,3),相较于上一个时间点,我们去掉了一个时间点(12,9), 新增了两个时间点(3,1), (5,4),我们能否在基于上一个时间点的基础上,快速的得到当前时间点在滑动窗口内的最大流量呢?

1,我们准备两个队列,一个队列叫maxQueue, 另一个队列叫workingQueue,这两个队列用于存储窗口期内的时间点。

2,用两个指针,start 和 end, end 指向当前时间点,start指向窗口期内的首个时间点,例如对于给定时间点(12,9) 及对应窗口期6 内的时间点集合(7,8), (9,3),(12, 9). 那么end指向点(12,9), start 指向(7,8).

3,然后从start开始,把start指向的时间点有条件的插入workingQueue的队列头,要求是,如果当前队列是空的,那么start指向的时间点直接加入队列,要不然,如果当前start指向的时间点,它的流量比workingQueue的尾结点流量大的话,就把start指向的节点加入队列头,要不然忽略start指向的时间点,让start加一,指向下一个时间点。

4,开始时,start 和 end 都指向A中最后一个时间点,然后让start 往前进,每前进一次,判断当前时间点十分在窗口范围内,如果当前时间点超出滑动窗口范围,这start往后退,用一个变量count来统计start往回退了几个时间点。

5,从start 开始,把时间节点依次根据步骤3加入队列,加入的节点数等于变量count.

6,执行步骤5时,如果有某个时间点的流量大于maxQueue尾结点的流量,那么把maxQueue里面的节点全部清空。

7,如果当前maxQueue队列为空,那么将workingQueue作为maxQueue

8,算法运行时,end先指向A中最后一个时间点, 执行完上面步骤后,maxQueue的头结点所对应的流量,就是end执行的时间点在滑动窗口内的最大流量。

9, 如果maxQueue的尾节点与当前end指向的节点相同,那么去掉maxQueue的尾节点,然后end和end同时往前挪动一个节点。

举个例子,假设A中存储的时间点为:
(1,10), (3,1), (5,4), (7,8),(9,3),(12,9)
start
end

一开始start和end 都指向最后一个节点,根据步骤3,start 往前挪:

(1,10), (3,1), (5,4), (7,8), (9,3), (12,9)
start end
start经过了三个节点,所以count的值为3.

根据步骤步骤3,从start开始后的count个变量加入workingQueue:

maxQueue: null
workingQueue: (7,8)->(12,9)

根据步骤7, 由于maxQueue为空,把workignQueue当做maxQueue:

maxQueue: (7,8)->(12,9)
workingQueue: null

根据步骤8,在时间点12,滑动窗口大小为6的范围内,最大网络流量是9.

接着执行步骤9,得到结果如下:
(1,10), (3,1), (5,4), (7,8), (9,3), (12,9)
start end

maxQueue: (7,8)
workingQueue: null

然后执行步骤2,得到结果如下:
(1,10), (3,1), (5,4), (7,8), (9,3), (12,9)
start end
count : 2
执行步骤3:
maxQueue: (7,8)
workingQueue: (3,1)->(5,4)

根据步骤8,maxQueue尾节点流量为8,于是当时间点是9,窗口大小为6时,最大流量是8.

继续执行步骤9,结果如下:
(1,10), (3,1), (5,4), (7,8), (9,3), (12,9)
start end

maxQueue: (7,8)
workingQueue: (3,1)->(5,4)

执行步骤2,结果如下:
(1,10), (3,1), (5,4), (7,8), (9,3), (12,9)
start end

count = 1;

执行步骤3,当我们把节点(1,10)加入working队列时,该节点的流量值大于maxQueue尾节点流量,于是把maxQueue清空:

maxQueue: null
workingQueue: (3,1)->(5,4)->(1,10)

根据步骤7,我们把workingQueue转换成maxQueue:

maxQueue: (3,1)->(5,4)->(1,10)
workingQueue: null

于是根据步骤8,当时间点是7时,在滑动窗口内的最大流量是10.

把上面步骤持续运行,知道得到所有节点在滑动窗口内的最大流量为止。

接下来我们证明算法是对的。

执行步骤2时,start往前走,每走一步就判断start指向的节点是否在end节点的窗口期内,所有当start停下来时,start 和 end之间的节点就是窗口期内的所有节点。

假设执行步骤2前,start 与 end间距离x个节点:
start |<- x ->| end
执行步骤2后,start前进了count个节点:
start|<- count ->|<- x ->| end

根据步骤3,我们在wokingQueue队列中的尾节点,其流量一定是从start开始的count个元素中流量最大的一个。

如果一开始时x是0,那么count就是end节点窗口期内的节点数,由于此时maxQueue是null,因此根据步骤7,我们把wokingQueue当做maxQueue,由于wokingQueue尾节点是窗口期内的流量最大点,那么执行步骤7后,maxQueue的尾节点的流量值就是窗口期内的最大流量值。

如果x不是0,那么maxQueue的尾节点流量值是从end节点开始,往前数x个节点所形成的集合中,流量值最大的那个节点。执行步骤3后workingQueue尾节点的流量值则是从start开始,往后数count个节点后,所形成集合中的流量最大值节点。

由于start 和 end 之间的节点形成了一个窗口期内的所有节点,而窗口期的流量值就是窗口期内流量最大的那个节点的流量值,显然这个节点要不在
|<- x ->| end 这后半部分,要不在 start |<- count ->| 的前半部分。如果在后半部分,那么最大流量值节点就是maxQueue的尾节点,如果在前半部分,那么workingQueue的尾节点就是最大流量值节点。

根据步骤6,如果最大值节点在前半部分,那么我们会把maxQueue情况,然后,根据步骤7,maxQueue清空后,我们把workingQueue变成maxQueue.

因此无论何种情况,执行完所有步骤后,maxQueue的尾巴节点的流量值就是当前end所指向节点在窗口期内的最大网络流量。

接下来看看算法效率,指针end会变量数组中每一个节点,指针start也会遍历每一个节点,当start前进后,会把它遍历过的每个节点加入队列,所以数组中每个节点最多被遍历三次,因此整个算法复杂度是O(n).

我们在算法中没有分配多余内存,所以算法的空间复杂度是O(1).

下面我们看看实现代码:

import java.util.ArrayList;


public class SlidingWindow {
    private ArrayList windowList ;
    private int start = 0, lastStart = 0;
    private int end = 0;
    private int count = 0;
    private ArrayList maxQueue = null;
    private ArrayList workingQueue = new ArrayList();
    private int windowSize = 0;

    public SlidingWindow(ArrayList winList, int size) throws Exception {
        this.windowList = winList;  
        this.start = this.end = windowList.size() - 1;
        this.windowSize = size;

        if (windowList.size() == 0 || size <= 0) {
            throw new Exception("Illegal arguments");
        }


    }

    public void printMaxVolumnForTimePoints() {
        while (end >= 0) {
            if (start <= end) {
                findSlidingWindow();
            }

            Window w = windowList.get(end);
            Window m = maxQueue.get(maxQueue.size() - 1);
            System.out.println("Max from from time: " + w.getTime() + " in sliding window size is " + m.getVolumn());

            if (w.equals(m)) {
                maxQueue.remove(maxQueue.size() - 1);
            }

            if (maxQueue.isEmpty()) {
                maxQueue = null;
            }

            end--;
        }
    }

    private void findSlidingWindow() {

        while (start >= 0 && (windowList.get(end).getTime() -  windowList.get(start).getTime() <= windowSize)) {
            if (maxQueue != null && windowList.get(start).getVolumn() > maxQueue.get(maxQueue.size() - 1).getVolumn()) {
                maxQueue.clear();
                maxQueue = null;
            }

            start--;
            count++;
        }

        if (start >= 0 && windowList.get(end).getTime() - windowList.get(start).getTime() > windowSize) {
            start++;
        }

        if (count > 0)
            buildMaxQueue();

        if (maxQueue == null) {
            maxQueue = workingQueue;
            workingQueue = new ArrayList();
        }
    }

    private void buildMaxQueue() {

        int s = start < 0 ? 0 : start;

        while (count > 0) {
            if (workingQueue.isEmpty() || 
                    windowList.get(s).getVolumn() > workingQueue.get(workingQueue.size() - 1).getVolumn() ) {
                workingQueue.add(windowList.get(s));
            }

            s++;
            count--;
        }

        start--;
    }
}

findSlidingWindow 执行的就是步骤2,3,4,它让start指针往前进,找到滑动窗口内的所有节点,然后调用buildMaxQueue来将节点加入workingQueue, buildMaxQueue 执行的是步骤3. printMaxVolumnForTimePoints 执行的是步骤7,8,9.
再看看入口处代码:

public class StackAndQuque {
    public static void main(String[] args) {

        ArrayList array = new ArrayList();
        array.add(new Window(1, 10));
        array.add(new Window(3,1));
        array.add(new Window(5,4));
        array.add(new Window(7,8));
        array.add(new Window(9,3));
        array.add(new Window(12,9));
        array.add(new Window(19, 4));

        try {
            SlidingWindow slidWin = new SlidingWindow(array, 6);
            slidWin.printMaxVolumnForTimePoints();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }
}

代码先构造了含有7个时间节点的数组,然后设置滑动窗口大小为6,
(1,10), (3,1), (5,4), (7,8), (9,3), (12,9), (19,4)
接着输出每个时间节点在窗口期内的最大网络流量,运行后结果如下:

Max from from time: 19 in sliding window size is 4
Max from from time: 12 in sliding window size is 9
Max from from time: 9 in sliding window size is 8
Max from from time: 7 in sliding window size is 10
Max from from time: 5 in sliding window size is 10
Max from from time: 3 in sliding window size is 10
Max from from time: 1 in sliding window size is 10

根据每个节点的时间点算出他们对应窗口内的最大流量,我们输出的结果应该是正确的。

更详细的讲解和代码调试演示,请参看视频。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述

你可能感兴趣的:(面试,算法,java,java,面试算法,滑动窗口,最大网络流量)