堆和堆排序:为什么堆排序没有快排快?
堆(HEAP)是一种原地的、时间复杂度为O(nlogn)的排序算法
在实际的软件开发中,快排的性能为什么要比堆排序好?
如何理解堆?
满足这两点就是一个堆
- 堆是一个完全二叉树
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值
对于每个节点的值都大于等于子树中每个节点值的堆,叫做“大顶堆”,反之则是“小顶堆”
如何实现一个堆?
堆都支持哪些操作?如果存储一个堆?
完全二叉树适合用数组来存储,只需要存储左右子节点的指针,单纯的通过数组的下标,就可以找到一个节点的左右子节点和父节点
数组中下标为i的节点的左子节点,就是下标为2i的节点,右子节点就是下标为2i+1的节点,父节点就是下标为i/2的节点
堆的核心操作是往堆中插入一个元素和删除堆顶元素(一般是大顶堆)
1.往堆中插入一个元素
插入一个元素后,需要继续满足堆的两个特性,但是如果把新插入的元素都放到堆的最后,就不符合堆的特性了(比如新插入的元素比较大,就不符合第二条特性了),所以就需要进行调整,让其重新满足特性,这个过程叫堆化
堆化有两种,从下往上和从上往下,先讲从下往上的堆化方法:顺着节点所在的路径,向上或者向下,对比,然后交换
public class Heap{
private int[] a ; //数组,从下标1开始存储数据
private int n; //堆可以存储的最大数据个数
private int count;// 堆中已经存储的数据个数
public Heap(int capacity){
a = new int[capacity + 1];
n = capacity;
count = 0;
}
public void insert(int data){
if(count >= n ) return;//堆满了
++count;
a[count] = data;
int i = count;
while(i/2 > 0 && a[i] > a[i/2]){ //自下往上堆化
swap(a,i,i/2); //swap()函数作用:交换小标为i 和i/2的两个元素
i=i/2;
}
}
}
2.删除堆顶元素
把最后一个节点放到堆顶,然后利用同样的父子节点对比方法,对于不满足父子节点大小关系的,互换两个节点,直到父子节点之间满足大小关系为止,这是从上往下的堆化方法。
我们移除的是数组中最后一个元素,而在堆化的过程中,都是交换操作,不会出现“空洞”
public void removeMax(){
if(count == 0) return -1; //堆中没有数据
a[1] = a[count]; //把最后一个元素放到堆顶
--count; //堆的规模-1
heapify(a,count,1); //堆化完成,count是长度
}
public void heapify(int[] a ,int n ,int i){ //自上往下堆化
while(true){
int maxPos = i ;
if(i*2 <= n && a[i] < a[i*2]) maxPos = i*2+1;
if(maxPos == i) break;
swap(a ,i ,maxPos);
i = maxPos;
}
}
一个包含n个节点的完全二叉树,树的高度不超过log2n,堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是O(log2n),往里面插入一个元素和删除堆顶元素的时间复杂度都是O(logn)
如何实现堆排序?
排序方法的时间复杂度是O(nlogn),堆排序的过程分解成两个大步骤:建堆和排序
1.建堆
将数组原地建个堆,就是在原数组上操作
一:在堆中插入一个元素的思路,假设起初堆中只有1个数据,就是下标为1的数据,调用插入操作,将下标为2到n的数据依次插入到堆中,这样就将n个数据的数据组织成了堆
二:叶子节点往下对话只能自己跟自己比较,所以我们直接从第一个非叶子节点开始,依次堆化即可
public 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){
while(true){
int maxPos = i;
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;
swap(a,i,maxPos);
i = maxPos;
}
}
从n/2开始到1的数据进行堆化,下表示n/2+1到n的节点是叶子节点,不需要堆化
实际上,堆排序的建堆过程的时间复杂度是O(n)
叶子节点不用堆化,所以需要堆化的节点从倒数第二层开始,每个节点堆化的过程中,需要比较和交换的节点个数,跟这个节点的高度k成正比
2.排序
建堆之后,数组中的数据是按照大顶堆的特性来组织的
数组中的第一个元素就是堆顶,也就是最大元素,跟最后一个元素交换,那最大元素就放到下标为n的位置,当堆顶元素移除之后,把下标为n的元素放到堆顶,再通过堆化,将剩下n-1个元素重新构建,堆化之后,再取堆顶元素,放到n-1的位置,一直重复,直到剩下标为1 的一个元素
//n表示数据的个数,数组a中的数据从下标1到n的位置
public static void sort(int[] a ,int n){
buildHeap(a,n);
int k = n;
while (k > 1){
swap(a,1,k);
--k;
heapify(a,k,1);
}
}
堆排序不是稳定的排序算法,因为在排序过程中,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序
在实际开发中,为什么快排比堆排序性能好?
一:堆排序数据访问的方式没有快排友好
快排的数据是顺序访问的,堆排序数据是跳着访问的
二:对于同样的数据,排序过程中,堆排序算法的数据交换次数多于快排