二叉堆的定義
二叉堆是完全二叉樹或者是近似完全二叉樹。
二叉堆滿足二個特性:
1.父結點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值。
2.每個結點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)。
當父結點的鍵值總是大於或等於任何一個子節點的鍵值時為最大堆。當父結點的鍵值總是小於或等於任何一個子節點的鍵值時為最小堆。下圖展示一個最小堆:
由於其它幾種堆(二項式堆,斐波納契堆等)用的較少,一般將二叉堆就簡稱為堆。
堆的存儲
一般都用數組來表示堆,i結點的父結點下標就為(i – 1) / 2。它的左右子結點下標分別為2 * i + 1和2 * i + 2。如第0個結點左右子結點下標分別為1和2。
堆的操作——插入刪除
下面先給出《數據結構C++語言描述》中最小堆的建立插入刪除的圖解,再給出本人的實現代碼,最好是先看明白圖后再去看代碼。
堆的插入
每次插入都是將新數據放在數組最后。可以發現從這個新數據的父結點到根結點必然為一個有序的數列,現在的任務是將這個新數據插入到這個有序數據中——這就類似於直接插入排序中將一個數據並入到有序區間中,對照《白話經典算法系列之二 直接插入排序的三種實現》不難寫出插入一個新數據時堆的調整代碼:
- // 新加入i結點 其父結點為(i - 1) / 2
- void MinHeapFixup(int a[], int i)
- {
- int j, temp;
- temp = a[i];
- j = (i - 1) / 2; //父結點
- while (j >= 0 && i != 0)
- {
- if (a[j] <= temp)
- break;
- a[i] = a[j]; //把較大的子結點往下移動,替換它的子結點
- i = j;
- j = (i - 1) / 2;
- }
- a[i] = temp;
- }
更簡短的表達為:
- void MinHeapFixup(int a[], int i)
- {
- for (int j = (i - 1) / 2; (j >= 0 && i != 0)&& a[i] > a[j]; i = j, j = (i - 1) / 2)
- Swap(a[i], a[j]);
- }
插入時:
- //在最小堆中加入新的數據nNum
- void MinHeapAddNumber(int a[], int n, int nNum)
- {
- a[n] = nNum;
- MinHeapFixup(a, n);
- }
堆的刪除
按定義,堆中每次都只能刪除第0個數據。為了便於重建堆,實際的操作是將最后一個數據的值賦給根結點,然后再從根結點開始進行一次從上向下的調整。調整時先在左右兒子結點中找最小的,如果父結點比這個最小的子結點還小說明不需要調整了,反之將父結點和它交換后再考慮后面的結點。相當於從根結點將一個數據的“下沉”過程。下面給出代碼:
- // 從i節點開始調整,n為節點總數 從0開始計算 i節點的子節點為 2i+1, 2i+2
- void MinHeapFixdown(int a[], int i, int n)
- {
- int j, temp;
- temp = a[i];
- j = 2 i + 1;
- while (j < n)
- {
- if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的
- j++;
- if (a[j] >= temp)
- break;
- a[i] = a[j]; //把較小的子結點往上移動,替換它的父結點
- i = j;
- j = 2 i + 1;
- }
- a[i] = temp;
- }
- //在最小堆中刪除數
- void MinHeapDeleteNumber(int a[], int n)
- {
- Swap(a[0], a[n - 1]);
- MinHeapFixdown(a, 0, n - 1);
- }
堆化數組
有了堆的插入和刪除后,再考慮下如何對一個數據進行堆化操作。要一個一個的從數組中取出數據來建立堆吧,不用!先看一個數組,如下圖:
很明顯,對葉子結點來說,可以認為它已經是一個合法的堆了即20,60, 65, 4, 49都分別是一個合法的堆。只要從A[4]=50開始向下調整就可以了。然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分別作一次向下調整操作就可以了。下圖展示了這些步驟:
寫出堆化數組的代碼:
- //建立最小堆
- void MakeMinHeap(int a[], int n)
- {
- for (int i = n / 2 - 1; i >= 0; i--)
- MinHeapFixdown(a, i, n);
- }
至此,堆的操作就全部完成了(注1),再來看下如何用堆這種數據結構來進行排序。
堆排序
首先可以看到堆建好之后堆中第0個數據是堆中最小的數據。取出這個數據再執行下堆的刪除操作。這樣堆中第0個數據又是堆中最小的數據,重復上述步驟直至堆中只有一個數據時就直接取出這個數據。
由於堆也是用數組模擬的,故堆化數組后,第一次將A[0]與A[n - 1]交換,再對A[0…n-2]重新恢復堆。第二次將A[0]與A[n – 2]交換,再對A[0…n - 3]重新恢復堆,重復這樣的操作直到A[0]與A[1]交換。由於每次都是將最小的數據並入到后面的有序區間,故操作完成后整個數組就有序了。有點類似於直接選擇排序。
- void MinheapsortTodescendarray(int a[], int n)
- {
- for (int i = n - 1; i >= 1; i--)
- {
- Swap(a[i], a[0]);
- MinHeapFixdown(a, 0, i);
- }
- }
注意使用最小堆排序后是遞減數組,要得到遞增數組,可以使用最大堆。
由於每次重新恢復堆的時間復雜度為O(logN),共N - 1次重新恢復堆操作,再加上前面建立堆時N / 2次向下調整,每次調整時間復雜度也為O(logN)。二次操作時間相加還是O(N * logN)。故堆排序的時間復雜度為O(N * logN)。STL也實現了堆的相關函數,可以參閱《STL系列之四 heap 堆》。
注1 作為一個數據結構,最好用類將其數據和方法封裝起來,這樣即便於操作,也便於理解。此外,除了堆排序要使用堆,另外還有很多場合可以使用堆來方便和高效的處理數據,以后會一一介紹。
-------------------------------------------自己的東西-------------------------------------------
Question ——Kth Largest Element in an Array
Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
For example,
Given [3,2,1,5,6,4] and k = 2, return 5.
Note:
You may assume k is always valid, 1 ≤ k ≤ array's length.
解題思路
這道題就是讓我們在數組中找第k大的數,直接來個排序,從大到小,然后輸出第k個,就結束了? 用快排時間復雜度為O(nlgn)。 題目有這么簡單?
顯然不是這樣的,說明要找一個比這個更快的算法。這其實是一個比較經典的top k的問題。 時間復雜度為O(nlgk)。
具體實現
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 建立節點個數為k的堆,直接用前k個節點
// 然后遍歷剩下的節點,如果比堆的頂點都小的直接舍棄,大的則將堆的頂點用新的節點替換,然后再進行堆的調整
// 所以時間復雜度為 O(nlgk)
// 建立大小為k的堆
for (int i = 0; i < k; i++) {
MinHeapFixup(nums, i);
}
for (int i = k; i < nums.size(); i++) {
if (nums[i] > nums[0]) {
nums[0] = nums[i];
MinHeapFixdown(nums, 0, k);
}
}
return nums[0];
}
// 堆的插入
void MinHeapFixup(vector<int>& nums, int i) {
int j, tmp;
tmp = nums[i];
j = (i - 1) / 2; //父節點
while(j >= 0 && i != 0) {
if (nums[j] < tmp)
break;
nums[i] = nums[j];
i = j;
j = (i - 1) / 2;
}
nums[i] = tmp;
}
// 堆的刪除(堆的調整,從根部開始)
// 堆刪除的實際做法,就是將數組中的最后一個節點和根節點對換,刪除最后一個元素,然后再把堆進行調整。
void MinHeapFixdown(vector<int>&nums, int i, int k) {
int j, tmp;
tmp = nums[i];
j = 2 * i + 1; // 孩子節點
while (j < k) {
if (j + 1 < k && nums[j + 1] < nums[j]) //在左右孩子中找最小的孩子
j++;
if (nums[j] > tmp)
break;
nums[i] = nums[j];
i = j;
j = 2 * i + 1;
}
nums[i] = tmp;
}
};
前面的內容轉載至:http://blog.csdn.net/morewindows/article/details/6709644/