在計算機編程中,排序算法是最經常使用的算法之一,本文介紹了幾種常見的排序算法以及它們之間的差別和複雜度。html
冒泡排序應該是最簡單的排序算法了,在全部講解計算機編程和數據結構的課程中,無一例外都會拿冒泡排序做爲開篇來說解排序的原理。冒泡排序理解起來也很容易,就是兩個嵌套循環遍歷數組,對數組中的元素兩兩進行比較,若是前者比後者大,則交換位置(這是針對升序排序而言,若是是降序排序,則比較的原則是前者比後者小)。咱們來看下冒泡排序的實現:算法
function bubbleSort(array) { let length = array.length; for (let i = 0; i < length; i++) { for (let j = 0; j < length - 1; j++) { if (array[j] > array[j + 1]) { [array[j], array[j + 1]] = [array[j + 1], array[j]]; } } } }
上面這段代碼就是經典的冒泡排序算法(升序排序),只不過交換兩個元素位置的部分咱們沒有用傳統的寫法(傳統寫法須要引入一個臨時變量,用來交換兩個變量的值),這裏使用了ES6的新功能,咱們可使用這種語法結構很方便地實現兩個變量值的交換。來看下對應的測試結果:編程
let array = []; for (let i = 5; i > 0; i--) { array.push(i); } console.log(array.toString()); // 5,4,3,2,1 bubbleSort(array); console.log(array.toString()); // 1,2,3,4,5
在冒泡排序中,對於內層的循環而言,每一次都是把這一輪中的最大值放到最後(相對於升序排序),它的過程是這樣的:第一次內層循環,找出數組中的最大值排到數組的最後;第二次內層循環,找出數組中的次大值排到數組的倒數第二位;第三次內層循環,找出數組中的第三大值排到數組的倒數第三位......以此類推。因此,對於內層循環,咱們能夠不用每一次都遍歷到length - 1的位置,而只須要遍歷到length - 1 - i的位置就能夠了,這樣能夠減小內層循環遍歷的次數。下面是改進後的冒泡排序算法:api
function bubbleSortImproved(array) { let length = array.length; for (let i = 0; i < length; i++) { for (let j = 0; j < length - 1 - i; j++) { if (array[j] > array[j + 1]) { [array[j], array[j + 1]] = [array[j + 1], array[j]]; } } } }
運行測試,結果和前面的bubbleSort()方法獲得的結果是相同的。數組
let array = []; for (let i = 5; i > 0; i--) { array.push(i); } console.log(array.toString()); // 5,4,3,2,1 bubbleSortImproved(array); console.log(array.toString()); // 1,2,3,4,5
在實際應用中,咱們並不推薦使用冒泡排序算法,儘管它是最直觀的用來說解排序過程的算法。冒泡排序算法的複雜度爲O(n2)。數據結構
選擇排序與冒泡排序很相似,它也須要兩個嵌套的循環來遍歷數組,只不過在每一次循環中要找出最小的元素(這是針對升序排序而言,若是是降序排序,則須要找出最大的元素)。第一次遍歷找出最小的元素排在第一位,第二次遍歷找出次小的元素排在第二位,以此類推。咱們來看下選擇排序的的實現:函數
function selectionSort(array) { let length = array.length; let min; for (let i = 0; i < length - 1; i++) { min = i; for (let j = i; j < length; j++) { if (array[min] > array[j]) { min = j; } } if (i !== min) { [array[i], array[min]] = [array[min], array[i]]; } } }
上面這段代碼是升序選擇排序,它的執行過程是這樣的,首先將第一個元素做爲最小元素min,而後在內層循環中遍歷數組的每個元素,若是有元素的值比min小,就將該元素的值賦值給min。內層遍歷完成後,若是數組的第一個元素和min不相同,則將它們交換一下位置。而後再將第二個元素做爲最小元素min,重複前面的過程。直到數組的每個元素都比較完畢。下面是測試結果:性能
let array = []; for (let i = 5; i > 0; i--) { array.push(i); } console.log(array.toString()); // 5,4,3,2,1 selectionSort(array); console.log(array.toString()); // 1,2,3,4,5
選擇排序算法的複雜度與冒泡排序同樣,也是O(n2)。測試
插入排序與前兩個排序算法的思路不太同樣,爲了便於理解,咱們以[ 5, 4, 3, 2, 1 ]這個數組爲例,用下圖來講明插入排序的整個執行過程:優化
在插入排序中,對數組的遍歷是從第二個元素開始的,tmp是個臨時變量,用來保存當前位置的元素。而後從當前位置開始,取前一個位置的元素與tmp進行比較,若是值大於tmp(針對升序排序而言),則將這個元素的值插入到這個位置中,最後將tmp放到數組的第一個位置(索引號爲0)。反覆執行這個過程,直到數組元素遍歷完畢。下面是插入排序算法的實現:
function insertionSort(array) { let length = array.length; let j, tmp; for (let i = 1; i < length; i++) { j = i; tmp = array[i]; while (j > 0 && array[j - 1] > tmp) { array[j] = array[j - 1]; j--; } array[j] = tmp; } }
對應的測試結果:
let array = []; for (let i = 5; i > 0; i--) { array.push(i); } console.log(array.toString()); // 5,4,3,2,1 insertionSort(array); console.log(array.toString()); // 1,2,3,4,5
插入排序比冒泡排序和選擇排序算法的性能要好。
歸併排序比前面介紹的幾種排序算法性能都要好,它的複雜度爲O(nlogn)。
歸併排序的基本思路是經過遞歸調用將給定的數組不斷分割成最小的兩部分(每一部分只有一個元素),對這兩部分進行排序,而後向上合併成一個大數組。咱們仍是以[ 5, 4, 3, 2, 1 ]這個數組爲例,來看下歸併排序的整個執行過程:
首先要將數組分紅兩個部分,對於非偶數長度的數組,你能夠自行決定將多的分到左邊或者右邊。而後按照這種方式進行遞歸,直到數組的左右兩部分都只有一個元素。對這兩部分進行排序,遞歸向上返回的過程當中將其組成和一個完整的數組。下面是歸併排序的算法的實現:
const merge = (left, right) => { let i = 0; let j = 0; const result = []; // 經過這個while循環將left和right中較小的部分放到result中 while (i < left.length && j < right.length) { if (left[i] < right[i]) result.push(left[i++]); else result.push(right[j++]); } // 而後將組合left或right中的剩餘部分 return result.concat(i < left.length ? left.slice(i) : right.slice(j)); }; function mergeSort(array) { let length = array.length; if (length > 1) { const middle = Math.floor(length / 2); // 找出array的中間位置 const left = mergeSort(array.slice(0, middle)); // 遞歸找出最小left const right = mergeSort(array.slice(middle, length)); // 遞歸找出最小right array = merge(left, right); // 將left和right進行排序 } return array; }
主函數mergeSort()經過遞歸調用自己獲得left和right的最小單元,這裏咱們使用Math.floor(length / 2)將數組中較少的部分放到left中,將數組中較多的部分放到right中,你可使用Math.ceil(length / 2)實現相反的效果。而後調用merge()函數對這兩部分進行排序與合併。注意在merge()函數中,while循環部分的做用是將left和right中較小的部分存入result數組(針對升序排序而言),語句result.concat(i < left.length ? left.slice(i) : right.slice(j))的做用則是將left和right中剩餘的部分加到result數組中。考慮到遞歸調用,只要最小部分已經排好序了,那麼在遞歸返回的過程當中只須要把left和right這兩部分的順序組合正確就能完成對整個數組的排序。
對應的測試結果:
let array = []; for (let i = 5; i > 0; i--) { array.push(i); } console.log(array.toString()); // 5,4,3,2,1 console.log(mergeSort(array).toString()); // 1,2,3,4,5
快速排序的複雜度也是O(nlogn),但它的性能要優於其它排序算法。快速排序與歸併排序相似,其基本思路也是將一個大數組分爲較小的數組,但它不像歸併排序同樣將它們分割開。快速排序算法比較複雜,大體過程爲:
下面是快速排序算法的實現:
const partition = (array, left, right) => { const pivot = array[Math.floor((right + left) / 2)]; let i = left; let j = right; while (i <= j) { while (array[i] < pivot) { i++; } while (array[j] > pivot) { j--; } if (i <= j) { [array[i], array[j]] = [array[j], array[i]]; i++; j--; } } return i; }; const quick = (array, left, right) => { let length = array.length; let index; if (length > 1) { index = partition(array, left, right); if (left < index - 1) { quick(array, left, index - 1); } if (index < right) { quick(array, index, right); } } return array; }; function quickSort(array) { return quick(array, 0, array.length - 1); }
假定數組爲[ 3, 5, 1, 6, 4, 7, 2 ],按照上面的代碼邏輯,整個排序的過程以下圖所示:
下面是測試結果:
let array = [3, 5, 1, 6, 4, 7, 2]; console.log(array.toString()); // 3,5,1,6,4,7,2 console.log(quickSort(array).toString()); // 1,2,3,4,5,6,7
快速排序算法理解起來有些難度,能夠按照上面給出的示意圖逐步推導一遍,以幫助理解整個算法的實現原理。
在計算機科學中,堆是一種特殊的數據結構,它一般用樹來表示數組。堆有如下特色:
堆排序是一種比較高效的排序算法。
在堆排序中,咱們並不須要將數組元素插入到堆中,而只是經過交換來造成堆,以數組[ 3, 5, 1, 6, 4, 7, 2 ]爲例,咱們用下圖來表示其初始狀態:
那麼,如何將其轉換成一個符合標準的堆結構呢?先來看看堆排序算法的實現:
const heapify = (array, heapSize, index) => { let largest = index; const left = index * 2 + 1; const right = index * 2 + 2; if (left < heapSize && array[left] > array[index]) { largest = left; } if (right < heapSize && array[right] > array[largest]) { largest = right; } if (largest !== index) { [array[index], array[largest]] = [array[largest], array[index]]; heapify(array, heapSize, largest); } }; const buildHeap = (array) => { let heapSize = array.length; for (let i = heapSize; i >= 0; i--) { heapify(array, heapSize, i); } }; function heapSort(array) { let heapSize = array.length; buildHeap(array); while (heapSize > 1) { heapSize--; [array[0], array[heapSize]] = [array[heapSize], array[0]]; heapify(array, heapSize, 0); } return array; }
函數buildHeap()將給定的數組轉換成堆(按最大堆處理)。下面是將數組[ 3, 5, 1, 6, 4, 7, 2 ]轉換成堆的過程示意圖:
在函數buildHeap()中,咱們從數組的尾部開始遍歷去查看每一個節點是否符合堆的特色。在遍歷的過程當中,咱們發現當索引號爲六、五、四、3時,其左右子節點的索引大小都超出了數組的長度,這意味着它們都是葉子節點。那麼咱們真正要作的就是從索引號爲2的節點開始。其實從這一點考慮,結合咱們利用徹底二叉樹來表示數組的特性,能夠對buildHeap()函數進行優化,將其中的for循環修改成下面這樣,以去掉對子節點的操做。
for (let i = Math.floor(heapSize / 2) - 1; i >= 0; i--) { heapify(array, heapSize, i); }
從索引2開始,咱們查看它的左右子節點的值是否大於本身,若是是,則將其中最大的那個值與本身交換,而後向下遞歸查找是否還須要對子節點繼續進行操做。索引2處理完以後再處理索引1,而後是索引0,最終轉換出來的堆如圖中的4所示。你會發現,每一次堆轉換完成以後,排在數組第一個位置的就是堆的根節點,也就是數組的最大元素。根據這一特色,咱們能夠很方便地對堆進行排序,其過程是:
直到整個過程結束。對應的示意圖以下:
堆排序的核心部分在於如何將數組轉換成堆,也就是上面代碼中buildHeap()和heapify()函數部分。
一樣給出堆排序的測試結果:
let array = [3, 5, 1, 6, 4, 7, 2]; console.log(array.toString()); // 3,5,1,6,4,7,2 console.log(heapSort(array).toString()); // 1,2,3,4,5,6,7
上面咱們在介紹各類排序算法的時候,提到了算法的複雜度,算法複雜度用大O表示法,它是用大O表示的一個函數,如:
咱們如何理解大O表示法呢?看一個例子:
function increment(num) { return ++num; }
對於函數increment(),不管我傳入的參數num的值是什麼數字,它的運行時間都是X(相對於同一臺機器而言)。函數increment()的性能與參數無關,所以咱們能夠說它的算法複雜度是O(1)(常數)。
再看一個例子:
function sequentialSearch(array, item) { for (let i = 0; i < array.length; i++) { if (item === array[i]) return i; } return -1; }
函數sequentialSearch()的做用是在數組中搜索給定的值,並返回對應的索引號。假設array有10個元素,若是要搜索的元素排在第一個,咱們說開銷爲1。若是要搜索的元素排在最後一個,則開銷爲10。當數組有1000個元素時,搜索最後一個元素的開銷是1000。因此,sequentialSearch()函數的總開銷取決於數組元素的個數和要搜索的值。在最壞狀況下,沒有找到要搜索的元素,那麼總開銷就是數組的長度。所以咱們得出sequentialSearch()函數的時間複雜度是O(n),n是數組的長度。
同理,對於前面咱們說的冒泡排序算法,裏面有一個雙層嵌套的for循環,所以它的複雜度爲O(n2)。
時間複雜度O(n)的代碼只有一層循環,而O(n2)的代碼有雙層嵌套循環。若是算法有三層嵌套循環,它的時間複雜度就是O(n3)。
下表展現了各類不一樣數據結構的時間複雜度:
數據結構 | 通常狀況 | 最差狀況 | ||||
插入 | 刪除 | 搜索 | 插入 | 刪除 | 搜索 | |
數組/棧/隊列 | O(1) | O(1) | O(n) | O(1) | O(1) | O(n) |
鏈表 | O(1) | O(1) | O(n) | O(1) | O(1) | O(n) |
雙向鏈表 | O(1) | O(1) | O(n) | O(1) | O(1) | O(n) |
散列表 | O(1) | O(1) | O(1) | O(n) | O(n) | O(n) |
BST樹 | O(log(n)) | O(log(n)) | O(log(n)) | O(n) | O(n) | O(n) |
AVL樹 | O(log(n)) | O(log(n)) | O(log(n)) | O(log(n)) | O(log(n)) | O(log(n)) |
數據結構的時間複雜度
節點/邊的管理方式 | 存儲空間 | 增長頂點 | 增長邊 | 刪除頂點 | 刪除邊 | 輪詢 |
領接表 | O(| V | + | E |) | O(1) | O(1) | O(| V | + | E |) | O(| E |) | O(| V |) |
鄰接矩陣 | O(| V |2) | O(| V |2) | O(1) | O(| V |2) | O(1) | O(1) |
圖的時間複雜度
算法(用於數組) | 時間複雜度 | ||
最好狀況 | 通常狀況 | 最差狀況 | |
冒泡排序 | O(n) | O(n2) | O(n3) |
選擇排序 | O(n2) | O(n2) | O(n2) |
插入排序 | O(n) | O(n2) | O(n2) |
歸併排序 | O(log(n)) | O(log(n)) | O(log(n)) |
快速排序 | O(log(n)) | O(log(n)) | O(n2) |
堆排序 | O(log(n)) | O(log(n)) | O(log(n)) |
排序算法的時間複雜度
順序搜索是一種比較直觀的搜索算法,上面介紹算法複雜度一小節中的sequentialSearch()函數就是順序搜索算法,就是按順序對數組中的元素逐一比較,直到找到匹配的元素。順序搜索算法的效率比較低。
還有一種常見的搜索算法是二分搜索算法。它的執行過程是:
下面是二分搜索算法的具體實現:
function binarySearch(array, item) { quickSort(array); // 首先用快速排序法對array進行排序 let low = 0; let high = array.length - 1; while (low <= high) { const mid = Math.floor((low + high) / 2); // 選取中間位置的元素 const element = array[mid]; // 待搜索的值大於中間值 if (element < item) low = mid + 1; // 待搜索的值小於中間值 else if (element > item) high = mid - 1; // 待搜索的值就是中間值 else return true; } return false; }
對應的測試結果:
const array = [8, 7, 6, 5, 4, 3, 2, 1]; console.log(binarySearch(array, 2)); // true
這個算法的基本思路有點相似於猜數字大小,每當你說出一個數字,我都會告訴你是大了仍是小了,通過幾輪以後,你就能夠很準確地肯定數字的大小了。
原文出處:https://www.cnblogs.com/jaxu/p/11382646.html