分治思想--快速排序解決TopK問題

speanut發表於2019-06-01

 

----前言

​ 最近一直研究演算法,上個星期刷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 > 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-sort/

相關文章