數據結構與算法 介紹

1. 引入:算法的提出

2. 算法的效率衡量:時間複雜度

3. 常見的時間複雜度

4. Python內置類型性能分析

5. 數據結構

 

 

1. 引入:算法的提出

先來看一道題:python

若是 a+b+c=1000,且 a^2+b^2=c^2(a、b、c爲天然數),如何求出全部a、b、c可能的組合?算法

第一次嘗試

 1 import time 
 2 
 3 start_time = time.time()
 4 
 5 # 三重循環
 6 for a in range(1001):
 7     for b in range(1001):
 8         for c in range(1001):
 9             if a + b + c == 1000 and a**2 + b**2 == c**2:
10                 print("a:{}, b:{}, c:{}".format(a, b, c))
11                 
12 end_time = time.time()
13 print("used time: %i" % end_time - start_time)
14 print("complete!")

運行結果:數據結構

a:0, b:500, c:500
a:200, b:375, c:425
a:375, b:200, c:425
a:500, b:0, c:500
used time: 620 seconds
complete!

算法的提出

算法的概念

算法是計算機處理信息的本質,由於計算機程序本質上是一個算法來告訴計算機按照確切的步驟來執行一個指定的任務。通常地,當算法在處理信息時,會從輸入設備或數據的存儲地址讀取數據,把結果寫入輸出設備或某個存儲地址供之後再調用。app

算法是獨立存在的一種解決問題的方法和思想。對於算法而言,實現的語言並不重要,重要的是思想。函數

算法能夠有不一樣的語言描述實現版本(如C描述、C++描述、Python描述等),咱們如今是在用Python語言進行描述實現。性能

算法的五大特性

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

第二次嘗試

 1 import time 
 2 
 3 start_time = time.time()
 4 
 5 # 兩重循環
 6 for a in range(1001):
 7     for b in range(1001):
 8         c = 1000 - a - b
 9         if a**2 + b**2 == c**2:
10             print("a:{}, b:{}, c:{}".format(a, b, c))
11                 
12 end_time = time.time()
13 print("used time: %i seconds" % (end_time - start_time))
14 print("complete!")

運行結果:測試

a:0, b:500, c:500
a:200, b:375, c:425
a:375, b:200, c:425
a:500, b:0, c:500
used time: 4 seconds
complete!

 

2. 算法的效率衡量:時間複雜度

執行時間

執行時間反應算法效率

對於同一問題,咱們給出了兩種解決算法,在兩種算法的實現中,咱們對程序執行的時間進行了測算,發現兩段程序執行的時間相差懸殊(620秒相比於4秒),由此咱們能夠得出結論:實現算法程序的執行時間能夠反應出算法的效率,即算法的優劣。spa

單靠時間值絕對可信嗎?

假設咱們將第二次嘗試的算法程序運行在一臺配置古老性能低下的計算機中,狀況會如何?極可能運行的時間並不會比在咱們的電腦中運行算法一的執行時間快多少。所以,單純依靠運行的時間來比較算法的優劣並不必定是客觀準確的!操作系統

程序的運行離不開計算機環境(包括硬件和操做系統),這些客觀緣由會影響程序運行的速度並反應在程序的執行時間上。那麼如何才能客觀的評判一個算法的優劣呢?設計

* 時間複雜度

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

即時間複雜度就是用算法所需的步驟數量來衡量其效率。那麼,如何來斷定所需步驟數量的標準呢?此時可使用「大O記法」。

* 「大O計法」

對於算法的時間效率,咱們能夠用「大O計法」來表示,如下從數學上理解:

  • 時間複雜度 —— T(n):假設存在函數g,使得算法A處理規模爲n的問題示例所用時間爲T(n)=O(g(n)),則稱O(g(n))爲算法A的漸近時間複雜度,簡稱時間複雜度,記爲T(n)。
  • 「大O記法」 —— g(n):對於單調的整數函數f,若是存在一個整數函數g和實常數c>0,使得對於充分大的n,總有f(n)<=c*g(n),就說函數g是f的一個漸近函數(忽略常數),記爲f(n)=O(g(n))。也就是說,在趨向無窮的極限意義下,函數f的增加速度受到函數g的約束,亦即函數f與函數g的特徵類似。

通俗理解:當解決問題的計算步驟跟n相關時,把旁支末節(如其相關係數)所有忽略掉,只留下最關鍵的特徵(n的部分),就是大O表示法。

以上述引入的示例代碼爲例:

# 例1:a+b+c=1000,且 a^2+b^2=c^2(a,b,c 爲天然數)
for a in range(1001):  # 基本計算步驟爲1000次
    for b in range(1001):  # 1000次
        for c in range(1001):  # 1000次
            if a + b + c == 1000 and a**2 + b**2 == c**2:  # 1次
                print("a:{}, b:{}, c:{}".format(a, b, c))  # 1次

# 例2:若是改成 a+b+c=2000,且 a^2+b^2=c^2(a,b,c 爲天然數)呢?

簡單劃分示例中的計算步驟數量:

  • 例1:T = 1000 * 1000 * 1000 * 2
  • 例2:T = 2000 * 2000 * 2000 * 2 
  • 即:T(n) = n * n * n * 2 = n^3 * 2

當使用大O計法時,去掉相關係數2,只會留下n^3,記爲 g(n) = n^3 。此時,可說T(n) = g(n)

所以,即便相關係數有所變化,如T(n) = n * n * n * 2 = n^3 * 10,咱們也認爲二者(n^3 * 2 與 n^3 * 10)效率「差很少」。

最壞時間複雜度

分析算法時,存在幾種可能的考慮:

  • 算法完成工做最少須要多少基本操做,即最優時間複雜度
  • 算法完成工做最多須要多少基本操做,即最壞時間複雜度
  • 算法完成工做平均須要多少基本操做,即平均時間複雜度

對於最優時間複雜度,其價值不大,由於它沒有提供什麼有用信息,其反映的只是最樂觀最理想的狀況,沒有參考價值。

對於最壞時間複雜度,提供了一種保證,代表算法在此種程度的基本操做中必定能完成工做。

對於平均時間複雜度,是對算法的一個全面評價,所以它完整全面的反映了這個算法的性質。但另外一方面,這種衡量並無保證,不是每一個計算都能在這個基本操做內完成。並且,對於平均狀況的計算,也會由於應用算法的實例分佈可能並不均勻而難以計算。

所以,咱們主要關注算法的最壞狀況,亦即最壞時間複雜度。

時間複雜度的幾條基本計算規則

  1. 基本操做,即只有常數項,認爲其時間複雜度爲O(1)
  2. 順序結構,時間複雜度按加法進行計算
  3. 循環結構,時間複雜度按乘法進行計算
  4. 分支結構,時間複雜度取各分支中的最大值
  5. 判斷一個算法的效率時,每每只須要關注操做數量的最高次項,其它次要項和常數項能夠忽略
  6. 在沒有特殊說明時,咱們所分析的算法的時間複雜度都是指最壞時間複雜度

示例:

1 for a in range(n):  # 循環
2     for b in range(n):  # 循環
3         c = 1000 - a - b  # 基本操做
4         if a**2 + b**2 == c**2:  # 分支:要麼進入分支中的print,要麼退出
5             print("a:{}, b:{}, c:{}".format(a, b, c))  # 基本操做            

其時間複雜度:T(n)

= n * n * (1 + max(1, 0))

= n^2 * 2

= O(n^2)

算法分析

第一次嘗試的算法核心部分

1 for a in range(0, 1001):
2     for b in range(0, 1001):
3         for c in range(0, 1001):
4             if a**2 + b**2 == c**2 and a+b+c == 1000:
5                 print("a, b, c: %d, %d, %d" % (a, b, c))

時間複雜度:T(n) = O(n*n*n) = O(n3)

第二次嘗試的算法核心部分

1 for a in range(0, 1001):
2     for b in range(0, 1001-a):
3         c = 1000 - a - b
4         if a**2 + b**2 == c**2:
5             print("a, b, c: %d, %d, %d" % (a, b, c))

時間複雜度:T(n) = O(n*n*(1+1)) = O(n*n) = O(n2)

因而可知,咱們嘗試的第二種算法要比第一種算法的時間複雜度好得多。

 

3. 常見的時間複雜度

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

常見時間複雜度之間的關係

 

 所消耗的時間從小到大:

 

4. Python內置類型性能分析

timeit模塊

Python3中的timeit模塊能夠用來測試小段代碼的運行時間。其中主要經過兩個函數來實現:timeit和repeat,代碼以下:

def timeit(stmt="pass", setup="pass", timer=default_timer,
           number=default_number, globals=None):
    """Convenience function to create Timer object and call timeit method."""
    return Timer(stmt, setup, timer, globals).timeit(number)

def repeat(stmt="pass", setup="pass", timer=default_timer,
           repeat=default_repeat, number=default_number, globals=None):
    """Convenience function to create Timer object and call repeat method."""
    return Timer(stmt, setup, timer, globals).repeat(repeat, number)

在上面的代碼中可見,不管是timeit仍是repeat都是先生成Timer對象,而後調用了Timer對象的timeit或repeat函數。

在使用timeit模塊時,能夠直接使用timeit.timeit()、tiemit.repeat(),還能夠先用timeit.Timer()來生成一個Timer對象,而後再用TImer對象用timeit()和repeat()函數,後者再靈活一些。

上述兩個函數的入參:

  • stmt:用於傳入要測試時間的代碼,能夠直接接受字符串的表達式,也能夠接受單個變量,也能夠接受函數。傳入函數時要把函數申明在當前文件中,而後在 stmt = ‘func()’ 執行函數,而後使用 setup = ‘from __main__ import func’
  • setup:傳入stmt的運行環境,好比stmt中使用到的參數、變量,要導入的模塊等。能夠寫一行語句,也能夠寫多行語句,寫多行語句時要用分號;隔開語句。
  • number:要測試的代碼的運行次數,默認100000次,對於耗時的代碼,運行太屢次會比較慢,此時建議本身修改一下運行次數。
  • repeat:指測試要重複幾回,每次的結果構成列表返回,默認3次。

list的操做測試

 1 import timeit
 2 
 3 # ----生成列表的效率----
 4 
 5 def t1():
 6     l = []
 7     for i in range(1000):
 8         l = l + [i]
 9 
10 def t2():
11     l = []
12     for i in range(1000):
13         l.append(i)
14         
15 def t3():
16     l = [i for i in range(1000)]
17 
18 def t4():
19     l = list(range(1000))
20 
21 
22 t1 = timeit.timeit("t1()", setup="from __main__ import t1", number=1000)
23 print("+ used time:{} seconds".format(t1))
24 print()
25 t2 = timeit.timeit("t2()", setup="from __main__ import t2", number=1000)
26 print("append used time:{} seconds".format(t2))
27 print()
28 t3 = timeit.timeit("t3()", setup="from __main__ import t3", number=1000)
29 print("[i for i in range(n)] used time:{} seconds".format(t3))
30 print()
31 t4 = timeit.timeit("t4()", setup="from __main__ import t4", number=1000)
32 print("list(range(n)) used time:{} seconds".format(t4))
33 print()
34 
35 # ----pop元素的效率----
36 
37 x = list(range(1000000))
38 pop_from_zero = timeit.timeit("x.pop(0)", setup="from __main__ import x", number=1000)
39 print("pop_from_zero used time:{} seconds".format(pop_from_zero))
40 print()
41 x = list(range(1000000))
42 pop_from_last = timeit.timeit("x.pop()", setup="from __main__ import x", number=1000)
43 print("pop_from_last used time:{} seconds".format(pop_from_last))

運行結果:

+ used time:3.7056619 seconds

append used time:0.46458129999999986 seconds

[i for i in range(n)] used time:0.18458229999999975 seconds

list(range(n)) used time:0.0845849000000003 seconds

pop_from_zero used time:0.5516430999999997 seconds

pop_from_last used time:0.0002724000000000615 seconds

list內置操做的時間複雜度

dict內置操做的時間複雜度

 

5. 數據結構

需求:咱們如何用Python中的類型來保存一個班的學生信息? 若是想要快速的經過學生姓名獲取其信息呢?

實際上當咱們在思考這個問題的時候,咱們已經用到了數據結構。列表和字典均可以存儲一個班的學生信息,可是想要在列表中獲取一名同窗的信息時,就要遍歷這個列表,其(最壞)時間複雜度爲O(n)。而使用字典存儲時,可將學生姓名做爲字典的鍵,學生信息做爲值,進而查詢時不須要遍歷即可快速獲取到學生信息,其時間複雜度爲O(1)。

咱們爲了解決問題,須要將數據保存下來,而後根據數據的存儲方式來設計算法實現進行處理,那麼數據的存儲方式不一樣就會致使須要不一樣的算法進行處理。咱們但願算法解決問題的效率越快越好,因而就須要考慮數據究竟如何保存的問題,這就是數據結構。

在上面的問題中咱們能夠選擇Python中的列表或字典來存儲學生信息。列表和字典就是Python內建幫咱們封裝好的兩種數據結構。

概念

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

Python給咱們提供了不少現成的數據結構類型,這些系統本身定義好的,不須要咱們本身去定義的數據結構就叫作Python的內置數據結構,好比列表、元組、字典。而有些數據組織方式,Python系統裏面沒有直接定義,須要咱們本身去定義實現這些數據的組織方式,這些數據組織方式稱之爲Python的擴展數據結構,好比棧,隊列等。

算法與數據結構的區別

數據結構只是靜態的描述了數據元素之間的關係。

高效的程序須要在數據結構的基礎上設計和選擇算法。

程序 = 數據結構 + 算法

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

抽象數據類型(Abstract Data Type)

抽象數據類型(ADT)的含義是指一個數學模型以及定義在此數學模型上的一組操做。即把數據類型和數據類型上的運算捆在一塊兒,進行封裝。

引入抽象數據類型的目的是把數據類型的表示和數據類型的運算實現,與這些數據類型和運算在程序中的引用隔開,使它們相互獨立。

最經常使用的數據運算有五種:

  • 插入
  • 刪除
  • 修改
  • 查找
  • 排序
相關文章
相關標籤/搜索