堆与堆排序
什么是堆?
堆的逻辑结构是一棵平衡二叉树,但是在使用过程中使用数组来进行存储。
我们还可以将堆分成两种类型,大根堆
和小根堆
。
大根堆是希望大的元素处于根部的位置,树中每层结点的值随着层数的增加而减小,而小根堆则相反。
堆的存储
使用一维数组来存储堆,根结点与左右孩子的下标关系:根结点 x ,左孩子 2x,右孩子 2x+1
对堆的两个基本的操作:
down(向下调整)
和up(向上调整)
down:
void down(int x) {
int t = x;
if(2 * x <= size && heap[2 * x] < heap[t]) t = 2 * x;
if(2 * x + 1 <= size && heap[2 * x + 1] < heap[t]) t = 2 * x + 1;
if(t != x){
int temp = heap[t];
heap[t] = heap[x];
heap[x] = temp;
//递归处理
down(t);
}
}
up:
void up(int x ) {
//跟它的父节点比较
while(x / 2 != 0 && heap[x / 2] > heap[x]){
//交换heap[x] heap[x / 2]
swap(heap[x],heap[x / 2]);
//继续向上
x / = 2;
}
}
对堆的操作
heap[N] 表示用来存储堆的数组,size表示堆中所用元素的总和。
- 插入一个数
heap[++size] = x; up(size);
- 求集合当中的最小值
heap[1]
- 删除最小值
heap[1] = heap[size]; size--; down(1);
- 删除任意一个元素
heap[k] = heap[size]; size--; down(k); up(k);
- 修改任意一个元素
heap[k] = x; down(k); up(k);
堆排序
例题:
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
思路:
题目中希望我们将前m个小的数输出出来,那么就可以将堆中元素进行排序,也就是将堆变为小根堆,每次都输出根位置上的数,然后再删除根位置上的数同时再进行down操作。
在使用堆之前需要对堆进行初始化操作:从 n/2的位置开始,也就是二叉树的倒数第二层的位置开始不断的向上进行down操作,这是因为进行down操作时要保证左右儿子都已经是一个堆结构,插入元素时是随机插入的,所以不能从根结点开始down,而n/2处的节点每个都是叶子结点,满足堆结构,所以从n/2的位置开始down
for (int i = n / 2; i != 0; i --) down(i);
题解:
import java.util.Scanner;
public class Main {
static final int N = 100010;
static int[] h = new int[N];
static int size;
private static void down(int u) {
int t = u;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (t != u) {
int tmp = h[u];
h[u] = h[t];
h[t] = tmp;
down(t);
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), m = sc.nextInt();
for (int i = 1; i <= n; i++)
h[i] = sc.nextInt();
size = n;
// 构建堆
for (int i = n / 2; i != 0; i --) down(i);
while (m -- != 0) {
System.out.print(h[1] + " ");
h[1] = h[size--];
down(1);
}
}
}
模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
I x
,插入一个数 x x x;PM
,输出当前集合中的最小值;DM
,删除当前集合中的最小值(数据保证此时的最小值唯一);D k
,删除第 k k k 个插入的数;C k x
,修改第 k k k 个插入的数,将其变为 x x x;
现在要进行 N N N 次操作,对于所有第 2 2 2 个操作,输出当前集合的最小值。
输入格式
第一行包含整数 N N N。
接下来
N
N
N 行,每行包含一个操作指令,操作指令为 I x
,PM
,DM
,D k
或 C k x
中的一种。
输出格式
对于每个输出指令 PM
,输出一个结果,表示当前集合中的最小值。
每个结果占一行。
数据范围
1
≤
N
≤
105
1≤N≤105
1≤N≤105
−
109
≤
x
≤
109
−109≤x≤109
−109≤x≤109
数据保证合法。
输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
代码实现:
import java.util.Scanner;
public class Main {
static final int N = 100010;
static int[] h = new int[N];
static int[] ph = new int[N]; //存放第k个插入点的下标
static int[] hp = new int[N]; //存放堆中点的插入次序
static int size;
public static void down(int x) {
int t = x;
if (x * 2 <= size && h[t] > h[x * 2]) t = x * 2;
if (x * 2 + 1 <= size && h[t] > h[x * 2 + 1]) t = x * 2 + 1;
if (t != x) {
heapSwap(t, x);
down(t);
}
}
public static void up(int x) {
while (x / 2 > 0 && h[x] < h[x / 2]) {
heapSwap(x , x / 2);
x /= 2;
}
}
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;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = 0;
while (n-- > 0) {
String s = sc.next();
if ("I".equals(s)) {
int x = sc.nextInt();
h[++size] = x;
ph[++m] = size;
hp[size] = m;
up(size);
} else if ("PM".equals(s)) {
System.out.println(h[1]);
} else if ("DM".equals(s)) {
heapSwap(1, size);
size--;
down(1);
} else if ("D".equals(s)) {
int k = sc.nextInt();
int u = ph[k];
heapSwap(u, size--);
up(u);
down(u);
} else {
int k = sc.nextInt(), x = sc.nextInt();
h[ph[k]] = x;
down(ph[k]);
up(ph[k]);
}
}
}
}