关于堆的介绍已经一大堆了,这里主要总结下笔记 && 对比下这几个 O(nlogn) 的排序效率。
基础知识
堆的基本知识:
大堆:堆顶元素大于左右孩子。
小堆:堆顶元素小于左右孩子。
堆的操作:
插入:在堆的最后位置插入一个元素,然后对这个元素进行向上调整,直至堆的结构稳定。
删除:弹出堆顶元素,可以将堆顶元素和最后一个叶子节点互换,然后对这个叶子节点进行向下调整。
堆的实现
// 大堆
class MaxHeap {
public:
// 初始化一个空堆
MaxHeap(const int cap) {
m_data = new int[cap];
m_cnt = 0;
m_cap = cap;
}
int size() { return m_cnt; }
// 插入一个元素
void insert(const int item) {
assert(m_cnt + 1 <= m_cap);
m_data[++m_cnt] = item;
__shiftUp(m_cnt);
}
// 删除堆顶元素
int pop() {
assert(m_cnt >= 0);
int ret = m_data[0];
swap(m_data[0], m_data[m_cnt--]);
__shiftDown(0);
return ret;
}
private:
// 向上调整
void __shiftUp(int k) {
// testPrintTree();
// cout << "==============" << endl;
int parent_index = (k - 1) / 2;
while (k > 0 && m_data[k] > m_data[parent_index]) {
swap(m_data[k], m_data[parent_index]);
k = parent_index;
parent_index = (k - 1) / 2;
}
}
// 向下调整
void __shiftDown(int k) {
while (k * 2 + 1 <= m_cnt) {
int j = k * 2 + 1;
if (j + 1 <= m_cnt &&m_data[j] < m_data[j + 1]) ++j;
if (m_data[k] >= m_data[j]) break;
swap(m_data[k], m_data[j]);
k = j;
}
}
private:
int* m_data;
int m_cnt; // 有效元素个数
int m_cap; // 总容量
};
堆排序
实现方式 1:
先对一个无序的数组每个元素都 insert 入堆,完成后,每次弹出最大元素放在数组最后一个,这样就形成了一个有序的数组。
void heapSort1(int arr[], int n) {
MaxHeap mh(n);
for (int i = 0; i < n; ++i) {
mh.insert(arr[i]);
}
for (int i = n - 1; i >= 0; --i) {
arr[i] = mh.pop();
}
}
插入操作时间复杂度为 O(logn),所以构建堆的过程为 O(nlogn)。弹出堆的时间复杂度也为 O(logn)。
空间复杂度为 O(n)。
实现方式 2:
heapify,堆化。
首先认为叶子节点为一个堆。
从最后一个非叶子节点开始,对此节点进行向下调整,至堆顶,这样堆化就完成了。
通过数学推导,heapify 的时间复杂度为 O(n)。
// heapify, 使用 arr 初始化一个大堆
MaxHeap(int* arr, int n) {
m_data = new int[n];
m_cap = n;
for (int i = 0; i < n; ++i) {
m_data[i] = arr[i];
}
m_cnt = n;
for (int i = (m_cnt - 1 - 1)/2; i >= 0; --i) {
__shiftDown(i);
}
}
void heapSort2(int arr[], int n) {
MaxHeap mh(arr, n);
for (int i = n - 1; i >= 0; --i) {
arr[i] = mh.pop();
}
}
此排序的空间复杂度为 O(n)。
实现方式 3:
先堆化,再原地对堆进行排序。
时间复杂度 O(n),空间 O(1)。
void __shiftDown(int arr[], int n, int k) {
while (k * 2 + 1 < n) {
int j = k * 2 + 1;
// 左孩子 < 右孩子
if (j + 1 < n && arr[j] < arr[j + 1]) {
++j;
}
// parent > max(left, right)
if (arr[k] > arr[j]) break;
swap(arr[k], arr[j]);
k = j;
}
}
void heapSort3(int arr[], int n) {
// heapify
for (int i = (n - 1 - 1) / 2; i >= 0; --i) {
__shiftDown(arr, n, i);
}
// sort
for (int i = n - 1; i > 0; --i) {
swap(arr[0], arr[i]);
__shiftDown(arr, i, 0);
}
}
性能对比
对 O(nlogn) 级别的排序和堆进行比较:
堆排序的效率是稳定的,但是哺乳其他排序来得快。
既然堆排序效率并没有其他的高,那为什么要有它呢?
堆的主要作用并不是一个静态的排序。
堆主要是为了维护一个动态变化的东西,比如操作系统的任务管理,根据优先级不用执行不同的操作。
EOF