1. 程式人生 > >資料結構筆記----搜尋,排序和複雜度分析

資料結構筆記----搜尋,排序和複雜度分析

演算法描述了最終能解決一個問題的計算過程。可讀性和易維護性是重要的質量指標。

在計算機上執行演算法會消耗兩種資源:處理時間和空間或記憶體。當解決相同的問題或處理相同的資料集的時候,消耗這兩種資源較少的演算法會比消耗資源更多的演算法具有更高的質量,因此,它也是更加合適的演算法。

3.1.1度量演算法的執行時間

# -*- coding:utf-8 -*-
import time

problemSize = 10000000
print('%12s%16s' % ('ProblemSize', 'second'))
for count in range(5):
    # 定義起始時間
    start = time.time()
    work = 1
    # 迴圈遍歷問題大小
    for x in range(problemSize):
        # 使用work增減統計次數
        work += 1
        work -= 1
    # 再次呼叫時間減去開始時間得到執行時間
    elapsed = time.time() - start

    print('%12s%16.3f' % (problemSize, elapsed))
    # 問題大小翻倍
    problemSize *= 2
 ProblemSize          second
    10000000           1.553
    20000000           2.749
    40000000           5.514
    80000000          11.129
   160000000          29.064

不同硬體平臺的處理速度不同,因此,一個演算法的執行時間,在不同機器上是不同的。

對於很大的資料集合來說,確定某些演算法的執行時間是不切實際的。

3.1.2統計指令

#-*- coding:utf-8 -*-
problemSize = 1000
print('%12s%15s' % ('ProblemSize', 'Iterations'))
for count in range(5):
    #定義次數
    number = 0
    
    work = 1
    #迴圈巢狀遍歷問題大小的平方
    for j in range(problemSize):
        for k in range(problemSize):
            #統計次數
            number += 1
            work += 1
            work -= 1

    print('%12d%15d' % (problemSize, number))
    #迴圈一次後問題大小翻倍
    problemSize *= 2
ProblemSize     Iterations
        1000        1000000
        2000        4000000
        4000       16000000
        8000       64000000
       16000      256000000

估算演算法效能的另一種技術,是統計對不同的問題規模所要執行的指令的數目。

#-*- coding:utf-8 -*-
from counter import Counter
#遞迴實現Fibonacci函式
def fib(n, counter):
    #遞增一次,並記數
    counter.incremnet()
    if n < 3:
        return 1
    else:
        return fib(n-1, counter) + fib(n-2, counter)

problemSize = 2
print('%12s%15s' % ('ProblemSize', 'Calls'))
for count in range(5):
    counter = Counter()

    fib(problemSize, counter)

    print('%12d%15d' % (problemSize, counter))
    problemSize *= 2

計算斐波那契數列對不同問題規模的呼叫次數。

統計指令方法的問題在於當問題規模過大時,計算機還是無法執行的足夠快。統計指令是正確的思路。

3.1.3度量演算法所使用的記憶體

對於演算法所使用的資源的完整分析,還包括所需的記憶體數量。一些演算法對於任何問題都需要相同大小的記憶體,另一些演算法則會隨著問題規模越來越大,從而需要更多的記憶體。

3.2複雜度分析

對於大多數問題規模n來說,線性行為所做的工作比二次階的行為所做的工作要少很多。

如果一個演算法對於任何的問題規模,都需要相同的操作次數,那麼它具有常數階的效能。

另一種複雜度的階要比線性階好一些,但是比常數階差一些,這就是對數階。

比多項式階更差一些的複雜度階,叫做指數階。對於較大的問題規模來說,執行指數階演算法是不切實際的。

3.3搜尋演算法

3.3.1搜尋最小值

def fMin(lyst):
    cIndex = 1
    minIndex = 0
    while cIndex < len(lyst):
        if lyst[cIndex] < lyst[minIndex]:
            minIndex = cIndex
        cIndex += 1
    return minIndex

print(fMin([1, 3, 4, 5, 1, 5, 6]))

這個演算法遍歷整個數列,所以時間複雜度是O(n)

3.3.2順序搜尋一個列表

#-*- coding:utf-8 -*-
def sList(target, lyst):#查詢目標的值是否在列表中
    position = 0
    while position < len(lyst):
        if target == lyst[position]:
            return position
        position += 1
    #沒找到則返回-1
    return -1

print(sList(3, [1, 3, 4, 5, 1, 5, 6]))

3.3.3最好情況,最壞情況和平均情況的效能

有些演算法的效能取決於所處理資料的放置方式。

順序搜尋的分析考慮三種情況:

1.最壞情況,目標在列表末尾,或者根本不在列表中,複雜度為O(n)

2.最好情況一次就找到目標,複雜度為O(1)

3.平均情況要把在每一個可能的位置找到目標項所需迭代次數相加,併除以n(列表的值數)。複雜度約為O(n)。

順序搜尋的最好情況的效能是很少見的,而平均情況和最壞情況的效能則基本相同。

3.3.4有序列表的二叉搜尋

當搜尋排序的資料的時候,可以使用二叉搜尋。

def binarySearch(target, soredLyst):
    left = 0
    right = len(soredLyst) - 1
    while left <= right:
        midpoint = (left + right) // 2
        if target == soredLyst[midpoint]:
            return midpoint
        elif target < soredLyst[midpoint]:
            right = midpoint - 1
        else:
            left = midpoint + 1
    return -1

print(binarySearch(3, [1, 2, 3, 5, 6]))

二叉搜尋可能比順序搜尋更為高效,但必須保證列表是有序的。

3.3.5比較資料項

class SavingAccount(object):
    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欄位呼叫了<運算子。

為了允許演算法對一個新物件的類使用比較運算子==,<,>,在該類中應定義__eq__,__lt__,__gt__方法。

3.4.1選擇排序

#-*- coding:utf-8 -*-
def selectionSort(lyst):
    #起始為第一個位置
    i = 0
    #當i未超出列表範圍時迴圈
    while i < len(lyst) - 1:
        #預設最小值索引為i
        mIndex = i
        #j為最小值後一位數
        j = i + 1
        #迴圈巢狀判斷j是否為最小值
        while j < len(lyst):
            if lyst[j] < lyst[mIndex]:
                mIndex = j
            #迴圈最後j+1判斷下一個值
            j += 1
        #如果最小值索引不等於i,則交換兩者的值
        if mIndex != i:
            swap(lyst, mIndex, i)
        #一次大迴圈後i自增1,排列第二個位置
        i += 1
    return lyst

def swap(lyst, i, j):
    temp = lyst[i]
    lyst[i] = lyst[j]
    lyst[j] = temp

print(selectionSort([5, 3, 1, 2, 4]))

選擇排序在各種情況下的複雜度為O(n2)。

3.4.2氣泡排序

#-*- coding:utf-8 -*-
def bubbleSortWithTweak(lyst):
    #定義n為列表的長度
    n = len(lyst)
    #列表不為0時迴圈
    while n > 1:
        #未轉換為False
        swapped = False
        #定義起始位置為1
        i = 1
        #當n大於起始位置時迴圈
        while i < n:
            #如果前一位數大於後一位數,交換它們的值
            if lyst[i] < lyst[i-1]:
                swap(lyst, i, i-1)
                swapped = True
            #迴圈最後i自增1
            i += 1

        #如果是已經排好的列表則直接返回
        if not swapped:return
        #大迴圈最後n自減1,表示開始進行下一次比較
        n -= 1
        return lyst

def swap(lyst, i, j):
    temp = lyst[i]
    lyst[i] = lyst[j]
    lyst[j] = temp

print(bubbleSortWithTweak([5,4,2,1,3]))

氣泡排序的複雜度也是O(n2)

3.4.3插入排序

和其他排序演算法一樣,插入排序包含兩個迴圈。外圍的迴圈遍歷從1到n-1的位置。對於這個迴圈中的每一個位置i,我們都儲存該項並且從位置i-1開始內部迴圈。對於這個迴圈的每一個位置j,我們都將項移動到位置j+1,直到找到了給儲存的項(第i項)的插入位置。

#-*- coding:utf-8 -*-
def insertionSort(lyst):
    #定義i為1
    i = 1
    #當i小於列表長度時迴圈
    while i < len(lyst):
        #預設插入點為第二個列表值
        itemToInsert = lyst[i]
        #定義j為i前一位數
        j = i - 1
        #當j大於等於0時開始迴圈
        while j >= 0:
            #如果插入點的值小於前一位的值,則將j後一項和j調換
            if itemToInsert < lyst[j]:
                lyst[j+1] = lyst[j]
                #調換一次後j自減1
                j -= 1
            else:
                break
        #內迴圈結束後將j後一項值向插入點
        lyst[j + 1] = itemToInsert
        #大迴圈結束後i自增1,插入點向後移
        i += 1
    return lyst

print(insertionSort([2, 5, 1, 4, 3]))

關鍵在與內迴圈,使小的值不斷上移,直到合適位置。

插入排序的最壞情況的複雜度是O(n2)。

3.4.4再談最好情況,最壞情況和平均情況效能

最好情況:在該條件下。演算法做的工作最少。

最壞情況:演算法做的事最多。

平均情況:演算法做的工作最典型。

3.5.1快速排序

#-*- coding:utf-8 -*-
#將列表的起始位置和結束位置和列表傳給Helper函式
def quicksort(lyst):
    quicksortHelper(lyst, 0, len(lyst) - 1)

def quicksortHelper(lyst, left, right):
    #如果列表不為0
    if left < right:
        
        pivotL = partition(lyst, left, right)
        #左值排序
        quicksortHelper(lyst, left, pivotL - 1)
        #右值排序
        quicksortHelper(lyst, pivotL + 1, right)
#旨在將小於中值的數向左,大於中值的數在右
def partition(lyst, left, right):
    #中點位置為起點與終點和的一半
    middle = (left + right) // 2
    #找到中點值並與最後一個值交換
    pivot = lyst[middle]
    lyst[middle] = lyst[right]
    lyst[right] = pivot
    #指向第一個位置
    boundary = left
    #在列表中迴圈
    for index in range(left, right):
        #如果最後的值大於前面的值,則將比pivot小的值往左移
        if lyst[index] < pivot:
            swap(lyst, index, boundary)
            boundary += 1
    #交換pivot和boundary
    swap(lyst, right, boundary)
    return boundary

def swap(lyst, i, j):
    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()

思路:分割左邊的子列表,在中值左右產生兩個子列表,對子列表重複分割過程,直到子列表的長度最大為1.

3.5.2合併排序

#-*- coding:utf-8 -*-
from arrays import Array

def mergeSort(lyst):
    copyBuffer = array(len(lyst))
    mergeSortHelper(lyst, copyBuffer, 0, len(lyst) - 1)

def mergeSortHelper(lyst, copyBuffer, low, high):
    if low < high:
        middle = (low + high) // 2
        mergeSortHelper(lyst, copyBuffer, low, middle)
        mergeSortHelper(lyst, copyBuffer, middle + 1, high)
        merge(lyst, copyBuffer, low, middle, high)

def merge(lyst, copyBuffer, low, middle, high):
    i1 = low
    i2 = middle + 1
    for i in range(low, high + 1):
        if i1 > middle:
            copyBuffer[i] = lyst[i2]
        elif i2 > high:
            copyBuffer[i] = lyst[i1]
        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]


import random

def main(size = 20, sort = mergeSort):
    lyst = []
    for count in range(size):
        lyst.append(random.randint(1, size + 1))
    print(lyst)
    sort(lyst)
    print(lyst)

if __name__ == '__main__':
    main()

計算一個列表的中間位置,並且遞迴地排序其左邊和右邊的子列表。

將倆個排好序的子列表重新合併成單個排好序的列表。

當子列表不再能夠劃分的時候停止這個過程。