排序算法的執行效率:最好狀況、最壞狀況、平均狀況的時間複雜度javascript
排序算法的內存消耗:空間複雜度html
排序算法的穩定性:若是待排序的序列中存在值相等的元素,通過排序以後,相等元素之間原有的前後順序不變,則稱該排序算法穩定java
穩定排序的好處:git
在真正的軟件開發中,咱們要排序的每每不是單純的整數,而是一組對象,而後按照對象的某個 key 來排序。若是咱們的需求是按 key1 進行排序,當 key1 的值相同時,按 key2 進行排序,不使用穩定排序的解決方案是先對 key1 進行排序,而後遍歷排序以後的值,對每一個 key1 值相同的小區間再進行 key2 排序,實現起來會比較複雜。github
使用穩定排序的方案:先按照 key2 進行排序,排序完成後,使用穩定排序算法按照 key1 從新排序。算法
試想一下對一組學生按年齡進行排序,在年齡相等的狀況下按照身高進行排序。chrome
如下是在 leetcode 上測的各個排序的時間和空間消耗,能夠看出快排不管是在時間仍是空間上都比較優秀,歸併算法要注意優化,直接所有遞歸很容易時間複雜度爆表。編程
flag
的值,則 break
;var sortArray = function(nums) {
const len = nums.length;
// 從後往前遍歷,排過序的就不用再走一遍了
for(let i=len-1; i>=0; i--){
let flag = true;
for(let j=0; j<i; j++){
if(nums[j]>nums[j+1]){
const temp = nums[j+1];
nums[j+1] = nums[j];
nums[j] = temp;
flag = false;
}
}
if(flag) break;
}
return nums;
};
複製代碼
分析:數組
空間複雜度:O(1)
,只涉及相鄰數據的交換操做,只須要常量級的臨時空間,是一個原地排序。瀏覽器
穩定性:穩定。當兩個相鄰的元素大小相等時,不作交換,排序後,其相對位置不變。
時間複雜度:
最好狀況 :徹底有序,一次冒泡,時間複雜度 O(n)
最壞狀況:徹底逆序,n
次冒泡,時間複雜度 O(n2)
平均狀況:n*(n-1)/4 = O(n2)
var sortArray = function(nums) {
const len = nums.length;
// 循環不變量:將 nums[i] 插入到區間 [0, i) 使之成爲有序數組
for(let i=1; i<len; i++){
// 先暫存這個元素,而後以前元素逐個後移,留出空位
const currentVal = nums[i];
let j = i;
// 注意邊界 j > 0
while(j>0 && nums[j-1] > currentVal ){
nums[j] = nums[j-1];
j--;
}
nums[j] = currentVal;
}
return nums;
};
複製代碼
分析:
最好狀況:在數組「幾乎有序」的時,插入排序的時間複雜度能夠達到 O(n)
; 最壞/平均狀況:O(n2)
O(1)
使用到常數個臨時變量,是原地排序var sortArray = function(nums) {
const len = nums.length;
// [0, i) 有序,且該區間裏全部元素就是最終排定的樣子
for(let i=0; i<len-1; i++){
let minIndex = i;
// 從未排序區間找出最小值, 交換到下標i
for(let j=i+1; j<len; j++){
if(nums[j]<nums[minIndex]){
minIndex = j;
}
}
swap(nums, i, minIndex);
}
return nums;
function swap(nums, index1, index2){
const temp = nums[index2];
nums[index2] = nums[index1];
nums[index1] = temp;
}
};
複製代碼
使用到的算法思想:
貪心算法:每一次決策只看當前,當前最優,則全局最優。注意:這種思想不是任什麼時候候都適用。
減治思想:外層循環每一次都能排定一個元素,問題的規模逐漸減小,直到所有解決,即「大而化小,小而化了」。運用「減治思想」很典型的算法就是大名鼎鼎的「二分查找」。
分析:
O(n2)
O(1)
原地排序var sortArray = function(nums) {
mergeSort(nums, 0, nums.length-1);
return nums;
// 對數組 nums 的子區間 [left, right] 進行歸併排序
// temp: 用於合併兩個有序數組的輔助數組,全局使用一份,避免屢次建立和銷燬
function mergeSort(nums, left, right, temp = []){
if(left>=right) return; //遞歸終止條件
// let mid = left + right >>> 1;
let mid = left + Math.floor((right-left) / 2);
// 遞歸左邊和右邊, 使左右兩邊分別有序
mergeSort(nums, left, mid, temp);
mergeSort(nums, mid+1, right, temp);
// 左右兩邊有序以後,對它們進行合併
mergeTwoSortedArray(nums, left, mid, right, temp);
}
// 合併兩個有序數組:先把值複製到臨時數組,再合併回去
function mergeTwoSortedArray(nums, left, mid, right, temp){
temp = [...nums];
let i = left;
let j = mid+1;
for(let k=left; k<=right; k++){
if(i === mid+1){ //i走到了中間,說明左邊已經放完了,以後只需循環把右邊放進去
nums[k] = temp[j];
j++;
}else if(j === right+1){ //j走到了最後,說明右邊已經放完了,以後只需循環把左邊放進去
nums[k] = temp[i];
i++;
}else if(temp[i] <= temp[j]){ // 比較i,j位置的元素,誰小放誰進去,並讓其指針右移
// 注意寫成 < 就丟失了穩定性(相同元素原來靠前的排序之後依然靠前)
nums[k] = temp[i];
i++;
}else{
nums[k] = temp[j];
j++;
}
}
}
};
複製代碼
優化:
// 優化後代碼:
var sortArray = function(nums) {
const INSERTION_SORT_THRESHOLD = 7;
mergeSort(nums, 0, nums.length-1);
return nums;
// 對數組 nums 的子區間 [left, right] 進行歸併排序
function mergeSort(nums, left, right, temp = []){
// 小區間使用插入排序
if(right - left <= INSERTION_SORT_THRESHOLD){
insertSort(nums, left, right);
return;
}
// let mid = left + right >>> 1;
let mid = left + Math.floor((right-left) / 2);
// 遞歸左邊和右邊, 使左右兩邊分別有序
mergeSort(nums, left, mid, temp);
mergeSort(nums, mid+1, right, temp);
// 若是數組的這個子區間自己有序,無需合併
if (nums[mid] <= nums[mid + 1]) {
return;
}
mergeTwoSortedArray(nums, left, mid, right, temp);
}
// 對數組 nums 的子區間 [left, right] 使用插入排序
function insertSort(nums, left, right){
for(let i=left+1; i<=right; i++){
const currentVal = nums[i];
let j = i;
while(j>left && nums[j-1] > currentVal ){
nums[j] = nums[j-1];
j--;
}
nums[j] = currentVal;
}
return nums;
}
// 合併兩個有序數組:先把值複製到臨時數組,再合併回去
function mergeTwoSortedArray(nums, left, mid, right, temp){
temp = [...nums];
let i = left;
let j = mid+1;
for(let k=left; k<=right; k++){
if(i === mid+1){ //i走到了中間,說明左邊已經放完了,以後只需循環把右邊放進去
nums[k] = temp[j];
j++;
}else if(j === right+1){ //j走到了最後,說明右邊已經放完了,以後只需循環把左邊放進去
nums[k] = temp[i];
i++;
}else if(temp[i] <= temp[j]){ // 比較i,j位置的元素,誰小放誰進去,並讓其指針右移
// 注意寫成 < 就丟失了穩定性(相同元素原來靠前的排序之後依然靠前)
nums[k] = temp[i];
i++;
}else{
nums[k] = temp[j];
j++;
}
}
}
};
複製代碼
分析:
O(nlogn)
O(n)
,不是原地排序,由於合併的時候須要額外的空間,輔助數組與輸入數組規模至關。若是咱們不考慮空間消耗的話,partition()
分區函數能夠寫得⾮常簡單。申請兩個臨時數組 X
和 Y
,遍歷給定數組 nums
,將⼩於 pivot
的元素都拷⻉到臨時數組 X
,將⼤於 pivot
的元素都拷⻉到臨時數組 Y
,最後再將數組 X
和數組 Y
中數據順序拷⻉到 nums
中。具體寫法能夠參考:阮一峯老師的快排實現
可是,若是按照這種思路實現的話,partition()
函數就須要不少額外的內存空間,因此快排就不是原地排序算法了。若是 咱們但願快排是原地排序算法,那它的空間複雜度得是 O(1)
,那 partition()
分區函數就不能佔⽤太多額外的內存空間,咱們就須要在 nums
數組的原地完成分區操做。
使用 單指針 & 交換
完成分區操做的快排:
var sortArray = function(nums) {
quickSort(nums, 0, nums.length-1)
return nums;
function quickSort(nums, left, right){
if(left>=right) return;
// 將nums分區,使得nums[pIndex]左邊的都小於nums[pIndex], 右邊的都大於nums[pIndex]
let pIndex = partition(nums, left, right);
// 對基準值左右兩邊遞歸的分區(排序)
quickSort(nums, left, pIndex-1);
quickSort(nums, pIndex+1, right);
}
// 分區函數
function partition(nums, left, right){
// 取中間的值做爲基準值
let mid = left + Math.floor((right-left) / 2);
swap(nums, left, mid); // 把基準值交換到第一項
// 基準值
let pivot = nums[left];
let lt = left; // 開拓小於基準值的區間的指針
// 循環不變量:
// all in [left + 1, lt] < pivot
// all in [lt + 1, i) >= pivot
for(let i=left+1; i<=right; i++){
if(nums[i]<pivot){
lt++;
swap(nums, i, lt)
}
}
swap(nums, left, lt); //將基準值換到中間
return lt;
}
function swap(nums, index1, index2){
const temp = nums[index2];
nums[index2] = nums[index1];
nums[index1] = temp;
}
};
複製代碼
注意事項:
pivot
選的是 left
或者 right
,快速排序會變得很是慢(等同於冒泡排序或者選擇排序), 時間複雜度退化到O(n2)
之因此解法有這些優化,原由都是來自「遞歸樹」的高度。關於「樹」的算法的優化,絕大部分都是在和樹的「高度」較勁。相似的經過減小樹高度、使得樹更平衡的數據結構還有「二叉搜索樹」優化成「AVL 樹」或者「紅黑樹」、「並查集」的「按秩合併」與「路徑壓縮」。
分析:
O(nlogn)
O(1)
問題:JavaScript
數組的原生 sort
排序穩定嗎?
不一樣瀏覽器的js引擎對sort的實現方式不同,這裏以 chrome V8
來講,
V8 引擎的 sort
實現:對於長度 <= 10 的數組使用 插入排序,比 10 大的數組則使用 原地快速排序。因此當數組長度小於 10 時是穩定的,而當數組長度大於 10 時,sort
是不穩定的。
V8 源碼 (710行開始)