首页 文章

有效地检查(大)列表的所有元素是否相同

提问于
浏览
28

问题

让我们假设我们有一个列表 xs (可能是一个非常大的列表),我们想检查它的所有元素是否相同 .

我想出了各种各样的想法:

解决方案0

检查 tail xs 中的所有元素是否等于 head xs

allTheSame :: (Eq a) => [a] -> Bool
allTheSame xs = and $ map (== head xs) (tail xs)

解决方案1

检查 length xs 是否等于从 xs 获取元素所获得的列表长度,而它们等于 head xs

allTheSame' :: (Eq a) => [a] -> Bool
allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)

解决方案2

递归解决方案: allTheSame 如果 xs 的前两个元素相等则返回 TrueallTheSame 返回 xs 的其余部分 True

allTheSame'' :: (Eq a) => [a] -> Bool
allTheSame'' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
    where  n = length xs

解决方案3

分而治之:

allTheSame''' :: (Eq a) => [a] -> Bool
allTheSame''' xs
  | n == 0 = False
  | n == 1 = True
  | n == 2 = xs !! 0 == xs !! 1
  | n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
  | otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
    where n = length xs
          split = splitAt (n `div` 2) xs

解决方案4

我在写这个问题的时候就想到了这个:

allTheSame'''' :: (Eq a) => [a] -> Bool
allTheSame'''' xs = all (== head xs) (tail xs)

问题

  • 我认为解决方案0效率不高,至少在内存方面,因为 map 将在将 and 应用于其元素之前构造另一个列表 . 我对吗?

  • 解决方案1仍然不是很有效,至少在内存方面,因为 takeWhile 将再次构建一个额外的列表 . 我对吗?

  • 解决方案2是尾递归(对吗?),它应该非常有效,因为只要 (xs !! 0 == xs !! 1) 为False它就会返回 False . 我对吗?

  • 解决方案3应该是最好的,因为它的复杂性应该是O(log n)

  • 解决方案4看起来很Haskellish给我(是吗?),但它可能与解决方案0相同,因为 all p = and . map p (来自Prelude.hs) . 我对吗?

  • 还有其他更好的写作方式 allTheSame 吗?现在,我希望有人会回答这个问题告诉我,我已经用hoogle搜索了这个问题,我还没有学习Haskell,我相信这对我来说是一个很好的练习:)

欢迎任何其他评论 . 谢谢!

9 回答

  • 0

    gatoatigrado的回答为衡量各种解决方案的性能提供了一些很好的建议 . 这是一个更具象征性的答案 .

    我认为解决方案0(或者,完全相同,解决方案4)将是最快的 . 请记住,Haskell是惰性的,因此 map 将不必在应用 and 之前构造整个列表 . Build 直觉的好方法是玩无限 . 例如:

    ghci> and $ map (< 1000) [1..]
    False
    

    这会询问是否所有数字都小于1,000 . 如果 map 在应用 and 之前构建了整个列表,那么这个问题永远无法回答 . 即使你给列表一个非常大的右 endpoints (即Haskell没有做任何"magic",取决于列表是否是无限的),表达式仍然会快速回答 .

    为了开始我的例子,让我们使用这些定义:

    and [] = True
    and (x:xs) = x && and xs
    
    map f [] = []
    map f (x:xs) = f x : map f xs
    
    True && x = x
    False && x = False
    

    以下是 allTheSame [7,7,7,7,8,7,7,7] 的评估顺序 . 写下来会有额外的分享,这太痛苦了 . 我还会比简洁性更早地评估 head 表达式(无论如何它都会被评估,所以它几乎没有什么不同) .

    allTheSame [7,7,7,7,8,7,7,7]
    allTheSame (7:7:7:7:8:7:7:7:[])
    and $ map (== head (7:7:7:7:8:7:7:7:[])) (tail (7:7:7:7:8:7:7:7:[]))
    and $ map (== 7)  (tail (7:7:7:7:8:7:7:7:[]))
    and $ map (== 7)          (7:7:7:8:7:7:7:[])
    and $ (== 7) 7 : map (== 7) (7:7:8:7:7:7:[])
    (== 7) 7 && and (map (== 7) (7:7:8:7:7:7:[]))
    True     && and (map (== 7) (7:7:8:7:7:7:[]))
                and (map (== 7) (7:7:8:7:7:7:[]))
    (== 7) 7 && and (map (== 7)   (7:8:7:7:7:[]))
    True     && and (map (== 7)   (7:8:7:7:7:[]))
                and (map (== 7)   (7:8:7:7:7:[]))
    (== 7) 7 && and (map (== 7)     (8:7:7:7:[]))
    True     && and (map (== 7)     (8:7:7:7:[]))
                and (map (== 7)     (8:7:7:7:[]))
    (== 7) 8 && and (map (== 7)       (7:7:7:[]))
    False    && and (map (== 7)       (7:7:7:[]))
    False
    

    看看我们怎么做't even have to check the last 3 7'?这是一种懒惰的评估,使列表更像循环 . 所有其他解决方案都使用昂贵的函数,如 length (它们必须一直走到列表末尾才能给出答案),因此它们的效率会降低,而且它们也无法在无限列表上运行 . 在无限列表上工作并且有效率通常在Haskell中一起使用 .

  • 3

    首先,我认为您不想使用列表 . 很多算法都依赖于计算长度,这很糟糕 . 您可能需要考虑vector包,它将为O(1)提供长度,而O(n)则为列表 . 向量也具有更高的内存效率,特别是如果您可以使用Unboxed或Storable变体 .

    话虽这么说,你真的需要考虑代码中的遍历和使用模式 . Haskell的列表非常有效,如果它们可以按需生成并消耗一次 . 这意味着您不应该继续引用列表 . 像这样的东西:

    average xs = sum xs / length xs
    

    要求整个列表保留在内存中(通过 sumlength ),直到两个遍历都完成 . 如果您可以一步完成列表遍历,那么效率会更高 .

    当然,您可能还需要保留列表,例如检查所有元素是否相等,如果不相同,请对数据执行其他操作 . 在这种情况下,对于任何大小的列表,您可能最好使用更紧凑的数据结构(例如矢量) .

    现在,这是他们的方式,这里看看这些功能 . 在我展示核心的地方,它是用 ghc-7.0.3 -O -ddump-simpl 生成的 . 此外,在使用-O0编译时,不要打扰判断Haskell代码性能 . 使用实际用于 生产环境 代码的标志进行编译,通常至少为-O,也可能是其他选项 .

    Solution 0

    allTheSame :: (Eq a) => [a] -> Bool
    allTheSame xs = and $ map (== head xs) (tail xs)
    

    GHC产生这个核心:

    Test.allTheSame
      :: forall a_abG. GHC.Classes.Eq a_abG => [a_abG] -> GHC.Bool.Bool
    [GblId,
     Arity=2,
     Str=DmdType LS,
     Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
             ConLike=True, Cheap=True, Expandable=True,
             Guidance=IF_ARGS [3 3] 16 0}]
    Test.allTheSame =
      \ (@ a_awM)
        ($dEq_awN :: GHC.Classes.Eq a_awM)
        (xs_abH :: [a_awM]) ->
        case xs_abH of _ {
          [] ->
            GHC.List.tail1
            `cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
                    :: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
          : ds1_axJ xs1_axK ->
            letrec {
              go_sDv [Occ=LoopBreaker] :: [a_awM] -> GHC.Bool.Bool
              [LclId, Arity=1, Str=DmdType S]
              go_sDv =
                \ (ds_azk :: [a_awM]) ->
                  case ds_azk of _ {
                    [] -> GHC.Bool.True;
                    : y_azp ys_azq ->
                      case GHC.Classes.== @ a_awM $dEq_awN y_azp ds1_axJ of _ {
                        GHC.Bool.False -> GHC.Bool.False; GHC.Bool.True -> go_sDv ys_azq
                      }
                  }; } in
            go_sDv xs1_axK
        }
    

    实际上,这看起来很不错 . 它会产生一个带有空列表的错误,但这很容易修复 . 这是 case xs_abH of _ { [] -> . 在GHC执行worker / wrapper转换之后,递归worker函数是 letrec { go_sDv 绑定 . Worker 检查其论点 . 如果 [] ,则它到达列表的末尾并返回True . 否则,它将剩余的头部与第一个元素进行比较,并返回False或检查列表的其余部分 .

    其他三个功能 .

    • map 已完全融合,并未分配临时列表 .

    • 在定义顶部附近注意 Cheap=True 语句 . 这意味着GHC认为函数"cheap",因此是内联的候选者 . 在调用站点,如果可以确定具体的参数类型,GHC可能会内联 allTheSame 并产生一个非常紧密的内循环,完全绕过 Eq 字典查找 .

    • worker函数是尾递归的 .

    判决:非常有力的竞争者 .

    Solution 1

    allTheSame' :: (Eq a) => [a] -> Bool
    allTheSame' xs = (length xs) == (length $ takeWhile (== head xs) xs)
    

    即使不看核心我也知道这不会那么好 . 该列表不止一次遍历,首先是 length xs ,然后是 length $ takeWhile . 您不仅有多次遍历的额外开销,这意味着列表必须在第一次遍历后保留在内存中,并且可以't be GC' d . 对于一个大清单,这是一个严重的问题 .

    Test.allTheSame'
      :: forall a_abF. GHC.Classes.Eq a_abF => [a_abF] -> GHC.Bool.Bool
    [GblId,
     Arity=2,
     Str=DmdType LS,
     Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
             ConLike=True, Cheap=True, Expandable=True,
             Guidance=IF_ARGS [3 3] 20 0}]
    Test.allTheSame' =
      \ (@ a_awF)
        ($dEq_awG :: GHC.Classes.Eq a_awF)
        (xs_abI :: [a_awF]) ->
        case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
        case GHC.List.$wlen
               @ a_awF
               (GHC.List.takeWhile
                  @ a_awF
                  (let {
                     ds_sDq :: a_awF
                     [LclId, Str=DmdType]
                     ds_sDq =
                       case xs_abI of _ {
                         [] -> GHC.List.badHead @ a_awF; : x_axk ds1_axl -> x_axk
                       } } in
                   \ (ds1_dxa :: a_awF) ->
                     GHC.Classes.== @ a_awF $dEq_awG ds1_dxa ds_sDq)
                  xs_abI)
               0
        of ww1_XCn { __DEFAULT ->
        GHC.Prim.==# ww_aC6 ww1_XCn
        }
        }
    

    看看核心并不能说明这一点 . 但请注意以下几行:

    case GHC.List.$wlen @ a_awF xs_abI 0 of ww_aC6 { __DEFAULT ->
            case GHC.List.$wlen
    

    这是列表遍历发生的地方 . 第一个获取外部列表的长度并将其绑定到 ww_aC6 . 第二个获取内部列表的长度,但绑定不会发生,直到接近底部,在

    of ww1_XCn { __DEFAULT ->
    GHC.Prim.==# ww_aC6 ww1_XCn
    

    长度(均为 Int s)可以拆箱并通过primop进行比较,但是已经引入了's a small consolation after the overhead that' .

    判决结果:不好 .

    Solution 2

    allTheSame'' :: (Eq a) => [a] -> Bool
    allTheSame'' xs
      | n == 0 = False
      | n == 1 = True
      | n == 2 = xs !! 0 == xs !! 1
      | otherwise = (xs !! 0 == xs !! 1) && (allTheSame'' $ snd $ splitAt 2 xs)
        where  n = length xs
    

    这与解决方案1具有相同的问题 . 列表被遍历多次,并且不能进行GC . 但这里更糟糕,因为现在计算每个子列表的长度 . 我希望这对任何重要大小的列表都有最糟糕的表现 . 另外,当你期望列表很大时,你为什么要特别设置1和2元素的套管列表?

    判决:甚至不要考虑它 .

    Solution 3

    allTheSame''' :: (Eq a) => [a] -> Bool
    allTheSame''' xs
      | n == 0 = False
      | n == 1 = True
      | n == 2 = xs !! 0 == xs !! 1
      | n == 3 = xs !! 0 == xs !! 1 && xs !! 1 == xs !! 2
      | otherwise = allTheSame''' (fst split) && allTheSame''' (snd split)
        where n = length xs
              split = splitAt (n `div` 2) xs
    

    这与解决方案2具有相同的问题 . 即,列表被 length 遍历多次 . 我不确定分而治之的方法对于这个问题是一个很好的选择,它最终可能比简单的扫描花费更长的时间 . 这取决于数据,值得测试 .

    结论:也许,如果你使用了不同的数据结构 .

    Solution 4

    allTheSame'''' :: (Eq a) => [a] -> Bool
    allTheSame'''' xs = all (== head xs) (tail xs)
    

    这基本上是我的第一个念头 . 让我们再次检查核心 .

    Test.allTheSame''''
      :: forall a_abC. GHC.Classes.Eq a_abC => [a_abC] -> GHC.Bool.Bool
    [GblId,
     Arity=2,
     Str=DmdType LS,
     Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
             ConLike=True, Cheap=True, Expandable=True,
             Guidance=IF_ARGS [3 3] 10 0}]
    Test.allTheSame'''' =
      \ (@ a_am5)
        ($dEq_am6 :: GHC.Classes.Eq a_am5)
        (xs_alK :: [a_am5]) ->
        case xs_alK of _ {
          [] ->
            GHC.List.tail1
            `cast` (CoUnsafe (forall a1_axH. [a1_axH]) GHC.Bool.Bool
                    :: (forall a1_axH. [a1_axH]) ~ GHC.Bool.Bool);
          : ds1_axJ xs1_axK ->
            GHC.List.all
              @ a_am5
              (\ (ds_dwU :: a_am5) ->
                 GHC.Classes.== @ a_am5 $dEq_am6 ds_dwU ds1_axJ)
              xs1_axK
        }
    

    好的,还不错 . 与解决方案1一样,这将在空列表上出错 . 列表遍历隐藏在 GHC.List.all 中,但它可能会在呼叫站点扩展为良好的代码 .

    判决:另一个强有力的竞争者 .

    因此,在所有这些之间,我希望解决方案0和4是唯一值得使用的,并且它们几乎相同 . 在某些情况下我可能会考虑选项3 .

    编辑:在这两种情况下,空列表中的错误可以简单地修复,如@ augustss的答案 .

    下一步是使用criterion进行一些时间分析 .

  • 11

    使用连续对的解决方案:

    allTheSame xs = and $ zipWith (==) xs (tail xs)
    
  • 28

    Q1 - 是的,我认为你的简单解决方案很好,没有内存泄漏 . Q4 - 解决方案3不是log(n),通过非常简单的参数,您需要查看所有列表元素以确定它们是否相同,并且查看1个元素需要1个时间步长 . Q5 - 是的 . Q6,见下文 .

    解决这个问题的方法是输入并运行它

    main = do
        print $ allTheSame (replicate 100000000 1)
    

    然后运行 ghc -O3 -optc-O3 --make Main.hs && time ./Main . 我最喜欢上一个解决方案(你也可以使用模式匹配来清理它),

    allTheSame (x:xs) = all (==x) xs
    

    打开ghci并在这些东西上运行“:step fcn” . 它将教你很多关于懒惰评估正在扩展的内容 . 通常,当您匹配构造函数时,例如“x:xs”,这是恒定的时间 . 当你调用“length”时,Haskell需要计算列表中的所有元素(尽管它们的值仍然是“待计算”),因此解决方案1和2都很糟糕 .

    编辑1

    对不起,如果我之前的回答有点浅薄 . 似乎手动扩展的东西确实有点帮助(虽然与其他选项相比,这是一个微不足道的改进),

    {-# LANGUAGE BangPatterns #-}
    allTheSame [] = True
    allTheSame ((!x):xs) = go x xs where
        go !x [] = True
        go !x (!y:ys) = (x == y) && (go x ys)
    

    似乎ghc已经专门化了这个函数,但你也可以查看specialize pragma,以防它对你的代码不起作用[link] .

  • 20

    这是另一个版本(如果某些内容不匹配,则不需要遍历整个列表):

    allTheSame [] = True
    allTheSame (x:xs) = isNothing $ find (x /= ) xs
    

    这可能在语法上不正确,但我希望你有这个想法 .

  • 0

    这是另一种有趣的方式:

    {-# INLINABLE allSame #-}
    allSame :: Eq a => [a] -> Bool
    allSame xs = foldr go (`seq` True) xs Nothing where
      go x r Nothing = r (Just x)
      go x r (Just prev) = x == prev && r (Just x)
    

    通过跟踪前一个元素而不是第一个元素,可以轻松地更改此实现以实现 increasingdecreasing . 要针对第一个检查所有这些,您可以将 prev 重命名为 first ,并将 Just x 替换为 Just first .


    这将如何优化?我没有详细检查,但我要去了根据我对GHC优化的一些了解,讲述一个好故事 .

    首先假设列表融合没有发生 . 那么 foldr 将被内联,给出类似的东西

    allSame xs = allSame' xs Nothing where
      allSame' [] = (`seq` True)
      allSame' (x : xs) = go x (allSame' xs)
    

    Eta扩张然后收益

    allSame' [] acc = acc `seq` True
    allSame' (x : xs) acc = go x (allSame' xs) acc
    

    内联 go

    allSame' [] acc = acc `seq` True
    allSame' (x : xs) Nothing = allSame' xs (Just x)
    allSame' (x : xs) (Just prev) =
      x == prev && allSame' xs (Just x)
    

    现在GHC可以识别递归调用 Maybe 值总是 Just ,并使用worker-wrapper转换来利用这个:

    allSame' [] acc = acc `seq` True
    allSame' (x : xs) Nothing = allSame'' xs x
    allSame' (x : xs) (Just prev) = x == prev && allSame'' xs x
    
    allSame'' [] prev = True
    allSame'' (x : xs) prev = x == prev && allSame'' xs x
    

    现在请记住

    allSame xs = allSame' xs Nothing
    

    并且 allSame' 不再是递归的,因此可以降低beta:

    allSame [] = True
    allSame (x : xs) = allSame'' xs x
    
    allSame'' [] _ = True
    allSame'' (x : xs) prev = x == prev && allSame'' xs x
    

    因此,高阶代码已经转变为有效的递归代码而没有额外的分配 .

    使用 -O2 -ddump-simpl -dsuppress-all -dno-suppress-type-signatures 编译定义 allSame 的模块会产生以下结果(我已将其清理了一下):

    allSame :: forall a. Eq a => [a] -> Bool
    allSame =
      \ (@ a) ($dEq_a :: Eq a) (xs0 :: [a]) ->
        let {
          equal :: a -> a -> Bool
          equal = == $dEq_a } in
        letrec {
          go :: [a] -> a -> Bool
          go =
            \ (xs :: [a]) (prev :: a) ->
              case xs of _ {
                [] -> True;
                : y ys ->
                  case equal y prev of _ {
                    False -> False;
                    True -> go ys y
                  }
              }; } in
        case xs0 of _ {
          [] -> True;
          : x xs -> go xs x
        }
    

    如您所见,这与我描述的结果基本相同 . equal = == $dEq_a 位是从 Eq 字典中提取相等方法并保存在变量中的位置,因此只需要提取一次 .


    如果列表融合发生怎么办?以下是对定义的提醒:

    allSame xs = foldr go (`seq` True) xs Nothing where
      go x r Nothing = r (Just x)
      go x r (Just prev) = x == prev && r (Just x)
    

    如果我们调用 allSame (build g)foldr 将根据规则 foldr c n (build g) = g c nbuild 融合,产生

    allSame (build g) = g go (`seq` True) Nothing
    

    除非 g 已知,否则这并不能让我们感到有趣 . 所以让我们选择一些简单的东西:

    replicate k0 a = build $ \c n ->
      let
        rep 0 = n
        rep k = a `c` rep (k - 1)
      in rep k0
    

    所以,如果 h = allSame (replicate k0 a)h 成为

    let
      rep 0 = (`seq` True)
      rep k = go a (rep (k - 1))
    in rep k0 Nothing
    

    Eta扩张,

    let
      rep 0 acc = acc `seq` True
      rep k acc = go a (rep (k - 1)) acc
    in rep k0 Nothing
    

    内联 go

    let
      rep 0 acc = acc `seq` True
      rep k Nothing = rep (k - 1) (Just a)
      rep k (Just prev) = a == prev && rep (k - 1) (Just a)
    in rep k0 Nothing
    

    再次,GHC可以看到递归调用总是 Just ,所以

    let
      rep 0 acc = acc `seq` True
      rep k Nothing = rep' (k - 1) a
      rep k (Just prev) = a == prev && rep' (k - 1) a
      rep' 0 _ = True
      rep' k prev = a == prev && rep' (k - 1) a
    in rep k0 Nothing
    

    由于 rep 不再递归,GHC可以减少它:

    let
      rep' 0 _ = True
      rep' k prev = a == prev && rep' (k - 1) a
    in
      case k0 of
        0 -> True
        _ -> rep' (k - 1) a
    

    正如您所看到的,这可以在没有任何分配的情况下运行!显然,这是一个愚蠢的例子,但在许多更有趣的案例中会发生类似的事情 . 例如,如果您编写导入 allSame 函数并定义的 AllSameTest 模块

    foo :: Int -> Bool
    foo n = allSame [0..n]
    

    并按上述方法编译它,您将得到以下内容(未清理) .

    $wfoo :: Int# -> Bool
    $wfoo =
      \ (ww_s1bY :: Int#) ->
        case tagToEnum# (># 0 ww_s1bY) of _ {
          False ->
            letrec {
              $sgo_s1db :: Int# -> Int# -> Bool
              $sgo_s1db =
                \ (sc_s1d9 :: Int#) (sc1_s1da :: Int#) ->
                  case tagToEnum# (==# sc_s1d9 sc1_s1da) of _ {
                    False -> False;
                    True ->
                      case tagToEnum# (==# sc_s1d9 ww_s1bY) of _ {
                        False -> $sgo_s1db (+# sc_s1d9 1) sc_s1d9;
                        True -> True
                      }
                  }; } in
            case ww_s1bY of _ {
              __DEFAULT -> $sgo_s1db 1 0;
              0 -> True
            };
          True -> True
        }
    
    foo :: Int -> Bool
    foo =
      \ (w_s1bV :: Int) ->
        case w_s1bV of _ { I# ww1_s1bY -> $wfoo ww1_s1bY }
    

    这可能看起来很恶心,但是你会注意到在任何地方都没有 : 构造函数,并且 Int 都是未装箱的,因此该函数可以在零分配的情况下运行 .

  • 4

    我想我可能只是在实现 find 并重做this . 不过,我认为看到它的内部结构是有益的 . (注意解决方案如何依赖于等式是可传递的,尽管还要注意问题如何要求相等的传递是连贯的 . )

    sameElement x:y:xs = if x /= y then Nothing else sameElement y:xs
    sameElement [x] = Just x
    allEqual [] = True
    allEqual xs = isJust $ sameElement xs
    

    我喜欢 sameElement 如何查看列表的第一个O(1)元素,然后返回结果或递归列表的某些后缀,特别是尾部 . 我对这个结构没有任何明智的说法,我只是喜欢它:-)

    我想我和this做了同样的比较 . 如果我用 sameElement x:xs 递归,我会将输入列表的头部与解决方案0中的每个元素进行比较 .

    切线:如果需要,可以通过将 Nothing 替换为 Left (x, y) 并将 Just x 替换为 Right x 并将 isJust 替换为 either (const False) (const True) 来报告两个不匹配的元素 .

  • 6

    虽然效率不高(即使前两个元素不匹配,它也会遍历整个列表),这是一个厚颜无耻的解决方案:

    import Data.List (group)
    
    allTheSame :: (Eq a) => [a] -> Bool
    allTheSame = (== 1) . length . group
    

    纯娱乐 .

  • 0

    这种实现是优越的 .

    allSame [ ] = True
    allSame (h:t) = aux h t
    
    aux x1 [ ]                 = True
    aux x1 (x2:xs) | x1==x2    = aux x2 xs 
                   | otherwise = False
    

    给定(==)运算符的传递性,如果您希望确保表达式链的相等性(例如a = b = c = d),则假设Eq的实例已得到很好的实现,您只需要确保a = b,b = c,c = d,并且d = a,代替上面提供的技术,例如a = b,a = c,a = d,b = c,b = d,c = d .

    我提出的解决方案与您希望测试的元素数量线性增长,后者是二次方,即使您引入常数因素以期提高其效率 .

    它也优于使用组的解决方案,因为您最终不必使用长度 .

    你也可以用逐点的方式写出来,但我不会厌烦你这么琐碎的细节 .

相关问题