リンクされたノード構造を不変にする実用的な方法はありますか?


26

単一リンクリストを作成することにし、内部リンクノード構造を不変にする計画を立てました。

しかし、私は思わぬ障害に遭遇しました。次のリンクされたノード(以前のadd操作から)があるとします。

1 -> 2 -> 3 -> 4

を追加したいと言います5

これを行うには、ノード4は不変なので、の新しいコピーを作成する必要があります4が、そのnextフィールドをを含む新しいノードに置き換えます5。問題は3、古いものを参照していること4です。追加されていないもの5。今、コピーし3、そのnextフィールドを置き換えて4コピーを参照する必要がありますが、今2は古いものを参照してい3ます...

または、言い換えると、追加を行うには、リスト全体をコピーする必要があるようです。

私の質問:

  • 私の考えは正しいですか?構造全体をコピーせずに追加を行う方法はありますか?

  • どうやら「効果的なJava」には推奨事項が含まれています。

    クラスを変更可能にする非常に正当な理由がない限り、クラスは不変でなければなりません...

    これは可変性の良いケースですか?

私はリスト自体について話していないので、これは提案された答えの複製ではないと思います。これは明らかに、インターフェイスに準拠するために変更可能でなければなりません(新しいリストを内部に保持し、getterを介して取得するなどの操作を行わないでください。リストの内部が不変でなければならないかどうかについて話している。


はい-Javaのまれな不変のコレクションは、スローするようにオーバーライドされた変更メソッドを持つもの、またはCopyOnWritexxxマルチスレッドに使用される非常に高価なクラスのいずれかです。コレクションが実際に不変であることを期待する人はいません(ただし、いくつかの奇抜な点はありますが)
Ordous

9
(引用に関するコメント。)それは、素晴らしい引用であることがしばしば誤解されています。ValueObjectには利点があるため、ValueObjectに不変性を適用する必要がありますが、Javaで開発している場合、可変性が予想される場所で不変性を使用することは実用的ではありません。ほとんどの場合、クラスには不変であるべき特定の側面と、要件に基づいて可変である必要がある特定の側面があります。
rwong

2
関連(おそらく重複):コレクションオブジェクトは不変であるほうが良いですか?「パフォーマンスのオーバーヘッドなしで、このコレクション(クラスLinkedList)を不変コレクションとして導入できますか?」
-gnat

不変のリンクリストを作成する理由 リンクされたリストの最大の利点は、可変性です。配列を使用した方が良いと思いませんか?
ピーターB

3
要件を理解してください。変化させたい不変のリストが必要ですか?それは矛盾ではありませんか?リストの「内部」とはどういう意味ですか?以前に追加したノードは後から変更できず、リストの最後のノードへの追加のみを許可するということですか?
nullの

回答:


46

関数型言語のリストでは、ほとんどの場合、先頭と末尾、最初の要素、およびリストの残りの部分を使用します。あなたが推測したように、追加はリスト全体(またはリンクリストに正確に似ていない他の遅延データ構造)をコピーする必要があるため、プリペンディングがより一般的です。

命令型言語では、アペンドははるかに一般的です。これは、セマンティクスにより自然に感じる傾向があるためであり、リストの以前のバージョンへの参照を無効にする必要はありません。

リスト全体をコピーする必要がない理由の例として、次のものがあると考えてください。

2 -> 3 -> 4

を先頭に追加すると、次のこと1ができます。

1 -> 2 -> 3 -> 4

ただし2、リストは不変であり、リンクは一方向にしか移動しないため、他の誰かがまだリストの先頭としてへの参照を保持していても問題ありません。へ1の参照のみを持っている場合でも、そこにあることを伝える方法はありません2。ここで、5いずれかのリストにを追加した場合、リスト全体のコピーを作成する必要があります。そうしないと、他のリストにも表示されるためです。


ああ、プリペンディングがコピーを必要としない理由の良い点です。私はそれを考えていませんでした。
発がん性物質

17

正しいです。ノードをインプレースで変更したくない場合は、リスト全体をコピーする必要があります。我々が設定する必要があるためnextのポインタを(現在は)最後から2番目の不変の設定で新しいノードを作成し、我々が設定する必要があるノード、nextように第三の最後のノードのポインタを、そして。

ここでの中心的な問題は不変性ではなく、append操作が不適切であることでもないと思います。両方とも、それぞれのドメインでまったく問題ありません。それらを混ぜることは悪いです:不変リストの自然な(効率的な)インターフェースはリストの先頭での操作を強調しましたが、可変リストの場合、最初から最後までアイテムを連続して追加することでリストを構築する方が自然です。

したがって、私はあなたがあなたの心を決定することをお勧めします:あなたは一時的なインターフェイスが必要ですか、それとも永続的なインターフェイスが必要ですか?ほとんどの操作で新しいリストが作成され、変更されていないバージョンはアクセス可能(永続的)のままにするか、次のようなコードを記述しますか(一時的)。

list.append(x);
list.prepend(y);

両方の選択は問題ありませんが、実装はインターフェースをミラーリングする必要があります:永続的なデータ構造は不変のノードから恩恵を受けますが、一時的な構造は暗黙的に行うパフォーマンスの約束を実際に果たすために内部の可変性が必要です。java.util.Listその他のインターフェースは短命です。不変のリストにそれらを実装することは不適切であり、実際にはパフォーマンスの危険があります。可変データ構造での優れたアルゴリズムは、不変データ構造での優れたアルゴリズムとは大きく異なることが多いため、不変データ構造を可変構造としてドレスアップすると(またはその逆の場合)、不良アルゴリズムが発生します。

多くのアルゴリズムは、考え方をシフトし、そのように高階関数を使用することによって効率的に配合することができます。永続的なリストは、いくつかの欠点(ノー効率的な添付)を持っていますが、この必要性は、機能的プログラミング深刻な問題ではないmapfold(2つの比較的原始的なものに名前を付けることを)、または繰り返し先頭に追加します。さらに、このデータ構造のみを使用することを強制する人はいません。他の(一時的な、または永続的ですが、より洗練された)より適切な場合は、それらを使用します。永続リストには、他のワークロードに対していくつかの利点があることにも注意してください。メモリを節約できるテールを共有します。


4

単独でリンクされたリストがある場合、背面よりも前面で作業することになります。

プロローグやハスケルのような関数型言語は、フロント要素と配列の残りを取得する簡単な方法を提供します。後ろに追加するのは、各ノードをコピーするO(n)操作です。


1
Haskellを使用しました。Afaik、それはまた怠problemを使用することで問題の一部を回避します。私はListそれがインターフェイスによって期待されるものだと思ったので、私はアペンドをしています(しかし、そこで間違っているかもしれません)。しかし、そのビットが本当に質問に答えるとは思わない。リスト全体をコピーする必要があります。完全に走査する必要がないため、最後に追加された要素へのアクセスが速くなります。
発がん性物質

基礎となるデータ構造/アルゴリズムと一致しないインターフェースに従ってクラスを作成することは、苦痛と非効率性を招きます。
ラチェットフリーク

JavaDocsは「追加」という言葉を明示的に使用します。実装のためにそれを無視する方が良いと言っているのですか?
発がん性物質

2
@Carcigenicateいいえ私は、不変ノードを含む単一リンクリストを型の型に合わせるのは間違いだと言っていますjava.util.List
ラチェットフリーク

1
怠Withでは、リンクリストはもうありません。リストはないため、ミューテーション(追加)はありません。代わりに、リクエストに応じて次のアイテムを生成するコードがあります。ただし、リストが使用されるすべての場所で使用することはできません。つまり、引数として渡されるリストを変更することを期待するメソッドをJavaで記述する場合、そのリストは最初から変更可能でなければなりません。ジェネレーターアプローチでは、コードを完全に再構築(再編成)して、それを機能させること、およびリストを物理的になくすことができます。
rwong

4

他の人が指摘しているように、不変の単一リンクリストは追加操作を実行するときにリスト全体をコピーする必要があることは正しいです。

多くの場合、アルゴリズムを実装するという回避策をcons(追加)操作の観点から使用し、最後のリストを一度逆にすることができます。これはまだリストを一度コピーする必要がありますが、複雑さのオーバーヘッドはリストの長さに対して線形です。一方、繰り返しappendを使用すると、2次の複雑さを簡単に得ることができます。

差分リスト(たとえば、こちらを参照)は興味深い選択肢です。差分リストはリストをラップし、一定時間で追加操作を提供します。基本的には、追加する必要がある限りラッパーを使用し、作業が完了したらリストに戻します。これは、a StringBuilderを使用して文字列を作成し、最後にStringを呼び出して結果を(不変!)として取得する場合と似ていますtoString。1つの違いは、a StringBuilderは可変ですが、差分リストは不変です。また、差分リストを変換してリストに戻す場合、新しいリスト全体を作成する必要がありますが、これも一度だけ行う必要があります。

DListHaskellのインターフェイスと同様のインターフェイスを提供する不変クラスを実装するのは非常に簡単Data.DListです。


4

あなたは見なければなりません。この偉大なビデオを 2015年からのことでconfに反応しImmutable.jsのクリエーターリーバイロン。コンテンツを複製しない効率的な不変リストを実装する方法を理解するためのポインターと構造を提供します。基本的な考え方は次のとおりです。-2つのリストが同じノード(同じ値、同じ次のノード)を使用している限り、同じノードが使用されます各リストの次の特定のノード

反応チュートリアルのこの画像は、私の壊れた英語よりも明確かもしれません:ここに画像の説明を入力してください


2

厳密にJavaではありませんが、Scalaで記述された不変だがパフォーマンスの高いインデックス付き永続データ構造に関するこの記事を読むことをお勧めします。

http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala

これはScalaデータ構造であるため、Javaからも使用できます(もう少し冗長です)。Clojureで使用可能なデータ構造に基づいており、それを提供する「ネイティブ」なJavaライブラリもあると確信しています。

また、不変のデータ構造の構築に関する注意:通常必要なのは、「構築中」のデータ構造に要素を追加することで(単一のスレッド内で)「変異」させることができる、ある種の「ビルダー」です。追加が完了したら、.build()or などの「構築中」オブジェクトのメソッドを呼び出して、オブジェクト.result()を「構築」し、安全に共有できる不変のデータ構造を提供します。



1

時には役立つアプローチは、リスト保持オブジェクトの2つのクラスを持つことです。final前方リンクオブジェクトは、前方リンクリストの最初のノードへの参照と、最初にnullの非final参照(非-null)同じアイテムを逆順で保持する逆リストオブジェクト、およびfinal逆リンクリストの最後のアイテムへの参照と最初にnullの非最終参照を持つ逆リストオブジェクトを識別します-null)同じアイテムを逆順で保持する順方向リストオブジェクトを識別します。

順リストに項目を付加または単にノードへのリンクにより識別された新しいノードの作成が必要となる逆リストに項目を追加するfinalと、参照を、元と同じタイプの新たなリストオブジェクトを作成final参照その新しいノードに。

順方向リストにアイテムを追加したり、逆方向リストに追加したりするには、反対のタイプのリストが必要です。特定のリストを初めて使用するときには、反対のタイプの新しいオブジェクトを作成し、参照を保存する必要があります。アクションを繰り返すと、リストが再利用されます。

リストオブジェクトの外部状態は、反対タイプのリストへの参照がnullであるか、反対順序のリストを識別するかに関係なく、同じと見なされることに注意してください。finalすべてのリストオブジェクトにはfinalその内容の完全なコピーへの参照があるため、マルチスレッドコードを使用する場合でも、変数を作成する必要はありません。

あるスレッドのコードがリストの反転コピーを作成してキャッシュし、そのコピーがキャッシュされるフィールドが揮発性でない場合、別のスレッドのコードがキャッシュされたリストを表示しない可能性がありますが、唯一の悪影響それから、他のスレッドは、リストの別の逆のコピーを構築する追加の作業を行う必要があるでしょう。このようなアクションは最悪のvolatile場合効率を低下させますが、正確性には影響を与えません。また、変数はそれ自体の効率の低下を引き起こすため、変数を不揮発性にして不定期の冗長操作の可能性を受け入れる方が良い場合がよくあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.