上一篇,咱們講述了一些簡單的排序算法,其實說到底,在前端的職業生涯中,不涉及node、不涉及後臺的狀況下,我目前還真的沒想到有哪些地方能夠用到這些數據結構和算法,可是我在前面的文章也說過了。或許你用不到,可是,真的,若是你想要在前端領域有一個不錯的發展。數據結構和算法必定是你的必修課。它不只僅讓你在處理問題的時候能夠有一個思惟底蘊,更重要的是,在遇到一些奇葩產品的時候,你能夠和他PK到底!嗯,到底!html
哈哈,開個小玩笑。我們仍是聊點有養分的。上一篇的算法比較簡單,主內容就是循環,次內容就是比較。可是,這篇文章所介紹的一些算法,在實現上有些許的複雜,甚至在其中涉及到了一部分數據結構相關的知識,若是你對數據結構還不是十分了解,請移步這裏用js來實現那些數據結構—目錄。前端
那麼,咱們這篇文章要一塊兒來看看另一些在執行上有着更高效率的算法,好比歸併排序,好比快速排序,還有堆排序等等。java
一、歸併排序node
咱們先來看看什麼是歸併排序,以及歸併排序是怎麼實現的。算法
歸併排序屬於一種分治算法。歸併排序的思想就是將原始數組切分紅一個一個較小的數組,直到每個數組只有一個元素爲止,而後再把一個一個小數組,一點一點的結合成一個最終排序後的數組。其實簡單來講,就是先分,再合。歸併排序的實現有兩種方法,一種是遞歸,一種是迭代。下面咱們只用遞歸的方式來實現一下代碼:api
//歸併排序 // 很少說,你懂的。 this.mergeSort = function () { array = mergeSortRec(array); }; //這個私有函數,其實就是整個歸併排序中「分」的部分。咱們來看看它是如何「分」的。 var mergeSortRec = function (array) { //存儲數組長度。 var length = array.length; //因爲是遞歸,因此當length 爲 1 的時候,說明咱們分到底了。直接返回這個數組。也就是隻有一個元素的數組。 //同時這個判斷條件也是遞歸的終止條件,要記住任何遞歸操做都必須有終止條件。否則會陷入死循環。 if(length === 1) { return array; } //咱們要把原始數組從中一分爲二。下面就是一分爲二的操做。無需多說。 var mid = Math.floor(length / 2), left = array.slice(0,mid), right = array.slice(mid,length); // 這裏,咱們先不去管merge函數是作什麼的。咱們先看遞歸到最底層。merge的兩個參數會變成什麼。 // 因爲咱們又返回了自身,至此遞歸就造成了。在merge的參數中咱們又遞歸調用了一次自身。 // 那麼此次調用咱們把left和right兩個數組又拆分了一次。直到最後array.length 爲 1(歸併的最小單位)。 // 那麼換句話說,實際上merge函數遞歸的最底層傳入的就是兩個只有一個元素的數組。 return merge(mergeSortRec(left),mergeSortRec(right)); }; var merge = function (left,right) { // 咱們聲明一個最終結果的數組result, // il和ir是用來控制左右兩個數組的迭代的變量 var result = [],il = 0,ir = 0; // 這裏,咱們的最底層是隻有兩個只有一個元素的數組 /* array[left]和array[right] 第一個while, 循環的條件是兩個長度變量是否在合法值內。 */ while(il < left.length && ir < right.length) { // 若是左側小於右側,此時的il和ir是相等的都是0。注意這一點 // 咱們就把左側的left[il]放入數組並il++。不然,咱們就把right[ir]存入數組result並ir++。此時,il和ir就不相等了。 // 因此,這時候,咱們下一次循環判斷的條件就是遞增的ir或il與沒有遞增的il或者ir作比較。這樣就使得一個數組中的一個元素與另外數組中全部元素都作過比較。 // 但願上面我說明白了想要表達的意思。 if(left[il] < right[ir]) { // 這裏,不太容易理解。爲何咱們要在result中加入il++而不是il? // 其實這裏的意思是,先加如left[il]再il++。 // 不信,你能夠把代碼改爲這個樣子 /* result.push(left[il]); il++ */ // 效果是同樣同樣的。 result.push(left[il++]); } else { result.push(right[ir++]); } }; //這兩個循環的目的是把剩餘的數組元素(包括left數組和right數組)都存入result數組中。 // 這樣咱們就好了一個歸併後的結果數組,而後進行下一次的歸併過程的初始參數。 while(il < left.length) { result.push(left[il++]); }; while(ir < right.length) { result.push(right[ir++]); }; return result; }; //咱們用上一章的方法來測試一下歸併排序 var arraylist = creatArrayList(5); console.log(arraylist.toString());//5,4,3,2,1 arraylist.mergeSort(); console.log(arraylist.toString());//1,2,3,4,5
其實歸併排序的核心思想就是先拆分數組直至分爲只有一個元素的數組時,再對其一點一點的進行合併。數組
二、快速排序數據結構
快速排序是咱們在組織架構或者實際應用中最爲經常使用的排序算法之一,你也能夠把快速排序用在你的項目中。架構
快速排序的思想和歸併排序有些相似,可是快速排序不須要把拆分開的元素裝入一個新的數組,而是直接在原數組上進行交換操做。數據結構和算法
那麼咱們來看看快速排序的操做步驟:
首先咱們要找到用於與之相比較的「主元」。理論上主元能夠爲數組中的任意的元素,可是在本文中咱們選取數組的中間項做爲主元。
而後,咱們選擇主元左側和右側的兩部分中的元素分別和主元作比較,直到咱們找到了左側比主元大而且右側比主元小的元素,那麼咱們就交換兩個元素的位置。直到迭代的左側指針超過了右側指針。這樣,咱們就使比主元小的元素和比主元大的元素分別存在於主元的兩側。
最後,咱們再對左右兩側的數組遞歸重複上面的步驟。直至數組排序結束。
咱們來看下代碼。
//快速排序 this.quickSort = function () { // 這裏傳入的參數是原始數組自己和首尾元素的下標。 quick(array,0,array.length - 1); }; var quick = function (array,left,right) { // 這個index是爲了幫助咱們分離出較小值數組和較大值數組的 var index; // 若是length>1才執行邏輯,由於只有一個元素的數組意味着無需排序。 if(array.length > 1) { // partition返回一個元素的下標。 index = partition(array,left,right); //下面兩個判斷條件爲了區分咱們要遞歸的是較小值數組仍是較大值數組。 if(left < index - 1) { quick(array,left,index - 1); }; if(index < right) { quick(array,index,right); }; } }; //咱們來看看劃分過程的這個方法 var partition = function (array,left,right) { // pivot(主元),也就是咱們要與之比較的元素。咱們選取中間項做爲主元。 // i和j表明着較小值數組和較大值數組當前元素的指針 var pivot = array[Math.floor((right + left) / 2)],i = left,j = right; //要知道最開始的i是0,j是lenght-1。因此i++是往右側移動指針,j--是往左側移動指針。 //循環的條件是較小值數組的下標小於較大值數組的下標。 while(i <= j) { // 若是array[i]元素小於主元,向右移動指針。 while(array[i] < pivot) { i++; }; // 若是array[j]元素大於主元,向左移動指針。 while(array[j] > pivot) { j--; }; //上面兩個while迭代,當遇到不符合條件的時候就會停下來。 // 而這種不符合條件的狀況是array[i]>array[j]。這是要調整的狀況。可是此時i仍舊是小於等於j的。要注意,i在這裏不可能比j大。 // 因此咱們此時交換兩個下標對應的元素,並改變i和j的指針。最後返回下標i。 if(i <= j) { swap(array,i,j); i++; j--; }; }; return i; }; var arraylist = creatArrayList(5); console.log(arraylist.toString());//5,4,3,2,1 arraylist.quickSort(); console.log(arraylist.toString());//1,2,3,4,5
在說堆排序以前,咱們得先了解一下什麼是堆,堆這種數據結構本質是一個徹底二叉樹(若是你還不知道什麼是樹,請看我前面的文章),那麼既然堆是一種樹,那麼有關於樹的一些概念均可以用在堆上面,好比深度,好比節點等。要知道,樹一般都會有左孩子又孩子節點和父節點的指針,可是在徹底二叉樹中,這些指針均可以去掉,由於咱們能夠用必定的規律來直接找到當前節點的關聯節點。好比給定某一個結點,假設它的下標爲i,那麼它的左孩子結點的下標就是2i + 1,右孩子結點的下標就是2i + 2,它的父結點爲(i−1)/2。這樣,咱們就把能夠省略去這些指針,直接將堆中的結點存儲在數組中了。
在瞭解了什麼是堆以後,咱們看看堆排序是怎麼操做的。咱們直接從代碼中看比較具體:
//堆排序 // 這裏你須要對樹數據結構有必定的瞭解和認識。若是你對樹結構還不是十分了解,請看我前面有關於樹結構的相關章節。 // 或者,你能夠直接用下面的代碼來解決問題,固然,你須要作一些細微的改動。 this.heapSort = function () { // 把數組長度存入一個變量 var heapSize = array.length; // 把該數組「堆」化。 buildHeap(array); //迭代遞減heapSize的長度,交換數組中下標爲0和當前的heapSize從新使變更的堆「合理化」。這裏的合理化是指讓交換了元素位置的數組從新生成符合堆原理的一個新數組。 while(heapSize > 1) { heapSize--; swap(array,0,heapSize); heapify(array,heapSize,0); }; }; //生成堆函數,咱們的i爲數組中間值而且遞減i的循環爲heapify函數傳入。 var buildHeap = function (array) { var heapSize = array.length; for(var i = Math.floor(array.length / 2);i >= 0; i--) { heapify(array,heapSize,i); }; }; //「堆」化函數。 var heapify = function (array,heapSize,i) { //咱們聲明左節點,右節點,以及父節點(也就是largest)的變量 var left = i * 2 + 1, right = i * 2 + 2, largest = i; //這兩個判斷是爲了知道在當前輪中的父節點是誰。 if(left < heapSize && array[left] > array[largest]) { largest = left; }; if(right < heapSize && array[right] > array[largest]) { largest = right; }; //若是largest變了,咱們就須要交換兩個值得位置。而且從新調用heapify。 if(largest !== i) { swap(array,i,largest); heapify(array,heapSize,largest); }; }; var arraylist = creatArrayList(125); console.log(arraylist.toString());//5,4,3,2,1 arraylist.heapSort(); console.log(arraylist.toString());//1,2,3,4,5
堆排序的概念其實並不難理解,惟一須要注意的就是堆數據結構的概念,但願我說清楚了,若是你以爲我對於堆的講解並不詳細,首先你能夠百度,其次你能夠谷歌,再次你還能夠去搜維基百科。實在不行,你去這裏看看https://zhuanlan.zhihu.com/p/25820535。這是一篇有關於java相關的系列教程之一。固然,就算你不懂java,相信你也同樣能夠看懂,真的,我沒開玩笑。
我在糾結要不要畫張圖仍是就這樣結束,由於畫一個完整的流程圖真的很花時間......我抽根菸考慮下......要不你們去買本書看吧。。。。嗯....一個不錯的建議。
下面咱們以數組[3,5,1,6,4,7,2]做爲圖例的基本數據:
圖片要結合代碼一塊兒看,否則看不懂的噢.......還有,這個圖你最好放大了看,否則累眼睛。。。
最後,其實有關於排序算法還有不少,好比計數排序,桶排序,基數排序等等等等等。排序算法遠不止如此。可是本系列不會介紹這麼多的算法,若是你想要更深刻的去了解其它算法的內容,能夠自行查找相關的資料。
好了,終於要結束了,但願本文的內容能帶給你一點點的收穫。
最後,因爲本人水平有限,能力與大神仍相差甚遠,如有錯誤或不明之處,還望你們不吝賜教指正。很是感謝!