JavaScript實現排序算法
一、大O表示法
大O表示法:
- 在計算機中采用粗略的度量來描述計算機算法的效率,這種方法被稱為“大O”表示法
- 在數據項個數發生改變時,算法的效率也會跟着改變。所以說算法A比算法B快兩倍,這樣的比較是沒有意義的。
- 因此我們通常使用算法的速度隨着數據量的變化會如何變化的方式來表示算法的效率,大O表示法就是方式之一。
常見的大O表示形式
| 符號 | 名稱 |
|---|---|
| O(1) | 常數 |
| O(log(n)) | 對數 |
| O(n) | 線性 |
| O(nlog(n)) | 線性和對數乘積 |
| O(n²) | 平方 |
| O(2n) | 指數 |
不同大O形式的時間復雜度:

可以看到效率從大到小分別是:O(1)> O(logn)> O(n)> O(nlog(n))> O(n²)> O(2n)
推導大O表示法的三條規則:
- 規則一:用常量1取代運行時間中所有的加法常量。如7 + 8 = 15,用1表示運算結果15,大O表示法表示為O(1);
- 規則二:運算中只保留最高階項。如N^3 + 3n +1,大O表示法表示為:O(N3);
- 規則三:若最高階項的常數不為1,可將其省略。如4N2,大O表示法表示為:O(N2);
二、排序算法
這里主要介紹幾種簡單排序和高級排序:
- 簡單排序:冒泡排序、選擇排序、插入排序;
- 高級排序:希爾排序、快速排序;
此處創建一個列表類ArrayList並添加一些屬性和方法,用於存放這些排序方法:
//創建列表類
function ArrayList() {
//屬性
this.array = []
//方法
//封裝將數據插入到數組中方法
ArrayList.prototype.insert = function(item){
this.array.push(item)
}
//toString方法
ArrayList.prototype.toString = function(){
return this.array.join('-')
}
//交換兩個位置的數據
ArrayList.prototype.swap = function(m, n){
let temp = this.array[m]
this.array[m] = this.array[n]
this.array[n] = temp
}
1.冒泡排序
冒泡排序的思路:
- 對未排序的各元素從頭到尾依次比較相鄰的兩個元素大小關系;
- 如果左邊的人員高,則將兩人交換位置。比如1比2矮,不交換位置;
- 向右移動一位,繼續比較2和3,最后比較 length - 1 和 length - 2這兩個數據;
- 當到達最右端時,最高的人一定被放在了最右邊;
- 按照這個思路,從最左端重新開始時,只需要走到倒數第二個位置即可;

實現思路:
兩層循環:
-
外層循環控制冒泡趟數:
- 第一次:j = length - 1,比較到倒數第一個位置 ;
- 第二次:j = length - 2,比較到倒數第二個位置 ;
-
內層循環控制每趟比較的次數:
- 第一次比較: i = 0,比較 0 和 1 位置的兩個數據;
- 最后一次比較:i = length - 2,比較length - 2和 length - 1兩個數據;
詳細過程如下圖所示:

動態過程:

代碼實現:
//冒泡排序
ArrayList.prototype.bubblesor = function(){
//1.獲取數組的長度
let length = this.array.length
//外層循環控制冒泡趟數
for(let j = length - 1; j >= 0; j--){
//內層循環控制每趟比較的次數
for(let i = 0; i < j; i++){
if (this.array[i] > this.array[i+1]) {
//交換兩個數據
let temp = this.array[i]
this.array[i] = this.array[i+1]
this.array[i+1] = temp
}
}
}
}
測試代碼:
//測試類
let list = new ArrayList()
//插入元素
list.insert(66)
list.insert(88)
list.insert(12)
list.insert(87)
list.insert(100)
list.insert(5)
list.insert(566)
list.insert(23)
//驗證冒泡排序
list.bubblesor()
console.log(list);
測試結果:

冒泡排序的效率:
- 上面所講的對於7個數據項,比較次數為:6 + 5 + 4 + 3 + 2 + 1;
- 對於N個數據項,比較次數為:(N - 1) + (N - 2) + (N - 3) + ... + 1 = N * (N - 1) / 2;如果兩次比較交換一次,那么交換次數為:N * (N - 1) / 4;
- 使用大O表示法表示比較次數和交換次數分別為:O( N * (N - 1) / 2)和O( N * (N - 1) / 4),根據大O表示法的三條規則都化簡為:O(N^2);
2.選擇排序
選擇排序改進了冒泡排序:
- 將交換次數由O(N^2)減小到O(N);
- 但是比較次數依然是O(N^2);
選擇排序的思路:
- 選定第一個索引的位置比如1,然后依次和后面的元素依次進行比較;
- 如果后面的元素,小於索引1位置的元素,則交換位置到索引1處;
- 經過一輪的比較之后,可以確定一開始指定的索引1位置的元素是最小的;
- 隨后使用同樣的方法除索引1意外逐個比較剩下的元素即可;
- 可以看出選擇排序,第一輪會選出最小值,第二輪會選出第二小的值,直到完成排序。

實現思路:
兩層循環:
-
外層循環控制指定的索引:
- 第一次:j = 0,指定第一個元素 ;
- 最后一次:j = length - 1,指定最后一個元素 ;
-
內層循環負責將指定索引(i)的元素與剩下(i - 1)的元素進行比較;
動態過程:

代碼實現:
//選擇排序
ArrayList.prototype.selectionSort = function(){
//1.獲取數組的長度
let length = this.array.length
//2.外層循環:從0開始獲取元素
for(let j = 0; j < length - 1; j++){
let min = j
//內層循環:從i+1位置開始,和后面的元素進行比較
for(let i = min + 1; i < length; i++){
if (this.array[min] > this.array[i]) {
min = i
}
}
this.swap(min, j)
}
}
測試代碼:
//測試類
let list = new ArrayList()
//插入元素
list.insert(66)
list.insert(88)
list.insert(12)
list.insert(87)
list.insert(100)
list.insert(5)
list.insert(566)
list.insert(23)
//驗證選擇排序
list.selectionSort()
console.log(list);
測試結果:

選擇排序的效率:
- 選擇排序的比較次數為:N * (N - 1) / 2,用大O表示法表示為:O(N^2);
- 選擇排序的交換次數為:(N - 1) / 2,用大O表示法表示為:O(N);
- 所以選擇排序的效率高於冒泡排序;
3.插入排序
插入排序是簡單排序中效率最高的一種排序。
插入排序的思路:
- 插入排序思想的核心是局部有序。如圖所示,X左邊的人稱為局部有序;
- 首先指定一數據X(從第一個數據開始),並將數據X的左邊變成局部有序狀態;
- 隨后將X右移一位,再次達到局部有序之后,繼續右移一位,重復前面的操作直至X移至最后一個元素。

插入排序的詳細過程:

動態過程:

代碼實現:
//插入排序
ArrayList.prototype.insertionSort = function(){
//1.獲取數組的長度
let length = this.array.length
//2.外層循環:從第二個數據開始,向左邊的已經局部有序數據進行插入
for(let i = 1; i < length; i++){
//3.內層循環:獲取i位置的元素,使用while循環(重點)與左邊的局部有序數據依次進行比較
let temp = this.array[i]
let j = i
while(this.array[j - 1] > temp && j > 0){
this.array[j] = this.array[j - 1]//大的數據右移
j--
}
//4.while循環結束后,index = j左邊的數據變為局部有序且array[j]最大。此時將array[j]重置為排序前的數據array[i],方便下一次for循環
this.array[j] = temp
}
}
測試代碼:
//測試類
let list = new ArrayList()
//插入元素
list.insert(66)
list.insert(88)
list.insert(12)
list.insert(87)
list.insert(100)
list.insert(5)
list.insert(566)
list.insert(23)
// console.log(list);
//驗證插入排序
list.insertionSort()
console.log(list);
測試結果:

插入排序的效率:
-
比較次數:第一趟時,需要的最大次數為1;第二次最大為2;以此類推,最后一趟最大為N-1;所以,插入排序的總比較次數為N * (N - 1) / 2;但是,實際上每趟發現插入點之前,平均只有全體數據項的一半需要進行比較,所以比較次數為:N * (N - 1) / 4;
-
交換次數:指定第一個數據為X時交換0次,指定第二個數據為X最多需要交換1次,以此類推,指定第N個數據為X時最多需要交換N - 1次,所以一共需要交換N * (N - 1) / 2次,平局次數為N * (N - 1) / 2;
-
雖然用大O表示法表示插入排序的效率也是O(N^2),但是插入排序整體操作次數更少,因此,在簡單排序中,插入排序效率最高;
4.希爾排序
希爾排序是插入排序的一種高效的改進版,效率比插入排序要高。
希爾排序的歷史背景:
- 希爾排序按其設計者希爾(Donald Shell)的名字命名,該算法由1959年公布;
- 希爾算法首次突破了計算機界一直認為的算法的時間復雜度都是O(N^2)的大關,為了紀念該算法里程碑式
的意義,用Shell來命名該算法;
插入排序的問題:
- 假設一個很小的數據項在很靠近右端的位置上,這里本應該是較大的數據項的位置;
- 將這個小數據項移動到左邊的正確位置,所有的中間數據項都必須向右移動一位,這樣效率非常低;
- 如果通過某種方式,不需要一個個移動所有中間的數據項,就能把較小的數據項移到左邊,那么這個算法的執行速度就會有很大的改進。
希爾排序的實現思路:
- 希爾排序主要通過對數據進行分組實現快速排序;
- 根據設定的增量(gap)將數據分為gap個組(組數等於gap),再在每個分組中進行局部排序;
假如有數組有10個數據,第1個數據為黑色,增量為5。那么第二個為黑色的數據index=5,第3個數據為黑色的數據index = 10(不存在)。所以黑色的數據每組只有2個,10 / 2 = 5一共可分5組,即組數等於增量gap。
- 排序之后,減小增量,繼續分組,再次進行局部排序,直到增量gap=1為止。隨后只需進行微調就可完成數組的排序;
具體過程如下:
- 排序之前的,儲存10個數據的原始數組為:

- 設初始增量gap = length / 2 = 5,即數組被分為了5組,如圖所示分別為:[8, 3]、[9, 5]、[1, 4]、[7, 6]、[2, 0]:

- 隨后分別在每組中對數據進行局部排序,5組的順序如圖所示,變為:[3, 8]、[5, 9]、[1, 4]、[6, 7]、[0, 2]:

- 然后縮小增量gap = 5 / 2 = 2,即數組被分為了2組,如圖所示分別為:[3,1,0,9,7]、[5,6,8,4,2]:

- 隨后分別在每組中對數據進行局部排序,兩組的順序如圖所示,變為:[0,1,3,7,9]、[2,4,5,6,8]:

- 然后然后縮小增量gap = 2 / 1 = 1,即數組被分為了1組,如圖所示為:[0,2,1,4,3,5,7,6,9,8]:

- 最后只需要對該組數據進行插入排序即可完成整個數組的排序:

動態過程:

圖中d表示增量gap。
增量的選擇:
- 原稿中希爾建議的初始間距為N / 2,比如對於N = 100的數組,增量序列為:50,25,12,6,3,1,可以發現不能整除時向下取整。
- Hibbard增量序列:增量序列算法為:2^k - 1,即1,3,5,7... ...等;這種情況的最壞復雜度為O(N3/2)**,平均復雜度為**O(N5/4)但未被證明;
- Sedgewcik增量序列:

以下代碼實現中采用希爾排序原稿中建議的增量即N / 2 。
代碼實現:
//希爾排序
ArrayList.prototype.shellSort = function(){
//1.獲取數組的長度
let length = this.array.length
//2.初始化增量
let gap = Math.floor(length / 2)
//3.第一層循環:while循環(使gap不斷減小)
while(gap >= 1 ){
//4.第二層循環:以gap為增量,進行分組,對分組進行插入排序
//重點為:將index = gap作為選中的第一個數據
for(let i = gap; i < length; i++){
let temp = this.array[i]
let j = i
//5.第三層循環:尋找正確的插入位置
while(this.array[j - gap] > temp && j > gap - 1){
this.array[j] = this.array[j - gap]
j -= gap
}
//6.將j位置的元素設置為temp
this.array[j] = temp
}
gap = Math.floor(gap / 2)
}
}
這里解釋一下上述代碼中的三層循環:
- 第一層循環:while循環,控制gap遞減到1;
- 第二層循環:分別取出根據g增量gap分成的gap組數據:將index = gap的數據作為選中的第一個數據,如下圖所示,gap=5,則index = gap的數據為3,index = gap - 1的數據為8,兩個數據為一組。隨后gap不斷加1右移,直到gap < length,此時實現了將數組分為5組。

- 第三層循環:對每一組數據進行插入排序;
測試代碼:
//測試類
let list = new ArrayList()
//插入元素
list.insert(66)
list.insert(88)
list.insert(12)
list.insert(87)
list.insert(100)
list.insert(5)
list.insert(566)
list.insert(23)
// console.log(list);
//驗證希爾排序
list.shellSort()
console.log(list);
測試結果:

希爾排序的效率:
- 希爾排序的效率和增量有直接關系,即使使用原稿中的增量效率都高於簡單排序。
5.快速排序
快速排序的介紹:
-
快速排序可以說是目前所有排序算法中,最快的一種排序算法。當然,沒有任何一種算法是在任意情況下都是最優的。但是,大多數情況下快速排序是比較好的選擇。
-
快速排序其實是冒泡排序的升級版;
快速排序的核心思想是分而治之,先選出一個數據(比如65),將比其小的數據都放在它的左邊,將比它大的數據都放在它的右邊。這個數據稱為樞紐
和冒泡排序的不同:
- 我們選擇的65可以一次性將它放在最正確的位置,之后就不需要做任何移動;
- 而冒泡排序即使已經找到最大值,也需要繼續移動最大值,直到將它移動到最右邊;

快速排序的樞紐:
- 第一種方案:直接選擇第一個元素作為樞紐。但是,當第一個元素就是最小值的情況下,效率不高;
- 第二種方案:使用隨機數。隨機數本身十分消耗性能,不推薦;
- 優秀的解決方法:取index為頭、中、位的三個數據排序后的中位數;如下圖所示,按下標值取出的三個數據為:92,31,0,經排序后變為:0,31,92,取其中的中位數31作為樞紐(當(length-1)/2不整除時可向下或向上取整):

實現樞紐選擇:
//交換兩個位置的數據
let swap = function(arr, m, n){
let temp = arr[m]
arr[m] = arr[n]
arr[n] = temp
}
//快速排序
//1.選擇樞紐
let median = function(arr){
//1.取出中間的位置
let center = Math.floor(arr.length / 2)
let right = arr.length - 1
let left = 0
//2.判斷大小並進行交換
if (arr[left] > arr[center]) {
swap(arr, left, center)
}
if (arr[center] > arr[right]){
swap(arr, center, right)
}
if (arr[left] > arr[right]) {
swap(arr, left, right)
}
//3.返回樞紐
return center
}
數組經過獲取樞紐函數操作之后,選出的3個下標值對應的數據位置變為:

動態過程:

快速排序代碼實現:
//2.快速排序
let QuickSort = function(arr){
if (arr.length == 0) {
return []
}
let center = median(arr)
let c = arr.splice(center, 1)
let l = []
let r = []
for (let i = 0; i < arr.length; i++) {
if (arr[i] < c) {
l.push(arr[i])
}else{
r.push(arr[i])
}
}
return QuickSort(l).concat(c, QuickSort(r))
}
算法的巧妙之處在於通過:
QuickSort(l).concat(c, QuickSort(r))
遞歸調用QuickSort函數實現了樞紐Center左邊數據l和右邊數據r的排序;
測試代碼:
let arr = [0, 13, 81, 43, 31, 27, 56, 92]
console.log(QuickSort(arr));
測試結果

快速排序的效率:
- 快速排序最壞情況下的效率:每次選擇的樞紐都是最左邊或最右邊的數據,此時效率等同於冒泡排序,時間復雜度為O(n2)。可根據不同的樞紐選擇避免這一情況;
- 快速排序的平均效率:為O(N*logN),雖然其他算法效率也可達到O(N*logN),但是其中快速排序是最好的。
參考資料:JavaScript數據結構與算法
