數據結構與算法(python)引入篇

數據結構與算法(python)

一個不太恰當的理解, 兵法
如果將寫好運行的程序比作戰場, 碼農就是指揮這場戰鬥的指揮官, 手中的代碼就是被指揮的士兵和武器.
兵法就是取得這場戰鬥的勝利的關鍵所在. 運籌帷幄之中, 決勝與千里之外.

我們的數據結構和算法, 就是程序員取勝的關鍵. 沒有看過數據結構與算法, 有事面對問題沒有任何的思路, 不知如何下手;雖然大部分時間可能解決了問題, 可是對程序運行的效率和開銷沒有意識, 性能底下; 面對強敵時, 有着兵法, 領軍打戰; 同樣問題時, 算法就是我們的兵法.

嘗試

如果 a+b+c=1000, 且a^2+b^2=c^2(a, b, c爲自然數), 如何求出所有a, b, c可能的值

第一次嘗試

通過枚舉a b c的值一個一個來試, 總有一個是我們想要的.

設計程序abc在1000範圍內一個一個的來遍歷直接三個循環算完了就得到答案了.

程序如下:

def first():
    start_time = time.time()
    for a in range(1000):
        for b in range(1000):
            for c in range(1000):
                if a+b+c == 1000 and a**2 + b**2 == c**2:
                    print ('a:b:c:%d, %d, %d' % (a, b, c))
    end_time = time.time()
    print ('程序所需時間', start_time - end_time)
    print ('finished!!!')

運行結果:

三次循環運行結果

66.929 我們的第一次持續了66秒

什麼是算法

算法是計算機處理信息的本質, 計算機的程序本質上是一個算法來告訴計算機確切的步驟執行一個指定的任務.

算法是獨立存在的一種解決問題的方法和思想
對於算法而言, 實現的語言並不重要, 重要的是思想. 算法可以用多種語言來實現, 這裏我選擇使用python來描述.

算法的五大特性

  1. 輸入: 算法具有0個或多個輸入
  2. 輸出: 至少有一個或多個輸出
  3. 有窮性: 算法在有限的步驟之後會自動結束而不會無限循環,並且每一個步驟可以在可接受的時間內完成
  4. 確定性: 算法的每一步都有確定的含義, 不會出現二義性
  5. 可行性: 每一步都是可行的, 每一步都能夠執行有限的次數完成

第二次嘗試

def second():
    start_time = time.time()
    for a in range(1000):
        for b in range(1000):
            c = 1000 - a - b
            if a**2 + b**2 == c**2:
                print ('a:b:c;%d, %d, %d' % (a, b, c))
    end_time = time.time()
    print ('花費的時間:', start_time - end_time)
    print ("finished!!!!!")

if __name__ = '__main__':
    second()

運行結果:

second

這次的時間只有0.6秒

通過這兩種方法的比較得到一個算法效率衡量的問題

算法效率衡量

執行時間反應算法效率

對於同一個問題, 以上給出的兩種解決算法, 通過對時間進行了測算,發現程序執行的時間相差懸殊(一個需要69秒,一個需要0.6秒), 由此推斷出: 實現算法程序的執行時間可以反應出算法的效率, 即算法的優劣

時間複雜度與"大o記法"

單純的依靠運行的時間來比較算法的優劣並不一定是客觀準確的 ,程序的運行離不開計算機環境(如果兩次運行程序的計算機環境不一樣, 時間就不一定了, 一個在i9-9900K跑,一個在大屁股上跑, 誰比誰快不一定.完全沒有可比性). 不同的計算機環境影響程序的執行時間. 所以引入時間複雜度和"大O記法"來客觀反應算法的時間效率.

我們假定計算機執行算法每一個基本操作的時間是固定的一個時間單位,那麼有多少個基本操作就代表會花費多少時間單位。算然對於不同的機器環境而言,確切的單位時間是不同的,但是對於算法進行多少個基本操作(即花費多少時間單位)在規模數量級上卻是相同的,由此可以忽略機器環境的影響而客觀的反應算法的時間效率。

時間效率, 可以使用"大O記法"來表示.

**“大O記法”: ****對於單調的整數函數f,如果存在一個整數函數g和實常數c>0,使得對於充分大的n總有f(n)<=c*g(n),就說函數g是f的一個漸近函數(忽略常數),記爲f(n)=O(g(n))。也就是說,在趨向無窮的極限意義下,函數f的增長速度受到函數g的約束,亦即函數f與函數g的特徵相似.

時間複雜度: 假設存在函數g,使得算法A處理規模爲n的問題示例所用時間爲T(n)=O(g(n)),則稱O(g(n))爲算法A的漸近時間複雜度,簡稱時間複雜度,記爲T(n)

簡單舉例:

for a in range(n):	# 	a要執行n次操作
    for b in range(n):#   b也要執行n次操作
        # 綜合起來要執行n*n次操作 也就是這兩次的時間複雜度爲n**2
        # 可以認爲2n**2和100n**2是屬於同一個數量級的.都是屬於n**2級

最壞時間複雜度

  • 最優時間複雜度: 算法完成工作最少需要多少基本操作, 反應的是最理想的情況, 沒有什麼參考價值
  • 最壞時間複雜度: 算法完成工作最多需要多少基本操作, 提供了一種保證,算法在經歷了指定的步驟一定能完成工作.
  • 平均時間複雜度: 算法完成工作平均需要多少基本操作.

時間複雜度計算規則

  1. 基本操作, 即只有常數項,複雜度爲O(1)
  2. 順序結構, 按加法計算
  3. 循環結構按乘法計算
  4. 分支結構按分支執行步驟的最大值計算
  5. 判斷算法的效率時, 只需關注操作數的最高次項, 其他可以忽略
  6. 沒有特殊說明的時候,算法複雜度就是 最壞時間複雜度

算法分析

第一次嘗試的算法核心部分複雜度:T(n)=O(n*n*n)=O(n**3)

第二次嘗試:T(n) = O(n*n*(1+1)) = O(n*n) = O(n**2)

所以第二次算法時間複雜度好, 程序時間相對塊點.

常見的時間複雜度

執行次數函數舉例 非正式術語
12 O(1) 常數階
2n+3 O(n) 線性階
3n2+2n+1 O(n2) 平方階
5log2n+20 O(logn) 對數階
2n+3nlog2n+19 O(nlogn) nlogn階
6n3+2n2+3n+4 O(n3) 立方階
2n O(2n) 指數階

注意,經常將log2n(以2爲底的對數)簡寫成logn

他們之間的關係圖如下:

時間複雜度關係圖

所以消耗時間的排序也就有了

O(1) < O(logn) < O(n) < O(nlogn) < O(n**2) < O(n**3) < O(2**n) < O(n!) < O(n**n)

python 內置類型性能分析

python內部timeit模塊可以用來測試python代碼的執行速度

class timeit.Time(stmt='pass', setup='pass', timer=<timer function>)

timer是測量小段代碼執行速度的類

stmt參數是要測試的代碼語句(statment)

setup參數是運行代碼需要的設置

timer參數是一個定時器函數, 與平臺有關.

timer.Timer.timeit(number=100000)

Timer類中測試語句執行速度的對象方法. number參數是測試代碼是的測試次數, 默認爲1000000次, 方法返回執行代碼的平均耗時, 一個float類型的秒數.

對四種常見的列表創建方法的性能分析

  • 列表的添加
  • append方法
  • 列表生成器
  • list方法

代碼如下:

#!/usr/bin/python3
# -*- coding=utf8 -*-
"""
# @Author : pig
# @CreatedTime:2020-03-18 14:38:37
# @Description : 
"""

from timeit import Timer

def tt1():
    l = []
    for i in range(10000):
        l = l + []

def tt2():
    l = []
    for i in range(10000):
        l.append(i)

def tt3():
    l = [i for i in range(10000)]

def tt4():
    l = list(range(10000))


t1 = Timer("tt1()", "from __main__ import tt1")
print ("[]+:", t1.timeit(number=1000))
t2 = Timer('tt2()', "from __main__ import tt2")
print ("append:", t2.timeit(number=1000))
t3 = Timer("tt3()", "from __main__ import tt3")
print ("[i for i in range(10000)]:", t3.timeit(number=1000))
t4 = Timer("tt4()", "from __main__ import tt4")
print ("list(range)", t4.timeit(number=1000))

性能對比運行結果

對於其他指定位置的插入, 列表的生成方法測試如下:


def tt5():
    l = []
    for i in range(10000):
        l.extend([i])

def tt6():
    l = []
    for i in range(10000):
        l.append(i)

def tt7():
    l = []
    for i in range(10000):
        l.insert(0, i)

def tt8():
    l = []
    for i in range(10000):
        l += [i]

t5 = Timer("tt5()", "from __main__ import tt5")
print ("extend:", t5.timeit(number=1000))
t6 = Timer("tt6", "from __main__ import tt6")
print ("append()", t6.timeit(number=1000))
t7 = Timer("tt7()", "from __main__ import tt7")
print ("insert", t7.timeit(number=1000))
t8 = Timer("tt8()", "from __main__ import tt8")
print ("+= :", t8.timeit(number=1000))

總體結果如下:
總體結果對比

可以自行測試pop操作, 看看pop操作的效率如何

list內置操作的時間複雜度

list內置時間操作複雜度

dict內置操作時間複雜度

dict內置操作時間複雜度

數據結構

我們如何使用python中的類型來保存一個班的學生信息? 如果想要快速通過學生姓名獲取其信息呢?

實際上我們考慮這個問題就需要數據結構了. 列表和字典都可以存儲一個班的信息, 但是想要在列表中獲取一名同學的信息時, 就要遍歷這個列表, 時間複雜度爲n, 而使用字典存儲是, 可以通過字典的鍵值查詢,其時間複雜度爲O(1). 爲了解決問題, 需要將數據保存下來, 數據的存儲方式不同就需要不同的算法進行處理.效率越高越好. 列表和字典就是python內建幫我們封裝好的兩種數據結構.

概念

數據是一個抽象的概念,將其進行分類後得到程序設計語言中的基本類型。如:int,float,char等。數據元素之間不是獨立的,存在特定的關係,這些關係便是結構。數據結構指數據對象中數據元素之間的關係。

Python給我們提供了很多現成的數據結構類型,這些系統自己定義好的,不需要我們自己去定義的數據結構叫做Python的內置數據結構,比如列表、元組、字典。而有些數據組織方式,Python系統裏面沒有直接定義,需要我們自己去定義實現這些數據的組織方式,這些數據組織方式稱之爲Python的擴展數據結構,比如棧,隊列等。

算法與數據結構的區別

數據結構只是靜態的描述了數據元素之間的關係. 高效的程序需要在數據結構的基礎上設計和選擇算法.

程序 = 數據結構 + 算法

算法是爲了解決實際問題而設計的, 數據結構是算法需要處理的問題載體

抽象數據類型(Abstract Data Type)

抽象數據類型(ADT)的含義是指一個數學模型以及定義在此數學模型上的一組操作。即把數據類型和數據類型上的運算捆在一起,進行封裝。引入抽象數據類型的目的是把數據類型的表示和數據類型上運算的實現與這些數據類型和運算在程序中的引用隔開,使它們相互獨立。

常用的數據運算有五種:

  • 插入
  • 刪除
  • 修改
  • 查找
  • 排序

頭條號
簡書

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章