一 点睛
在算法设计中,经常需要从序列中查找最值(最小值或最大值),例如:最短路径、哈特曼编码等都需要查找最小值。若顺序查找最值需要 O(n) 时间,而使用优先级队列查找需要 O(1) 时间,则入队和出队需要 O(logn) 时间。
在树形结构中有两种比较特殊的二叉树:满二叉树和完全二叉树。
二 满二叉树
指一棵深度为 k 且有二的 k 次方减 1 个节点的二叉树,满二叉树的每一层都充满节点,达到最大节点数。
三 完全二叉树
除了最后一层,每一层都是满的,达到最大节点数,最后一层节点从左向右出现的。深度为 k 的完全二叉树,其每个节点都与深度为 k 的满二叉树中的节点一一对应,完全二叉树和上图的满二叉树节点一一对应。完全二叉树除了最后一层,前面每一层都是满的,最后一层必须从左向右排列。也就是说,若2 没有左子节点,则 2 不可以有右子节点,若 2 没有右子节点,则 3 不可以有左子节点。
四 性质
若对完全二叉树从上至下,从左至右编号,则编号为 i 的节点,其左子节点编号必为 2i,其右子节点编号必为 2i+1,其双亲编号必为 i/2。
例如,有一棵完全二叉树 2 号节点的双亲节点为 1,左子节点为4,右子节点为 5; 3 号节点的双亲节点为 1,左子节点为 6,右子节点为 7。
五 最大堆和最小堆
若每个节点的值都大于或等于左右子节点的值,则称之为最大堆。若每个节点的值都小于或等于左右子节点的值,则称为最小堆。可以将堆看作一棵完全二叉树的顺序存储结构。一个数据元素序列及其对应的完全二叉树如下图所示,该完全二叉树满足最大堆的定义。
普通队列是先进先出的,优先队列与普通队列不同,每次出队都按照优先级顺序出队。优先队列是通过堆实现的,优先队列中的元素存储满足堆的定义,上图中的每一个节点的值都是大于或等于左右子节点的值,满足最大堆的定义,是最大值优先的最大堆。
优先队列有出队和入队两种基本操作。
1 出队
出队时,堆顶出队,最后一个元素代替堆顶的位置,重新调整为堆。
2 图解
一个最大堆如下图所示。出堆时,堆顶 30 出队,最后一个元素 12 代替堆顶。
出队后,除了堆顶,其他节点都满足最大堆的定义,只需堆顶执行下沉操作,即可调整为堆。下沉指堆顶与左右子节点比较,若比子节点大,则已调整为堆,若比子节点小,则与较大的子节点交换,交换到新的位置后继续向下比较,从根节点一直比较到叶子。
堆顶下沉的过程如下。
a 堆顶 12 和两个子节点 28、20 比较,比子节点小,与较大的子节点 28 交换。
b 12 再和两个子节点 16、18 比较,比子节点小,与较大的子节点 18 交换。
c 比较到叶子时停止,已调整为堆。
调整堆的过程,就是从堆顶从根下沉到叶子的过程。
3 入队
入队时,将新元素放入最后一个元素之后,重新调整为堆。
4 图解
例如 29入队,首先将 29 放入最后一个元素 12 的后面。
入队后,除了新入队的元素,其他节点都满足最大堆的定义,只需新元素执行上浮操作,即可调整为堆。上浮指新元素,与其父节点比较。若小于或等于父节点,则已调整为堆,若比父节点大,则与父节点交换,交换到新的位置后继续向上比较,从叶子一直比较到根。
新元素上浮的过程如下。
a 新元素 29 与其父节点 18 比较,比父节点大,与父节点交换。
b 29 再和父节点 28 比较,比父节点大,与父节点交换
c 29 再和父节点 30 比较,比父节点小,已调整为堆。
六 代码
package com.platform.modules.alg.alglib.p21;
public class P21 {
public String output = "";
private int maxN = 1000;
int r[] = new int[maxN];
public static void swap(int[] arr, int num1, int num2) {
int temp = arr[num1];
arr[num1] = arr[num2];
arr[num2] = temp;
}
void Sink(int k, int n) // 下沉操作
{
while (2 * k <= n) // 如果有左孩子
{
int j = 2 * k; // j 指向左孩子
if (j < n && r[j] < r[j + 1]) // 如果有右孩子且左孩子比右孩子小
j++; // j 指向右孩子
if (r[k] >= r[j]) // 比较大的孩子大
break; // 已满足堆
else
swap(r, k, j); // 与较大的孩子交换
k = j; // k 指向交换后的新位置,继续向下比较,一直下沉到叶子
}
}
void Swim(int k) // 上浮操作
{
while (k > 1 && r[k] > r[k / 2]) // 如果大于双亲
{
swap(r, k, k / 2); // 与双亲交换
k = k / 2; // k 指向交换后的新位置,继续向上比较,一直上浮到根
}
}
void CreatHeap(int n) // 构建初始堆
{
for (int i = n / 2; i > 0; i--) // 从最后一个分支结点 n/2 开始下沉调整为堆,直到第一个结点
Sink(i, n);
}
void push(int n, int x) // 入队
{
r[++n] = x; // n 加 1 后,将新元素放入尾部
Swim(n); // 最后一个元素上浮操作
}
void pop(int n) // 出队
{
output += r[1] + "\n"; // 输出堆顶
r[1] = r[n--]; // 最后一个元素代替堆顶,n减1
Sink(1, n); // 堆顶下沉操作
}
public String cal(String input) {
String[] line = input.split("\n");
String[] words = line[0].split(" ");
for (int i = 0; i < words.length; i++)
r[i + 1] = Integer.parseInt(words[i]);
int n = words.length;
CreatHeap(n); // 创建初始堆
int N = Integer.parseInt(line[1]);
int x;
for (int i = 0; i < N; i++) {
String[] command = line[i + 2].split(" ");
int commandNum = Integer.parseInt(command[0]);
switch (commandNum) {
case 1:
x = Integer.parseInt(command[1]);
push(n, x); // 入队
n = n + 1;
break;
case 2:
pop(n); // 出队
n = n - 1;
break;
}
}
return output;
}
}