1 堆
- 设计一种数据结构,用来存放整数,要求提供 3 个接口
- 添加元素
- 获取最大值
- 删除最大值

- 堆中获取最大值:O(1)、删除最大值:O(logn)、添加元素:O(logn)
- 堆的性质
- 是一种树状的数据结构(不要跟内存模型中的“堆空间”混淆)
- 任意节点的值总是 ≥( ≤ )子节点的值
- 如果任意节点的值总是 ≥ 子节点的值,称为:最大堆、大根堆、大顶堆
- 如果任意节点的值总是 ≤ 子节点的值,称为:最小堆、小根堆、小顶堆
- 堆中的元素必须具备可比较性

2 二叉堆(Binary Heap)
- 二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆
- 鉴于完全二叉树的一些特性,二叉堆的底层(物理结构)一般用数组实现即可

- 获取最大值:直接获取数组索引0的元素
- 添加
- 将元素添加到数组最后一个位置
- 对该元素进行上滤(sift up)
- 循环执行如果node>父节点,与父节点换位
- 如果node<=父节点,退出循环

- 上滤的优化:不交换位置,只让父节点依次覆盖上溢的节点,最后停留的位置由上溢节点覆盖

- 上滤时间复杂度为O(logn)
- 删除
- 使用数组中最后一个元素覆盖第一个元素,删除最后一个元素
- 将新的第一个元素进行下滤
- 循环,如果 node < 最大的子节点,与最大的子节点交换位置
- 如果 node ≥ 最大的子节点, 或者 node 没有子节点,退出循环
- 下滤时间复杂度也是O(logn)

- replace:使用新元素替换堆顶元素
- 使用新传入的元素替换数组中第一个元素
- 对新的第一个元素进行下滤
- 批量建堆
- 自上而下的上滤:从非根节点开始,从上到下,从左到右,对所有节点进行上滤,逻辑其实与添加逻辑相同,因此可以保证结果有堆的性质

- 自下而上的下滤:从第一个非叶子节点开始,从右至左,从下到上,对所有节点进行下滤,和删除的逻辑类似,因此也能保证最后结果有堆的性质

- 批量建堆效率对比
- 对于自上而下的上滤,底层大量的节点,需要执行的上滤次数反而多,而对于自下而上的下滤,对于底层大量的节点,需要执行下滤的次数少
- 对于自上而下:时间复杂度就是所有节点的深度之和,复杂度为O(nlogn)
- 仅仅是叶子节点,就有近 n/2 个,而且每一个叶子节点的深度都是 O(logn) 级别的
- 因此,在叶子节点这一块,就达到了 O(nlogn) 级别
- O(nlogn) 的时间复杂度足以利用排序算法对所有节点进行全排序
- 对于自下而上:时间复杂度为所有节点的高度之和,复杂度为O(n)
- 因此批量建堆时,不要直接调用堆的add方法
- 构建小顶堆:只需修改Comparator中compare逻辑,或者Comparable中compareTo逻辑
- Heap
package com.mj.heap;
public interface Heap<E> {
int size();
boolean isEmpty();
void clear();
void add(E element);
E get();
E remove();
E replace(E element);
}
- AbstractHeap
package com.mj.heap;
import java.util.Comparator;
@SuppressWarnings("unchecked")
public abstract class AbstractHeap<E> implements Heap<E> {
protected int size;
protected Comparator<E> comparator;
public AbstractHeap(Comparator<E> comparator) {
this.comparator = comparator;
}
public AbstractHeap() {
this(null);
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
protected int compare(E e1, E e2) {
return comparator != null ? comparator.compare(e1, e2)
: ((Comparable<E>)e1).compareTo(e2);
}
}
- BinaryHeap
package com.mj.heap;
import java.util.Comparator;
import com.mj.printer.BinaryTreeInfo;
@SuppressWarnings("unchecked")
public class BinaryHeap<E> extends AbstractHeap<E> implements BinaryTreeInfo {
private E[] elements;
private static final int DEFAULT_CAPACITY = 10;
public BinaryHeap(E[] elements, Comparator<E> comparator) {
super(comparator);
if (elements == null || elements.length == 0) {
this.elements = (E[]) new Object[DEFAULT_CAPACITY];
} else {
size = elements.length;
int capacity = Math.max(elements.length, DEFAULT_CAPACITY);
this.elements = (E[]) new Object[capacity];
for (int i = 0; i < elements.length; i++) {
this.elements[i] = elements[i];
}
heapify();
}
}
public BinaryHeap(E[] elements) {
this(elements, null);
}
public BinaryHeap(Comparator<E> comparator) {
this(null, comparator);
}
public BinaryHeap() {
this(null, null);
}
@Override
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
@Override
public void add(E element) {
elementNotNullCheck(element);
ensureCapacity(size + 1);
elements[size++] = element;
siftUp(size - 1);
}
@Override
public E get() {
emptyCheck();
return elements[0];
}
@Override
public E remove() {
emptyCheck();
int lastIndex = --size;
E root = elements[0];
elements[0] = elements[lastIndex];
elements[lastIndex] = null;
siftDown(0);
return root;
}
@Override
public E replace(E element) {
elementNotNullCheck(element);
E root = null;
if (size == 0) {
elements[0] = element;
size++;
} else {
root = elements[0];
elements[0] = element;
siftDown(0);
}
return root;
}
private void heapify() {
for (int i = (size >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
}
private void siftDown(int index) {
E element = elements[index];
int half = size >> 1;
while (index < half) {
int childIndex = (index << 1) + 1;
E child = elements[childIndex];
int rightIndex = childIndex + 1;
if (rightIndex < size && compare(elements[rightIndex], child) > 0) {
child = elements[childIndex = rightIndex];
}
if (compare(element, child) >= 0) break;
elements[index] = child;
index = childIndex;
}
elements[index] = element;
}
private void siftUp(int index) {
E element = elements[index];
while (index > 0) {
int parentIndex = (index - 1) >> 1;
E parent = elements[parentIndex];
if (compare(element, parent) <= 0) break;
elements[index] = parent;
index = parentIndex;
}
elements[index] = element;
}
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
}
private void emptyCheck() {
if (size == 0) {
throw new IndexOutOfBoundsException("Heap is empty");
}
}
private void elementNotNullCheck(E element) {
if (element == null) {
throw new IllegalArgumentException("element must not be null");
}
}
@Override
public Object root() {
return 0;
}
@Override
public Object left(Object node) {
int index = ((int)node << 1) + 1;
return index >= size ? null : index;
}
@Override
public Object right(Object node) {
int index = ((int)node << 1) + 2;
return index >= size ? null : index;
}
@Override
public Object string(Object node) {
return elements[(int)node];
}
}
3 Top K问题
- 从 n 个整数中,找出最大的前 k 个数( k 远远小于 n )
- 如果使用排序算法进行全排序,需要 O(nlogn) 的时间复杂度
- 如果使用二叉堆来解决,可以使用 O(nlogk) 的时间复杂度来解决
- 解决方案
- 新建一个小顶堆
- 扫描 n 个整数
- 先将遍历到的前 k 个数放入堆中
- 从第 k + 1 个数开始,如果大于堆顶元素,就使用 replace 操作(删除堆顶元素,将第 k + 1 个数添加到堆中)
- 扫描完毕后,堆中剩下的就是最大的前 k 个数
package com.mj;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
import com.mj.heap.BinaryHeap;
import com.mj.printer.BinaryTrees;
public class Main {
static void test4() {
BinaryHeap<Integer> heap = new BinaryHeap<>(new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
int k = 3;
Integer[] data = { 51, 30, 39, 92, 74, 25, 16, 93, 91, 19, 54, 47, 73, 62, 76, 63, 35, 18, 90, 6, 65, 49, 3, 26,
61, 21, 48 };
for (int i = 0; i < data.length; i++) {
if (heap.size() < k) {
heap.add(data[i]);
} else if (data[i] > heap.get()) {
heap.replace(data[i]);
}
}
BinaryTrees.println(heap);
}
public static void main(String[] args) {
test4();
test5();
}
}
4 优先级队列(Priority Queue)
- 按添加元素的从小到大顺序出队
- 底层使用堆来实现,PriorityQueue就是Java中的最小堆
public int compareTo(Person o){
return boneBreak - o.boneBreak;
}
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}