【程序员必修数学课】->基础思想篇->递归(下)->分而治之从归并排序到MapReduce
遞歸(下)
- 前言
- 歸并排序中的分治思想
- 分布式系統中的分治思想
- 1.數據分割和映射
- 2.歸約
- 3.合并
- 總結
前言
在上一篇中,我介紹了如何使用遞歸,來處理迭代法中比較復雜的數值計算。但是我們知道,有些迭代法并不是簡單的數值計算,而是要通過迭代的過程進行一定的操作,過程更加復雜,需要考慮很多中間數據的分配或者保存。比如我在迭代法中提到的使用二分查找進行數據匹配,或者這篇文章里將要講解的歸并排序中的數據排序等等。在這種情況下,要怎么使用遞歸法呢?
【程序員必修數學課】->基礎思想篇->迭代法
【程序員必修數學課】->基礎思想篇->遞歸(上)->泛化數學歸納
我們可以先分析一下這些復雜的問題是否可以簡化成更小的、更簡單的子問題來解決,這是一般思路。如果可以,那就意味著我們可以使用遞歸的核心思想,將復雜的問題逐步簡化成最基本的情況來求解。在這篇文章里,我將從歸并排序開始,延伸到多臺機器的并行處理,詳細介紹遞歸思想在“分而治之”這個領域的應用。
歸并排序中的分治思想
首先,我們先考慮如何使用遞歸編程解決數字的排序問題。
對一堆雜亂無序的數字,按照從小到大或者從大到小的規則進行排序,這是計算機領域非常經典,也非常流行的問題。小到Excel電子表格,大到搜索引擎,都需要對一堆數字進行排序。因此,計算機領域的前輩們研究排序問題已經很多年了,也提出了許多優秀的算法,比如歸并排序、快速排序、堆排序等等。其中,歸并排序和快速排序都很好地體現了分治的思想,這篇文章主要就說一說歸并排序(merge sort)。
很顯然,歸并排序算法的核心就是“歸并”,也就是把兩個有序的數列合并起來,形成一個更大的有序數列。
假設我們需要按照從小到大的順序,合并兩個有序數列 A 和 B。我們需要開辟一個新的存儲空間 C,用于保存合并后的結果。
我們首先比較兩個數列的第一個數,如果 A 數列的第一個數小于 B 數列的第一個數,那么就先取出 A 數列的第一個數放入 C,并把這個數從 A 數列中刪除。如果是 B 的第一個數更小,那么就先取出 B 數列的第一個數放入 C,并把它從 B 數列里刪除。
以此類推,直到 A 和 B 里所有的數據都被取出來放入 C。如果到某一步, A 或 B 數列為空,那直接將另一個數列的數據依次取出放入 C 就可以了。這種操作,可以保證兩個有序的數列 A 和 B 合并到 C 之后,C 數列仍然是有序的。
比如說合并有序數組 {6, 11, 13, 17} 和 {8, 10, 16}的過程👇
為了保證得到有序的 C 數列,我們必須保證參與合并的 A 和 B 也是有序的。但是,等待排序的數組一開始都是亂序的,如果無法保證這點,那歸并又有什么意義呢?
這就需要用到遞歸了。我們可以利用遞歸的思想,把問題不斷簡化,也就是把數列不斷簡化,一直簡化到最后只有一個數,那它本身就是有序的了。那么如何進行每一次的簡化呢?
最簡單的想法就是把長度為 n 的數列,每次簡化為長度為 n - 1 的數列,直至長度為 1。不過,這樣的處理沒有并行性,要進行 n - 1 次的歸并操作,效率就會很低。
所以,我們可以在歸并排序中引入了分而治之(Divide and Conquer) 的思想。分而治之,我們通常簡稱為分治。它的思想就是,將一個復雜的問題,分解成兩個甚至多個規模相同或類似的子問題,然后對這些子問題再進一步細分,直到最后的子問題變得簡單,很容易就能被求解出來,這樣這個復雜的問題就求解出來了。
歸并排序通過分治的思想,把長度為 n 的數列,每次簡化為兩個長度為 n / 2 的數列。這樣更有利于計算機的并行處理,只需要 log2n 次歸并。
我們把歸并和分治的思想結合起來,這其實就是歸并排序算法。這種算法每次把數列進行二等分,直到唯一的數字,也就是最基本的有序數列。然后從這些最基本的有序數列開始,兩兩合并有序的數列,直到所有的數字都參與了歸并排序。
我用一個包含 0~9 這 10 個數字的數組,分析一下歸并排序的過程。
- 假設初始的數組為 {7, 6, 2, 4, 1, 9, 3, 8, 0, 5},我們要對它進行從小到大的排序。
- 第一次分解后,變成兩個數組 {7, 6, 2, 4, 1} 和 {9, 3, 8, 0, 5}。
- 然后,我們將 {7, 6, 2, 4, 1} 分解成 {7, 6} 和 {2, 4, 1},將 {9, 3, 8, 0, 5} 分解成 {9, 3} 和 {8, 0, 5}。
- 按照這個規律繼續細分下去,直到每個組只包含一個數字。到這里,都是遞歸的嵌套調用過程。
- 接下來,就要開始進行合并了。我們可以將 {4, 1} 分解為 {4} 和 {1}。現在無法再細分了,我們開始合并,在合并的過程中進行排序,所以合并的結果為 {1, 4}。合并后的結果將返回當前函數的調用者,這就是函數返回的過程。
- 重復上述合并的過程,直到完成整個數組的排序,得到 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}。
這個過程可以畫一張圖來理解👇
可以看到 歸并排序使用了分治的思想,而這個過程需要使用遞歸來實現。
歸并排序算法用分治的思想把數列不斷地簡化,直到每個數列僅剩下一個單獨的數,然后再使用歸并逐步合并有序的數列,從而達到將整個數列進行排序的目的。而這個歸并排序,正好可以使用遞歸的方式來實現。我們可以看看下面這張圖,可以發現,分治的過程和遞歸的過程是一致的。
分治的過程可以通過遞歸來表達,因此,歸并排序最直觀的實現方式就是遞歸。所以,我們從遞歸的步驟出發,來看歸并排序如何實現。
我們假設 n = k - 1 的時候,我們已經對較小的兩組數進行了排序。那我們只要在 n = k 的時候,將這兩組數合并起來,并且保證合并后的數組仍然是有序的就行了。
所以,在遞歸的每次嵌套調用中,代碼都將一組數分解成更小的兩組,然后將這兩個小組的排序交給下一次的嵌套調用。而本次調用只需要關心,如何將排好序的兩個小組進行合并。
在初始狀態,也就是 n = 1 的時候,對于排序的案例而言,只包含單個數字的分組。由于分組里只有一個數字,所以它已經是排好序的了,之后就可以開始遞歸調用的返回階段。
現在我用Java簡單實現一下歸并排序。
測試代碼如下👇
package com.tyz.merge_sort.test;import com.tyz.merge_sort.core.MergeSort;public class Test {public static void main(String[] args) {int[] arr = {78, 12, 444, 710, 18, 322, 0, 45, 95471, 99, 1024};int[] sorted = MergeSort.mergeSort(arr);for (int i = 0; i < sorted.length; i++) {System.out.println(sorted[i]);}}}結果如下👇
分布式系統中的分治思想
到這里我們對分而治之的思想已經有了一個基本的認識了,不過,分而治之更有趣的應用其實是在分布式系統中。
舉個例子,當需要排序的數組很大很大,比如 1024GB ,我們沒法把這些數據都塞入一臺普通的計算機的內存里。有一個辦法,我們可以把這個超級大的數據集,分解成多個更小的數據集(比如 16GB),然后分配到多臺機器上,讓它們并行地處理。
等所有機器處理完后,中央服務器再進行結果的合并。由于多個小任務間不會相會干擾,可以同時處理,這樣會大大增加處理的速度,減少等待時間。
在單臺機器上實現歸并排序的時候,我們只需要在遞歸函數內,實現數據分組以及合并就行了。而在多個機器之間分配數據的時候,遞歸函數內除了分組及合并,還要負責把數據分發到某臺機器上。
可以看到,分布式集群種話的數據切分和合并,同單臺機器上歸并排序的過程是一樣的,因此也是使用了分治的思想。從理論的角度來看,上面這個圖很容易理解。不過在實際運用中,有個地方需要注意一下。
上圖中的父節點,例如機器1、2、3,它們都沒有被分配排序的工作,只是在子節點的排序完成后進行有序數組的合并,因此集群的性能沒有得到充分的利用。那么,另一種可能的數據切分方式是,每個機器拿出一半的數據給另一臺機器處理,而自己完成剩下的一半數據。
如果分治的時候,只進行一次問題切分,那么上述層級型的分布式架構就可以轉化為類似 MapReduce 的架構。下圖我接用黃申老師畫的主要步驟,以供參考。
這里面主要有三個步驟用到了分治的思想。
1.數據分割和映射
分割是指將數據源進行切分,并將分片發送到 Mapper 上。映射是指 Mapper 根據應用的需求,將內容按照 鍵 - 值 的匹配,存儲到哈希結構中。這兩個步驟將大的數據集合切分為更小的數據集,降低了每臺機器節點的負載,因此和分治中的問題分解類似。不過,MapReduce 采用了哈希映射來分配數據,而普通的分治或遞歸不一定需要。
2.歸約
歸約是指接受到的一組鍵值對,如果是鍵內容相同的配對,就將它們的值歸并。這和本機的遞歸調用后返回結果的過程類似。不過,由于哈希映射的關系,MapReduce 還需要洗牌的步驟,也就是將 鍵 - 值 的配對不斷地發給對應的 Reducer 進行歸約。普通的分治或遞歸不一定需要洗牌的步驟。
3.合并
為了提升洗牌階段的效率,可以選擇減少發送到歸約階段的 鍵 - 值 配對。具體做法是在數據映射和洗牌之間,加入合并的過程,在每個 Mapper 節點上先進行一次本地的歸約。然后只將合并的結果發送到洗牌和歸約階段。這和本機的遞歸嗲用后返回結果的過程類似。
總結
遞歸采用了和數學歸納法類似的思想,但是它用的是逆向遞推,化繁為簡,把復雜的問題逐步簡化。再加上分治原理,我們就可以更有效地把問題細分,進行并行化的處理。
而計算機編程中的函數嵌套調用,正好對應了數學中遞歸的逆向遞推,所以你只要弄明白了數學遞推式,就能非常容易地寫出對應的遞歸編碼。需要注意的是,遞歸編程在沒有開始返回結果之前,保存了大量的中間結果,所以比較消耗系統資源,這也是一般的編程語言都會限制遞歸的深度(嵌套的次數)的原因。
另,這篇文章的知識點來源于極客時間黃申的《程序員的數學基礎課》。
總結
以上是生活随笔為你收集整理的【程序员必修数学课】->基础思想篇->递归(下)->分而治之从归并排序到MapReduce的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: webpack学习笔记(六):图片打包处
- 下一篇: Cognex ToolBlockEdit