分治思想--快速排序解決TopK問題
----前言
最近一直研究演算法,上個星期刷leetcode遇到從兩個陣列中找TopK問題,因此寫下此篇,在一個數組中如何利用快速排序解決TopK問題。
先理清一個邏輯解決TopK問題→快速排序→遞迴→分治思想,因此本章內容會從此邏輯由後往前敘述
何為分治思想?
從字面上就很容易能夠推出"分而治之",維基百科的解釋為"就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。" 簡述一下後半部分"遇到子問題可以簡單的直接求解",打個比方,當最後分解到最後子問題不可再分時,例如只有一個元素或者該元素小於某個值。返回該值,這時子問題就成功解決。通過一個函式,將子問題合併,最後解決了原問題。這裡用歸併排序來讓大家可以更容易的理解。
在講解歸併排序之前,通過簡單的介紹一下遞迴,這是分治思想的基礎。
用遞迴需要滿足三個條件
- 一個問題的解可以分為多個子問題的解
- 這個問題分解之後的子問題,除資料規模不同,其餘完全相同
- 有邊界條件以此限制
若是不容易理解,打個比方,當你與朋友去電影院觀影時,你現在想知道自己的位置是第幾排,恰好現場黑燈瞎火,什麼都看不見,這時你詢問你前一排座位號是什麼,若恰好他也不知道,這時,他同你一樣做出相同的行為也問前面的人,最終問到第一排,第一排就是邊界條件,第一排告訴第二排,以此類推,最後你就清楚當前你所在的排數。以下是一張歸併排序的圖片與視訊
歸併排序首先是分解成子問題,如下所示,分解到只剩下一個元素,然後從這個元素開始,通過歸併排序,由下而上返回結果,最終解決原問題,因此關鍵是分解問題函式與歸併函式
以下是我用Python寫的原始碼,可供參考
def merge_sort(array):
if (len(array) <= 1):
return array
mid = int(len(array) / 2)
left = merge_sort(array[:mid])
right = merge_sort(array[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
#此處有i,j兩個索引,當其中一邊推入完成,另一邊可直接將剩下的推入
result += left[i:]
result += right[j:]
return result
array = [5, 3, 2, 8, 6, 1, 4, 7]
print(merge_sort(array))
此處解決遞迴與分治思想的問題
快速排序
快速排序同樣是運用分治思想,以一箇中樞軸元素,左邊放置小於中樞軸元素,右邊大於中樞軸元素,中樞元素一般選為最後一個元素(更方便理解),通過分治思想--下面的qucik_sort_c函式為劃分為成子問題,partition為分割槽函式,最後得出原問題的答案。
快速排序的難點在於partition分割槽函式,但本質也是很簡單,同樣是有兩個索引,一個索引用於遍歷當前分割槽陣列所有元素(下面即為j),一個索引為指向小於中樞軸元素,若是小於中樞軸元素,增加該索引的值,如下i即為該索引,下面的視訊的中樞軸元素為第一個元素
def quick_sort(A):
qucik_sort_c(A, 0, len(A) - 1)
def qucik_sort_c(A, p, r):
if p >= r: return
q = partition(A, p, r) # 獲得分割槽點
qucik_sort_c(A, p, q - 1)
qucik_sort_c(A, q + 1, r)
def partition(A, p, r):
pivot = A[r]
i = p
for j in range(p, r):
if A[j] < pivot:
A[i], A[j] = A[j], A[i]
i = i + 1
A[i], A[r] = A[r], A[i]
return i
A = [8,10,2,3,6,1,5]
quick_sort(A)
print(A)
當然,快速排序也可以像歸併排序,建立一個新的陣列,最後兩個陣列歸併,返回成一個新的陣列,但這樣增加了空間複雜度,且快速排序由於中樞軸的選取不同,最壞時間複雜度為n2,因此最好還是原地排序。
快速排序解決TopK問題
TopK問題是一個數組中第K大的數字,比如[1,7,3,5,4]中第2大的數字為3,如果說成第K小的數字也可以,只要能夠理解即可。TopK問題在大資料中是一個常用的演算法,比如說從100萬的資料中找出前100個熱點頻率最高的詞。解決TopK問題有很多種方法,大家若是有興趣可以自己搜尋,因為作者本人對演算法也只是處在初步的階段。這裡僅僅是通過快速排序的方法解決TopK問題。
選擇當前陣列元素的最後一個為中樞軸,由上面的快速排序可以知道,每一次的排序都可以知道中樞軸的下標是多少,這樣可以確定當前中樞軸為第幾大的數字,這裡通過快速排序的思想,TopK小於當前的中樞軸下標,那麼向左走,反之,若是中樞軸下標等於TopK的值,直接返回即可。原理其實並不難,下面有一處地方需注意,當TopK的值大於中樞軸下標時,需要向右走,每一次需要減去之前的中樞軸下標,可以通過下面自己所畫的圖理解。
def smallest_k(arr, l, r, k):
if (k > 0 and k <= r - l + 1):
index = partiton(arr, l, r)
if (index - l == k - 1):
return arr[index]
elif (index - l > k - 1):
return smallest_k(arr, l, index - 1, k)
else:
return smallest_k(arr, index + 1, r, k - 1 - index + l )
def partiton(arr, l, r):
pivot = arr[r]
i = l
for j in range(l, r):
if arr[j] < pivot:
arr[i], arr[j] = arr[j], arr[i]
i = i + 1
arr[i], arr[r] = arr[r], arr[i]
return i
array = [13,4,12,17,2,44,55,92,1,18,6]
print(smallest_k(array, 0, len(array) - 1, 8),array)
後記
自己本身是打算去搜索找到答案,但並沒有自己認同的答案,因此只能不斷的嘗試。此文章借鑑了許多大佬的推文,之後會推如何在兩個陣列中找到TopK問題,這是自己刷leetcode 中的尋找兩個有序陣列的中位數有感,因為得考慮時間複雜度,自己也通過各種途徑才知道如何解決的。
若是覺的不錯,部落格園因為傳送不了視訊,因此無法動畫演示,若感興趣的同學可以去此文章看看
參考連結:
極客時間-資料結構與演算法之美:https://time.geekbang.org/column/intro/126
演算法動畫:https://visualgo.net/en
GeeksForGeek:https://www.geeksforgeeks.org/quick-s