排序算法
相關概念
- 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
- 不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
- 時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
- 空間複雜度:是指算法在計算機
內執行時所需存儲空間的度量,它也是數據規模n的函數。
(一)冒泡排序
冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。
算法描述
- 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
- 針對所有的元素重複以上的步驟,除了最後一個;
- 重複步驟1~3,直到排序完成。
function bubbleSort(arr) {
if (arr === null || arr.length < 2) {
return arr
}
let len = arr.length;
for (let i = 0; i < len - 1; i ++) {
for (let j = i + 1; j < len - i - 1; j ++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
優化:當一次循環沒有發生冒泡,說明已經排序完成,停止循環。
function bubbleSort(arr) {
for (let j = 0; j < arr.length; j++) {
let complete = true;
for (let i = 0; i < arr.length - 1 - j; i++) {
// 比較相鄰數
if (arr[i] > arr[i + 1]) {
[arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
complete = false;
}
}
// 沒有冒泡結束循環
if (complete) {
break;
}
}
return arr;
}
複雜度:
時間複雜度:O(n²)
空間複雜度:O(1)
穩定性:
穩定
(二)選擇排序
選擇排序算法是一種原址比較排序算法。選擇排序算法的思路是:找到數據結構中的最小值並 將其放置在第一位,接着找到第二小的值並將其放在第二位,以此類推。
function selectSort(arr) {
if (arr === null || arr.length < 2) {
return arr
}
let len = arr.length;
for (let i = 0; i < len - 1; i ++) {
let min = i;
for (let j = i + 1; j < len; j ++) {
if (arr[j] < arr[min]) {
min = j;
}
}
let temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
return arr;
}
複雜度:
時間複雜度:O(n²)
空間複雜度:O(1)
穩定性:
不穩定
(三)插入排序
插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。
算法描述
一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:
- 從第一個元素開始,該元素可以認爲已經被排序;
- 取出下一個元素,在已經排序的元素序列中從後向前掃描;
- 如果該元素(已排序)大於新元素,將該元素移到下一位置;
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
- 將新元素插入到該位置後;
- 重複步驟2~5。
function insertSort(arr) {
if (arr === null || arr.length < 2) {
return arr
}
let len = arr.length;
for (let i = 0; i < len; i ++) {
let temp = arr[i];
let k = i - 1;
while (k >= 0 && arr[k] > temp) {
arr[k + 1] = arr[k];
k --;
}
arr[k + 1] = temp;
}
return arr;
}
複雜度:
時間複雜度:O(n²)
空間複雜度:O(1)
穩定性:
穩定
(四)希爾排序
1959年Shell發明,第一個突破O(n²)的排序算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
算法描述
先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,具體算法描述:
- 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列個數k,對序列進行k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。
function shellSort(arr) {
if (arr === null || arr.length < 2) {
return arr;
}
let len = arr.length;
for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < len; i ++) {
let temp = arr[i];
let k = i - gap;
while (k >=0 && arr[k] > temp) {
arr[k + gap] = arr[k];
k = k - gap;
}
arr[k + gap] = temp;
}
}
return arr;
}
複雜度:
時間複雜度:O(n1.3)
空間複雜度:O(1)
穩定性:
不穩定
(五)歸併排序
歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。
算法描述
- 把長度爲n的輸入序列分成兩個長度爲n/2的子序列;
- 對這兩個子序列分別採用歸併排序;
- 將兩個排序好的子序列合併成一個最終的排序序列。
function mergeSort(arr) {
if (arr === null || arr.length < 2) {
return arr;
}
let len = arr.length;
let middle = Math.floor(len / 2);
let left = arr.slice(0, middle);
let right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
let result = [];
while(left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}
while(left.length) {
result.push(left.shift())
}
while(right.length) {
result.push(right.shift())
}
}
複雜度:
時間複雜度:O(nlogn)
空間複雜度:O(n)
穩定性:
穩定
(六) 快速排序
快速排序的基本思想:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據比另一部分的所有數據要小,再按這種方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,使整個數據變成有序序列。
算法描述
快速排序使用分治法來把一個串(list)分爲兩個子串(sub-lists)。具體算法描述如下:
- 選擇一個基準元素
target
(一般選擇第一個數) - 將比
target
小的元素移動到數組左邊,比target
大的元素移動到數組右邊 - 分別對
target
左側和右側的元素進行快速排序
從上面的步驟中我們可以看出,快速排序也利用了分治的思想(將問題分解成一些小問題遞歸求解)
function quickSort(arr) {
if (arr === null || arr.length < 2) {
return arr;
}
const target = arr[0];
const left = [];
const right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < target) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([target], quickSort(right));
}
複雜度:
時間複雜度:平均O(nlogn)
,最壞O(n2)
,實際上大多數情況下小於O(nlogn)
空間複雜度:O(logn)
(遞歸調用消耗)
穩定性:
不穩定
(七)堆排序
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
思想:
創建一個大頂堆,大頂堆的堆頂一定是最大的元素。
交換第一個元素和最後一個元素,讓剩餘的元素繼續調整爲大頂堆。
從後往前以此和第一個元素交換並重新構建,排序完成。
function heapSort(array) {
creatHeap(array);
console.log(array);
// 交換第一個和最後一個元素,然後重新調整大頂堆
for (let i = array.length - 1; i > 0; i--) {
[array[i], array[0]] = [array[0], array[i]];
adjust(array, 0, i);
}
return array;
}
// 構建大頂堆,從第一個非葉子節點開始,進行下沉操作
function creatHeap(array) {
const len = array.length;
const start = parseInt(len / 2) - 1;
for (let i = start; i >= 0; i--) {
adjust(array, i, len);
}
}
// 將第target個元素進行下沉,孩子節點有比他大的就下沉
function adjust(array, target, len) {
for (let i = 2 * target + 1; i < len; i = 2 * i + 1) {
// 找到孩子節點中最大的
if (i + 1 < len && array[i + 1] > array[i]) {
i = i + 1;
}
// 下沉
if (array[i] > array[target]) {
[array[i], array[target]] = [array[target], array[i]]
target = i;
} else {
break;
}
}
}
複雜度:
時間複雜度:O(nlogn)
空間複雜度:O(1)
穩定性:
不穩定