の問題
((((a ++ b) ++ c) ++ d) ++ e) ++ f
入れ子です。のアプリケーション(++)
は左にネストされており、それは悪いことです。右入れ子
a ++ (b ++ (c ++ (d ++ (e ++f))))
問題にはなりません。これは、(++)
が次のように定義されているためです。
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
したがって、使用する方程式を見つけるには、実装は式ツリーに飛び込む必要があります
(++)
/ \
(++) f
/ \
(++) e
/ \
(++) d
/ \
(++) c
/ \
a b
左側のオペランドが空かどうかがわかるまで。空でない場合は、頭を上に向けてバブリングしますが、左のオペランドの末尾はそのままにしておくため、連結の次の要素が要求されると、同じ手順が再開されます。
連結が右にネストされている場合、の左のオペランド(++)
は常に上部にあり、空のチェック/ヘッドのバブリングはO(1)です。
ただし、連結が左にネストされ、n
レイヤーが深く、最初の要素に到達するn
場合、結果の要素ごとに(最初のリストからn-1
のもの、2番目のリストからのものなど)、ツリーのノードをトラバースする必要があります。
a = "hello"
で考えてみましょう
hi = ((((a ++ b) ++ c) ++ d) ++ e) ++ f
そして、評価したいと思いtake 5 hi
ます。したがって、最初に、次のことを確認する必要があります
(((a ++ b) ++ c) ++ d) ++ e
空です。そのためには、
((a ++ b) ++ c) ++ d
空です。そのためには、
(a ++ b) ++ c
空です。そのためには、
a ++ b
空です。そのためには、
a
空です。ふぅ。そうではないので、組み立てて再び泡立てることができます
a ++ b = 'h':("ello" ++ b)
(a ++ b) ++ c = 'h':(("ello" ++ b) ++ c)
((a ++ b) ++ c) ++ d = 'h':((("ello" ++ b) ++ c) ++ d)
(((a ++ b) ++ c) ++ d) ++ e = 'h':(((("ello" ++ b) ++ c) ++ d) ++ e)
((((a ++ b) ++ c) ++ d) ++ e) ++ f = 'h':((((("ello" ++ b) ++ c) ++ d) ++ e) ++ f)
そして、のために'e'
、私たちは繰り返さなければなりません、そして'l'
sのためにも...
木の一部を描くと、泡立ちは次のようになります。
(++)
/ \
(++) c
/ \
'h':"ello" b
最初になります
(++)
/ \
(:) c
/ \
'h' (++)
/ \
"ello" b
その後
(:)
/ \
'h' (++)
/ \
(++) c
/ \
"ello" b
トップに戻るまで。最終的にトップレベルの右の子になるツリーの(:)
構造は、左端のリストが空でない限り、元のツリーの構造とまったく同じです。
(++)
/ \
[] b
ノードはちょうどに折りたたまれますb
。
したがって、短いリストの左にネストされた連結がある場合、連結の先頭を取得することはO(ネストの深さ)操作であるため、連結は2次式になります。一般に、左ネストの連結
(...((a_d ++ a_{d-1}) ++ a_{d-2}) ...) ++ a_2) ++ a_1
O(sum [i * length a_i | i <- [1 .. d]])
完全に評価することです。
差分リスト(説明を簡単にするためのニュータイプラッパーはありません)では、構成が左ネストされているかどうかは重要ではありません
((((a ++) . (b ++)) . (c ++)) . (d ++)) . (e ++)
または右ネスト。ネスティングを通過して、に到達する(a ++)
と、それ(++)
は式ツリーの一番上に持ち上げられます。したがって、の各要素に到達すると、a
再びO(1)になります。
実際、最初の要素が必要になるとすぐに、構成全体が差分リストに再関連付けされます。
((((a ++) . (b ++)) . (c ++)) . (d ++)) . (e ++) $ f
になります
((((a ++) . (b ++)) . (c ++)) . (d ++)) $ (e ++) f
(((a ++) . (b ++)) . (c ++)) $ (d ++) ((e ++) f)
((a ++) . (b ++)) $ (c ++) ((d ++) ((e ++) f))
(a ++) $ (b ++) ((c ++) ((d ++) ((e ++) f)))
a ++ (b ++ (c ++ (d ++ (e ++ f))))
その後、各リストは、(++)
前のリストが消費された後のトップレベルのすぐ左のオペランドになります。
その中で重要なことは、付加関数(a ++)
が引数を検査せずに結果の生成を開始できることです。そのため、
($)
/ \
(.) f
/ \
(.) (e ++)
/ \
(.) (d ++)
/ \
(.) (c ++)
/ \
(a ++) (b ++)
経由
($)---------
/ \
(.) ($)
/ \ / \
(.) (d ++) (e ++) f
/ \
(.) (c ++)
/ \
(a ++) (b ++)
に
($)
/ \
(a ++) ($)
/ \
(b ++) ($)
/ \
(c ++) ($)
/ \
(d ++) ($)
/ \
(e ++) f
最終リストの構成された関数について何も知る必要がないf
ので、それは単なるO(depth)
書き直しです。次にトップレベル
($)
/ \
(a ++) stuff
になります
(++)
/ \
a stuff
のすべての要素をa
1つのステップで取得できます。この例では、純粋な左ネストがあり、1回の書き換えのみが必要です。その場所の関数が(たとえば)(d ++)
左にネストされたコンポジションであった(((g ++) . (h ++)) . (i ++)) . (j ++)
場合、、トップレベルの再関連付けはそれをそのままにし、($)
以前のすべてのリストの後でトップレベルの左オペランドになると再関連付けされます消費されました。
すべての再関連付けに必要な合計作業量はO(number of lists)
であるため、連結の全体的なコストはですO(number of lists + sum (map length lists))
。(つまり、深く左にネストされたものをたくさん挿入することで、これにも悪いパフォーマンスをもたらす可能性があります([] ++)
。)
The
newtype DiffList a = DiffList {getDiffList :: [a] -> [a] }
instance Monoid (DiffList a) where
mempty = DiffList (\xs -> [] ++ xs)
(DiffList f) `mappend` (DiffList g) = DiffList (\xs -> f (g xs))
抽象的に処理する方が便利なように、それをラップするだけです。
DiffList (a ++) `mappend` DiffList (b ++) ~> DiffList ((a ++) . (b++))
出力の生成を開始するために引数を検査する必要がない関数に対してのみ効率的であることに注意してください。任意の関数がDiffList
sでラップされている場合、そのような効率の保証はありません。特に、(ラップされているかどうかに関係なく)追加すると、右ネストで構成された場合(++ a)
の左ネストツリーが作成される可能性があるため、コンストラクターが公開されている場合は、それを使用して連結動作を作成できます。(++)
O(n²)
DiffList