私はstackoverflowを見回していました 非自明な遅延評価 、それは私をKeegan McAllisterのプレゼンテーションに導きました: なぜHaskellを学ぶのか 。スライド8で、彼は次のように定義された最小関数を示しています。
minimum = head . sort
そして、その複雑さはO(n)であると述べています。置換によるソートがO(nlog n)の場合、複雑さが線形であると言われる理由がわかりません。投稿で参照されている並べ替えは、データについて何も想定していないため、線形にすることはできません。これは、カウント並べ替えなどの線形並べ替え方法で必要になるためです。
ここで遅延評価が不思議な役割を果たしているのでしょうか。もしそうなら、その背後にある説明は何ですか?
_minimum = head . sort
_では、sort
はupfrontで実行されないため、完全には実行されません。 sort
は、head
によって要求される最初の要素を生成するために必要なだけ実行されます。
例:マージソートでは、最初にリストのn
番号がペアごとに比較され、次に勝者がペアになって比較され(_n/2
_番号)、次に新しい勝者(_n/4
_)などになります。全部でO(n)
最小要素を生成するための比較。
_mergesortBy less [] = []
mergesortBy less xs = head $ until (null.tail) pairs [[x] | x <- xs]
where
pairs (x:y:t) = merge x y : pairs t
pairs xs = xs
merge (x:xs) (y:ys) | less y x = y : merge (x:xs) ys
| otherwise = x : merge xs (y:ys)
merge xs [] = xs
merge [] ys = ys
_
上記のコードを拡張して、生成された各数値に、生成されたいくつかの比較をタグ付けすることができます。
_mgsort xs = go $ map ((,) 0) xs where
go [] = []
go xs = head $ until (null.tail) pairs [[x] | x <- xs] where
....
merge ((a,b):xs) ((c,d):ys)
| (d < b) = (a+c+1,d) : merge ((a+1,b):xs) ys -- cumulative
| otherwise = (a+c+1,b) : merge xs ((c+1,d):ys) -- cost
....
g n = concat [[a,b] | (a,b) <- Zip [1,3..n] [n,n-2..1]] -- a little scrambler
_
いくつかのリストの長さで実行すると、実際には_~ n
_であることがわかります:
_*Main> map (fst . head . mgsort . g) [10, 20, 40, 80, 160, 1600]
[9,19,39,79,159,1599]
_
ソートコード自体が_~ n log n
_であるかどうかを確認するために、生成された各数値が独自のコストだけを運ぶようにコードを変更し、ソートされたリスト全体の合計によって合計コストを求めます。
_ merge ((a,b):xs) ((c,d):ys)
| (d < b) = (c+1,d) : merge ((a+1,b):xs) ys -- individual
| otherwise = (a+1,b) : merge xs ((c+1,d):ys) -- cost
_
さまざまな長さのリストの結果は次のとおりです。
_*Main> let xs = map (sum . map fst . mgsort . g) [20, 40, 80, 160, 320, 640]
[138,342,810,1866,4218,9402]
*Main> map (logBase 2) $ zipWith (/) (tail xs) xs
[1.309328,1.2439256,1.2039552,1.1766101,1.1564085]
_
上記は 成長の経験的順序 リストの長さを増やすためにn
を示しています。これは、_~ n log n
_によって通常示されるように急速に減少しています。計算。 このブログ投稿 も参照してください。簡単な相関チェックは次のとおりです。
_*Main> let xs = [n*log n | n<- [20, 40, 80, 160, 320, 640]] in
map (logBase 2) $ zipWith (/) (tail xs) xs
[1.3002739,1.2484156,1.211859,1.1846942,1.1637106]
_
編集:遅延評価は、比喩的には一種の生産者/消費者イディオムと見なすことができます1、仲介として独立したメモ化ストレージを使用します。私たちが書く生産的な定義は、消費者の要求に応じて、少しずつ出力を生成するプロデューサーを定義しますが、それより早くはありません。生成されたものはすべてメモ化されるため、別のコンシューマーが同じ出力を異なるペースで消費した場合、以前にいっぱいになった同じストレージにアクセスします。
ストレージを参照するコンシューマーがなくなると、ガベージコレクションが行われます。最適化を使用すると、コンパイラーは中間ストレージを完全に排除して、中間の人を排除できる場合があります。
1 参照: SimpleGeneratorsv。LazyEvaluation Oleg Kiselyov、Simon Peyton-Jones、AmrSabryによる。
minimum' :: (Ord a) => [a] -> (a, [a])
が、リスト内の最小の要素を、その要素が削除されたリストとともに返す関数であるとします。明らかに、これはO(n)時間で実行できます。次にsort
を次のように定義すると
sort :: (Ord a) => [a] -> [a]
sort xs = xmin:(sort xs')
where
(xmin, xs') = minimum' xs
その場合、遅延評価とは、(head . sort) xs
では最初の要素のみが計算されることを意味します。ご覧のとおり、この要素は単純にペアminimum' xs
(の最初の要素)であり、O(n)時間で計算されます。
もちろん、delnanが指摘しているように、複雑さはsort
の実装に依存します。
head . sort
の詳細に取り組む多くの回答を得ています。さらにいくつかの一般的なステートメントを追加します。
先行評価により、さまざまなアルゴリズムの計算の複雑さが単純な方法で構成されます。たとえば、f . g
の最小上限(LUB)は、f
とg
のLUBの合計である必要があります。したがって、f
とg
をブラックボックスとして扱い、LUBの観点からのみ推論することができます。
ただし、遅延評価では、f . g
はf
とg
のLUBの合計よりも優れたLUBを持つことができます。 LUBを証明するためにブラックボックス推論を使用することはできません。実装とその相互作用を分析する必要があります。
したがって、遅延評価の複雑さは、熱心な評価よりも推論するのがはるかに難しいというよく言われる事実。次のことを考えてみてください。 f . g
の形式のコードの漸近的パフォーマンスを改善しようとしているとします。熱心な言語では、これを行うために従うことができる明白な戦略があります。f
とg
のより複雑なものを選び、最初にそれを改善します。それで成功した場合は、f . g
タスクで成功します。
一方、怠惰な言語では、次のような状況が発生する可能性があります。
f
とg
のより複雑なものを改善しますが、f . g
は改善しません(または悪いになります)。f . g
は、f
またはg
に役立たない(または悪化)方法で改善できます。説明はsort
の実装によって異なり、一部の実装では当てはまりません。たとえば、リストの最後に挿入する挿入ソートでは、遅延評価は役に立ちません。それでは、調べる実装を選択しましょう。簡単にするために、選択ソートを使用しましょう。
_sort [] = []
sort (x:xs) = m : sort (delete m (x:xs))
where m = foldl (\x y -> if x < y then x else y) x xs
_
この関数は明らかにO(n ^ 2)時間を使用してリストをソートしますが、head
はリストの最初の要素のみを必要とするため、sort (delete x xs)
は評価されません。
それはそれほど神秘的ではありません。最初の要素を提供するために、どのくらいのリストを並べ替える必要がありますか?最小要素を見つける必要があります。これは、線形時間で簡単に実行できます。たまたま、sort
の一部の実装では、遅延評価がこれを行います。
これを実際に確認する興味深い方法の1つは、比較関数をトレースすることです。
import Debug.Trace
import Data.List
myCmp x y = trace (" myCmp " ++ show x ++ " " ++ show y) $ compare x y
xs = [5,8,1,3,0,54,2,5,2,98,7]
main = do
print "Sorting entire list"
print $ sortBy myCmp xs
print "Head of sorted list"
print $ head $ sortBy myCmp xs
まず、リスト全体の出力がトレースメッセージとインターリーブされる方法に注意してください。次に、単にヘッドを計算する場合に、トレースメッセージがどのように類似しているかに注意してください。
私はこれをGhciで実行しましたが、正確にはO(n)ではありません。最初の要素を見つけるには15回の比較が必要であり、必要な10個の要素ではありません。しかし、それでもO(n log n)未満です。
編集: Vitusが以下で指摘しているように、10ではなく15の比較を行うことは、O(n)ではないと言うことと同じではありません。理論上の最小値よりも多くかかることを意味しました。
Paul Johnsonの回答に触発されて、2つの関数の成長率をプロットしました。最初に、比較ごとに1文字を出力するように彼のコードを変更しました。
import System.Random
import Debug.Trace
import Data.List
import System.Environment
rs n = do
gen <- newStdGen
let ns = randoms gen :: [Int]
return $ take n ns
cmp1 x y = trace "*" $ compare x y
cmp2 x y = trace "#" $ compare x y
main = do
n <- fmap (read . (!!0)) getArgs
xs <- rs n
print "Sorting entire list"
print $ sortBy cmp1 xs
print "Head of sorted list"
print $ head $ sortBy cmp2 xs
*
文字と#
文字を数えると、等間隔のポイントで比較カウントをサンプリングできます(Pythonを失礼します)。
import matplotlib.pyplot as plt
import numpy as np
import envoy
res = []
x = range(10,500000,10000)
for i in x:
r = envoy.run('./sortCount %i' % i)
res.append((r.std_err.count('*'), r.std_err.count('#')))
plt.plot(x, map(lambda x:x[0], res), label="sort")
plt.plot(x, map(lambda x:x[1], res), label="minimum")
plt.plot(x, x*np.log2(x), label="n*log(n)")
plt.plot(x, x, label="n")
plt.legend()
plt.show()
スクリプトを実行すると、次のグラフが表示されます。
下の線の傾きは..
>>> import numpy as np
>>> np.polyfit(x, map(lambda x:x[1], res), deg=1)
array([ 1.41324057, -17.7512292 ])
..1.41324057(線形関数であると仮定)