12 | 排序(下):如何用快排思想在O(n)内查找第K大元素?
算法對(duì)比:
| 算法 | 時(shí)間復(fù)雜度 | 適合場(chǎng)景 |
| 冒泡排序、插入排序、選擇排序 | O(n2) | 小規(guī)模數(shù)據(jù) |
| 歸并排序、快速排序 | O(nlogn) | 大規(guī)模數(shù)據(jù) |
歸并排序和快速排序都用到了分治思想,非常巧妙。我們可以借鑒這個(gè)思想,來(lái)解決非排序的問(wèn)題,比如:如何在 O(n) 的時(shí)間復(fù)雜度內(nèi)查找一個(gè)無(wú)序數(shù)組中的第 K 大元素?
歸并排序
使用分治思想:大問(wèn)題分解為小問(wèn)題,分而治之,小問(wèn)題解決了,大問(wèn)題也就解決了。
歸并排序的遞推公式:
遞推公式 merger_sort(p..r) = merge(merger_sort(p..q),merger_sort(q+1..r)) 終止條件 p>=r 不再繼續(xù)分解歸并排序的偽代碼
merge_sort(A,n){merge_sort_c(A,0,n-1) } merge_sort_c(A,p,r){if p >= r then returnq = (p+r)/2;//分治遞歸merge_sort_c(A,p,q)merge_sort_c(A,q+1,r)//合并merge(A[p...r],A[p...q],A[q+1...r])}merge()函數(shù)的實(shí)現(xiàn)思路:
申請(qǐng)一個(gè)臨時(shí)數(shù)組 tmp,大小與 A[p...r]相同。我們用兩個(gè)游標(biāo) i 和 j,分別指向 A[p...q]和 A[q+1...r]的第一個(gè)元素。比較這兩個(gè)元素 A[i]和 A[j],如果 A[i]<=A[j],就把 A[i]放入到臨時(shí)數(shù)組 tmp,并且 i 后移一位,否則將 A[j]放入到數(shù)組 tmp,j 后移一位。繼續(xù)上述比較過(guò)程,直到其中一個(gè)子數(shù)組中的所有數(shù)據(jù)都放入臨時(shí)數(shù)組中,再把另一個(gè)數(shù)組中的數(shù)據(jù)依次加入到臨時(shí)數(shù)組的末尾,這個(gè)時(shí)候,臨時(shí)數(shù)組中存儲(chǔ)的就是兩個(gè)子數(shù)組合并之后的結(jié)果了。最后再把臨時(shí)數(shù)組 tmp 中的數(shù)據(jù)拷貝到原數(shù)組 A[p...r]中。
merge函數(shù)的偽代碼
merge(A[p...r], A[p...q], A[q+1...r]) {var i := p,j := q+1,k := 0 // 初始化變量i, j, kvar tmp := new array[0...r-p] // 申請(qǐng)一個(gè)大小跟A[p...r]一樣的臨時(shí)數(shù)組while i<=q AND j<=r do {if A[i] <= A[j] {tmp[k++] = A[i++] // i++等于i:=i+1} else {tmp[k++] = A[j++]}}// 判斷哪個(gè)子數(shù)組中有剩余的數(shù)據(jù)var start := i,end := qif j<=r then start := j, end:=r// 將剩余的數(shù)據(jù)拷貝到臨時(shí)數(shù)組tmpwhile start <= end do {tmp[k++] = A[start++]}// 將tmp中的數(shù)組拷貝回A[p...r]for i:=0 to r-p do {A[p+i] = tmp[i]} }歸并排序性能分析
穩(wěn)定排序、非原地排序、時(shí)間復(fù)雜度為?O(nlogn),歸并排序的執(zhí)行效率與要排序的原始數(shù)組的有序程度無(wú)關(guān),所以其時(shí)間復(fù)雜度是非常穩(wěn)定的,不管是最好情況、最壞情況,還是平均情況,時(shí)間復(fù)雜度都是 O(nlogn)。空間復(fù)雜度為O(n)。
快速排序
快排的思想是這樣的:如果要排序數(shù)組中下標(biāo)從 p 到 r 之間的一組數(shù)據(jù),我們選擇 p 到 r 之間的任意一個(gè)數(shù)據(jù)作為 pivot(分區(qū)點(diǎn))。我們遍歷 p 到 r 之間的數(shù)據(jù),將小于 pivot 的放到左邊,將大于 pivot 的放到右邊,將 pivot 放到中間。經(jīng)過(guò)這一步驟之后,數(shù)組 p 到 r 之間的數(shù)據(jù)就被分成了三個(gè)部分,前面 p 到 q-1 之間都是小于 pivot 的,中間是 pivot,后面的 q+1 到 r 之間是大于 pivot 的。根據(jù)分治、遞歸的處理思想,我們可以用遞歸排序下標(biāo)從 p 到 q-1 之間的數(shù)據(jù)和下標(biāo)從 q+1 到 r 之間的數(shù)據(jù),直到區(qū)間縮小為 1,就說(shuō)明所有的數(shù)據(jù)都有序了。
快排的遞推公式
遞推公式: quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1… r)終止條件: p >= r快排的偽代碼
// 快速排序,A是數(shù)組,n表示數(shù)組的大小 quick_sort(A, n) {quick_sort_c(A, 0, n-1) } // 快速排序遞歸函數(shù),p,r為下標(biāo) quick_sort_c(A, p, r) {if p >= r then returnq = partition(A, p, r) // 獲取分區(qū)點(diǎn)quick_sort_c(A, p, q-1)quick_sort_c(A, q+1, r) }歸并排序中有一個(gè) merge() 合并函數(shù),我們這里有一個(gè) partition() 分區(qū)函數(shù)。partition() 分區(qū)函數(shù)實(shí)際上我們前面已經(jīng)講過(guò)了,就是隨機(jī)選擇一個(gè)元素作為 pivot(一般情況下,可以選擇 p 到 r 區(qū)間的最后一個(gè)元素),然后對(duì) A[p...r]分區(qū),函數(shù)返回 pivot 的下標(biāo)。
如果不考慮空間消耗的,我們申請(qǐng)2個(gè)臨時(shí)數(shù)組X,Y,遍歷A[p...r],將小于pivot的元素復(fù)制到X,大于pivot的元素復(fù)制到Y(jié),最后再將數(shù)組X和Y的數(shù)據(jù)順序拷貝到A[p...r]即可。示意圖:
如果我們希望快排是原地排序算法,那它的空間復(fù)雜度得是 O(1),那 partition() 分區(qū)函數(shù)就不能占用太多額外的內(nèi)存空間,我們就需要在 A[p...r]的原地完成分區(qū)操作:
/**游標(biāo)i對(duì)應(yīng)的是什么數(shù)字并不關(guān)心,它只是一個(gè)分界線,游標(biāo)i前面的區(qū)間都是小于pivot的,后面遍歷到某個(gè)數(shù)時(shí),如果小于pivot,那么就跟當(dāng)前的游標(biāo)i進(jìn)行數(shù)字交換,然后游標(biāo)i++ 游標(biāo)j是遍歷游標(biāo),一直往前走 游標(biāo)i是分界游標(biāo),只有出現(xiàn)交換操作才會(huì)往前走 **/ partition(A, p, r) {pivot := A[r]i := pfor j := p to r-1 do {if A[j] < pivot {swap A[i] with A[j]i := i+1}}swap A[i] with A[r]return i快排性能分析
通過(guò)游標(biāo) i 把 A[p...r-1]分成兩部分。A[p...i-1]的元素都是小于 pivot 的,我們暫且叫它“已處理區(qū)間”,A[i...r-1]是“未處理區(qū)間”。我們每次都從未處理的區(qū)間 A[i...r-1]中取一個(gè)元素 A[j],與 pivot 對(duì)比,如果小于 pivot,則將其加入到已處理區(qū)間的尾部,也就是 A[i]的位置。在數(shù)組某個(gè)位置插入元素,需要搬移數(shù)據(jù),非常耗時(shí)。當(dāng)時(shí)我們也講了一種處理技巧,就是交換,在 O(1) 的時(shí)間復(fù)雜度內(nèi)完成插入操作。借助這個(gè)思想,只需要將 A[i]與 A[j]交換,就可以在 O(1) 時(shí)間復(fù)雜度內(nèi)將 A[j]放到下標(biāo)為 i 的位置。因此快排不是穩(wěn)定排序算法
快排和歸并排序區(qū)別:歸并排序是自頂向下,先處理子問(wèn)題,然后再合并;快排是自底向上,先分區(qū),然后再處理子問(wèn)題。
歸并排序雖然是穩(wěn)定排序,但是是非原地排序;而快速可以實(shí)現(xiàn)原地排序。歸并排序的時(shí)間復(fù)雜度跟原數(shù)據(jù)的是否有序無(wú)關(guān),始終是O(nlogn);快速排序的時(shí)間復(fù)雜度在極端情況下:如果數(shù)組中的數(shù)據(jù)原來(lái)已經(jīng)是有序的了,比如 1,3,5,6,8。如果每次選擇最后一個(gè)元素作為 pivot,那每次分區(qū)得到的兩個(gè)區(qū)間都是不均等的。需要進(jìn)行大約 n 次分區(qū)操作,才能完成快排的整個(gè)過(guò)程。每次分區(qū)我們平均要掃描大約 n/2 個(gè)元素,這種情況下,快排的時(shí)間復(fù)雜度就從 O(nlogn) 退化成了 O(n2)。但是也有很多方法將這個(gè)概率降到很低。所以實(shí)際中,快排應(yīng)用最多而歸并排序很少使用。
標(biāo)題解答
快排核心思想就是分治和分區(qū),我們可以利用分區(qū)的思想,來(lái)解答開篇的問(wèn)題:O(n) 時(shí)間復(fù)雜度內(nèi)求無(wú)序數(shù)組中的第 K 大元素。比如4, 2, 5, 12, 3 這樣一組數(shù)據(jù),第 3 大元素就是 4。
我們選擇數(shù)組區(qū)間 A[0...n-1]的最后一個(gè)元素 A[n-1]作為 pivot,對(duì)數(shù)組 A[0...n-1]原地分區(qū),這樣數(shù)組就分成了三部分,A[0...p-1]、A[p]、A[p+1...n-1]。如果 p+1=K,那 A[p]就是要求解的元素;如果 K>p+1, 說(shuō)明第 K 大元素出現(xiàn)在 A[p+1...n-1]區(qū)間,我們?cè)侔凑丈厦娴乃悸愤f歸地在 A[p+1...n-1]這個(gè)區(qū)間內(nèi)查找。同理,如果 K<p+1,那我們就在 A[0...p-1]區(qū)間查找。
?
總結(jié)
以上是生活随笔為你收集整理的12 | 排序(下):如何用快排思想在O(n)内查找第K大元素?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: LSTM神经网络图解
- 下一篇: Jenkins教程:使用Jenkins进