【算法】 算法和數據結構緒論

算法和算法分析python

  先說點可有可無的。初中的時候,知道有CS這門專門的學科存在的時候最開始的概念中CS就是等同於算法。這有多是由於當時的前桌是後來一代CS傳奇WJMZBMR。。由於當時看起來十分高端,再加上後來努力的方向徹底和CS不搭邊,因此對於算法二字一直心中抱着一種敬畏之情,以爲是整個CS中最乾的乾貨部分。後來決定入這行以後,個人領導對我說算法這東西雖然很高大上,可是在平常工做中咱們用的並很少(咱們部門主要作運維和DevOps,確實對這方面的需求不大)因此也就一直耽擱着。可是隨着深刻,以及在網上和各類場合查閱到愈來愈多的資料中接觸算法和一些算法術語愈來愈頻繁。我以爲是時候學習一下了。因此我就買了一本北大一位先生著的數據結構與算法~Python語言描述~,一方面想了解一點算法和數據結構的知識,另外一方面也能夠學習一下Python。這本書不是很厚,做者也說了沒有涉及很高深的知識,雖然我也不必定能學會學好,可是我想,努力一下試試看把,什麼都不學總比在牀上刷刷B站,玩玩遊戲要好一點。算法

■  問題,問題實例和算法編程

  要搞清楚算法的一些概念,區分這三個概念十分重要。問題對應的是一種需求,對應一種需求,人們能夠經過分析和推斷來抽象出一個計算機須要解決的問題。問題具備通用的特色,好比判斷一個正整數是否爲素數這就是一個問題。問題實例通俗點來講就是一個具體的問題,它很明確的指出一個問題的很具體的描述,通常具備正確解。好比1013這個正整數是否爲素數,相對於上面的那個問題,就是一個問題實例。顯然,一個問題反映出全部相關問題實例的共性。算法是對解決問題過程的一個嚴格的描述。由於算法是和問題對應的,因此這個問題的全部實例均可以用算法來求得解。好比設計一個判斷某個正整數是否爲素數的算法A,這個算法A對應的是上面的問題,固然把這個算法A套用在問題實例中,咱們就能夠得出1013以及其餘各類各樣的正整數是否是素數了。設計模式

 

  ●  算法具備的性質數據結構

  算法是一種對問題解決過程的具體描述,爲了使其嚴格有效,算法一般具備如下性質app

  有窮性(算法描述的有窮性):算法應該能夠用有限的語言,尤爲是語言中有限的祈使(或者計算機語言中就是指令)進行描述。運維

  可行性:算法中的的指令必須清晰明確,所描述的過程徹底能夠經過機器來機械地執行。函數

  肯定性:根據某個問題(一般也是以問題實例的形式給出,經過對問題實例的分析以及配合算法的測試來抽象出一個問題),算法將產生一個惟一肯定的動做序列,任何一個相關問題的實例經過這個肯定的動做序列以後,就能夠獲得相關解性能

  終止性(算法行爲的有窮性):對於任何實例,算法產生的動做序列都是有限的學習

  輸入/輸出:算法有明確的輸入和輸出

 

  ●  算法的描述形式

  算法能夠用天然語言描述,這樣對不瞭解計算機語言的人比較友好,可是一般比較囉嗦並且容易出現歧義

  若是用計算機語言描述算法,能夠作到很精確(由於最終算法就是以這種形式呈現到程序中的。)可是對於通常閱讀者,即便是懂計算機語言的人而言閱讀起來也須要一些力氣,能夠說對閱讀者不是很友好

  折中一下,用僞代碼的形式描述。僞代碼結合了計算機語言(一般用於邏輯結構的表示)和天然語言(對於具體的內容操做表示)。僞代碼描述的形式結合了天然語言的表達友好和機器語言的簡潔清晰。

 

■  算法設計與分析

  所謂算法設計,就是從一個問題出發,經過分析和思考來獲得一個可以解決問題的算法。算法設計中有一些常見的設計模式:

    枚舉法:列舉出問題全部可能的解並從中篩選出合適的解。這種方法能夠說是利用了計算機強大的計算性能。電腦比人腦高明之處就在於它能夠快速地重複大量類似或者相同的工做,以快速獲得結果。

    貪心發:根據問題的信息獲得部分解,認爲部分解能夠做爲解的一種或者把部分解逐步擴充獲得完整解。在問題比較複雜時適用が,一般找到的解並非最好的解

    分治法:把問題分解成一個個小問題,而後逐步解決這些小問題,組合每一個小問題的解獲得整個問題的解

    回溯法:當問題解決沒有清晰的路徑時,程序須要逐步試錯,當發現一種方法走不通的時候就要回溯到以前的路徑點來嘗試新的路徑

    動態規劃:當問題很難直接了當地求解,須要更多信息時,能夠在解決問題的過程當中逐漸積累信息,這些信息能夠爲後面問題解決過程所用,然後面的這些過程又能夠進一步地積累到更多的信息

    分支限界法:能夠看作是回溯法的一種優化,在搜索過程當中可能會獲得一些沒有用的信息,就把這些信息給刪除以減少求解成本

  以上算法設計模式,並非嚴格推導出來的,而是前人在無數的實踐中的一些總結,固然這些描述也是很是抽象的,光看下來可能無法知道任何有用的信息。可是須要記住的是,真正的算法設計過程一般須要綜合考慮多種設計模式。

 

  另,算法實現爲一個程序以後就須要開始運算,而運算做爲處理信息的一種過程確定是要有運算消耗的。好比時間上的消耗和空間上的消耗。這種消耗除了跟算法有關,跟硬件狀況,運行環境,實現方式(哪一種語言)等等。在以上條件都相同的狀況下,算法就肯定一個程序的消耗。消耗越小的算法,其運行效率固然也就越高了。

  當咱們設計出一個算法以後,咱們就須要分析這個算法是否是夠高效。在有些狀況下,由於計算機的高效的特性,算法是否高效可能顯得不是那麼有意義,可是在更多時候,極可能決定了算法有沒有存在的價值。好比一個算法要花三天算出明天的天氣預報和三小時算出明天的天氣預報意義徹底不同。爲了衡量算法是否是高效,咱們還須要一種度量。

  ●  算法度量的單位和方法

  在計算過程當中,硬件每執行算法中的一個操做所帶來的時間上和空間上的消耗都是不一樣的,而爲了算法度量可以有必定通用性(比較不一樣操做算法的效率),在制定算法度量的時候就須要必定抽象,好比下面的兩條假設:

  1. 所用的計算設備準備了一組儲存單元,每一個單元都能保存固定的一點有限數據(以此標準化空間上的消耗)

  2. 機器可以執行的一次基本操做都是消耗一個單位時間(以此標準化時間上的消耗)

  假設中提到的儲存單元的大小,以及單位時間的長短,可能根據硬件,環境等條件不一樣而不一樣,可是這不是算法度量須要考慮的。在算法比較中,一般默認是比較除了算法以外,其餘條件都徹底相同的兩個程序的執行效率。因此能夠藉助上面兩條假設把算法度量抽象化,標準化。

  雖然算法是針對地解決問題的,可是機器不可能看得懂一個問題的描述,因此一般算法度量仍是得以具體的問題實例來進行。這就帶出一個概念,問題規模。好比解1013是否爲素數和10331310131是否爲素數這兩個問題,顯然二者能夠套用同一套算法,可是二者的消耗徹底不一樣。對於這樣一種算法,到底算高效仍是不高效,並非經過一個問題實例的具體消耗能決定的。因此,算法的度量一般是一種計算資源消耗和問題規模相關的函數關係。若是問題規模很小,不論用哪一種算法的消耗都差很少,且在能夠接受的範圍內,那麼算法度量就顯得不是那麼有意義。而當問題規模愈來愈大時,若是計算消耗增加得愈來愈快,那麼就能夠說算法的效率不是太好,應該避免。問題規模在上面求素數那兩個實例中,能夠認爲是數的大小,或者數的位數等等,通常來講只要有問題實例的一個統一的度量,具體這個度量是什麼並非很重要。總之能看出來哪些問題規模較大哪些較小便可。

  另外還須要注意的一點,即便是規模相同的問題實例,在有些算法中消耗也是不一樣的。好比判斷1013和1012是不是素數的話,好比在算法中最開始添加一個判斷:若是是偶數就直接返回否,這樣二者消耗就相差不少了。對於這種狀況,其實對於規模相同的問題實例,咱們一般關注的是最壞的狀況下算法的消耗(有時候也會關注平均消耗),可是不太會關注比較樂觀的狀況下的消耗。

  ●  算法複雜度

  算法複雜度就是一種算法的度量方法。如上所說,對於抽象的算法一般沒法給出精確地度量,因此要作的是估計算法的複雜性,而算法複雜性量化一點說就是算法的消耗處在的量級(由於不論在何種外部條件下,算法度量中的單位時間和空間都是很小的,因此多一個少一個不是頗有所謂)。在估算量級的過程當中,常量因子能夠認爲沒有什麼價值,好比100n**2和3n**2都是n**2量級的(n是問題規模的描述)。這裏借用了微積分中經常使用的無窮小的概念,並採起了無窮小的記法f(n) = O(g(n))。f(n)就是算法複雜度這個算法度量(一個消耗關於問題規模的函數),而g(n)是相似於n**2,logn,n,1(常量函數)的一個關於問題規模的n的函數。把g(n)記入大O代表算法複雜度f(n)隨着n的增加,其增加速度受到g(n)的限制。兩個算法,只要其g(n)相同,就能夠認爲兩個算法的量級相同,就認爲二者的複雜度基本同樣。

  經常使用的g(n)有1,logn,n,nlogn,n**2,n**3,2**n。這幾個函數從前到後其增加速率逐漸變快。具備這些g(n)的算法複雜度也被稱爲常量複雜度,對數複雜度,平方複雜度,指數複雜度等等。假如一個算法A1是對數複雜度而A2是平方複雜度,一般而言一樣規模的問題實例用A1算法進行運算的消耗要遠小於用A2算法計算的消耗(固然這只是一般,上面也說了算法度量只是關注最壞狀況,假如某個實例恰好是A2的樂觀狀況那麼可能A2很快就能算出來了)

  ●  算法分析

  算法分析就是經過一個已知的算法來得出其複雜度的過程。以考慮時間開銷的時間複雜度爲例,從算法層面看,一個普通的程序一般包含了基本操做,順序結構,循環結構和選擇結構這幾種結構。

  基本操做的複雜度一般認爲是常量複雜度,好比賦值,四則運算,以及這些的組合都是基本操做。

  順序結構是指多個操做按順序複合的狀況。一般其複雜性是每一步操做複雜性的總和。

  循環結構的複雜度是循環頭的複雜度乘以循環體的複雜度。

  選擇結構的複雜度是各個選擇子句中最大複雜度(這裏又體現出考慮最壞狀況)

  好比這樣一個Python程序:

#把n階矩陣m1和m2的乘積存入矩陣m
for i in range(n):  #O(n)
  for j in range(n):  #O(n)
    x = 0.0  #O(1)
    for k in range(n):  #O(n)
      x = x + m1[i][k] * m2[k][j]  #O(1)
    m[i][j] = k  #O(1)

  其複雜度T(n)是:

T(n) = O(n)*O(n)*(O(1)+O(n)*O(1)+O(1))

= O(n)*O(n)*O(n)

= O(n**3)

 

  能夠看到python中的for i in range(n)這樣的語句,由於是遍歷一個長度爲n的列表,其複雜度n個O(1)相加,即O(n)。循環頭的O(n)再拿去乘以循環體的複雜度,嵌套循環則用括號在算式中也嵌套。在獲得算式後面的化簡過程遵循無窮小之間的運算規律。好比括號中只考慮階最高的無窮小,相加的低階無窮小被忽略了,相乘的無窮小則其參數互相相加。

  最終能夠獲得這條算法的複雜度是立方複雜度。

 

■  Python的複雜度

  上面所說的算法複雜度是泛泛而談,具體到Python中又有一些特殊的狀況。好比Python做爲一門比較高級(相對底層而言)的語言,它已經提供了不少包裝好了的「基本操做」。在用這些操做的時候,有時候咱們會覺得咱們作的是一個基本操做可是實際上有多是一個複雜度並不是爲O(1)的操做。下面是一些簡單的說明,具體的分析留到後面具體的章節中

  基本運算和賦值是基本操做,複雜度是O(1)

  序列的複製和切片操做是O(n),跟序列的長度有關

  list,tuple的元素訪問、賦值和修改都是O(1)

  構造一個空的對象是O(1),若是像是list,str這種類型構造時若是指定了長度爲n的內容那麼就是O(n)

  dict加入新鍵值對最壞狀況下是O(n)可是平均狀況下的複雜度是O(1)

  以上覆雜度都是針對時間消耗而言。對於空間消耗須要注意的是

  Python中對於各類組合元素(一般是指str,list,tuple,dict等python自帶的高級一點的數據類型)都沒有預設最大元素個數。但在實際使用中,從內存角度看元素個數長度只會增不會減。好比li = [1,2,3]以後li中確實是有3個元素的長度。若是li.append(4)以後就是4個長度。這很好理解。可是若是此時del(li[3])以後,雖然len(li)變成了3可是內存中的li對象依然保持4個元素 的長度,這是須要注意的

 

Python中的數據結構

■  什麼是數據結構

  書上有很大一堆比較學術性的解釋。在個人體驗中,我認爲所謂數據結構就是人爲地規定一些數據格式來方便對問題的抽象和編程。從集合論來看,通常而言,一個數據結構D = (E,R)。其中E表示一個數量有窮的數據集合而R表明E中這些數據之間的某種關係。換言之,一個具體的數據結構就是要有具體的數據和這些數據之間的邏輯關係。

  一些典型的數據結構有:

  集合結構:其數據元素之間沒有明確的關係指定,即R是一個空集,這樣的數據結構就是把元素包裝成一個總體,是最簡單的一類數據結構

  序列結構:數據元素之間有明確的前後關係,存在一個排位在最前的元素。除了最後的元素以外每一個元素都有惟一的後元素。序列結構還能夠細分紅簡單線性結構,環形結構和ρ型結構

  層次結構:其數據元素分屬於一些不一樣的層次,一個上層元素能夠關聯着一個或者多個下層元素,關係R造成一種層次性。

  樹形結構:屬於層次結構的一種。

  圖結構:數據元素之間能夠有任意的互相關聯,其R十分複雜且靈活多變,是一類複雜的數據結構。其實前面全部的數據結構均可以認爲是圖結構的一種簡化或限制的狀況

 

  根據數據結構的不一樣特色,還能夠細分數據結構結構性數據結構和功能性數據結構。結構性數據結構(Python中如list,str,tuple)等,結構性數據結構指出的是一種有具體結構要求的數據結構。功能性數據結構沒有結構上死的規定,能夠看作是容器同樣支持存放數據,而後利用其特性進行一些運算,功能性數據結構的例子有棧,隊列,優先隊列等等。

■  內存單元和地址

  (不知道爲何這部份內容要放在數據結構中。。)

  內存的基本結構是一批線性排列的數據單元,每一個單元有惟一的編號被稱爲單元地址,對內存中的數據進行訪問必需要知道相關單元的地址。在許多計算機中,能夠一次性存取多個單元的內容,在如今常見的64位計算機中,CPU一次能夠存取8個字節的數據,也就是說能夠一次性訪問8個數據單元。

  正如上面提到過的大多組合數據類型存取值是一個O(1)的操做,這也就說明了,基於單元地址的對內存中一個存儲單元的訪問是一個O(1)的操做,這和單元所在位置,內存總體大小等無關。

■  Python對象和數據結構

  ●  python中的變量和對象

  對於初學Python的人來講這兩個概念常常容易搞混,其實Python在數據存儲的本質上來講和C,Java等語言是不一樣的。在Python中,給變量約束一個值看似和C中差很少,可是實際上,python首先把這個值構形成一個對象存儲在內存中,而後把這個內存中對象的地址約束給相應的變量。因此在Python中,咱們不須要指出某個變量的類型和它應該有的長度,由於不管是什麼變量,都存的是一個地址,全部變量所須要的空間大小是同樣的。而那個地址指向的內存儲存單元(或者以該單元爲開始的一片內存空間中)儲存的纔是真的數據。這種變量的實現方式被稱爲變量的引用語義。而像C同樣把值直接存在變量的儲存區的作法被稱爲變量的值語義。

  在Python中,經過變量來取得一些具體數據的操做也是O(1)的因此這方面的消耗並不比低級語言大不少。

  ●  Python中對象的表示

  表示是指爲了讓電腦可以更好的理解邏輯數據的構造的數據結構。Python中的對象的表示實際上是已經設計完成,不須要咱們太多關心的,可是瞭解一下有利於咱們更好地進行工做。

  Python語言的實現基於一套精心設計的連接結構,變量與其值對象的關係經過連接的方式實現,對象之間的聯繫一樣也經過連接。一個複雜的對象內部也可能包含了幾個子部分。相互之間經過連接創建聯繫,例如一個list中包含了10個字符串的話,在實現中,這個list在內存中其實保存了這10個字符串各自的連接關係。

  Python中的組合對象能夠是任意大規模的,每一個對象須要的儲存單元數量不一樣,還能夠有內部的複雜結構。對於這樣一種複雜的狀況,要有效地安排,管理內存是比較麻煩的。不過好在Python自帶了一套存儲管理系統,負責管理可用內存,釋放再也不使用的內存,安排各類對象的存儲以實現靈活有效的內存管理。程序中要求創建對象時,管理系統會爲它安排存儲;當某些對象再也不使用時,就回收其佔有的內存。存儲管理系統屏蔽了具體內存使用的細節,減輕了編程人員的負擔。

相關文章
相關標籤/搜索