0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PriorityQueueに関するメモ

0
Last updated at Posted at 2026-03-30

PriorityQueueに関するAI回答メモ。

std::priority_queueの並び順は

C++のstd::priority_queue(優先度付きキュー)の並び順についてですね。

結論から言うと、デフォルト設定では**「値が大きいものほど優先度が高く、先頭(top)に来る」**という設定になっています。いわゆる 「最大ヒップ (Max Heap)」 の状態です。1

1.デフォルトの挙動(降順)

何も指定せずに宣言すると、内部では std::less という比較関数が使われます。これは「AがBより小さい場合に真」を返すものですが、優先度付きキューにおいては**「より小さいものは後ろへ、より大きいものは前へ」**という解釈になります。

#include <queue>
#include <iostream>

int main() {
    std::priority_queue<int> pq;
    pq.push(10);
    pq.push(30);
    pq.push(20);

    // 30, 20, 10 の順に取り出される
    while(!pq.empty()) {
        std::cout << pq.top() << " "; 
        pq.pop();
    }
}

2. 昇順(値が小さい順)にしたい場合

最小値から取り出したい(Min Heapにしたい)場合は、テンプレート引数を3つ指定し、比較関数に std::greater を渡します。

// 型, 内部コンテナ, 比較関数
std::priority_queue<int, std::vector<int>, std::greater<int>> pq;

javaのPriorityQueueの並び順は

Javaの PriorityQueue についてですね。C++と対比させると非常に面白いのですが、実は**JavaのデフォルトはC++とは「逆」**になっています。

結論から言うと、Javaの PriorityQueue はデフォルトで 「値が小さいものほど優先度が高く、先頭(poll)に来る」 設定です。いわゆる 「最小ヒップ (Min Heap)」 です。

1. デフォルトの挙動(昇順)

Javaでは、要素が Comparable インターフェースを実装している場合(Integer や String など)、自然順序付けに従って最小値がキューの先頭になります。

import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        pq.add(30);
        pq.add(10);
        pq.add(20);

        // 10, 20, 30 の順に取り出される
        while (!pq.isEmpty()) {
            System.out.println(pq.poll());
        }
    }
}

2. 降順(値が大きい順)にしたい場合

C++のデフォルトと同じ挙動(最大ヒップ)にしたい場合は、Collections.reverseOrder() をコンストラクタに渡します。2

PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());

なぜ Java は最小ヒップなのか?

Javaの設計思想として、SortedSet や TreeMap など、多くのコレクションが「自然な順序(昇順)」をデフォルトとしているため、それに合わせていると考えられます。

注意点: > iterator() や forEach で中身を覗くと、要素がソートされていないように見えることがあります。これは、内部の「ヒップ構造(完全二分木)」をそのまま配列として表示しているためです。「取り出す時(poll)にだけ最小値が出てくる」 というのが正しい理解です。

Heapはヒップでなくヒープじゃないの?

その通りです!完全に私の**誤変換(あるいはカタカナの読み間違い)**でした。大変失礼いたしました。

Heap は日本語で 「ヒープ」 と呼ぶのが正解です。
「お尻(Hip)」や「流行の(Hip)」と同じような響きで書いてしまいましたが、データ構造としては「積み重なった山」を意味する Heap(ヒープ) ですね。

IntPriorityQueue

PriorityQueue<E>のObject[] queueを、int[] queueに変えた優先度付きキューをAI8割、手作業2割でJDKそっくりに作ってみた。
最初は、自然順序付けにおける最小要素が先頭のみだったが、コンストラクタでComparatorを渡すようにdescendingフラグを渡し、最大要素が先頭になるモードも用意した。

あまり意味がないかと思っていたが、Pair(key, val)val * max_key + keyの値として扱えば、valを優先度としたkeyのキューがint[]で作れる。
もしkeyが大きい値ならば、LongPriorityQueueを用意すれば、int key,val;の情報をそのまま置き換えられる。

import java.util.Arrays;
import java.util.NoSuchElementException;

public class IntPriorityQueue {

	private static final int DEFAULT_INITIAL_CAPACITY = 11;

	private int[] queue;

	private int size = 0;

    private final boolean descending;

    public IntPriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, false);
    }

    public IntPriorityQueue(int initialCapacity) {
        this(initialCapacity, false);
    }

    public IntPriorityQueue(boolean descending) {
        this(DEFAULT_INITIAL_CAPACITY, descending);
    }

    public IntPriorityQueue(int initialCapacity, boolean descending) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new int[initialCapacity];
        this.descending = descending;
    }

    private void grow() {
        int oldCapacity = queue.length;
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        queue = Arrays.copyOf(queue, newCapacity);
    }

    public boolean add(int e) {
        return offer(e);
    }

    public boolean offer(int e) {
        int i = size;
        if (i >= queue.length)
            grow();
        if (size == 0) {
            queue[0] = e;
        } else {
            siftUp(i, e);
        }
        size = i + 1;
        return true;
    }

    public int peek() {
        if (size == 0) throw new NoSuchElementException("Queue is empty"); //!!
        return queue[0];
    }

    public int size() {
        return size;
    }

    public void clear() {
        size = 0;
    }

    public int poll() {
        final int[] es = queue;
        final int result;

        if (size == 0) throw new NoSuchElementException("Queue is empty"); //!!
        result = es[0];
        final int n = --size;
        final int x = es[n];
        if (n > 0)
            siftDown(0, x);
        return result;
    }

    // --- 内部アルゴリズム ---

    private int compare(int x, int e) {
    	if (descending)
    		return Integer.compare(e, x); // 降順: e < x なら負(xの方が優先度高)
    	else
    		return Integer.compare(x, e); // 昇順: x < e なら負(xの方が優先度高)
    }

    private void siftUp(int k, int x) {
    	siftUpUsingComparator(k, x, queue);
    }

    private void siftUpUsingComparator(int k, int x, int[] es) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            int e = es[parent];
            if (compare(x, e) >= 0)
            	break;
            es[k] = e;
            k = parent;
        }
        es[k] = x;
    }

    private void siftDown(int k, int x) {
    	siftDownUsingComparator(k, x, queue, size);
    }

    private void siftDownUsingComparator(int k, int x, int[] es, int n) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            int c = es[child];
            int right = child + 1;
            if (right < n && compare(c, es[right]) > 0)
                c = es[child = right];
            if (compare(x, c) <= 0)
            	break;
            es[k] = c;
            k = child;
        }
        es[k] = x;
    }

}

テスト

本体はJDKのソースと見比べて、まあ、ほとんど同じだったけど、変数名を揃えた。

次に手直したjavaソースをnotebookLMに追加して、
JUnitテストを作ってと頼むと、そのままOKだった。
さらに本家PriorityQueueとIntPriorityQueueの比較してと頼むと、これもOKだった。
これはすごいな。

100万件を超えると違うな。
100万件を一気に追加するのと、1000件づつトータル100万件ではまた違うだろうけど、Integerインスタンスを生成するコストが効いてくるのだろうか。

--- benchmark (10000) ---
java.util.PriorityQueue: 3 ms
IntPriorityQueue:        1 ms
improvement: 3.00 

--- benchmark (100000) ---
java.util.PriorityQueue: 25 ms
IntPriorityQueue:        17 ms
improvement: 1.47 

--- benchmark (1000000) ---
java.util.PriorityQueue: 508 ms
IntPriorityQueue:        116 ms
improvement: 4.38 

--- benchmark (10000000) ---
java.util.PriorityQueue: 10397 ms
IntPriorityQueue:        1963 ms
improvement: 5.30 

ドキュメント

(javaソースを元にAIが生成)3

Java SE 24 PriorityQueue とプリミティブ特化型実装の技術リファレンス:構造、アルゴリズム、性能特性

1. はじめに:優先度付きキューの戦略的役割

Javaプラットフォームにおける優先度付きキューは、タスクスケジューリング、ダイクストラ法などのグラフアルゴリズム、あるいは低遅延なストリーム処理において、計算資源の配分を決定する極めて重要なデータ構造です。

Java Collections Framework(JCF)の標準実装である PriorityQueue は、ジェネリクスによる型抽象化を提供し、任意の参照型に対して柔軟な順序制御を可能にします。しかし、システムアーキテクトの視点からは、この抽象化がもたらす「型ヒープ汚染(Heap Pollution)」への配慮や、ラッパークラス(Integer/Long)起因の「ポインタ間接参照」によるオーバーヘッドを無視することはできません。特に数値処理が支配的なワークロードにおいて、標準の PriorityQueue と、IntPriorityQueue / LongPriorityQueue のようなプリミティブ特化型実装を適切に使い分けることは、スループット向上とGC停止時間の短縮に直結する戦略的選択となります。

次のセクションでは、これらのデータ構造を下支えする内部構造と、実行効率を決定づけるストレージメカニズムについて詳述します。

2. 内部構造とデータ格納メカニズムの比較

標準の PriorityQueue とプリミティブ特化型実装では、メモリ上の物理的なレイアウトが根本的に異なります。これが現代のCPUアーキテクチャにおけるキャッシュ効率に多大な影響を与えます。

内部配列とメモリレイアウトの評価

JDK 24 の PriorityQueue は内部で Object[] を保持します。これは各要素がオブジェクトへの参照(ポインタ)であることを意味し、実際のデータ(例:Integer)はヒープ上の別の場所に散在します。一方、プリミティブ特化型は int[]long[] を直接使用します。

  1. オブジェクトヘッダーのオーバーヘッド: Integer オブジェクトは、データ自体は4バイトですが、JVM内で12〜16バイト程度のオブジェクトヘッダーを伴います。プリミティブ配列はこのオーバーヘッドを排除し、メモリ消費を1/3〜1/4に抑えます。
  2. 空間局所性の向上: Object[] では要素へのアクセスにポインタ間接参照が発生しますが、プリミティブ配列はデータがメモリ上に連続して配置されるため、CPUキャッシュのヒット率が劇的に向上します。
容量管理アルゴリズム

内部配列の拡張ポリシー(grow メソッド)は、いずれの実装も JDK の標準的なヒューリスティックに従っています。

  • 64未満: oldCapacity + (oldCapacity + 2)、つまり約2倍以上の急激な拡張を行い、初期の挿入コストを下げます。
  • 64以上: oldCapacity + (oldCapacity >> 1)、つまり1.5倍の緩やかな拡張により、大規模データセットにおけるメモリの過剰割り当てを抑制します。
内部構造の比較表
項目 PriorityQueue (JDK 24) IntPriorityQueue LongPriorityQueue
内部ストレージ型 Object[](ポインタ配列) int[](連続値配列) long[](連続値配列)
メモリ効率 低(ヘッダーと参照による肥大) 極めて高い 極めて高い
デフォルト初期容量 11 11 11
動的リサイズポリシー 64未満: 2倍+2 / 以上: 1.5倍 64未満: 2倍+2 / 以上: 1.5倍 64未満: 2倍+2 / 以上: 1.5倍
null 許容性 許可されない (NPE) 非該当(プリミティブを直接保持) 非該当(プリミティブを直接保持)

この物理的な構造が、どのようにヒープ順序を維持するアルゴリズムへと繋がるかを説明し、次章へ移行します。

3. ヒープ構造に基づく要素順序付けとアルゴリズムの詳細

これらの実装はすべて、完全二分木を配列で表現した「二分ヒープ(Binary Heap)」構造を採用しています。要素の移動は、配列インデックスの算術演算のみで行われます。

siftUp(浮上処理)の解析

offer メソッド実行時、新要素を末尾から根方向へ移動させるプロセスです。 親ノードのインデックス計算には (k - 1) >>> 1 が用いられます。ここでの符号なし右シフト(>>>)の使用は、単なる2分の1計算以上に、CPUレベルでの効率的な演算と、インデックスが負になることを防ぐ(理論上の配列境界制限に対する)防御的プログラミングの側面を持ちます。

siftDown(下降処理)の解析

poll メソッド実行後、根を再構築するプロセスです。 左右の子ノード (k << 1) + 1(k << 1) + 2 を比較し、優先度の高い方を引き上げます。プリミティブ版の実装では、比較関数 compare(int x, int e) を通じて Integer.compare 等を直接呼び出すため、オブジェクト版のような Comparable へのキャストやインターフェース経由のメソッド呼び出しが不要です。

順序付けの柔軟性とオーバーヘッド

JDK標準版は Comparator による実行時の柔軟性を提供しますが、これは「ポリモーフィック・ディスパッチ(多態的呼び出し)」となり、JITコンパイラによる最適化の障壁になる場合があります。対してプリミティブ版は、内部の descending フラグによる単純な条件分岐のみで昇順・降順を切り替えるため、分岐予測が効きやすく実行効率が極めて高くなります。

アルゴリズムの論理的ステップを整理したところで、次にそれらがもたらす計算量と実行性能について分析します。

4. 計算量(Big O)と性能特性の評価

計算量のオーダーは同一であっても、実効速度には「定数項」の差が顕著に現れます。

計算量サマリー
  • offer / add: O(log n) - ヒープの深さに比例。
  • poll: O(log n) - 再構築に要する比較・移動。
  • peek / size: O(1) - 配列先頭への直接アクセス。
  • remove(Object) / contains(Object): O(n) - JDK標準実装は線形探索を行います。プリミティブ版において検索系メソッドを実装する場合も同様に O(n) ですが、前述の空間局所性により、メモリの連続スキャン速度はプリミティブ版が圧倒的に高速です。
プリミティブ版の優位性とJIT最適化

プリミティブ特化型の最大の利点は、Integer.compare や Long.compare が「静的メソッド」として呼び出される点にあります。これにより、JITコンパイラは「インライン展開(Inlining)」を容易に行うことができ、メソッド呼び出しのスタック構築をスキップできます。一方、Comparator インターフェースを介した比較は、仮想関数テーブル(vtable)のルックアップが必要となる「バーチャルコール」となり、特に小規模な比較を繰り返すヒープ操作においては、このレイテンシの差が無視できない累積遅延となります。

性能面での利点を理解した上で、実運用において不可欠なスレッドセーフティの制約について確認します。

5. スレッドセーフティと並行アクセスに関する制約

高いパフォーマンスを実現するための設計として、これらのクラスには排他制御が組み込まれておらず、利用には注意が必要です。

非同期設計とリスク

PriorityQueue およびプリミティブ版は、いずれもスレッドセーフではありません。マルチスレッド環境で保護なしにアクセスした場合、内部配列の状態不整合や size フィールドの競合が発生し、システムのクラッシュや無限ループを招くリスクがあります。

堅牢性の違い:Fail-Fast メカニズム
  • JDK標準実装: 構造的な変更を追跡する modCount フィールドを持ち、イテレータによる走査中に変更が発生した際、ConcurrentModificationException をスローする「フェイルファスト」挙動を備えています。
  • プリミティブ特化型: 提供された実装には modCount や Iterator 実装自体が含まれていません。これは、極限のパフォーマンスを追求するために「安全性チェック」というオーバーヘッドすら削ぎ落とした設計であることを意味します。利用者は呼び出し側で完全にアクセスを制御する責任を負います。

並行性が必要な場合は、java.util.concurrent.PriorityBlockingQueue を採用すべきです。これは内部でロックを管理し、スレッド間の安全な受け渡しを保証します。

最後に、本ドキュメントの要点をまとめ、最適な実装選択のための指針を示します。

6. 結論:ユースケースに応じた最適な実装の選択

データ構造の選択は、エンジニアリングにおけるトレードオフのバランスを決定する行為そのものです。

  • PriorityQueue (JDK標準) を選択すべき場合:
    • 文字列や複雑なドメインオブジェクトを管理する必要がある。
    • 実行時に Comparator を切り替えるなど、高度な柔軟性が求められる。
    • 標準ライブラリによる保守性と、フェイルファストな安全性を優先したい。
  • プリミティブ特化型 (Int/LongPriorityQueue) を選択すべき場合:
    • 数千万件規模の数値を保持し、GCの圧力とメモリ消費を最小化したい。
    • 低レイテンシが要求される環境で、JITのインライン展開による恩恵を最大化したい。
    • 反復処理の安全性よりも、数値計算のスループットを極限まで追求したい。
設計上の教訓

優れたアーキテクトは、単に O(log n) という計算量を見るだけでなく、メモリアライメント、ポインタ間接参照のコスト、JITによるインライン化の可否までを洞察して実装を選択します。標準実装の汎用性と特化型の効率性を正しく理解し、要件に応じてこれらを使い分けることが、堅牢かつ高性能なJavaシステム構築の鍵となります。

ソース置き場

  • IntPriorityQueue.java: int[]を持つPriorityQueue
  • IntPriorityQueueTest.java: IntPriorityQueueのJUnitテスト
  • LongPriorityQueue.java: long[]を持つPriorityQueue
  • LongPriorityQueueTest.java: LongPriorityQueueのJUnitテスト
  • PriorityQueueComparison.java: PriorityQueueとIntPriorityQueueの比較

  1. AIはヒープをヒップと読むらしい。

  2. いつも自分でComparatorを作っていた。

  3. AIが生成した文書に対して、見出しとテーブル、一部のコードスパンのMarkdown修正を行った。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?