一、簡介
定義和特征
定義:算法(Algorithm)是指解題方案的准確而完整的描述,是一系列解決問題的清晰指令,算法代表着用系統的方法描述解決問題的策略機制。也就是說,能夠對一定規范的輸入,在有限時間內獲得所要求的輸出。如果一個算法有缺陷,或不適合於某個問題,執行這個算法將不會解決這個問題。不同的算法可能用不同的時間、空間或效率來完成同樣的任務。一個算法的優劣可以用空間復雜度與時間復雜度來衡量。
一個算法應該具有以下五個重要的特征:
- 有窮性:算法的有窮性是指算法必須能在執行有限個步驟之后終止;
- 確切性:算法的每一步驟必須有確切的定義;
- 輸入項:一個算法有0個或多個輸入,以刻畫運算對象的初始情況,所謂0個輸入是指算法本身定出了初始條件;
- 輸出項:一個算法有一個或多個輸出,以反映對輸入數據加工后的結果,沒有輸出的算法是毫無意義的;
- 可行性:算法中執行的任何計算步驟都是可以被分解為基本的可執行的操作步,即每個計算步都可以在有限時間內完成(也稱之為有效性)。
設計要求
算法設計的要求:
- 確定性: 指的是算法至少應該有輸入,輸出和加工處理無歧義性,能正確反映問題的需求,能夠得到問題的正確答案。確定性大體分為四個層次:
1.算法程序無語法錯誤;
2.算法程序對於合法的輸入產生滿足要求的輸出;
3.對於非法輸入能夠產生滿足規格的說明;
4.算法程序對於故意刁難的測試輸入都有滿足要求的輸出結果。
- 可讀性: 程序便於閱讀,理解交流。
- 健壯性: 當輸入數據不合法時,算法也能作出相關處理,而不是產生異常,崩潰或者莫名其妙的結果。
- 時間效率高和存儲量低。
算法效率的度量方法
事后統計方法:主要是通過設計好的測試程序和數據,利用計算機計時器對不同算法編制的程序的運行時間進行比較,從而確定算法效率的高低,但這種方法有很大缺陷,一般不予采納。
事前分析估算方法:在計算機程序編制前,依據統計方法對算法進行估算。
一個用高級語言編寫的程序在計算機上運行時所消耗的時間取決於以下因素:
- 算法采用的策略,方法;(算法好壞的根本)
- 編譯產生的代碼質量;(由軟件來支持)
- 問題的輸入規模;(由數據決定)
- 機器執行指令的速度。(看硬件的性能)
算法時間復雜度
定義:在進行算法分析時,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化情況並確定T(n)的數量級。算法的時間復雜度,也就是算法的時間量度,記作:T(n}=0(f(n))。它表示隨問題規模n的增大,算法執行時間的埔長率和 f(n)的埔長率相同,稱作算法的漸近時間復雜度,簡稱為時間復雜度。其中f( n)是問題規橫n的某個函數。
根據定義,求解算法的時間復雜度的具體步驟是:
⑴ 找出算法中的基本語句;
算法中執行次數最多的那條語句就是基本語句,通常是最內層循環的循環體。
⑵ 計算基本語句的執行次數的數量級;
只需計算基本語句執行次數的數量級,這就意味着只要保證基本語句執行次數的函數中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的系數。這樣能夠簡化算法分析,並且使注意力集中在最重要的一點上:增長率。
⑶ 用大Ο記號表示算法的時間性能。
將基本語句執行次數的數量級放入大Ο記號中。
如何推導大o階呢?下面是基本的推導方法:
1.用常數1取代運行時間中的所有加法常數。
2.在修改后的運行次數函數中,只保留最髙階項。
3.如果最高階項存在且不是1,則去除與這個項相乘的常數。
簡單的說,就是保留求出次數的最高次冪,並且把系數去掉。 如T(n)=n2+n+1 =O(n2)
一些例子
######復雜度O(1) print("this is wd") ######復雜度O(n) for i in range(n): print(i) ######復雜度O(n2) for i in range(n): for j in range(n): print(j) ######復雜度O(n3) for i in range(n): for j in range(n): for k in range(n): print('wd') ######復雜度O(log2n) while n > 1: print(n) n = n // 2
常見的復雜度按效率排序:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2nlogn)<O(n2)
空間復雜度
空間復雜度(Space Complexity)是對一個算法在運行過程中臨時占用存儲空間大小的量度。一個算法在計算機存儲器上所占用的存儲空間,包括存儲算法本身所占用的存儲空間,算法的輸入輸出數據所占用的存儲空間和算法在運行過程中臨時占用的存儲空間這三個方面。算法的輸入輸出數據所占用的存儲空間是由要解決的問題決定的,是通過參數表由調用函數傳遞而來的,它不隨本算法的不同而改變。存儲算法本身所占用的存儲空間與算法書寫的長短成正比,要壓縮這方面的存儲空間,就必須編寫出較短的算法。算法在運行過程中臨時占用的存儲空間隨算法的不同而異,有的算法只需要占用少量的臨時工作單元,而且不隨問題規模的大小而改變,這種算法是節省存儲的算法;有的算法需要占用的臨時工作單元數與解決問題的規模n有關,它隨着n的增大而增大,當n較大時,將占用較多的存儲單元。
如當一個算法的空間復雜度為一個常量,即不隨被處理數據量n的大小而改變時,可表示為O(1);當一個算法的空間復雜度與以2為底的n的對數成正比時,可表示為0(log2n);當一個算法的空間復雜度與n成線性比例關系時,可表示為0(n).若形參為數組,則只需要為它分配一個存儲由實參傳送來的一個地址指針的空間,即一個機器字長空間;若形參為引用方式,則也只需要為其分配存儲一個地址的空間,用它來存儲對應實參變量的地址,以便由系統自動引用實參變量。
二、python中的常見算法
冒泡排序
效率:O(n2)
原理:
-
比較相鄰的元素,如果第一個比第二個大,就交換他們兩個;
-
對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最后一對。做完以后,最后的元素會是最大的數,這里可以理解為走了一趟;
-
針對所有的元素重復以上的步驟,除了最后一個;
-
持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較,最后數列就是從大到小一次排列;
demo:
def bubble_sort(data): """ 冒泡排序 :param data: :return: """ for i in range(len(data)-1): # 趟數 for j in range(len(data)-i-1): # 遍歷數據,依次交換 if data[j]>data[j+1]: # 當較大數在前面 data[j],data[j+1]=data[j+1],data[j] #交換兩個數的位置 if __name__=='__main__': import random data_list=list(range(30)) random.shuffle(data_list) print("pre:",data_list) bubble_sort(data_list) print("after:",data_list) #結果: #pre: [22, 11, 19, 16, 12, 18, 20, 28, 27, 4, 21, 10, 9, 7, 1, 6, 5, 29, 8, 0, 17, 26, 13, 14, 15, 24, 25, 23, 3, 2] #after: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
優化版本:當某一趟走完以后發現並沒有進行數據交換,那么此時的數列已經排列好了,沒有必要在進行下去。例如:極端情況下,數列本來已經排序好的,我們只需要走一趟即可完成排序。
def bubble_sort(data): """ 冒泡排序優化版 :param data: :return: """ for i in range(len(data)-1): # 趟數 exchange=False # 交換標志 for j in range(len(data)-i-1): # 遍歷數據,依次交換 if data[j]>data[j+1]: # 當較大數在前面 data[j],data[j+1]=data[j+1],data[j] # 交換兩個數的位置 exchange = True # 改變標志 if not exchange: # 如果某一趟沒有進行交換,代表排序完成 break return i # 返回次數的趟數 if __name__=='__main__': data_list=list(range(30)) print("pre:",data_list) num =bubble_sort(data_list) print("after:",data_list,'趟數:',num+1) #結果: #pre: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] #after: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] 趟數: 1
選擇排序
效率:O(n2)
原理:
- 每一次從待排序的列表中選出一個元素,並將其與其他數依次比較,若列表中的某個數比選中的數小,則交換位置,把所有數比較完畢,則會選出最小的數,將其放在最左邊(這一過程稱為一趟);
- 重復以上步驟,直到全部待排序的數據元素排完;
demo:
def select_sort(data): """ 選擇排序 :param data: 待排序的數據列表 :return: """ for i in range(len(data)-1): #趟數 min_index=i # 記錄i趟開始最小的數的索引,我們從最左邊開始 for j in range(i+1,len(data)): # 每一次趟需要循環的次數 if data[j] < data[min_index]: # 當數列中的某一個數比開始的數要小時候,更新最小值索引位置 min_index=j data[i],data[min_index]=data[min_index],data[i] # 一趟走完,交換最小值的位置,第一趟最小 if __name__=='__main__': import random data_list=list(range(30)) random.shuffle(data_list) # 打亂列表數據 print("pre:",data_list) select_sort(data_list) print("after:",data_list) #結果: #pre: [20, 11, 22, 0, 18, 21, 14, 19, 7, 23, 27, 29, 24, 4, 17, 15, 5, 10, 26, 13, 25, 1, 8, 16, 3, 9, 2, 28, 12, 6] #after: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
插入排序
效率:O(n2)
原理:
- 以從小到大排序為例,元素0為第一個元素,插入排序是從元素1開始,盡可能插到前面。
- 插入時分插入位置和試探位置,元素i的初始插入位置為i,試探位置為i-1,在插入元素i時,依次與i-1,i-2······元素比較,如果被試探位置的元素比插入元素大,那么被試探元素后移一位,元素i插入位置前移1位,直到被試探元素小於插入元素或者插入元素位於第一位。
- 重復上述步驟,最后完成排序
demo:
def insert_sort(data): """ 插入排序 :param data: 待排序的數據列表 :return: """ for i in range(1, len(data)): # 無序區域數據 tmp = data[i] # 第i次插入的基准數 for j in range(i, -1, -1): if tmp < data[j - 1]: # j為當前位置,試探j-1位置 data[j] = data[j - 1] # 移動當前位置 else: # 位置確定為j break data[j] = tmp # 將當前位置數還原 if __name__=='__main__': import random data_list=list(range(30)) random.shuffle(data_list) # 打亂列表數據 print("pre:",data_list) insert_sort(data_list) print("after:",data_list) #結果: #pre: [7, 17, 10, 16, 23, 24, 13, 11, 2, 5, 15, 29, 27, 18, 4, 19, 1, 9, 3, 21, 0, 14, 12, 25, 22, 28, 20, 6, 26, 8] #after: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
快速排序
效率:平均O(nlogn)
原理:
- 從數列中隨機挑選出一個數作為基數;
- 重新排列數列,使得比基數小的元素在左邊,比基數大元素在右邊,相等的元素放左邊或者右邊都可以,最后使得該基數在處於數列中間位置,這個稱為分區操作;
- 遞歸上述操作,完成排序,如下如;

demo:
#!/usr/bin/env python3 #_*_ coding:utf-8 _*_ #Author:wd def quick_sort(data,left,right): """ 快速排序 :param data: 待排序的數據列表 :param left: 基准數左邊元素的索引 :param right: 基准數右邊元素的索引 :return: """ if left < right: mid = partition(data,left,right) # 分區操作,mid代表基數所在的索引 quick_sort(data,left,mid-1) # 對基准數前面進行排序 quick_sort(data,mid+1,right) # 對基准數后面進行排序 def partition(data,left,right): tmp=data[left] # 隨機選擇的基准數,從最左邊開始選 while left < right: while left < right and data[right] >= tmp: # 右邊的數比基准數大 right-=1 # 保留該數,然后索引指針往左移動 data[left]=data[right] # 否則此時右邊數比基數小,則將該數放到基准位置 while left < right and data[left] <= tmp: # 右邊的數比基准數小 left+=1 # 此時保持該數位置不動,索引指針往前移動 data[right]=data[left] # 否則此時左邊的數比基數大,則將該數放到右邊 data[left] = tmp # 最后將基准數量放回中間 return left # 返回基准數位置 if __name__=='__main__': data_list=[1,3,21,6,50,33,34,58,66] quick_sort(data_list,0,len(data_list)-1) print(data_list) ###結果:[1, 3, 6, 21, 33, 34, 50, 58, 66]
堆排序
堆定義:本質是一個完全二叉樹,如果根節點的值是所有節點的最小值稱為小根堆,如果根節點的值是所有節點的最大值,稱為大根堆。
效率:O(nlogn)
原理:
- 將待排序數據列表建立成堆結構(建立堆);
- 通過上浮(shift_up)或下沉(shift_down)等操作得到堆頂元素為最大元素(已大根堆為例);
- 去掉堆頂元素,將最后的一個元素放到堆頂,重新調整堆,再次使得堆頂元素為最大元素(相比第一次為第二大元素);
- 重復3操作,直到堆為空,最后完成排序;

demo:
def sift(data, low, high): """ 調整堆函數 :param data: 帶排序的數據列表 :param low: 值較小的節點的位置,可以理解為是根節點 :param high:值較大的節點的位置 :return: """ i = low j = 2 * i # 父節點i所對應的左孩子 tmp = data[i] # 最較小節點的值 while j <= high: if j < high and data[j] < data[j + 1]: # 如果右孩子比左孩子大則把j指向右節點 j += 1 # 指向右節點 if tmp < data[j]: # 如果此時位置較小的節點值比該節點值小,則將該節點上浮最為新的父節點,並調整該節點雙親 data[i] = data[j] i = j # 調整該節點的雙親的位置 j = 2 * i else: break # 否則代表本次調整已經完成,並且節點i已經無值 data[i] = tmp # 最后將被調整節點的值放到i節點上(空出的位置) def heap_sort(data): """ 堆排序 :param data: 待排序的數據列表 :return: """ n = len(data) for i in range(n // 2 - 1, -1, -1): sift(data, i, n - 1) # 構建堆 for i in range(n - 1, -1, -1): # 調整過程,從最后一個元素開始交換 data[0], data[i] = data[i], data[0] # 交換 sift(data, 0, i - 1) # 開始調整 if __name__ == '__main__': import random data_list = [1, 3, 21, 6, 50, 33, 34, 58, 66] random.shuffle(data_list) # 打亂列表數據 print("pre:", data_list) heap_sort(data_list) print("after:", data_list) #結果: #pre: [66, 3, 58, 34, 1, 33, 21, 6, 50] #after: [1, 3, 6, 21, 33, 34, 50, 58, 66]
歸並排序
效率:O(nlogn)
空間復雜度:O(n)
原理:
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並后的序列;
- 設定兩個指針,最初位置分別為兩個已經排序序列的起始位置;
- 比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置;
- 重復步驟3直到某一指針達到序列尾;
- 將另一序列剩下的所有元素直接復制到合並序列尾。

demo:
def merge(data, low, mid, high): """ 合並函數 :param data: 數據列表 :param low: 列表開頭位置 :param mid: 分割中間位置 :param high: 列表最后位置 :return: """ i = low # 第一個指針 j = mid + 1 # 第二個指針 tmp = [] # 臨時存放的列表 while i <= mid and j <= high: # 分割的列表當兩邊都有數才進行 if data[i] < data[j]: tmp.append(data[i]) i += 1 # 低的指針往右移動 else: tmp.append(data[j]) # 右邊大,存右邊的數 j += 1 # 同時指針右移動 while i <= mid: # 左邊分割有剩下 tmp.append(data[i]) i += 1 while j <= high: # 右邊有剩下 tmp.append(data[j]) j += 1 data[low:high + 1] = tmp # 最后將tmp中的數寫入到原來的列表中 def merge_sort(data, low, high): """ 歸並排序 :param data: 待排序的數據列表 :param low: 數據列表開始位置 :param high: 數據列表結束位置 :return: """ if low < high: # 至少有兩個元素才進行 mid = (low + high) // 2 # 分割 merge_sort(data, low, mid) # 遞歸分割上一部分 merge_sort(data, mid + 1, high) # 遞歸分割下一部分 merge(data, low, mid, high) # 合並 if __name__ == '__main__': import random data_list = [1, 3, 21, 6, 50, 33, 34, 58, 66] random.shuffle(data_list) # 打亂列表數據 print("pre:", data_list) merge_sort(data_list, 0, len(data_list) - 1) print("after:", data_list) #結果: #pre: [21, 3, 33, 58, 34, 66, 1, 6, 50] #after: [1, 3, 6, 21, 33, 34, 50, 58, 66]
希爾排序
效率:與增量有關,O(n1+£)其中<0£<1,如增量為2k-1 復雜度為O(n3/2)
原理:
- 先取一個小於n的整數d1作為第一個增量,把文件的全部記錄分組。所有距離為d1的倍數的記錄放在同一個組中。
- 先在各組內進行直接插入排序;
- 取第二個增量d2<d1重復上述的分組和排序,直至所取的增量 =1( < …<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。
def shell_sort(data): """ 希爾排序 :param data:待排序的數據列表 :return: """ d1 = len(data) // 2 # 設置分割大小為d1, while d1 > 0: for i in range(d1, len(data)): tmp = data[i] # 當前分割元素位置 j = i - d1 # 上一個分割元素位置 while j >= 0 and tmp < data[j]: # 上一個元素分割位置比當前分割位置要大,則需要調整位置 data[j + d1] = data[j] # 后移動當前分割元素位置 j -= d1 # 往前移d1 data[j + d1] = tmp d1 //= 2 # 繼續分割 if __name__ == '__main__': import random data_list = [1, 3, 21, 6, 50, 33, 34, 58, 66] random.shuffle(data_list) # 打亂列表數據 print("pre:", data_list) shell_sort(data_list) print("after:", data_list) #結果: #pre: [3, 66, 58, 34, 33, 50, 6, 21, 1] #after: [1, 3, 6, 21, 33, 34, 50, 58, 66]
