什么是堆
堆是一种特殊的树,他有俩个要求
1 堆是一个完全二叉树
2 堆每一个节点的值,都大于等于(或小于等于)其左右子树节点的值
解释一下:
第一点,完全二叉树就是除了最后一层,其它层的节点数都是满的,最后一层的节点都靠左排列
每个节点都大于等于左右子树节点的叫做大顶堆,每个节点都小于等于左右子树节点的叫做小顶堆
12 是大顶堆,3是小顶堆,4不是堆
如何实现一个堆
要实现一个堆,首先我们需要知道如何储存一个堆
我们之前讲过,完全二叉树比较适合数组来储存,用数组去储存一个完全二叉树,非常的节省空间单纯的通过下标,就可以找到一个节点的左右子节点,和父节点
下标为i的节点,左子节点的下标为i2,右子节点的下标i2+1,父节点的下标i/2
向堆中插入一个数据
每当插入一个数据,我们还需要继续满足堆的俩个特性
如果我们把新插入的数据放到堆的最后,那么就不满足堆的俩个特性,那么就需要调整一下,让他重新复合这个特性,这个过程我们叫做堆化
堆化有俩种方式,从下向上和从上向下,我们先说从下向上
我们让新插入的节点,和父节点进行比较,如果不满足堆的特性,那么久交换一下,一直重复这个过程,直到满足为止
public class Head {
//存储堆的数组
private int[] head;
//堆存储的最大数据的个数
private int n;
//堆中目前数据的个数
private int count;
public Head(int capacity) {
//为什么要+1,因为下标为0的位置不能用
head = new int[capacity + 1];
n = capacity;
count = 0;
}
public void insert(int data) {
//如果堆满了就直接返回
if (count == n) return;
++count;
//把数据插入到最后
head[count] = data;
//自下向上堆化
int i = count;
while (i / 2 > 0 && head[i] > head[i / 2]) {
int temp = head[i / 2];
head[i / 2] = head[i];
head[i] = temp;
i = i / 2;
}
}
}
删除堆顶元素
从堆的定义来看,堆顶储存的是堆中最大元素或者最小元素,假如我们删除的是大顶堆,堆顶是最大元素,那该如何正确的删除呢?
我们可以把最后一个元素放到堆顶,然后从堆顶开始,对比父子节点的数据,不满足要求的交换,一直重复,直到满足父子节点的关系为止,这个就是从上向下的堆化
public void removeMax() {
if (count == 0) return; // 堆中没有数据
head[1] = head[count];
--count;
heapify(head, count, 1);
}
private void heapify(int[] a, int n, int i) {
// 自上往下堆化
int maxPos = i;
while (true) {
if (i * 2 <= n && a[i] < a[i * 2]) maxPos = i * 2;
if (i * 2 + 1 <= n && a[maxPos] < a[i * 2 + 1]) maxPos = i * 2 + 1;
if (maxPos == i) break;
//交换数据
int temp = head[i];
head[i] = head[maxPos];
head[i] = temp;
i = maxPos;
}
}
一个完全二叉树,高度不大于log2n,而堆化是根据节点的所在路径进行交换的,所以跟树的盖度成正比时间复杂度为O(logn),插入和删除主要逻辑就是堆化,说以时间复杂度为O(logn)
如何实现堆排序
我们可以把堆排序分为俩个操作,建堆和排序
建堆
建堆指的是,把一个无需数组建成一个符合堆定义的数组,建堆有俩种思路,一种就是上面的插入,我们可以假设堆中只有一个元素,然后把2到n的元素依次插入,这样就组成了堆
第二种思路和第一种相反,第一种是从前向后处理数据,从下向上堆化,第二种是从数组后向前处理数据,从上向下堆化
因为叶子节点向下堆化只能和自己比较,所以我们需要从第一个非叶子节点的开始堆化
private static void buildHeap(int[] a, int n) {
for (int i = n/2; i >= 1; --i) {
heapify(a, n, i);
}
}
private static void heapify(int[] a, int n, int i) {
int maxPos = i;
while (true) {
if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;
if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;
if (maxPos == i) break;
//交换数据
int temp = head[i];
head[i] = head[maxPos];
head[i] = temp;
i = maxPos;
}
}
第一个非叶子节点是从 n/2 开始的,n/2+1 到 n 都是叶子节点我们不需要堆化,建堆的时间复杂度为O(n)
排序
经过建堆以后,现在已经是大顶堆了,堆顶就是最大元素,我们把堆顶移到下标n的位置,然后剩下的进行堆化,然后重复上方的操作,和删除堆顶元素差不多,直到堆中只有一个元素,就排序完成了
堆排序的完整代码
// n 表示数据的个数,数组 a 中的数据从下标 1 到 n 的位置。
public static void sort(int[] a, int n) {
//第一步 建堆
buildHeap(a, n);
int k = n;
//直到剩下一个元素不在循环
while (k > 1) {
//交换堆顶和最后一个元素的位置
int temp = a[1];
a[1] = a[k];
a[k] = temp;
--k;
//把剩下的元素进行堆化
heapify(a, k, 1);
}
}
整个堆排序后只需要极少的临时空间,所以堆排序是原地排序
建堆时间复杂度O(n),排序是将复杂度为O(nlogn),所以堆排序的时间复杂度为O(nlogn)
堆排序不是稳定排序,因为在堆排序过程中,存在把最后一个元素和第一个元素交换的过程,会打乱原有的顺序
上面所有的操作都是假设数组是从下标1出开始的,如果下标是从0开始的,处理的思路是没有任何改变的,唯一的变化就是计算子树坐标的公式
如果下标从0开始,那么节点的下标是i,左子节点下标就是2i+1,右子节点就是2i+2,父节点就是i-1/2。
所有的代码
/**
* 大顶堆
*/
public class Head {
//存储堆的数组
private int[] head;
//堆存储的最大数据的个数
private int n;
//堆中目前数据的个数
private int count;
public Head(int capacity) {
//为什么要+1,因为下标为0的位置不能用
head = new int[capacity + 1];
n = capacity;
count = 0;
}
//堆中插入数据
public void insert(int data) {
//如果堆满了就直接返回
if (count == n) return;
++count;
//把数据插入到最后
head[count] = data;
//自下向上堆化
int i = count;
while (i / 2 > 0 && head[i] > head[i / 2]) {
int temp = head[i / 2];
head[i / 2] = head[i];
head[i] = temp;
i = i / 2;
}
}
//堆中删除堆顶元素
public void removeMax() {
if (count == 0) return; // 堆中没有数据
head[1] = head[count];
--count;
heapify(head, count, 1);
}
private void heapify(int[] a, int n, int i) {
// 自上往下堆化
int maxPos = i;
while (true) {
if (i * 2 <= n && a[i] < a[i * 2]) maxPos = i * 2;
if (i * 2 + 1 <= n && a[maxPos] < a[i * 2 + 1]) maxPos = i * 2 + 1;
if (maxPos == i) break;
//交换数据
int temp = head[i];
head[i] = head[maxPos];
head[i] = temp;
i = maxPos;
}
}
//堆排序 假设数组是从下标1开始的
public void sort(int[] a, int n) {
//第一步 建堆
buildHeap(a, n);
int k = n;
//直到剩下一个元素不在循环
while (k > 1) {
//交换堆顶和最后一个元素的位置
int temp = a[1];
a[1] = a[k];
a[k] = temp;
--k;
//把剩下的元素进行堆化
heapify(a, k, 1);
}
}
//建堆
private void buildHeap(int[] a, int n) {
for (int i = n / 2; i >= 1; --i) {
heapify(a, n, i);
}
}
}