不得不说,堆排序太容易出现了,选择填空问答算法大题都会出现。建堆的过程,堆调整的过程,这些过程的时间复杂度,空间复杂度,以及如何应用在海量数据Top K问题中等等,都是需要重点掌握的。
1)算法简介
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
2)算法描述
我们这里介绍几个问题,一步步推到堆排序的算法。
1、什么是堆?
我们这里提到的堆一般都指的是二叉堆,它满足二个特性:
- 1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
- 2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
如下为一个最小堆(父结点的键值总是小于任何一个子节点的键值)
2、什么是堆调整(Heap Adjust)?
这是为了保持堆的特性而做的一个操作。对某一个节点为根的子树做堆调整,其实就是将该根节点进行“下沉”操作(具体是通过和子节点交换完成的),一直下沉到合适的位置,使得刚才的子树满足堆的性质。
例如对最大堆的堆调整我们会这么做:
1、在对应的数组元素A[i], 左孩子A[LEFT(i)], 和右孩子A[RIGHT(i)]中找到最大的那一个,将其下标存储在largest中。
2、如果A[i]已经就是最大的元素,则程序直接结束。
3、否则,i的某个子结点为最大的元素,将A[largest]与A[i]交换。
4、再从交换的子节点开始,重复1,2,3步,直至叶子节点,算完成一次堆调整。
这里需要提一下的是,一般做一次堆调整的时间复杂度为log(n)。
如下为我们对4为根节点的子树做一次堆调整的示意图,可帮我们理解。
3、如何建堆
建堆是一个通过不断的堆调整,使得整个二叉树中的数满足堆性质的操作。在数组中的话,我们一般从下标为n/2的数开始做堆调整,一直到下标为0的数(因为下标大于n/2的数都是叶子节点,其子树已经满足堆的性质了)。下图为其一个图示:
很明显,对叶子结点来说,可以认为它已经是一个合法的堆了即20,60, 65, 4, 49都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分别作一次向下调整操作就可以了。
4、如何进行堆排序
堆排序是在上述3中对数组建堆的操作之后完成的。
数组储存成堆的形式之后,第一次将A[0]与A[n -1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n-2]交换,再对A[0…n-3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。
如下图所示:
最差时间复杂度: O(n log n)
最优时间复杂度: O(n log n)
平均时间复杂度: O(n log n)
最差空间复杂度: O(n)
3)算法图解、flash演示、视频演示
图解:
略,见上一节。
Flash:
可参见http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=88中的flash动画,帮助理解
视频 堆排序
http://v.youku.com/v_show/id_XMzQzNzAwODQ=.html
4)算法代码
直接上代码吧,重点注意HeapAdjust,BuildHeap和HeapSort的实现。
#include <cstdio>
#include <cstdlib>
#include <cmath>
using namespace std;
int parent(int);
int left(int);
int right(int);
void HeapAdjust(int [], int, int);
void BuildHeap(int [], int);
void print(int [], int);
void HeapSort(int [], int);
/*返回父节点*/
int parent(int i)
{
return (int)floor((i - 1) / 2);
}
/*返回左孩子节点*/
int left(int i)
{
return (2 * i + 1);
}
/*返回右孩子节点*/
int right(int i)
{
return (2 * i + 2);
}
/*对以某一节点为根的子树做堆调整(保证最大堆性质)*/
void HeapAdjust(int A[], int i, int heap_size)
{
int l = left(i);
int r = right(i);
int largest;
int temp;
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)
{
temp = A[i];
A[i] = A[largest];
A[largest] = temp;
HeapAdjust(A, largest, heap_size);
}
}
/*建立最大堆*/
void BuildHeap(int A[],int heap_size)
{
for(int i = (heap_size-2)/2; i >= 0; i--)
{
HeapAdjust(A, i, heap_size);
}
}
/*输出结果*/
void print(int A[], int heap_size)
{
for(int i = 0; i < heap_size;i++)
{
printf("%d ", A[i]);
}
printf("
");
}
/*堆排序*/
void HeapSort(int A[], int heap_size)
{
BuildHeap(A, heap_size);
int temp;
for(int i = heap_size - 1; i >= 0; i--)
{
temp = A[0];
A[0] = A[i];
A[i] = temp;
HeapAdjust(A, 0, i);
}
print(A, heap_size);
}
/*测试,对给定数组做堆排序*/
int main(int argc, char* argv[])
{
const int heap_size = 13;
int A[] = {19, 1, 10, 14, 16, 4, 7, 9, 3, 2, 8, 5, 11};
HeapSort(A, heap_size);
system("pause");
return 0;
}
5)考察点,重点和频度分析
堆排序相关的考察太多了,选择填空问答算法大题都会出现。建堆的过程,堆调整的过程,这些过程的时间复杂度,空间复杂度,需要比较交换多少次,以及如何应用在海量数据Top K问题中等等。堆又是一种很好做调整的结构,在算法题里面使用频度很高。
6)笔试面试题
例题1、编写算法,从10亿个浮点数当中,选出其中最大的10000个。
典型的Top K问题,用堆是最典型的思路。建10000个数的小顶堆,然后将10亿个数依次读取,大于堆顶,则替换堆顶,做一次堆调整。结束之后,小顶堆中存放的数即为所求。代码如下(为了方便,这里直接使用了STL容器):
#include "stdafx.h"
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional> // for greater<>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
vector<float> bigs(10000,0);
vector<float>::iterator it;
// Init vector data
for (it = bigs.begin(); it != bigs.end(); it++)
{
*it = (float)rand()/7; // random values;
}
cout << bigs.size() << endl;
make_heap(bigs.begin(),bigs.end(), greater<float>()); // The first one is the smallest one!
float ff;
for (int i = 0; i < 1000000000; i++)
{
ff = (float) rand() / 7;
if (ff > bigs.front()) // replace the first one ?
{
// set the smallest one to the end!
pop_heap(bigs.begin(), bigs.end(), greater<float>());
// remove the last/smallest one
bigs.pop_back();
// add to the last one
bigs.push_back(ff);
// mask heap again, the first one is still the smallest one
push_heap(bigs.begin(),bigs.end(),greater<float>());
}
}
// sort by ascent
sort_heap(bigs.begin(), bigs.end(), greater<float>());
return 0;
}
例题2、设计一个数据结构,其中包含两个函数,1.插入一个数字,2.获得中数。并估计时间复杂度。
使用大顶堆和小顶堆存储。
使用大顶堆存储较小的一半数字,使用小顶堆存储较大的一半数字。
插入数字时,在O(logn)时间内将该数字插入到对应的堆当中,并适当移动根节点以保持两个堆数字相等(或相差1)。
获取中数时,在O(1)时间内找到中数。