對資料結構和演算法的總結和思考(五)--堆排序
本篇分享的內容為堆排序,提到堆排序就不得不提一下堆這個資料結構。 堆實際上是一棵完全二叉樹,因此其任何一非葉節點滿足性質:
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非葉節點的關鍵字不大於或者不小於其左右孩子節點的關鍵字。
堆分為大頂堆和小頂堆,滿足Key[i]>=Key[2i+1]&&key>=key[2i+2]稱為大頂堆,滿足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]稱為小頂堆。由上述性質可知大頂堆的堆頂的關鍵字肯定是所有關鍵字中最大的,小頂堆的堆頂的關鍵字是所有關鍵字中最小的。
如果你不知道完全二叉樹,那可以去搜索一下或者等我後續的文章,我會詳細介紹樹這種資料結構。言歸正傳,既然現在知道了堆,那如何使用js來描述呢。下面我用陣列的結構來實現一個大頂堆:
function heapify(arr, i, len) {
let max = i,//雙親
left = 2 * i + 1, //左孩子
right = 2 * i + 2;// 右孩子
console.log('i: ', i);
console.log('left: ', left);
console.log('right: ' , right);
if (len > left && arr[left] > arr[max]) {//左孩子存在,並且左孩子大於雙親,則最大值為左孩子
max = left;
}
if (len > right && arr[right] > arr[max]) {//右孩子存在,並且右孩子大於雙親,則最大值為右孩子
max = right;
}
if (max !== i) { //如果雙親不是最大值,則雙親和當前的最大值交換,並重新排序比較。
swap(arr, i, max );
heapify(arr, max, len);
}
//經過這樣一輪之後,就構建了一個完整的雙親和孩子結構。
return arr;
}
// 建堆
let len = arr.length;
for (let i = parseInt(len / 2) - 1; i >= 0 ; i --) {
// 從最下面一層,一層一層往上建堆,這樣就保證了上面一層一定比下面一層的左右孩子大
heapify(arr, i, len);
console.log(arr);
}
上面的程式碼我都有詳盡的註釋,有一點必須要注意,就是建大頂堆必須從下往上建,這樣才能保證堆頂為最大值,也就是let i = parseInt(len / 2) - 1;為什麼i要為parseInt(len / 2) - 1;這也是有道理的,因為完全二叉樹的有這麼兩個性質:
性質1:在二叉樹的第i層上至多有2^(i-1)個結點
性質2:深度為k的二叉樹最多有2^k - 1個結點
也就是說最下層元素為總元素個數加一然後除二。具體為什麼,可以從以2為指數的等比數列求和得知,這裡就不一一展開。當然我這裡講的是滿二叉樹的情況,額,滿二叉樹就是完全二叉樹的一種特殊情況。算了,具體概念這裡就不深究了,以後再一一講明。現在建好了大頂堆,那就開始關鍵性的步驟–堆排序。
堆排序的核心思想是:
1)將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆為初始的無序區;
2)將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
3)由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整為新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。
程式碼如下:
for (let j = len - 1; j >= 0; j --) {
swap(arr, 0, j);
heapify(arr, 0, j);
}
是不是很簡單,是不是很清晰,好了,堆排序就是這樣簡單明瞭清晰,程式碼合在一起如下所示:
// var swap = require('./swap.js');
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
count = 0;
function heap(arr) {
// 建堆
let len = arr.length;
for (let i = parseInt(len / 2) - 1; i >= 0 ; i --) {
// 從最下面一層,一層一層往上建堆,這樣就保證了上面一層一定比下面一層的左右孩子大
heapify(arr, i, len);
}
for (let j = len - 1; j >= 0; j --) {
swap(arr, 0, j);
heapify(arr, 0, j);
}
return arr;
}
function heapify(arr, i, len) {
let max = i,//雙親
left = 2 * i + 1, //左孩子
right = 2 * i + 2;// 右孩子
if (len > left && arr[left] > arr[max]) {//左孩子存在,並且左孩子大於雙親,則最大值為左孩子
max = left;
}
if (len > right && arr[right] > arr[max]) {//右孩子存在,並且右孩子大於雙親,則最大值為右孩子
max = right;
}
if (max !== i) { //如果雙親不是最大值,則雙親和當前的最大值交換,並重新排序比較。
swap(arr, i, max);
heapify(arr, max, len);
}
//經過這樣一輪之後,就構建了一個完整的雙親和孩子結構。
return arr;
}
var arr=[91,60,96,13,35,63,324,223,23,44,22,90, 25,45];
console.log(heap(arr));
console.log(count);
堆排序就分享完了,堆排序的難點在建立堆,只要建好堆結構那剩下的排序就簡單明瞭了。好了,今天就到這裡了。預告一下下一篇內容為計數排序,唯一不需要比較的排序哦,thx~