【C++】std::dequeの使い方について詳しく解説

この記事では、C++の標準ライブラリに含まれるstd::dequeについて詳しく解説します。

std::dequeは、両端からの要素の追加や削除が効率的に行える便利なデータ構造です。

この記事を読むことで、std::dequeの基本的な使い方や、他のコンテナとの違い、具体的な活用例、パフォーマンスの特徴などを理解することができます。

初心者の方でもわかりやすいように、サンプルコードを交えながら説明していきますので、ぜひ参考にしてください。

目次から探す

std::dequeとは

std::dequeの基本概念

std::deque(ダブルエンドキュー、Double-Ended Queue)は、C++標準ライブラリに含まれるコンテナの一つです。

名前の通り、両端からの要素の追加と削除が効率的に行えるデータ構造です。

std::dequeは、スタックやキューのようなデータ構造として利用されることが多く、特に要素の追加と削除が頻繁に行われる場合に有効です。

std::vectorとの違い

std::dequestd::vectorはどちらもシーケンスコンテナですが、いくつかの重要な違いがあります。

  1. 要素の追加と削除の効率:
  • std::vectorは末尾への要素の追加と削除が効率的ですが、先頭への操作は効率が悪いです。
  • std::dequeは先頭と末尾の両方への要素の追加と削除が効率的です。
  1. メモリの管理:
  • std::vectorは連続したメモリ領域を使用します。

そのため、要素の追加に伴い再配置が必要になることがあります。

  • std::dequeは非連続のメモリブロックを使用します。

これにより、再配置の必要が少なくなります。

  1. アクセス速度:
  • std::vectorは連続したメモリ領域を使用するため、ランダムアクセスが高速です。
  • std::dequeは非連続のメモリブロックを使用するため、ランダムアクセスはstd::vectorに比べて若干遅くなります。

std::dequeの内部構造

std::dequeの内部構造は、複数の固定サイズのメモリブロック(チャンク)で構成されています。

これにより、要素の追加や削除が効率的に行えるようになっています。

以下に、std::dequeの内部構造の概略を示します。

  1. メモリブロック:
  • std::dequeは複数の固定サイズのメモリブロックを持ちます。

各ブロックは連続したメモリ領域を持ちますが、ブロック間は非連続です。

  1. インデックス管理:
  • std::dequeは内部でインデックスを管理し、各ブロックへのアクセスを効率的に行います。

これにより、先頭と末尾への要素の追加と削除が高速に行えます。

  1. 再配置の回避:
  • std::dequeは非連続のメモリブロックを使用するため、要素の追加や削除に伴う再配置が少なくなります。

これにより、パフォーマンスが向上します。

以下に、std::dequeの基本的な使い方を示すサンプルコードを紹介します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq;
    // 要素の追加
    dq.push_back(1);  // 末尾に追加
    dq.push_front(2); // 先頭に追加
    // 要素のアクセス
    std::cout << "先頭の要素: " << dq.front() << std::endl;
    std::cout << "末尾の要素: " << dq.back() << std::endl;
    // 要素の削除
    dq.pop_back();  // 末尾の要素を削除
    dq.pop_front(); // 先頭の要素を削除
    return 0;
}

このコードでは、std::dequeの基本的な操作である要素の追加、アクセス、削除を示しています。

std::dequeは、先頭と末尾の両方から効率的に操作できるため、特定のシナリオで非常に有用です。

std::dequeの基本操作

std::dequeの宣言と初期化

デフォルトコンストラクタ

std::dequeを宣言する最も基本的な方法は、デフォルトコンストラクタを使用することです。

これは空のdequeを作成します。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq;
    std::cout << "サイズ: " << dq.size() << std::endl; // 出力: サイズ: 0
    return 0;
}

サイズを指定したコンストラクタ

サイズを指定してdequeを初期化することもできます。

この場合、指定したサイズ分のデフォルト値(通常は0)が設定されます。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq(5);
    std::cout << "サイズ: " << dq.size() << std::endl; // 出力: サイズ: 5
    return 0;
}

初期値を指定したコンストラクタ

サイズと初期値を指定してdequeを初期化することも可能です。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq(5, 10);
    for (int i : dq) {
        std::cout << i << " "; // 出力: 10 10 10 10 10
    }
    return 0;
}

イテレータを使ったコンストラクタ

他のコンテナからイテレータを使ってdequeを初期化することもできます。

#include <deque>
#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::deque<int> dq(vec.begin(), vec.end());
    for (int i : dq) {
        std::cout << i << " "; // 出力: 1 2 3 4 5
    }
    return 0;
}

要素の追加と削除

push_backとpush_front

dequeの末尾に要素を追加するにはpush_back、先頭に追加するにはpush_frontを使用します。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq;
    dq.push_back(1);
    dq.push_front(2);
    for (int i : dq) {
        std::cout << i << " "; // 出力: 2 1
    }
    return 0;
}

pop_backとpop_front

dequeの末尾の要素を削除するにはpop_back、先頭の要素を削除するにはpop_frontを使用します。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq = {1, 2, 3};
    dq.pop_back();
    dq.pop_front();
    for (int i : dq) {
        std::cout << i << " "; // 出力: 2
    }
    return 0;
}

insertとerase

特定の位置に要素を挿入するにはinsert、特定の位置の要素を削除するにはeraseを使用します。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq = {1, 2, 3};
    dq.insert(dq.begin() + 1, 4); // 1の次に4を挿入
    dq.erase(dq.begin() + 2); // 3を削除
    for (int i : dq) {
        std::cout << i << " "; // 出力: 1 4 3
    }
    return 0;
}

要素のアクセス

at関数

at関数を使うと、範囲外アクセス時に例外を投げるため、安全に要素にアクセスできます。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq = {1, 2, 3};
    try {
        std::cout << dq.at(1) << std::endl; // 出力: 2
        std::cout << dq.at(3) << std::endl; // 例外が発生
    } catch (const std::out_of_range& e) {
        std::cerr << "範囲外アクセス: " << e.what() << std::endl;
    }
    return 0;
}

operator[]

operator[]を使うと、範囲外アクセス時に例外は投げられませんが、直接要素にアクセスできます。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq = {1, 2, 3};
    std::cout << dq[1] << std::endl; // 出力: 2
    // 範囲外アクセスは未定義動作
    return 0;
}

frontとback

front関数を使うと先頭の要素に、back関数を使うと末尾の要素にアクセスできます。

#include <deque>
#include <iostream>
int main() {
    std::deque<int> dq = {1, 2, 3};
    std::cout << "先頭: " << dq.front() << std::endl; // 出力: 先頭: 1
    std::cout << "末尾: " << dq.back() << std::endl; // 出力: 末尾: 3
    return 0;
}

以上が、std::dequeの基本操作に関する解説です。

次のセクションでは、std::dequeのメンバ関数について詳しく見ていきます。

std::dequeのメンバ関数

std::dequeは多くの便利なメンバ関数を提供しており、これらを使うことで効率的にデータを操作できます。

ここでは、サイズ関連、容量関連、イテレータ関連のメンバ関数について詳しく解説します。

サイズ関連のメンバ関数

size

size関数は、dequeの現在の要素数を返します。

以下の例を見てみましょう。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq = {1, 2, 3, 4, 5};
    std::cout << "Size: " << dq.size() << std::endl; // 出力: Size: 5
    return 0;
}

max_size

max_size関数は、dequeが保持できる最大の要素数を返します。

これはシステムやコンパイラによって異なります。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq;
    std::cout << "Max Size: " << dq.max_size() << std::endl;
    return 0;
}

resize

resize関数は、dequeのサイズを変更します。

新しいサイズが現在のサイズより大きい場合、追加された要素はデフォルト値で初期化されます。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq = {1, 2, 3};
    dq.resize(5); // サイズを5に変更
    for (int i : dq) {
        std::cout << i << " "; // 出力: 1 2 3 0 0
    }
    std::cout << std::endl;
    return 0;
}

容量関連のメンバ関数

empty

empty関数は、dequeが空であるかどうかを判定します。

空の場合はtrueを返し、そうでない場合はfalseを返します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq;
    if (dq.empty()) {
        std::cout << "Deque is empty" << std::endl; // 出力: Deque is empty
    }
    dq.push_back(1);
    if (!dq.empty()) {
        std::cout << "Deque is not empty" << std::endl; // 出力: Deque is not empty
    }
    return 0;
}

shrink_to_fit

shrink_to_fit関数は、dequeの容量を現在の要素数に合わせて縮小します。

これはメモリの効率的な利用に役立ちます。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq = {1, 2, 3, 4, 5};
    dq.resize(3); // サイズを3に変更
    dq.shrink_to_fit(); // 容量を縮小
    std::cout << "Size after shrink_to_fit: " << dq.size() << std::endl; // 出力: Size after shrink_to_fit: 3
    return 0;
}

イテレータ関連のメンバ関数

beginとend

begin関数はdequeの最初の要素を指すイテレータを返し、end関数はdequeの最後の要素の次を指すイテレータを返します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq = {1, 2, 3, 4, 5};
    for (auto it = dq.begin(); it != dq.end(); ++it) {
        std::cout << *it << " "; // 出力: 1 2 3 4 5
    }
    std::cout << std::endl;
    return 0;
}

rbeginとrend

rbegin関数はdequeの最後の要素を指す逆イテレータを返し、rend関数はdequeの最初の要素の前を指す逆イテレータを返します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq = {1, 2, 3, 4, 5};
    for (auto it = dq.rbegin(); it != dq.rend(); ++it) {
        std::cout << *it << " "; // 出力: 5 4 3 2 1
    }
    std::cout << std::endl;
    return 0;
}

cbeginとcend

cbegin関数はdequeの最初の要素を指す定数イテレータを返し、cend関数はdequeの最後の要素の次を指す定数イテレータを返します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> dq = {1, 2, 3, 4, 5};
    for (auto it = dq.cbegin(); it != dq.cend(); ++it) {
        std::cout << *it << " "; // 出力: 1 2 3 4 5
    }
    std::cout << std::endl;
    return 0;
}

これらのメンバ関数を活用することで、std::dequeを効率的に操作することができます。

次のセクションでは、std::dequeの活用例について詳しく解説します。

std::dequeの活用例

キューとしての利用

std::dequeは、キュー(FIFO: First In, First Out)として利用するのに非常に適しています。

キューは、最初に追加された要素が最初に取り出されるデータ構造です。

std::dequeは、両端からの要素の追加と削除が高速であるため、キューの実装に最適です。

以下に、std::dequeを使ってキューを実装する例を示します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> queue;
    // 要素をキューに追加
    queue.push_back(1);
    queue.push_back(2);
    queue.push_back(3);
    // キューの内容を表示
    std::cout << "Queue: ";
    for (int n : queue) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    // 要素をキューから取り出す
    queue.pop_front();
    queue.pop_front();
    // キューの内容を表示
    std::cout << "Queue after pop: ";
    for (int n : queue) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

このプログラムの実行結果は以下の通りです。

Queue: 1 2 3 
Queue after pop: 3

スタックとしての利用

std::dequeは、スタック(LIFO: Last In, First Out)としても利用できます。

スタックは、最後に追加された要素が最初に取り出されるデータ構造です。

std::dequepush_backpop_backを使うことで、スタックの操作を簡単に実現できます。

以下に、std::dequeを使ってスタックを実装する例を示します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> stack;
    // 要素をスタックに追加
    stack.push_back(1);
    stack.push_back(2);
    stack.push_back(3);
    // スタックの内容を表示
    std::cout << "Stack: ";
    for (int n : stack) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    // 要素をスタックから取り出す
    stack.pop_back();
    stack.pop_back();
    // スタックの内容を表示
    std::cout << "Stack after pop: ";
    for (int n : stack) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

このプログラムの実行結果は以下の通りです。

Stack: 1 2 3 
Stack after pop: 1

双方向リストとしての利用

std::dequeは、双方向リストとしても利用できます。

双方向リストは、各要素が前後の要素へのポインタを持つデータ構造で、両端からの要素の追加と削除が高速です。

std::dequeは、内部的に複数の配列を管理することで、双方向リストのような操作を効率的に行います。

以下に、std::dequeを使って双方向リストを操作する例を示します。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> deque;
    // 要素を双方向リストに追加
    deque.push_back(1);
    deque.push_back(2);
    deque.push_front(0);
    deque.push_front(-1);
    // 双方向リストの内容を表示
    std::cout << "Deque: ";
    for (int n : deque) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    // 要素を双方向リストから削除
    deque.pop_back();
    deque.pop_front();
    // 双方向リストの内容を表示
    std::cout << "Deque after pop: ";
    for (int n : deque) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

このプログラムの実行結果は以下の通りです。

Deque: -1 0 1 2 
Deque after pop: 0 1

以上のように、std::dequeはキュー、スタック、双方向リストとして柔軟に利用できる非常に便利なコンテナです。

用途に応じて適切に使い分けることで、効率的なプログラムを作成することができます。

std::dequeのパフォーマンス

メモリ管理

std::dequeは、メモリ管理の面で他のコンテナと異なる特徴を持っています。

std::vectorが連続したメモリ領域を使用するのに対し、std::dequeは複数のメモリブロックを使用します。

このため、std::dequeはメモリの再配置が少なく、要素の追加や削除が効率的に行えます。

以下に、std::dequeのメモリ管理の特徴を示します。

  • メモリブロックの分割: std::dequeは複数の小さなメモリブロックを使用します。

これにより、メモリの再配置が少なくなります。

  • 動的なメモリ割り当て: 必要に応じてメモリブロックを動的に割り当てるため、大量の要素を効率的に管理できます。

時間計算量

std::dequeの操作にかかる時間計算量は以下の通りです。

  • 要素の追加と削除: push_backpush_frontpop_backpop_frontは平均してO(1)の時間計算量です。

これは、std::dequeがメモリブロックを使用しているため、再配置が少ないためです。

  • 要素のアクセス: operator[]at関数を使用した要素のアクセスはO(1)の時間計算量です。

これは、std::dequeがランダムアクセスをサポートしているためです。

  • 挿入と削除: inserteraseは、位置によって異なりますが、最悪の場合O(n)の時間計算量です。

これは、要素のシフトが必要になるためです。

std::vectorとのパフォーマンス比較

std::dequestd::vectorのパフォーマンスを比較すると、それぞれの利点と欠点が明確になります。

  • メモリ管理:
  • std::vectorは連続したメモリ領域を使用するため、メモリの再配置が頻繁に発生します。

これにより、大量の要素を追加する際にパフォーマンスが低下することがあります。

  • std::dequeは複数のメモリブロックを使用するため、メモリの再配置が少なく、要素の追加や削除が効率的です。
  • 要素の追加と削除:
  • std::vectorは末尾への追加と削除がO(1)の時間計算量ですが、先頭や中間への操作はO(n)の時間計算量です。
  • std::dequeは先頭と末尾への追加と削除がO(1)の時間計算量であり、これが大きな利点です。
  • 要素のアクセス:
  • std::vectorstd::dequeの要素アクセスはどちらもO(1)の時間計算量です。

ただし、std::vectorは連続したメモリ領域を使用するため、キャッシュ効率が高いです。

以下に、std::dequestd::vectorのパフォーマンスを比較するサンプルコードを示します。

#include <iostream>
#include <vector>
#include <deque>
#include <chrono>
int main() {
    const int N = 1000000;
    // std::vectorのパフォーマンステスト
    std::vector<int> vec;
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        vec.push_back(i);
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> vec_duration = end - start;
    std::cout << "std::vector push_back: " << vec_duration.count() << " seconds" << std::endl;
    // std::dequeのパフォーマンステスト
    std::deque<int> deq;
    start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        deq.push_back(i);
    }
    end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> deq_duration = end - start;
    std::cout << "std::deque push_back: " << deq_duration.count() << " seconds" << std::endl;
    return 0;
}

このサンプルコードでは、std::vectorstd::dequeに対して100万回のpush_back操作を行い、その時間を計測しています。

実行結果は環境によって異なりますが、一般的にstd::dequeの方がメモリ再配置が少ないため、パフォーマンスが良いことが確認できます。

以上のように、std::dequeは特定のシチュエーションで非常に有効なコンテナです。

特に、先頭と末尾への頻繁な追加や削除が必要な場合に適しています。

std::dequeの注意点とベストプラクティス

std::dequeの利点と欠点

利点

  1. 両端からの高速な操作:

std::dequeは両端からの要素の追加と削除が高速です。

これは、内部的に複数のメモリブロックを使用しているため、再配置の必要がないからです。

  1. ランダムアクセスのサポート:

std::dequestd::vectorと同様にランダムアクセスをサポートしています。

これにより、特定のインデックスに直接アクセスすることができます。

  1. メモリ効率:

std::dequeはメモリの再配置が少ないため、大量の要素を扱う場合でも効率的です。

欠点

  1. メモリの断片化:

std::dequeは内部的に複数のメモリブロックを使用するため、メモリの断片化が発生する可能性があります。

  1. 中間要素の操作が遅い:

中間要素の挿入や削除は、std::vectorに比べて遅くなることがあります。

これは、複数のメモリブロックを操作する必要があるためです。

  1. メモリ使用量:

std::dequestd::vectorに比べてメモリ使用量が多くなることがあります。

これは、内部的に複数のメモリブロックを管理するためです。

効率的な使い方

  1. 両端からの操作を多用する場合:

std::dequeは両端からの要素の追加と削除が高速であるため、キューやデック(両端キュー)として使用するのに適しています。

  1. ランダムアクセスが必要な場合:

std::dequeはランダムアクセスをサポートしているため、特定のインデックスに直接アクセスする必要がある場合に適しています。

  1. メモリ再配置を避けたい場合:

std::dequeはメモリの再配置が少ないため、大量の要素を扱う場合でも効率的です。

std::dequeを使うべきシチュエーション

  1. キューの実装:

std::dequeは両端からの要素の追加と削除が高速であるため、キューの実装に適しています。

以下は、std::dequeを使用してキューを実装する例です。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> queue;
    // 要素の追加
    queue.push_back(1);
    queue.push_back(2);
    queue.push_back(3);
    // 要素の削除
    queue.pop_front();
    // キューの内容を表示
    for (int i : queue) {
        std::cout << i << " ";
    }
    return 0;
}
  1. スタックの実装:

std::dequeは両端からの要素の追加と削除が高速であるため、スタックの実装にも適しています。

以下は、std::dequeを使用してスタックを実装する例です。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> stack;
    // 要素の追加
    stack.push_back(1);
    stack.push_back(2);
    stack.push_back(3);
    // 要素の削除
    stack.pop_back();
    // スタックの内容を表示
    for (int i : stack) {
        std::cout << i << " ";
    }
    return 0;
}
  1. 双方向リストの実装:

std::dequeは両端からの要素の追加と削除が高速であるため、双方向リストの実装にも適しています。

以下は、std::dequeを使用して双方向リストを実装する例です。

#include <iostream>
#include <deque>
int main() {
    std::deque<int> deque;
    // 要素の追加
    deque.push_back(1);
    deque.push_front(2);
    deque.push_back(3);
    // 要素の削除
    deque.pop_front();
    deque.pop_back();
    // 双方向リストの内容を表示
    for (int i : deque) {
        std::cout << i << " ";
    }
    return 0;
}

以上のように、std::dequeは特定のシチュエーションで非常に有用です。

特に、両端からの操作が頻繁に行われる場合や、ランダムアクセスが必要な場合に適しています。

適切なデータ構造を選択することで、プログラムの効率を大幅に向上させることができます。

まとめ

この記事では、C++の標準ライブラリであるstd::dequeについて詳しく解説しました。

std::dequeは、両端からの高速な要素の追加と削除が可能なコンテナであり、特定のシチュエーションで非常に有用です。

この記事を通じて、std::dequeの基本的な使い方から応用までを理解し、実際のプログラミングに役立てていただければ幸いです。

今後もC++の標準ライブラリを活用して、効率的で読みやすいコードを書いていきましょう。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

目次から探す