如需轉(zhuǎn)載, 請咨詢作者, 并且注明出處.
有任何問題, 可以關(guān)注我的微博: coderwhy, 或者添加我的微信: 372623326
排序算法是筆試中經(jīng)常出現(xiàn)的, 其實(shí)排序算法是很容易考察出一個(gè)人的思維水平的.
排序算法有很多: 冒泡排序/選擇排序/插入排序/歸并排序/計(jì)數(shù)排序(counting sort)/基數(shù)排序(radix sort)/希爾排序/堆排序/桶排序.
我們這里不一一列舉它們的實(shí)現(xiàn)思想, 而是選擇幾個(gè)簡單排序和高級排序.(后續(xù)有機(jī)會給大家視頻講解)
簡單排序: 冒泡排序 - 選擇排序 - 插入排序
高級排序: 希爾排序 - 快速排序
其他排序的理論和思想, 大家可以自行學(xué)習(xí).
一. 排序介紹
我們先對排序有個(gè)簡單的認(rèn)識, 然后開始介紹幾種簡單排序.
排序介紹
- 一旦我們將數(shù)據(jù)放置在某個(gè)數(shù)據(jù)結(jié)構(gòu)中存儲起來后(比如數(shù)組), 就可能根據(jù)需求對數(shù)據(jù)進(jìn)行不同方式的排序
- 比如對姓名按字母排序
- 對學(xué)生按年齡排序
- 對商品按照價(jià)格排序
- 對城市按照面積或者人口數(shù)量排序
- 對恒星按照大小排序
- 等等
- 由于排序非常重要而且可能非常耗時(shí), 所以它已經(jīng)成為一個(gè)計(jì)算機(jī)科學(xué)中廣泛研究的課題, 而且人們已經(jīng)研究出一套成熟的方案來實(shí)現(xiàn)排序.
- 但是, 我們學(xué)習(xí)已有的排序方法是非常有必要的.
如何排序?
- 需求: 對一組身高不等的10個(gè)人進(jìn)行排序
- 人來排序:
- 如果是人來排序事情會非常簡單, 因?yàn)槿酥灰獟哌^去一眼就能看出來誰最高誰最低.
- 然后讓最低(或者最高)的站在前面, 其他人依次后移.
- 按照這這樣的方法. 依次類推就可以了.
- 計(jì)算機(jī)來排序:
- 計(jì)算機(jī)有些笨拙, 它只能執(zhí)行指令. 所以沒辦法一眼掃過去.
- 計(jì)算機(jī)也很聰明, 只要你寫出了正確的指令, 可以讓它幫你做無數(shù)次類似的事情而不用擔(dān)心出現(xiàn)錯誤.
- 并且計(jì)算機(jī)排序也無需擔(dān)心數(shù)據(jù)量的大小.(想象一樣, 讓人排序10000個(gè), 甚至更大的數(shù)據(jù)項(xiàng)你還能一眼掃過去嗎?)
- 人在排序時(shí)不一定要固定特有的空間, 他們可以相互推推嚷嚷就騰出了位置, 還能互相前后站立.
- 但是計(jì)算機(jī)必須有嚴(yán)密的邏輯和特定的指令.
- 計(jì)算機(jī)排序的特點(diǎn):
- 計(jì)算機(jī)不能像人一樣, 一眼掃過去這樣通覽所有的數(shù)據(jù).
- 它只能根據(jù)計(jì)算機(jī)的比較操作原理, 在同一個(gè)時(shí)間對兩個(gè)隊(duì)員進(jìn)行比較.
- 在人類看來很簡單的事情, 計(jì)算機(jī)的算法卻不能看到全景, 因此它只能一步步解決具體問題和遵循一些簡單的規(guī)則.
- 簡單算法的主要操作:
- 比較兩個(gè)數(shù)據(jù)項(xiàng).
- 交換兩個(gè)數(shù)據(jù)項(xiàng), 或者復(fù)制其中一項(xiàng).
- 但是, 每種算法具體實(shí)現(xiàn)的細(xì)節(jié)有所不同.
創(chuàng)建列表
-
在開始排序前, 我們先來創(chuàng)建一個(gè)列表封裝我們的數(shù)據(jù)項(xiàng).
// 封裝ArrayList function ArrayList() { this.array = [] ArrayList.prototype.insert = function (item) { this.array.push(item) } ArrayList.prototype.toString = function () { return this.array.join() } }
-
初始化數(shù)據(jù)項(xiàng)
// 初始化數(shù)據(jù)項(xiàng) var list = new ArrayList() list.insert(3) list.insert(6) list.insert(4) list.insert(2) list.insert(11) list.insert(10) list.insert(5) alert(list)
二. 冒泡排序
冒泡排序算法相對其他排序運(yùn)行效率較低, 但是在概念上它是排序算法中最簡單的.
因此, 冒泡排序是在剛開始學(xué)習(xí)排序時(shí), 最適合學(xué)習(xí)的一種排序方式.
冒泡排序的思路
-
冒泡排序的思路:
- 對未排序的各元素從頭到尾依次比較相鄰的兩個(gè)元素大小關(guān)系
- 如果左邊的隊(duì)員高, 則兩隊(duì)員交換位置
- 向右移動一個(gè)位置, 比較下面兩個(gè)隊(duì)員
- 當(dāng)走到最右端時(shí), 最高的隊(duì)員一定被放在了最右邊
- 按照這個(gè)思路, 從最左端重新開始, 這次走到倒數(shù)第二個(gè)位置的隊(duì)員即可.
- 依次類推, 就可以將數(shù)據(jù)排序完成
-
冒泡排序的圖解:
img -
思路再分析:
- 第一次找出最高人放在最后, 我們需要兩個(gè)兩個(gè)數(shù)據(jù)項(xiàng)進(jìn)行比較, 那么這個(gè)應(yīng)該是一個(gè)循環(huán)操作.
- 第二次將次高的人找到放在倒數(shù)第二個(gè)位置, 也是兩個(gè)比較, 只是不要和最后一個(gè)比較(少了一次), 但是前面的兩個(gè)兩個(gè)比較也是一個(gè)循環(huán)操作.
- 第三次...第四次...
- 有發(fā)現(xiàn)規(guī)律嗎? 這應(yīng)該是一個(gè)循環(huán)中嵌套循環(huán), 并且被嵌套的循環(huán)次數(shù)越來越少的.
- 根據(jù)這個(gè)分析, 你能寫出代碼實(shí)現(xiàn)嗎?
冒泡排序的實(shí)現(xiàn)
-
冒泡排序的實(shí)現(xiàn):
ArrayList.prototype.bubbleSort = function () { // 1.獲取數(shù)組的長度 var length = this.array.length // 2.反向循環(huán), 因此次數(shù)越來越少 for (var i = length - 1; i >= 0; i--) { // 3.根據(jù)i的次數(shù), 比較循環(huán)到i位置 for (var j = 0; j < i; j++) { // 4.如果j位置比j+1位置的數(shù)據(jù)大, 那么就交換 if (this.array[j] > this.array[j+1]) { // 交換 this.swap(j, j+1) } } } } ArrayList.prototype.swap = function (m, n) { var temp = this.array[m] this.array[m] = this.array[n] this.array[n] = temp }
-
代碼解析:
- 代碼序號1: 獲取數(shù)組的長度.
- 代碼序號2: 我們現(xiàn)在要寫的外層循環(huán), 外層循環(huán)應(yīng)該讓i依次減少, 因此我們這里使用了反向的遍歷.
- 代碼需要3: 內(nèi)層循環(huán), 內(nèi)層循環(huán)我們使用 j < i. 因?yàn)樯厦娴膇在不斷減小, 這樣就可以控制內(nèi)層循環(huán)的次數(shù).
- 代碼需要4: 比較兩個(gè)數(shù)據(jù)項(xiàng)的大小, 如果前面的大, 那么就進(jìn)行交換.
-
代碼圖解流程:
img -
測試代碼:
// 測試冒泡排序 list.bubbleSort() alert(list) // 2,3,4,5,6,10,11
冒泡排序的效率
- 冒泡排序的比較次數(shù):
- 如果按照上面的例子來說, 一共有7個(gè)數(shù)字, 那么每次循環(huán)時(shí)進(jìn)行了幾次的比較呢?
- 第一次循環(huán)6次比較, 第二次5次比較, 第三次4次比較....直到最后一趟進(jìn)行了一次比較.
- 對于7個(gè)數(shù)據(jù)項(xiàng)比較次數(shù): 6 + 5 + 4 + 3 + 2 + 1
- 對于N個(gè)數(shù)據(jù)項(xiàng)呢? (N - 1) + (N - 2) + (N - 3) + ... + 1 = N * (N - 1) / 2
- 大O表示法:
- 大O表示法是描述性能和復(fù)雜度的一種表示方法.
- 推導(dǎo)大O表示法通常我們會使用如下規(guī)則:
- 用常量1取代運(yùn)行時(shí)間中的所有加法常量
- 在修改后的運(yùn)行次數(shù)函數(shù)中, 只保留最高階項(xiàng)
- 如果最高階項(xiàng)存在并且不是1, 則去除與這個(gè)項(xiàng)相乘的常數(shù).
- 通過大O表示法推到過程, 我們來推到一下冒泡排序的大O形式.
- N * (N - 1) / 2 = N2/2 - N/2,根據(jù)規(guī)則2, 只保留最高階項(xiàng), 編程N(yùn)2 / 2
- N2 / 2, 根據(jù)規(guī)則3, 去除常量, 編程N(yùn)2
- 因此冒泡排序的大O表示法為O(N2)
- 冒泡排序的交換次數(shù):
- 冒泡排序的交換次數(shù)是多少呢?
- 如果有兩次比較才需要交換一次(不可能每次比較都交換一次.), 那么交換次數(shù)為N2 / 4
- 由于常量不算在大O表示法中, 因此, 我們可以認(rèn)為交換次數(shù)的大O表示也是O(N2)
三. 選擇排序
選擇排序改進(jìn)了冒泡排序, 將交換的次數(shù)由O(N2)減少到O(N), 但是比較的次數(shù)依然是O(N2)
選擇排序的思路
-
選擇排序的思路:
- 選定第一個(gè)索引位置,然后和后面元素依次比較
- 如果后面的隊(duì)員, 小于第一個(gè)索引位置的隊(duì)員, 則交換位置
- 經(jīng)過一輪的比較后, 可以確定第一個(gè)位置是最小的
- 然后使用同樣的方法把剩下的元素逐個(gè)比較即可
- 可以看出選擇排序,第一輪會選出最小值,第二輪會選出第二小的值,直到最后
-
選擇排序的圖解
img -
思路再分析:
- 選擇排序第一次將第0位置的人取出, 和后面的人(1, 2, 3...)依次比較, 如果后面的人更小, 那么就交換.
- 這樣經(jīng)過一輪之后, 第一個(gè)肯定是最小的人.
- 第二次將第1位置的人取出, 和后面的人(2, 3, 4...)依次比較, 如果后面的人更小, 那么就交換.
- 這樣經(jīng)過第二輪后, 第二個(gè)肯定是次小的人.
- 第三輪...第四輪...直到最后就可以排好序了. 有發(fā)現(xiàn)規(guī)律嗎?
- 外層循環(huán)依次取出0-1-2...N-2位置的人作為index(N-1不需要取了, 因?yàn)橹皇K粋€(gè)了肯定是排好序的)
- 內(nèi)層循環(huán)從index+1開始比較, 直到最后一個(gè).
- 經(jīng)過分析, 你能寫出最終的算法嗎?
選擇排序的實(shí)現(xiàn)
-
選擇排序的實(shí)現(xiàn):
ArrayList.prototype.selectionSort = function () { // 1.獲取數(shù)組的長度 var length = this.array.length // 2.外層循環(huán): 從0位置開始取出數(shù)據(jù), 直到length-2位置 for (var i = 0; i < length - 1; i++) { // 3.內(nèi)層循環(huán): 從i+1位置開始, 和后面的內(nèi)容比較 var min = i for (var j = min + 1; j < length; j++) { // 4.如果i位置的數(shù)據(jù)大于j位置的數(shù)據(jù), 那么記錄最小的位置 if (this.array[min] > this.array[j]) { min = j } } // 5.交換min和i位置的數(shù)據(jù) this.swap(min, i) } }
-
代碼解析:
- 代碼序號1: 依然獲取數(shù)組的長度.
- 代碼序號2: 外層循環(huán), 我們已經(jīng)講過, 需要從外層循環(huán)的第0個(gè)位置開始, 依次遍歷到length - 2的位置.
- 代碼序號3: 先定義一個(gè)min, 用于記錄最小的位置, 內(nèi)層循環(huán), 內(nèi)層循環(huán)是從i+1位置開始的數(shù)據(jù)項(xiàng), 和i位置的數(shù)據(jù)項(xiàng)依次比較, 直到length-1的數(shù)據(jù)項(xiàng).
- 代碼序號4: 如果比較的位置i的數(shù)據(jù)項(xiàng), 大于后面某一個(gè)數(shù)據(jù)項(xiàng), 那么記錄最小位置的數(shù)據(jù).
- 代碼序號5: 將min位置的數(shù)據(jù), 那么i位置的數(shù)據(jù)交換, 那么i位置就是正確的數(shù)據(jù)了.
- 注意: 這里的交換是基于之前的交換方法, 這里直接調(diào)用即可.
-
代碼圖解流程:
img -
測試代碼:
// 測試選擇排序 list.selectionSort() alert(list) // 2,3,4,5,6,10,11
選擇排序的效率
- 選擇排序的比較次數(shù):
- 選擇排序和冒泡排序的比較次數(shù)都是N*(N-1)/2, 也就是O(N2).
- 選擇排序的交換次數(shù):
- 選擇排序的交換次數(shù)只有N-1次, 用大O表示法就是O(N).
- 所以選擇排序通常認(rèn)為在執(zhí)行效率上是高于冒泡排序的.
四. 插入排序
插入排序是簡單排序中效率最好的一種.
插入排序也是學(xué)習(xí)其他高級排序的基礎(chǔ), 比如希爾排序/快速排序, 所以也非常重要.
插入排序的思路
-
局部有序:
- 插入排序思想的核心是局部有序. 什么是局部有序呢?
- 比如在一個(gè)隊(duì)列中的人, 我們選擇其中一個(gè)作為標(biāo)記的隊(duì)員. 這個(gè)被標(biāo)記的隊(duì)員左邊的所有隊(duì)員已經(jīng)是局部有序的.
- 這意味著, 有一部門人是按順序排列好的. 有一部分還沒有順序.
-
插入排序的思路:
- 從第一個(gè)元素開始,該元素可以認(rèn)為已經(jīng)被排序
- 取出下一個(gè)元素,在已經(jīng)排序的元素序列中從后向前掃描
- 如果該元素(已排序)大于新元素,將該元素移到下一位置
- 重復(fù)上一個(gè)步驟,直到找到已排序的元素小于或者等于新元素的位置
- 將新元素插入到該位置后, 重復(fù)上面的步驟.
-
插入排序的圖解
img -
思路再分析:
- 插入排序應(yīng)該從下標(biāo)值1開始(因?yàn)?位置默認(rèn)可以被認(rèn)為是有序的)
- 從1位置開始取出元素, 并且判斷該元素的大小和0位置進(jìn)行比較, 如果1位置元素小于0位置元素, 那么交換, 否則不交換.
- 上面步驟執(zhí)行完成后, 0 - 1位置已經(jīng)排序好.
- 取出2位置的元素, 和1位置進(jìn)行比較:
- 如果2位置元素大于1位置元素, 說明2位置不需要任何動作. 0 - 1 - 2已經(jīng)排序好.
- 如果2位置元素小于1位置元素, 那么將1移動到2的位置, 并且2繼續(xù)和0進(jìn)行比較.
- 如果2位置元素大于0位置的元素, 那么將2位置放置在1的位置, 排序完成. 0 - 1 - 2搞定.
- 如果2位置元素小于1位置的元素, 那么將0位置的元素移動到1位置, 并且將2位置的元素放在0位置, 0 - 1 - 2搞定.
- 按照上面的步驟, 依次找到最后一個(gè)元素, 整個(gè)數(shù)組排序完成.
- 經(jīng)常上面的分析, 你能轉(zhuǎn)化成對應(yīng)的代碼嗎?
插入排序的實(shí)現(xiàn)
-
插入排序的實(shí)現(xiàn):
ArrayList.prototype.insertionSort = function () { // 1.獲取數(shù)組的長度 var length = this.array.length // 2.外層循環(huán): 外層循環(huán)是從1位置開始, 依次遍歷到最后 for (var i = 1; i < length; i++) { // 3.記錄選出的元素, 放在變量temp中 var j = i var temp = this.array[i] // 4.內(nèi)層循環(huán): 內(nèi)層循環(huán)不確定循環(huán)的次數(shù), 最好使用while循環(huán) while (j > 0 && this.array[j-1] > temp) { this.array[j] = this.array[j-1] j-- } // 5.將選出的j位置, 放入temp元素 this.array[j] = temp } }
-
代碼解析
- 代碼序號1: 獲取數(shù)組的長度.
- 代碼序號2: 外層循環(huán), 從1位置開始, 因?yàn)?位置可以默認(rèn)看成是有序的了.
- 代碼序號3: 記錄選出的i位置的元素, 保存在變量temp中. i默認(rèn)等于j
- 代碼序號4: 內(nèi)層循環(huán)
- 內(nèi)層循環(huán)的判斷j - 1位置的元素和temp比較, 并且j > 0.
- 那么就將j-1位置的元素放在j位置.
- j位置向前移.
- 代碼序號5: 將目前選出的j位置放置temp元素.
-
代碼的圖解流程(來自維基百科):
img -
測試代碼:
// 測試插入排序 list.insertionSort() alert(list) // 2,3,4,5,6,10,11
插入排序的效率
- 插入排序的比較次數(shù):
- 第一趟時(shí), 需要的最多次數(shù)是1, 第二趟最多次數(shù)是2, 依次類推, 最后一趟是N-1次.
- 因此是1 + 2 + 3 + ... + N - 1 = N * (N - 1) / 2.
- 然而每趟發(fā)現(xiàn)插入點(diǎn)之前, 平均只有全體數(shù)據(jù)項(xiàng)的一半需要進(jìn)行比較.
- 我們可以除以2得到 N * (N - 1) / 4. 所以相對于選擇排序, 其他比較次數(shù)是少了一半的.
- 插入排序的復(fù)制次數(shù):
- 第一趟時(shí), 需要的最多復(fù)制次數(shù)是1, 第二趟最多次數(shù)是2, 依次類推, 最后一趟是N-1次.
- 因此是1 + 2 + 3 + ... + N - 1 = N * (N - 1) / 2.
- 對于基本有序的情況
- 對于已經(jīng)有序或基本有序的數(shù)據(jù)來說, 插入排序要好很多.
- 當(dāng)數(shù)據(jù)有序的時(shí)候, while循環(huán)的條件總是為假, 所以它變成了外層循環(huán)中的一個(gè)簡單語句, 執(zhí)行N-1次.
- 在這種情況下, 算法運(yùn)行至需要N(N)的時(shí)間, 效率相對來說會更高.
- 另外別忘了, 我們的比較次數(shù)是選擇排序的一半, 所以這個(gè)算法的效率是高于選擇排序的.