1. 程式人生 > >演算法課堂實驗報告(二)——python遞迴和分治(第k小的數,大數乘法問題)

演算法課堂實驗報告(二)——python遞迴和分治(第k小的數,大數乘法問題)

python實現遞迴和分治

一、開發環境

開發工具:jupyter notebook 並使用vscode,cmd命令列工具協助程式設計測試演算法,並使用codeblocks輔助編寫C++程式
程式語言:python3.6

二、實驗目標

1. 熟悉遞迴和分治演算法實現的基本方法和步驟;

2. 學會分治演算法的實現方法和分析方法:

三、實驗內容

問題1,線性時間選擇問題:
1) 在 4 59 7 23 61 55 46中找出最大值,第二大值,和第四大的值(要求不允許採用排序演算法),並與第一章實現的快速排序演算法進行比較。

2) 隨機生成10000個數,要求找出其中第4999小的數,並與第一章實現的快速排序演算法進行比較。

將4 59 7 23 61 55 46作為列表輸入函式中,並且返回輸出的結果

方法:直接遍歷找到最大值,然後刪除,再遍歷,依次找到第2,3,4大的資料並且返回

每次都需要遍歷一遍列表中所有的元素

快速排序的方法:將列表使用快速排序進行從大到小的排列,複雜度O(nlogn)然後直接返回對應的數,複雜度O(1),快速排序程式碼來自上一節

程式碼如下所示:

# 直接遍歷找到最大值,然後刪除,再遍歷,依次找到第2,3,4大的資料並且返回
def find_1_2_4(lis):
    find1=lis[0];find2=lis[0];find3=lis[0];
    for i in range(len(lis)):
        if find1<lis[i]:
            find1 = lis[i]
    lis.remove(find1)
    for i in range(len(lis)):
        if find2<lis[i]:
            find2=lis[i]
    lis.remove(find2)
    for i in range(len(lis)):
        if find3<lis[i]:
            find3=lis[i]
    lis.remove(find3)
    find3=lis[0]
    for i in range(len(lis)):
        if find3<lis[i]:
            find3=lis[i]
    lis.remove(find3)
            
    return find1, find2, find3

# 快速排序,劃分操作, 快速排序的輔助函式
def split(lis, first, last):
    pivot = lis[first]
    
    left = first
    right = last
    while left<right:
        while pivot < lis[right]:
            right=right-1
        while left < right and (lis[left] < pivot or lis[left] == pivot):
            left=left+1
        if left < right:
            lis[left], lis[right] = lis[right], lis[left]
    # 確定好基準位置
    pos = right
    lis[first] = lis[pos]
    lis[pos] = pivot
    return pos

# 快速排序實現
def quicksort(lis, first, last):
    if first < last:
        pos = split(lis, first, last)
        quicksort(lis, first, pos-1)
        quicksort(lis, pos+1, last)
    return lis

if __name__ == '__main__':
    lis1 = [4, 59, 7, 23, 61, 55, 46]
    lis2 = [4, 59, 7, 23, 61, 55, 46]
    import time
    starttime = time.time()
    print(find_1_2_4(lis1))
    endtime = time.time()
    print("執行時間為:",endtime-starttime)
    
    starttime = time.time()
    quicksort(lis2, 0,len(lis2)-1)
    print(lis2[-1], lis2[-2], lis2[-4])
    endtime = time.time()
    print("執行時間為:",endtime-starttime)

實驗結果如下圖所示:

可見在資料量如此之小的情況下,時間相差無幾,並不能反映出效率問題。

2)首先生成10000個隨機資料,找出第4999小的數,如果直接使用排序演算法,那麼時間複雜度就是O(nlogn),然後直接輸出第4999個數就行了。

但是我們可以使用分治策略對演算法進行優化,使得演算法的複雜度小於排序演算法的O(nlogn)

演算法步驟如下:

首先選取一個基點,比支點大的放支點右邊,比支點小的放支點左邊(到這裡和快排一樣),看看支點左邊有多少個數,如果大於k-1說明在k在左邊,左邊遞迴,如果小於k-1說明在k右邊,右邊開始遞迴,並且新尋找的是k-pos小的數,如果相等,那麼返回

程式碼如下:


# 尋找第k小的數的輔助函式
def k_find(lis, k):
    pivot = lis[len(lis)//2]
    
    left = 0
    right = len(lis)-1
    
    lis[0],lis[len(lis)//2]=lis[len(lis)//2],lis[0]
    while left<right:
        while pivot < lis[right]:
            right=right-1
        while left < right and (lis[left] < pivot or lis[left] == pivot):
            left=left+1
        if left < right:
            lis[left], lis[right] = lis[right], lis[left]
    # 確定好基準位置
    pos = right
    lis[0] = lis[pos]
    lis[pos] = pivot
    
    count=pos+1
    if count==k:
        return pivot
    elif count>k:
        return k_find(lis[0:pos], k)
    else:
        return k_find(lis[pos:], k-pos)


if __name__ == "__main__":
    import random
    import cProfile
    
#     產生100000個隨機陣列
    num = 100000
#     array = [random.randint(1, 1000) for i in range(num)]
    array=[]
    a=1
    for i in range(num):
        a = a+random.randint(1,20)+1
        array.append(a)
    
    # 亂序操作
    random.shuffle(array)
    random.shuffle(array)
    
    import copy
    # 進行一個深度拷貝,用於測試
    arraycopy = copy.deepcopy(array)
    

#     用O(n)的演算法得到第k小的數
    k=4999
    
    import time
    starttime= time.time()
    n = k_find(array, k)
    endtime=time.time()
    print("使用線性查詢的時間為:",endtime-starttime)
    print("查詢得到的數為:",n)

    starttime= time.time()
    quicksort(arraycopy, 0, len(arraycopy)-1)
    endtime=time.time()
    print("使用快排查詢的時間為:",endtime-starttime)
    print("查詢得到的數為:",arraycopy[k-1])

執行結果如下圖所示:

查詢到了相同的數字,然後發現效率竟然相差了十倍。

在這一版本的程式碼中,我們只要將程式碼中的

while pivot < lis[right]:
            right=right-1
        while left < right and (lis[left] < pivot or lis[left] == pivot):
            left=left+1

這一部分的大於號與小於號略作修改,即可用於解決第k大的數這一問題,即:

while pivot > lis[right]:
            right=right-1
        while left < right and (lis[left] > pivot or lis[left] == pivot):
            left=left+1

將修改後的程式碼用於求解leetcode上第215號題,成功通過

我實現的版本還是存在很多問題的,比如基準數的選擇上,而且在出現相同數字的時候可能會出現問題,參考教科書上的解決方案還是很好的,取中間數的中間數,保證了效率,所以我照著書上敲了一遍程式碼(下面這個方案純粹是照著敲上去的)

# 線性時間選擇
lis1 = [4, 59, 7, 23, 61, 55, 46]

# 選擇第k小的數的分治演算法
def select_fct(array, k):
    if len(array) <= 10:  # 邊界條件
        array.sort()
        return array[k]
    
    pivot = get_pivot(array)  # 得到陣列的支點數
    array_lt, array_gt, array_eq = patition_array(array, pivot) # 按照支點數劃分陣列
    
    if k<len(array_lt):    # 所求數在支點數左邊
        return select_fct(array_lt, k)
    elif k < len(array_lt)+len(array_eq):   # 所求數為支點數
        return array_eq[0]
    else:    # 所求數在支點數右邊
        normalized_k = k-(len(array_lt)+len(array_eq))
        return select_fct(array_gt, normalized_k)
    
        
# 得到陣列的支點數
def get_pivot(array):
    subset_size = 5 # 每一個數組有5個元素
    subsets = []
    num_medians = len(array) // subset_size # 用於記錄各組元素
    if(len(array) % subset_size) > 0:
        num_medians +=1    # 不能被5整除,組數加1
        
    for i in range(num_medians):    # 劃分成若干組,每組5個元素
        beg = i*subset_size
        end = min(len(array), beg+subset_size)
        subset = array[beg:end]
        subsets.append(subset)
    medians = []
    for subset in subsets:
        median = select_fct(subset, len(subset)//2)    # 計算每一組的中間數
        medians.append(median)
    pivot = select_fct(medians, len(subset)//2)    # 中間數的中間數
    return pivot
    
# 按照支點數劃分陣列
def patition_array(array, pivot):
    array_lt = []
    array_gt = []
    array_eq = []
    for item in array:
        if item < pivot:
            array_lt.append(item)
        elif item > pivot:
            array_gt.append(item)
        else:
            array_eq.append(item)
    return array_lt, array_gt, array_eq

import random
if __name__ == "__main__":
    import cProfile
    # 產生100個隨機陣列
    num = 100000
    array = [random.randint(1, 1000) for i in range(num)]
#     print(array)
    random.shuffle(array)
    random.shuffle(array)
    # 用O(n)的演算法得到第k小的數
    k=15000
#     kval = select_fct(array, k)
    cProfile.run("select_fct(array, k)")
#     print(kval)

問題2,大數乘法問題。分別嘗試計算9*9, 9999*9999, 9999999999*8888888888的結果
問題背景:
      所謂的大數相乘,就是指兩個位數比較大的數字進行相乘,一般而言相乘的結果超過了進本型別的表示範圍,所以這樣的數不能夠直接做乘法運算。
      其實乘法運算可以拆成兩步,第一部,將乘數與被乘數逐位相乘,第二步,將逐位相乘得到的結果對應相加起來。
傳統的大數乘法問題就是將數值的計算利用字串來計算,從而解決位數溢位的問題。這種型別的以前曾經使用C++實現過。
然而我們現在要做的不是這個問題,我們的問題是請設計一個有效的演算法,可以進行兩個n位大整數的乘法運算,因為是nXn位的問題,解決方案偏向於定製化,我們需要的是將大數乘法O(n²)的複雜度降低,利用分治演算法,將大的問題分解成為小的問題,先分,然而再合,從而優化解的方案。
程式碼實現:
前兩個問題很簡單,不再實現

9999999999*8888888888
對於這個問題,我們首先直接使用C++進行計算,結果肯定是有問題的
初次實驗如圖所示:

下面改用python語言 利用分治演算法進行大數乘法的計算:

# 大數乘法
9*9
9999*9999
9999999999*8888888888

import math

# 計算符號
def sign(x):
    if x<0:
        return -1
    else:
        return 1
    
# 寫一個計算大數乘法的函式 a和b是大數 計算他們倆相乘
def divideConquer(x, y, n):
    s = sign(x)*sign(y)
    x = abs(x)
    y = abs(y)

    if x==0 or y==0:    # 如果其中有一個為0 直接返回0
        return 0
    elif n==1:    # 遞迴結束條件 n=1
        return x*y*s
    else:
        A = x // math.pow(10, n//2)    # 獲得第一個數的前半部分
        B = x - A*math.pow(10, n//2)    # 獲得第一個數的後半部分
        C = y // math.pow(10, n//2)    # 獲得第二個數的前半部分
        D = y - C*math.pow(10, n//2)    # 獲得第二個數的後半部分
        AC = divideConquer(A, C, n//2)    # AC相乘的結果
        BD = divideConquer(B, D, n//2)    # BD相乘的結果
        ABCD = divideConquer(A-B, D-C, n//2)+AC +BD    # 計算中間量AD+BC的結果 實際的計算方式是(a-b)(d-c)+ac+db
        return s * (AC*math.pow(10, n)+ABCD*math.pow(10, n//2)+BD)
    
    print(A,B,C,D)

if __name__ =='__main__':
    print(int(divideConquer(9999999999,8888888888,10)))

在很快的時間之內就可以完成。

測試結果:

提出問題:是不是因為python的內部機制才使得計算這麼快的呢?

於是重新進行了測試

if __name__ =='__main__':

    import cProfile
    cProfile.run("divideConquer(123456789123456789123456789123456789123456789123456789123456789123456789,123456789123456789123456789123456789123456789123456789123456789123456789,72)")
    cProfile.run("123456789123456789123456789123456789123456789123456789123456789123456789*123456789123456789123456789123456789123456789123456789123456789123456789")

這裡使用了72位的數字

結果如圖所示:

分治法的函式呼叫次數還是相當的多的

可是為什麼直接用py內部的機制也是這麼快呢?python到底是如何實現大數相乘的?查閱了相關資料之後發現,大整數乘法還可以採用FFT求迴圈卷積來實現,複雜度為O(nlogn)(網上這麼說的,我也不知道對不對),然而python內部實現大整數相乘還是採用的是分治演算法,理由是醬紫的:py日常計算的大整數不夠大,所以還是用的是分治。