パイル
1.ヒープ定義:
この完全なバイナリツリーのいずれかのノードのキーがその左右の子のキー以下である場合、それは小さなルートヒープと呼ばれ、そうでない場合は大きなルートヒープです。そのような:
実際、ヒープが格納されるとき、それはツリー構造ではなく、配列の形式で格納されます。それを理解する方法は?
簡単に言えば、ヒープは完全なバイナリツリーとして論理的に表され、配列として物理的に表されます。そのような:
ツリーと配列の間には関連付けがあります。ツリー内の配列内の親(親)、左の子(左)、および右の子(右)の添え字は、次の関係を満たします。(1)配列内の親がわかっている場合、配列内の左の子と右の子の対応する添え字は次のようになります。
左=2*親+1;
右=2*親+2;
(2)子の下付き文字の子を知る(左の子または右の子に関係なく):parent =(child-1)/ 2;
この配列は、上記の完全な二分木のレベル順トラバーサルと見なすこともできます。
2.ヒープの操作
2.1ヒープの再構築:質問:ヒープの最上位レコードが変更されたときにヒープを再構築するにはどうすればよいですか?例として小さなヒープを取り上げましょう:
[アルゴリズムのアイデア]最初に、に対応する完全なバイナリツリーのルートノードのレコードを削除します。調整済みレコードと呼ばれるヒープ。
このとき、キーワードが大きい元の子ノードは空のノードに相当し、キーワードが小さいレコードが空のノードの左右のサブツリーから選択されます。レコードのキーワードがまだ調整するレコードキーの1つで、レコードを空のノードに移動します。
上記の移動プロセスは、空のノードの左右のサブツリーのキーワードが、調整するレコードのキーワードよりも小さくなるまで繰り返されます。この時点で、
調整するレコードを空のノードに配置できます。
上記の調整方法は、下方修正されるレコードを段階的に「スクリーニング」するプロセスに相当するため、一般に「スクリーニング」方式または「下方調整」と呼ばれます。
コードを使用して説明します。
public static void shiftDown(long[] array, int size, int index) {
// index代表要调整的位置。size为堆的大小。
//
while (true) {
// 1. 判断 index 所在位置是不是叶子
// 逻辑上,没有左孩子一定就是叶子了(因为完全二叉树这个前提)
int left = 2 * index + 1;
if (left >= size) {
// 越界 -> 没有左孩子 -> 是叶子 -> 调整结束
return; // 循环的出口一:走到的叶子的位置
}
// 2. 找到两个孩子中的最值【最小值 via 小堆】
// 先判断有没有右孩子
int right = left + 1; // right = 2 * index + 2
int min = left; // 假设最小值就是左孩子,所以 min 保存的最小值孩子所在的下标
if (right < size && array[right] < array[left]) {
// right < size 必须在 array[right] < array[left] 之前,不能交换顺序
// 因为先得确定有右孩子,才有比较左右孩子的意义
// 有右孩子为前提的情况下,然后右孩子的值 < 左孩子的值
min = right; // min 应该是右孩子所在的下标
}
// 3. 将最值和当前要调整的位置进行比较,判断是否满足堆的性质
if (array[index] <= array[min]) {
// 当前要调整的结点的值 <= 最小的孩子值;说明这里也满足堆的性质了,所以,调整结束
return; // 循环的出口一:循环期间,已经满足堆的性质了
}
// 4. 交换两个值,物理上对应的就是数组的元素交换 min 下标的值、index 下标的值
long t = array[index];
array[index] = array[min];
array[min] = t;
// 5. 再对 min 位置重新进行同样的操作(对 min 位置进行向下调整操作)
index = min;
}
}
プロセスは次のとおりです。
2.2初期ヒープの構築:質問:任意のシーケンスから初期ヒープを構築するにはどうすればよいですか?
【アルゴリズムの考え方】任意のシーケンスを対応する完全な二分木と見なします。リーフノードは単一要素の
ヒープと見なすことができるため、上記の調整ヒープアルゴリズム(「スクリーニング」方式)を繰り返し使用できます。
完全な二分木全体がヒープに調整されるまで、サブツリーはヒープに調整されます。
上記の完全な二分木では、最後の非葉ノードがLn / 2J番目の位置にあり、nは二分木のノードの数であることが証明できます
。したがって、「スクリーニング」は、Ln / 2J番目のノードから開始し、ルートノードまでレイヤーごとに逆方向に進む必要があります。
public static void buildHeap (int[] array) {
//我们假定传入的数组是经过处理的,即数组内的元素个数就是堆的元素个数。
//通过二叉树可以观察到只需要从最后一个节点的双亲结点开始从底向上进行向下调整
for (int i = (array.length-2)/2; i >=0 ; i--) {
shiftDown(array,array.length,i);
}
}
private static void shiftDown(int[] array, int size, int index) {
//index 为当前需要调整的位置
while (index * 2 + 1 < size){
int left = index * 2 + 1;
int right = left + 1;
//找出最小孩子的下标
int min = left;
if (right < size && array[min] > array[right]){
min = right;
}
//如果当前结点满足堆的性质则结束。
if (array[index] < array[min]){
return;
}
//交换当前结点与最小孩子的值
swap(array,min,index);
//继续向下调整
index = min;
}
}
private static void swap(int[] array, int min, int index) {
int t = array[index];
array[index] = array[min];
array[min]= t;
}
第二に、優先キュー(優先キュー)
1.定義:優先キュー内の要素は任意の順序で挿入できますが、順序付けられた順序で取得されます。つまり、removeメソッドが呼び出されるたびに、現在の優先度キュー内の最小の要素が常に取得されます。ただし、優先キューはすべての要素を並べ替えるわけではありません。これらの要素が繰り返し処理される場合、それらを並べ替える必要はありません。優先キューは、ヒープと呼ばれるきちんとした効率的なデータ構造を使用します。ヒープは自己組織化された二分木であり、その追加(広告)および(削除)操作により、要素の並べ替えに時間を費やすことなく、最小の要素をルートに移動できます。
TreeSetと同様に、優先度付きキューは、Ccomparableインターフェイスを実装するクラスオブジェクトまたは
コンストラクターで提供されるComparatorオブジェクトのいずれかを保持できます。
優先キューの一般的な使用法は、タスクのスケジューリングです。各タスクには優先順位があり、タスクはランダムな順序でキューに追加されます
。新しいタスクが開始されるたびに、最も高い優先度のタスクがキューから削除されます(通常、1が
「最も高い」優先度として設定されるため、削除操作は最小の要素を削除します)。
優先キューを実装します。
// 直接使用 long 类型作为我们的元素类型,不考虑泛型了
public class MyPriorityQueue {
// 很重要的属性:堆 = 数组 + 有效元素个数
private long[] array;
private int size;
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
// 由于我们的元素类型是 long 类型,不需要考虑 Comparable 和 Comparator 的问题
// 所以我们只需要一个构造方法即可
public MyPriorityQueue() {
array = new long[16];
size = 0;
}
public void offer(long e) {
// 放入我们的优先级队列中,放入之后,保证堆的性质仍然是满足的
ensureCapacity();
array[size] = e;
size++;
// [size - 1] 就是刚刚插入的元素的位置
shiftUp(array, size - 1);
}
// 前提:size > 0
public long peek() {
// 返回堆顶元素
if (size < 0) {
throw new RuntimeException("队列是空的");
}
return array[0];
}
public long poll() {
// 返回并删除堆顶元素
if (size < 0) {
throw new RuntimeException("队列是空的");
}
long e = array[0];
// 用最后一个位置替代堆顶元素,删除最后一个位置
array[0] = array[size - 1];
array[size - 1] = 0; // 0 代表这个位置被删除了,不是必须要写的
size--;
// 针对堆顶位置,做向下调整
shiftDown(array, size, 0);
return e;
}
// 检查我们的优先级队列对象是否正确
// 1. 0 <= size && size <= array.length
// 2. 满足小堆的特性(任取结点(除开叶子结点),其值 <= 它的两个孩子的值(如果存在的话)
public void check() {
if (size < 0 || size > array.length) {
throw new RuntimeException("size 约束出错");
}
// 如果每个结点都没问题,说明小堆成立
for (int i = 0; i < size; i++) {
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left >= size) {
// 说明是叶子,跳过
continue;
}
// 左孩子破坏了规则
if (array[i] > array[left]) {
throw new RuntimeException(String.format("[%d] 位置的值大于其左孩子的值了", i));
}
// 右孩子破坏了规则
if (right < size && array[i] > array[right]) {
throw new RuntimeException(String.format("[%d] 位置的值大于其右孩子的值了", i));
}
}
}
private void shiftDown(long[] array, int size, int index) {
while (2 * index + 1 < size) {
// 说明 index 一定有左孩子的
int min = 2 * index + 1;
int right = min + 1;
if (right < size && array[right] < array[min]) {
min = right;
}
if (array[index] <= array[min]) {
return;
}
swap(array, index, min);
index = min;
}
}
private void swap(long[] array, int i, int j) {
long t= array[i];
array[i] = array[j];
array[j] = t;
}
private void ensureCapacity() {
if (size < array.length) {
return;
}
array = Arrays.copyOf(array, array.length * 2);
}
// 向上调整期间,不需要 size
private void shiftUp(long[] array, int index) {
while (index != 0) {
int parent = (index - 1) / 2;
if (array[parent] <= array[index]) {
return;
}
swap(array, index, parent);
index = parent;
}
}
}
2. Javaで実装された優先キュー:
優先度ヒープに基づく無制限の優先度キュー。優先キューの要素は、使用される構築方法に応じて、自然な順序Comparator
で、またはキューの構築時に提供された順序に従ってソートされます。優先キューはnull
要素。自然順序付けに依存する優先キューも、比較できないオブジェクトの挿入を許可しません(そうすると、結果として生じる可能性があります
ClassCastException
)。。
このキューの先頭は、指定された順序によって決定される最小の要素です。複数の要素が最小値である場合、ヘッダーは要素の1つです-選択方法は任意です。キューは操作poll
を取得しremove
、キューの先頭にある要素にアクセスpeek
します。element
優先キューには制限がありませんが、キュー要素の格納に使用される配列のサイズを制御する内部容量があります。通常、少なくともキューのサイズと同じです。要素が優先キューに追加されると、その容量は自動的に増加します。容量増加戦略の詳細を指定する必要はありません。
このクラスとそのCollection
イテレータは、およびIterator
インターフェイスのすべてのオプションのメソッドを実装します。iterator()
メソッドで提供されるイテレータは、特定の順序で優先キューの要素をトラバースすることが保証されていません。順番にトラバースする必要がある場合は、の使用を検討してくださいArrays.sort(pq.toArray())
。
メソッドの概要 | ||
---|---|---|
boolean |
add(E e) 指定された要素をこの優先キューに挿入します。 |
|
void |
clear() この優先キューからすべての要素を削除します。 |
|
Comparator<? super E> |
comparator() このキュー内の要素を並べ替えるために使用されるコンパレータを返します。または、このキューがその要素の自然な順序に従って並べ替えられている場合を返します null 。 |
|
boolean |
contains(Object o) このキューに指定された要素が含まれているかどうかを返します true 。 |
|
Iterator<E> |
iterator() このキューの要素に対するイテレータを返します。 |
|
boolean |
offer(E e) 指定された要素をこの優先キューに挿入します。 |
|
E |
peek() このキューの先頭を取得しますが、削除しません。このキューが空の場合はnullを返します。 |
|
E |
poll() このキューの先頭を取得して削除します。このキューが空の場合はnullです。 |
|
boolean |
remove(Object o) 指定された要素の単一のインスタンスが存在する場合は、このキューから削除します。 |
|
int |
size() このコレクションの要素数を返します。 |
|
Object[] |
toArray() このキューのすべての要素を含む配列を返します。 |
|
|
toArray(T[] a) このキューのすべての要素を含む配列を返します。返される配列のランタイムタイプは、指定された配列のランタイムタイプです。 |
3.要約:
1.ヒープは、定義上は2分木ですが、実際の実装では配列です。
配列内のバイナリツリーの親ノードと子ノードの間の添え字関係:
左=親*2+1;
右=親*2+ 2;
親=(子-1)/ 2;
2.ヒープ:頻繁に変更されるデータセットの中で最大の価値を見つけます。
3.ヒープのコア操作:下方調整、初期ヒープの構築。
4、優先キューを実現できます。
5.トップkの問題。