演算法分類
十種常見排序演算法可以分為兩大類:
非線性時間比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此稱為非線性時間比較類排序。
線性時間非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間執行,因此稱為線性時間非比較類排序。
演算法複雜度
相關概念
穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
時間複雜度:對排序資料的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
空間複雜度:是指演算法在計算機內執行時所需儲存空間的度量,它也是資料規模n的函式。
氣泡排序(Bubble Sort)
原理:
比較兩個相鄰的元素,將值大的元素交換到右邊
演算法描述
- 比較第一位與第二位,如果第一位比第二位大,則交換位置
- 繼續比較後面的數,按照同樣的方法進行比較,到最後一位的時候,最大的數將被排在最後一位
- 重複進行比較,直到排序完成,注意由於上一次排序使得最後一位已經是最大的數,所以每次排序結束之後,下一次比較的時候可以相應的減少比較數量
動圖演示
程式碼解析
let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
let flag;
let len = arr.length;
let num1 = 0; // 比較的次數
let num2 = 0; // 交換的次數
// 有15個數,只需要選出14個最大的數,最後一個數就是最小的,不用進行比較
for(let i = 0; i < len -1 ; i++){
// 每次i變化之後最大的值已經排序到最後一位,無需對最後一位進行比較,所以j的最大值為len-i-1
for(let j = 0; j < len - i - 1; j++){
num1 += 1;
// 如果當前位置的數比下一位置的數大,則交換位置
if(arr[j] > arr[j+1]){
num2 =+ 1;
flag = arr[j];
arr[j] = arr[j+1];
arr[j+1] = flag
}
}
}
console.log(arr,num);
複製程式碼
輸出的結果為:
計數器num1和num2的值分別為:105和46
從程式碼中看出,排序過程中,所需要的臨時變數一直都沒有變化,因此空間複雜度為O(1);程式碼進行了兩次for迴圈且是巢狀迴圈,因此時間複雜度為O(n²)。
氣泡排序的最優情況是原陣列預設正序排序,此時比較的次數num1仍為105,而交換次數num2為0,此時的時間複雜度仍然為O(n²),那麼為什麼前面的複雜度表格中說是O(n)呢?經過一番研究發現,需要對上述程式碼進行簡單優化。
如果排序的陣列是:[1,2,3,4,5],此時完全符合最優複雜度情況,當我們進行第一次迴圈發現,兩兩相鄰的資料一次都沒有進行交換,也就是說所有的數都比前一個數大,此時就是正序,無需再進行下次排序,所以我們只需要加上一個變數進行判斷:
// 初始未產生交換
let isSwap = false;
for(let i = 0; i < len -1 ; i++){
// 每次i變化之後最大的值已經排序到最後一位,無需對最後一位進行比較,所以j的最大值為len-i-1
for(let j = 0; j < len - i - 1; j++){
num1 += 1;
// 如果當前位置的數比下一位置的數大,則交換位置
if(arr[j] > arr[j+1]){
num2 =+ 1;
isSwap = true;
flag = arr[j];
arr[j] = arr[j+1];
arr[j+1] = flag
}
}
// 如果產生交換,直接結束迴圈
if(!isSwap){
return
}
}
複製程式碼
當產生交換時,isSwap變成true,第一次迴圈結束之後,如果isSwap如果還是false表示未經過交換,陣列已經是正序,無需繼續排序,此時的時間複雜度為O(n)
在比較過程中只判斷了大於後一個數,如果兩個數相等無需交換,所以氣泡排序是穩定的排序。
選擇排序(Selection Sort)
原理
將序列分為未排序和已排序,從未排序序列中找到最小的數,放到無序序列起始位置,然後繼續從剩餘未排序序列中繼續尋找最小值
演算法描述
- 初始無序序列為待排序序列,有序序列為空
- 從無序序列中找到最小值,放到無序序列起始位置,也就是和起始位置交換
- 將無序序列起始位向後推一個位置,繼續2步驟
動圖演示
程式碼解析
// 選擇出無序序列中最小的值放到無序第一位
let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
let len = arr.length;
let minIndex;
let flag;
let num1 = 0; // 比較次數
let num2 = 0; // 交換次數
for(let i = 0; i < len - 1;i++){
// 每次選擇最小值之後,無序區的開始位置往後推1
minIndex = i;
// j迴圈到最後一位,選擇出當前無序陣列中數值最小的索引值
for(let j = i + 1; j < len;j++){
num1 += 1;
if(arr[minIndex]>arr[j]){
minIndex = j;
}
}
num2 += 1;
flag = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = flag;
}
console.log(arr,num1,num2)
複製程式碼
輸出結果為:
計數器num1和num2的值分別為:105和14
也就是說,選擇排序的比較次數和氣泡排序未優化時一樣,而交換的次數只有14次
再舉剛剛的栗子:[1,2,3,4,5],正序序列,無需進行任何交換,我們對最後交換程式碼進行優化:
if(minIndex == i){
num2 += 1;
flag = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = flag;
}
複製程式碼
此時沒有發生資料交換。
其實從程式碼中可以看到,無論如何,選擇排序總會經過N^2/2
次比較,而受原始數列影響,交換的次數最大為n-1,最小次數為0。
因此選擇排序時間複雜度總為:O(n平方),空間複雜度為:O(1)
選擇排序是不穩定的,為什麼這麼說,看個栗子:[5,3,8,5,2],好了不說也能看出來了。
插入排序(Insertion Sort)
原理
構建有序序列,對於未排序的資料,從有序序列後向前掃描,找到相應位置插入
動圖演示
程式碼實現
let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
let len = arr.length;
// 定義當前未排序資料起始位置值也就是即將插入的資料
let currentValue;
// 有序序列遍歷位置
let preIndex;
let num1 = 0; // 比較次數
let num2 = 0; // 交換次數
for(let i = 1;i < len; i++){
// 定義原始資料第二位為未排序資料第一位,預設原始資料第一位已排序
currentValue = arr[i];
// 當前有序序列最大索引
preIndex = i - 1;
// 當索引大於等於0且當前索引值大於需要插入的資料時
for(let j = preIndex;j>=0;j--){
// 第一次比較,如果有序序列最大索引值其實就是待插入資料前一位,比待插入資料大,則後移一位
num1+=1;
if(arr[preIndex]>currentValue){
arr[preIndex+1] = arr[preIndex];
preIndex --;
// 索引減1,繼續向前比較
}
}
// 當出現索引所在位置值比待插入資料小時,將待插入資料插入
// 為什麼是preIndex+1,因為在while迴圈裡面後移一位之後,當前索引已經變化
num2 += 1;
arr[preIndex+1] = currentValue;
}
console.log(...arr,num1,num2)
複製程式碼
輸出的結果為:
計數器num1和num2的值分別為:105和14
插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。
在時間複雜度上,如果按照上述寫法,執行的次數依次為,1,2,3....n-1,因此時間複雜度為O(n²)
氣泡排序出現了優化之後最優複雜度變小的情況,看看前面的表格,插入排序的最優時間複雜度為O(n),那麼這又是什麼原因呢?
我們看看判斷待插入值是否小於當前索引位置值的地方,用了一個for迴圈和一個if,我們能不能改寫一下呢?
while(preIndex>=0&&arr[preIndex]>currentValue){
arr[preIndex+1] = arr[preIndex];
preIndex --;
}
複製程式碼
這樣看,如果在正序情況下:[1,2,3,4,5],每次比較都不會進入while迴圈,因此只執行了n-1次比較操作,因此此時時間複雜度為O(n),那麼最壞複雜度其實也就是逆序情況了,需要執行1,2,3...n次,因此最壞時間複雜度為O(n²)。
總結
綜合比較一下最簡單的三種排序方法:
選擇排序在氣泡排序上做了優化,氣泡排序兩兩比較每一輪選出一個最大值,而選擇排序則從序列中直接選擇出最小值插入無序序列首部(進行交換),相對於氣泡排序減少了不必要的換位操作。
插入排序在思想上和選擇排序差不多,選擇排序是從無序序列中找出最小值,與無序序列的首位進行交換,從而生成一個有序序列,而插入排序則從無序序列中直接選出首位,將首位與有序序列進行比較,插入相應的位置。
使用場景
對於一般工作來說,這三種使用沒什麼體驗上的差距
如果非要選擇的話,插入排序比選擇排序少一些比較的次數,但選擇排序有時候比插入排序少挪動次數,建議資料較大時用插入排序,資料量較小時可以用選擇排序。(其實這倆差不多--)
參考
新手上路請多指教,如有錯誤,輕噴