目次
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];
}
}