目錄
簡介
定義和特徵
定義:算法(Algorithm)是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,算法代表着用系統的方法描述解決問題的策略機制。也就是說,能夠對一定規範的輸入,在有限時間內獲得所要求的輸出。如果一個算法有缺陷,或不適合於某個問題,執行這個算法將不會解決這個問題。不同的算法可能用不同的時間、空間或效率來完成同樣的任務。一個算法的優劣可以用空間複雜度與時間複雜度來衡量。
一個算法應該具有以下五個重要的特徵:
- 有窮性:算法的有窮性是指算法必須能在執行有限個步驟之後終止;
- 確切性:算法的每一步驟必須有確切的定義;
- 輸入項:一個算法有0個或多個輸入,以刻畫運算對象的初始情況,所謂0個輸入是指算法本身定出了初始條件;
- 輸出項:一個算法有一個或多個輸出,以反映對輸入數據加工後的結果,沒有輸出的算法是毫無意義的;
- 可行性:算法中執行的任何計算步驟都是可以被分解爲基本的可執行的操作步,即每個計算步都可以在有限時間內完成(也稱之爲有效性)。
設計要求
算法設計的要求:
- 確定性: 指的是算法至少應該有輸入,輸出和加工處理無歧義性,能正確反映問題的需求,能夠得到問題的正確答案。確定性大體分爲四個層次:
- 算法程序無語法錯誤;
- 算法程序對於合法的輸入產生滿足要求的輸出;
- 對於非法輸入能夠產生滿足規格的說明;
- 算法程序對於故意刁難的測試輸入都有滿足要求的輸出結果。
- 可讀性: 程序便於閱讀,理解交流。
- 健壯性: 當輸入數據不合法時,算法也能作出相關處理,而不是產生異常,崩潰或者莫名其妙的結果。
- 時間效率高和存儲量低。
算法效率的度量方法
事後統計方法:主要是通過設計好的測試程序和數據,利用計算機計時器對不同算法編制的程序的運行時間進行比較,從而確定算法效率的高低,但這種方法有很大缺陷,一般不予採納。
事前分析估算方法:在計算機程序編制前,依據統計方法對算法進行估算。
一個用高級語言編寫的程序在計算機上運行時所消耗的時間取決於以下因素:
- 算法採用的策略,方法;(算法好壞的根本)
- 編譯產生的代碼質量;(由軟件來支持)
- 問題的輸入規模;(由數據決定)
- 機器執行指令的速度。(看硬件的性能)
算法時間複雜度
定義:在進行算法分析時,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化情況並確定T(n)的數量級。算法的時間複雜度,也就是算法的時間量度,記作:T(n) = O(f(n))。它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸近時間複雜度,簡稱爲時間複雜度。其中f(n)是問題規定n的某個函數。
根據定義,求解算法的時間複雜度的具體步驟是:
- 找出算法中的基本語句;算法中執行次數最多的那條語句就是基本語句,通常是最內層循環的循環體。
- 計算基本語句的執行次數的數量級;只需計算基本語句執行次數的數量級,這就意味着只要保證基本語句執行次數的函數中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的係數。這樣能夠簡化算法分析,並且使注意力集中在最重要的一點上:增長率。
- 用大Ο記號表示算法的時間性能。將基本語句執行次數的數量級放入大Ο記號中。
如何推導大o階呢?下面是基本的推導方法:
- 用常數1取代運行時間中的所有加法常數。
- 在修改後的運行次數函數中,只保留最髙階項。
- 如果最高階項存在且不是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).若形參爲數組,則只需要爲它分配一個存儲由實參傳送來的一個地址指針的空間,即一個機器字長空間;若形參爲引用方式,則也只需要爲其分配存儲一個地址的空間,用它來存儲對應實參變量的地址,以便由系統自動引用實參變量。
最壞情況與平均情況
我們查找一個有n 個隨機數字數組中的某個數字,最好的情況是第一個數字就是,那麼算法的時間複雜度爲O(1),但也有可能這個數字就在最後一個位置上待着,那麼算法的時間複雜度就是O(n),這是最壞的一種情況了。
最壞情況運行時間是一種保證,那就是運行時間將不會再壞了。 在應用中,這是一種最重要的需求, 通常, 除非特別指定, 我們提到的運行時間都是最壞情況的運行時間。
而平均運行時間也就是從概率的角度看, 這個數字在每一個位置的可能性是相同的,所以平均的查找時間爲n/2次後發現這個目標元素。平均運行時間是所有情況中最有意義的,因爲它是期望的運行時間。也就是說,我們運行一段程序代碼時,是希望看到平均運行時間的。可現實中,平均運行時間很難通過分析得到,一般都是通過運行一定數量的實驗數據後估算出來的。一般在沒有特殊說明的情況下,都是指最壞時間複雜度。
搜索算法
搜索是在一個數據集合中找到一個特定數據的算法,通常的答案是真的或假的。搜索的常見方法有二分查找、哈希查找等
二分法查找
二分查找又稱折半查找,優點是比較次數少,查找速度快,平均性能好。缺點是要求待查表爲有序表。因此,折半查找方法適用於不經常變動而查找頻繁的有序列表。
二分查找的工作原理
- 首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查找關鍵字比較,如果兩者相等,則查找成功。
- 否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查找關鍵字,則進一步查找前一子表,否則進一步查找後一子表。
- 重複以上過程,直到找到滿足條件的記錄,使查找成功,或直到子表不存在爲止,此時查找不成功。
二分查找的實現
def binary(l, val):
max = len(l)
min = 0
while min <= max:
mid = (max + min) // 2
if l[mid] > val:
max = mid - 1
elif l[mid] < val:
min = mid + 1
elif l[mid] == val:
print(mid)
break
else:
print('no')
def binary_rec(l, val, min, max):
mid = (max + min) // 2
if min > max:
print('no')
return 'no'
if l[mid] == val:
print(mid)
return mid
elif l[mid] < val:
return binary_rec(l, val, mid+1, max)
elif l[mid] > val:
return binary_rec(l, val, min, mid-1)
排序算法
冒泡排序(Bubble Sort)
- 時間複雜度:最優時間複雜度:O(n),最壞時間複雜度:O(n²)。
- 優點:穩定,簡單
- 缺點:效率不很高,運行時間較長
原理如下:
- 比較相鄰的元素,如果第一個比第二個大,就交換他們兩個;
- 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對。做完以後,最後的元素會是最大的數,這裏可以理解爲走了一趟;
- 針對所有的元素重複以上的步驟,除了最後一個;
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較,最後數列就是從大到小一次排列;
代碼實現
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] #交換兩個數的位置
優化版本:當某一趟走完以後發現並沒有進行數據交換,那麼此時的數列已經排列好了,沒有必要在進行下去。例如:極端情況下,數列本來已經排序好的,我們只需要走一趟即可完成排序。
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 # 返回次數的趟數
選擇排序(Selection Sort)
- 時間複雜度:最優時間複雜度:O(n²);最壞時間複雜度:O(n²)
- 穩定性:不穩定
- 優點:移動次數少
- 缺點:比較次數多
工作原理
- 每一次從待排序的列表中選出一個元素,並將其與其他數依次比較,若列表中的某個數比選中的數小,則交換位置,把所有數比較完畢,則會選出最小的數,將其放在最左邊(這一過程稱爲一趟);
- 重複以上步驟,直到全部待排序的數據元素排完;
實現
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] # 一趟走完,交換最小值的位置,第一趟最小
插入排序(Insert Sort)
- 時間複雜度:最優時間複雜度:O(n);最壞時間複雜度:O(n²)
- 穩定性:穩定
- 優點:穩定,比較快
- 缺點:比較次數不確定,數據量越大,該算法越渣
工作原理如下:
- 以從小到大排序爲例,元素0爲第一個元素,插入排序是從元素1開始,儘可能插到前面。
- 插入時分插入位置和試探位置,元素i的初始插入位置爲i,試探位置爲i-1,在插入元素i時,依次與i-1,i-2······元素比較,如果被試探位置的元素比插入元素大,那麼被試探元素後移一位,元素i插入位置前移1位,直到被試探元素小於插入元素或者插入元素位於第一位。
- 重複上述步驟,最後完成排序
實現
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 # 將當前位置數還原
快速排序(Quick Sort)
- 時間複雜度:
- 最優時間複雜度:O(nlogn)遍歷每個數是O(n),訪問每個數是O(logn),最終是O(nlogn)可以轉換爲求二叉樹深度的思想
- 最壞時間複雜度:O(n²)
- 穩定性:不穩定
- 優點:效率高,數據移動比較少,數據量越大,優勢越明顯
- 缺點:不穩定
快速排序(Quicksort),又稱劃分交換排序(partition-exchange sort)。通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序。整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
工作原理如下:
- 從數列中隨機挑選出一個數作爲基數;
- 重新排列數列,使得比基數小的元素在左邊,比基數大元素在右邊,相等的元素放左邊或者右邊都可以,最後使得該基數在處於數列中間位置,這個稱爲分區操作;
- 遞歸上述操作,完成排序
實現
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 # 返回基準數位置
歸併排序(Merge Sort)
- 最優時間複雜度:O(nlogn)最壞時間複雜度:O(nlogn)
- 穩定性:穩定
- 優點:穩定,數據量越大越優秀
- 缺點:需要額外空間
歸併排序(MergeSort)是採用分治法的一個非常典型的應用。歸併排序的思想就是先遞歸分解數組,再合併數組。
歸併排序是一種穩定的排序方法。和選擇排序一樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因爲始終都是O(nlogn)的時間複雜度。代價是需要額外的內存空間。
工作原理如下:
- 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;
- 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置;
- 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;
- 重複步驟3直到某一指針達到序列尾;
- 將另一序列剩下的所有元素直接複製到合併序列尾
實現
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) # 合併
堆排序Heap Sort)
- 時間複雜度:最優時間複雜度:O(nlogn),最壞時間複雜度:O(nlogn)
- 穩定性:不穩定
本質是一個完全二叉樹,如果根節點的值是所有節點的最小值稱爲小根堆,如果根節點的值是所有節點的最大值,稱爲大根堆。
原理:
- 將待排序數據列表建立成堆結構(建立堆);
- 通過上浮(shift_up)或下沉(shift_down)等操作得到堆頂元素爲最大元素(已大根堆爲例);
- 去掉堆頂元素,將最後的一個元素放到堆頂,重新調整堆,再次使得堆頂元素爲最大元素(相比第一次爲第二大元素);
- 重複3操作,直到堆爲空,最後完成排序;
# -*- coding:utf8 -*-
import random
def sift(li, low, high):
# li表示樹, low表示樹根, high表示樹最後一個節點的位置
tmp = li[low]
i = low
j = 2 * i + 1 # 初識j指向空位的左孩子
# i指向空位,j指向兩個孩子
while j <= high: # 循環退出的第二種情況: j>high,說明空位i是葉子節點
if j + 1 <= high and li[j] < li[j+1]: #如果右孩子存在並且比左孩子大,指向右孩子
j += 1
if li[j] > tmp:
li[i] = li[j]
i = j
j = 2 * i + 1
else: # 循環退出的第一種情況:j位置的值比tmp小,說明兩個孩子都比tmp小
break
li[i] = tmp
def heap_sort(li):
n = len(li)
# 1. 構造堆
for low in range(n//2-1, -1, -1):
sift(li, low, n-1)
# 2. 挨個出數
for high in range(n-1, -1, -1):
li[0], li[high] = li[high], li[0] # 退休 棋子
sift(li, 0, high-1)
import copy
li = list(range(1000))
random.shuffle(li)
heap_sort(li)
print li
希爾排序(Shell Sort)
- 最優時間複雜度:根據步長序列的不同而不同,最優是1.3,根據數學運算算出的gap
- 最壞時間複雜度:O(n²)
- 穩定性:不穩定
- 優點:平均時間短,數據移動少
- 缺點:不穩定
希爾排序(Shell Sort)是插入排序的一種,也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法,是DL.Shell於1959年提出的。
工作原理如下:
- 先取一個小於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)): # 在插入排序中 d1 爲 1
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 # 繼續分割
計數排序(count_sort)
- 時間複雜度:最優時間複雜度:O(n),最壞時間複雜度:O(n)
- 穩定性:穩定
- 優點:速度快
- 缺點:有侷限性,範圍不能太大,只能是整數
計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。
計數排序是一個穩定的排序算法。當輸入的元素是 n 個 0到 k 之間的整數時,時間複雜度是O(n+k),空間複雜度也是O(n+k),其排序速度快於任何比較排序算法。當k不是很大並且序列比較集中時,計數排序是一個很有效的排序算法。
算法描述
- 找出待排序的數組中最大和最小的元素;
- 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項;
- 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
- 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。
def count_sort(li, max_num):
count = [0 for i in range(max_num + 1)] #設置計數序列並初始化爲0
for num in li:
count[num] += 1
i = 0
for num,m in enumerate(count):
for j in range(m):
li[i] = num
i += 1
桶排序(Bucket Sort)
- 時間複雜度:最優時間複雜度:O(n),最壞時間複雜度:O(n)
- 穩定性:穩定
- 缺點:如果數據分佈集中,如只分布在一個桶,效果不好。需要額外空間
桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。桶排序 (Bucket sort)的工作的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排)。
桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,因爲其它部分的時間複雜度都爲O(n)。很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
算法描述
- 設置一個定量的數組當作空桶;
- 遍歷輸入數據,並且把數據一個一個放到對應的桶裏去;
- 對每個不是空的桶進行排序;
- 從不是空的桶裏把排好序的數據拼接起來。
def bucket_sort(a):
buckets = [0] * ((max(a)-min(a))+1) #初始化桶元素爲0
for i in range(len(a)):
buckets[a[i] - min(a)] +=1 #遍歷數組a,在桶的相應位置累加值
b = []
for i in range(len(buckets)):
if bucket[i] != 0:
b += [i + min(a)] * buckets[i]
return b
基數排序(Radix Sort)
- 時間複雜度:最優時間複雜度:O(n),最壞時間複雜度:O(n)
- 穩定性:穩定
- 缺點:只能處理正整數。需要額外空間
基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。
基數排序基於分別排序,分別收集,所以是穩定的。但基數排序的性能比桶排序要略差,每一次關鍵字的桶分配都需要O(n)的時間複雜度,而且分配之後得到新的關鍵字序列又需要O(n)的時間複雜度。假如待排數據可以分爲d個關鍵字,則基數排序的時間複雜度將是O(d*2n) ,當然d要遠遠小於n,因此基本上還是線性級別的。
基數排序的空間複雜度爲O(n+k),其中k爲桶的數量。一般來說n>>k,因此額外空間需要大概n個左右。
算法描述
- 取得數組中的最大數,並取得位數;
- arr爲原始數組,從最低位開始取每個位組成radix數組;
- 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
def radix_sort(li):
max_num = max(li)
i = 0
while (10 ** i <= max_num):
buckets = [[] for _ in range(10)]
for val in li:
digit = val // (10 ** i) % 10
buckets[digit].append(val)
li.clear()
for bucket in buckets:
for val in bucket:
li.append(val)
i += 1
總結
https://www.cnblogs.com/onepixel/articles/7674659.html
https://www.cnblogs.com/wdliu/p/9481122.html
https://blog.csdn.net/danieljackz/article/details/90288964