前言:前几天在面试中被问到一个问题:如何在100个数中找出前5大的数?其实我知道这是一个很经典的算法题,我也曾在刷剑指offer编程题的时候做过这道题,当时我是用最简单的冒泡排序来做的,比如上面那个问题就可以用冒泡五次来找到前5大的数。这个解法的时间复杂度是O(n*k),其中n是数据规模,k是找出前k个大小的数。后来,我发现了一种更高效的解法-堆排序算法,维持一个k大小的最大堆,遍历后面的元素,如果比堆的最小元素还要小就忽略,否则更新最大堆。下面我就来详细讲解一下什么是堆排序。
堆
- 堆是什么?这里的堆指的是一种数据机构,它是一个近似的完全二叉树:
根据图片我们知道,它是一个完全二叉树,但是它跟二叉排序树不一样,二叉排序树是左孩子比父节点小,右节点比父节点大。而一个二叉堆的左右孩子没有固定的大小关系,但是父节点要么比孩子小,要么比孩子大。 - 由于1的性质,堆可以用数组来存储。给定一个节点的下标i,我们可以求出它的父节点、左孩子、右孩子分别为i/2(下取整)、2i、2i+1;一般来说,这三个操作都可以在c++中以宏定义或者内联的方式来实现。
- 二叉堆有两种形式:最大堆和最小堆。最大堆性质是除了根节点以外所有节点i都满足:
A[PARENT(i)] >= A[i];
最大堆用来对数据进行升序排序,最小堆为降序排序。
维护堆的性质
/**max_heapify用来维护堆的性质,中心思想是从一颗子树的根节点出发,看
是否符合堆的性质,然后逐级递减到叶子节点。
从a[i]、a[left(i)],a[right(i)]中找到一个最大的,用largest记录其下标。
如果a[i]是最大的,说明已经符合最大堆性质,程序结束;
如果最大的不是a[i],那么只能是它的孩子,交换a[i]与a[largest]的值,
使下标为i的节点和它的直接孩子都满足最大堆性质,但是,交换了以后,
以a[largest]的节点可能违反最大堆性质,因此需要递归调用max_heapify函数
**/
int max_heapify(int i){
int l = left(i);
int r = right(i);
int largest;
if(l<heap_size && a[l]>a[i]){
largest = l;
}
else
largest = i;
if(r<heap_size && a[r]>a[largest])
largest = r;
if(largest != i){
swap(a[i],a[largest]);
max_heapify(largest);
}
}
建堆
void build_max_heap(){
/**为什么循环次数是heap_size/2?为什么i要递减?i可以从0到heap_size/2吗?
因为当i>heap_size/2时,该节点为叶节点,自然符合最大堆性质;因为递减的话,
对当前节点进行操作时能够保证以当前节点为根节点满足最大堆性质,降低操作时间
**/
for(int i=heap_size/2;i>=0;--i){
max_heapify(i);
}
}
堆排序算法
中心思想:因为根节点的值是最大的,所以每次交换根节点与最后一个叶子节点,使得最后一个节点值最大,然后”去掉”最后一个节点,这样最大的元素就排在数组最后面,而堆因为节点交换可能会违背堆的性质,因此需要调用一次max_heapify(logn)来维护堆的性质。堆排序算法不断重复,直到堆的大小从n-1到2。因为堆排序需要遍历每一个节点,而每一个节点时间是(logn),因此时间复杂度是O(n*logn)。
void heapSort(){
build_max_heap();
for(int i = heap_size-1;i>0;--i){
swap(a[0],a[i]);
heap_size--;
max_heapify(0);
}
}
运行结果: