1. 程式人生 > >演算法學習(一)——分治以及排序演算法總結

演算法學習(一)——分治以及排序演算法總結

分治策略:

  • 分解(Divide):將問題劃分為若干子問題
  • 解決(Conquer):遞迴求解子問題
  • 合併(combine):子問題組合成原問題
主方法:T(n) = aT(n/b)+f(n)
分解成a個問題,每個子問題降b倍,合併為O(f(n))
主定理:
比較f(n)和aT(n/b)的階,要求是多項式意義上的比較,即只能差n^x。
1.f(n)的階大,T就是O(f(n))
2.反之,為O[n^logb(a)] 正則條件:(對常數c<1,有af(n/b)<=cf(n))
3.最後就是O(f(n)) = O[n^logb(a)],O(T) = O[n^logb(a)*lgn]

n^logb(a)就是滿a叉樹中葉子節點的個數,對應遞迴樹上,以上三種情況可以解釋為:1.樹的總代價由根節點決定 2.樹的總代價由葉節點決定 3.樹的總代價均勻分佈在樹的所有層次上

舉個反例,不能使用主定理的情況:
T(n) = 2T(n/2)+nlgn

f(n)/n^logb(a) = nlgn/n = lgn 它是漸近小於n^c.

排序演算法總結

排序的分類:
(一)內部排序和外部排序:內部排序就是在記憶體中排序;外部排序涉及資料量大,記憶體不夠用,需要對外存訪問的排序(多路歸併排序)。
(二)基於比較和不基於比較的排序(基數排序)
還有一個穩定性的問題,若存在次關鍵字(不唯一),排序後位置變化就是不穩定的。

內部排序的分類(下面只標註時間複雜度不是O(n^2)的演算法):
1.交換排序:氣泡排序(穩定),快速排序 (不穩定 O(nlgn) 空間複雜度:O(nlgn~n))
2.插入排序:直接插入排序(穩定), 折半插入排序(穩定),
希爾排序/縮小增量排序(不穩定)
3.選擇排序:簡單選擇排序(不穩定),堆排序(不穩定 O(nlgn) ),二路歸併排序(穩定 O(nlgn) 空間複雜度:O(n) )
4.不基於比較的基數排序(穩定 O(d(n+r))r:基數 n:節點數  d:每個節點關鍵字的位數

先介紹三個時間複雜度為O(n^2)的排序演算法
冒泡,選擇排序,插入排序

1.冒泡
def bubbleSort(alist):
    n = len(alist)
    for i in range(n-1, 0, -1):
        for j in range(0, i):
            if alist[j] > alist[j+1]:
                alist[j], alist[j+1] = alist[j+1], alist[j]
    return alist
    # 改進的氣泡排序
def bubbleSort(alist):
    n = len(alist)
    exchange = False
    for i in range(n-1, 0, -1):
        for j in range(0, i):
            if alist[j] > alist[j+1]:
                alist[j], alist[j+1] = alist[j+1], alist[j]
                exchange = True
        # 如果發現整個排序過程中沒有交換,提前結束
        if not exchange:
            break
    return alist
    
2. 選擇排序 
它與冒泡的不同之處在於,需要一個多出變數儲存遍歷當前序列的最小值下標,每輪遍歷只需要交換一次
def selectionSort(alist):
    n = len(alist)

    for i in range(n - 1):
        # 尋找[i,n]區間裡的最小值
        min_index = i
        for j in range(i+1, n):
            if alist[j] < alist[min_index]:
                min_index = j
        alist[i], alist[min_index] = alist[min_index], alist[i]
    return alist 
#這是最簡單的選擇排序,另外還有堆排序
3. 插入排序 
它是將新元素插入已排好序的序列中,同樣需要儲存排好序的隊尾元素下標,找到第一個比它小的位置之前插入,關鍵就是找到比currentvalue小的元素的位置,比currentvalue大就後移。它是需要比較和移動的。
移動次數會比選擇排序多,但是它大大減少了比較次數,只要在已經基本有序的序列中找到位置就停止比較和移動了。
def insertionSort(alist):
    for i in range(1,len(alist)):
        currentvalue=alist[i]
        position=i
        while alist[position-1]>currentvalue and position>0:
            alist[position]=alist[position-1]
            position=position-1
        alist[position]=currentvalue
    return alist
    
希爾排序
#基於插入排序的效能提升:需要待排記錄基本有序和n值較小,可以大大減少移動比較次數。(如果序列已經有序,只需要比較n-1次)
#增量序列函式有很多,沒有最好,它的好壞直接影響時間複雜度。
# 希爾排序
def shellSort(alist):
    n = len(alist)
    gap = n // 2
     #" / "就表示 浮點數除法,返回浮點結果;" // "表示整數除法,代表不大於n/2的整數
    while gap > 0:
        for i in range(gap):   #每輪gap個子列,gap//2產生子列的函式
            gapInsetionSort(alist, i, gap)  #每個子列分別調直接插入排序
        gap = gap // 2
    return alist

# # start子數列開始的起始位置, gap表示間隔

def gapInsetionSort(alist,startpos,gap):
    #希爾排序的輔助函式
    for i in range(startpos+gap,len(alist),gap):
    #每輪有gap個子序列,每個子列有len(alist)/gap個元素,
        position=i
        currentvalue=alist[i]

        while position>startpos and alist[position-gap]>currentvalue:
            alist[position]=alist[position-gap]
            position=position-gap
        alist[position]=currentvalue
        
        

下面介紹使用分治法的經典演算法,快速排序

快速排序

快排充分體現了分治法的思想,運用遞迴,每次將待排序列一分為二,最終分到不可分。
就是三個迴圈只需要一個變數的額外儲存空間,選取樞軸量,空出一個元素,然後“左右互搏”,左右指標從序列兩端分別遍歷,交換元素。快排需要一個棧空間實現遞迴,深度為lgn~n

改進:
(1)隨機化改進:不是選取第一個值為基準,而是隨機選取。
平衡化改進:取第一個、最後一個和中間點三個值中中間值為基準進行排序。
設定閥值–混合排序:當陣列長度小於某一值時使用其他較快的排序。
(2)可以改進版的冒泡+快速排序,即在左右指標向中間移動時,同時執行氣泡排序,如果碰頭時沒有交換,證明該半邊已經基本有序,那麼就不需要再操作該半邊元素。
(3)每次只對元素較少的半邊進行快排

1.
def quick_sort(nums, start, end):
    if start >= end:
        return
    pivot = nums[start]  # 基準值
    low = start  # 左指標
    high = end  # 右指標
    while low < high:
        while low < high and nums[high] >= pivot:
            high -= 1
        nums[low] = nums[high]

        while low < high and nums[low] <= pivot:
            low += 1
        nums[high] = nums[low]
    nums[low] = pivot
    quick_sort(nums, start, low - 1)
    quick_sort(nums, low + 1, end)
    
nums = [1,3,54, 26, 93, 44,17, 77, 31, 1,321,44, 55, 20]
quick_sort(nums, 0, len(nums) - 1)
print(nums)

#結果:
[1, 1, 3, 17, 20, 26, 31, 44, 44, 54, 55, 77, 93, 321]

2.廖雪峰版本
from random import Random

def quick_sort(arr):
    if len(arr) > 1:
        qsort(arr, 0, len(arr) - 1)

def qsort(arr, start, end):
    base = arr[start]
    pl = start
    pr = end
    while pl < pr:
        while pl < pr and arr[pr] >= base:
            pr -= 1
        if pl == pr:
            break
        else:
            arr[pl], arr[pr] = arr[pr], arr[pl]

        while pl < pr and arr[pl] <= base:
            pl += 1
        if pl == pr:
            break
        else:
            arr[pl], arr[pr] = arr[pr], arr[pl]

    # now pl == pr
    if pl - 1 > start:
        qsort(arr, start, pl - 1)
    if pr + 1 < end:
        qsort(arr, pr + 1, end)

r = Random()
a = []
for i in range(20):
    a.append(r.randint(0, 100))

print a
quick_sort(a)
print a

#結果:
[40, 57, 29, 21, 98, 37, 16, 77, 63, 86, 63, 12, 23, 68, 38, 66, 64, 13, 63, 36]
[12, 13, 16, 21, 23, 29, 36, 37, 38, 40, 57, 63, 63, 63, 64, 66, 68, 77, 86, 98]

如果設f(n)為陣列長為n時的比較次數,則f(n)=[(f(1)+f(n-1))+(f(2)+f(n-2))+…+(f(n-1)+f(1))]/n.
利用數學知識易知f(n)=(n+1)*[1/2+1/3+…+1/(n+1)]-2n~1.386nlog(n).

歸併排序

歸併也體現分治的思想
詳見這篇部落格

# 歸併排序
def mergesort(seq):
    """歸併排序"""
    if len(seq) <= 1:
        return seq
    mid = len(seq) / 2  # 將列表分成更小的兩個列表
    # 分別對左右兩個列表進行處理,分別返回兩個排序好的列表
    left = mergesort(seq[:mid])
    right = mergesort(seq[mid:])
    # 對排序好的兩個列表合併,產生一個新的排序好的列表
    return merge(left, right)

def merge(left, right):
    """合併兩個已排序好的列表,產生一個新的已排序好的列表"""
    result = []  # 新的已排序好的列表
    i = 0  # 下標
    j = 0
    # 對兩個列表中的元素 兩兩對比。
    # 將最小的元素,放到result中,並對當前列表下標加1
    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
    result += left[i:]
    result += right[j:]
    return result



自底向上(非遞迴法)方法

# 自底向上的歸併演算法
def mergeBU(alist):
    n = len(alist)
    #表示歸併的大小
    size = 1
    while size <= n:
        for i in range(0, n-size, size+size):
            merge(alist, i, i+size-1, min(i+size+size-1, n-1))
        size += size
    return alist

# 合併有序數列alist[start....mid] 和 alist[mid+1...end],使之成為有序數列
def merge(alist, start, mid, end):
    # 複製一份
    blist = alist[start:end+1]
    l = start
    k = mid + 1
    pos = start

    while pos <= end:
        if (l > mid):
            alist[pos] = blist[k-start]
            k += 1
        elif (k > end):
            alist[pos] = blist[l-start]
            l += 1
        elif blist[l-start] <= blist[k-start]:
            alist[pos] = blist[l-start]
            l += 1
        else:
            alist[pos] = blist[k-start]
            k += 1
        pos += 1