算法介紹(二分,冒泡,選擇,插入;快排,堆排,歸併)

1.概念

  算法:一個計算過程(函數),或者說是解決問題的方法可以理解成一個算法

  時間複雜度:用來估算算法運行時間的一個式子(單位)。一般來說,時間複雜度高的算法比複雜度低的算法慢

  空間複雜度:用來估算算法佔用內存的一個式子

1.1 常見時間複雜度按照效率排序

  O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)

  一般來說

      循環減半的過程,時間複雜度通常是O(logn)

      幾次循環,時間複雜度就是n的幾次方

1.2 案例

"""
一段有n個臺階組成的樓梯,小明從樓梯的最底層向最高處前進,它可以選擇一次邁一級臺階或者一次邁兩級臺階。問:他有多少種不同的走法?
"""

思路:

  假設上 n 級臺階有 f(n)種方法,把這 n 種方法分爲兩大類,第一種最後一次上了一級臺階,這類方法共有 f(n-1)種,第二種最後一次上了兩級臺階,這種方法共有 f(n-2)種,則得出遞推公式f(n)=f(n-1)+f(n-2),顯然,f(1)=1,f(2)=2,遞推公式如下:

def steps(n):
    if n <= 2:
        return n
    else:
        return steps(n-1) + steps(n-2)

print(steps(10))

這種方式簡單,但是效率低,很有可能超出遞歸最大層數

我們可以考慮採用循環來做

1.3 二分排序法(時間複雜度O(logn))

  這裏針對的是一個有序列表,一般是在算法中輸入該列表中某個數,然後輸出這個數在列表中的位置,即索引

示意圖:

代碼:

def bin_search(key, lis):
    low = 0     #
    high = len(lis) - 1     #
    while low < high:
        mid = (low + high) // 2
        if lis[mid] == key:    # 命中
            return mid
        elif lis[mid] > key:  # key在左邊,說明key的範圍在low到mid-1之間
            high = mid - 1      
        elif lis[mid] < key:  # key在右邊,說明key的範圍在high+1到high之間
            low = mid + 1

利用遞歸實現二分查找(可以使用bisect模塊來實現):

def bin_search_2(key, lis, low, high):
    if low <= high:
        mid = (low + high) // 2
        if lis[mid] == key:
            return mid
        elif lis[mid] > key:    # 在左邊,說明key在low和mid-1之間
            return bin_search_2(key,lis,low, mid-1)
        else:                   # 在右邊,說明key在mid+1和high之間
            return bin_search_2(key,lis,mid+1,high)
    else:
        return

 對於一個二維列表實現的二分查找:

li = [
    [1, 3, 5, 7],
    [9,10,14,17],
    [18,22,25,30]
]

代碼:

def binary_search_2d(li, val):
    left = 0
    m = len(li)
    n = len(li[0])
    right = m * n - 1
    while left <= right:    # 候選區非空
        mid = (left + right) // 2
        i = mid // n       # 取整
        j = mid % n        # 取餘
        if li[i][j] == val:
            return i, j
        elif li[i][j] < val:
            left = mid + 1  # 更新候選區
        else: # >
            right = mid - 1 # 更新候選區
    return None

print(binary_search_2d(li, 22))

2. 排序入門篇

2.1 冒泡排序(時間複雜度O(n**2))

  針對的是列表中的兩個相鄰的數,如果前邊比後面的大,則交換這兩個數,如此持續進行

  代碼關鍵點在於幾趟以及無序區

示例代碼1:

def bubble_sort(lis):
    # j表示每次便利需要遍歷的最小次數,它是逐漸減小的
    for j in range(len(lis)-1,0,-1):
        for i in range(j):
            if lis[i] > lis[i+1]:
                lis[i],lis[i+1] = lis[i+1], lis[i]

lis = [9,5,2,7,6,8,3,1,4]
bubble_sort(lis)
print(lis)

示例代碼2:

import random

def bubble_sort(li):
    for i in range(len(li)-1): # i表示第i趟
        for j in range(len(li)-i-1): # j表示箭頭的下標
            if li[j] > li[j+1]:  # 後面的數比前面的數大
                li[j], li[j+1] = li[j+1], li[j]
        print(li)
            
                
li = list(range(10))
random.shuffle(li)
bubble_sort(li)

 以上代碼忽視了極端情況,從運行效率上來講,我們還可以在做優化,排除已經是有序的列表

import random

def bubble_sort_2(li):
    for i in range(len(li)-1): # i表示第i趟
        exchange = False
        for j in range(len(li)-i-1): # j表示箭頭的下標
            if li[j] > li[j+1]:     # 如果前面比後面大
                li[j], li[j+1] = li[j+1], li[j]
                exchange = True
        if not exchange:
            return
        print(li)

li = list(range(10))
random.shuffle(li)
bubble_sort_2(li)

2.2 選擇排序(時間複雜度O(n**2))

選擇排序第一種代碼:

  一趟遍歷記錄最小的數,放到第一個位置;

  再一趟遍歷記錄剩餘列表中最小的數,繼續放置;

  關鍵點在於無序區以及最小數的位置

代碼:

import random

def select_sort(li):
    for i in range(len(li)-1):  # i可以理解成趟數,這裏看做索引也行
        min_pos = i # 最小值默認爲無序區第一個數 i
        for j in range(i+1, len(li)):
            if li[j] < li[min_pos]:
                min_pos = j
        li[i], li[min_pos] = li[min_pos], li[i]


li = list(range(100))
random.shuffle(li)
select_sort(li)
print(li)

選擇排序第二種代碼:

  這裏我也可以先去查找數組中最小值的索引,然後再追加的方式來排序(好理解,但是代碼量變大)

# 查找最小值的索引
def findSmanllest(arr):
    smallest = arr[0]
    smallest_index = 0
    for i in range(1,len(arr)):      # 這裏應該是range(1,len(arr))還是range(len(arr))??嘗試兩個都可以
        if arr[i] < smallest:
            smallest = arr[i]
            smallest_index = i
    return smallest_index       # 返回數組中最小值的索引

# 進行排序
def selectionSort(arr):
    newarr = []
    for i in range(len(arr)):
        smallest = findSmanllest(arr)
        newarr.append(arr.pop(smallest))    # 追加數組中指定索引的值
    return newarr

print(selectionSort([5,3,6,2,10,41,1]))

2.3 插入排序(時間複雜度O(n**2))

  列表被分爲有序區和無序區兩個部分。最初有序區只有一個元素。

  每次從無序區選擇一個元素,插入到有序區的位置,直到無序區變空。(類似我們玩紙牌)

  代碼關鍵點在於我們摸到的牌,以及手裏的牌

 代碼:

import random

def insert_sort(li):
    for i in range(1, len(li)): # i表示摸到牌的位置,也表示趟
        j = i - 1       # j是當前用來比較的牌
        tmp = li[i]     # 摸到的牌
        while j >= 0 and li[j] > tmp:   # 摸到的牌和他前面位置(前面位置必須大於等於0)的牌作比較
            li[j+1] = li[j]     # 前面的牌比手裏的大,把它往後移
            j -= 1      # 再去看前面那張牌
        li[j+1] = tmp   # 用來比較的牌比摸到的牌小,直接放到j+1的位置


li = list(range(100, -1, -1))
random.shuffle(li)
insert_sort(li)
print(li)

3. 排序升級篇

3.1 快排(時間複雜度O(n*log(n)))      --->算法裏面簡單,也是必須要掌握的一個

  取一個元素p(第一個元素),使元素p歸位;

  列表被p分成兩部分,左邊都比p小,右邊都比p大;

  遞歸完成排序。

  該算法的關鍵點是先對數組進行整理,然後再做遞歸操作

代碼第一部分

def quick_sort(lis, left, right):
    if left < right:
        mid = partition(lis, left, right)  # 返回中間這個值
        quick_sort(lis, left, mid - 1)      # 對左邊的進行排序
        quick_sort(lis, mid + 1, right)     # 對右邊的進行排序

但是怎麼對它做p歸位處理呢?這裏我們先取出第一個數,即5,5的空位需要從右邊往左找一個比5小的數填充,即2

2放在了5的空位上,現在需要再從左邊取一個比5大的數放在2的空位上,即7

7放在了2的空位上,現在需要再從右邊取一個比5小的數放在7的空位上,即1

---

代碼第二部分(做歸位處理)

def partition(lis, left, right):
    tmp = lis[left]  # 先取出最左邊這個數,存起來
    while left < right:  # 從右邊找比tmp小的數
        while left < right and lis[right] >= tmp:
            right -= 1  # 如果右邊的數總比取出的數大,則往左前進一位繼續找直到找見爲止
        lis[left] = lis[right]  # 找見比tmp小的數,此時插入到左邊空出的那個位置
        while left < right and lis[left] <= tmp:  # 從左邊找比tmp大的數
            left += 1  # 如果左邊的數總比取出的數小,則往右前進一位繼續找直到找見爲止
        lis[right] = lis[left]  # 找見比tmp大的數,此時插入到右邊空出的那個位置
    lis[left] = tmp  # 再把中間這個值tmp寫回來
    return left  # 返回中間這個值,left,right最後重合在一起了


lis = [5, 7, 4, 6, 3, 1, 2, 9, 8] partition(lis, 0, len(lis) - 1) print(lis) # [2, 1, 4, 3, 5, 6, 7, 9, 8] # 可以看出5歸位

以上是我們的基本思想,但是如果不從最左邊取這個tmp,而是從中任意取一個數作爲我們的tmp值呢?

代碼:

import random


def partition(li, left, right):
    i = random.randint(left, right)  # 從中取一個隨機數i
    li[left], li[i] = li[i], li[left]  # 把它調換位置放在最左邊
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:
            right -= 1
        li[left] = li[right]
        while left < right and li[left] <= tmp:
            left += 1
        li[right] = li[left]
    li[left] = tmp
    return left


def quick_sort(lis, left, right):
    if left < right:
        mid = partition(lis, left, right)  # 返回中間這個值
        quick_sort(lis, left, mid - 1)  # 對左邊的進行排序
        quick_sort(lis, mid + 1, right)  # 對右邊的進行排序


lis = list(range(10000))
random.shuffle(lis)
quick_sort(lis, 0, len(lis) - 1)
print(lis)

如果上面沒有記住的話,ok可以看下面這種,這種快排方式代碼更加簡略和易懂,也更加pythonic

def quick_sort(lis):
    if len(lis) < 2:
        return lis
    else:
        pivot = lis[0]  # 選定的基準值,總取左邊第一個值
        left = [i for i in lis[1:] if i <= pivot]  # 小於等於基準值的元素組成的列表
        right = [i for i in lis[1:] if i > pivot]  # 大於基準值的元素組成的列表
        return quick_sort(left) + [pivot] + quick_sort(right)


lis = [9, 5, 2, 8, 6, 1, 4, 7, 3]
print(quick_sort(lis))
# 把左邊的數組+基準值+右邊的數組
# 第一次基準值9 [5,2,8,6,1,4,7,3] + [9] + []
# 第二次基準值5 [2,1,4,3] + [5] + [8,6,7] + [9] + []
# 第三次基準值2 [1]+[2] + [4,3] + [5] + [8,6,7] + [9] + []
# 第四次基準值4 [1]+[2] + [3]+[4]+[] + [5] + [8,6,7] + [9] + []
# 第四次基準值8 [1]+[2] + [3]+[4]+[] + [5] + [6,7]+[8]+[] + [9] + []
# 第五次基準值6 [1]+[2]+[3]+[4]+[]+[5]+[]+[6]+[7] +[8] + [] + [9] + []

把上面代碼一行實現

lis = [3, 6, 12, 21]

quick_sort = lambda lis: lis if len(lis) < 2 else quick_sort(
    [i for i in lis[1:] if i <= lis[0]]) + [lis[0]] + quick_sort([i for i in lis[1:] if i > lis[0]])

print(quick_sort(lis))

3.2 堆排(最複雜的一個,利用二叉樹)

3.2.1 二叉樹相關概念

  結點的度(Degree):一個結點擁有的子樹數目稱爲該結點的度 

  二叉樹:度不超過2的樹(節點最多有兩個叉)

  深度(Depth):樹中結點最大層次的值

  滿二叉樹:一個二叉樹,如果每一個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹。樹的特點是每一層上的節點數都是最大節點數

  完全二叉樹:在一棵二叉樹中,除最後一層外,若其餘層都是滿的,並且最後一層或者是滿的,或者是在右邊缺少連續若干節點,則此二叉樹完全二叉樹

      

關於二叉樹的存儲方式:

3.2.2 關於堆排序

  堆分爲最大堆和最小堆,其實就是完全二叉樹

  堆排序算法就是抓住了堆的特點,每次都取堆頂的元素,然後將剩餘的元素重新調整爲最大堆,依次類推,最終得到排序的序列。

 堆排序過程:

  1.建立堆
  2.得到堆頂元素,爲最大元素
  3.去掉堆頂,將堆最後一個元素放到堆頂,此時可通過一次調整重新使堆有序
  4.堆頂元素爲第二大元素
  5.重複步驟3,直到堆變空

這裏我們要怎麼建立堆呢???

堆排序的基本實現思路:

  給定一個列表array=[6,8,1,9,3,0,7,2,4,5],對其進行堆排序

第一步:根據該數組元素構建一個完全二叉樹

第二步:初始化構造(從最後一個有子節點開始往上調整最大堆)

繼續調整

繼續調整

繼續調整

繼續調整

交換到這裏的時候我們可以看到6和8其實還不滿足堆的性質,所以還需要進行調整

這樣就得到了一個初始堆

 我們構造好了堆,怎麼一個出數呢???這裏我們還要對它進行出數調整:

  每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換

  交換之後可能造成被交換的孩子節點不滿足堆的性質,因此每次交換之後要重新對被交換的孩子節點進行調整

出數:

第一步:9出來,3上去

堆調整,讓它滿足堆的性質

第二步:8出來,3上去

堆調整,讓它滿足堆的性質

 

第三步:7出來,2上去

堆調整,讓它滿足堆的性質

第四步:6出來,1上去

堆調整,讓它滿足堆的性質

第五步:5出來,0上去

堆調整,讓它滿足堆的性質

 

第六步:4出來,0上去

堆調整,讓它滿足堆的性質

 第七步:3出來,1上去

堆調整,讓它滿足堆的性質

第八步:2出來,1上去

堆調整,讓它滿足堆的性質

第九步:1出來,0上去

第十步:0出來

 

說明:

  不管是初始大頂堆的從下往上調整,還是堆頂堆尾元素交換,每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換,交換之後都可能造成被交換的孩子節點不滿足堆的性質,因此每次交換之後要重新對被交換的孩子節點進行調整。

 以上邏輯已經有了,下來是編寫代碼:

# 調整堆,單獨運行只能一次調整一下
def sift(lis, low, high):
    """
    low是待調整的堆的根節點的位置索引
    high是堆的最後節點的位置,用來判斷是否越界
    """
    i = low         # 當前的空位
    j = 2 * i + 1  # 左邊那個孩子
    tmp = lis[i]    # 出來的那個數
    while j <= high:
        if j < high and lis[j + 1] > lis[j]:  # 右孩子存在並且右孩子大於左孩子
            j += 1  # j指向右孩子
        if tmp < lis[j]:    # 出來的數小於左邊那個數
            lis[i] = lis[j] # lis[j]應該上去
            i = j       # 再重新算
            j = 2 * i + 1
        else:  # tmp比左邊這個孩子大則不用動
            break
    lis[i] = tmp


# 堆排序
def heap_sort(li):
    # 先構造堆
    n = len(li)
    for i in range(n//2,-1,-1): # i是調整子樹的根,從n//2開始,一直到0
        sift(li,i,n-1)      # 整個堆的最後一個節點的位置當做high
    # 挨個出數
    for i in range(n-1,-1,-1):   # i是當前堆的最後一個元素位置
        li[0],li[i] = li[i],li[0]
        sift(li,0,i-1)  # 調整


li = [6,8,1,9,3,0,7,2,4,5]
heap_sort(li)
print(li)

可以看圖

第二個版本,代碼比較詳細,可以作爲參考:

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]:  # 當左右孩子的最大值大於父結點時,則交換
            temp = array[left_child]
            array[left_child] = array[start]
            array[start] = temp

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


def heap_sort(array):  # 堆排序
    # 初始化大頂堆
    first = len(array) // 2 - 1  # 最後一個有孩子的節點(//表示取整的意思)
    for i in range(first, -1, -1):  # 從最後一個有孩子的節點開始往上調整
        sift_down(array, i, len(array) - 1)  # 初始化大頂堆

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


if __name__ == "__main__":
    array = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
    heap_sort(array)
    print(array)

該代碼參考來源鏈接:猛戳此處

以上說了這麼多,能看懂已經很不錯了,更何況還需要嘴啃。但是對於學Python的朋友來說,python內置模塊heapq已經幫我們實現了堆排序,我們可以直接調用它實現一個堆排序

python內置模塊heapq的簡單使用:

import heapq

x = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]

heapq.heapify(x)
print(x)    # 一個堆排,默認小頂堆
# [0, 2, 1, 4, 3, 6, 7, 9, 8, 5]

heapq.heappush(x, -1)   # 添加一個值
print(x)    # [-1, 0, 1, 4, 2, 6, 7, 9, 8, 5, 3]

print(heapq.heappop(x)) # 取出堆頂元素
print(x)    # -1

print(heapq.nsmallest(5, [1,5,4,7,2,8,9,3,0]))  # 返回最小的5個元素
# [0, 1, 2, 3, 4]
print(heapq.nlargest(5, [1,5,4,7,2,8,9,3,0]))  # 返回最大的5個元素
# [9, 8, 7, 5, 4]

實現排序:

import heapq

def heap_sort(li):
    heapq.heapify(li)   # 先進行堆排序
    res = []
    for i in range(len(li)):
        res.append(heapq.heappop(li))
    return res

print(heap_sort([6, 8, 1, 9, 3, 0, 7, 2, 4, 5]))
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

例題:

  現有n個數,設計算法找出前k大的數(k<n)

  思路:

  1.取列表前k個元素建立一個小根堆。堆頂就是目前第k大的數。
  2.依次向後遍歷原列表,對於列表中的元素,如果小於堆頂,則忽略該元素;如果大於堆頂,則將堆頂更換爲該元素,並且對堆進行一次調整。
  3.遍歷列表所有元素後,倒序彈出堆頂。

# 調整堆
def sift(lis, low, high):
    # low是待調整的堆的根節點的位置,
    # high是堆的最後節點的位置,用來判斷是否越界

    i = low  # 當前的空位
    j = 2 * i + 1  # 左邊那個孩子
    tmp = lis[i]  # 出來的那個數
    while j <= high:
        if j < high and lis[j + 1] > lis[j]:  # 右孩子存在並且右孩子大於左孩子
            j += 1  # j指向右孩子
        if tmp < lis[j]:  # 出來的數小於左邊那個數
            lis[i] = lis[j]  # lis[j]應該上去
            i = j  # 再重新算
            j = 2 * i + 1
        else:  # tmp比左邊這個孩子大則不用動
            break
    lis[i] = tmp


def top(lis, k):
    heap = lis[0:k]     # 取出k之前的數
    for i in range(k // 2 - 1, -1, -1): # i是調整子樹的根
        sift(heap, i, k - 1)    # 調整堆
    for i in range(k, len(lis)):
        if lis[i] > heap[0]:
            heap[0] = lis[i]
            sift(heap, 0, i - 1)
    for i in range(k - 1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i - 1)


lis = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
top(lis, 3)
View Code

3.3 歸併排序    時間複雜度:O(nlogn)  空間複雜度:O(n)

  將兩段有序的列表合併成一個有序的列表

圖解

 代碼

# 兩段有序排序
def merge(li, low, mid, high):
    i = low
    j = mid + 1
    li_tmp = []
    while i <= mid and j <= high:
        if li[i] <= li[j]:
            li_tmp.append(li[i])
            i += 1
        else:
            li_tmp.append(li[j])
            j += 1
    while i <= mid:
        li_tmp.append(li[i])
        i += 1
    while j <= high:
        li_tmp.append(li[j])
        j += 1
    li[low:high + 1] = li_tmp


# li = [2,5,7,8,9,1,3,4]
# merge(li,0,(len(li)-1)//2,len(li)-1)
# print(li)  # [2, 5, 7, 8, 9, 1, 3, 4]

def _merge_sort(li, low, high):
    if low < high:  # 至少有兩個元素
        mid = (low + high) // 2
        _merge_sort(li, low, mid)
        _merge_sort(li, mid + 1, high)    
        merge(li, low, mid, high)
    
    
li = [2,5,7,8,9,1,3,4]
_merge_sort(li, 0, len(li)-1)
print(li)

3.5 以上算法說明

一般情況下,就運行時間而言:
  快速排序 < 歸併排序 < 堆排序

三種高級排序算法的缺點:
  快速排序:極端情況下排序效率低
  歸併排序:需要額外的內存開銷
  堆排序:在快的排序算法中相對較慢

 3.6 算法例題

  有一個長度爲 n 的數組 a,裏面的元素都是整數,現有一個整數 b,寫程序判斷數組 a 中是否有兩個元素的和等於 b?

def func(lis,d):
    count = []
    for index,i in enumerate(lis):
        for j in lis[index+1:]:
            if i +j == d:
                count.append((i,j))
    return count

lis = [2,3,8,9,16,11]
ret = func(lis,11)
print(ret)
View Code

 

參考博客:https://www.jianshu.com/p/a33aa5cf7af1

 官方演示效果參考圖:猛戳此處

更多相關介紹可供參考:猛戳此處

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章