Haskell:搜索大型列表的最佳方式

时间:2014-03-25 15:23:11

标签: list search haskell functional-programming

我有一个100K +元素列表,这是一个有限列表。目前我正在使用Data.List函数elem。查看Data.List信息页面时,还有find和filter。其中一个会比elem函数更快吗?

5 个答案:

答案 0 :(得分:4)

以防万一我们没有足够的时间击败死马......

不同的集合表示存在巨大的性能差异。作为一个例子(可能与您的用例匹配或不匹配)考虑采用200K随机元素列表和计算来确定200个随机元素的成员资格。

我已经实现了三种明显的方法 - 在列表上使用elem,转换为HashSet并检查成员资格,并执行Bloom过滤器和哈希集的混合。基准测试显示列表解决方案比散列集慢3个数量级,比混合集慢约2倍。

benchmarking list
mean: 460.7106 ms, lb 459.2952 ms, ub 462.8491 ms, ci 0.950
std dev: 8.741096 ms, lb 6.293703 ms, ub 12.23082 ms, ci 0.950

benchmarking hashset
mean: 175.2730 us, lb 173.9140 us, ub 177.0802 us, ci 0.950
std dev: 7.966790 us, lb 6.391454 us, ub 10.25774 us, ci 0.950

benchmarking bloom+hashset
mean: 88.22402 us, lb 87.35856 us, ub 89.66884 us, ci 0.950
std dev: 5.642663 us, lb 3.793715 us, ub 8.264222 us, ci 0.950

代码:

import qualified Data.HashSet as Set
import           Data.HashSet (Set)
import qualified Data.BloomFilter as BF
import qualified Data.BloomFilter.Easy as BF
import           Data.BloomFilter (Bloom)
import           Data.BloomFilter.Hash as H2
import           Data.Hashable as H1
import Criterion.Main
import System.Random

data MySet a = MS (Set a) (Bloom a)

fromList :: (H2.Hashable a, H1.Hashable a, Ord a) => [a] -> MySet a
fromList as =
    let hs = Set.fromList as
        bf = BF.easyList 0.2 as
    in hs `seq` bf `seq` MS hs bf

member :: (H2.Hashable a, H1.Hashable a, Ord a) => a -> MySet a -> Bool
member e (MS hs bf)
    | BF.elemB e bf = Set.member e hs
    | otherwise      = False

main = do
  list   <- take 200000 `fmap` randomsIO :: IO [Int]
  xs     <- take 200    `fmap` randomsIO
  let hs  = Set.fromList list
      bhs = fromList list
  defaultMain
        [ bench "list" $ nf (map (`elem` list)) xs
        , bench "hashset" $ nf (map (`Set.member` hs)) xs
        , bench "bloom+hashset" $ nf (map (`member` bhs)) xs
        ]

randomsIO = randoms `fmap` newStdGen

答案 1 :(得分:1)

在每种情况下,您都需要对列表进行线性遍历。如果你要反复检查遏制,你应该改为更有效的结构。如果你只需要进行一次查找,那么O(n)最坏的情况是你能得到的最好的 - 只需在创建元素时查找它。

如果您的类型是有序的(实例化Ord),那么您应该使用Set包中的containers(它是Haskell平台的一部分)。

import qualified Data.Set as Set

mySet :: Set.Set Elems
mySet = Set.fromList bigList -- expensive, eventually requires a 1 linear traversal

-- cheaper!
checkElems :: [Elem] -> Set.Set Elems -> [Bool]
checkElems es s = map (\e -> Set.member e s) es

如果Ord不可能,您可以通过unordered-containers中的数据结构使用哈希。在该程序包中,我们Data.HashSet实际上与Data.Set完全相同,只是它需要(有时更自由,有时更快)Hashable实例而不是Ord

如果您的Elem类型实际上是Int,那么Data.IntSet也是一个不错的选择。

最后,值得注意的是,虽然Set是用于检查成员资格的优化结构,但它确实会丢弃重复。如果重复是有价值的,您将需要检查其他数据类型或某些类型的预处理。带有重复的集合通常称为包,可以使用Data.MapData.HashMapData.IntMap模块进行模拟(具有相似的性能特征)。在这种情况下,您将列表存储为Data.Map.Map Elem Count并通过查看结果映射中是否正在使用特定键来检查成员资格。

答案 2 :(得分:1)

让我们看一下定义:

elem :: Eq a => a -> [a] -> Bool
elem _ []     = False
elem x (y:ys) = x == y || elem x ys

find :: (a -> Bool) -> [a] -> Maybe a
find p = listToMaybe . filter p

filter :: (a -> Bool) -> [a] -> [a]
filter p [] = []
filter p (x:xs) = if p x then x : filter p xs else filter p xs

很明显,findfilter具有相同的复杂性。 elem函数与filter具有相同的基本递归模式,因此它也具有相同的复杂性。实际上,使用哪一个并不重要,所有这些都具有最差的O(n)复杂性。如果您只是测试会员资格,那么elem应该是您的首选功能。如果您做的不仅仅是这些,您可能需要考虑切换到VectorSet或其他更好地针对您正在做的事情进行优化的数据结构。 Haskell中的列表非常适用于不确定性并处理少量数据,但是当您拥有大量数据点时,其效率低下会变得非常明显。

答案 3 :(得分:0)

他们做不同的事情。 filter只是根据谓词删除元素。在所有情况下,这将比elem慢,因为它必须遍历整个列表并检查谓词,即使您的元素位于列表的头部。

find只会返回一个元素,所以它的性能在所有意图和目的上都是相同的。

因此,elem / find可能是本地最大值,以提高搜索列表的效率。但这是一个可怜的局部最大值。

另一方面,如果您正在操纵大量数据,[]可能是错误的选择。从缓存的角度来看,这绝对是可怕的,几乎所有的操作都是O(n)。毕竟,这只是一个愚蠢的单链表。如果您正在进行大量的成员资格检查,请考虑切换到Data.Set,这是一个非常轻松的过渡。

答案 4 :(得分:0)

对于那么多元素,您可能希望使用进行子线性搜索的数据结构。我的Haskell数据结构的首选库是Edison。 GHC包括Data.Set,它仍然是次线性的,平台有unordered-containers,它应该非常快。