単一リンクリストを作成することにし、内部リンクノード構造を不変にする計画を立てました。
私は思わぬ障害に出くわしました。次のリンクされたノードがあるとします(以前のadd
操作から):
1 -> 2 -> 3 -> 4
5
を追加したいと言います。
これを行うには、ノード4
は不変なので、4
の新しいコピーを作成する必要がありますが、そのnext
フィールドを5
を含む新しいノードに置き換えます。問題は、3
が古い4
を参照していることです。 5
が追加されていないもの。次に、3
をコピーし、そのnext
フィールドを置き換えて4
コピーを参照する必要がありますが、2
が古い3
を参照しています...
つまり、追加を行うには、リスト全体をコピーする必要があるようです。
私の質問:
私の考えは正しいですか?構造全体をコピーせずに追加を行う方法はありますか?
どうやら「Effective Java」には推奨が含まれています:
クラスを変更可能にする十分な理由がない限り、クラスは不変でなければなりません...
これは可変性の良い例ですか?
私はリスト自体について話していないので、これは提案された回答の複製ではないと思います。それは明らかにインターフェースに準拠するために変更可能である必要があります(新しいリストを内部で保持し、ゲッターを介して取得するようなことをせずに。ただし、考え直しても、いくつかの変更が必要になるため、最小限に抑えます)。リストの内部が不変でなければならないかどうかについて話している。
関数型言語のリストでは、ほとんどの場合、ヘッドとテール、最初の要素、およびリストの残りの部分を操作します。ご想像のとおり、追加ではリスト全体(またはリンクされたリストに正確に似ていない他の遅延データ構造)をコピーする必要があるため、先頭に追加する方がはるかに一般的です。
命令型言語では、意味的により自然に感じる傾向があり、以前のバージョンのリストへの参照を無効にする必要がないため、追加がはるかに一般的です。
先頭に追加することでリスト全体をコピーする必要がない理由の例として、
2 -> 3 -> 4
1
を付加すると、次のようになります。
1 -> 2 -> 3 -> 4
ただし、リストは不変であり、リンクは一方向にしか移動しないため、他の誰かがリストの先頭として2
への参照を保持しているかどうかは関係ありません。 1
への参照しかない場合、2
が存在することを確認する方法はありません。さて、どちらかのリストに5
を追加した場合、リスト全体のコピーを作成する必要があります。そうしないと、他のリストにも表示されるためです。
正解です。ノードをインプレースで変更したくない場合は、リスト全体をコピーする必要があります。最後から2番目(最後)のノードのnext
ポインターを設定する必要があるため、不変の設定で新しいノードが作成され、最後から3番目のノードのnext
ポインターを設定する必要があります。オン。
ここでの中心的な問題は不変性ではなく、append
の操作が不適切であることはないと思います。どちらも、それぞれのドメインで完全に問題ありません。 ミキシングそれらは悪い:リストの前で操作を強調する不変リストの自然な(効率的な)インターフェースですが、可変リストの場合は、からのアイテムを連続して追加してリストを作成する方が自然なことがよくあります最初から最後まで。
したがって、私はあなたがあなたの決心をすることを提案します:あなたは短命なインターフェースまたは永続的なインターフェースを望みますか?ほとんどの操作で新しいリストを作成し、変更されていないバージョンをアクセス可能なまま(永続的)にしますか、または次のようなコード(エフェメラル)を記述しますか?
list.append(x);
list.prepend(y);
どちらの選択も問題ありませんが、実装はインターフェースをミラーリングする必要があります。永続的なデータ構造は不変のノードからメリットを得ますが、一時的なものは暗黙的に行うパフォーマンスの約束を実際に満たすために内部の可変性を必要とします。 Java.util.List
およびその他のインターフェースは短命です。不変のリストにそれらを実装することは不適切であり、実際にはパフォーマンスの危険があります。可変データ構造の優れたアルゴリズムは、不変データ構造の優れたアルゴリズムとはかなり異なることが多いため、不変データ構造を可変データ構造(またはその逆)にドレスアップすると、悪いアルゴリズムが発生します。
永続的なリストにはいくつかの欠点があります(効率的な追加はありません)が、機能的にプログラミングするときに深刻な問題である必要はありません。多くのアルゴリズムは、マインドセットをシフトし、map
やfold
などの高次関数を使用して効率的に定式化できますプリミティブのもの)、または前置を繰り返し使用します。さらに、このデータ構造のみを使用するように強制されている人はいません。他のもの(エフェメラル、または永続的ですがより洗練されている)がより適切な場合は、それらを使用してください。また、永続的なリストには他のワークロードに対していくつかの利点があることに注意してください。それらはテールを共有するため、メモリを節約できます。
あなたは必ず見る必要があります この素晴らしいビデオ 2015年からReact conf by Immutable.js の作成者、リーバイロン。ポインタと構造を提供しますコンテンツを複製するしない効率的な不変リストを実装する方法を理解するための基本的な考え方は次のとおりです:-2つのリストが同じノードを使用する限り(同じ値) 、同じ次のノード)、同じノードが使用されます-リストが異なる状態で開始すると、分岐のノードに構造が作成され、各リストの次の特定のノードへのポインタが保持されます
反応チュートリアル からのこの画像は、私の壊れた英語よりもはっきりしているかもしれません:
単一にリンクされたリストがある場合は、背面よりも前面を操作します。
プロローグやハスケルのような関数型言語は、フロントエレメントと配列の残りの部分を取得する簡単な方法を提供します。後ろに追加するのは、各ノードをコピーするO(n)操作です。
他の人が指摘したように、不変の単一リンクリストでは、追加操作を実行するときにリスト全体をコピーする必要があることは正しいです。
アルゴリズムを実装する回避策をcons
(プリペンド)演算で使用し、最終的なリストを1度反転させることができます。これでもリストを1回コピーする必要がありますが、複雑さのオーバーヘッドはリストの長さに比例しますが、追加を繰り返し使用することで簡単に2次の複雑さを得ることができます。
差分リスト(たとえば、 ここ を参照)は興味深い代替手段です。差分リストはリストをラップし、一定の時間で追加操作を提供します。追加する必要がある限り、基本的にラッパーを操作し、終了したらリストに変換します。これは、StringBuilder
を使用して文字列を作成し、最後にString
を呼び出してtoString
(不変!)として結果を取得する場合と似ています。 。 1つの違いは、StringBuilder
は変更可能ですが、違いのリストは不変です。また、差分リストをリストに戻す場合でも、新しいリスト全体を作成する必要がありますが、これも1回だけ実行する必要があります。
HaskellのData.DList
と同様のインターフェースを提供する不変のDList
クラスを実装するのは非常に簡単です。
厳密にはJavaではありませんが、Scalaで記述された不変でパフォーマンスの高いインデックス付き永続データ構造について、この記事を読むことをお勧めします。
http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala
これはScalaデータ構造なので、Javaからも使用できます(詳細度は少し高くなります)。Clojureで利用可能なデータ構造に基づいています。そして、もっと「ネイティブ」なJavaライブラリも提供していると思います。
また、不変のデータ構造の構築に関する注記:通常必要なのは、ある種の「ビルダー」です。これにより、「アンダー」を「変更」できます要素を(単一スレッド内に)追加することによる「構築」データ構造。追加が完了したら、.build()
や.result()
などの「アンダーコンストラクション」オブジェクトのメソッドを呼び出し、オブジェクトを「構築」して不変のデータ構造を提供します安全に共有できます。
時々役立つ可能性のあるアプローチは、2つのクラスのリスト保持オブジェクトを持つことです。つまり、前方リンクリストの最初のノードへのfinal
参照を持つ前方リストオブジェクトと、最初はnull以外の非リストオブジェクトです。 -final
参照(null以外の場合)は、同じアイテムを逆の順序で保持する逆リストオブジェクトと、最後のアイテムへのfinal
参照を持つ逆リストオブジェクトを識別します。逆方向にリンクされたリストと、最初にnullである最終でない参照(null以外の場合)は、同じアイテムを反対の順序で保持するforward-listオブジェクトを識別します。
フォワードリストにアイテムを追加するか、リバースリストにアイテムを追加するには、final
参照で識別されるノードにリンクする新しいノードを作成し、同じタイプの新しいリストオブジェクトを作成するだけです。オリジナル、その新しいノードへのfinal
参照。
フォワードリストにアイテムを追加したり、リバースリストに追加したりするには、反対のタイプのリストが必要です。特定のリストを使用して初めて行われる場合は、反対のタイプの新しいオブジェクトを作成し、参照を保存する必要があります。アクションを繰り返すと、リストが再利用されます。
リストオブジェクトの外部状態は、反対の型のリストへの参照がnullであるか、反対の順序のリストを識別するかに関係なく、同じであると見なされることに注意してください。すべてのリストオブジェクトには、その内容の完全なコピーへのfinal
参照があるため、マルチスレッドコードを使用する場合でも、変数をfinal
にする必要はありません。
あるスレッドのコードがリストの逆のコピーを作成してキャッシュし、そのコピーがキャッシュされるフィールドが揮発性でない場合、別のスレッドのコードがキャッシュされたリストを表示しない可能性がありますが、唯一の悪影響それから、他のスレッドはリストの別の反転コピーを作成する追加の作業を行う必要があるということです。そのようなアクションは効率を最悪に損なうが正確性には影響を及ぼさないため、およびvolatile
変数は独自の効率障害を作成するため、変数を非揮発性にし、時折発生する可能性を受け入れる方が良い場合がよくあります。冗長操作。