1. 简介
堆(Heap)在计算机科学中可以表示内存的一部分,也是数据结构与算法中一种非常常用的数据结构。
尤其是在较大数据量的排序等问题中,堆排序经常被使用,因为堆排序的时间复杂度为 O(nlogn) 。堆排序也是面试中经常被问到的对大量数据排序的算法,如海量数据选出 TOP K 等。
堆的结构 堆 是一棵完全二叉树 ,且分为 大顶堆 和 小顶堆 两种结构:大顶堆中根结点的值大于左右子结点的值;小顶堆中根结点的值小于左右子结点的值。
大顶堆示例如下图所示:
2. 建堆和添加、删除操作
2.1 建堆
我们以如下所示的原始完全二叉树为例,详细描述大顶堆建堆的整过程。
给定的数字的初始序列为: 85、55、82、57、68、92、99、98、66、56 将它们按照树的层序从上到下、从左到右依次摆放就得到如上图所示的初始的完全二叉树(堆是一种完全二叉树)
现在要调整这个初始堆,使它成为一个大顶堆。 调整的策略是:从最后一个结点开始从右向左、从下到上调增,使得父结点的值大于两个子结点的值。并且,对交换的子结点还要进行递归,使得它下面的子树仍然满足大顶堆的性质。
1)56比它的父结点68的值下,因此56不需要调整;
2)66和98两个子结点的值和它们的父结点相比较,选择最大的值做父结点,将原来的父结点的值与最大值的根结点交换,如下图:
3)99和92与它们的父结点相比较,选择最大的做父结点,得到如下图
4)68和98与它们的父结点相比较,选择最大的98做父结点,即55和98交换。交换后还要对55进行递归,55比它的子结点小,继续调整,最终得到如下图:
5)99和98与它们的父结点相比较,选择最大的99做父结点,即将85与99交换。交换后还需要对85递归,最终得到如下图:
最终,所有结点都进行了调整,得到了大顶堆如下:
对树进行调整的代码
vector<int> heap;
int size;
void downAdjust(int low, int high) {
int i = low, j = i * 2;
while (j <= high) {
if (j + 1 <= high && heap[j + 1] > heap[j]) {
j = j + 1;
}
if (heap[j] > heap[i]) {
swap(heap[i], heap[j]);
i = j;
j = i * 2;
} else {
break;
}
}
}
heap 数组下标的范围为 1~n。完全二叉树的叶子结点个数为 ?n/2? ,因此数组下标在 [1, ?n/2? 范围内的结点都是非叶子结点。于是可以从 ?n/2? 号位开始倒着枚举结点,对每个遍历到的结点i进行 [i, n] 范围的调整。
建堆代码
void createHeap() {
for (int i = n / 2; i >= 1; i--) {
downAdjust(i, n);
}
}
2.2 删除堆顶元素操作
如果要删除堆中的最大元素(也就是删除堆顶元素),并且让其仍然保持堆的结构,那么只需要最后一个元素覆盖堆顶元素,然后对根结点进行调整即可。时间复杂度为 O(logn) .
int deleteTop() {
int res = heap[1];
heap[1] = heap[size--];
heap.pop_back();
downAdjust(1, size);
return res;
}
2.3 添加元素操作
添加元素操作:可以把要被添加的元素放在数组最后(也就是完全二叉树最后一个结点的后面),然后向上进行调整操作。 向上调整操作:总是把欲调整结点与父结点比较,如果权值比父结点大,那么就交换其与父结点,止痒反复比较,直到堆顶或者父结点的权值较大为止。时间复杂度为 O(logn) .
void upAdjust(int low, int high) {
int i = high, j = i / 2;
while (j >= low) {
if (heap[i] > heap[j]) {
swap(heap[i], heap[j]);
i = j;
j = i / 2;
} else {
break;
}
}
}
void insert(int x) {
heap.push_back(x);
size++;
upAdjust(1, size);
}
3. 堆排序
堆排序是使用堆结构对一个序列进行排序。
思路:堆顶元素是最大的,因此可以考虑取出堆顶元素,然后将堆的最后一个元素替换至堆顶,再进行一次针对堆顶元素的向下调整。如此重复,直到堆中只剩下一个元素为止。
void heapSort() {
createHeap();
for (int i = n; i >= 1; i--) {
swap(heap[i], heap[1]);
downAdjust(1, i - 1);
}
}
4. 测试
C++
#include <iostream>
#include <vector>
using namespace std;
class HeapSort {
public:
vector<int> heap;
int size;
HeapSort(vector<int> &nums) {
size = (int) nums.size();
heap.push_back(-1);
for (int i = 0; i < size; i++) {
heap.push_back(nums[i]);
}
}
void downAdjust(int low, int high) {
int i = low, j = i * 2;
while (j <= high) {
if (j + 1 <= high && heap[j + 1] > heap[j]) {
j = j + 1;
}
if (heap[j] > heap[i]) {
swap(heap[i], heap[j]);
i = j;
j = i * 2;
} else {
break;
}
}
}
void createHeap() {
for (int i = size / 2; i >= 1; i--) {
downAdjust(i, size);
}
}
int deleteTop() {
int res = heap[1];
heap[1] = heap[size--];
heap.pop_back();
downAdjust(1, size);
return res;
}
void upAdjust(int low, int high) {
int i = high, j = i / 2;
while (j >= low) {
if (heap[i] > heap[j]) {
swap(heap[i], heap[j]);
i = j;
j = i / 2;
} else {
break;
}
}
}
void insert(int x) {
heap.push_back(x);
size++;
upAdjust(1, size);
}
void heapSort() {
createHeap();
for (int i = size; i >= 1; i--) {
swap(heap[i], heap[1]);
downAdjust(1, i - 1);
}
}
};
int main() {
vector<int> nums = {9, 7, 5, 3, 1, 2, 4, 6, 8};
HeapSort heapSort(nums);
for (int i = 1; i <= heapSort.size; i++) {
cout << heapSort.heap[i] << " ";
}
cout << endl;
heapSort.createHeap();
int res = heapSort.deleteTop();
cout << "top element: " << res << endl;
res = heapSort.deleteTop();
cout << "top element: " << res << endl;
res = heapSort.deleteTop();
cout << "top element: " << res << endl;
heapSort.insert(-8);
heapSort.insert(-2);
heapSort.heapSort();
for (int i = 1; i <= heapSort.size; i++) {
cout << heapSort.heap[i] << " ";
}
return 0;
}
测试结果
JAVA
public class HeapSort {
private int[] heap;
private int size;
public HeapSort(int[] nums) {
size = nums.length;
heap = new int[size + 1];
for (int i = 0; i < nums.length; i++) {
heap[1 + i] = nums[i];
}
}
public void downAdjust(int low, int high) {
int i = low, j = i * 2;
while (j <= high) {
if (j + 1 <= high && heap[j + 1] > heap[j]) {
j = j + 1;
}
if (heap[j] > heap[i]) {
int tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
i = j;
j = i * 2;
} else {
break;
}
}
}
public void createHeap() {
for (int i = size / 2; i >= 1; i--) {
downAdjust(i, size);
}
}
public int deleteTop() {
int res = heap[1];
heap[1] = heap[size--];
downAdjust(1, size);
return res;
}
public void upAdjust(int low, int high) {
int i = high, j = i / 2;
while (j >= low) {
if (heap[i] > heap[j]) {
int tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
i = j;
j = i / 2;
} else {
break;
}
}
}
public void insert(int x) {
if (size + 1 >= heap.length) {
int[] newHeap = new int[size * 2];
System.arraycopy(heap, 0, newHeap, 0, size);
heap = newHeap;
}
heap[++size] = x;
upAdjust(1, size);
}
public void heapSort() {
for (int i = size; i >= 1; i--) {
int tmp = heap[1];
heap[1] = heap[i];
heap[i] = tmp;
downAdjust(1, i - 1);
}
}
public void printHeap() {
for (int i = 1; i <= size; i++) {
System.out.print(heap[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] nums = {9, 7, 5, 3, 1, 2, 4, 6, 8};
HeapSort heapSort = new HeapSort(nums);
heapSort.printHeap();
heapSort.createHeap();
int res = heapSort.deleteTop();
System.out.println("top element: " + res);
res = heapSort.deleteTop();
System.out.println("top element: " + res);
res = heapSort.deleteTop();
System.out.println("top element: " + res);
heapSort.insert(-8);
heapSort.insert(-2);
heapSort.heapSort();
heapSort.printHeap();
}
}
测试结果
5. TOP K 问题
TOP K问题是面试中常常被问到的问题,从大量的数据中,找出最小的或者最大的几个值,这就可以采用 堆 来实现。
例如,给定1,000,000个整数,要求从中找出最大的10个数。
我们可以首先用前10个数建立一个size为10的 小顶堆 ,即堆顶元素是10个数中最小的。然后依次取出后面的10 ~ 1,000,000个数,将其与小顶堆的堆顶元素进行比较:
- 如果小于等于堆顶元素,则说明这个数比堆中的10个元素都小,跳过;
- 如果大于堆顶元素,则用该数替换掉小顶堆的堆顶元素,然后对小顶堆从堆顶向下做一次调整,使得还是形成小顶堆。
这样,当所有的数都验证完后,小顶堆中的10个元素就是这1,000,000个元素中最大的10个元素。
并且,当数据量非常大,内存无法一次性载入时仍然可以使用堆来获取 TOP K,将硬盘中的数据分块的载入内存,再与堆顶元素进行比较、替换、调整。
参考文献
《算法笔记》,胡凡、曾磊等.
|