多くのデータ構造に挿入するよりも、削除を実装するのが通常はるかに難しいのはなぜですか?


33

多くの(ほとんどの)データ構造の挿入よりも、削除の実装が通常はるかに難しい特定の理由を考えることができますか?

簡単な例:リンクリスト。挿入はささいなことですが、削除にはいくつかの特殊なケースがあり、それが非常に難しくなります。AVLやRed-blackなどの自己バランス型バイナリ検索ツリーは、痛みを伴う削除の実装の典型的な例です。

私はそれがほとんどの人々が考える方法に関係していると言いたい:私たちが物事を建設的に定義することはより簡単であり、それはうまく挿入を容易に導く。


4
何についてpopextract-min
コアダンプ

5
「実装するのが難しい」というのは、プログラミング(データ構造とアルゴリズムの特性)よりも心理学(認知と人間の心の長所と短所)の問題です。
-outis

1
コアダンプがほのめかしているように、スタックは少なくともaddと同じくらい簡単に削除できるはずです(配列に裏打ちされたスタックの場合、ポップは単なるポインターの減少です[1]。アレイ)。また、挿入は頻繁で削除が少ないと想定されるいくつかのユースケースがありますが、削除の数が挿入を超える非常に魔法のようなデータ構造になります。[1]あなたはおそらくもリスコフの教科書がなかったので、私は覚えて避けるのメモリリークにポップされたオブジェクトへの参照をnull今は見えないはずです
FOON

43
「ウェイター、このサンドイッチにマヨネーズを追加してもらえますか?」「もちろん、問題ありません。」「マスタードもすべて取り除いていただけますか?」「ええと......」
cobaltduck

3
なぜ減算は加算よりも複雑なのですか?除算(または素因数分解)は乗算よりも複雑ですか?根はべき乗よりも複雑ですか?
muは

回答:


69

それは単なる心の状態以上のものです。削除が難しい物理的な(つまりデジタルの)理由があります。

削除すると、以前は何かがあった場所に穴が残ります。結果のエントロピーの技術用語は「フラグメンテーション」です。リンクされたリストでは、削除されたノードを「パッチ処理」して、使用中のメモリの割り当てを解除する必要があります。バイナリツリーでは、ツリーの不均衡が発生します。メモリシステムでは、新しく割り当てられたブロックが削除によって残されたブロックよりも大きい場合、メモリがしばらく使用されなくなります。

つまり、挿入する場所を選択できるため、挿入が簡単になります。どのアイテムが削除されるかを事前に予測できないため、削除はより困難になります。


3
フラグメンテーションは、メモリ内またはダイアグラム内の構造のいずれかで、ポインタと間接化が作用する問題ではありません。インメモリでは、間接性のために個々のノードがどこに存在するかは関係ありません。リストの場合、内部ノード(図に穴がある場所)を削除すると、挿入(1つのポインター割り当てと1つの空き対1つの割り当てと2つのポインター割り当て)よりも操作が若干少なくなります。ツリーの場合、ノードを挿入すると、削除と同様にツリーのバランスが崩れる可能性があります。ブリトが言及する困難を引き起こすのは、断片化が重要でないエッジケースです。
-outis

12
挿入と削除の予測可能性が異なることに同意しません。リストノードの「パッチング」は、同じノードが代わりに挿入される場合に逆に起こることとまったく同じです。どちらの方向にも不確実性はなく、その要素に固有の構造を持たないコンテナ(バランスの取れた二分木、要素オフセット間の厳密な関係を持つ配列など)には「穴」はまったくありません。したがって、私はあなたがここで何について話しているのか分からないのではないかと心配しています。
sqykly

2
非常に興味深いが、議論が欠けていると思う。単純/高速削除を中心に問題なくデータ構造を整理できます。それはあまり一般的ではなく、おそらくあまり有用ではありません。
luk32

@sqykly中間の挿入と中間の関係も同様に難しいため、リストは悪い選択の例だと思います。1つのケースは、他のケースが再割り当てされた場所にメモリを割り当てます。1つは穴を開け、もう1つは穴を塞ぎます。そのため、すべてのケースが追加よりも複雑な削除ではありません。
ydobonebi

36

なぜ挿入するよりも削除するのが難しい傾向があるのですか?データ構造は、削除よりも挿入を念頭に置いて設計されているため、当然です。

これを考慮してください-データ構造から何かを削除するには、そもそもそこにある必要があります。そのため、最初に追加する必要があります。つまり、最大で挿入と同じ数の削除を行うことができます。データ構造を挿入用に最適化すると、少なくとも削除用に最適化された場合と同じくらいの利益が得られることが保証されます。

さらに、各要素を順番に削除するのにどのような用途がありますか?一度にすべてをクリアする関数を(単に新しい関数を作成して)呼び出すだけではどうでしょうか?また、データ構造は、実際に何かを含む場合に最も役立ちます。したがって、実際には、挿入と同数の削除を行うケースはあまり一般的ではありません。

何かを最適化するとき、あなたはそれが最もすることと最も時間がかかることを最適化したいです。通常の使用法では、データ構造の要素の削除は挿入よりも頻繁に発生しません。


4
私が想像できるユースケースが1つあります。最初の挿入と個々の消費のために準備されるデータ構造。もちろん、これはめったにないケースであり、アルゴリズム的にはあまり興味深いものではありません。なぜなら、あなたが言ったように、そのような操作は挿入を漸近的に支配できないからです。たぶん、バッチ挿入はかなり良いコストで削除でき、高速で簡単に削除できるため、複雑でありながら実用的なバッチ挿入と簡単で高速な個々の削除ができるという希望があるかもしれません。確かに非常に珍しい実用的なニーズ。
luk32

1
ええと、例としては逆順ベクトルが考えられます。k要素のバッチを非常に高速に追加できます。並べ替え入力を逆にし、既存のベクターとマージします- O(k log k + n)。次に、かなり複雑な挿入の構造がありますが、上位u要素の消費は簡単で高速です。u最後に、ベクトルの終わりを移動するだけです。しかし、誰かがそのようなことを必要とするなら、私は気の毒に思うでしょう。これが少なくともあなたの議論を強化することを願っています。
luk32

あなたが最もやりたいことではなく、平均的な使用パターンに最適化したいと思わないでしょうか?
シブ

通常、単純なFIFOワークキューはほとんどの場合空になります。適切に設計されたキューは、挿入と削除の両方に対して適切に最適化されます(つまりO(1))(また、非常に優れたキューも高速な並行操作をサポートしますが、それは別の問題です)。
ケビン

6

難しくありません。

二重リンクリストを使用すると、挿入時にメモリが割り当てられ、ヘッドまたは前のノードのいずれか、およびテールまたは次のノードのいずれかとリンクします。削除すると、まったく同じものからリンクが解除され、メモリが解放されます。これらの操作はすべて対称です。

これは、どちらの場合でも挿入/削除するノードがあることを前提としています。(また、挿入の場合、前に挿入するノードもあるため、ある意味、挿入はやや複雑であると考えることができます。)削除するノードではなくペイロードを削除しようとする場合ノードの場合は、もちろん最初にペイロードのリストを検索する必要がありますが、それは削除の欠点ではありませんか?

バランスの取れたツリーでも同じことが当てはまります。通常、ツリーは挿入直後と削除直後にバランスを取る必要があります。バランスルーチンを1つだけ試してみて、それが挿入であるか削除であるかに関係なく、各操作の後に適用することをお勧めします。ツリーのバランスを常に維持する挿入と、ツリーのバランスを常に維持する削除を実装しようとしている場合、2つが同じバランスルーチンを共有することはないため、不必要に複雑になります。

要するに、一方が他方よりも硬くなるべき理由はありません。もしあなたがそれを見つけているなら、あなたが考えるのがより自然であるという(非常に人間的な)傾向の犠牲者である可能性があります。つまり、必要以上に複雑な方法で削除を実装している可能性があります。しかし、それは人間の問題です。数学的な観点からは、問題はありません。


1
私は反対しなければなりません。AVL削除アルゴリズムは、挿入よりも複雑です。特定のノードの削除では、ツリー全体のバランスを再調整する必要があります。これは通常、再帰的に実行されますが、非再帰的にも実行できます。挿入のためにこれを行う必要はありません。このようなツリー全体のリバランスがすべての場合に回避できるアルゴリズムの進歩については知りません。
デニス

@Dennis:AVLツリーはルールではなく例外に従う可能性があります。
-outis

@outis IIRC、すべてのバランスの取れた検索ツリーには、挿入よりも複雑な削除ルーチンがあります。
ラファエル

何についてのクローズドハッシュのハッシュテーブル?挿入は(比較的)簡単で、「インデックスXにあるはずだったものが現在インデックスYにあるので、それを見つけて元に戻す必要がある」すべてを修正する必要があるため、削除は少なくとも概念化が困難です。問題。
ケビン

3

ランタイムに関しては、Wikipedia のデータ構造操作の時間の複雑さの比較を見て、挿入操作と削除操作の複雑さは同じであることに注意してください。プロファイルされた削除操作は、インデックスによる削除です。削除する構造要素への参照があります。挿入はアイテムごとです。実際には、削除の実行時間が長くなるのは、通常はインデックスではなく削除するアイテムがあるため、検索操作も必要だからです。テーブル内のほとんどのデータ構造では、配置位置がアイテムに依存していないか、挿入中に暗黙的に位置が決定されるため、挿入の追加の検索は必要ありません。

認知の複雑さに関しては、質問に答えがあります:エッジケース。削除は挿入よりも多くの可能性があります(これは一般的なケースではまだ確立されていません)。ただし、特定の設計では、これらのエッジケースの少なくとも一部を回避できます(リンクリストにセンチネルノードがあるなど)。


2
「ほとんどのデータ構造では、挿入の検索は必要ありません。」 - といった?実際、私は反対の主張をします。(挿入位置を「見つける」ことは、後で同じ要素を再び見つけるのと同じくらい高価です。)
ラファエル

@Raphael:この回答は、削除の一部として検索操作を含まない、操作の複雑さのリンクテーブルのコンテキストで読む必要があります。あなたの質問に答えて、構造を一般名で分類しました。配列、リスト、ツリー、ハッシュテーブル、スタック、キュー、ヒープ、およびセットのうち、ツリーとセットには挿入の検索が必要です。他のアイテムは、アイテムに接続されていないインデックスを使用します(基本スタック、キュー、およびヒープの場合、1つのインデックスのみが公開され、検索はサポートされていません)またはアイテムから計算します。グラフは、使用方法に応じて、どちらの方法でも使用できます。
-outis

...トライは木と考えることができます。ただし、独自の構造として分類される場合、挿入中に「検索」があるかどうかは議論の問題であるため、ここでは説明しません。データ構造のリストでは、インターフェイスと実装は考慮されていません。また、カウント方法は、カテゴリの分類方法に大きく依存します。もっと客観的な発言を考えられるかどうかを確認します。
-outis

ディクショナリ/セットインターフェイスを念頭に置いていたことを認めます(CSで一般的)。とにかく、そのテーブルは誤解を招くものであり、(iirc)いくつかの場所でさえ間違っています-ウィキペディア、CSの誤報のピット。:/
ラファエル

0

上記のすべての問題に加えて、データ参照の整合性が関係しています。SQLのデータベースのようなデータ構造を最も適切に構築するには、Oracleの参照整合性が非常に重要です。
誤って多くの異なるものが破壊されないようにするために。
たとえば、削除しようとするものを削除するだけでなく、関連データのクリーンアップもトリガーする削除時のカスケード。
これにより、ジャンクデータからデータベースをクリーンアップし、データの整合性を維持します。
たとえば、2番目のテーブルの関連レコードとして、親と種類を持つテーブルがあります。
親はメインテーブルです。参照整合性を強化していない場合、任意のテーブルのレコードを削除できますが、後で子テーブルにデータがあり、親テーブルには何もないため、完全な家族情報を取得する方法がわかりません。
そのため、参照整合性チェックでは、子テーブルのレコードがクリーンアップされるまで、親テーブルからレコードを削除できません。
そして、それがほとんどのデータソースでデータを削除することがより難しい理由です。


質問は、データベースではなく、リンクリスト、ハッシュテーブルなどのメモリ内構造について尋ねていたと思いますが、参照整合性は、メモリ内構造でも大きな問題です。
supercat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.