摘要
堆排序需要用到一種數據結構,大頂堆。大頂堆是一種二叉樹結構,本質是父節點的數大於它的左右子節點的數,左右子節點的大小順序不限制,也就是根節點是最大的值。
這里就是不斷的將大頂堆的根節點的元素和尾部元素交換,交換到大頂堆沒有可以被交換的元素為止。后面再說大頂堆的邏輯。
邏輯
首先將序列通過大頂堆排序。然后不斷的從堆中取出頂部元素放在尾部,直到大頂堆元素為空。
流程
- 對序列進行原地建堆操作
- 重復下面操作,直到堆元素數量為 1
- 交換堆頂元素與尾元素
- 堆的元素數量減 1
- 對 0 位置進行 1 次 自下而上的下濾
下面在代碼中解釋原地建堆和自下而上的下濾這兩個詞的邏輯。
實現
首先進行原地建堆。原地建堆是先將序列按照大頂堆的排序邏輯處理序列。
大頂堆的序列邏輯是父節點的值大於它的左右子節點的值,可以想象成一個二叉樹。這里的原地排序用到了siftDown
方法,而且在循環中只循環到序列一半數量,為什么?這個在下面看siftDown
方法時詳細探究一下。
// 原地建堆
// 自下而上的下濾
heapSize = array.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
交換堆頂和尾部元素,然后將需要比較的序列元素數量減少1,並將要進行比較的序列再使用siftDown
方法過濾,保持序列的大頂堆的性質。然后繼續開始的交換,直到可以比較的序列數量為 1 就截止。
while (heapSize > 1) {
// 交換堆頂元素和尾部元素
swap(0, --heapSize);
// 對 0 位置進行 siftDown(恢復堆的性質)
siftDown(0);
}
大頂堆的 siftDown
方法
這里來探究一下siftDown
(下濾)。
二叉樹的父節點和子節點的關系符合這樣的公式
- leftChilder = partner * 2 + 1
- rightChilder = parnter * 2 + 1 + 1
- half (葉子)節點的數量是總節點數量的 1/2
siftDown
方法主要是將 index
位置上的元素放在合適的位置上。那么什么位置是合適的位置呢?
依據大頂堆的父節點值大於左右子節點的值的性質來看,只要是保證 index
位置的元素大於它的左右子節點就好。
看下面代碼,如果 index < half
才進行循環比較,那么就有一個問題,當 index >= half
為什么不用比較?
這就要提到很巧妙的點,首先看大頂堆的性質,左右子節點沒有具體順序的要求,其次子節點的值小於父節點。那么就可以依據二叉樹的葉子節點性質,如果index
的位置是在葉子節點位置,那么就本來比它的父節點要小,就不用比較(這個是建立在序列本來符合大頂堆的順序,出現一個位置的元素有變化時進行的過濾處理)。
這也是上面的原地排序中,只從一半的位置開始,是因為從這個位置開始,肯定會給它的子節點比較,過濾出大的,並放在合適位置。
代碼中有三個巧妙的點
- 循環從序列的一半位置開始比較,如果位置不在前半部分,就不進行比較,這個在上面分析過
- 在比較的時候,獲取到它左右子節點中最大的節點比較。在獲取右子節點的時候看右子節點是否存在
rightIndex<heapSize
。因為大頂堆是符合完全二叉樹的(盡量往左子樹安排元素)。 - 說是二叉樹,但是沒有實際的節點,還是一個線性序列,通過公式來獲取左右子樹的位置,這個就是心中有樹,沒有樹也是樹
/*
* 讓 index 位置的元素下濾
*/
private void siftDown(int index) {
E element = array[index];
int half = heapSize >> 1; // 取出非葉子節點
// 第一個葉子結點的索引 == 非葉子節點的數量
// 必須保證 index 是非葉子節點
while (index < half) {
// index 的節點有2種情況
// 1、只有左子節點
// 2、同時有左右子節點
// 默認左子節點跟它進行比較
int childIndex = (index << 1) + 1;
E child = array[childIndex];
// 右子節點
int rightIndex = childIndex + 1;
if (rightIndex < heapSize && cmp(array[rightIndex], child) > 0) {
child = array[ childIndex = rightIndex];
}
if (cmp(child, element) < 0) break;
// 將子節點存放到index位置
array[index] = child;
// 重新設置 index
index = childIndex;
}
array[index] = element;
}
時間和空間復雜度
- 最好、平均時間復雜度:O(nlogn)
- 最壞時間復雜度:O((nlogn)
- 空間復雜度:O(1)
- 屬於不穩定排序
題外話
這次的排序用到了二叉樹和大頂堆的一些知識,可能看下來有諸多疑問,這里就先請諸位看官有個印象,后續我會分享二叉樹的知識,然后在回過頭來看堆排序,會讓你思路大開。