6

deque::insert()次のように、C++ 標準 2003 (第 23.2.1.3 章) からの複雑さを学びました。

最悪の場合、1 つの要素を両端キューに挿入すると、挿入ポイントから両端キューの先頭までの距離と、挿入ポイントから両端キューの末尾までの距離の最小値に比例して時間がかかります。

stl deque の実装は、メモリ チャンクのコレクションとして常に理解しています。したがって、挿入は、挿入位置と同じメモリ チャンク内の要素にのみ影響します。私の質問は、「挿入ポイントから両端キューの先頭までの距離と、挿入ポイントから両端キューの末尾までの距離の最小値で線形」という標準の意味は何ですか?

私の理解は、C++ 標準が deque の特定の実装を強制していないためです。複雑さは、最悪の場合の一般的なものです。ただし、コンパイラでの実際の実装では、メモリ チャンク内の要素の数に比例し、要素のサイズによって異なる場合があります。

別の推測として、insert()はすべてのイテレータを無効にするため、 deque はすべてのイテレータを更新する必要があります。したがって、線形です。

4

4 に答える 4

8

std::deque は通常 (常に?) メモリのチャンクのコレクションとして実装されますが、コレクションの途中に 1 つの新しい要素を挿入するためだけに、まったく新しいチャンクを挿入することは通常ありません。そのため、挿入ポイントが先頭または末尾に近いかどうかを検出し、既存の要素をシャッフルして、既存のチャンクに新しい要素用のスペースを作ります。コレクションの先頭または末尾に新しいメモリ チャンクを追加するだけです。

于 2011-09-27T16:35:20.613 に答える
3

ダイアグラムの方がいいと思います...アスキーアートで遊んでみましょう!

通常、deque はメモリ チャンクの配列ですが、フロント メモリ チャンクとバック メモリ チャンクはすべて満杯です。これが必要なのは、deque が RandomAccessContainer であり、任意のコンテナーへの O(1) アクセスを取得するために、サイズを読み取るコンテナーの無制限の数を持つことができないためです。

bool size() const { return first.size + (buckets.size()- 2) * SIZE + last.size; }

T& operator[](size_t i) {
  if (i < first.size) { return first[SIZE - i]; }

  size_t const correctedIndex = i - first.size;
  return buckets[correctedIndex / SIZE][correctedIndex % SIZE];
}

これらの演算は、乗算/除算により O(1) です!

この例では、メモリ チャンクに 8 つの要素が含まれている場合、メモリ チャンクがいっぱいであると想定します。実際には、すべての内側のバケットが同じサイズでなければならないというだけで、サイズを固定する必要があるとは誰も言いませんでした。

 // Deque
 0:       ++
 1: ++++++++
 2: ++++++++
 3: ++++++++
 4: +++++

ここで、インデックス 13 に挿入したいとします。これは、2 というラベルの付いたバケットのどこかに収まります。考えられるいくつかの戦略があります。

  • 拡張バケット 2 (のみ)
  • 2 の前または後に新しいバケットを導入し、いくつかの要素のみをシャッフルする

しかし、これら 2 つの戦略は、すべての「内部」バケットが同じ数の要素を持つという不変条件に違反します。

したがって、この場合、最初または最後 (どちらか安い方) に向かって要素をシャッフルする必要があります。

 // Deque
 0:      +++
 1: ++++++++
 2: +O++++++
 3: ++++++++
 4: +++++

バケット 0 がどのように成長したかに注意してください。

このシャッフルは、最悪の場合、要素の半分 (O(N/2)) を移動することを意味します。

dequeただし、最初または最後に O(1) 挿入があります。これは、要素を適切な場所に追加するか、(バケットがいっぱいの場合) 新しいバケットを作成するだけの問題だからです。

B+ Treesに基づいて、ランダムなインデックスでより優れた挿入/消去動作を持つ他のコンテナーがあります。インデックス付きの B+ ツリーでは、「キー」の代わりに (または並行して)、特定の位置より前の要素の数を内部的に維持できます。これを効率的に行うためのさまざまなテクニックがあります。最後に、次のコンテナを取得します。

  • O(1): 空、サイズ
  • O(log N): at、insert、erase

blistこのような構造に裏打ちされた Python リストのような要素を実装する Pythonのモジュールを確認できます。

于 2011-09-27T18:13:27.033 に答える
2

あなたの推測は... 99.9%真実です。すべては、実際の実装が何であるかに依存します。標準で指定されているのは、実装者 (仕様に適合しない場合は標準であると主張できない) とユーザー (実装に依存しないコードを作成する場合に「より良いパフォーマンス」を期待してはならない) の両方の最小要件です。

仕様の背後にあるアイデアは、初期化されていないメモリのチャンク (== 1) であり、要素が中央に割り当てられます... それらのためのスペースができるまで。真ん中に入れるということはシフトを意味します。先頭または末尾に挿入するとは、その場で構築することを意味します。(スペースが存在しない場合、再割り当てが行われます) インデックスとイテレータは、変更後は信頼できません。これは、何がシフトされ、どの方向にシフトされたかを推測できないためです。

より効率的な実装では、単一のチャンクを使用するのではなく、複数のチャンクを使用して「シフト」の問題を再分散し、基盤となるシステムから一定のサイズでメモリを割り当てます (したがって、再割り当てと断片化を制限します)。それらのいずれかをターゲットにしている場合は、より良いパフォーマンスが期待できますが、そうでない場合は、構造の最適化を想定しない方がよいでしょう。

于 2011-09-27T16:46:10.047 に答える
1

挿入された要素の数に比例します (コピー構築)。さらに、特定のライブラリの実装によっては、位置と両端キューのいずれかの間の要素数までの追加の線形時間がかかります。 参照...

于 2011-09-27T16:42:09.193 に答える