《Algorithms Unlocked》是 《算法導(dǎo)論》的合著者之一 Thomas H. Cormen 寫的一本算法基礎(chǔ),算是啃CLRS前的開胃菜和輔助教材。如果CLRS的厚度讓人望而生畏,這本200多頁的小讀本剛好合適帶你入門。
書中沒有涉及編程語言,直接用文字描述算法,我用 JavaScript 對書中的算法進(jìn)行描述。
二分查找
在排好序的數(shù)組中查找目標(biāo)值x。在p到r區(qū)間中,總是取索引為q的中間值與x進(jìn)行比較,如果array[q]大于x,則比較p到q-1區(qū)間,否則比較q+1到r區(qū)間,直到array[q]等于x或p>r。
// 利用二分法在已經(jīng)排好序的數(shù)組中查找值xfunction binarySearch(array, x) { let p = 1; let r = array.length - 1; while (p <= r) { let q = Math.round((p + r) / 2); //四舍五入取整 if (array[q] === x) { return q; } else { if (array[q] > x) { // 如果q沒有減一,遇到找不到x的情況, // 就會(huì)陷入while循環(huán)中出不來,因?yàn)閜會(huì)一直等于r r = q - 1; } else { p = q + 1; } } } return 'NOT-FOUND';}
也可以把二分查找寫成遞歸風(fēng)格。
// 二分法遞歸風(fēng)格function recursiveBinarySearch(array, p, r, x) { if (p > r) { // 基礎(chǔ)情況 console.log('NOT-FOUND'); return; } let q = Math.round((p + r) / 2); if (array[q] === x) { // 基礎(chǔ)情況 console.log(q); return; } else { if (array[q] > x) { recursiveBinarySearch(array, p, q-1, x); } else { recursiveBinarySearch(array, q+1, r, x); } }}
排序
選擇排序
從第一個(gè)元素開始遍歷,把該元素跟在它之后的所有元素進(jìn)行比較,選出最小的元素放入該位置。
以書架上的書本排序?yàn)槔?。我們看一眼書架上的第一本書的書名,接著與第二本進(jìn)行比較,如果第二本書的書名第一個(gè)字母的順序小于第一本,那我們忘掉第一本書的書名,記下第二本書的書名,此時(shí)我們并沒有對書籍進(jìn)行移動(dòng),只是比較了書名的順序,并把順序最小的書名記在腦子里。直到與最后一本進(jìn)行比較結(jié)束,我們把腦子里順序最小的書名對應(yīng)的書與第一本書對調(diào)了一下位置。
function selectionSort (array) { for (let i = 0; i < array.length - 1; i++) { let smallest = i; let key = array[i]; // 保存當(dāng)前值 for (let j = i + 1; j < array.length; j++) { // 比較當(dāng)前值和最小值,如果當(dāng)前值小于最小值則把當(dāng)前值的索引賦給smallest if (array[j] < array[smallest]) { smallest = j; } } // 最小值和當(dāng)前值交換 array[i] = array[smallest]; array[smallest] = key; } return array;}
選擇排序效率很低,因?yàn)檫x擇排序進(jìn)行了較多的比較操作,但移動(dòng)元素的操作次數(shù)很少。所以當(dāng)遇到移動(dòng)元素相當(dāng)耗時(shí)——或者它們所占空間很大或者它們存儲(chǔ)在一個(gè)存儲(chǔ)較慢的設(shè)備中——那么選擇排序可能是一個(gè)合適的算法。
插入排序
以書架為例,假設(shè)前4個(gè)位置已經(jīng)排好序了,我們拿起第五本書與第四本進(jìn)行比較,如果第四本大于第五本,把第四本向右移動(dòng)一個(gè)位置,再把第三本與第五本進(jìn)行比較,如果第三本還大于第五本,把第三本向右移動(dòng)一個(gè)位置,剛好放入第四本空出來的位置。直到遇到一本小于第五本的書或者已經(jīng)沒有書可以比較了,把第五本書插入小于它的那本書的后面。
function insertionSort (array) { for (let i = 1; i < array.length; i++) { let key = array[i]; // 把當(dāng)前操作值保存到key中 let j = i - 1; // j 為當(dāng)前值的前一位 // 在j大于等于0且前一位大于當(dāng)前值時(shí),前一位向右移動(dòng)一個(gè)位置 while (j >= 0 && array[j] > key) { array[j+1] = array[j]; j -= 1; }; // 直到遇到array[j]小于當(dāng)前操作值或者j小于0時(shí),把當(dāng)前值插入所空出來的位置 array[j+1] = key; } return array;}
插入排序與選擇排序時(shí)間差不多,如果移動(dòng)操作太過耗時(shí)最好用選擇排序。插入排序適用于數(shù)組一開始就已經(jīng)“基本有序”的狀態(tài)。
歸并排序
歸并排序中使用一個(gè)被稱為分治法的通用模式。在分治法中,我們將原問題分解為類似原問題的子問題,并遞歸的求解這些子問題,然后再合并這些子問題的解來得出原問題的解。
分解:把一個(gè)問題分解為多個(gè)子問題,這些子問題是更小實(shí)例上的原問題。
解決:遞歸地求解子問題。當(dāng)子問題足夠小時(shí),按照基礎(chǔ)情況來求解。
合并:把子問題的解合并成原問題的解。
在歸并排序中,我們把數(shù)組不斷用二分法分解成兩個(gè)小數(shù)組,直到每個(gè)數(shù)組只剩一個(gè)元素(基礎(chǔ)情況)。再把小數(shù)組排好序并進(jìn)行合并。
// array: 數(shù)組// p: 開始索引// r: 末尾索引function mergeSort (array, p, r) { if (p >= r) { return; } else { // 不可以用四舍五入,找了一夜的bug竟然是因?yàn)樗纳嵛迦脒@個(gè)小蹄子 let q = Math.floor((p + r) / 2); // 遞歸調(diào)用,把數(shù)組拆分成兩部分,直到每個(gè)數(shù)組只剩一個(gè)元素 mergeSort(array, p, q); mergeSort(array, q + 1, r); // 把兩個(gè)子數(shù)組排序并合并 merge(array, p, q, r); } return array;}
程序的真正工作發(fā)生在 merge
函數(shù)中。歸并排序不是原址的。
假設(shè)有兩堆已經(jīng)排好序的書,書堆A和書堆B。把A中的第一本與B中的第一本拿起來比較,小的那本放入書架中,再把A中的“第一本”和B中的“第一本”進(jìn)行比較,此時(shí)的“第一本”不一定是剛才的第一本了,因?yàn)橐呀?jīng)有一本書放入書架了,不過該書堆的“第一本”任然是該書堆中最小的一本。直到把兩堆書全部放入書架。
function merge (array, p, q, r) { let n1 = q - p + 1; // 子數(shù)組的長度 let n2 = r - q; // 把兩個(gè)子數(shù)組拷貝到B、C數(shù)組中 // slice不包含end參數(shù),所以end參數(shù)要加一 let arrB = array.slice(p, q + 1); let arrC = array.slice(q + 1, r + 1); // 兩個(gè)數(shù)組的最后一個(gè)元素設(shè)為無窮大值,確保了無需再檢查數(shù)組中是否有剩余元素 arrB[n1] = Number.MAX_VALUE; arrC[n2] = Number.MAX_VALUE; // 因?yàn)榛靥钊朐瓟?shù)組的個(gè)數(shù)是固定的,所以無窮大值不會(huì)被填入,也無需判斷是否有剩余 // 一旦B、C兩個(gè)數(shù)組中的所有元素拷貝完就自動(dòng)終止 // 因?yàn)锽、C中的元素已經(jīng)按照非遞減順序排好了,所以最小索引值對應(yīng)的就是最小值 // 兩個(gè)子數(shù)組的最小值比較,小的則為當(dāng)前最小值 let i = j = 0; for (let k = p; k < r + 1; k++) { if (arrB[i] < arrC[j]) { array[k] = arrB[i]; i++; } else { array[k] = arrC[j]; j++; } } return;}
由于歸并排序不是在原址上工作,需要拷貝出子數(shù)組,如果你的儲(chǔ)存空間較小或空間非常寶貴,可能不適合使用歸并排序。
快速排序
與歸并排序類似,快速排序也是使用分治模式。與歸并排序不同的是,快速排序是在原址上工作的,歸并排序是拷貝出兩個(gè)子數(shù)組進(jìn)行操作并不在原址上工作。
在書架中隨機(jī)挑選一本書作為主元(這里我們總是選擇位于書架最末尾的那本書),所有小于主元的書放在主元左側(cè),所有大于或等于主元的書放在主元右側(cè),這時(shí)就把書分為左右兩組(不包括主元),再分別對這兩組書進(jìn)行相同的操作(遞歸),直到子數(shù)組只剩一本書觸發(fā)基礎(chǔ)情況。
function quickSort (array, p, r) { if (p >= r) { return; } else { let q = partition(array, p, r); // 遞歸中不再包含array[q],因?yàn)樗呀?jīng)處在正確的位置(左邊所有元素都小于它,右邊所有元素都大于或等于它) // 如果遞歸調(diào)用還包含array[q],就會(huì)陷入死循環(huán) quickSort(array, p, q - 1); quickSort(array, q + 1, r); } return array;}
重要的操作都在 partition
函數(shù)中。這個(gè)函數(shù)把數(shù)組按照大于或小于主元分為左右兩堆,并返回主元所在位置的索引q。注意,左右兩堆數(shù)組并不是有序的(見上圖),只是大于或小于主元。
在書架中隨機(jī)挑選一本書作為主元(這里我們總是選擇位于書架最末尾的那本書),此時(shí)主元位于最末尾。還未進(jìn)行比較的為未知組,稱為組U,位于主元左側(cè)。小于主元的稱為組L,位于書架最左側(cè)。大于或小于主元的稱為組R,位于組L左側(cè)組U右側(cè)。如下圖。
我們拿起組U中最左側(cè)的那本書,與主元進(jìn)行比較,如果小于主元?jiǎng)t放入組L,大于或等于主元?jiǎng)t放入組R。放入組R的操作比較簡單,只需要把組R和組U的分割線往右移一位,無需移動(dòng)書籍。
放入組L的操作則比較復(fù)雜。我們將它與組R中最左側(cè)的書籍進(jìn)行調(diào)換,并將組L和組R之間的分割線向右移一位,將組R和組U的分割線向右移一位。如下圖
// 主元:數(shù)組中隨機(jī)挑選單獨(dú)的一個(gè)數(shù)(這里我們總是選數(shù)組中的最后一位)array[r]// 組L(左側(cè)組):所有小于主元的數(shù),array[p...q-1]// 組R(右側(cè)組):所有大于或等于主元的數(shù),array[q...u-1]// 組U(未知組):還未進(jìn)行比較的數(shù),array[u...r-1]function partition(array, p, r) { let q = p; // 遍歷array[p...r-1] for (let u = p; u < r; u++) { // 如果未知數(shù)小于主元,放入組L if (array[u] < array[r]) { // 把未知數(shù)和組R最左側(cè)值(array[q])進(jìn)行交換,并讓q和u往右移一位(加1) let key = array[q]; array[q] = array[u]; array[u] = key; q += 1; } // 如果未知數(shù)大于或等于主元,放入組R // 無需其他操作,只需要把u往右移一位 } // 把主元和組R最左側(cè)值(array[q])進(jìn)行交換,讓主元位于組L合組R中間 let key = array[q]; array[q] = array[r]; array[r] = key; return q;}
本例的快速排序總是選擇最末尾的元素作為主元,稱為確定的快速排序。如果每次選擇主元時(shí)都從數(shù)組中隨機(jī)選擇,則稱為隨機(jī)快速排序,隨機(jī)快速排序在測試中會(huì)快于確定的快速排序。
根據(jù)數(shù)據(jù)量的不同,儲(chǔ)存空間的大小,存儲(chǔ)速度的快慢,每個(gè)排序方法都有不同的表現(xiàn),并不是說哪個(gè)方法一定是最快的,也不一定最快就是最好的,合適才是最好的。
http://www.cnblogs.com/chaohangz/p/6719874.html