堆的性质
堆是一种特殊的树。
只要满足以下两点,它就是一个堆:
- 堆是一个完全二叉树。
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
第一点,堆必须是一个完全二叉树。完全二叉树要求,除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列,自然堆也具有完全二叉树的所有性质。
第二点,堆中的每个节点的值必须大于等于(或者小于等于)其子树中每个节点的值。实际上,我们还可以换一种说法,堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。
对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”。
以下讲解都用小根堆为例。
堆的相关参数定义
static int[] h; //存放堆中的数据
static int[] ph; //存放第k个插入点的下标
static int[] hp; //存放堆中点的插入次序
static int size; //存放堆中数据个数
堆虽然是一种树,但在堆的存储中,通常使用数组存储。这是因为数组在从下标1开始存储值的时候,假设树根root为n,那么它的左子树为2n,右子树为2n+1。
堆的平衡
堆是个树状存储结构,在你对堆中的数据做出修改时,可能会破坏平衡,所以需要对堆做出操作让其重新平衡。
下沉
//顾名思义,down()就是把当前节点在树中从上往下沉
public static void down(int u) {
//比较当前节点和其左右节点,找出最小的节点与当前节点交换
int t = u;
if (u * 2 <= size && h[t] > h[u * 2]) t = u * 2;
if (u * 2 + 1 <= size && h[t] > h[u * 2 + 1]) t = u * 2 + 1;
//如果当前节点已经是最小的,说明当前节点已经在合适的位置
if (u != t) {
heapSwap(u, t);
down(t);
}
}
上浮
//将当前节点往上浮
public static void up(int u) {
//比较当前节点和其父节点的大小,并交换
if (u / 2 > 0 && h[u] < h[u / 2]) {
heapSwap(u, u / 2);
up(u / 2);
}
}
而堆中最核心的操作也是下沉和上浮,基本上有了这两个方法,所有操作都没什么问题了。
其他方法
在这里因为需要维护堆中插入数据的顺序,所以这里需要一个额外的swap
/**
* 此方法保证了可以找到第k个插入的数
* 之所以要进行这样的操作是因为 经过一系列操作 堆中的元素并不会保持原有的插入顺序
* 从而我们需要对应到原先第K个堆中元素
* 如果理解这个原理 那么就能明白其实三步交换的顺序是可以互换
* h,hp,ph之间两两存在映射关系 所以交换顺序的不同对结果并不会产生影响
*
* @param u
* @param v
*/
public static void heapSwap(int u, int v) {
swap(h, u, v);
swap(hp, u, v);
swap(ph, hp[u], hp[v]);
}
public static void swap(int[] a, int u, int v) {
int tmp = a[u];
a[u] = a[v];
a[v] = tmp;
}
堆的插入
//在堆中插入元素x
int x = sc.nextInt();
m++;
h[++size] = x;
ph[m] = size;
hp[size] = m;
//插入操作默认是插入到最后面的节点,所以只需要up一次就可以达到平衡
//down(size);
up(size);
堆的删除
//删除最小值,不能直接删除堆顶,需要将堆底的元素与堆顶交换,然后删除堆底(也就是最小值),因为是堆顶,所以只需要down,恢复平衡
heapSwap(1, size);
size--;
down(1);
注:如果想要删除任意节点,也需要把节点k与堆底节点交换,然后删除再平衡
堆的修改
//修改第k个插入的数为x
int k = sc.nextInt(), x = sc.nextInt();
h[ph[k]]=x; //此处由于未涉及heapSwap操作且下面的up、down操作只会发生一个
down(ph[k]); //所以可直接传入ph[k]作为参数
up(ph[k]);
完整代码
package Hello.Acwing;
import java.util.Scanner;
public class Heap {
static int[] h; //存放堆中的数据
static int[] ph; //存放第k个插入点的下标
static int[] hp; //存放堆中点的插入次序
static int size; //存放堆中数据个数
//堆是个树状存储结构,在你对堆中的数据做出修改时,可能会破坏平衡,所以需要down()和up()
//顾名思义,down()就是把当前节点在树中从上往下沉
public static void down(int u) {
//比较当前节点和其左右节点,找出最小的节点与当前节点交换
int t = u;
if (u * 2 <= size && h[t] > h[u * 2]) t = u * 2;
if (u * 2 + 1 <= size && h[t] > h[u * 2 + 1]) t = u * 2 + 1;
//如果当前节点已经是最小的,说明当前节点已经在合适的位置
if (u != t) {
heapSwap(u, t);
down(t);
}
}
//将当前节点往上浮
public static void up(int u) {
//比较当前节点和其父节点的大小,并交换
if (u / 2 > 0 && h[u] < h[u / 2]) {
heapSwap(u, u / 2);
up(u / 2);
}
}
/**
* 此方法保证了可以找到第k个插入的数
* 之所以要进行这样的操作是因为 经过一系列操作 堆中的元素并不会保持原有的插入顺序
* 从而我们需要对应到原先第K个堆中元素
* 如果理解这个原理 那么就能明白其实三步交换的顺序是可以互换
* h,hp,ph之间两两存在映射关系 所以交换顺序的不同对结果并不会产生影响
*
* @param u
* @param v
*/
public static void heapSwap(int u, int v) {
swap(h, u, v);
swap(hp, u, v);
swap(ph, hp[u], hp[v]);
}
public static void swap(int[] a, int u, int v) {
int tmp = a[u];
a[u] = a[v];
a[v] = tmp;
}
// I x,插入一个数 x;
// PM,输出当前集合中的最小值;
// DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
// D k,删除第 k个插入的数;
// C k x,修改第 k个插入的数,将其变为 x;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
//操作的次数
int n = sc.nextInt();
//初始化
h = new int[n + 1];
ph = new int[n + 1];
hp = new int[n + 1];
size = 0;
//m用来记录插入的数的个数
int m = 0;
for (int i = 0; i < n; i++) {
String s = sc.next();
if (s.equals("I")) {
//插入
int x = sc.nextInt();
m++;
h[++size] = x;
ph[m] = size;
hp[size] = m;
//插入操作默认是插入到最后面的节点,所以只需要up一次就可以达到平衡
//down(size);
up(size);
} else if (s.equals("PM")) {
//输出最小值
//小根堆,堆顶就是最小的
System.out.println(h[1]);
} else if (s.equals("DM")) {
//删除最小值,不能直接删除堆顶,需要将堆底的元素与堆顶交换,然后删除堆底(也就是最小值),因为是堆顶,所以只需要down,恢复平衡
heapSwap(1, size);
size--;
down(1);
} else if (s.equals("D")) {
//删除第k个插入的数
int k = sc.nextInt();
int u=ph[k]; //这里一定要用u=ph[k]保存第k个插入点的下标
heapSwap(u,size); //因为在此处heapSwap操作后ph[k]的值已经发生
size--; //如果在up,down操作中仍然使用ph[k]作为参数就会发生错误
//鉴于堆的性质,up()和down()只会有一个执行
up(u);
down(u);
} else if (s.equals("C")) {
//修改第k个插入的数为x
int k = sc.nextInt(), x = sc.nextInt();
h[ph[k]]=x; //此处由于未涉及heapSwap操作且下面的up、down操作只会发生一个
down(ph[k]); //所以可直接传入ph[k]作为参数
up(ph[k]);
}
}
}
}
堆的建立
for (int i = n / 2; i; i--){
down(i);
}
// 时间复杂度为O(n);
那么堆为什么从n/2开始down呢?