堆的相关概念
堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
举例说明:
小根堆可以看到堆顶元素是最小的,每颗子树的根节点都是该树所有元素中最小的。
大根堆和小根堆相反,堆顶是最大的。
堆只是保证了堆顶的原始是整个堆中最大的或最小的,并不能保证层序遍历得到的序列是有序的。
Java中的堆
java中提供了一个优先队列PriorityQueue
,他的底层实现就是堆,常见的操作有peek()
、offer()
、poll()
。
示例代码:
PriorityQueue<Integer> que1 = new PriorityQueue<>(); // 默认是小根堆
PriorityQueue<Integer> que2 = new PriorityQueue<>((a, b)->(b - a)); // 大根堆
que.offer(3); // 添加元素
que.offer(1);
que.peek(); // 获取堆顶 1
Integer num = que.poll(); // 返回堆顶 1
offer方法
java的优先队列底层也是数组,每次调用offer()方法插入一个元素之后,要对堆进行重新调整,关键函数是siftUp()
:
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
如果没有指定comparator,会继续调用siftUpComparable(k, x)
:
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
可以看到添加一个元素实际上是向上调整堆的一个过程。
poll方法
poll方法是从堆顶移除一个元素,所以移除之后也要调整,堆是基于数组的,和末尾元素交换就行了,之后再向下调整。
向下调整:
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
手写堆需要实现哪些方法
已知堆支持操作:
- 获取堆顶元素
- 向堆中添加元素
- 删除堆顶元素
所以只实现down和up的逻辑就能支持上述操作。且可以支持任意位置插入和删除。
下面是实例代码:
int[] nums = new int[]{4,2,1,5,3}; // 长度为5的序列
int[] arr = new int[10]; // 堆数组,预留的大一点,方便演示add
int size = 0;
// 初始化
for (int i = 1; i <= 5; i++) {
arr[i] = nums[i - 1];
size++;
}
// 建堆 从第一个非叶子节点开始down
for (int i = 5 / 2; i >= 1; i--) {
down(arr, i, 5);
}
建堆的时候只用到了down,并且从第一个非叶子节点开始down。
以下是down和up函数:
public static void down(int[] arr, int u, int size) {
int t = u; // 标记最小的
if (u * 2 <= size && arr[u * 2] < arr[t]) t = u * 2;
if (u * 2 + 1 <= size && arr[u * 2 + 1] < arr[t]) t = u * 2 + 1;
if (t != u) {
// 交换
int temp = arr[t];
arr[t] = arr[u];
arr[u] = temp;
down(arr, t, size);
}
}
public static void up(int[] arr, int u) {
while (u / 2 != 0 && arr[u / 2] > arr[u]) {
int parent = u / 2;
int temp = arr[parent];
arr[parent] = arr[u];
arr[u] = temp;
u /= 2;
}
}
堆中插入元素之后,在对应的位置需要up。
堆中删除元素之后需要down。
任意位置修改后需要down和up一遍。
堆排序O(nlogn):
while (size > 0) {
int top = arr[1];
arr[1] = arr[size--];
down(arr, 1, size);
System.out.print(top + " ");
}
手写堆不支持动态扩容,所以适合知道元素最大长度的场景。