1. 程式人生 > 實用技巧 >【核心演算法9】分治演算法

【核心演算法9】分治演算法

分治演算法的核心思想是將一個規模很大的問題化簡為n個規模較小的問題,這些子問題雖然獨立而不同,但是問題的本質是一致的,從而達到分而治之的目的

  • 歸併排序
  • 連續子列表的最大和
  • 幾何問題-凸包
  • 數學問題-多項式乘法

歸併排序

歸併排序是一種有效的排序演算法。其他常見的八種排序演算法包括氣泡排序,插入排序,二叉樹排序,快速排序,堆排序,希爾排序等

問題描述

將亂序的數列用歸併演算法將其排序輸出

思路描述

歸併演算法有兩種型別:遞迴法(Top-down) 和迭代法(Bottom-up)

遞迴法描述

遞迴法的核心思想是

  1. 把列表分為兩個子列表,單獨排序子列表再合併子列表。

  2. 如圖,遞迴法可以利用二叉樹理解:列表為根節點,子列表為子節點。

  3. 首先排序子列表,再一步步合併子列表

這個演算法之所以稱為遞迴法是因為它利用遞迴的思想,把根節點的問題遞給子節點,一環套一環解決問題

迭代法描述

迭代法的核心思想是

  1. 利用for迴圈將列表的子列表成對排序合併,最後組成一個整體

  2. 如圖,迭代法可以利用倒二叉樹:對獨立子列表(子節點) 進行多次迭代,直到完整列表(根節點)

  3. 首先把列表看成n個長度為1的子列表,利用迴圈把相鄰的兩個子列表合併

  4. 得到 n/2 個長度為2的子列表

  5. 依次類推,最後得到長度為n的完整列表

程式碼實現

遞迴法

def merge_sort1(arr):
    """
    遞迴法 歸併排序
    :param arr: 亂序陣列
    :return: 
    """
    if len(arr) < 2:
        return
    # 找到列表中點
    cut = len(arr) // 2
    # 左子列表
    arr1 = arr[: cut]
    # 右子列表
    arr2 = arr[cut: ]
    # 左子列表歸併排序
    merge_sort1(arr1)
    # 右子列表歸併排序
    merge_sort1(arr2)

    pointer1 = 0
    pointer2 = 0
    counter = 0
    while pointer1 < len(arr1) and pointer2 < len(arr2):
        if arr1[pointer1] < arr2[pointer2]:
            arr[counter] = arr1[pointer1]
            pointer1 += 1
        else:
            arr[counter] = arr2[pointer2]
            pointer2 += 1
        counter += 1
    while pointer1 < len(arr1):
        arr[counter] = arr1[pointer1]
        pointer1 += 1
        counter += 1
    while pointer2 < len(arr2):
        arr[counter] = arr2[pointer2]
        pointer2 += 1
        counter += 1
        
array = [2, 3, 4, 1, -2, 0, 5, 1]
merge_sort1(array)
print(array)

# >>>

迭代法

def merge_sort2(arr):
    length = len(arr)
    n = 1
    while n < length:
        for i in range(0, length, n*2):
            arr1 = arr[i: i + n]
            arr2 = arr[i + n: i + n*2]
            pointer1 = 0
            pointer2 = 0
            counter = i
            while pointer1 < len(arr1) and pointer2 < len(arr2):
                if arr1[pointer1] < arr2[pointer2]:
                    arr[counter] = arr1[pointer1]
                    pointer1 += 1
                else:
                    arr[counter] = arr2[pointer2]
                    pointer2 += 1
                counter += 1

            while pointer1 < len(arr1):
                arr[counter] = arr1[pointer1]
                pointer1 += 1
                counter += 1
            while pointer2 < len(arr2):
                arr[counter] = arr2[pointer2]
                pointer2 += 1
                counter += 1
        n = n*2
        
array = [2, 3, 4, 1, -2, 0, 5, 1]
merge_sort2(array)
print(array)

# >>>

連續子列表的最大和

問題描述

在一個列表中找出連續子列表的最大和,列表中的數字可負可正,並且子列表不能為空

思路解析

  1. 列表的最大子列表的和,有三種可能:左子列表,右子列表或左子列表與右子列表之間
  2. 左子列表和右子列表答案是知道的,找到左右子列表之間的子列表最大和就可以了
  3. 設一箇中點,遍歷中點左邊的值,跟蹤記錄已遍歷過的值的總和,取這些總和的最大值
  4. 遍歷中點右邊的值,同樣跟蹤記錄已遍歷過的值的總和,取這些總和的最大值
  5. 最後,左面的最大值+右面的最大值+中點值就是第三種可能的答案了

程式碼實現

def max_sub_array(arr):
    if arr == []:
        return
    if len(arr) == 1:
        return arr[0]
    # 設中點
    cut = len(arr) // 2
    # 分治:找到左子列表最大和
    left_sum = max_sub_array(arr[: cut])
    # 分治:找到右子列表最大和
    right_sum = max_sub_array(arr[cut: ])
    
    left_middle_sum = 0
    max_l = 0
    right_middle_sum = 0
    max_r = 0
    # 找到左子列表與右子列表之間子列表的最大和
    for i in range(cut - 1, -1, -1):
        left_middle_sum += arr[i]
        max_l = max(left_middle_sum, max_l)
    for i in range(cut + 1, len(arr), 1):
        right_middle_sum += arr[i]
        max_r = max(right_middle_sum, max_r)
    # 返回三種可能的最大值
    return (max(left_sum, max_l + arr[cut] + max_r, right_sum))

arr = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
max_sum = max_sub_array(arr)
print(max_sum)

#>>>