在前一篇文章中,我們完成了泡沫排序和選擇排序,並詳細說明了如何自製迭代方程式,並將其與依賴迭代生成器的方法進行比較。今天,藉著這股勢頭,我們將繼續探索插入排序和希爾排序,這兩種排序演算法同樣在實際應用中非常重要。
插入排序以其簡單和直觀的特性受到青睞,而希爾排序則是一種改進的插入排序,能有效提高排序效率。接下來,我們將分別探討這兩種排序演算法的實作及其視覺化的實作。
延續自系列文C2 讓排序演算法起舞吧!最小單位:華爾滋中使用的架構
插入排序是一種簡單的排序演算法,它的工作原理是將數組分為已排序和未排序兩部分,然後逐步將未排序部分的元素插入到已排序部分的適當位置。這種方法的時間複雜度為 O(n²),但對於小數據集或近乎排序的數據集,它的性能相對較好。
function insertionSort(arr) {
const n = arr.length;
for (let i = 1; i < n; i++) {
let key = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
return arr;
}
同樣地,提供設定和執行的功能,來靈活地控制排序的過程和每個動畫禎執行的次數。
class SortAlgorithm{
insertionSortSetting(columns){
this.i = 1;
this.j = 0;
}
insertionSort(columns){
const len = columns.length;
const i = this.i;
const j = this.j;
if (i < len) {
if (j >= 0 && columns[j].height > columns[j + 1].height) {
const a = columns[j + 1];
const b = columns[j];
SortAlgorithm.swapColumn(a, b, 30);
this.j--;
} else{
this.i++;
this.j = this.i - 1;
}
} else return true;
}
}
相比昨天,這次我有保留排序的巢狀架構,相信較能看出是如何把 for 和 while 修改成 if else 判斷式。
利用生成器來實作插入排序,能在修改幅度最小的前提下,更直觀地表示排序過程。我們可以在排序的每一步進行狀態的暫停與恢復,從而在動畫中更好地展示每次元素的移動。
class SortAlgorithmIterable{
* insertionSortMaker(columns) {
const len = columns.length;
for (let i = 1; i < len; i++) {
let key = columns[i].height;
let j = i - 1;
while (j >= 0 && columns[j].height > key) {
const a = columns[j + 1];
const b = columns[j];
SortAlgorithm.swapColumn(a, b, 30);
yield false;
j--;
}
}
yield true;
}
}
希爾排序是一種改進的插入排序,通過先對元素進行分組來提高排序效率。它使用間隔(gap)來對元素進行分組排序,隨著排序的進行,間隔逐漸縮小,最終實現全數組的排序。希爾排序的時間複雜度取決於間隔的選擇,最佳情況下可達 O(n log n)。
function shellSort(arr) {
const n = arr.length;
let gap = Math.floor(n / 2);
while (gap > 0) {
for (let i = gap; i < n; i++) {
let temp = arr[i];
let j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
gap = Math.floor(gap / 2);
}
return arr;
}
關於間隔,雖然最簡單的方式就是設置為陣列長度的一半,Ciura 提出,可以設置一系列的數字來優化算法:
根據實驗和測試結果,這些 gap 值可以更有效地縮短元素之間的距離,從而提高整體的排序速度。詳細可以參考:
在希爾排序的實作中,我們也封裝了排序的過程,雖然使用了 gap 因此多了一層迴圈,不過前面三個排序演算法都已實現,相信這個也不成問題。先據透,真正的挑戰會是明天的快速排序,要把遞迴改成迭代的方式會相當麻煩!
class SortAlgorithm{
shellSortSetting(columns){
this.gap = Math.floor(columns.length / 2);
this.i = this.gap;
this.j = this.i;
}
shellSort(columns){
const len = columns.length;
const gap = this.gap;
const i = this.i;
const j = this.j;
if(gap > 0){
if(i < len){
const a = columns[j];
const b = columns[j - gap];
if (j >= gap && b.height > a.height) {
const a = columns[j];
const b = columns[j - gap];
SortAlgorithm.swapColumn(a, b, 60);
this.j-= gap;
} else{
this.i++;
this.j = this.i;
}
} else{
this.gap = Math.floor(gap / 2);
this.i = this.gap;
this.j = this.i;
}
} else return;
}
}
有個小細節是範例代碼中用 temp 來暫存被覆蓋的數據,是很常見的優化寫法。相比之下,我們為了讓動畫清楚展示,並不會直接覆蓋,而是用交換的方式,視覺上看起來更加直觀!
生成器就沒什麼好說的了,透過 yield 就能輕鬆實現。當然,對於普通的迭代是這樣,那如果是遞迴呢?這個問題留給大家思考,明天,讓我們見真章!
class SortAlgorithmIterable{
* shellSortMaker(columns) {
const len = columns.length;
let gap = Math.floor(len / 2);
while (gap > 0) {
for (let i = gap; i < len; i++) {
let j = i;
while (j >= gap && columns[j - gap].height > columns[j].height) {
const a = columns[j];
const b = columns[j - gap];
SortAlgorithm.swapColumn(a, b, 60);
yield false;
j -= gap;
}
}
gap = Math.floor(gap / 2);
}
yield true;
}
}
在本篇文章中,我們深入探討了插入排序和希爾排序的實作。透過不同的實作方法,包括傳統的函式、迭代方程式和生成器,我們不僅學會了如何實現這些排序演算法,還能在動畫中直觀地展現每一步的排序過程。這不僅增強了我們對排序演算法的理解,也為未來更複雜的演算法鋪平了道路。
說到明天的快速排序嘛...我想到就頭痛><