日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

线性时间查找固定频率的元素

發布時間:2024/9/30 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 线性时间查找固定频率的元素 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

轉自:http://blog.ibread.net/467/linear-time-iceberg-query-algorithm/

一、從面試題開始

在進入到枯燥的正文之前,先來看一道據說在很多面試過程中都會問到的題目:

已知一個長度為n的數組,求出現半數以上的元素。

這道題目看似簡單,其實得到完美的答案并不容易。首先,不難想到,出現半數以上的元素最多只有一個。而為了選出出現次數達到半數以上的元素,最笨的方法當然就是對數組中出現的每一個元素,都遍歷一遍數組記錄下其出現頻率,通過與比較確定是否符合要求。這樣就可以在的時間內,用O(1)空間解決問題。 但是,用Prof. Maggs?的話說,的時間開銷顯然是完全沒有辦法忍受的,:) 于是呢,為了解決這個問題,我們可以借助一個比較作弊的數據結構: hash表來存儲每個元素出現的次數。這樣就可以得到一個時間復雜度和空間復雜度都是O(n)的解決方案。 而之所以說hash表這個方案有點作弊呢,是因為我們假定有一個完美的hash函數,使得數組中的不同元素并不會有碰撞。所以要真的依靠設計良好的hash函數達到存取開銷都是O(1),這本身就是不是一個好解決的問題。不過hash函數的實現并不在本文的討論范圍之內,所以就姑且默認為可以完美實現O(1)時間的存取吧。

以上我們分別討論了時間 O(1)空間以及O(n)時間O(n)空間的兩個方案,那么問題來了,有沒有時間復雜度O(n)空間復雜度O(1)的算法呢? 實際上,早在91年就有人專門就這個問題發表了論文,介紹了一種線性時間的算法: Majority Vote Algorithm。通過名字就可以看出,這個算法是專門用來解決這個問題的。而由于作者是J Moore?(目前是Utexas的計算機系主任),這個算法有時候也會被稱為Moore’s Voting Algorithm?(當然這個Moore并不是提出Moore’s Law的那個Gordon Moore)。

算法的基本思想非常簡潔: 每次都找出一對不同的元素,從數組中刪掉,直到數組為空或只有一種元素。 不難證明,如果存在元素e出現頻率超過半數,那么數組中最后剩下的就只有e。當然,最后剩下的元素也可能并沒有出現半數以上。比如說數組是[1, 2, 3],最后剩下的3顯然只出現了1次,并不到半數。排除這種false positive情況的方法也很簡單,只要保存下原始數組,最后掃描一遍驗證一下就可以了。

現在來分析一下復雜度。刪除元素可以在常數時間內完成,但找不同元素似乎有點麻煩。實際上,我們可以換個角度來想,用一個小trick來重新實現下該算法。

在算法執行過程中,我們使用常量空間實時記錄一個候選元素c以及其出現次數f(c),c即為當前階段出現次數超過半數的元素。在遍歷開始之前,該元素c為空,f(c)=0。然后在遍歷數組A時,

  • 如果f(c)為0,表示當前并沒有候選元素,也就是說之前的遍歷過程中并沒有找到超過半數的元素。那么,如果超過半數的元素c存在,那么c在剩下的子數組中,出現次數也一定超過半數。因此我們可以將原始問題轉化為它的子問題。此時c賦值為當前元素, 同時f(c)=1。
  • 如果當前元素A[i] == c, 那么f(c) += 1。(沒有找到不同元素,只需要把相同元素累計起來)
  • 如果當前元素A[i] != c,那么f(c) -= 1 (相當于刪除1個c),不對A[i]做任何處理(相當于刪除A[i])

如果遍歷結束之后,f(c)不為0,那么再次遍歷一遍數組,記錄c真正出現的頻率,從而驗證c是否真的出現了超過半數。上述算法的時間復雜度為O(n),而由于并不需要真的刪除數組元素,我們也并不需要額外的空間來保存原始數組,空間復雜度為O(1)。實際上,在Moore大牛的主頁上有針對這個算法的一個演示,感興趣的同學可以直接移步觀看。

這個問題看上去已經完美的解決了。

二、更一般的情況呢?

那么,如果我們想找的并不是超過半數的元素,而是出現頻率超過一定頻率的元素都要找出來,是否也存在一個類似的線性時間的算法呢?答案是肯定的。實際上,這一類從特定的數據集中找出出現頻率超過某個閾值的元素的問題,有一個形象的名字叫做Iceberg query,或者叫做host list分析。而Richard Karp 老爺子當年就專門寫了一篇論文來討論這種一般性問題的解決方案,而通過下文的介紹,大家也可以發現,Karp的方案應該也是受到了Moore的算法的啟發。

首先還是看一下問題的形式化定義吧:

對于一個序列?以及一個在(0,1)之間的實數。假定表示元素的出現頻率,我們需要找到所有滿足的元素。

首先,滿足條件的元素個數必然小于。否則的話,。因此,如果令,我們可以知道最終的候選元素的個數不會超過K。與上文中介紹的算法相同,我們還是基于序列的遍歷來完成新的算法。同樣的,我們需要維護一個規模為K的候選元素表,其中存儲候選元素c以及其出現頻率f(c)。遍歷開始前,所有的f(c)初始化為0。在遍歷過程中:

  • 若A[i]已在候選表中,即存在c使得,那么f(c) += 1。
  • 若A[i]不在候選表中,且候選表仍有空位,即,那么將A[i]插入到表中,且f(A[i]) = 1。
  • 若A[i]不在候選表中,且候選表已滿。那么丟棄A[i],且對于候選表中每一元素,做f(c) -= 1。該操作完畢之后,所有f(c)歸零的元素從候選表中移除。

算法介紹完了,我們先來證明下其正確性。

首先,false positive的情況可以通過對序列做第二次遍歷,同時記錄候選元素的出現次數來排除。

然后,我們來排除false negative的情況,也就是說,對于元素c沒有出現在候選元素表中,那么一定有。可以注意到,算法中的第三步,實際上相當于從目前遍歷過的元素中移除個,其中個是候選表中元素,1個是當前元素。而如果c最終不在候選表中,那么要么c從來沒有被選中過,要么是選中后又被排除了。對于前者,c一直扮演著第三步中當前A[i]的角色,被直接丟棄; 對于后者,c可能還扮演著第三步候選表中元素的角色,頻率自減1。但不管怎樣,可以肯定的是,每次丟棄c,都有另外K個元素也一起被丟棄。假定表示元素c的總體出現頻率,由于元素c最終不在候選表中,我們可以認定所有的c都被丟棄掉,也就是說,我們一共丟棄了個元素。又?,我們有?,繼而得到?。也就是說,false negative的情況也被排除了。

那么,這個算法的復雜度是多少呢? 由于采用了hash表,我們需要的空間。同時如果假定存取操作都可以O(1)完成的話,算法中的三個步驟時間復雜度分別是,??和?。然而如果使用平攤分析,我們可以在第一步和第二步都多計一個credit,用于今后可能進行的第三步中的刪除操作,那么每一步的平攤時間代價就是,從而可以在時間內完成該算法。

三、看平攤分析不順眼?

看樣子對于一般性的iceberg query問題,我們也已經成功給出了一個比較完美的答案,復雜度為O(N)的時間,的空間。無論是時間和空間,都是沒有辦法再少了。事情到了這個地步,似乎已經可以結束了。但是要注意的是,對于時間復雜度,我們 采用的是平攤分析,雖然也是O(N)沒錯,但看上去總是不太爽(其實我還可以接受了…)。比如說如果在處理當前元素時,走到了上述算法的第3個分支, 那復雜度顯然是與當前候選表中元素個數相關的,也就是說。那么,能不能想想辦法,使得無論走哪個分支,都只用O(1)的時間復雜度,同時繼續保持的空間呢?

其實很容易就想到一個小trick來解決這個問題。第三個分支所做的,不就是把所有f(c)都自減1么,那么如果我們把所有的f(c)都存成相對于一個變量的差值,每次只要改變這一個變量不就可以實現對所有count的減一操作了么? ?但是這樣還是沒有完全解決問題,因為我們還有另外的操作,就是如果f(c)歸零,要將該元素刪除。要做到這一點,似乎還是得以此掃描候選表中的f(c)。要解決這個問題也很容易,只要把f(c)相同的元素歸類到一起形成一個鏈表,然后按照f(c)的遞增順序將鏈表串起來就行了。每次只要檢測最小的f(c)是否歸零,如果歸零,那么就把整個鏈表刪掉。

圖1:改進后的數據結構

基本的思想就是這樣了,先來看最終的數據結構吧。如圖1所示,利用hash表,我們可以將各元素一一對應到中間雙向鏈表中的結點,每個結點存取相應元素的出現頻率。而處于同一雙向鏈表中的結點頻率數相同。這里的雙向鏈表實際上就起到我們上文提到的候選表的作用。最左側的單向鏈表用于將各個雙向鏈表按照頻率從低到高的順序串在一起。其中,頭結點中存儲著真正的頻率值 (如圖中的3,我們在下文中稱之為base),其他結點皆存儲相對其前驅結點的頻率差值 (如圖中所示的+3, +1)。理所當然的,base值只能為正。同時,每個單鏈表的結點也作為雙向鏈表的頭結點,維護相應雙向鏈表的長度,我們還需要另外維護一個變量total表示當前處于雙向鏈表中的元素個數,也就是候選表的大小。空間復雜度不難分析出來,是。

結合這個數據結構,再來把上面的操作重新分析一下吧。其實我們需要的只有3種操作: 查找當前元素所在的結點,插入新結點,已有結點頻率+1,所有已有結點頻率-1并刪除所有頻率降為0的結點,下面我們分別看一下時間復雜度復雜度。

  • 查找: O(1) 借助hash表,我們可以在O(1)時間內找到當前元素A[i]所在的結點,進而得知其頻率。
  • 插入新結點: O(1) 如果沒有找到該結點,我們首先需要判斷候選表是否已滿,如果未滿 (),則需要將其插入到頻率值為1的雙向鏈表中。我們通過檢測base可以判斷該鏈表是否存在。有的話則直接插入,否則則為單鏈表新建一個頭結點指向一個新的雙向鏈表。當然同時也要更新hash表,以及單鏈表原頭結點的數值 (自減1,變成相對值)。
  • 頻率自增1: O(1) 如果找到該結點,則需要將其頻率自增1。這個操作需要先將其從當前所在的雙向鏈表中摘下來 (O(1) 時間),插入到f(c)+1所屬的鏈表中。由于雙向鏈表中每個結點我們都額外維護了指向表頭的指針,所以我們可以在常量時間內找到其后繼雙線鏈表的表頭。但注意這個表頭未必合適。我們還需要查看其中維護的頻率差值,如果為1則直接插入其中;否則的話則建立一個新結點,指向一個新的雙向鏈表。同時也要把這個新結點插入到單鏈表中,更新其后繼結點的差值。當然,鏈表長度也要進行相應的更新。由于我們采用了雙向鏈表,所以刪除和插入新結點都只需要常數時間開銷; 同時,對于單鏈表來說,在當前結點之后插入新結點也只需要常數時間。總體來說,此步驟只需O(1)時間。
  • 所有頻率自減1:O(1)并刪除所有頻率歸零的結點: 由于雙向鏈表是按照頻率由低到高依次排列的,僅有第一個鏈表的頻率可能降為0,因此我們只需要檢查其頻率,也就是base就可以了。如果base為1,那么直接刪除該頭結點,和其指向的雙向鏈表;否則的話,base自減1就可以了。因為后繼結點存儲的都是差值,并不需要一一更新。容易看出,這一步步驟也只需O(1)的時間。

現在基本上是大功告成了,我們通過幾個簡單數據結構的組合,把每個步驟的時間復雜度從平攤的O(1)優化到了嚴格的O(1)。不過確實好累。。。其實對于這個問題,還可以繼續做下去。比如說,是否存在線性時間的確定性online算法呢? 比如說,輸入不是靜態的序列,而是stream,也就是沒有辦法進行二次掃描。那么是否存在線性時間的online算法,來確定地找出iceberg query的結果呢? 說實話這部分內容我還沒有深入的了解。目前只知道線性時間帶有false positive結果的算法,且如果要做到online的話,至少需要的空間。另外,在這篇論文中,介紹了一種基于概率的方法,不過我沒有仔細看 (慚愧啊。。。) 不過有點題外話就是,雖然這篇論文用了跟Karp老爺子一樣的算法,但是并沒有引用其論文,不知道是怎么個情況。。


與50位技術專家面對面20年技術見證,附贈技術全景圖

總結

以上是生活随笔為你收集整理的线性时间查找固定频率的元素的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。