那個讓你頭大的數據結構,用Python學會變簡單嗎?

前言

學完了基本的語言語法之後,接下來就應該學習數據結構這個讓人腦殼大的的東西了,如果是計算機專業的同學一般都是在大二上學期學習數據結構這門課程,且肯定是學習的用C語言或者C++來實現的版本。用C語言實現的數據結構中一個非常重要的工具就是指針,但是大家在學C的時候肯定看到指針就頭痛,我也一樣,所以我的數據結構算是白學了,會做題就是不會敲代碼。當我在學Python數據結構之前就在想,Python當中並沒有指針這個東西啊,該怎麼像C語言那樣來實現鏈表、二叉樹這些需要存儲結點地址才能實現的數據結構呢?

什麼是數據結構

在解決上面這個疑惑之前,我們首先要了解什麼是數據結構?所謂數據結構啊就是研究數據之間的關聯和組合形式,總結其中的規律,然後找出有用的結構,並將這些結構運用於數據的組織中,實現更高效的程序。說白了就是數據之間的的關係形式,像線性表、二叉樹、圖這些數據結構就是表示數據之間的關係,數據怎麼在計算機中存儲、我們怎麼高效的表示和利用這些數據。當然我也不可能講的很詳細,具體的東西大家可以看看書。

爲什麼要學數據結構

這裏我簡單打個比方哈,如果將寫好的程序比作戰場,我們程序猿就是領軍作戰的將領,而我們所寫的代碼就是士兵和武器。那麼數據結構這些東西是什麼?我想肯定是兵法!
如果我們沒有兵法在戰場上拼刺刀,如此這般我們可能會勝利,可能會失敗,即便勝利也會付出慘重的代價。我們寫程序也是一樣,解決一個問題的方式有很多種,有的時候雖然我們想到辦法解決了這個問題,但是可能沒有注意到程序的時間開銷和效率問題,導致性能低下;有的時候雖然憑藉某些別人開發的工具解決了效率問題,但是碰到其他的問題時又不知道如何解決,就知道暴力搜索啥的。
如果我們知道兵法,打仗時便課胸有成竹;同樣如果我們學好了數據結構,寫代碼的時候就能遊刃有餘,找到問題的根源。
當然,如果我們只知兵法而不知實際情況就可能會成爲紙上談兵的趙括;同樣,如果我們只去學數據結果而不去運用他,不去解決實際問題,不去寫代碼,不去刷題,到最後也是無用功。數據結構只是我們程序員的基本功,冰凍三尺非一日之寒,需要不斷地努力學習積累。

時間複雜度和空間複雜度

時間複雜度

時間複雜度指的是代碼的運行時間快慢的量級,有的代碼運行時間快,那麼他的時間複雜度就高;有的代碼運行時間慢,那麼他的時間複雜度就低。下面請看這一段代碼就是來直觀的描述代碼的時間複雜度的:a+b+c=1000,a2+b2=c2求解滿足這樣要求的所有a,b,c的值。

import time
start_time = time.time()
for a in range(0, 1001):
    for b in range(0, 1001):
        for c in range(0, 1001):
            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("%d" % (end_time - start_time))
print("finished")

運行時間大家可以看到是548s,當然有點誇張,因爲我這是在虛擬機裏面運行的所以有點慢,正常9代i7的話應該幾十秒左右就解決了。
在這裏插入圖片描述
如果我們換個方式來寫這個代碼會有啥反應呢?

import time
start_time = time.time()
for a in range(0, 1001):
    for b in range(0, 1001):
        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("%d" % (end_time - start_time))
print("finished")

在這裏插入圖片描述
大家可以看到這裏的運行時間只有1s,前面是548s,代碼只是稍微改了一點,運行時間卻相差了幾百倍,由此可見代碼的效率是有多麼重要,這裏只是求1000以內的,要是後面幾萬甚至更大的數字,代碼孰優孰劣一對比便知。這就是我們學習數據結構的意義啊!高效的數據組成形式,其反應的時間就能節省幾百倍,大大降低了延遲,你總不想以後開發的應用程序在那裏等着幾百秒不動,被用戶罵吧。

時間複雜度的計算規則

  • 基本操作,即只有常數項,認爲其時間複雜度爲O(1)。
  • 順序結構,時間複雜度按加法計算。
  • 循環結構,時間複雜度按乘法計算。
  • 分支結構,時間複雜度取最大值。
  • 判斷一個算法的效率時,往往只需要關注操作數量的最高項,其他次要項和常數項可以忽略。
  • 沒有特殊說明,我們分析是都是計算的最壞時間複雜度。

常見時間複雜度

執行函數舉例 時間複雜度
12 O(1)
2n+3 O(n)
3n2+2n+3 O(n2)
5lg2n+20 O(logn)
2n+3nlgn+100 O(nlogn)
3n3+2n+3 O(n3)
2n O(2n)

在這裏插入圖片描述)
這個曲線圖就可以明顯的感受到時間複雜度的快慢排序是:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)。

遞歸算法的時間複雜度

其實循環語句的時間複雜度大都可以看成是簡單的遞歸算法,但是比較簡單,不用這裏的方法也可以解決。遞歸算法通常具有如下的模式:

def recur(n):
    if n == 0:
        return g(...)
    somework
    for i in range(a):
        x = recur(n/b)
        somework
    somework

也就是說,n值爲0時直接得到結果,否則原問題將歸結爲a個規模爲n/b的子問題,其中a和b是由具體問題決定的兩個常量。另外,在本層遞歸中還需要做一些工作,上面描述的用somework表示,其時間複雜度可能與n有關,設爲O(nk)。這個k也應該是常量,k=0表示這部分的工作與n無關。這樣就得到了遞歸方程:T(n)=O(nk)+a*T(n/b)
有如下結論:

  • 如果a>bk,那麼T(n)=O(n(logba))。
  • 如果a=bk,那麼T(n)=O(nklogn)。
  • 如果a<bk,那麼T(n)=O(nk)。
    這些結論可以涵蓋很大一部分遞歸定義算法的時間複雜度情況。
    注意,因爲這裏要求a,b,k爲常量,由此有關結論不能處理行列式的遞歸運算,在這裏規模爲n的問題歸結爲n個規模爲n-1的問題。

空間複雜度

在程序裏使用任何類型的對象都要付出空間的代價,建立一個元組,至少要佔用元素個數個空間。如果一個表的元素個數與問題規模線性相關,建立他的空間付出至少爲O(n)(如果元素是新建的,還要考慮元素本身的存儲開銷)。
相對而言,列表和元組是比較簡單的數據結構。字典需要支持快速查詢等操作,其結構更加複雜,包含n個元素的字典至少需要佔用O(n)的存儲空間。
注意:
Python的各種組合數據對象都沒有預設最大元素個數,在實際使用中,這些結構能根據元素個數的增長自動擴充存儲空間。從空間佔用的角度看,其實際存儲空間在數據的存續期間可能變大,但通常不會自動縮小(即使後來元素變得很少了)。舉個例子,假設程序裏面建立了一個列表,而後不斷地加入元素導致表變大,而後又不斷地刪除元素,表中的元素又變得很少,但佔用的存儲空間並不減少。
不過在當今時代,隨着大存儲容量的硬盤的開發,以空間換時間的做法還是值得的,也不失爲一個好辦法,在我們目前的學習中,時間複雜度可以不用考慮,畢竟我們寫的代碼是不會導致內存不夠用的,但是也不是說硬盤很大就隨便瞎用,在你已知的方法中還是要選擇空間複雜度小的結構。

最後

今天就記錄這麼多了,後續還會繼續更新的!

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