1. 程式人生 > >Python之常見算法介紹

Python之常見算法介紹

info 適合 不同的 輔助 != ide top for 完全二叉樹

一、算法介紹

1、 算法是什麽

算法是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,算法代表著用系統的方法描述解決問題的策略機制。也就是說,能夠對一定規範的輸入,在有限時間內獲得所要求的輸出。如果一個算法有缺陷,或不適合於某個問題,執行這個算法將不會解決這個問題。不同的算法可能用不同的時間、空間或效率來完成同樣的任務。一個算法的優劣可以用空間復雜度與時間復雜度來衡量。

2、時間復雜度

在計算機科學中,算法的時間復雜度是一個函數,它定性描述了該算法的運行時間。這是一個關於代表算法輸入值的字符串的長度的函數。時間復雜度常用大O符號表述,不包括這個函數的低階項和首項系數。

一般情況下,算法中基本操作重復執行的次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函數。記作T(n)=O(f(n)),稱O(f(n))為算法的漸進時間復雜度(O是數量級的符號 ),簡稱時間復雜度。

常見時間復雜度單位:效率從上到下變低,
O(1) 簡單的一次運算(常數階)
O(n) 一次循環(線性階)
O(n^2) 兩個循環(平方階)

O(logn) 循環減半
O(nlogn) 一個循環加一個循環減半

O(n^2logn)
O(n^3)


一般情況下,隨著n的增大,T(n)增長最慢的算法為最優算法

O(1) 常數階 < O(logn) 對數階 < O(n) 線性階 < O(nlogn) < O(n^2) 平方階 < O(n^3) < { O(2^n) < O(n!) < O(n^n)

大O推導法:

  1. 用常數1取代運行時間中的所有加法常數
  2. 在修改後的運行函數中,只保留最高階項
  3. 如果最高階項存在且不是1,則去除與這個項相乘的常數

比如:

這是一段C的代碼
#include "stdio.h
" int main() { int i, j, x = 0, sum = 0, n = 100; /* 執行1次 */ for( i = 1; i <= n; i++) { sum = sum + i; /* 執行n次 */ for( j = 1; j <= n; j++) { x++; /* 執行n*n次 */ sum = sum + x; /* 執行n*n此 */ } } printf("%d", sum); /* 執行1次 */ }

分析:

執行總次數 = 1 + n + n*n + n*n + 1 = 2n2 + n + 2

根據大O推導法:

1.用常數 1 取代運行時間中的所有加法常數:執行總次數為: 2n2 + n + 1

2.在修改後的運行次數函數中,只保留最高階項,這裏的最高階是 n 的二次方: 執行總次數為: 2n2

3.如果最高階項存在且不是 1 ,則去除與這個項相乘的常數,這裏 n 的二次方不是 1 所以要去除這個項的相乘常數:執行總次數為: n2

因此最後我們得到上面那段代碼的算法時間復雜度表示為: O(n2)

3、空間復雜度

空間復雜度是用來評估算法內存占用大小的單位

空間換時間:如果需要增快算法的速度,需要的空間會更大

二、python實現常見的算法

前三種比較LowB,後三種比較NB

前三種時間復雜度都是O(n^2),後三種時間復雜度都是O(nlog(n))

1、冒泡(交換)排序

原理:列表中兩個相鄰的數,如果前一個數比後一個數大,就做交換。一共需要遍歷列表的次數是len(lst)-1
時間復雜度:O(n^2)

技術分享圖片

def bubble_sort(lst):
    for i in range(len(lst) - 1):  # 這是需要循環遍歷多少次
        for j in range(len(lst) - 1 - i):  # 每次數組中的無序區
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]


lst = [1, 2, 44, 3, 5]
bubble_sort(lst)
print(lst)

優化:如果在循環的時候,有一次沒有進行交換,就表示數列中的數據已經是有序的
時間復雜度:最好情況是0(n),只遍歷一次,一般情況和最壞情況都是O(n^2)

def bubble_sort(lst):
    for i in range(len(lst)-1):     # 這是需要循環遍歷多少次
        change = False      # 做一個標誌變量
        for j in range(len(lst)-1-i):   # 每次數組中的無序區
            if lst[j] >lst[j+1]:
                lst[j],lst[j+1] = lst[j+1],lst[j]
                change = True   # 每次遍歷,如果進來排序的話,就會改變change的值
        if not change:  # 如果change沒有改變,那就表示當前的序列是有序的,直接跳出循環即可
            return
 
 
lst = [1, 2, 44, 3, 5]
bubble_sort(lst)
print(lst)

2、選擇排序

原理:每次遍歷找到當下數組最小的數,並把它放到第一個位置,下次遍歷剩下的無序區,記錄剩余列表中最小的數,繼續放置

時間復雜度 O(n^2)

def select_sort(lst):
    for i in range(len(lst) - 1):  # 當前需遍歷的次數
        min_loc = i  # 當前最小數的位置
        for j in range(i + 1, len(lst)):  # 無序區
            if lst[j] < lst[min_loc]:  # 如果有更小的數
                min_loc = j  # 最小數的位置改變
        if min_loc != i:
            lst[i], lst[min_loc] = lst[min_loc], lst[i]  # 把最小數和無序區第一個數交換


lst = [1, 2, 44, 3, 5]
select_sort(lst)
print(lst)

3、插入排序

原理:列表分為有序區和無序區,有序區是一個相對有序的序列,最初有序區只有一個元素,每次從無序區選擇一個值,插入到有序區,直到無序區為空

時間復雜度:O(n^2)

def insert_sort(lst):
    for i in range(1, len(lst)):  # 從1開始遍歷表示無序區從1開始,有序區初始有一個值
        tmp = lst[i]  # tmp表示拿到的無序區的第一張牌
        j = i - 1  # j表示有序區的最後一個值
        while j >= 0 and lst[j] > tmp:  # 當有序區有值,並且有序區的值比無序區拿到的值大就一直循環
            lst[j + 1] = lst[j]  # 有序區的值往後移
            j -= 1  # 找到上一個有序區的值,然後再循環
        lst[j + 1] = tmp  # 跳出循環之後,只有j+1的位置是空的,要把當下無序區的值放到j+1的位置


lst = [1, 2, 44, 3, 5]
insert_sort(lst)
print(lst)

4、快速排序

思路:取第一個元素,讓它歸位,就是放到一個位置,使它左邊的都比它小,右邊的都比它大,然後遞歸完成排序

時間復雜度:O(nlog(n))

def partion(lst, left, right):
    key = lst[left]  # 5
    while left < right:
        # right下標位置開始,向左邊遍歷,查找不大於基準數的元素
        while left < right and lst[right] >= key:
            right -= 1
        if left < right:  # 找到小於準基數key的元素,然後交換lst[left],lst[right]
            lst[left], lst[right] = lst[right], lst[left]
        else:  # left〉=right 跳出循環
            break
        # left下標位置開始,向右邊遍歷,查找不小於基準數的元素
        while left < right and lst[left] < key:
            left += 1
        if left < right:  # 找到比基準數大的元素,然後交換lst[left],lst[right]
            lst[right], lst[left] = lst[left], lst[right]
        else:  # left〉=right 跳出循環
            break
    return left  # 此時left==right 所以返回right也是可以的


def quick_sort_standord(lst, left, right):
    if left < right:
        key_index = partion(lst, left, right)
        quick_sort_standord(lst, left, key_index)
        quick_sort_standord(lst, key_index + 1, right)


lst = [5, 1, 6, 7, 7, 4, 2, 3, 6]
quick_sort_standord(lst, 0, len(lst) - 1)
print(lst)

5、歸並排序

歸並排序(MERGE-SORT)是利用歸並的思想實現的排序方法,該算法采用經典的分治(divide-and-conquer)策略,分是將問題分成一些小的問題然後遞歸求解,而治的階段則將分的階段得到的各答案"歸並"在一起,即分而治之。

技術分享圖片

def merge_sort(li):
    # 不斷遞歸調用自己一直到拆分成成單個元素的時候就返回這個元素,不再拆分了
    if len(li) == 1:
        return li

    # 取拆分的中間位置
    mid = len(li) // 2
    # 拆分過後左右兩側子串
    left = li[:mid]
    right = li[mid:]

    # 對拆分過後的左右再拆分 一直到只有一個元素為止
    # 最後一次遞歸時候ll和lr都會接到一個元素的列表
    # 最後一次遞歸之前的ll和rl會接收到排好序的子序列
    ll = merge_sort(left)
    rl = merge_sort(right)

    # 我們對返回的兩個拆分結果進行排序後合並再返回正確順序的子列表
    # 這裏我們調用拎一個函數幫助我們按順序合並ll和lr
    return merge(ll, rl)


# 這裏接收兩個列表
def merge(left, right):
    # 從兩個有順序的列表裏邊依次取數據比較後放入result
    # 每次我們分別拿出兩個列表中最小的數比較,把較小的放入result
    result = []
    while len(left) > 0 and len(right) > 0:
        # 為了保持穩定性,當遇到相等的時候優先把左側的數放進結果列表,因為left本來也是大數列中比較靠左的
        if left[0] <= right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))
    # while循環出來之後 說明其中一個數組沒有數據了,我們把另一個數組添加到結果數組後面
    result += left
    result += right
    return result


li = [1, 5, 2, 4, 7, 5, 3, 2, 1]
li2 = merge_sort(li)
print(li2)

6、堆排序

1.堆是一個完全二叉樹
2.完全二叉樹即是:若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數(2層),第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。
3.堆滿足兩個性質: 堆的每一個父節點數值都大於(或小於)其子節點,堆的每個左子樹和右子樹也是一個堆。
4.堆分為最小堆和最大堆。最大堆就是每個父節點的數值要大於孩子節點,最小堆就是每個父節點的數值要小於孩子節點。排序要求從小到大的話,我們需要建立最大堆,反之建立最小堆。
5.堆的存儲一般用數組來實現。假如父節點的數組下標為i的話,那麽其左右節點的下標分別為:(2*i+1)和 (2*i+2)。如果孩子節點的下標為j的話,那麽其父節點的下標為(j-1)/2。
完全二叉樹中,假如有n個元素,那麽在堆中最後一個父節點位置為(n/2-1)。

def swap(a, b):  # 將a,b交換
    a, b = b, a
    return a, b


def sift_down(array, start, end):
    """
    調整成大頂堆,初始堆時,從下往上;交換堆頂與堆尾後,從上往下調整
    :param array: 列表的引用
    :param start: 父結點
    :param end: 結束的下標
    :return: 無
    """
    while True:

        # 當列表第一個是以下標0開始,結點下標為i,左孩子則為2*i+1,右孩子下標則為2*i+2;
        # 若下標以1開始,左孩子則為2*i,右孩子則為2*i+1
        left_child = 2 * start + 1  # 左孩子的結點下標
        # 當結點的右孩子存在,且大於結點的左孩子時
        if left_child > end:
            break

        if left_child + 1 <= end and array[left_child + 1] > array[left_child]:
            left_child += 1
        if array[left_child] > array[start]:  # 當左右孩子的最大值大於父結點時,則交換
            array[left_child], array[start] = swap(array[left_child], array[start])

            start = left_child  # 交換之後以交換子結點為根的堆可能不是大頂堆,需重新調整
        else:  # 若父結點大於左右孩子,則退出循環
            break

        print(">>", array)


def heap_sort(array):  # 堆排序
    # 先初始化大頂堆
    first = len(array) // 2 - 1  # 最後一個有孩子的節點(//表示取整的意思)
    # 第一個結點的下標為0,很多博客&課本教材是從下標1開始,無所謂吧,你隨意
    for i in range(first, -1, -1):  # 從最後一個有孩子的節點開始往上調整
        print(array[i])
        sift_down(array, i, len(array) - 1)  # 初始化大頂堆

    print("初始化大頂堆結果:", array)
    # 交換堆頂與堆尾
    for head_end in range(len(array) - 1, 0, -1):  # start stop step
        array[head_end], array[0] = swap(array[head_end], array[0])  # 交換堆頂與堆尾
        sift_down(array, 0, head_end - 1)  # 堆長度減一(head_end-1),再從上往下調整成大頂堆


array = [1, 1, 16, 7, 2, 3, 20, 3, 17, 8]
heap_sort(array)
print("堆排序最終結果:", array)

Python之常見算法介紹