Java实现最小堆二
如何建立这个堆呢,可以从空的堆开始,然后依次往堆中插入每一个元素,直到所有数都被插入。
因为插入第N个元素的所用的时间是O(logN),所以插入所有元素的整体时间复杂度是O(NlogN),代码如下。
n=0;
for(i=1;i<=m;i++)
{
n++;
h[n]=a[i]; //或者写成scanf("%d",&h[n]);
siftup();
}
其实我们还有更快得方法来建立堆。它是这样的,直接把99、5、36、7、22、17、46、12、2、19、25、28、1和92这14个数放入一个完全二叉树中(这里我们还是用一个一维数组来存储完全二叉树)。
在这个棵完全二叉树中,我们从最后一个结点开始依次判断以这个结点为根的子树是否符合最小堆的特性。如果所有的子树都符合最小堆的特性,那么整棵树就是最小堆了。如果这句话没有理解不要着急,继续往下看。
首先我们从叶结点开始。因为叶结点没有儿子,所以所有以叶结点为根结点的子树(其实这个子树只有一个结点)都符合最小堆的特性(即父结点的值比子结点的值小)。这些叶结点压根就没有子节点,当然符合这个特性。因此所有叶结点都不需要处理,直接跳过。从第n/2个结点(n为完全二叉树的结点总数,这里即7号结点)开始处理这棵完全二叉树。注意完全二叉树有一个性质:最后一个非叶结点是第n/2个结点。
以7号结点为根的子树不符合最小堆的特性,因此要向下调整。
同理以6号、5号和4结点为根的子树也不符合最小对的特性,都需要往下调整。
下面是已经对7号、6号、5号和4结点为根结点的子树调整完毕之后的状态。
当然目前这棵树仍然不符合最小堆的特性,我们需要继续调整以3号结点为根的子树,即将3号结点向下调整。
同理继续调整以2号结点为根的子树,最后调整以1号结点为根的子树。调整完毕之后,整棵树就符合最小堆的特性啦。
小结一下这个创建堆的算法。把n个元素建立一个堆,首先我可以将这n个结点以自顶向下、从左到右的方式从1到n编码。这样就可以把这n个结点转换成为一棵完全二叉树。紧接着从最后一个非叶结点(结点编号为n/2)开始到根结点(结点编号为1),逐个扫描所有的结点,根据需要将当前结点向下调整,直到以当前结点为根结点的子树符合堆的特性。虽然讲起来起来很复杂,但是实现起来却很简单,只有两行代码如下:
for(i=n/2;i>=1;i--)
siftdown(i);
用这种方法来建立一个堆的时间复杂度是O(N),如果你感兴趣可以尝试自己证明一下,
堆还有一个作用就是堆排序,与快速排序一样堆排序的时间复杂度也是O(NlogN)。堆排序的实现很简单,比如我们现在要进行从小到大排序,可以先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。最终输出的或者存放在新数组中数就已经是排序好的了。
完整代码如下
package com.usoft.heap;
import java.util.Random;
/**
* Created by xinxingegeya on 16/6/30.
*/
public class MinHeap<T extends Comparable<T>> {
/**
* 最小堆内部使用数组存储
*/
private Node<T>[] h;
private int maximumCapacity; //数组初始化大小
private int capacity; //堆的大小
private int index;
/**
* 最小堆的大小
*
* @param maxCapacity
*/
public MinHeap(int capacity, int maxCapacity) {
this.capacity = capacity;
this.maximumCapacity = maxCapacity;
h = (Node<T>[]) new Node[maxCapacity];
}
/**
* 使用Node封装最小堆的元素节点
*
* @param <T>
*/
static class Node<T extends Comparable<T>> implements Comparable<Node> {
private T data; //节点数据
public Node(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public int compareTo(Node o) {
return this.data.compareTo((T) o.getData());
}
}
void swap(int x, int y) {
Node<T> t;
t = h[x];
h[x] = h[y];
h[y] = t;
}
//向下调整节点
void siftDown(int i) {//传入一个需要向下调整的结点编号i,这里传入1,即从堆的顶点开始向下调整
int t, flag = 0;//flag用来标记是否需要继续向下调整
//当i结点有儿子的时候(其实是至少有左儿子的情况下)并且有需要继续调整的时候循环窒执行
while (i * 2 <= capacity && flag == 0) {
//首先判断他和他左儿子的关系,并用t记录值较大的结点编号
if (h[i].compareTo(h[i * 2]) > 0) {
t = i * 2;
} else {
t = i;
}
//如果他有右儿子的情况下,再对右儿子进行讨论
if (i * 2 + 1 <= capacity) {
//如果右儿子的值更大,更新较小的结点编号
if (h[t].compareTo(h[i * 2 + 1]) > 0) {
t = i * 2 + 1;
}
}
//如果发现最大的结点编号不是自己,说明子结点中有比父结点更大的
if (t != i) {
swap(t, i);//交换它们,注意swap函数需要自己来写
i = t;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整
} else {
flag = 1;//则否说明当前的父结点已经比两个子结点都要大了,不需要在进行调整了
}
}
}
//向上调整节点
void siftUp(int i) { //传入一个需要向上调整的结点编号i
int flag = 0; //用来标记是否需要继续向上调整
if (i == 1) return; //如果是堆顶,就返回,不需要调整了
//不在堆顶 并且 当前结点i的值比父结点小的时候继续向上调整
while (i != 1 && flag == 0) {
//判断是否比父结点的小
if (h[i].compareTo(h[i / 2]) < 0) {
swap(i, i / 2);//交换他和他爸爸的位置
} else {
flag = 1;//表示已经不需要调整了,当前结点的值比父结点的值要大
}
i = i / 2; //这句话很重要,更新编号i为它父结点的编号,从而便于下一次继续向上调整
}
}
/**
* 把数组初始化成最小堆
*/
void creatBySiftDown() {
int i;
//从最后一个非叶结点到第1个结点依次进行向上调整
for (i = capacity / 2; i >= 1; i--) {
siftDown(i);
}
}
/**
* 插入元素,并sift up
*
* @param data
*/
void insert(T data) {
if (capacity < maximumCapacity) {
Node<T> node = new Node<>(data);
h[++index] = node;
siftUp(index);
} else {
throw new RuntimeException("heap is full");
}
}
/**
* 使用最小堆的性质进行堆排序
*
* @return
*/
Node deleteMin() {
Node node;
node = h[1];
h[1] = h[capacity];
capacity--;
siftDown(1);
return node;
}
public static void main(String args[]) {
int maxCapacity = 100;
int capacity = 9;
MinHeap<Item> minHeap = new MinHeap<>(capacity, maxCapacity);
for (int i = 0; i < capacity; i++) {
Random random = new Random();
int r = random.nextInt();
Item item = new Item(Math.abs(r) % 1000);
minHeap.insert(item);
}
for (int i = 1; i <= capacity; i++) {
System.out.print(minHeap.h[i].getData().getId() + ",");
}
System.out.println();
for (int i = 1; i <= capacity; i++) {
System.out.println(((Item) minHeap.deleteMin().getData()).getId());
}
}
}
========END========