Python 搜尋、排序、複雜度分析
概述
演算法是計算機程式的一個基本的構建模組。評價演算法質量的最基本的標準是正確性,另一個重要的標準是執行時間效能。當在一臺真實、資源有限的計算機上執行一個演算法的時候,經濟性的考慮就有了用武之地,這樣一個過程會消耗兩種資源:處理時間和空間或記憶體。
統計指令
用於估算演算法效能的另一種技術是統計對不同的問題規模所要執行的指令的數目。不管演算法在什麼平臺上執行,這個統計數字對於演算法所要執行的抽象的工作量給出了一個很好的預計。然而要記住,當統計指令的時候,所統計的是用於編寫演算法的較高階程式碼中的指令數目,而不是執行機器語言的程式中的指令數目。
當以這種方式分析演算法的時候,你需要區分兩種指令:
(1)不管問題規模多大,都執行相同次數的指令
(2)根據問題的規模,執行不同次數的指令
現在,我們先忽略第一類指令,因為它們的影響並不顯著,第二類指令通常在迴圈或遞迴函式中可以找到。
複雜度分析
複雜度的階
大O表示法
一個演算法幾乎不太可能只是嚴格執行等於n、n^2或k^n的那麼多次的操作,演算法通常在迴圈體內、迴圈體之前或之後還要執行其他的工作。例如,我們說演算法執行2n+3或2n^2操作,可能會更加精確。
一個演算法的總工作量,通常是多項式中的數項之和,當用多項式來表示工作量的時候,有一個項是主項,隨著n變得越來越大,主項也變得很大,以至於你可以忽略其他的項所表示的工作量。例如,在多項式(1/2)n^2-(1/2)n中,我們主要關注平方項(1/2)n^2,實際上忽略掉線性項(1/2)n,還可以刪除掉係數1/2,因為(1/2)n^2和n^2之間的差別不會隨著n的增加而改變。這種型別的分析有時候叫做漸進分析
搜尋演算法
搜尋最小值
Python的min函式返回列表中的最小的項。為了研究這個演算法的複雜度,我們開發了一個替代的版本,它返回了最小項的索引。這個演算法假設列表不為空,並且其中的項的順序是任意的,該演算法首先將列表中的第1個位置當作最小項,然後,向右搜尋以找到一個更小的項,如果找到了,將最小項的位置重新設定為當前位置,當這個演算法到達列表末尾的時候,它就返回最小項的位置。
class indexOfMin(): def indexOfMin(self,lyst): """Return the index of the minmum item.""" minIndex = 0 currentIndex = 1 while currentIndex < len(lyst): if lyst[currentIndex] < lyst[minIndex]: minIndex = currentIndex currentIndex += 1 return minIndex if __name__ == "__main__": a = indexOfMin() lyst = [3,5,7,1,9,10] print(a.indexOfMin(lyst))
對於大小為n的列表,該演算法必須進行n-1次比較,因此,演算法的複雜度為O(n)。
順序搜尋一個列表
Python的in運算子作為list類中名為__contains__的一個方法而實現。該方法在列表(任意排列的項)中搜索一個特定的項(叫做目標項)。在這樣的一個列表中搜索一個目標項的唯一的方法是,從第1個位置的項開始,將其與目標項進行比較,如果這兩個項相等,該方法返回一個True。否則,該方法移動到下一個位置並且將其項與目標項進行比較。如果該方法到達了最後一個位置,卻仍然沒有找到目標項,它就返回False。這種搜尋叫做順序搜尋(sequential search)或線性搜尋(linear search)。一個更為有用的順序搜尋函式,應該返回它所找到的目標項的索引,或者如果沒有找到目標項的話,返回-1。
class Search():
def sequentialSearch(self,target,lyst):
"""Returns the position of the target item if found, or -1 otherwise."""
position = 0
while position < len(lyst):
if target == lyst[position]:
return position
position += 1
return -1
if __name__ == '__main__':
a = Search()
target = 3
lyst = [2,4,5,1,3,7]
print(a.sequentialSearch(target,lyst))
最好情況、最壞情況和平均情況的效能
有些演算法的效能取決於所處資料的放置方式。順序搜尋演算法在查詢一個目標項的時候,在列表的開頭處所做的工作比在列表的末尾所做的工作要少。對於這樣的演算法,我們可以確定其最好情況的效能、最壞情況的效能和平均情況的效能。通常一般考慮的是最壞情況的效能和平均情況的效能。
順序搜尋的分析要考慮如下3種情況:
(1)在最壞情況下,目標項位於列表的末尾,或者根本就不在列表之中。那麼,演算法必須訪問每一個項,並且對大小為n的列表要執行n次迭代。因此,順序搜尋的最壞情況的複雜度為O(n)。
(2)在最好的情況下,演算法只進行了1次迭代就在第1個位置找到目標項,複雜度為O(1)。
(3)要確定平均情況,把在每一個可能的位置找到目標項所需的迭代次數相加,並且總和除以n。因此,演算法執行了(n+1)/2次迭代,對於很大的n,常數因子2的作用並不大,因此,平均情況下的複雜度仍然為O(n)。
顯然,順序搜尋的最好情況的效能是很少見的,而平均情況和最壞情況的效能則基本相同。
有序列表的二叉搜尋
對於那些沒有按照特定的順序排列的資料,順序搜尋是必要的。當搜尋經過排序的資料的時候,可以使用二叉搜尋(又稱為二分搜尋)。
現在來考慮Python實現二叉搜尋的一個示例。首先,假設列表中的項都是按照升序排列的,搜尋演算法直接找到列表的中間位置,並且將該位置的項和目標項進行比較。如果它們是一致的,演算法返回該位置。否則,如果目標項小於當前項,演算法搜尋列表中間位置以前的部分。反之,搜尋中間以後的部分。
def binarySearch(target, lyst, profiler):
"""Returns the position of the target item if found,
or -1 otherwise."""
left = 0
right = len(lyst) - 1
while left <= right:
profiler.comparison()
midpoint = (left + right) // 2
if target == lyst[midpoint]:
return midpoint
elif target < lyst[midpoint]:
right = midpoint - 1
else:
left = midpoint + 1
return -1
在最壞的情況下,迴圈要執行列表的大小除以2直到得到的商為1的次數,對於大小為n的列表,實際上執行了n/2/2.../2的連續除法,直到結果為1。假設k是用n除以2的次數,要求解k,讓n/(2^k)=1就行了,那麼n=2^k,k=log2n,因此,二叉搜尋的最壞情況的複雜度為O(log2n)。
比較資料項
二叉搜尋和搜尋最小項,都是假設列表中的項是可以相互比較的。在Python中,這意味著這些項具有相同的型別,並且它們都識別比較運算子==、<和>。幾個內建的Python型別的物件,例如數字、字串和列表,都可以使用這些運算子來比較。
為了允許演算法對一個新物件的類使用比較運算子==、<和>,程式設計師應該在該類中定義__eq__、__lt__和__gt__方法。__lt__方法的方法頭如下所示:
def __lt__(self,other):
如果self小於other,該方法返回True,否則,它返回False。比較物件的標準,取決於它們內部的結構,以及應該以何種方式對其排序。
例如,SavingAccount物件可能包含3個數據欄位,一個用於名稱,一個用於PIN,還有一個用餘額。假設賬戶應該按照名稱的字母順序來排序,那麼,需要對__lt__方法按照如下的方式來實現:
class SavingAccount(object):
"""This class represents a saving account with the owner's name, PIN, and balance."""
def __init__(self,name,pin,balance = 0.0):
self._name = name
self._pin = pin
self._balance = balance
def __lt__(self,other):
return self._name < other._name
注意:__lt__方法對兩個賬戶物件的_name欄位呼叫了<運算子(名稱是字串),並且字串型別已經包含了一個__lt__方法。當對字串應用<運算子的時候,Python自動執行__lt__方法。就像是在呼叫str函式時執行str方法一樣。
基本排序演算法
這裡介紹的幾個演算法很容易編寫,但是效率不高,下一節介紹的演算法都很難編寫,但是更為高效(這是一種常見的取捨)。每個Python排序函式都是在整數的一個列表上進行操作的,並且都會使用一個swap函式來交換列表中的兩項的位置。
swap函式的程式碼如下:
def swap(lyst,i,j):
"""Exchanges the items at position i and j."""
temp = lyst[i]
lyst[i] = lyst[j]
lyst[j] = temp
選擇排序
最簡單的策略就是搜尋整個列表,找到最小項的位置。如果該位置不是列表的第1個位置,演算法就會交換在這兩個位置的項,然後,演算法回到第2個位置並且重複這個過程,如果必要的話,將最小項和第2個位置的項進行交換。當演算法到達整個過程的最後一個位置,列表就是排序好的了。這個演算法叫做選擇排序(selection sort)。
def selectionSort(lyst):
i = 0
while i < len(lyst) - 1:
minIndex = 1
j = i + 1
while j < len(lyst):
if lyst[j] < lyst[minIndex]:
minIndex = j
j += 1
if minIndex != i:
swap(lyst,minIndex,i)
i += 1
這個函式包含了一個巢狀的迴圈。總的比較次數為(1/2)(n^2)-(1/2)n,對於較大的n,我們可以選擇影響很大的項,而忽略常數項。因此,選擇排序在各種情況下的複雜度為O(n^2)。
氣泡排序
另一種排序方法相對容易理解和編碼,它叫做氣泡排序(bubble sort)。其策略是從列表的開頭處開始,並且比較一對資料項,直到移動到列表的末尾,每當成對的兩項之間的順序不正確的時候,演算法就交換其位置。這個過程的效果就是將最大的項以冒泡的方法排到列表的末尾。然後,演算法從列表開頭到倒數第2個列表項重複這一個過程。依次類推,直到該演算法從列表的最後一項開始執行。此時,列表是已經排序好的。
def bubbleSort(lyst):
n = len(lyst)
while n > 1:
i = 1
while i < n:
if lyst[i] < lyst[i-1]:
swap(lyst,i,i-1)
i += 1
n -= 1
和選擇排序一樣,氣泡排序也有一個巢狀的迴圈,對於大小為n的列表,內部的迴圈執行(1/2)(n^2)-(1/2)n次,因此,氣泡排序的複雜度是O(n^2)。
插入排序
(1)在第i輪通過列表的時候(其中i的範圍從1到n-1),第i個項應該插入到列表的前i個項之中的正確位置。
(2)在第i輪之後,前i個項應該是排好序的。
(3)這個過程類似於人們排列手中的撲克牌的順序,也就是說,如果你按照順序放好了前i-1張牌,抓取了第i張牌,並且將其與手中的這些牌進行比較,直到找到其合適的位置。
(4)插入排序包括兩個迴圈。外圍的迴圈遍歷從1到n-1的位置。對於這個迴圈中的每一個位置i,我們都儲存該項並且從位置i-1開始內部迴圈。
def insertSort(lyst):
i = 1
while i < len(lyst):
itemInsert = lyst[i]
j = i - 1
while j >= 1:
if itemToInsert < lyst[j]:
lyst[j + 1] = lyst[j]
j -= 1
else:
break
lyst[j + 1] = itemToInsert
i += 1
插入排序的最壞情況下的複雜度是O(n^2)。列表中排好序的項越多,插入排序的效果越好。在最好情況下,列表本身是有序的,那麼插入排序的複雜度是線性的。然而,在平均情況下,插入排序的複雜度仍然是二次方階的。
更快的排序
到目前為止,我們介紹的3種排序演算法都擁有O(n^2)的執行時間。然而,我們可以利用一些複雜度為O(nlogn)的更好的演算法。這些更好的演算法都採用了分而治之(divide-and-conquer)的策略,也就是說,每個演算法都找到了一種方法,將列表分解為更小的子列表,隨後,這些子列表再遞迴的排序。理想情況下,如果這些子列表的複雜度為log(n),而重新排列每一個子列表中的資料所需的工作量為n,那麼,這樣的排序演算法總的複雜度就是O(nlogn)。
快速排序
快速排序所使用的策略可以概括如下:
(1)首先,從列表的中點位置選取一項。這一項叫作基準點。
(2)將列表中的項分割槽,以便小於基準點的所有項都移動到基準點的左邊,而剩下的項都移動到基準點的右邊。根據相關的實際項,基準點自身的最終位置也是變化的。但是,不管基準點最終位於何處,這個位置都是它在完全排序的列表中的最終位置。
(3)分而治之。對於在基準點分割列表而形成的子列表,遞迴地重複應用該過程。一個子列表包含了基準點左邊的所有的項(現在是較小的項),另一個子列表包含了基準點右邊的所有的項(現在是較大的項)。
(4)每次遇到小於2個項的一個子列表,就結束這個過程。
1.分割
從程式設計師的角度來看,該演算法最複雜的部分就是對子列表中的項進行分割的操作。步驟如下:
(1)將基準點和子列表中的最後一項交換。
(2)在已知小於基準點的項和剩餘的項之間建立一個邊界。一開始,這個邊界就放在第一項之前。
(3)從子列表的第一項開始掃描整個子列表,每次遇到小於基準點的項,就將其與邊界之後的第一項進行交換,並且邊界向後移動。
(4)將基準點和邊界之後的第一項進行交換,從而完成這個過程。
例子:
2.快速排序的複雜度分析
在第1次分割操作中,從列表頭部到其尾部掃描了所有的項。因此,這個操作過程的工作量和列表的長度n成正比。
在這次分割之後的工作量,和左邊的子列表的長度加上右邊的子列表的長度(加在一起是n-1)成正比。當再次分割這些子列表的時候,有了4個子列表,它們組合在一起的長度近似為n,因此,組合的工作量還是和n成正比的。隨著將列表分割為更多的子列表,總的工作量還是和n成正比。
然後我們需要確定對列表分割多少次。如果每一次新的子列表之間的分割線都儘可能地靠近當前子列表的中央,大概經過log2n步之後會得到一個單個的元素。因此,這個演算法在最好的情況下的效能為O(nlog2n)。
對於最壞的情況,考慮一個列表已經排好序的情況。如果所選擇的基準點元素是第1個元素,那麼在第1次分割的時候,基準點的右邊有n-1個元素,在第2次分割的時候,基準點的右邊有n-2個元素,依次類推,因此,在最壞的情況下,快速排序演算法的效能是O(n^2)。
如果將快速排序實現為一個遞迴演算法,你的分析還必須考慮到呼叫棧所使用的記憶體。每一次遞迴呼叫都需要一個固定大小的記憶體用於棧,並且每一次分割之後有兩次遞迴呼叫。因此,在最好的情況下,記憶體使用是O(log2n),在最壞的情況下,記憶體使用是O(n)。
3.快速排序的實現
快速排序使用遞迴演算法更容易編碼。
def quicksort(lyst):
quicksortHelper(lyst, 0, len(lyst) - 1)
def quicksortHelper(lyst, left, right):
if left < right:
pivotLocation = partition(lyst, left, right)
quicksortHelper(lyst, left, pivotLocation - 1)
quicksortHelper(lyst, pivotLocation + 1, right)
def partition(lyst, left, right):
# Find the pivot and exchange it with the last item
middle = (left + right) // 2
pivot = lyst[middle]
lyst[middle] = lyst[right]
lyst[right] = pivot
# Set boundary point to first position
boundary = left
# Move items less than pivot to the left
for index in range(left, right):
if lyst[index] < pivot:
swap(lyst, index, boundary)
boundary += 1
# Exchange the pivot item and the boundary item
swap (lyst, right, boundary)
return boundary
def swap(lyst, i, j):
"""Exchanges the items at positions i and j."""
# You could say lyst[i], lyst[j] = lyst[j], lyst[i]
# but the following code shows what is really going on
temp = lyst[i]
lyst[i] = lyst[j]
lyst[j] = temp
import random
def main(size = 20, sort = quicksort):
lyst = []
for count in range(size):
lyst.append(random.randint(1, size + 1))
print(lyst)
sort(lyst)
print(lyst)
if __name__ == "__main__":
main()
合併排序
另一種名為合併排序(merge sort,又稱為歸併排序)的演算法利用了遞迴、分而治之的策略來突破O(n^2)的障礙。如下是該演算法的一個非正式的概述:
(1)計算一個列表的中間位置,並且遞迴地排序其左邊和右邊的子列表(分而治之)。
(2)將兩個排好序的子列表重新合併為單個的排好序的列表。
(3)當子列表不再能夠劃分的時候,停止這個過程。
有3個Python函式在這個頂層的設計策略中協作:
(1)mergeSort——使用者呼叫的函式。
(2)mergeSortHelper——一個輔助函式,它隱藏了遞迴呼叫所需要的額外引數。
(3)Merge——實現合併過程的一個函式。
1.實現合併過程
合併的過程使用了和列表相同大小的一個數組,這個陣列名為copyBuffer,為了避免每次呼叫merge的時候為copyBuffer分配和釋放記憶體的開銷,只在mergeSort中分配一次該緩衝區,並且在後續將其作為一個引數傳遞給mergeSortHelper和merge,每次呼叫mergeSortHelper的時候,都需要知道它所操作的子列表的邊界。這些邊界通過另外的引數low和high來提供,如下是mergeSort的程式碼:
from arrays import Array
def mergeSort(lyst):
# lyst: list being sorted
# copyBuffer: temporary space needed during merge
copyBuffer = Array(len(lyst))
mergeSortHelper(lyst,copyBuffer,0,len(lyst)-1)
在檢查到至少有兩項的一個子列表已經傳遞給它之後,mergeSortHelper函式計算了這個子列表的中點,遞迴地對中點以上和中點以下的部分進行排序,並且呼叫merge來合併結果。如下是mergeSortHelper的程式碼:
def mergeSortHelper(lyst,copyBuffer,low,high):
# lyst: list being sorted
# copyBuffer: temp space needed during merge
# low,high : bounds of sublist
# middle: midpoint of sublist
if low < high:
middle = (low+high)//2
mergeSortHelper(lyst,copyBuffer,low,middle)
mergeSortHelper(lyst,copyBuffer,middle+1,high)
merge(lyst,copyBuffer,low,middle,high)
下圖展示了在對擁有8項的一個列表開始遞迴呼叫mergeSortHelper的過程中所生成的子列表。注意,在這個例子中,子列表在每一個層級都是平均劃分的,在第k層,有2^k個子列表需要合併。如果最初列表的長度不是2的冪,那麼,無法在每一個層級都做到完全平均的劃分,並且最後的層級將不會擁有足夠的子列表。
最後,下面是merge函式的程式碼:
def merge(lyst,copyBuffer,low,middle,high):
# Initialize i1 and i2 to the first items in each sublist
i1 = low
i2 = middle + 1
# Interleave items from the sublists into the copyBuffer in such a way that order is maintained.
for i in range(low,high+1):
if i1 > middle:
copyBuffer[i] = lyst[i2] # 列表1已經用完
i2 += 1
elif i2 > high:
copyBuffer[i] = lyst[i1] # 列表2已經用完
i1 += 1
elif lyst[i1] < lyst[i2]:
copyBuffer[i] = lyst[i1] # 將小的數放上去
i1 += 1
else:
copyBuffer[i] = lyst[i2] # 將小的數放上去
i2 += 1
for i in range(low,high+1):
lyst[i] = copyBuffer[i] # 排序完的copyBuffer返回給lyst
merge函式將兩個排好序的子列表合併到一個大的排好序的子列表中。第1個子列表在low和middle之間,第2個子列表在middle+1和high之間,這個過程包含步驟:
(1)將索引指標設定為每個子列表的第1項,這分別是low和middle+1的位置。
(2)從每個子列表的第1項開始,重複地比較各項。將較小的項從其子列表中複製到快取中去,並且繼續處理子列表中的下一項。重複這個過程,直到兩個子列表中的所有項都已經複製過了。如果先到達了其中一個子列表的末尾,通過從另一個子列表複製剩餘的項,從而結束這個過程。
(3)將copyBuffer中low到high之間的部分,複製回lyst對應的位置。
2.合併排序的複雜度分析
合併排序的執行時間由兩條for語句主導,其中每一條都迴圈(high-low+1)次,結果,該函式的執行時間是O(high-low),在一個層的所有合併花費的時間是O(n)。由於mergeSortHelper在每一層都是儘可能平均地分割子列表,層級數是O(logn),並且在所有情況下,該函式的最大執行時間是O(nlogn)。
根據列表的大小,合併排序有兩個空間需求,首先,在支援遞迴呼叫的呼叫棧上,需要O(logn)的空間,其次,複製快取需要使用O(n)的空間。
指數演算法:遞迴式的Fibonacci
遞迴實現
如果我們採用遞迴的Fibonacci函式來實現,呼叫的次數似乎比問題規模的平方增長的還要快,具體程式碼如下:
from counter import Counter
def fib(n, counter):
"""Count the number of calls of the Fibonacci
function."""
counter.increment()
if n < 3:
return 1
else:
return fib(n - 1, counter) + fib(n - 2, counter)
problemSize = 2
print("%12s%15s" % ("Problem Size", "Calls"))
for count in range(5):
counter = Counter()
# The start of the algorithm
fib(problemSize, counter)
# The end of the algorithm
print("%12d%15s" % (problemSize, counter))
problemSize *= 2
counter函式如下:
class Counter(object):
"""Models a counter."""
# Class variable
instances = 0
#Constructor
def __init__(self):
"""Sets up the counter."""
Counter.instances += 1
self.reset()
# Mutator methods
def reset(self):
"""Sets the counter to 0."""
self._value = 0
def increment(self, amount = 1):
"""Adds amount to the counter."""
self._value += amount
def decrement(self, amount = 1):
"""Subtracts amount from the counter."""
self._value -= amount
# Accessor methods
def getValue(self):
"""Returns the counter's value."""
return self._value
def __str__(self):
"""Returns the string representation of the counter."""
return str(self._value)
def __eq__(self, other):
"""Returns True if self equals other
or False otherwise."""
if self is other: return True
if type(self) != type(other): return False
return self._value == other._value
結果如下:Problem Size Calls
2 1
4 5
8 41
16 1973
32 4356617
說明工作量的快速增長的另一種方式是顯示該函式針對給定的問題規模的一個呼叫樹,下圖展示了當使用遞迴函式來計算第6個斐波那契數的時候所用到的呼叫。
注意:每個填滿的層級上的呼叫綜述,都是其上一個層級的呼叫總數的兩倍。因此,完全平衡樹的遞迴呼叫次數通常是2^(n+1)-2,其中n是呼叫樹的頂部或者根部的引數,這顯然是一個指數級的O(k^n)的演算法。
指數演算法通常只對較小的問題規模才切合實際。儘管遞迴的Fibonacci在設計上很優雅,但是和使用迴圈按照線性時間執行的較快版本相比,它還是差很多。
此外,使用相同引數重複呼叫的遞迴函式,例如Fibonacci函式,可以通過一種叫作記憶(memoization)的技術,來使得其更有效率,根據這個技術,程式維護一張記錄了函式中所使用的每一個引數的值的表。在函式遞迴地計算一個給定引數的值之前,它會檢查這張表,看看該引數是否已經有了一個值了。如果是的,就直接返回這個值。如果沒有,繼續計算過程,並且隨後會將引數和值新增到這個表中。
Fibonacci函式轉換為一個線性演算法
這種演算法可以將複雜度降低到線性時間階。
class Counter(object):
"""Tracks a count."""
def __init__(self):
self._number = 0
def increment(self):
self._number += 1
def __str__(self):
return str(self._number)
def fib(n, counter):
"""Count the number of iterations in the Fibonacci
function."""
sum = 1
first = 1
second = 1
count = 3
while count <= n:
counter.increment()
sum = first + second
first = second
second = sum
count += 1
return sum
problemSize = 2
print("%12s%15s" % ("Problem Size", "Iterations"))
for count in range(5):
counter = Counter()
# The start of the algorithm
fib(problemSize, counter)
# The end of the algorithm
print("%12d%15s" % (problemSize, counter))
problemSize *= 2
結果如下:Problem Size Iterations
2 0
4 2
8 6
16 14
32 30
這裡可以看出,該函式的新版本的效能已經提升到了線性階。
本文是資料結構(用Python實現)這本書的讀書筆記!相關推薦
資料結構筆記----搜尋,排序和複雜度分析
演算法描述了最終能解決一個問題的計算過程。可讀性和易維護性是重要的質量指標。在計算機上執行演算法會消耗兩種資源:處理時間和空間或記憶體。當解決相同的問題或處理相同的資料集的時候,消耗這兩種資源較少的演算法會比消耗資源更多的演算法具有更高的質量,因此,它也是更加合適的演算法。3
一、演算法時間複雜度分析
參考書目:《資料結構與演算法 (java語言班)》 P25 評價演算法的執行時間是通過分析在一定規模下演算法的基本操作完成的,並且我們只對大規模問題的執行時間感興趣。O、Ω、Θ分別定義了時間複雜度的上界、下界、精確階。 計算時間複雜度,最簡單的方式就是計算
演算法 歸併排序的複雜度分析(含圖解流程和Master公式)
圖解流程 整體流程如下: 細節流程: 第一步: 第二步: 第三步: 第四步: 第五步: 第六步: 第七步: 第八步:
插入排序和迭代歸併排序以及複雜度分析
引言: 演算法是電腦科學中的基礎,程式=演算法+資料結構,演算法描述了我們將如何來處理資料的過程。本文將介紹兩種演算法的實現以及其中一種演算法的複雜度分析過程。1. 演算法介紹 歸併排序是利用歸併思想對數列進行排序,核心點是將陣列進行分組拆分,拆分到最小單元之時,即為有序
歸併排序及其複雜度分析
/* * (二分)歸併排序(穩定演算法) * 基本思想:將陣列遞迴分成越來越小的數集,直到每個數集只有一個數 * 然後將資料遞迴排序,使其合併成與原來資料一樣的有序序列 * 時間複雜度分析:遞迴分解資料,需要遞迴logN次,每次都需要對n個數據掃描一次,最好最壞平均都一樣,所
演算法分析——排序演算法(歸併排序)複雜度分析(遞迴樹法)
前面對演算法分析的一些常用的 漸進符號 做了簡單的描述,這裡將使用歸併排序演算法做為一個實戰演練。 這裡首先假設讀者對歸併排序已經有了簡單的瞭解(如果不瞭解的同學可以自行百度下歸併排序的原理)。瞭解此演算法的同學應都知道,歸併排序的主要思想是分而治之(簡稱分治)。分治演算法
演算法分析——排序演算法(歸併排序)複雜度分析(代換法)
上篇文章中我們對歸併排序的時間複雜度使用遞迴樹法進行了簡答的分析,並得出了結果歸併排序的時間複雜度為,這裡將使用代換法再進行一次分析。使用代換法解遞迴式的時候,主要的步驟有兩步: 1)猜測解的結果 2)使用數學歸納法驗證猜測結果
演算法分析——排序演算法(歸併排序)複雜度分析(主定理法)
前兩篇文章中分別是要用遞迴樹、代換法對歸併排序的時間複雜度進行了簡單的分析和證明,經過兩次分析後,我們發現遞迴樹法的特點是:可以很直觀的反映出整個歸併排序演算法的各個過程,但因為要畫出遞迴樹所以比較麻煩,所以遞迴樹演算法更適合新手,因為它可以讓分析者更直觀、簡易
氣泡排序(時間複雜度分析)
氣泡排序: public static void bubbleSort(int[] arr) { if(arr == null || arr.length < 2) { return; }
Python 搜尋、排序、複雜度分析
概述演算法是計算機程式的一個基本的構建模組。評價演算法質量的最基本的標準是正確性,另一個重要的標準是執行時間效能。當在一臺真實、資源有限的計算機上執行一個演算法的時候,經濟性的考慮就有了用武之地,這樣一個過程會消耗兩種資源:處理時間和空間或記憶體。統計指令用於估算演算法效能的
快速排序、程式碼實現(python3版)及其時間空間複雜度分析
快速排序是對氣泡排序的一種改進。基本思想是:通過一躺排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按次方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。最壞情況的時間複雜度為O(n2),最好情況時間複雜度
常見排序演算法的基本原理、程式碼實現和時間複雜度分析
排序演算法無論是在實際應用還是在工作面試中,都扮演著十分重要的角色。最近剛好在學習演算法導論,所以在這裡對常見的一些排序演算法的基本原理、程式碼實現和時間複雜度分析做一些總結 ,也算是對自己知識的鞏固。 說明: 1.本文所有的結果均按照非降序排列; 2.本文所有的程式均用c++實現,
排序演算法——希爾排序的圖解、程式碼實現以及時間複雜度分析
希爾排序(Shellsort) 希爾排序是衝破二次時間屏障的第一批演算法之一。 希爾排序通過比較相距一定間隔的元素來工作;各躺比較所用的距離隨著演算法的進行而減小,直到只比較相鄰元素的最後一趟排序為止。由於這個原因,希爾排序有時也叫做縮減增量排序。 希爾排
常用排序演算法穩定性、時間複雜度分析
1、 選擇排序、快速排序、希爾排序、堆排序不是穩定的排序演算法, 氣泡排序、插入排序、歸併排序和基數排序是穩定的排序演算法。 2、研究排序演算法的穩定性有何意義? 首先,排序演算法的穩定性大家應該都知道,通俗地講就是能保證排序前兩個相等的資
經典內部排序演算法學習總結(演算法思想、視覺化、Java程式碼實現、改進、複雜度分析、穩定性分析)
一、什麼是排序演算法? 排序,顧名思義,就是按照一定的規則排列事物,使之彼此間有序 而排序演算法所要做的工作,就是將資料按照人為制定的比較規則排列好,使資料處於彼此間有序的狀態。 二、為什麼要進行排序? 那為什麼要將資料排序呢?計算機處理速度這麼
排序演算法——插入排序的圖解、程式碼實現以及時間複雜度分析
插入排序 插入排序的原理: 插入排序由N-1躺排序完成,對於p=1到N-1躺,插入排序保證從位置0到位置p的元素為已排序狀態。 插入排序的程式碼實現: /** * 插入排序的實現例程,特點是使用了泛型,可以接受任何實現了Compa
算法系列-複雜度分析:淺析最好、最壞、平均、均攤時間複雜度
整理自極客時間-資料結構與演算法之美。原文內容更完整具體,且有音訊。購買地址: 上一節,我們講了複雜度的大 O 表示法和幾個分析技巧,還舉了一些常見覆雜度分析的例子,比如 O(1)、O(logn)、O(n)、O(nlogn) 複雜度分析。掌握了這些內容,對於複雜度分析這個知識點,你已經
13、【演算法】演算法複雜度分析
一、演算法的時間複雜度分析 1、時間複雜度的定義 在進行演算法分析時,演算法中基本操作語句重複執行的次數是問題規模n的某個函式,用T(n)表示,若有某個輔助函式f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函式,
【資料結構與演算法-java實現】二 複雜度分析(下):最好、最壞、平均、均攤時間複雜度的概念
上一篇文章學習了:如何分析、統計演算法的執行效率和資源消耗? 點選連結檢視上一篇文章:複雜度分析上 今天的文章學習以下內容: 最好情況時間複雜度 最壞情況時間複雜度 平均情況時間複雜度 均攤時間複雜度 1、最好與最壞情況時間複雜度 我們首先
複雜度分析(上):如何分析、統計演算法的執行效率和資源消耗
一、什麼是複雜度分析? 1.資料結構和演算法本身解決的是“快”和“省”的問題,即如何讓程式碼執行得更快,如何讓程式碼更省儲存空間。 2.因此從執行時間和佔用空間兩個維度來評估資料結構和演算法的效能 3.分別用時間複雜度和空間複雜度兩個概念來描述效能問題,二者統稱為複雜度