在科技飛速發展的今天,天天都會產生大量新數據,例如銀行交易記錄,衛星飛行記錄,網頁點擊信息,用戶日誌等。爲了充分利用這些數據,咱們須要對數據進行分析。在數據分析領域,很重要的一塊內容是流式數據分析。流式數據,也即數據是實時到達的,沒法一次性得到全部數據。一般狀況下咱們須要對其進行分批處理或者以滑動窗口的形式進行處理。分批處理也即每次處理的數據之間沒有交集,此時須要考慮的問題是吞吐量和批處理的大小。滑動窗口計算表示處理的數據每次向前移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),相比樸素算法也有明顯地提高。數組