快排問題
快排核心思想就是分區和分治。
算法原理
快排的思想是這樣的:如果要排序數組中下標從p到r之間的一組數據,我們選擇p到r之間的任意一個數據作爲pivot(分區點)。
然後遍歷p到r之間的數據,將小於pivot的放到左邊,將大於pivot的放到右邊,將povit放到中間。經過這一步之後,數組p到r之間的數據就分成了3部分,前面p到q-1之間都是小於povit的,中間是povit,後面的q+1到r之間是大於povit的。
根據分治、遞歸的處理思想,我們可以用遞歸排序下標從p到q-1之間的數據和下標從q+1到r之間的數據,直到區間縮小爲1,就說明所有的數據都有序了。
遞推公式:quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)
終止條件:p >= r
代碼實現
快排的思想是這樣的:如果要排序數組中下標從 p 到 r 之間的一組數據,我們選擇 p 到 r 之間的任意一個數據作爲 pivot(分區點)。
我們遍歷 p 到 r 之間的數據,將小於 pivot 的放到左邊,將大於 pivot 的放到右邊,將 pivot 放到中間。經過這一步驟之後,數組 p 到 r 之間的數據就被分成了三個部分,前面 p 到 q-1 之間都是小於 pivot 的,中間是 pivot,後面的 q+1 到 r 之間是大於 pivot 的。
根據分治、遞歸的處理思想,我們可以用遞歸排序下標從 p 到 q-1 之間的數據和下標從 q+1 到 r 之間的數據,直到區間縮小爲 1,就說明所有的數據都有序了。
// 快速排序,A 是數組,n 表示數組的大小
quick_sort(A, n) {
quick_sort_c(A, 0, n-1)
}
// 快速排序遞歸函數,p,r 爲下標
quick_sort_c(A, p, r) {
if p >= r then return
q = partition(A, p, r) // 獲取分區點
quick_sort_c(A, p, q-1)
quick_sort_c(A, q+1, r)
}
//分區函數
partition(A, p, r) {
pivot := A[r]
i := p
for 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
}
分區函數代碼說明:通過遊標i把A[p...r-1]分成2部分,A[p...i-1]的元素都是小於pivot的,我們暫且叫它“已處理區間”,A[i+1...r-1]是“未處理區間”。我們每次都從未處理區間取出一個元素A[j],與poivt相比,如果小於pivot,則將其加入到已處理區間的尾部,也就是A[i]位置。
def quick_sort2(lists, left, right):
"""
通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,
然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
:param lists:
:param left:
:param right:
:return:
"""
if left >= right:
return lists
pivot = lists[left]
low = left
high = right
while left != right:
while left < right and lists[right] >= pivot:
right -= 1
lists[left] = lists[right]
while left < right and lists[left] <= pivot:
left += 1
lists[right] = lists[left]
lists[right] = pivot
quick_sort2(lists, low, left - 1)
quick_sort2(lists, left + 1, high)
return lists
性能分析
- 算法穩定性:
因爲分區過程中涉及交換操作,如果數組中有兩個8,其中一個是pivot,經過分區處理後,後面的8就有可能放到了另一個8的前面,先後順序就顛倒了,所以快速排序是不穩定的排序算法。比如數組[1,2,3,9,8,11,8],取後面的8作爲pivot,那麼分區後就會將後面的8與9進行交換。
- 時間複雜度:最好、最壞、平均情況
快排也是用遞歸實現的,所以時間複雜度也可以用遞推公式表示。
如果每次分區操作都能正好把數組分成大小接近相等的兩個小區間,那快排的時間複雜度遞推求解公式跟歸併的相同。
T(1) = C; n=1 時,只需要常量級的執行時間,所以表示爲 C。
T(n) = 2*T(n/2) + n; n>1
所以,快排的時間複雜度也是O(nlogn)。
如果數組中的元素原來已經有序了,比如1,3,5,6,8,若每次選擇最後一個元素作爲pivot,那每次分區得到的兩個區間都是不均等的,需要進行大約n次的分區,才能完成整個快排過程,而每次分區我們平均要掃描大約n/2個元素,這種情況下,快排的時間複雜度就是O(n^2)。
前面兩種情況,一個是分區及其均衡,一個是分區極不均衡,它們分別對應了快排的最好情況時間複雜度和最壞情況時間複雜度。那快排的平均時間複雜度是多少呢?T(n)大部分情況下是O(nlogn),只有在極端情況下才是退化到O(n^2),而且我們也有很多方法將這個概率降低。
- 空間複雜度:快排是一種原地排序算法,空間複雜度是O(1)
應用
- O(n)時間複雜度內求無序數組中第K大元素,比如4,2,5,12,3這樣一組數據,第3大元素是4。
快排核心思想就是分治和分區。
我們選擇數組區間A[0…n-1]的最後一個元素作爲pivot,對數組A[0…n-1]進行原地分區,這樣數組就分成了3部分,A[0…p-1]、A[p]、A[p+1…n-1]。
如果p+1=K,那A[p]就是要求解的元素;如果K>p+1,說明第K大元素出現在A[p+1…n-1]區間,我們按照上面的思路遞歸地在A[p+1…n-1]這個區間查找。同理,如果K<p+1,那我們就在A[0…p-1]區間查找。
時間複雜度分析?
第一次分區查找,我們需要對大小爲n的數組進行分區操作,需要遍歷n個元素。第二次分區查找,我們需要對大小爲n/2的數組執行分區操作,需要遍歷n/2個元素。依次類推,分區遍歷元素的個數分別爲n、n/2、n/4、n/8、n/16…直到區間縮小爲1。如果把每次分區遍歷的元素個數累加起來,就是等比數列求和,結果爲2n-1。所以,上述解決問題的思路爲O(n)。
你可能會說,我有個很笨的辦法,每次取數組中的最小值,將其移動到數組的最前面,然後在剩下的數組中繼續找最小值,以此類推,執行 K 次,找到的數據不就是第 K 大元素了嗎?
不過,時間複雜度就並不是 O(n) 了,而是 O(K * n)。你可能會說,時間複雜度前面的係數不是可以忽略嗎?O(K * n) 不就等於 O(n) 嗎?
這個可不能這麼簡單地劃等號。當 K 是比較小的常量時,比如 1、2,那最好時間複雜度確實是 O(n);但當 K 等於 n/2 或者 n 時,這種最壞情況下的時間複雜度就是 O(n^2)。
- 有10個訪問日誌文件,每個日誌文件大小約爲300MB,每個文件裏的日誌都是按照時間戳從小到大排序的。現在需要將這10個較小的日誌文件合併爲1個日誌文件,合併之後的日誌仍然按照時間戳從小到大排列。如果處理上述任務的機器內存只有1GB,你有什麼好的解決思路能快速地將這10個日誌文件合併?
先構建十條io流,分別指向十個文件,每條io流讀取對應文件的第一條數據,然後比較時間戳,選擇出時間戳最小的那條數據,將其寫入一個新的文件,然後指向該時間戳的io流讀取下一行數據,然後繼續剛纔的操作,比較選出最小的時間戳數據,寫入新文件,io流讀取下一行數據,以此類推,完成文件的合併, 這種處理方式,日誌文件有n個數據就要比較n次,每次比較選出一條數據來寫入,時間複雜度是O(n),空間複雜度是O(1)
Leetcode
- 148 排序鏈表
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def sortList(self, head):
"""
:type head: ListNode
:rtype: ListNode
分治
"""
if not head or not head.next:
return head
second = self.findMid(head) # 找到鏈表後半段的head
l = self.sortList(head)
r = self.sortList(second)
return self.merge(l, r)
def merge(self, l, r): # O(NlgN)
if not l or not r:
return l or r
dummy = head = ListNode(None)
head.next = l
while l and r:
if l.val < r.val:
head.next = l
l = l.next
else:
head.next = r
r = r.next
head = head.next
head.next = l or r # l and r at least one is None
return dummy.next
def findMid(self, head):
fast, slow = head, head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next
second = slow.next
slow.next = None
return second