python 常用算法學習(1)


算法就是為了解決某一個問題而采取的具體有效的操作步驟

算法的復雜度,表示代碼的運行效率,用一個大寫的O加括號來表示,比如O(1),O(n)

認為算法的復雜度是漸進的,即對於一個大小為n的輸入,如果他的運算時間為n3+5n+9,那么他的漸進時間復雜度是n3

遞歸

遞歸就是在函數中調用本身,大多數情況下,這會給計算機增加壓力,但是有時又很有用,比如下面的例子:

漢諾塔游戲

漢諾塔

把A柱的盤子,移動到C柱上,最少需要移動幾次,大盤子只能在小盤子下面

遞歸實現:

def hanoi(x, a, b, c):  # 所有的盤子從 a 移到 c

    if x > 0:
        hanoi(x-1, a, c, b)  # step1:除了下面最大的,剩余的盤子 從 a 移到 b
        print('%s->%s' % (a, c))  # step2:最大的盤子從 a 移到 c
        hanoi(x-1, b, a, c)  # step3: 把剩余的盤子 從 b 移到 c

hanoi(10, 'A', 'B', 'C')

#計算次數

def h(x):
    num = 1
    for i in range(x-1):
        num = 2*num +1

    print(num)
h(10)

 

用遞歸打印斐波那契數列

def fei(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fei(n-1)+fei(n-2)

 

你會發現,即使n只有幾十的時候,你的計算機內存使用量已經飆升了

其實,如果結合生成器,你會發現不管n有多大,都不會出現卡頓,但這是生成器的特性,本篇博客不重點介紹

# 結合生成器
def fei(n):
    pre,cur = 0,1
    while n >=0:
        yield pre
        n -= 1
        pre,cur = cur,pre+cur

for i in fei(400000):
    print(i)

 

關於遞歸次數,Python中有個限制,可以通過sys模塊來修改

import sys
sys.setrecursionlimit(1000000)

 


 

查找

1.順序查找

這個沒的說,就是for循環唄,時間復雜度O(n)

def linear_search(data_set, value):
    for i in range(len(data_set)):
        if data_set[i] == value:
            return i
    return

 

2.二分查找

時間復雜度O(logn)

就是一半一半的查找,看目標值在左邊一半還是右邊一半,然后替換左端點或者右端點,繼續判斷

非遞歸版本:

def binary_serach(li,val):
    low = 0
    high = len(li)-1
    while low <= high:
        mid = (low+high)//2
        if li[mid] == val:
            return mid
        elif li[mid] > val:
            high = mid-1
        else:
            low = mid+1
    else:
        return None

 

遞歸版本的二分查找

def bin_search_rec(data_set, value, low, high):
    if low < high:
        mid = (low + high) // 2
        if data_set[mid] == value:
            return mid
        elif data_set[mid] > value:
            return bin_search_rec(data_set, value, low, mid - 1)
        else:
            return bin_search_rec(data_set, value, mid + 1, high)
    else:
        return None

 


 

排序

速度慢的三個:

1.冒泡排序

  原理就是,列表相鄰的兩個數,如果前邊的比后邊的小,那么交換順序,經過一次排序后,最大的數就到了列表最前面

  代碼:  

def bubble_sort(li):

    for j in range(len(li)-1):
        for i in range(1, len(li)):
            if li[i] > li[i-1]:
                li[i], li[i-1] = li[i-1], li[i]

    return li

 

冒泡排序的最差情況,即每次都交互順序的情況,時間復雜度是O(n2)

存在一個最好情況就是列表本來就是排好序的,所以可以加一個優化,加一個標志位,如果沒有出現交換順序的情況,那就直接return 

# 優化版本的冒泡
def bubble_sort_opt(li):
    for j in range(len(li)-1):
        flag = False
        for i in range(1, len(li)):
            if li[i] > li[i-1]:
                li[i], li[i-1] = li[i-1], li[i]
                flag = True
        if not flag:
            return li
    return li

 

2.插入排序

  原理:把列表分為有序區和無序區兩個部分。最初有序區只有一個元素。然后每次從無序區選擇一個元素,插入到有序區的位置,直到無序區變空。

def insert_sort(li):
    for i in range(1,len(li)):
        tmp = li[i]
        j = i - 1
        while j >= 0 and tmp < li[j]:    # 找到一個合適的位置插進去
            li[j+1] = li[j]
            j -= 1
        li[j+1] = tmp
    return li

   簡單形象的一張圖:

時間復雜度是O(n2)

   示例如下:

def insert_sort(li):
    '''
    初始時手里(有序區)只有一張牌
    每次(從無序區)摸一張牌,插入到手里已有牌的正確位置
    :param li:
    :return:
    '''
    for i in range(1, len(li)):
        tmp = li[i]
        j = i - 1
        while j >= 0 and tmp < li[j]:
            li[j+1] = li[j]
            j = j - 1
        li[j+1] = tmp
        print(li)
    return li
li = [6,5,4,3,2,1]
print('li:   ', li)
res = insert_sort(li)
print('res:   ', res)

'''
li:    [6, 5, 4, 3, 2, 1]
[5, 6, 4, 3, 2, 1]
[4, 5, 6, 3, 2, 1]
[3, 4, 5, 6, 2, 1]
[2, 3, 4, 5, 6, 1]
[1, 2, 3, 4, 5, 6]
res:    [1, 2, 3, 4, 5, 6]
'''

  

3.選擇排序

  原理:遍歷列表一遍,拿到最小的值放到列表第一個位置,再找到剩余列表中最小的值,放到第二個位置。。。。

def select_sort(li):
    for i in range(len(li)-1):
        min_loc = i         # 假設當前最小的值的索引就是i
        for j in range(i+1,len(li)):
            if li[j] < li[min_loc]:
                min_loc = j
        if min_loc != i:   # min_loc 值如果發生過交換,表示最小的值的下標不是i,而是min_loc
            li[i],li[min_loc] = li[min_loc],li[i]

    return li

 

時間復雜度是O(n2)

 

速度快的幾種排序:

4.快速排序(快排)

原理:讓指定的元素歸位,所謂歸位,就是放到他應該放的位置(左變的元素比他小,右邊的元素比他大),然后對每個元素歸位,就完成了排序

可以參考這個動圖來理解下面的代碼

代碼:

#  歸位函數
def partition(data, left, right): # 左右分別指向兩端的元素
    tmp = data[left]                # 把左邊第一個元素賦值給tmp,此時left指向空
    while left < right:             # 左右兩個指針不重合,就繼續
        while left < right and data[right] >= tmp:  # right指向的元素大於tmp,則不交換
            right -= 1                      # right 向左移動一位
        data[left] = data[right]            # 如果right指向的元素小於tmp,就放到左邊現在為空的位置
        while left < right and data[left] <= tmp:   # 如果left指向的元素小於tmp,則不交換
            left += 1                       # left向右移動一位
        data[right] = data[left]            # 如果left指向的元素大於tmp,就交換到右邊
    data[left] = tmp            # 最后把最開始拿出來的那個值,放到左右重合的那個位置
    return left                 # 最后返回這個位置

#  寫好歸位函數后,就可以遞歸調用這個函數,實現排序
def quick_sort(data, left, right):
    if left < right:
        mid = partition(data, left, right)  # 找到指定元素的位置
        quick_sort(data, left, mid - 1)     # 對左邊元素排序
        quick_sort(data, mid + 1, right)    # 對右邊元素排序
    return data

 

正常的情況,快排的復雜度是O(nlogn)

快排存在一個最壞情況,就是每次歸位,都不能把列表分成兩部分,此時復雜度就是O(n2)了,如果要避免設計成這種最壞情況,可以在取第一個數的時候不要取第一個了,而是取一個列表中的隨機數

 

5.歸並排序

原理:列表分成兩段有序,然后分解成每個元素后,再合並成一個有序列表,這種操作就叫做一次歸並

  應用到排序就是,把列表分成一個元素一個元素的,一個元素當然是有序的,將有序列表一個一個合並,最終合並成一個有序的列表

  

 

圖示:

 

代碼:

def merge(li, left, mid, right):
    # 一次歸並過程,把從mid分開的兩個有序列表合並成一個有序列表
    i = left
    j = mid + 1
    ltmp = []
    # 兩個列表的元素依次比較,按從大到小的順序放到一個臨時的空列表中
    while i <= mid and j <= right:
        if li[i] < li[j]:
            ltmp.append(li[i])
            i += 1
        else:
            ltmp.append(li[j])
            j += 1

    # 如果兩個列表並不是平均分的,就會存在有元素沒有加入到臨時列表的情況,所以再判斷一下
    while i<= mid:
        ltmp.append(li[i])
        i += 1
    while j <= right:
        ltmp.append(li[j])
        j += 1
    li[left:right+1] = ltmp
    return li


def _merge_sort(li, left, right):
    # 細分到一個列表中只有一個元素的情況,對每一次都調用merge函數變成有序的列表
    if left < right:
        mid = (left+right)//2
        _merge_sort(li, left, mid)
        _merge_sort(li, mid+1, right)
        merge(li, left, mid, right)
    return li

def merge_sort(li):
    return(_merge_sort(li, 0, len(li)-1))

 

照例,時間復雜度是O(nlogn)

特殊的,歸並排序還有一個O(n)的空間復雜度

 

6.堆排序

把這個放到最后,是因為這個是最麻煩的,把最麻煩的放到最后,是一種對工作負責的表現

如果要說堆排序,首先得先把‘樹’搞明白

樹是一種數據結構

樹是由n個節點組成的集合; -->如果n為0,那這是一顆空樹,如果n>0,那么那存在1個節點作為樹的根節點,其他節點可以分為m個集合,每個集合本身又是一棵樹。

一些可能會用到的概念:

  根節點:樹的第一個節點,沒有父節點的節點

  葉子節點:不帶分叉的節點

  樹的深度(高度):就是分了多少層

  孩子節點、父節點:節點與節點之間的關系

圖示:

 

二叉樹

然后在樹的基礎上,有一個二叉樹,二叉樹就是每個節點最多有兩個子節點的樹結構,比如這個:

 

滿二叉樹:除了葉子節點,所有節點都有兩個孩子,並且所有葉子節點深度都一樣

完全二叉樹:是有滿二叉樹引申而來,假設二叉樹深度為k,那么除了第k層,之前的每一層的節點數都達到最大,即沒有空的位置,而且第k層的子節點也都集中在左子樹上(順序)

 

二叉樹的存儲方式

有鏈式存儲和順序存儲的方式(列表),本篇只討論順序存儲的方式

思考:

  父節點和左孩子節點的編號下標有什么關系?    0-1 1-3 2-5 3-7 4-9         i  ---->   2i+1

  父節點和右孩子節點的編號下標有什么關系?    0-2 1-4 2-6 3-8 4-10  i  ----->  2i+2

 

再來了解下堆,堆說起來又麻煩了,我將在另一篇博客中單獨寫堆,棧等這些數據結構,本篇先討論與排序有關的東西

堆是一類特殊的樹,要求父節點大於或小於所有的子節點

大根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點大    ,升序用大根堆

小根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點小

 

 

堆的調整:當根節點的左右子樹都是堆時,可以通過一次向下的調整來將其變換成一個堆。

所謂一次向下調整,就是說把堆頂的值,向下找一個合適的位置,是一次一次的找,跟他交換位置的值,也要找到一個合適的位置

    “瀏覽器寫的沒保存,丟失了,所以這塊不想再寫了。。。”

 

堆排序的過程

  1.構造堆

  2.得到堆頂元素,就是最大的元素

  3.去掉堆頂,將堆的最后一個元素放到堆頂,此時可以通過一次調整重新使堆有序

  4.堆頂元素為第二大元素

  5.重復步驟3,直到堆為空

 

其中構造堆的過程:

 

 

挨個出數的過程:

代碼:

def sift(li, left, right):  # left和right 表示了元素的范圍,是根節點到右節點的范圍,然后比較根和兩個孩子的大小,把大的放到堆頂
                                    # 和兩個孩子的大小沒關系,因為我們只需要拿堆頂的元素就行了
    # 構造堆
    i = left        # 當作根節點
    j = 2 * i + 1   # 上面提到過的父節點與左子樹根節點的編號下標的關系
    tmp = li[left]
    while j <= right:
        if j+1 <= right and li[j] < li[j+1]:    # 找到兩個孩子中比較大的那個
            j = j + 1
        if tmp < li[j]:     # 如果孩子中比較大的那個比根節點大,就交換
            li[i] = li[j]
            i = j           # 把交換了的那個節點當作根節點,循環上面的操作
            j = 2 * i + 1
        else:            
            break
    li[i] = tmp             # 如果上面發生交換,現在的i就是最后一層符合條件(不用換)的根節點,

def heap_sort(li):
    n = len(li)
    for i in range(n//2-1, -1, -1):  # 建立堆        n//2-1 是為了拿到最后一個子樹的根節點的編號,然后往前走,最后走到根節點0//2 -1 = -1
        sift(li, i, n-1)                # 固定的把最后一個值的位置當作right,因為right只是為了判斷遞歸不要超出當前樹,所以最后一個值可以滿足
                                                    # 如果每遍歷一個樹,就找到它的右孩子,太麻煩了
    for i in range(n-1, -1, -1):    # 挨個出數
        li[0], li[i] = li[i],li[0]      # 把堆頂與最后一個數交換,為了節省空間,否則還可以新建一個列表,把堆頂(最大數)放到新列表中
        sift(li, 0, i-1)            # 此時的列表,應該排除最后一個已經排好序的,放置最大值的位置,所以i-1

 

時間復雜度也是O(nlogn)

來擴展一下,如果要取一個列表的top10,就是取列表的前十大的數,怎么做?

可以用堆來實現,取堆的前十個數,構造成一個小根堆,然后依次遍歷列表后面的數,如果比堆頂小,則忽略,如果比堆頂大,則將堆頂替換成改元素,然后進行一次向下調整,最終這個小根堆就是top10

其實Python自帶一個heapq模塊,就是幫我們對堆進行操作的

heapq模塊

隊列中的每個元素都有優先級,優先級最高的元素優先得到服務(操作),這就是優先隊列,而優先隊列通常用堆來實現

如果用heapq模塊來實現堆排序,就簡單多了:

import heapq
def heapq_sort(li):
    h = []
    for value in li:
        heapq.heappush(h,value)
    return [heapq.heappop(h) for i in range(len(h))]

而想取top10 ,直接一個方法就行了

heapq.nlargest(10,li)

 

這三種速度快的排序方式就說完了,其中,快排是速度最快的,即使這樣,也不如Python自帶的sort快

再來介紹兩種排序,希爾排序和計數排序

7.希爾排序

希爾排序是一種分組插入排序的算法  

思路:

  首先取一個整數d1=n/2,將元素分為d1個組,每組相鄰量元素之間距離為d1,在各組內進行直接插入排序;

  取第二個整數d2=d1/2,重復上述分組排序過程,直到di=1,即所有元素在同一組內進行直接插入排序。

希爾排序每趟並不使某些元素有序,而是使整體數據越來越接近有序;最后一趟排序使得所有數據有序。

 

圖示:

  

代碼:

def shell_sort(li):
    gap = int(len(li)//2)   # 初始把列表分成 gap個組,但是每組最多就兩個元素,第一組可能有三個元素
    while gap >0:
        for i in range(gap,len(li)):
            tmp = li[i]
            j = i - gap
            while j>0 and tmp<li[j]:    # 對每一組的每一個數,都和他前面的那個數比較,小的在前面
                li[j+gap] = li[j]
                j -= gap
            li[j+gap] = tmp
        gap = int(gap//2)    # Python3中地板除也是float類型
    return li

   通過diamante也能看出來,其實希爾排序和插入排序是非常相像的,插入排序就可以看做是固定間隔為1的希爾排序,希爾排序就是把插入排序分了個組,同一個組內,相鄰兩個數之間不是相差1,而是相差gap

時間復雜度:O((1+t)n),其中t是個大於0小於1的數,取決於gap的取法,當gap=len(li)//2的時候,t大約等於0.3

 

8.計數排序

需求:有一個列表,列表中的數都在0到100之間(整數),列表長度大約是100萬,設計算法在O(n)時間復雜度內將列表進行排序

分析:列表長度很大,但是數據量很少,會有大量的重復數據。可以考慮對這100個數進行排序

代碼:

def count_sort(li):
    count = [0 for i in range(101)]  # 根據原題,0-100的整數
    for i in li:
        count[i] += 1

    i = 0
    for num,m in enumerate(count):  # enumerate函數將一個可遍歷的數據對象(如列表、元組或字符串)組合為一個索引序列,同時列出數據和數據下標,一般用在 for 循環當中。
        for j in range(m):
            li[i] = num
            i += 1

 

 此文摘抄來自    http://www.cnblogs.com/zhang-can/p/8011002.html   
抄過來的目的是為了學習,並不是炫耀什么的,不喜勿噴,謝謝

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM