你知道怎么样排序才能做到多快好省?
小智最近迷上了計算機算法,今天過來給大家講講排序算法。
準備講排序算法之前,我們還是要先回顧一下排序這個概念。
排序是一門古老的科學。排序問題,用數(shù)學的方式可以表達如下
問題輸入:給定n個數(shù),a1,? a2,? a3, ..., an
要求輸出:給出n個數(shù)的排列,a1', a2', a3', ..., an',使得?a1'?≤?a2'?≤?a3'?≤?...?≤?an'。
從更形象的角度來說,排序就是一群人站成一列,高的站前面,矮的站在后面。
一個家庭的所有成員按身高排列的示意圖(小智實在找不到圖了,畫一個示意-_-)
關(guān)于排序,有幾個描述算法特征的詞語:
穩(wěn)定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不穩(wěn)定:如果a原本在b的前面,而a=b,排序之后a可能會出現(xiàn)在b的后面;
時間復雜度:一個算法執(zhí)行所耗費的時間。
空間復雜度:運行完一個程序所需內(nèi)存的大小。
ok,了解完排序的相關(guān)概念以后,我有一個新的問題了:如何評價一個算法?小智認為,可以用一個詞語來形容一個優(yōu)秀的算法:多快好省。
什么是“多快好省”?我們從算法的角度來解析一下:
多:能夠可靠處理大規(guī)模的數(shù)據(jù)
快:算法的時間復雜度要更低的
好:算法實現(xiàn)要符合穩(wěn)定性
省:算法的空間復雜度要更低的
到底哪些算法能夠符合“多快好省”這個目標呢?我們先看一下目前的算法分類:
基于比較的排序算法的特征總結(jié)
注:本文不討論非基于比較算法,比如計數(shù)排序、桶排序和基數(shù)排序
“多”的角度:從數(shù)據(jù)量的處理性能來看,需要不受隨機因素影響的算法。冒泡算法、插入排序、快速排序這三個算法最好情況和最壞情況相差了一個數(shù)量級,這種不可靠性可能影響數(shù)據(jù)處理的效率。
“快”的角度:從時間復雜度的角度來看,希爾排序、歸并排序、快速排序、堆排序的算法復雜度比較好,均低于O(n2)。
“好”的角度:從穩(wěn)定性的角度來看,歸并排序、插入排序、冒泡排序是穩(wěn)定的
“省”的角度:從空間復雜度的角度來看,冒泡排序、插入排序、希爾排序、堆排序,空間復雜度為O(1)。其中,值得注意的是,一般寫法的歸并排序,空間復雜度為O(n),經(jīng)過優(yōu)化以后,使用空間可以為O(1)。
大家都看出了我的傾向了吧?小智從“多快好省”這四個方面,分析發(fā)現(xiàn),歸并算法在這四個方面表現(xiàn)不錯。這次小智決定跟大家探討一下歸并算法。
為什么歸并排序能夠做到多快好省?
解答這個問題之前,我們先了解一下什么是歸并排序。
歸并算法(merge sort)是一個分治算法(divide and conquer algorithm),馮·諾依曼(John von Neumann?)在1945年發(fā)明了這個算法。這個算法將已有序的子序列合并,得到完全有序的序列,即先使每個子序列有序,再使子序列段間有序。
實際上,歸并算法的算法步驟非常簡單:
第1步:拆分,把長度為n的輸入序列分成兩個長度為n/2的子序列
第2步:遞歸調(diào)用,對這兩個子序列分別使用歸并排序
第3步:合并,將兩個排序好的子序列合并成一個最終的排序序列
這幾步,我們來分別地講一下:
什么是拆分?將一個長序列拆分成兩個子序列。這個屬于分治法中的“分”,將大問題降解為小問題。
將長序列拆分成兩個子序列示意圖
如果n為奇數(shù),不能被2整除,可以將n/2向上取整,這個對算法沒有影響。
有一個特殊情況是需要注意的:當子序列長度為1時,已經(jīng)沒有辦法進行拆分了,可以直接執(zhí)行下一步。
什么是遞歸調(diào)用?實際上是重復調(diào)用歸并排序的程序。我們假想一下,在最開始的情況下,執(zhí)行完第1步,長序列被拆分為2個子序列。接著執(zhí)行第二步時,直接對子序列進行歸并排序,那么下一步則是繼續(xù)拆分子序列。如此下去,直至將長序列拆分到無法分拆的情形。
我們以圖來說明會更加清晰一點:
歸并算法遞歸調(diào)用的分拆效果
經(jīng)過不斷地遞歸調(diào)用,要處理的子序列長度越來越短,最后直至子序列長度為1。
有一個特殊情況是需要注意的:當子序列長度為1時,已經(jīng)不需要繼續(xù)調(diào)用歸并排序了,因此可以直接跳過遞歸調(diào)用這個步驟。
什么是合并?是將兩個排好序的子序列,合成一個也是排好序的序列。這個合并算法也是比較簡單。
合并過程可以表述為:對于兩個子序列,X和Y,其中X和Y都是升序排列,以及知道X和Y每個數(shù)據(jù)在原來長序列的位置,分別為Xp和Yp,如何將X和Y合并成一個穩(wěn)定的升序排列?
由于這兩個子序列都是升序排列,因此我們同時遍歷這兩個子序列,從遍歷的位置各拿出一個數(shù)字,哪個數(shù)字小就把它拿出來插入合并序列中。如果遇到拿出來的兩個數(shù)字相等的情況,則比較原來序列的位置,哪個小就把它拿出來即可。
按照上述合并步驟,我們可以寫出合并偽代碼了:
i = 1, j = 1, l = 1
申請一個臨時變量temp,用于儲存合并后序列
如果 X[i] < Y[j]: temp[l] = X[i], l = l + 1, i = i + 1
如果 X[i] < Y[j]: temp[l] = Y[j], l = l + 1, j = j + 1
如果 X[i] = Y[j]:
? ? 如果 Xp[i] < Yp[j]: temp[l] = X[i], l = l + 1, i = i + 1
? ? 如果 Xp[i] > Yp[j]: temp[l] = Y[j], l = l + 1, j = j + 1
我將合并流程補充到整個歸并算法的執(zhí)行流程當中,以圖的形式做了一個示例:
歸并排序整體流程示意圖
歸并排序我們了解完了,我們可以開始回答為什么歸并排序能夠表現(xiàn)如此出色:
“多”的角度:歸并排序為什么能保證可靠地處理數(shù)據(jù)?
歸并排序的拆分的流程,跟數(shù)據(jù)的原始排序沒有關(guān)系,無論是最壞情況還是最好情況,時間復雜度都是一致的,能夠穩(wěn)定處理大量數(shù)據(jù)。
“快”的角度:歸并排序的時間復雜度為什么是?O(n log n)?
設(shè)歸并排序所消耗的時間為T(n),進行歸并排序的拆分操作以后,將原來的問題劃分為原來規(guī)模的二分之一,每一個劃分出來的問題將耗費時間是T(n/2),最后把這兩部分有序的數(shù)組合并在一起所花的時間為O(n),因此:
T(n) = 2×T(n/2) + O(n)
而劃分次數(shù)最多可以有log2?n次,因此累加起來可得T(n) = O(n log n)
“好”的角度:歸并排序為什么能保證穩(wěn)定呢?
歸并排序?qū)⑿蛄蟹指畹阶钚?#xff0c;而后將序列進行合并。假如兩個數(shù)字是相等的,在分割子序列的時候,不會更改他們之間的相對位置;在合并子序列的時候,只要能夠做到先將位置在前的數(shù)字放到合并數(shù)組,這樣就保證了排序結(jié)果的穩(wěn)定。
“省”的角度:歸并排序為什么空間復雜度可以為O(1)?
歸并排序雖然原始寫法的確是這樣的,但是算法是可以改造的,我們只需要做到原地排序就可以了,而對于歸并排序而言,原地排序的關(guān)鍵是合并。歸并排序的原來寫法,當進行合并時,是申請一個臨時空間,將合并的數(shù)據(jù)放到臨時空間當中,這個算法復雜度為?O(n)?。
實際上,合并過程可以直接使用已有的位置。假設(shè)我們要合并的兩段數(shù)組還是[13, 23, 37, 54]和[11,17, 26,29],在歸并算法里,它們可以看成一個連續(xù)的數(shù)組形式:
[13, 23, 37, 54,?11, 17, 26, 29]
首先我們檢查兩段數(shù)組的第一個數(shù),發(fā)現(xiàn)13 > 11,那么11這個數(shù)要拿出來,原來是要放到臨時空間的第一個位置,但我們可以利用數(shù)組已有的位置,將11直接移到數(shù)組的第一個位置,然后原來第一段數(shù)組往后移一個位置,這樣數(shù)組變?yōu)?/span>
[11,?13, 23, 37, 54,?17, 26, 29]
而后我們繼續(xù)按照這種方式,合并[13, 23. 37. 54]和[17, 26 29]這兩段數(shù)組。
可以發(fā)現(xiàn),這種方式空間復雜度只需一個臨時變量,用于協(xié)助數(shù)字移動,因此空間復雜度為O(1)。當然這樣優(yōu)化增加了移動的次數(shù),為了空間,就要損失時間了。
總結(jié)一下,歸并排序是個寶,一個多快好省的排序算法,大家如果遇到數(shù)據(jù)排序問題,可以優(yōu)先考慮它。當然,歸并排序并不是全能的,在某些類型問題下,有些算法比歸并排序表現(xiàn)更為出色,往后小智會給大家解讀。
總結(jié)
以上是生活随笔為你收集整理的你知道怎么样排序才能做到多快好省?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 化学版2048,你玩过吗?内含游戏链接
- 下一篇: 我用Python玩小游戏“跳一跳”,瞬间