在科技飛速發展的今天,每天都會產生大量新數據,例如銀行交易記錄,衛星飛行記錄,網頁點擊信息,用戶日志等。為了充分利用這些數據,我們需要對數據進行分析。在數據分析領域,很重要的一塊內容是流式數據分析。流式數據,也即數據是實時到達的,無法一次性獲得所有數據。通常情況下我們需要對其進行分批處理或者以滑動窗口的形式進行處理。分批處理也即每次處理的數據之間沒有交集,此時需要考慮的問題是吞吐量和批處理的大小。滑動窗口計算表示處理的數據每次向前移N個單位,N小於要處理數據的長度。例如,在語音識別中,每個包處理大約25ms的音頻數據,然后以步幅10ms向前移動處理下一個包的數據。語音識別就是一個典型的流式數據通過滑動窗口方式進行處理的例子。在本文中,我們關注N=1的情況,也即每次處理完一個包之后,向前移動一個單位繼續處理下一個包,如下圖所示。
圖1 基於滑動窗口的流式數據處理示例
我們主要關注幾個常見的數學統計量:最小(大)值、平均值和中位數。事實上,只要知道了最大值和最小值的求法,很容易計算極差;知道了平均值的求法,就可以很容易地計算方差和標准差。針對上述統計量的計算都有一個naïve算法,也即不考慮前后兩個包之間數據重疊,將每個包看成獨立的,對每一個包分別計算上述統計量。如果總數據長度為n,每個包的長度為k,則計算上述統計量的復雜度為O(nk)(針對給定數組求中位數的問題,存在復雜度O(k)的算法,實現方法是基於快排進行改進,網上資料很多在此不再做介紹)。我們嘗試在naïve算法的基礎上降低每個統計量的計算復雜度,下面開始正式的介紹。
1. 最小(大)值
這是一個經典問題,通常被稱為滑動極值問題。問題描述:給定一個長度為n的數列a0,a1,...,an−1和一個整數k,求數列bi=min{ai,ai+1,...,ai+k−1}(i=0,1,...,n−k)。
通過使用單調隊列可以在O(n)的時間內解決。單調隊列維護數列的下標,隊列內的元素滿足:
設單調隊列從頭部開始的元素值為xi,則xi<xi+1且axi<axi+1。
簡單來說單調隊列就是下標對應的元素是嚴格遞增的順序(當然在實際應用過程中,可能不嚴格單調,也可能是遞減的順序)。
考慮以ai結尾的k個元素,求bi−k+1。假定單調遞增隊列中維護了ai之前的k-1個元素相關的最小值下標,為了求bi−k+1,我們需要將ai和單調隊列中元素進行比較。當隊列末尾的元素j滿足aj≥ai,則不斷取出末尾元素,直到隊列為空或者aj<ai。 ai不僅會影響bi−k+1的計算,也會影響后續k-1個bi的計算。如果ai是這一段的最小值,則它在單調隊列中就不會被刪除,進而可以用O(1)的時間求單個bi。
當刪除單調隊列的元素時,需要判斷頭部元素是否還需要。如果已經脫離計算bi的范圍,則可以刪除頭部元素。求單個bi的值,只需要返回單調隊列的頭部元素即可。均攤復雜度為O(n)。求最小值的代碼如下:
#define MAX_N 100000
static int a[MAX_N];
static int b[MAX_N];
static int deque[MAX_N];
void range_min(int n,int k)
{
int s=0,t=0;//單調隊列的頭和尾指針
for (int i=0;i<n;i++)
{
//在單調隊列的末尾加入i
while (s<t&&a[deque[t-1]]>=a[i]) t--;//維護嚴格的單調遞增隊列
deque[t++]=i;
if (i-k+1>=0)
{
b[i-k+1]=a[deque[s]www.lieqibiji.com];
}
//從單調隊列頭部刪除元素
if (deque[s]==i-k+1)
求滑動最大值只需要將大於等於號改為小於等於號即可,維護一個單調遞減隊列。通過使用單調隊列,流式數據中極值計算的復雜度可以由O(nk)降為O(n),當每個包的長度很大時,算法的優化效果會非常明顯。滑動極值問題具有很廣泛的應用,希望大家能知道這個優雅的解法。單調隊列還有很多其他應用場景,比如解決《leetcode之Largest Rectangle in Histogram》。此外,在一些動態規划問題中,它也可以用來降低時間復雜度。
2. 平均值
滑動平均值的計算比較容易優化,我們需要做的就是維護區間內元素的和,除以區間元素個數k即是區間平均值。當計算下一個區間的平均值時,我們先將上一個區間的和減掉上一個區間第一個元素的值,然后加上當前區間最后一個元素的值,然后除以k即是當前區間的平均值。求區間平均值的代碼如下:
#define MAX_N 100000
static int www.boayulevip.cn a[MAX_N];
static int b[MAX_N];
void range_mean(int n,int k)
{
int sum=0;
for (int i=0;i www.yszxylpt.com <n;i++)
{
sum+=a[i];
if(i-k+1>=0)
{
b[i-k+1]=sum/k;
很明顯可以看出上述代碼的復雜度為O(n)。求方差可以采用類似的思路,在求和的同時也求一個平方和,之后采用方差的平方和公式即可求得方差。
3. 中位數
中位數是一個非常重要的指標,在很多應用中都會用到,但是相比前兩個統計量,中位數的優化要麻煩很多。
在介紹基於滑動窗口的中位數計算之前,我們先看一個類似的問題:也是流式數據求中位數,但是每次都求前面所有數據的中位數。該問題也很經典,出現在劍指offer一書中,具體解法可參考《數據流中的中位數》。簡單來說,就是構造一個最大堆和一個最小堆,最大堆的元素都小於最小堆中的元素,而且最小堆中的元素個數至多比最大堆中的元素個數多1。每次來新元素的時候,根據當前兩個堆的元素個數來決定往哪個堆插入元素,在插入的同時保證上面所說的兩個前提。插入復雜度是O(log n),查詢復雜度是O(1)。
基於滑動窗口的中位數計算解法和上面的問題類似,也需要構造一個最大堆和最小堆,同時也滿足上面的兩個條件,區別就在於我們每次計算完一次中位數之后,都需要從堆中刪除一個最老的元素。可以通過和中位數比較來確定刪除哪個堆中的元素。通常的堆操作一般是插入和刪除堆頂元素,在此需要實現一個函數可以刪除任意位置的堆元素,同時保證堆的結構不被破壞,這不是一個困難的問題,實現和刪除堆頂元素類似。如果數據是以數組形式一次給定,最老的元素可以通過訪問原數組獲得,如果流式數據一次只給定一個數據,我們可以通過循環隊列保存最近的k個元素來獲得最老的元素。代碼實現可以參考博客《找滑動窗口的中位數》,在此就不給出詳細代碼。每來一個數據都需要執行一次插入和刪除,復雜度是O(log k),所以針對流式數據的中位數問題算法復雜度是O(nlogk),相比朴素算法也有明顯地提升。