Java データ構造 | プライオリティ キューのアナログ実装

目次

I.はじめに

2.ヒープシミュレーションは優先キューを実装します

2.1 ヒープの概念

2.2 ヒープの性質

2.3 ヒープ格納方法

2.4 ヒープの作成


I.はじめに

キューについては以前に学びました. キューは先入れ先出し (FIFO) のデータ構造ですが, 場合によっては, 操作するデータが優先されることがあります. 一般に, キューから取り出すときは, より優先度の高い要素が必要になることがあります. , このシナリオでは, キューを使用することは明らかに不適切です. たとえば, 携帯電話でゲームをプレイしているとき, 着信がある場合, システムは着信を優先する必要があります.

この場合、データ構造は 2 つの基本的な操作を提供する必要があります。1 つは最も優先度の高いオブジェクトを返すことで、もう 1 つは新しいオブジェクトを追加することです。このデータ構造がプライオリティ キュー (Priority Queue) です。

2.ヒープシミュレーションは優先キューを実装します

JDK1.8 の PriorityQueue の最下層はヒープ データ構造を使用し、ヒープは実際には完全なバイナリ ツリーに基づいていくつかの要素を調整します。

2.1 ヒープの概念

キーコードの集合 K = {k0, k1, k2, ..., kn-1} がある場合、そのすべての要素を完全な二分木の順序で 1 次元配列に格納し、次の条件を満たします。 Ki < = K2i +1 and Ki<= K2i+2 (Ki >= K2i+1 and Ki >= K2i+2) i = 0, 1, 2... の場合、これは小さなヒープ (または大きなヒープ) と呼ばれます。 . ルート ノードが最大のヒープを最大ヒープまたは大ルート ヒープと呼び、ルート ノードが最小のヒープを最小ヒープまたは小ルート ヒープと呼びます。

2.2ヒープの性質

  • ヒープ内のノードの値は常に、その親ノードの値よりも大きくも小さくもありません。

  • ヒープは常に完全なバイナリ ツリーです。

二分探索木とは異なり、ヒープの左右のノードはルートノードよりも小さく、左右のノードの値には大小関係がありません

2.3 ヒープ格納方法

ヒープは完全なバイナリ ツリーであるため、レイヤー順序のルールを使用して順次格納できます。

注:不完全なバイナリ ツリーの場合、シーケンシャル ストレージの使用は適していません。これは、バイナリ ツリー スペースを復元できるようにするために空のノードを格納する必要があり、スペースの使用率が比較的低くなるからです。

要素を配列に格納した後、バイナリ ツリーの章のプロパティ 5 に従って、ツリーを復元できます。i が配列内のノードの添え字であると仮定すると、次のようになります。

  • i が 0 の場合、i で表されるノードはルート ノードです。それ以外の場合、i ノードの親ノードは (i - 1)/2 です。

  • 2 * i + 1 がノード数より少ない場合、ノード i の左側の子の添字は 2 * i + 1 であり、それ以外の場合、左側の子はありません。

  • 2 * i + 2 がノード数より少ない場合、ノード i の右側の子の添字は 2 * i + 2 であり、それ以外の場合は右側の子はありません

2.4 ヒープの作成

  • ヒープ調整

条件: 調整するには、左右のサブツリーがすでにヒープである必要があります

コレクション {27,15,19,18,28,34,65,49,25,37} のデータをヒープに作成するとどうなるでしょうか。

このツリーを観察すると、ルート ノードの左側と右側が小さなルート ヒープのプロパティを満たしていることがわかり、ルート ノードを下に調整するだけで済みます。

調整プロセス:

  • この二分木のルートノードを親ノードに設定し、

  • 親ノードの子ノードの値を比較し、子ノードのうち小さい方のノードを子ノードとする

    • 初期状態

  • 親ノードと子ノードの値の大きさを比較する

    • 親 > 子の場合、小さなルート ヒープの性質が満たされず、両者が入れ替わります。

    • 親<子の場合、小さなルートヒープの性質が満たされ、交換は行われず、調整は終了します。

  • 各交換の後、子と親の位置を更新します。親 = 子供、子 = 2 * 親 + 1;

  • コードの実装: 時間の複雑さ: O(logN) — 親は固定、子 x 毎回 2

//    小根堆的向下调整(满足parent的左子树和右子树已经是堆了)
    public void shiftDown(int parent,int len){
        int child = 2*parent +1;
//        必须保证右左孩子
        while(child < len){
//            找到左右孩子的最小值
            if(child +1 < len && elem[child] > elem[child+1]){
                child++;
            }
            if(elem[child] < elem[parent]){
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
//                向下调整重新更新
                parent =child;
                child = 2 *parent+1;
            }else{
                break;
            }
        }
    }

下方調整のアイデアについては、ヒープを構築し、配列をヒープに構築し、最後から 2 番目の非リーフ ノードから開始し、配列を後ろから前にトラバースし、順番に下に調整して小さい値を取得します。ルートヒープ

例: 次の配列 [9,5,2,7,3,6,8] を小さなルート ヒープに構築します。

この時点で、ルート ノードの左右の子の両側の木は、小さなルート ヒープの特性を満たしています. 9 をルートとするツリーを調整するだけで、下向きに調整できます. 調整プロセスと結果は次のとおりです.

最終結果は

  • コード

時間の複雑さ: ヒープを構築する時間の複雑さは O(n) (複雑な数学的計算)

    public void crearHeap(){
//        最后一个节点的下标为  i  = usedSize -1
//         (i - 1) / 2 即为最后一个非叶子节点的下标
        for(int parent = (usedSize-1-1)/2; parent >= 0;parent--){
           shiftDown(parent,usedSize);
          //对每一个非叶子节点进行向下调整
        }
    }

usedsize - 最後の葉ノードの添字です ((usedsize -1) - 1) / 2 は最後の非葉ノードの添字です

  • ヒープ調整

要素を挿入するときは、ヒープが大きなルート ヒープであることを確認する必要があるため、ヒープを上方に調整する必要があります。

ヒープを上方に調整する手順

  • 挿入された要素、つまり最後の葉ノードを子に設定し、その親ノードを親に設定 = (子-1) /2

  • 子>親の場合、大きなルートヒープの性質が満たされないため、親ノードの値と葉ノードの値が入れ替わる

  • 子<親の場合は条件を満たし、調整不要

調整が完了したら、親子の位置を再更新します。つまり、子 = 親、親 = 2 * 子 +1 です。

大きなルート ヒープに合わせて上方に調整するコードは次のとおりです。

  public void shiftUp(int child){
        int parent = (child-1) /2;
        while(child > 0){
            if(elem[child] > elem[parent]){
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child -1)/2;
            }else {
                break;
            }
        }
    }
  • 要素がヒープに挿入されると、コードは次を実装します

public void offer(int val){
//如果堆为满,则对数组进行扩容
        if(isFull()){
            elem = Arrays.copyOf(elem,2*elem.length);
        }
 //将插入的元素设置为堆的最后一个元素
        this.elem[usedSize] = val;
        usedSize++;
 //将堆中元素进行向上调整
        shiftUp(usedSize-1);
    }
     public boolean isFull(){
        return elem.length == usedSize;
    }
  • ヒープの削除(ヒープの先頭要素を削除)

    • ヒープの一番上の要素を、キュー内のヒープの最後のノードの値と交換します

    • ヒープ内の要素の値を 1 減らします

    • ヒープ内の要素を下方に調整します

コードは次のように実装されます。

public int pop(){
        if(isEmpty()){
            return -1;
        }
        int tmp = elem[0];
        elem[0] = elem[usedSize -1];
        elem[usedSize -1] = tmp;
        usedSize--;
    //将堆中元素进行向下调整
        shiftDown(0,usedSize);
        return tmp;
    }
  • ヒープシミュレーションによるプライオリティキューの実装

public class TestHeap {
    public int[] elem;
​
    public int usedSize;
​
    public static int DEFAULT_SIZE = 10 ;
​
    public TestHeap() {
        this.elem = new int[DEFAULT_SIZE];
    }
​
    public void init(int[] array){
        for(int i = 0; i < array.length;i++){
            elem[i] = array[i];
            usedSize++;
        }
    }
//   建堆的时间复杂度为O(n)
    public void crearHeap(){
//        最后一个节点的下标为  i  = usedSize -1
//         (i - 1) / 2 即为父亲节点的下标
        for(int parent = (usedSize-1-1)/2; parent >= 0;parent--){
           shiftDown(parent,usedSize);
        }
    }
    /**
     *
     * @param parent 每棵子树的根节点
     * @param len 每棵子树的
     * 时间复杂度:O(log(n))
     */
//    小根堆的向下调整(满足parent的左子树和右子树已经是堆了)
    public void shiftDown(int parent,int len){
        int child = 2*parent +1;
//        必须保证右左孩子
        while(child < len){
//            找到左右孩子的最小值
            if(child +1 < len && elem[child] > elem[child+1]){
                child++;
            }
            if(elem[child] < elem[parent]){
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
//                向下调整重新更新
                parent =child;
                child = 2 *parent+1;
            }else{
                break;
            }
        }
    }
    public void offer(int val){
        if(isFull()){
            elem = Arrays.copyOf(elem,2*elem.length);
        }
        this.elem[usedSize] = val;
        usedSize++;
        shiftUp(usedSize-1);
    }
//    向上调整
        public void shiftUp(int child){
        int parent = (child-1) /2;
        while(child > 0){
            if(elem[child] > elem[parent]){
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child -1)/2;
            }else {
                break;
            }
        }
    }
    public boolean isFull(){
        return elem.length == usedSize;
    }
    public boolean isEmpty(){
        return usedSize == 0;
    }
    public int pop(){
        if(isEmpty()){
            return -1;
        }
        int tmp = elem[0];
        elem[0] = elem[usedSize -1];
        elem[usedSize -1] = tmp;
        usedSize--;
        shiftDown(0,usedSize);
        return tmp;
    }
    public int peek(){
        if(isEmpty()){
            return -1;
        }
        return elem[0];
    }
}

ced485cbb11e458d81a746890b32cf3f.gif

おすすめ

転載: blog.csdn.net/m0_56361048/article/details/127839213