您的位置:首页 > 其它

浅谈堆-Heap(一)

2019-05-28 23:30 696 查看

应用场景和前置知识复习

  • 堆排序

排序我们都很熟悉,如冒泡排序、选择排序、希尔排序、归并排序、快速排序等,其实堆也可以用来排序,严格来说这里所说的堆是一种数据结构,排序知识它的应用场景之一

  • Top N的求解

  • 优先队列

堆得另一个重要的应用场景就是优先队列

我们知道普通队列是:先进先出

而 优先队列:出队顺序和入队顺序无关;和优先级相关

实际生活中有很多优先队列的场景,如医院看病,急诊病人是最优先的,虽然这一类病人可能比普通病人到的晚,但是他们可能随时有生命危险,需要及时进行治疗. 再比如 操作系统要"同时"执行多个任务,实际上现代操作系统都会将CPU的执行周期划分成非常小的时间片段,每个时间片段只能执行一个任务,究竟要执行哪个任务,是有每个任务的优先级决定的.每个任务都有一个优先级.操作系统动态的每一次选择一个优先级最高的任务执行.要让操作系统动态的选择优先级最高的任务去执行,就需要维护一个优先队列,也就是说所有任务都会进入这个优先队列.

 

基本实现

首先堆是一颗二叉树,这个二叉树必须满足两个两条件

  1. 这个二叉树必须是一颗完全二叉树,所谓完全二叉树就是除了最后一层外,其他层的节点的个数必须是最大值,且最后一层的节点都必须集中在左侧.即最后一层从左往右数节点必须是紧挨着的,不能是中间空出一个,右边还有兄弟节点.

  2. 这个二叉树必须满足 左右子树的节点值必须小于或等于自身的值(大顶堆) 或者 左右子树的节点值必须大于或等于自身的值(小顶堆)

下图分别是一个大顶堆和小顶堆的示例

public class MaxHeap {

/*
* 堆中有多少元素
*/
private int count;

/*
* 存放堆数据的数组
*/
private Object[] data;

public MaxHeap(int capacity) {
/*
* 因为序号是从1 开始的,我们不用下标是0的这个位置的数
*/
this.data = new Object[capacity + 1];
}

/**
* 返回堆中有多少数据
* @return
*/
public int size()  {
return count;
}

/**
* 堆是否还有元素
* @return
*/
public boolean isEmpty() {
return count == 0;
}

}
View Code [p]骨骼是构建好了,乍一看堆中存放的数据是一个object类型的数据, 父子节点按节点值 无法比较,这里再调整一下

public class MaxHeap<T extends Comparable<T>> {

/*
* 堆中有多少元素
*/
private int count;

/*
* 存放堆数据的数组
*/
private T[] data;

/**
* @param clazz 堆里放的元素的类型
* @param capacity  堆的容量
*/
public MaxHeap(Class<T> clazz, int capacity) {
/*
* 因为序号是从1 开始的,我们不用下标是0的这个位置的数
*/
this.data = (T[]) Array.newInstance(clazz, capacity + 1);
}

/**
* 返回堆中有多少数据
*
* @return
*/
public int size() {
return count;
}

/**
* 堆是否还有元素
*
* @return
*/
public boolean isEmpty() {
return count == 0;
}

public T[] getData() {
return data;
}
}

这样骨架算是相对完好了,下面实现向堆中添加数据的过程,首先我们先把上面的二叉树的形式按标号映射成数组的形式如图对比(已经说了0号下标暂时不用)

public static void main(String[] args) {
MaxHeap<Integer> mh = new MaxHeap<Integer>(Integer.class, 12);
mh.insert(66);
mh.insert(44);
mh.insert(30);
mh.insert(27);
mh.insert(17);
mh.insert(25);
mh.insert(13);
mh.insert(19);
mh.insert(11);
mh.insert(8);
mh.insert(45);
Integer[] data = mh.getData();
for (int i = 1 ; i <= mh.count ; i++ ) {
System.err.print(data[i] + " ");
}
mh.popMax();
for (int i = 1 ; i <= mh.count ; i++ ) {
System.err.print(data[i] + " ");
}
}
View Code 嗯,还不错,结果跟上面图上对应的数组一样.结果倒是期望的一样,但总感觉上面的shiftDown的代码比shiftUp的代码要多几倍,而且看着很多类似一样的重复的代码, 看着难受.于是乎想个办法优化一下. 对我这种强迫症来说,不干这件事,晚上老是睡不着觉.

思路: 上面我们不断的循环条件是这个index对应的节点有子节点.如果节点堆的性质破坏,最终是要用这个值与其左子节点或者右子节点的值交换,所以我们计算出了左子节点和右子节点的序号.其实不然,我们定义一个抽象的最终要和父节点交换的变量,这个变量可能是左子节点,也可能是右子节点,初始化成左子节点的序号,只有在其左子节点的值小于右子节点,且父节点的值也左子节点,父节点才可能与右子节点,这时让其这个交换的变量加1变成右子节点的序号即可,其他情况则要么和左子节点交换,要么不作交换,跳出循环,所以shiftDown简化成:[/p]
/**
* 下沉
*
* @param index
*/
private void shiftDown(int index) {
//只要这个index对应的节点有左子节点(完全二叉树中不存在 一个节点只有 右子节点没有左子节点)
while (count >= (index << 1)) {
//比较左右节点谁大,当前节点跟谁换位置
//左子节点的inedx
int left = index << 1;
//data[index]预交换的index的序号
int t = left;
//如果右子节点存在,且右子节点比左子节点大,则当前节点可能与右子节点交换
if (((t + 1) <= count) && (data[t].compareTo(data[t + 1]) < 0))
t += 1;
//如果index序号节点比t序号的节点小,才交换,否则什么也不作, 退出循环
if(data[index].compareTo(data[t]) >= 0)
break;
swap(index, t);
index = t;
}
}

嗯,还不错,这下完美了.简单多了.其他还有待优化的地方留在下篇讨论

总结

  • 首先复习了堆的应用场景,具体的应用场景代码实现留在下一篇.

  • 引入堆的概念,性质和大顶堆,小顶堆的概念,实现了大顶堆的元素添加和弹出

  • 根据堆的性质和弹出时下沉的规律,优化下沉方法代码.

  • 下一篇优化堆的构建,用代码实现其应用场景,如排序, topN问题,优先队列等并引入其他的堆分析及其与普通堆的性能差异

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: