一、堆的特性
这里的堆统指储存数据的结构方式,并不是内存堆栈,首先它具有以下特性:
- 堆中任意节点的值总是不大于(不小于)其子节点的值;
- 堆是完全二叉树
- 常常用数组实现
它是完全二叉树,除了树的最后一层节点不需要是满的,其它的每一层从左到右都是满的。注意下面两种情况,第二种最后一层从左到右中间有断隔,那么也是不完全二叉树。
- 它通常用数组来实现。这种用数组实现的二叉树,假设节点的索引值为index,那么:节点的左子节点是 2*index+1,节点的右子节点是 2*index+2,节点的父节点是 (index-1)/2。
- 堆中的每一个节点的关键字都大于(或等于)这个节点的子节点的关键字。
![](https://i-blog.csdnimg.cn/blog_migrate/2bd885b8f96ec55bb036d6b93f91bae9.png)
从1到n编号,就得到结点的一个线性系列。每一层结点个数恰好是上一层结点个数的2倍,也因此通过一个节点的编号就可以推知他的左右孩子节点的编号。
通过分析和数学归纳可以得出一个结论,很方便的知道他的左右孩子节点和父节点。
- 父节点 parent(i) = (i - 1) / 2,算下结点10的父节点 (7 - 1) / 2 = 3 就是 60
- 左孩子 left child(i) = 2 * i + 1,可以算出 10 的左孩子 7 * 2 + 1 = 15 > 7 (这里的7为最大索引值)没有左孩子这个结点
- 右孩子 right child(i) = 2 * i + 2,可以算出 10 的右孩子 7 * 2 + 2 = 16 > 7 没有右孩子这个结点
这里要注意堆和二叉搜索树的区别,二叉搜索树中所有节点的左子节点关键字都小于右子节点关键字,在二叉搜索树中通过一个简单的算法就可以按序遍历节点。但是在堆中,按序遍历节点是很困难的,如上图所示,堆只有沿着从根节点到叶子节点的每一条路径是降序排列的,指定节点的左边节点或者右边节点,以及上层节点或者下层节点由于不在同一条路径上,他们的关键字可能比指定节点大或者小。所以相对于二叉搜索树,堆是弱序的,要想遍历其中的节点很困难。
二、堆排序
我们知道,堆分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。 鉴于最大堆和最小堆是对称关系,理解其中一种即可。本文将对最大堆实现的升序排序进行详细说明。
堆每添加一个元素,则将其与父节点进行比较,如果新添加节点小于等于父节点,则添加元素到该位置;否则,继续向上寻找父节点,直到找到某个位置,使得位于该位置的新元素的值小于等于对应父节点的元素的值,并且将原位置上的元素向后挪动。
我们先构建一个最大堆:
public class MaxHeap<E extends Comparable<E>> {
//使用数组存储
private Array<E> data;
public MaxHeap(){
data = new Array<>();
}
public MaxHeap(int capacity){
data = new Array<>(capacity);
}
public int size(){
return this.data.getSize();
}
public boolean isEmpty(){
return this.data.isEmpty();
}
//根据当前节点索引index计算其父节点的索引
private int parent(int index) {
if(index ==0){
throw new IllegalArgumentException("该节点为根节点");
}
return (index - 1) / 2;//这里为什么不分左右?因为java中 / 运算符只保留整数位。
}
//返回索引为 index 节点的左孩子节点的索引
private int leftChild(int index){
return index*2 + 1;
}
//返回索引为 index 节点的右孩子节点的索引
private int rightChild(int index){
return index*2 + 2;
}
}
上浮shift up
现在再来模拟上浮shift up操作,时间复杂度为O(logn)(在最大二叉堆中,要堆化一个元素,需要向上查找,找到它的父节点,大于父节点则交换两个元素,重复该过程直到每个节点都满足堆的性质为止。这个过程我们称之为上浮操作。):
我们先用图来说明:假设我们对下面的最大堆加入新元素52,放在数组最后一位,52大于父节点16,因此需要调整
首先交换索引为5和11数组中数值的位置吗,也就是52与16互换。
此时依然52大于父节点41,继续交换。
此时满足堆的定义,这就是上浮shift up。
用代码表示:
public void add(E e){
// 向数组尾部添加元素
this.data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int k) {
// 上浮,如果大于父节点,进行交换
while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
data.swap(k, parent(k));
k = parent(k);
}
}
下沉shift down
一般的如果我们取出堆顶元素,我们选择将该数组中的最后一个元素替换堆顶元素,返回堆顶元素,删除最后一个元素。然后再对该元素做下沉操作 sift down,时间复杂度为O(logn)。
先取出最大元素,我们会将最后一位数也就是16放到根节点,此时不满足最大堆的定义。
调整的过程是将这个根节点 16 一步一步向下挪,16 比子节点都小,先比较子节点 52 和 30 哪个大,和大的交换。
继续比较 16 的子节点 28 和 41,41 大,所以 16 和 41 交换位置。
最后满足堆的特性。
用代码实现:
public E extractMax(){
E ret = findMax();
this.data.swap(0, (data.getSize() - 1));
data.removeLast();
siftDown(0);
return ret;
}
//下沉shift down
public void siftDown(int k){
while(leftChild(k) < data.getSize()){
// 从左节点开始,如果左节点小于数组长度,就没有右节点了
int j = leftChild(k);
if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){
// 选举出左右节点最大的那个
j ++;
}
if(get(k).compareTo(get(j)) >= 0){
// 如果当前节点大于左右子节点,循环结束
break;
}
data.swap(k, j);
k = j;
}
}
完整代码:
public class MaxHeap<E extends Comparable<E>> {
//使用数组存储
private Array<E> data;
public MaxHeap(){
data = new Array<>();
}
public MaxHeap(int capacity){
data = new Array<>(capacity);
}
public int size(){
return this.data.getSize();
}
public boolean isEmpty(){
return this.data.isEmpty();
}
//根据当前节点索引index计算其父节点的索引
private int parent(int index) {
if(index ==0){
throw new IllegalArgumentException("该节点为根节点");
}
return (index - 1) / 2;//这里为什么不分左右?因为java中 / 运算符只保留整数位。
}
//返回索引为 index 节点的左孩子节点的索引
private int leftChild(int index){
return index*2 + 1;
}
//返回索引为 index 节点的右孩子节点的索引
private int rightChild(int index){
return index*2 + 2;
}
//向堆中添加元素
public void add(E e){
// 向数组尾部添加元素
this.data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int k) {
// 上浮,如果大于父节点,进行交换
while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
data.swap(k, parent(k));
k = parent(k);
}
}
//取出堆中最大元素
public E extractMax(){
E ret = findMax();
this.data.swap(0, (data.getSize() - 1));
data.removeLast();
siftDown(0);
return ret;
}
//下沉shift down
public void siftDown(int k){
while(leftChild(k) < data.getSize()){
// 从左节点开始,如果左节点小于数组长度,就没有右节点了
int j = leftChild(k);
if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){
// 选举出左右节点最大的那个
j ++;
}
if(get(k).compareTo(get(j)) >= 0){
// 如果当前节点大于左右子节点,循环结束
break;
}
data.swap(k, j);
k = j;
}
}
}