一、关于最大值数据结构的比较
获取最大值的平均时间复杂度 | 删除最大值的平均时间复杂度 | 尾部添加元素的平均时间复杂度 | |
---|---|---|---|
动态数组 | O(N) | O(N) | O(1) |
双向链表 | O(N) | O(N) | O(1) |
平衡的二叉搜索树 | O(log N) | O(log N) | O(log N) |
最大堆 | O(1) | O(log N) | O(log N) |
二、堆
1、堆的一些性质
任意结点的值都大于等于其子结点的值,叫做最大堆。
任意结点的值都小于等于其子结点的值,叫做最小堆。
最大堆和最小堆如下图所示:
堆中元素必须具有可比较性。而且所有元素并不是全排列的!
2、堆(最大堆)中元素的添加
上滤:比较自己的值和父结点上值的大小关系。如果自己比父结点大,就交换两者的位置;如果比父节点小则不做处理。
直接加在数组的末尾即可,然后从最后一个结点开始往上挨个进行上滤。
3、堆(最大堆)中元素的删除
下滤:比较自己的值和左右子结点上最大值的大小关系。如果自己比子结点上最大值小,就交换两者的位置;如果比子结点上最大值大则不做处理。
将数组的最后个元素替换堆顶元素,将数组最后个元素删除,然后从第一个结点开始挨个往下进行下滤。
4、批量建堆
自上而下的上滤法本质:相当于是往堆中添加元素的情况。添加之前这个堆已经是最大堆了,所以每添加一个元素之后都要用上滤法维护一下。
自下而上的下滤法本质:相当于是往堆中删除元素的情况。“删除”即添加之前这个结点的两边已经是最大堆了,所以每“删除”即添加一个元素都要用下滤法维护一下。
自下而上的下滤法效率比自上而下的上滤法高。
三、堆的实现
1、设计结构图
2、代码
接口代码:
public interface Heap<E> {
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void add(E element); // 添加元素
E get(); // 获得堆顶元素
E remove(); // 删除堆顶元素
E replace(E element); // 删除堆顶元素的同时插入一个新元素
}
抽象类代码:
public abstract class AbstractHeap<E> implements Heap<E> {
protected int size = 0;
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) e1).compareTo(e2);
}
}
二叉堆代码:
public class BinaryHeap<E> extends AbstractHeap<E> {
private static final int DEFAULT_CAPACITY = 10;
private E[] elements;
public BinaryHeap(Comparator<E> comparator) {
super(comparator);
}
public BinaryHeap() {
super(null);
}
public BinaryHeap(E[] elements, Comparator<E> comparator) {
this.comparator = comparator;
if (elements == null || elements.length == 0) {
this.elements = (E[]) new Object[DEFAULT_CAPACITY];
} else {
this.size = elements.length;
int max = Math.max(elements.length, DEFAULT_CAPACITY);
this.elements = (E[]) new Object[max];
// this.elements = elements; //直接用地址指向外部数组不可行。一旦外部数组改变了, 堆也会受到影响
for (int i = 0; i < elements.length; i++) {
this.elements[i] = elements[i];
}
heapBatch();
}
}
public BinaryHeap(E[] elements) {
this(elements, null);
}
@Override
public void clear() {
size = 0;
for (int i = 0; i < size; i++)
elements[i] = null;
}
@Override
public void add(E element) {
if (element == null)
throw new IllegalArgumentException("传入的元素为空!");
ensureCapacity(size + 1);
elements[size++] = element;
//上滤
slipUp(size - 1);
}
@Override
public E get() {
if (size == 0)
throw new IndexOutOfBoundsException("堆为空!");
return elements[0];
}
@Override
public E remove() {
if (size == 0)
throw new IndexOutOfBoundsException("堆为空!");
E old = elements[0];
elements[0] = elements[--size];
slipDown(0);
elements[size] = null;
return old;
}
@Override
public E replace(E element) {
E old = null;
if (size != 0)
old = elements[0];
elements[0] = element;
slipDown(0);
return old;
}
/**
* 批量建堆
*/
private void heapBatch() {
//自上而下的上滤法
// for(int i=1;i<size;i++)
// slipUp(i);
//自下而上的下滤法
for (int i = (size >> 1) - 1; i >= 0; i--)
slipDown(i);
}
/**
* 动态扩容
*
* @param capacity
*/
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容为1.5倍
if (capacity > oldCapacity) {
//位运算比浮点高效
E[] newArray = (E[]) new Object[newCapacity];
//拷贝数据
for (int i = 0; i < size; i++) {
newArray[i] = elements[i];
}
elements = newArray;
}
}
/**
* 上滤法
*
* @param index
*/
private void slipUp(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;
}
/**
* 下滤法
*
* @param index
*/
private void slipDown(int index) {
E element = elements[index];
while (index < (size >> 1)) { //遍历到的都是非叶子结点
int leftChildIndex = (index << 1) + 1;
//左子树一定存在, 但是右子树不一定存在
int maxChildIndex = leftChildIndex + 1 < size
&& compare(elements[leftChildIndex], elements[leftChildIndex + 1]) < 0
? leftChildIndex + 1
: leftChildIndex;
E maxChild = elements[maxChildIndex];
if (compare(element, maxChild) >= 0)
break;
elements[index] = maxChild;
index = maxChildIndex;
}
elements[index] = element;
}
}
四、Top K问题
1、问题描述
从arr[1, n]这n个数中,找出最大的k个数,k远小于n。
2、思路
遍历数组,维护一个容量为k的最小堆,每次比较堆顶元素和数组中的元素大小关系。如果遍历到的元素比堆顶元素大,就用该元素替换堆顶元素;如果遍历到的元素比堆顶元素要小,不做任何处理。
随机生成了一百万个在10000000 - 99999999的随机数进行测试。
public static void main(String[] args) {
final int k = 3;
BinaryHeap<Integer> heap = new BinaryHeap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
long begin = System.currentTimeMillis();
Integer[] datas = new Integer[1000000];
for (int i = 0; i < 1000000; i++) {
datas[i] = 10000000 + (int) (Math.random() * 9000000);
if (heap.size() < k)
heap.add(datas[i]);
else if (datas[i] > heap.get())
heap.replace(datas[i]);
}
long end = System.currentTimeMillis();
double delta = (end - begin) / 1000.0;
BinaryTrees.println(heap);
System.out.println("最小堆耗时:" + delta + "秒");
System.out.println("--------------------");
begin = System.currentTimeMillis();
Arrays.sort(datas);
for (int i = datas.length - 1; i >= datas.length - k; i--)
System.out.println(datas[i]);
end = System.currentTimeMillis();
delta = (end - begin) / 1000.0;
System.out.println("快排耗时:" + delta + "秒");
}
用堆不知道比快排牛逼多少勒!