在計算機科學所使用的排序算法通常被分類為:
- 計算的 時間復雜度(最差、平均、和最好性能),依據列表(list)的大小(n)。一般而言,好的性能是O(n log n),且壞的性能是O(n^2)。對於一個排序理想的性能是O(n)。僅使用一個抽象關鍵比較運算的排序算法總平均上總是至少需要O(n log n)。
- 存儲器使用量(以及其他電腦資源的使用)
- 穩定性:穩定排序算法會讓原本有相等鍵值的紀錄維持相對次序。也就是如果一個排序算法是穩定的,當有兩個相等鍵值的紀錄R和S,且在原本的列表中R出現在S之前,在排序過的列表中R也將會是在S之前。
- 依據排序的方法:插入、交換、選擇、合並等等。
依據排序的方法分類的三種排序算法:
冒泡排序
冒泡排序對一個需要進行排序的數組進行以下操作:
- 比較第一項和第二項;
- 如果第一項應該排在第二項之后, 那么兩者交換順序;
- 比較第二項和第三項;
- 如果第二項應該排在第三項之后, 那么兩者交換順序;
- 以此類推直到完成排序;
實例說明:
將數組[3, 2, 4, 5, 1]以從小到大的順序進行排序:
- 3應該在2之后, 因此交換, 得到[2, 3, 4, 5, 1];
- 3, 4順序不變, 4, 5也不變, 交換5, 1得到[2, 3, 4, 1, 5];
- 第一次遍歷結束, 數組中最后一項處於正確位置不會再有變化, 因此下一次遍歷可以排除最后一項;
- 開始第二次遍歷, 最后結果為[2, 3, 1, 4, 5], 排除后兩項進行下一次遍歷;
- 第三次遍歷結果為[2, 1, 3, 4, 5];
- 最后得到[1, 2, 3, 4, 5], 排序結束;
代碼實現:
function swap(items, firstIndex, secondIndex){ var temp = items[firstIndex]; items[firstIndex] = items[secondIndex]; items[secondIndex] = temp; }; function bubbleSort(items){ var len = items.length, i, j, stop; for (i = 0; i < len; i++){ for (j = 0, stop = len-i; j < stop; j++){ if (items[j] > items[j+1]){ swap(items, j, j+1); } } } return items; }
外層的循環決定需要進行多少次遍歷, 內層的循環負責數組內各項的比較, 還通過外層循環的次數和數組長度決定何時停止比較.
冒泡排序極其低效, 因為處理數據的步驟太多, 對於數組中的每n
項, 都需要n^2
次操作來實現該算法(實際比n^2略小, 但可以忽略, 具體原因見⤵️), 即時間復雜度為O(n^2)
.
對於含有n個元素的數組, 需要進行
(n-1)+(n-2)+...+1
次操作, 而(n-1)+(n-2)+...+1 = n(n-1)/2 = n^2/2 - n/2
, 如果n趨於無限大, 那么n/2的大小對於整個算式的結果影響可以忽略, 因此最終的時間復雜度用O(n^2)
表示
選擇排序
選擇排序對一個需要進行排序的數組進行以下操作:
- 假定數組中的第一項為最小值(min);
- 比較第一項和第二項的值;
- 若第二項比第一項小, 則假定第二項為最小值;
- 以此類推直到排序完成.
實例說明:
將數組["b", "a", "d", "c", "e"]以字母a-z的順序進行排序:
- 假定數組中第一項"b"(index0)為min;
- 比較第二項"a"與第一項"b", 因"a"應在"b"之前的順序, 故"a"(index1)為min;
- 然后將min與后面幾項比較, 由於"a"就是最小值, 因此min確定在index1的位置;
- 第一次遍歷結束后, 將假定的min(index0), 與真實的min(index1)進行比較, 真實的min應該在index0的位置, 因此將兩者交換, 第一次遍歷交換之后的結果為["a", "b", "d", "c", "e"];
- 然后開始第二次遍歷, 遍歷從第二項(index1的位置)開始, 這次假定第二項為最小值, 將第二項與之后幾項逐個比較, 因為"b"就在應該存在的位置, 所以不需要進行交換, 這次遍歷之后的結果為"a", "b", "d", "c", "e"];
- 之后開始第三次遍歷, "c"應為這次遍歷的最小值, 交換index2("d"), index3("c")位置, 最后結果為["a", "b", "c", "d", "e"];
- 最后一次遍歷, 所有元素在應有位置, 不需要進行交換.
代碼實現:
function swap(items, firstIndex, secondIndex){ var temp = items[firstIndex]; items[firstIndex] = items[secondIndex]; items[secondIndex] = temp; }; function selectionSort(){ let items = [...document.querySelectorAll('.num-queue span')].map(num => +num.textContent); let len = items.length, min; for (i = 0; i < len; i++){ min = i; for(j = i + 1; j < len; j++){ if(items[j] < items[min]){ min = j; } } if(i != min){ swap(items, i, min); } } return items; };
外層循環決定每次遍歷的初始位置, 從數組的第一項開始直到最后一項. 內層循環決定哪一項元素被比較.
選擇排序的時間復雜度為O(n^2)
.
插入排序
與上述兩種排序算法不同, 插入排序是穩定排序算法(stable sort algorithm), 穩定排序算法指不改變列表中相同元素的位置, 冒泡排序和選擇排序不是穩定排序算法, 因為排序過程中有可能會改變相同元素位置. 對簡單的值(數字或字符串)排序時, 相同元素位置改變與否影響不是很大. 而當列表中的元素是對象, 根據對象的某個屬性對列表進行排序時, 使用穩定排序算法就很有必要了.
一旦算法包含交換(swap)這個步驟, 就不可能是穩定的排序算法. 列表內元素不斷交換, 無法保證先前的元素排列為止一直保持原樣. 而插入排序的實現過程不包含交換, 而是提取某個元素將其插入數組中正確位置.
插入排序的實現是將一個數組分為兩個部分, 一部分排序完成, 一部分未進行排序. 初始狀態下整個數組屬於未排序部分, 排序完成部分為空. 然后進行排序, 數組內的第一項被加入排序完成部分, 由於只有一項, 自然屬於排序完成狀態. 然后對未完成排序的余下部分的元素進行如下操作:
- 如果這一項的值應該在排序完成部分最后一項元素之后, 保留這一項在原有位置開始下一步;
- 如果這一項的值應該排在排序完成部分最后一項元素之前, 將這一項從未完成部分暫時移開, 將已完成部分的最后一項元素移后一個位置;
- 被暫時移開的元素與已完成部分倒數第二項元素進行比較;
- 如果被移除元素的值在最后一項與倒數第二項的值之間, 那么將其插入兩者之間的位置, 否則繼續與前面的元素比較, 將暫移出的元素放置已完成部分合適位置. 以此類推直到所有元素都被移至排序完成部分.
實例說明:
現在需要將數組var items = [5, 2, 6, 1, 3, 9];
進行插入排序:
- 5屬於已完成部分, 余下元素為未完成部分. 接下來提取出2, 因為5比2大, 於是5被移至靠右一個位置, 覆蓋2, 占用2原本存在的位置. 這樣本來存放5的位置(已完成部分的首個位置)就被空出, 而2在比5小, 因此將2置於這個位置, 此時結果為[2, 5, 6, 1, 3, 9];
- 接下來提取出6, 因為6比5大, 所以不操作提取出1, 1與已完成部分各個元素(2, 5, 6)進行比較, 應該在2之前, 因此2, 5, 6各向右移一位, 1置於已完成部分首位, 此時結果為[1, 2, 5, 6, 3, 9];
- 對余下未完成元素進行類似操作, 最后得出結果[1, 2, 3, 5, 6, 9];
代碼實現:
function insertionSort(items) { let len = items.length, value, i, j; for (i = 0; i < len; i++) { value = items[i]; for (j = i-1; j > -1 && items[j] > value; j--) { items[j+1] = items[j]; } items[j+1] = value; } return items; };
外層循環的遍歷順序是從數組的第一位到最后一位, 內層循環的遍歷則是從后往前, 內層循環同時負責元素的移位.
插入排序的時間復雜度為O(n^2)
以上三種排序算法都十分低效, 因此實際應用中不要使用這三種算法, 遇到需要排序的問題, 應該首先使用JavaScript內置的方法Array.prototype.sort()
;