更详细的讲解和代码调试演示过程,请参看视频
如何进入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
根据每个节点的时间点算出他们对应窗口内的最大流量,我们输出的结果应该是正确的。
更详细的讲解和代码调试演示,请参看视频。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号: