關於堆的介紹已經一大堆了,這裏主要總結下筆記 && 對比下這幾個 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