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)
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)
參考博客:https://www.jianshu.com/p/a33aa5cf7af1
官方演示效果參考圖:猛戳此處
更多相關介紹可供參考:猛戳此處