堆排序及其应用
1、大顶堆和小顶堆
堆是具有下列特征的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于都等于其左右孩子结点的值,称为小顶堆。
结点之间的关系:大顶堆:a[n] >= a[2 * n] && a[n] >= a[2 * n + 1];小顶堆:a[n] <= a[2 * n] && a[n] <= a[2 * n + 1]
2、堆排序
由于堆中结点之间满足的关系,因此可以直接利用数组的下标对其进行排序,代码如下:
void AdjustHeap(vector<int>&a, int s, int len) {
int i = s;
int temp = a[i];
int j = 2 * i;
for (int j = 2 * i; j <= len; j *= 2) {
if (j < len && a[j] < a[j + 1]) {
++j;
}
if (temp >= a[j]) {
break;
}
a[i] = a[j];
i = j;
}
a[i] = temp;
}
void HeapSort(vector<int>&a) {
int len = a.size() - 1; // vector中第一个元素填入0
for (int i = len / 2; i >= 1; --i) { //建立大顶堆
AdjustHeap(a, i, len);
}
for (int i = a.size() - 1; i > 1; --i) {
swap(a[1], a[i]);
AdjustHeap(a, 1, i - 1); // 堆剩下的i - 1个元素重新调整为大顶堆
}
}
算法复杂度: O(nlogn)
在构建堆的过程中,是从完全二叉树的最下层最右边的非叶子结点开始构建,将它与其孩子结点进行比较和交换,对于每个非叶子结点来说,最多进行两次比较和互换动作,因此整个构建堆的过程时间复杂度为O(n)。
正式排序时,第i次取堆顶元素重建堆需要时间复杂度O(logi), (因为剩下i个元素无序),并且需要取n次堆顶元素,因此重建堆的过程时间复杂度为O(nlogn)。
因此整体来说,时间复杂度为O(nlogn)。
3、堆排序的应用
- topK问题
1)对于找出前K个最小元素:用大顶堆
创建一个K维的数组,每次从N个数中读入一个,如果当前数组中的个数小于K,则直接放入,当数组中的个数等于K时只能替换,(如果读入的数比当前数组中最大的值都大则继续读入下一个,否则替换其中最大的一个)。因此这个过程需要确认K维数组中当前最大的数,用大顶堆。
时间复杂度O(nlogk)
2)找出前K个元素最大值:用小顶堆 - 海量数据问题
对于海量数据的问题,考虑到内存占用,一般需要放到多个文件中进行处理,利用分治的思想。如果有重复的数据,需要利用hash表计算出每种数据出现的次数,然后在单个文件中利用堆进行排序。
参考:在100G文件中找出出现次数最多的100个IP
在100G文件中找出出现次数最多的前100个IP
要解决该问题需要找出一种分类方式,将重复出现的放到一个文件中,比如可以根据IP地址的前8位或者后8位进行分类(前8位比较稀疏,考虑用后8位),这样一共有2的8次幂中分类方式。这样将一个大文件拆成多个小文件,依次对每个小文件进行处理。
对其中一个小文件,建立hash表,key是ip地址,value是其出现的次数。然后建立一个小顶堆,大小为100,逐个对出现的次数进行排序,挑选出前100个。