【數據結構與算法】堆排序、堆化 heapify

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

heapify
實現方式 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章