web-dev-qa-db-ja.com

STLのdequeとは何ですか?

私はSTLコンテナを見て、それらが実際に何であるか(すなわち、使用されているデータ構造)を理解しようとしていました、そしてdequeは私を止めました:私は最初に考えました一定の時間で両端からの挿入と削除を可能にする二重リンクリストでしたが、演算子[]による 約束 が一定の時間で行われることに困っています。リンクリストでは、任意のアクセスはO(n)である必要がありますか?

そして、それが動的配列の場合、どうすれば一定の時間で 要素を追加 できるでしょうか?再割り当てが発生する可能性があり、O(1)は償却コストであることに注意する必要があります ベクターの場合のように

だから私は、一定の時間に任意のアクセスを許可し、同時に新しい大きな場所に移動する必要がないこの構造は何だろうと思います。

163
Zonko

Dequeは幾分再帰的に定義されます:内部的には、固定サイズのchunksの両端キューを維持します。各チャンクはベクトルであり、チャンク自体のキュー(下図の「マップ」)もベクトルです。

schematic of the memory layout of a deque

CodeProject でパフォーマンス特性とvectorと比較する方法の優れた分析があります。

GCC標準ライブラリの実装は、マップを表すためにT**を内部的に使用します。各データブロックは、T*(固定サイズ__deque_buf_sizesizeof(T)に依存)で割り当てられます。

154
Konrad Rudolph

ベクトルのベクトルとして想像してください。標準のstd::vectorsではありません。

外部ベクトルには、内部ベクトルへのポインターが含まれています。 std::vectorのようにすべての空スペースを最後に割り当てるのではなく、再割り当てによって容量が変更されると、空スペースはベクトルの最初と最後で等しい部分に分割されます。これにより、このベクターのPush_frontPush_backの両方が、償却されたO(1)時間に発生することができます。

内部ベクトルの振る舞いは、dequeの前にあるか後ろにあるかに応じて変更する必要があります。背後では、標準のstd::vectorとして動作し、最後に成長し、Push_backがO(1)時間で発生します。正面では、反対のことを行う必要があり、最初はPush_frontごとに成長します。実際には、これはフロント要素にポインタを追加し、サイズとともに成長方向を追加することで簡単に実現できます。この簡単な変更により、Push_frontはO(1)時間にもなります。

任意の要素にアクセスするには、O(1)で発生する適切な外部ベクトルインデックスへのオフセットと分割、およびO(1)である内部ベクトルへのインデックス付けが必要です。これは、dequeの先頭または末尾のベクトルを除き、内部ベクトルがすべて固定サイズであることを前提としています。

20
Mark Ransom

deque =両端キュー

どちらの方向にも成長できるコンテナ。

Dequeは通常vector of vectorsとして実装されます(ベクトルのリストは一定時間のランダムアクセスを与えることはできません)。二次ベクトルのサイズは実装に依存しますが、一般的なアルゴリズムはバイト単位の一定サイズを使用することです。

16
Alok Save

(これは、私が 別のスレッド で与えた答えです。本質的に、単一のvectorを使用したかなり単純な実装でさえ、「一定の非償却」の要件に準拠していると主張しています。 Push_ {front、back}」。驚くかもしれませんが、これは不可能だと思うかもしれませんが、驚くべき方法でコンテキストを定義する他の関連する引用符を標準で見つけました。この答えは、私がどのことを正しく言ったか、そして私の論理がどこで壊れたかを特定することは非常に役立ちます。

この回答では、goodの実装を特定しようとはせず、C++標準の複雑さの要件を解釈するのを手助けしようとしています。 N3242 から引用しています。これは Wikipedia によると、無料で入手可能な最新のC++ 11標準化ドキュメントです。 (最終的な標準とは異なるように編成されているように見えるため、正確なページ番号を引用しません。もちろん、これらの規則は最終的な標準で変更された可能性がありますが、私はそうは思われません。)

deque<T>を使用すると、vector<T*>を正しく実装できます。すべての要素がヒープにコピーされ、ポインターがベクターに保存されます。 (ベクターについては後で詳しく説明します)。

なぜTではなくT*なのですか?標準では

「両端キューの両端に挿入すると、両端キューに対するすべての反復子が無効になりますが、両端キューの要素への参照の有効性に影響はありません。 "

(私の強調)。 T*はそれを満たすのに役立ちます。また、次のことも満たすのに役立ちます。

「常にdequeの先頭または末尾に単一の要素を挿入すると、..... Tのコンストラクターへの単一呼び出しが発生します。」

(議論の余地のある)ビットについて。 vectorを使用してT*を保存する理由ランダムアクセスが可能です。これは良いスタートです。ベクトルの複雑さをしばらく忘れて、これを慎重に構築してみましょう。

標準では、「含まれているオブジェクトに対する操作の数」について説明しています。 deque::Push_frontの場合、これは明らかに1です。これは、Tオブジェクトが1つだけ構築され、既存のTオブジェクトが0個、何らかの方法で読み取りまたはスキャンされるためです。この数1は明らかに定数であり、現在dequeにあるオブジェクトの数とは無関係です。これにより、次のことが言えます。

deque::Push_frontの場合、含まれるオブジェクト(Ts)に対する操作の数は固定されており、既に両端キューにあるオブジェクトの数とは無関係です。」

もちろん、T*に対する操作の数はそれほどうまくいきません。 vector<T*>が大きくなりすぎると、再割り当てされ、多くのT*がコピーされます。そのため、T*の操作の数は大きく異なりますが、Tの操作の数は影響を受けません。

なぜTのカウント操作とT*のカウント操作の違いを気にするのですか?それは標準が言っているからです:

この句の複雑さの要件はすべて、含まれているオブジェクトに対する操作の数に関してのみ述べられています。

dequeの場合、含まれるオブジェクトはT*ではなくTです。つまり、T*をコピー(または再割り当て)する操作は無視できます。

ベクトルがdequeでどのように動作するかについてはあまり説明していません。おそらく、ベクトルを常に最大のcapacity()を使用する循環バッファーとして解釈し、ベクトルがいっぱいになったらすべてをより大きなバッファーに再割り当てします。詳細は関係ありません。

最後のいくつかの段落では、deque::Push_frontと、すでに両端キュー内のオブジェクトの数と、含まれているT-オブジェクトでPush_frontによって実行された操作の数との関係を分析しました。そして、それらは互いに独立していることがわかりました。 標準では、複雑さはoperations-on -Tの観点から義務付けられているため、一定の複雑さがあると言えます。

はい、Operations-On-T * -Complexityは償却されます(vectorにより)が、私たちが興味を持っているのはOperations-On-T-Complexity、これは一定です(償却されていません)。

Vector :: Push_backまたはvector :: Push_frontの複雑さは、この実装では無関係です。これらの考慮事項にはT*の操作が関係するため、無関係です。標準が複雑さの「従来の」理論的概念に言及している場合、それらは「含まれているオブジェクトに対する操作の数」にそれ自体を明示的に制限していなかったでしょう。私はその文を過剰に解釈していますか?

11
Aaron McDaid

標準では特定の実装は義務付けられていませんが(一定時間のランダムアクセスのみ)、dequeは通常、連続したメモリ「ページ」のコレクションとして実装されます。必要に応じて新しいページが割り当てられますが、依然としてランダムアクセスが可能です。 std::vectorとは異なり、データが連続して格納されることは保証されていませんが、ベクターのように、中間に挿入するには多くの再配置が必要です。

6
Kerrek SB

Adam Drozdekが書いた「C++のデータ構造とアルゴリズム」を読んでいて、これが便利だとわかりました。 HTH。

STL dequeの非常に興味深い側面は、その実装です。 STL両端キューは、リンクリストとしてではなく、ブロックまたはデータの配列へのポインターの配列として実装されます。ブロックの数はストレージのニーズに応じて動的に変化し、ポインターの配列のサイズはそれに応じて変化します。

中央にデータへのポインターの配列(右側のチャンク)があり、中央の配列が動的に変化していることもわかります。

画像は千の言葉に値します。

enter image description here

3
Keloo