web-dev-qa-db-ja.com

(大きな)リストのすべての要素が同じであることを効率的にチェックする

問題

リストxs(おそらく非常に大きいもの)があり、そのすべての要素が同じであることを確認するとします。

私はさまざまなアイデアを思いつきました:

ソリューション0

_tail xs_のすべての要素が_head xs_と等しいことを確認します。

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

解決策1

_length xs_が_head xs_と等しいときに、xsから要素を取得して取得したリストの長さに等しいことを確認する

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

解決策2

再帰的ソリューション:allTheSameは、Trueの最初の2つの要素が等しい場合にxsを返し、allTheSameTrueの残りの部分でxsを返す場合

_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)
_

ご質問

  1. mapは要素にandを適用する前に別のリストを作成するため、ソリューション0は少なくともメモリの点では効率的ではないと思います。私は正しいですか?

  2. takeWhileが再び追加のリストを作成するため、ソリューション1は、少なくともメモリの点では依然として非常に効率的ではありません。私は正しいですか?

  3. ソリューション2は末尾再帰(正しい?)であり、_(xs !! 0 == xs !! 1)_がFalseになるとすぐにFalseを返すため、かなり効率的です。私は正しいですか?

  4. 複雑さはO(log n)である必要があるため、ソリューション3が最適です。

  5. ソリューション4は私にはかなりHaskellishのように見えますが(そうですか?)、_all p = and . map p_(Prelude.hsから)であるため、おそらくソリューション0と同じです。私は正しいですか?

  6. allTheSameを書く他のより良い方法はありますか?今、誰かがこの質問に答えて、これを行う組み込み関数があることを教えてくれることを期待しています:私はhoogleで検索しましたが、見つかりませんでした。とにかく、私はハスケルを学んでいるので、これは私にとって良い練習だったと思います:)

他のコメントは大歓迎です。ありがとうございました!

28
MarcoS

gatoatigradoの答えは、さまざまなソリューションのパフォーマンスを測定するための素晴らしいアドバイスです。ここに、より象徴的な答えがあります。

ソリューション0(または完全に同等、ソリューション4)が最速だと思います。 Haskellはlazyであるため、mapを適用する前にandでリスト全体を作成する必要はありません。これについて直観を構築する良い方法は、無限で遊ぶことです。だから例えば:

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

これは、すべての数値が1,000未満であるかどうかを尋ねます。 mapandが適用される前にリスト全体を作成した場合、この質問に答えることはできません。リストに非常に大きな右端を指定しても、式はすばやく応答します(つまり、リストが無限であるかどうかに応じて、Haskellは「マジック」を実行しません)。

私の例を始めるために、これらの定義を使用しましょう:

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

最後の3の7をチェックする必要がなかった方法をご覧ください。これは、リストをループのように機能させる遅延評価です。他のすべてのソリューションはlength(リストの最後まで進んで答えを返す必要がある)などの高価な関数を使用するため、効率が低下し、無限リストでは機能しません。無限のリストでの作業と効率的な作業は、Haskellで一緒に行われることがよくあります。

28
luqui

まず第一に、あなたはリストを扱いたくないと思います。多くのアルゴリズムは長さの計算に依存していますが、これは悪いことです。 vector パッケージを検討すると、O(1)と比較してO(n)の長さが得られます。リスト。特にボックス化されていない、または格納可能なバリアントを使用できる場合は、ベクトルを使用するとメモリ効率が大幅に向上します。

とは言っても、コード内の走査と使用パターンを考慮する必要があります。 Haskellのリストは、オンデマンドで生成して1回使用できる場合、非常に効率的です。つまり、リストへの参照を保持するべきではありません。このようなもの:

average xs = sum xs / length xs

両方の走査が完了するまで、リスト全体を(sumまたはlengthのいずれかによって)メモリに保持する必要があります。リストの走査を1つのステップで実行できる場合は、はるかに効率的です。

もちろん、すべての要素が等しいかどうかを確認するために、とにかくリストを保持する必要がある場合があります。等しくない場合は、データに対して他の処理を行います。この場合、任意のサイズのリストを使用すると、よりコンパクトなデータ構造(ベクトルなど)を使用したほうがよいでしょう。

これで問題は解決しました。次に、これらの各関数について説明します。コアを示すところ、ghc-7.0.3 -O -ddump-simplで生成されました。また、-O0を指定してコンパイルするときに、Haskellコードのパフォーマンスを判断しないでください。実稼働コードで実際に使用するフラグを使用してコンパイルします。通常、少なくとも-Oと他のオプションも使用します。

ソリューション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がワーカー/ラッパー変換を実行した後、再帰的なワーカー関数はletrec { go_sDvバインディングです。労働者はその議論を調べます。 []の場合、リストの最後に到達してTrueを返します。それ以外の場合は、残りの先頭を最初の要素と比較し、Falseを返すか、リストの残りをチェックします。

他の3つの機能。

  1. mapは完全に融合されており、一時リストを割り当てません。
  2. 定義の上部にあるCheap=Trueステートメントに注目してください。これは、GHCが関数を「安い」と見なし、インライン化の候補と見なすことを意味します。呼び出しサイトで、具体的な引数のタイプを決定できる場合、GHCはおそらくallTheSameをインライン化し、Eq辞書の検索を完全にバイパスして非常にタイトな内部ループを生成します。
  3. ワーカー関数は末尾再帰です。

評決:非常に強い候補。

ソリューション1

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

コアを見ていなくても、これはそれほど良くないことを私は知っています。リストは、最初にlength xsによって、次にlength $ takeWhileによってトラバースされます。複数のトラバーサルのオーバーヘッドが増えるだけでなく、リストは最初のトラバーサル後にメモリに保持する必要があり、GCを実行できません。大きなリストの場合、これは深刻な問題です。

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にバインドします。 2番目は内部リストの長さを取得しますが、バインドは最後の近くまで行われません。

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

長さ(両方のInts)はボックス化解除してprimopで比較できますが、これは導入されたオーバーヘッドの後の小さな慰めです。

評決:良くない。

ソリューション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要素の特別なケースのリストがあるのですか?

評決:それについてさえ考えないでください。

ソリューション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によって複数回トラバースされます。分割統治法がこの問題に適しているかどうかはわかりませんが、単純なスキャンよりも時間がかかる可能性があります。ただし、データに依存するため、テストする価値があります。

評決:多分、別のデータ構造を使用した場合。

ソリューション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 を使用してしばらくの間プロファイリングを行うことです。

21
John L

連続ペアを使用するソリューション:

allTheSame xs = and $ zipWith (==) xs (tail xs)
12
tokland

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プラグマも確認できます[ link ]。

6
gatoatigrado

ここに別のバージョンがあります(何かが一致しない場合にリスト全体を走査する必要はありません):

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

これは構文的に正しくない可能性がありますが、理解していただければ幸いです。

4
Ankur

ここに別の楽しい方法があります:

_{-# 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)
_

最初の要素ではなく前の要素を追跡することにより、この実装はincreasingまたはdecreasingを実装するように簡単に変更できます。それらすべてを最初のものと比較してチェックするには、prevの名前をfirstに変更し、_Just x_を_Just first_に置き換えます。


これはどのように最適化されますか?詳細は確認していませんが、GHCの最適化について知っているいくつかのことに基づいて良い話をします。

最初に、リストの融合が発生しないと仮定します。次にfoldrがインライン化され、次のようになります

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

イータ拡張は

_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であることを認識でき、これを利用するためにワーカーラッパー変換を使用します。

_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'_は再帰的ではなくなったため、ベータ削減することができます。

_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辞書から抽出され、変数に保存されるため、一度だけ抽出する必要があります。


リストの融合doesが発生した場合はどうなりますか?定義の注意点は次のとおりです。

_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 nに従ってbuildと融合し、

_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
_

イータ拡張、

_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
_

ご覧のとおり、これはまったく割り当てなしで実行できます。明らかに、これはばかげた例ですが、より多くの興味深いケースで同様のことが起こります。たとえば、AllSameTest関数をインポートして定義するallSameモジュールを作成すると、

_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 }
_

不快に見えるかもしれませんが、どこにも_:_コンストラクターがなく、Intsはすべてボックス化されていないため、関数は割り当てなしで実行できます。

4
dfeuer

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
_

リストの最初のO(1)要素)でsameElementがピークになり、結果を返すか、リストの一部のサフィックス、特にテールで再帰するのが好きです。私はその構造についてスマートに言うことは何もありません、私はそれが好きです:-)

this と同じ比較を行うと思います。代わりに_sameElement x:xs_で再帰した場合、ソリューション0のように、入力リストの先頭を各要素と比較します。

正接:必要に応じて、NothingLeft (x, y)に置き換え、_Just x_を_Right x_およびisJustに置き換えることで、2つの不一致要素を報告できますeither (const False) (const True)

0
Jonas Kölker

あまり効率的ではありませんが(最初の2つの要素が一致しなくてもリスト全体をトラバースします)、ここでは生意気な解決策を示します。

import Data.List (group)

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

ちょうど楽しみのために。

この実装は優れています。

allSame [ ] = True
allSame (h:t) = aux h t

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

(==)演算子の推移性を前提として、式のチェーンの等価性を保証する場合にEqのインスタンスが適切に実装されていると想定します(例:a = b = c = d)、a =のみを保証する必要があります。 b、b = c、c = d、およびそのd = a。上記の提供された手法の代わりに、例えばa = b、a = c、a = d、b = c、b = d、c = d。

私が提案した解決策は、テストしたい要素の数とともに直線的に増加します。後者は、効率を向上させるために一定の因子を導入したとしても、二次式です。

最後に長さを使用する必要がないため、グループを使用するソリューションよりも優れています。

ポイントごとに上手に書くこともできますが、そのような些細な詳細に飽きることはありません。

0
Gonçalo Faria