回顧
? ? ? 上一篇文章主要對書中的第一章節數據結構的緒論進行了總結和自己的一些理解,包括數據,數據元素,數據項還有數據對象,數據具有兩個前提一個是可以輸入到計算機內,第二個是能被計算機處理,然后數據元素就對應一張數據庫表的一條記錄,數據項就是數據庫表的列,數據對象就是相同性質的數據元素的集合比如整張表就可以看做一個數據對象,那么在這種情況下,數據即可以是表中某一行某一列的內容,也可以是一個數據元素,更可以是一個數據對象。當然,有一點需要注意的是,雖然數據項是數據不可分割的最小單位,但我們實際情況下討論數據的著眼點其實是數據元素。
? ? ? 接著,總結了一下數據結構的概念,數據結構是相對于數據元素來說的,就是一個數據對象之間,數據元素的相互關系和組織形式,分為邏輯結構和物理結構(存儲結構)。邏輯結構是指數據對象中數據元素之間的相互關系,是面向問題的;物理結構是指數據元素之間的邏輯結構在計算機中的存儲形式,是面向計算機的。“數據的存儲結構應正確反映數據元素之間的邏輯關系,這才是最為關鍵的”,“數據元素的存儲關系并不能反映其邏輯關系”這兩句話總結了邏輯結構和物理結構的關系,需要思考一下。然后,邏輯結構分為四種:集合結構,線性結構,樹形結構,圖形結構;物理結構分為兩種:順序存儲結構,類似于排隊,數組在內存中的存儲結構就是順序存儲,鏈式存儲結構,是把數據元素存放在任意的存儲單元里,這組存儲單元可以是連續的,也可以是不連續的,那么怎么表示數據元素之間的邏輯結構呢,指針就應運而生了,指針:存放數據元素的地址,通過指針就可以找到數據元素在內存中的位置。
? ? ? ? 最后還稍微講了一下數據類型(指一組性質相同的值的集合及定義在此集合上的一些操作的總稱),抽象數據類型(我們對已有的數據類型進行抽象,就有了抽象數據類型。指一個數學模型及定義在該模型上的一組操作),以及抽象(指取出事物具有的普遍性的本質)的概念。
??????? 好了,接下來就是書中的第二章:算法。
算法
?????? 算法:是解決特定問題求解步驟的描述,在計算機中表現為指令的有限序列,并且每條指令表示一個或多個操作。
??????? 怎么理解呢?很簡單,比如我們做一道數學題,會有多種解決過程,那么每一種完整的解決過程就可以稱為一個算法,那么指令就是解決過程中的每一步,每一步需要的東西可能又會由一個或多個操作得出。書中列舉了一個最簡單也是最經典的案例,高斯算法。高斯算法:將1到n的求和表示為(n+1)*n/2,相對于最基本的求和(1+2+3+..+n)簡單了很多。這其實就是兩個算法的比較,稍候會講解。
?????? 那么算法和數據結構到底是什么關系呢?為什么說到數據結構就會說到算法?書中打了一個很經典的比方,如果一部話劇《梁山伯與祝英臺》,最后變成了《梁山伯》會怎么樣,當然還是可以演下去,但是演出質量就會下降好幾個檔次。那么數據結構和算法也是如此,如果單單講數據結構也沒有問題,但是可能講完也不知道數據結構到底有什么用。這里怎么說呢,因為我也是小白,所以倒沒有特別深的理解,所以這里就引用書中的介紹,可能當我學完整本書或者對數據結構和算法有更深的理解的時候,我會過來補充上我自己對于數據結構和算法兩者之間的理解。=。=
?????? 算法具有五個基本特性:輸入、輸出、有窮性、確定性、可行性。
??????? 輸入輸出:算法具有零個或多個輸入,至少具有一個或多個輸出。算法可以沒有輸入(比如打印“hello world”,應該是所有計算機相關人員最熟悉的一句輸出了),算法是一定需要輸出的,輸出的形式可以有多種,可以是打印輸出,也可以返回一個或多個值等等。
??????? 有窮性:指算法在執行有限的步驟之后,自動結束而不會出現無限循環,并且每一個步驟在可接受的時間內完成。需要特別注意的是可接受的時間,這里有窮的概念并不是純數字意義的,而是在實際應用中合理的,可以接受的“邊界”,如果你一個算法跑20年才會出來一個正確的結果,雖然是有窮了,但是媳婦都熬成婆了,還有什么意義呢... ????
??????? 確定性:算法的每一個步驟都具有確定的含義,不會出現二義性。算法在一定條件下,只有一條執行路徑,相同的輸入只能有唯一的輸出結構。算法的每個步驟被精確定義而無歧義。
??????? 可行性:算法的每一步都必須是可行的,也就是說,每一步都可以通過執行有限次數完成??尚行砸詾橹惴梢赞D換為程序上機運行,并得到正確的結果。
??????? 那么對于同一個問題的多種算法,什么才是一個好的算法呢?接下來就來了解一下好的算法需要的特征。
?????? 正確性:一個好的算法最基本的要求就是正確性,正確性是指算法至少應該具有輸入、輸出和加工處理無歧義性、能正確反映問題的需求、能夠得到問題的正確答案。算法的正確性大致分為四個層次:1.算法程序沒有語法錯誤;2.算法程序對于合法的輸入數據能夠產生滿足要求的輸出結果;3.算法程序對于非法的輸入數據能夠得出滿足規格說明的結果;4.算法程序對于精心選擇的、甚至刁難的測試數據都有滿足要求的輸出結果。因此算法的正確性大部分情況下都不可能通過程序來證明,而是用數學方法證明的。證明一個復雜算法在所有層次上都是正確的,代價非常昂貴。所以一般情況下,我們把層次3作為一個算法是否正確的標準。
?????? 可讀性:算法設計的另一個目的是為了便于閱讀、理解和交流??勺x性高有助于人們理解算法,晦澀難懂的算法往往隱含錯誤,不易被發現,并且難以調試和修改。我們寫代碼的目的,一方面是為了讓計算機執行,但還有一個重要的目的是為了便于他人閱讀,讓人理解和交流,如果可讀性不好,時間長了自己都不知道寫了什么。所以可讀性好壞是算法很重要的標志。
?????? 健壯性:當輸入數據不合法時,算法也能做出相關處理,而不是產生異常或莫名其妙的結果。
?????? 時間效率高和存儲量低:時間效率值得是算法的執行時間,存儲量指的是算法在執行過程中需要的最大存儲空間。設計算法應盡量滿足時間效率高和存儲量低的要求。
?????? 那么如何度量一個算法的時間效率呢?也就是執行時間。俗話講是騾子是馬,拉出來溜溜。所以第一種方法就是事后統計方法。
?????? 事后統計方法:通過設計好的測試程序和數據,利用計算機計時器對不同算法編制的程序的運行時間進行比較,從而確定算法效率的高低。
?????? 但是這種度量方法實際上是很不科學的,理解一下,首先我們需要編制不同的算法,萬一這種算法很糟糕,那不是浪費時間和精力嗎?其次,時間的比較依賴計算機硬件和軟件等環境因素,有時會掩蓋算法本身的優劣;再次,算法的測試數據設計困難,并且算法的優劣往往還與測試數據的規模有關。綜上所述,事后統計方法,我們不予采納。
?????? 基于此,我們很容易就想到,我們可以對算法在計算機編制前,先進行估算。也就是事前分析估算方法。
?????? 事前分析估算方法:在計算機程序編制前,依據統計方法對算法進行估算。
?????? 從事后統計方法的分析中,我們可以得出一個算法的執行時間,取決于算法本身的好壞,編譯產生的代碼質量(軟件因素),機器執行指令的速度(硬件因素),以及輸入的規模,那么拋棄軟件因素和硬件因素,比較一個算法的好壞,就是比較在同等規模下,算法的執行時間效率。
?????? 那么,回到開篇提到的高斯算法和普通求和算法,將這兩種算法寫成計算機語言,如下:
普通求和:??????????????????????????????????????
int i, sum = 0, n =100;????????????????????? /* 執行了1次*/
for(int i = 1;i <= n;i++){? ? ? ? ? ? ? ? ? ? /* 執行了n+1次*/
? ? ? sum = sum + i;? ? ? ? ? ? ? ? ? ? ? ???? /* 執行了n次*/
}
printf("%d",sum);? ? ? ? ? ? ? ? ? ? ? ? ? ?? /* 執行了1次*/
高斯算法:
int sum = 0,n = 100;? ? ? ? ? ? ? ? ? ? ? ? ? ? /* 執行了1次*/
sum = (i+n)*n/2;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /* 執行了1次*/
printf("%d",sum);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? /* 執行了1次*/
?????? 顯然普通求和算法,執行了1+(n+1)+n+1 = (2n+3)次,而高斯算法,執行了1+1+1 = 3次。事實上,兩條算法的開始變量定義,和結尾的輸出語句是一樣的,所以我們只關注中間的代碼,我們把第一種方法的for循環看做一個整體,忽略頭尾循環判斷的開銷,那么兩個算法其實就是n次與1次的區別。算法差距顯而易見,且隨著n的增大,差距越來越明顯。
?????? 這里需要理解的是,我們不關心編寫程序所用的程序設計語言,也不關心程序將跑在什么計算機上,我們只關心它所實現的算法。這樣,不計哪些循環索引的遞增和循環的終止條件,變量聲明和打印結果等操作,最終,在分析程序的運行時間時,最重要的是把程序看成是獨立與程序設計語言的算法或一些列的步驟。
?????? 接下來,書中拋出了一個概念,函數的漸進增長。
????? 函數的漸進增長:給定兩個函數f(n)和g(n),如果存在一個整數N,使得對于所有的n>N,f(n)總是大于g(n),那么我們就說f(n)的增長漸進快于g(n);
??????? 那么漸進增長怎么理解呢?我們根據字面意思理解一下,我們都知道函數可以表示在一個坐標系中,當兩個函數的兩條線,慢慢靠近,直到某一點,之后,其中一條函數線一直都大于另一條函數線。
?????? 那么,我們判斷一個算法好不好,我們可以對比幾個算法的關鍵執行次數函數的漸進增長性,基本就可以分析出,某個算法,隨著n的增大,它會越來越優于另一個算法,或者越來越差于另一個算法。其實這就是事前估算方法的理論依據,通過算法時間復雜度來估算算法時間效率。
?????? 算法時間復雜度:在進行算法分析時,語句總的執行次數T(n)是關于問題規模n的函數,進而分析T(n)隨n的變化情況并確定T(n)的數量級。算法的時間復雜度,也就是算法的時間量度,記作:T(n)=O(f(n))。它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸進時間復雜度,簡稱時間復雜度。其中f(n)是問題規模n的某個函數。
?????? 這樣用大寫O()來體現算法時間復雜度的記法,我們稱之為大O記法。一般情況下,隨著n的增大,T(n)增長最慢的算法為最優算法。
?????? 那么如何分析一個算法的時間復雜度呢,也就是如何推導大O階呢?如下:
推導大O階:
1.用常數1取代運行時間中的所有加法常數;
2.在修改后的運行次數函數中,只保留最高階項;
3.如果最高階項存在且不是1,則去除與這個項相乘的常數。
得到的結果就是大O階。
?????? 哈哈,是不是有點暈?下面我就來簡單介紹一下書中的推導大O階的例子,相信會很好理解。
常數階:O(1)
再回想一下高斯算法的程序表示:
高斯算法:
int sum = 0,n = 100;? ? ? ? ? ? ? ? ? ? ? ? ? ?? /* 執行了1次*/
sum = (i+n)*n/2;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /* 執行了1次*/
printf("%d",sum);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??? /* 執行了1次*/
????? 很明顯,這個算法的運行次數函數是f(n)=3。根據我們推導大O階的方法,第一步就是把常數3改為1。然后發現,它根本沒有最高階項,所以這個算法的時間復雜度為O(1),也就是常數階。
線性階:O(n)
for(int i = 0;i<n;i++){
????? /* 時間復雜度為O(1)的程序步驟序列*/
}
?????? 因為有n次循環,所以這個算法的時間復雜度就是O(n),也就是線性階。
對數階:O(logn)
?????? 思考一下,下面這段代碼的時間復雜度是多少?
int count = 1;
while (count<n){
?? count = count * 2;
? /* 時間復雜度為O(1)的程序步驟序列*/
}
????? 這段代碼的意思就是有多少個2相乘后大于n就會結束循環,用函數來表達就是:2的x次方 = n,那么x=logn,所以這個循環的時間復雜度為O(logn)。
平方階:O(n^2)
看下面代碼:
int i , j;
for(i = 0; i < n ; i ++)
{
??????? for(j = 0; j < n; j ++)
??????? {
? ? ? ? ? ? ? ?? /*時間復雜度為O(1)的程序步驟序列*/ ??????
???????? }
}
?????? 這是一個嵌套循環,其中內循環,剛才已經介紹了,就是O(n),那么外循環,就是O(n)的時間復雜度再循環n次,那么整個的時間復雜度就是O(n*n)=O(n^2),也就是平方階。
總結:對于循環來說,循環的時間復雜度,就是循環體的時間復雜度乘以該循環的次數。
變一下,下面的循環,時間復雜度又是多少呢?
int i,j;
for (i = 0;i<n;i++)
{
?????????? for(j = i;j<n;j++)?????????????? //注意這里是j=i,不是j=0
????????? {
? ? ? ? ? ? ? ? ? ? ? /*時間復雜度為O(1)的程序步驟序列*/
?????????? }
}
分析一下這個嵌套循環,當i=0時,內循環執行了n次,當i = 1時,內循環執行了n-1次,...,一次類推,當i= n-1時,內循環執行了1次,所以總的執行次數是:
n+(n-1)+(n-2)+...+1 = n*(n+1)/2 = n^2/2+n/2;
根據大O階推導,第一條沒有加法常數,第二條,只保留最高階項,所以保留n^2/2,第三條,去除這個項相乘的常數,也就是去除1/2,最終這端代碼的時間復雜度為O(n^2)。
?????? 總結:我相信大家如果看下來都能夠理解大O推導。其實理解大O推導不算難,難的是對數列的一些運算,更多的是考察數學知識和能力。(嚇人~~)
那么常見的時間復雜度有哪些呢?
常數階:O(1);????????? 線性階:O(n);?????? 平方階:O(n^2);???????? 對數階:O(logn);???
nlogn階:O(nlogn);??????? 立方階:O(n^3);????? 指數階:O(2^n);
常見的時間復雜度所耗費的時間從小到大依次是:
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
?????? 我們有n個隨機數字,打亂且不重復,要從中找到我們想要的數字,那么最好的情況就是第一個就是我們想要找的數字,那么這個時候的時間復雜度為O(1),最壞的情況,可能那個數字就在這個隊列的最后,這個時候的時間復雜度為O(n)。
?????? 最壞情況運行時間是一種保證,那就是運行時間不會更壞了。在應用中,這是一種最重要的需求,通常,除非特別情況,我們提到的運行時間都是最壞情況的運行時間。
?????? 而平均運行時間是所有情況中最有意義的,因為它是期望的運行時間。
?????? 一般沒有特殊說明的情況下,我們說的時間復雜度都是最壞時間復雜度。
?????? 第二章,終于整理完了,這篇整理的有點亂,有一些函數的漸進比較的圖表,我沒辦法弄上來,所以可能中間會有點迷糊(我估計后面的內容也會有很多這種圖表,所以我需要思考一下這些圖表怎么拿上來)。但其實很明顯的感覺出來,第二章算法,更多的是需要你的數學水平,需要進行一些推導和運算。然后對于我來說,雖然我高考數學還挺高的(130+),并且由于我的專業信息與計算科學專業也要學超級多數學(傳說有這個專業的其他學校,這個專業都是數理學院的,我們學校是計算機學院,導致我們上大課經常跟數理學院的幾個班一起上QAQ);但是雖然如此,我真的覺得對不起我的歷任數學老師,我真的忘記的一干二凈,啊,難過,原地爆炸。。。在這里勸解一些,讀書的朋友,如果想做計算機相關的,不管是考研還是工作,真的數學還是挺重要的,請以我為戒。
??????? By the way,杭州最近的天氣早上起床會有點小冷,希望大家注意身體,不要感冒。
Better Late Than Never!
努力是為了當機會來臨時不會錯失機會。
?????????? 共勉!