查找最频繁项目的最有效数据结构

Most efficient data structure for finding most frequent items

我想从 Google N-Grams dataset 中提取最常用的单词,它的未压缩形式约为 20 GB。我不想使用整个数据集,只使用最常见的 5000 个。但是如果我写

take 5000 $ sortBy (flip $ comparing snd) dataset
-- dataset :: IO [(word::String, frequency::Int)]

这将是一个无尽的等待。但是我应该怎么做呢?

我知道有Data.Array.MArray package available for in-place array computation, but I cannot see any function for items modification on its documentation page. There is also Data.HashTable.IO,但它是无序数据结构。

我想使用简单的 Data.IntMap.Strict(具有方便的 lookupLE 功能),但我认为它不会非常有效,因为它会在每次更改时生成新地图。 ST monad 可以改进吗?

UPD: 我还在 CoreReview.SX 上发布了程序的最终版本。

怎么样

  • 使用splitAt将数据集划分为前 5000 项和其余项。
  • 按频率(升序)对前 5000 项进行排序
  • 完成其余部分
    • 如果某个项目的频率高于已排序项目中的最低频率
    • 从已排序的项目中删除频率最低的项目
    • 将新项目插入已排序项目中的适当位置

该过程随后变为有效线性,但如果您对已排序的 5000 个元素使用具有次线性最小删除和插入的数据结构,则系数会提高。

例如,使用Data.Heap from the heap package:

import Data.List (foldl')
import Data.Maybe (fromJust)
import Data.Heap hiding (splitAt)

mostFreq :: Int -> [(String, Int)] -> [(String, Int)]
mostFreq n dataset = final
  where
    -- change our pairs from (String,Int) to (Int,String)
    pairs = map swap dataset
    -- get the first `n` pairs in one list, and the rest of the pairs in another
    (first, rest) = splitAt n pairs
    -- put all the first `n` pairs into a MinHeap
    start = fromList first :: MinHeap (Int, String)
    -- then run through the rest of the pairs
    stop = foldl' step start rest
    -- modifying the heap to replace its least frequent pair
    -- with the new pair if the new pair is more frequent
    step heap pair = if viewHead heap < Just pair
                       then insert pair (fromJust $ viewTail heap)
                       else heap
    -- turn our heap of (Int, String) pairs into a list of (String,Int) pairs
    final = map swap (toList stop)
    swap ~(a,b) = (b,a)

试过这个还是你只是猜测?因为许多 Haskell 排序函数尊重 惰性 ,当您只要求前 5000 个时,它们会很乐意避免对其余元素进行排序。

同样,对"it produces a new map on each alteration"要非常小心。大多数插入操作在这种数据结构上都是 O(log n),n 限制为 5000:因此您可能会在每次更改时在堆中分配 ~30 个新单元,但这当然不是特别大的成本没有 5000 那么大。

如果 Data.List.sort 效果不佳,您想要的是:

import Data.List (foldl')
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IM

type Freq = Int
type Count = Int
data Summarizer x = Summ {tracking :: !IntMap [x], least :: !Freq, 
                        size :: !Count, size_of_least :: !Count }

inserting :: x -> Maybe [x] -> Maybe [x]
inserting x Nothing = Just [x]
inserting x (Just xs) = Just (x:xs)

sizeLimit :: Summarizer x -> Summarizer x
sizeLimit skip@(Summ strs f_l tot lst) 
    | tot - lst < 5000 = skip
    | otherwise        = Summ strs' f_l' tot' lst'
        where (discarded, strs') = IM.deleteFindMin strs
              (f_l', new_least) = IM.findMin dps'
              tot' = tot - length discarded
              lst' = length new_least

addEl :: (x, Freq) -> Summarizer x -> Summarizer x
addEl (str, f) skip@(Summ strs f_l tot lst)
    | i < f_l && tot >= 5000 = skip
    | otherwise              = sizeLimit $ Summ strs' f_l' tot' lst'
        where strs' = IM.alter (inserting str) f strs
              tot' = tot + 1
              f_l' = min f_l f
              lst' = case compare f_l f of LT -> lst; EQ -> lst + 1; GT -> 1

请注意,我们存储字符串列表以处理重复频率;我们大多跳过更新,当我们 do 更新时,它是一个 O(log n) 操作来放入新元素,有时(再次取决于复制)一个 O(log n) 操作来剪掉最小的元素,然后进行 O(log n) 操作以找到新的最小元素。