問題のコードがレベル番号を割り当てる理由を最初に説明したいと思います。これにより、2 つの異なるソリューションに直接導かれます。1 つはキャッシュに渡され、もう 1 つは一度に 2 つのトラバーサルを実行することに基づいています。最後に、2 番目の解決策が他の回答によって提供される解決策とどのように関連するかを示します。
質問のコードで何を変更する必要がありますか?
問題のコードは、レベル番号を各ノードに割り当てます。number'
関数の再帰ケースを見ると、コードがそのように動作する理由を理解できます。
number' a (Node x xl xr) = Node (a,x) (number' (a+1) xl) (number' (a+1) xr)
a + 1
両方の再帰呼び出しに同じ番号 を使用していることに注意してください。したがって、両方のサブツリーのルート ノードには同じ番号が割り当てられます。各ノードに異なる番号を持たせたい場合は、異なる番号を再帰呼び出しに渡す方がよいでしょう。
再帰呼び出しに渡すべき番号は?
左から右への事前順序トラバーサルに従って番号を割り当てたい場合a + 1
、左のサブツリーの再帰呼び出しは正しいですが、右のサブツリーの再帰呼び出しは正しくありません。代わりに、左側のサブツリー全体に注釈を付けるのに十分な数を除外し、次の番号で右側のサブツリーに注釈を付け始めます。
左のサブツリーに予約する必要がある数はいくつですか? これは、この関数によって計算されるサブツリーのサイズによって異なります。
size :: Tree a -> Int
size Empty = 0
size (Node _ xl xr) = 1 + size xl + size xr
関数の再帰ケースに戻りnumber'
ます。左のサブツリーのどこかに注釈が付けられている最小の数値はa + 1
です。左のサブツリーのどこかに注釈が付けられている最大の数字はa + size xl
です。したがって、右のサブツリーで使用できる最小の数は ですa + size xl + 1
。number'
この推論は、正しく機能する再帰ケースの次の実装につながります。
number' :: Int -> Tree a -> Tree (Int, a)
number' a Empty = Empty
number' a (Node x xl xr) = Node (a,x) (number' (a+1) xl) (number' (a + size xl + 1) xr)
残念ながら、このソリューションには問題があります。不必要に遅くなります。
ソリューションがsize
遅いのはなぜですか?
関数size
はツリー全体をトラバースします。この関数number'
はツリー全体もトラバースし、size
すべての左側のサブツリーを呼び出します。これらの各呼び出しは、サブツリー全体をトラバースします。したがって、全体として、関数size
は同じノードで複数回実行されますが、もちろん常に同じ値が返されます。
を呼び出すときにツリーをトラバースしないようにするにはどうすればよいsize
でしょうか?
私は 2 つの解決策を知っています:size
すべてのツリーのサイズをキャッシュすることで の実装でツリーをトラバースするのを避けるかsize
、ノードに番号を付けて 1 回のトラバースでサイズを計算することで最初の呼び出しを避けるかのいずれかです。
ツリーをトラバースせずにサイズを計算するにはどうすればよいでしょうか?
すべてのツリー ノードにサイズをキャッシュします。
data Tree a = Empty | Node Int a (Tree a) (Tree a) deriving (Show)
size :: Tree a -> Int
size Empty = 0
size (Node n _ _ _) = n
Node
の場合size
、キャッシュされたサイズを返すだけであることに注意してください。したがって、このケースは再帰的でsize
はなく、ツリーをトラバースしませんnumber'
。上記の実装の問題は解消されます。
しかし、に関する情報size
はどこかから来なければなりません! を作成するたびにNode
、キャッシュを埋めるために正しいサイズを提供する必要があります。このタスクをスマート コンストラクターに任せることができます。
empty :: Tree a
empty = Empty
node :: a -> Tree a -> Tree a -> Tree a
node x xl xr = Node (size xl + size xr + 1) x xl xr
leaf :: a -> Tree a
leaf x = Node 1 x Empty Empty
node
本当に必要なのはOnly だけですが、完全を期すために他の 2 つを追加しました。これら 3 つの関数のいずれかを使用してツリーを作成すると、キャッシュされたサイズ情報は常に正確になります。
number'
これらの定義で動作するのバージョンは次のとおりです。
number' :: Int -> Tree a -> Tree (Int, a)
number' a Empty = Empty
number' a (Node _ x xl xr) = node (a,x) (number' (a+1) xl) (number' (a + size xl + 1) xr)
2 つのことを調整する必要があります。 でパターン マッチングを行う場合Node
、サイズ情報を無視します。を作成するときNode
は、スマート コンストラクターを使用しnode
ます。
これは問題なく機能しますが、ツリーの定義を変更しなければならないという欠点があります。いずれにせよ、サイズをキャッシュすることは良い考えかもしれませんが、メモリをいくらか使用し、ツリーを有限にする必要があります。number'
ツリーの定義を変更せずに断食を実装したい場合はどうすればよいでしょうか? これにより、私が約束した 2 番目の解決策にたどり着きます。
サイズを計算せずにツリーに番号を付けるにはどうすればよいでしょうか?
できない。しかし、ツリーに番号を付けてサイズを計算することで、1 回のトラバーサルで複数回のsize
呼び出しを回避できます。
number' :: Int -> Tree a -> (Int, Tree (Int, a))
すでに型シグネチャで、このバージョンの がnumber'
2 つの情報を計算していることがわかります。結果のタプルの最初のコンポーネントはツリーのサイズで、2 番目のコンポーネントは注釈付きツリーです。
number' a Empty = (0, Empty)
number' a (Node x xl xr) = (sl + sr + 1, Node (a, x) yl yr) where
(sl, yl) = number' (a + 1) xl
(sr, yr) = number' (a + sl + 1) xr
実装は、再帰呼び出しからタプルを分解し、結果のコンポーネントを構成します。は前のソリューションのようであり、 のようsl
であることに注意してください。注釈付きサブツリーにも名前を付ける必要があります。はノード番号を持つ左側のサブツリーであるため、前のソリューションと同様であり、ノード番号を持つ右側のサブツリーであるため、前のソリューションと同様です。size xl
sr
size xr
yl
number' ... xl
yr
number' ... xr
number
の結果の 2 番目のコンポーネントのみを返すように変更する必要もありますnumber'
。
number :: Tree a -> Tree (Int, a)
number = snd . number' 1
ある意味、これが最も明確な解決策だと思います。
他に何を改善できるでしょうか?
前のソリューションは、サブツリーのサイズを返すことで機能します。次に、その情報を使用して、次に利用可能なノード番号を計算します。代わりに、次に利用可能なノード番号を直接返すこともできます。
number' a Empty = (a, Empty)
number' a (Node x xl xr) = (ar, Node (a, x) yl yr) where
(al, yl) = number' (a + 1) xl
(ar, yr) = number' al xr
は前のソリューションと同様であり、とal
同様であることに注意してください。明らかに、この変更によりいくつかの追加が回避されます。a + sl + 1
ar
a + sl + sr + 1
これは本質的にセルゲイの答えからの解決策であり、これがほとんどの Haskeller が作成するバージョンであると期待しています。a
、al
およびの操作をar
状態モナドで非表示にすることもできますが、そのような小さな例では実際には役に立たないと思います。Ankur による回答は、それがどのように見えるかを示しています。