算法是什么?、
算法(Algorithm)?代表著用系統的方法描述解決問題的策略機制,可以通過一定規范的?輸入,在有限時間內獲得所需要的?輸出。
一個算法的好壞是通過?時間復雜度?與?空間復雜度?來衡量的。
簡單來說,時間復雜度?就是執行算法的?時間成本?,空間復雜度?則是執行算法的?空間成本?。
時間復雜度?與?空間復雜度?都是用?“大O”?來表示,寫作?O(*)。有一點值得注意的是,我們談論復雜度,一般談論的都是時間復雜度。
常見時間復雜度的?“大O表示法”?描述有以下幾種:
時間復雜度 | 非正式術語 |
---|---|
O(1) | 常數階 |
O(n) | 線性階 |
O(n2) | 平方階 |
O(log n) | 對數階 |
O(n log n) | 線性對數階 |
O(n3) | 立方階 |
O(2n) | 指數階 |
一個算法在N規模下所消耗的時間消耗從大到小如下:
O(1) < O(log n) < O(n) < O(n log n) < O(n2) < O(n3) < O(2n)
常見的排序算法
1.O(n2)?的排序算法
-
冒泡排序
-
選擇排序
-
插入排序
-
希爾排序
2.O(n log n)?的排序算法
-
歸并排序
-
快速排序
-
堆排序
3.線性的排序算法
-
計數排序
-
桶排序
-
基數排序
冒泡排序
冒泡排序之所以叫冒泡排序,是因為它每一種元素都像小氣泡一樣根據自身大小一點一點往數組的一側移動。
算法步驟如下:
-
比較相鄰的元素。如果第一個比第二個大,就交換他們兩個;
-
對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。這步做完后,最后的元素會是最大的數;
-
針對所有的元素重復以上的步驟,除了最后一個;
-
持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
const?bubbleSort?=?arr?=>?{
????const?len?=?arr.length?-?1
????for?(let?i?=?0;?i?<?len;?++i)?{?/*?外循環為排序趟數,len個數進行len-1趟?*/
????????for?(let?j?=?0;?j?<?len?-?i;?++j)?{?/*?內循環為每趟比較的次數,第i趟比較len-i次?*/
????????????if?(arr[j]?>?arr[j?+?1])?{?/*?相鄰元素比較,若逆序則交換(升序為左大于右,逆序反之)?*/
????????????????[arr[j],?arr[j?+?1]]?=?[arr[j?+?1],?arr[j]]
????????????}
????????}
????}
????return?arr
}
選擇排序(Selection sort)?是一種簡單直觀的排序算法。
選擇排序的主要優點與數據移動有關。
如果某個元素位于正確的最終位置上,則它不會被移動。
選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對?n?個元素的表進行排序總共進行至多?n - 1?次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬于非常好的一種。
選擇排序的算法步驟如下:
-
在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
-
然后,再從剩余未排序元素中繼續尋找最小(大)元素,然后放到已排序序列的末尾;
-
以此類推,直到所有元素均排序完畢。
代碼實現:
? ??const?selectionSort?=?arr?=>?{
????const?len?=?arr.length
????let?min
????for?(let?i?=?0;?i?<?len?-?1;?++i)?{
????????min?=?i?/*?初始化未排序序列中最小數據數組下標?*/
????????for?(let?j?=?i?+?1;?j?<?len;?++j)?{?/*?訪問未排序的元素?*/
????????????if?(arr[j]?<?arr[min])?{?/*?找到目前最小值?*/
????????????????min?=?j?/*?記錄最小值?*/
????????????}
????????}
????????[arr[i],?arr[min]]?=?[arr[min],?arr[i]]?/*?交換位置?*/
????}
????return?arr
}
插入排序
插入排序(Selection sort)?是一種簡單直觀的排序算法。
它的工作原理是通過構建有序序列,對于未排序數據,在已排序序列中從后向前掃描,找到相應位置并插入。
插入排序的算法步驟如下:
-
從第一個元素開始,該元素可以認為已經被排序;
-
取出下一個元素,在已經排序的元素序列中從后向前掃描;
-
如果該元素(已排序)大于新元素,將該元素移到下一位置;
-
重復步驟3,直到找到已排序的元素小于或者等于新元素的位置;
-
將新元素插入到該位置后;
-
重復步驟2~5。
const?insertionSort?=?arr?=>?{
????const?len?=?arr.length
????let?j,?temp
????for?(let?i?=?0;?i?<?len;?++i)?{
????????j?=?i?/*?存儲當前索引,便于后續與數組其他元素對比?*/
????????while?(j?>?0?&&?arr[j?-?1]?>?temp)?{
????????????arr[j]?=?arr[j?-?1]
????????????j--
????????}
????????[arr[j],?arr[i]]?=?[arr[i],?arr[i]]
????}
????return?arr
}
希爾排序
希爾排序,也稱?遞減增量排序算法,是?插入排序?的一種更高效的改進版本。希爾排序是非穩定排序算法。
希爾排序是基于插入排序的以下兩點性質而提出改進方法的:
-
插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到?線性排序?的效率;
-
但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位。
步長的選擇是希爾排序的重要部分。
只要最終步長為1任何步長序列都可以工作。
算法最開始以一定的步長進行排序。
然后會繼續以一定步長進行排序,最終算法以步長為1進行排序。
當步長為1時,算法變為普通插入排序,這就保證了數據一定會被排序。
插入排序的算法步驟如下:
-
定義一個用來分割的步長;
-
按步長的長度K,對數組進行K趟排序;
-
不斷重復上述步驟。
const?shellSort?=?arr?=>?{
????let?gaps?=?[5,?3,?1]?//?定義步長以及分割次數
????let?len?=?arr.length
????for?(let?g?=?0,?gLen?=?gaps.length;?g?<?gaps.length;?++g)?{
????????for?(let?i?=?gaps[g];?i?<?len;?++i)?{
????????????let?j
????????????for?(j?=?i;?j?>=?gaps[g]?&&?arr[j?-?gaps[g]]?>?arr[i];?j?-=?gaps[g])?{
????????????????arr[j]?=?arr[j?-?gaps[g]]
????????????}
????????????[arr[i],?arr[j]]?=?[arr[j],?arr[i]]
????????}
????}
????return?arr
}
快速排序
快速排序(Quicksort),又稱?劃分交換排序(partition-exchange sort)?。
快速排序(Quicksort)?在平均狀況下,排序?n?個項目要?O(n log n)?次比較。在最壞狀況下則需要?O(n2)?次比較,但這種狀況并不常見。事實上,快速排序?O(n log n)?通常明顯比其他算法更快,因為它的?內部循環(inner loop)?可以在大部分的架構上很有效率地達成。
快速排序使用?分治法(Divide and conquer)?策略來把一個序列分為較小和較大的2個子序列,然后遞歸地排序兩個子序列。
快速排序的算法步驟如下:
-
挑選基準值:從數列中挑出一個元素,稱為?“基準”(pivot)?;
-
分割:重新排序序列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準后面(與基準值相等的數可以到任何一邊)。在這個分割結束之后,對基準值的排序就已經完成;
-
遞歸排序子序列:遞歸地將小于基準值元素的子序列和大于基準值元素的子序列排序。
遞歸到最底部的判斷條件是序列的大小是零或一,此時該數列顯然已經有序。
選取基準值有數種具體方法,此選取方法對排序的時間性能有決定性影響。
const?quickSort?=?arr?=>?{
????const?len?=?arr.length
????if?(len?<?2)?{
????????return?arr
????}
????const?pivot?=?arr[0]
????const?left?=?[]
????const?right?=?[]
????for?(let?i?=?1;?i?<?len;?++i)?{
????????if?(arr[i]?>=?pivot)?{
????????????right.push(arr[i])
????????}
????????if?(arr[i]?<?pivot)?{
????????????left.push(arr[i])
????????}
????}
????return?[...quickSort(left),?pivot,?...quickSort(right)]
}
三路快排
const?quickSort?=?arr?=>?{
????const?len?=?arr.length
????if?(len?<?2)?{
????????return?arr
????}
????let?left?=?[]
????let?center?=?[]
????let?right?=?[]
????let?pivot?=?arr[0]
????for?(let?i?=?0;?i?<?len;?++i)?{??????
????????if?(arr[i]?<?pivot)?{
????????????left.push(arr[i])
????????}?else?if?(arr[i]?===?pivot)?{
????????????center.push(arr[i])
????????}?else?{
????????????right.push(arr[i])
????????}
????}
????return?[...quickSort(left),?...center,?...quickSort(right)]
}
歸并排序
第一種是?自上而下的遞歸?,算法步驟如下:
-
申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合并后的序列;
-
設定兩個指針,最初位置分別為兩個已經排序序列的起始位置;
-
比較兩個指針所指向的元素,選擇相對小的元素放入到合并空間,并移動指針到下一位置;
-
重復步驟3直到某一指針到達序列尾;
-
將另一序列剩下的所有元素直接復制到合并序列尾。
具體實現:
?
?
?
?
?
?
?
?
?
?
const?merge?=?(left,?right)?=>?{
????let?resArr?=?[]
????while?(left.length?&&?right.length)?{
????????if?(left[0]?<?right[0])?{
????????????resArr.push(left.shift())
????????}?else?{
????????????resArr.push(right.shift())
????????}
????}
????return?resArr.concat(left,?right)
}
const?mergeSort?=?arr?=>?{
????if?(arr.length?<=?1)?{
????????return?arr
????}
????let?middle?=?Math.floor(arr.length?/?2)
????let?left?=?arr.slice(0,?middle)
????let?right?=?arr.slice(middle)
????return?merge(mergeSort(left),?mergeSort(right))
}
自下而上的迭代?,由于?分治法?的具體算法基本都能用?遞歸?跟?迭代?來實現,所有才有這種寫法,其主要步驟如下:
-
將序列每相鄰兩個數字進行?歸并操作?,形成?ceil(n / 2)?個序列,排序后每個序列包含兩/一個元素;
-
若此時序列數不是1個則將上述序列再次歸并,形成?ceil(n / 4)??個序列,每個序列包含四/三個元素;
-
重復步驟2,直到所有元素排序完畢,即序列數為1。
具體實現如下:
const?merge?=?(arr,?startLeft,?stopLeft,?startRight,?stopRight)?=>?{
????/*?建立左右子序列?*/
????let?rightArr?=?new?Array(stopRight?-?startRight?+?1)
????let?leftArr?=?new?Array(stopLeft?-?startLeft?+?1)
????/*?給左右序列排序?*/
????let?k?=?startRight
????for?(let?i?=?0,?len?=?rightArr.length;?i?<?len?-?1;?++i)?{
????????rightArr[i]?=?arr[k]
????????++k
????}
????k?=?startLeft
????for?(let?i?=?0,?len?=?leftArr.length;?i?<?len?-?1;?++i)?{
????????leftArr[i]?=?arr[k]
????????++k
????}
????//設置哨兵值,當左子列或右子列讀取到最后一位時,即Infinity,可以讓另一個剩下的列中的值直接插入到數組中
????rightArr[rightArr.length?-?1]?=?Infinity
????leftArr[leftArr.length?-?1]?=?Infinity
????let?m?=?0
????let?n?=?0
????//?比較左子列和右子列第一個值的大小,小的先填入數組,接著再進行比較
????for?(let?c?=?startLeft;?c?<?stopRight;?++c)?{
????????if?(leftArr[m]?<=?rightArr[n])?{
????????????arr[c]?=?leftArr[m]
????????????m++
????????}?else?{
????????????arr[c]?=?rightArr[n]
????????????n++
????????}
????}
}
const?mergeSort?=?arr?=>?{
????if?(arr.length?<=?1)?{
????????return?arr
????}
????//設置子序列的大小
????let?step?=?1
????let?left
????let?right
????while?(step?<?arr.length)?{
????????left?=?0
????????right?=?step
????????while?(right?+?step?<=?arr.length)?{
????????????merge(arr,?left,?left?+?step,?right,?right?+?step)
????????????left?=?right?+?step
????????????right?=?left?+?step
????????}
????????if?(right?<?arr.length)?{
????????????merge(arr,?left,?left?+?step,?right,?arr.length)
????????}
????????step?*=?2
????}
????return?arr
}
魚頭注:迭代比起遞歸還是安全很多,太深的遞歸容易導致堆棧溢出。
堆排序
堆排序的算法步驟如下:
-
把無序數列構建成二叉堆;
-
循環刪除堆頂元素,替換到二叉堆的末尾,調整堆產生新的堆頂。
/*?堆下沉調整?*/
const?adjustHeap?=?(arr,?parentIndex,?length)?=>?{
????let?temp?=?arr[parentIndex]?/*?temp保存父節點值,用于最后賦值?*/
????let?childIndex?=?2?*?parentIndex?+?1?/*?保存子節點位置?*/
????while?(childIndex?<?length)?{
????????/*?如果有右子節點,且右子節點大于左子節點的值,則定位到右子節點?*/
????????if?(childIndex?+?1?<?length?&&?arr[childIndex?+?1]?>?arr[childIndex])?{
????????????childIndex++
????????}
????????/*?如果父節點小于任何一個子節點的值,直接退出循環?*/
????????if?(temp?>=?arr[childIndex])?{
????????????break;
????????}
????????/*?無序交換,單向賦值即可?*/
????????arr[parentIndex]?=?arr[childIndex]
????????parentIndex?=?childIndex
????????childIndex?=?2?*?childIndex?+?1
????}
????arr[parentIndex]?=?temp
}
const?heapSort?=?arr?=>?{
????/*?把無序數列構建成最大堆?*/
????for?(let?i?=?Math.floor(arr.length?/?2);?i?>=?0;?--i)?{
????????adjustHeap(arr,?i,?arr.length?-?1)
????}
????for?(let?i?=?arr.length?-?1;?i?>?0;?--i)?{
????????/*?交換最后一個元素與第一個元素?*/
????????[arr[i],?arr[0]]?=?[arr[0],?arr[i]]
????????/*?調整最大堆?*/
????????adjustHeap(arr,?0,?i)
????}
????return?arr
}
計數排序
計數排序(Counting sort)?是一種穩定的線性時間排序算法。該算法于1954年由 Harold H. Seward 提出。計數排序使用一個額外的數組來存儲輸入的元素,計數排序要求輸入的數據必須是有確定范圍的整數。
當輸入的元素是?n?個?0?到?k?之間的整數時,它的運行時間是?O(n + k)?。計數排序不是比較排序,排序的速度快于任何比較排序算法。
計數排序的算法步驟如下:
-
找出待排序的數組中最大和最小的元素;
-
統計數組中每個值為?i?的元素出現的次數,存入數組?C?的第?i?項;
-
對所有的計數累加(從數組?C?中的第一個元素開始,每一項和前一項相加);
-
反向填充目標數組:將每個元素?i?放在新數組的第?C[i]?項,每放一個元素就將 C[i] 減去1
具體實現如下:
const?countSort?=?arr?=>?{
????const?C?=?[]
????for?(let?i?=?0,?iLen?=?arr.length;?i?<?iLen;?++i)?{
????????const?j?=?arr[i]
????????if?(C[j]?>=?1)?{
????????????C[j]++
????????}?else?{
????????????C[j]?=?1
????????}
????}
????const?D?=?[]
????for?(let?j?=?0,?jLen?=?C.length;?j?<?jLen;?++j)?{
????????if?(C[j])?{
????????????while?(C[j]?>?0)?{
????????????????D.push(j)
????????????????C[j]--
????????????}
????????}
????}
????return?D
}
桶排序(Bucket Sort)?跟?計數排序(Counting sort)?一樣是一種穩定的線性時間排序算法,不過這次需要的輔助不是計數,而是桶。
工作的原理是將數列分到有限數量的桶里。每個桶再個別排序。當要被排序的數組內的數值是均勻分配的時候,桶排序使用線性時間?O(n)。
桶排序的算法步驟如下:
-
設置一個定量的數組當作空桶子;
-
尋訪序列,并且把項目一個一個放到對應的桶子去;
-
對每個不是空的桶子進行排序;
-
從不是空的桶子里把項目再放回原來的序列中。
const?bucketSort?=?arr?=>?{
????let?bucketsCount?=?10?/*?默認桶的數量?*/
????const?max?=?Math.max(...arr)?/*?序列最大數字?*/
????const?min?=?Math.min(...arr)?/*?數列最小數字?*/
????const?bucketsSize?=?Math.floor((max?-?min)?/?bucketsCount)?+?1?/*?桶的深度?*/
????const?__buckets?=?[]?/*?空桶?*/
????for?(let?i?=?0,?len?=?arr.length;?i?<?len;?++i)?{
????????const?index?=?~~(arr[i]?/?bucketsSize)?/*?騷操作,取數列中最大或最小的序列?*/
????????if?(!__buckets[index])?{
????????????__buckets[index]?=?[]?/*?創建子桶?*/
????????}
????????__buckets[index].push(arr[i])
????????let?bLen?=?__buckets[index].length
????????while?(bLen?>?0)?{?/*?子桶排序?*/
????????????if?(__buckets[index][bLen]?<?__buckets[index][bLen?-?1])?{
????????????????[__buckets[index][bLen],?__buckets[index][bLen?-?1]]?=?[__buckets[index][bLen?-?1],?__buckets[index][bLen]]
????????????}
????????????bLen--
????????}
????}
????let?buckets?=?[]?/*?真實序列?*/
????for?(let?i?=?0,?len?=?__buckets.length;?i?<?len;?++i)?{
????????if?(__buckets[i])?{
????????????buckets.push(...__buckets[i])
????????}
????}
????return?buckets
}
基數排序
基數排序(Radix sort)?是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然后按每個位數分別比較。由于整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是只能使用于整數。
工作原理是將所有待比較數值(正整數)統一為同樣的數字長度,數字較短的數前面補零。然后,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以后,數列就變成一個有序序列。
const?LSDRadixSort?=?arr?=>?{
????const?max?=?Math.max(...arr)?/*?獲取最大值?*/
????let?digit?=?`${max}`.length?/*?獲取最大值位數?*/
????let?start?=?1?/*?桶編號?*/
????let?buckets?=?[]?/*?空桶?*/
????while?(digit?>?0)?{
????????start?*=?10
????????/*?入桶?*/
????????for?(let?i?=?0,?len?=?arr.length;?i?<?len;?++i)?{
????????????const?index?=?(arr[i]?%?start)
????????????if?(!buckets[index])?{
????????????????buckets[index]?=?[]
????????????}
????????????buckets[index].push(arr[i])?/*?往不同桶里添加數據?*/
????????}
????????arr?=?[]
????????/*?出桶?*/
????????for(let?i?=?0;?i?<?buckets.length;?i++)?{
????????????if?(buckets[i])?{
????????????????arr?=?arr.concat(buckets[i])
????????????}
????????}
????????buckets?=?[]
????????digit?--
????}
????return?arr
}
雞尾酒排序,是?冒泡排序?的一種變形。此算法與?冒泡排序?不同的地方在于從低到高然后從高到低,而?冒泡排序?則僅從低到高去比較序列里的每個元素。它可以得到比?冒泡排序?稍微好一點的性能,原因是?冒泡排序?只從一個方向進行比對(由低到高),每次循環只移動一個項目。
步驟跟冒泡算法差不多,區別在于從起點到終點遍歷完之后會進行一次終點到起點的遍歷。
const?cocktailSort?=?arr?=>?{
????let?i
????let?left?=?0
????let?right?=?arr.length?-?1
????while?(left?<?right)?{
????????for?(i?=?left;?i?<?right;?++i)
????????????if?(arr[i]?>?arr[i?+?1])?{
????????????????[arr[i],?arr[i?+?1]]?=?[arr[i?+?1],?arr[i]]
????????????}
????????right--
????????for?(i?=?right;?i?>?left;?--i)
????????????if?(arr[i?-?1]?>?arr[i])?{
????????????????[arr[i],?arr[i?-?1]]?=?[arr[i?-?1],?arr[i]]
????????????}
????????left++
????}
????return?arr
}
?