版權聲明:本文出自汪磊的博客,未經作者允許禁止轉載。
一、前言
項目進入收尾階段,忙忙碌碌將近一個多月吧,還好,不算太難,就是麻煩點。
數據結構與算法這個系列早就想寫了,一是梳理總結,順便逼迫自己把一些模模糊糊的概念弄明白,最重要的我覺得數據結構與算法平時我們總是接觸,什么ArrayList,LinkedList,HashMap...這些我們總是接觸,但是在使用的時候有多少是會考慮一下性能方面問題,是不是你也不管三七二十一上來就是ArrayList,往里面扔數據唄,反正運行一般沒問題,就像寫文章一逗到底,一個字:爽。
此外,對於個人長遠發展也十分重要的,基礎扎實,不懼任何所謂的新技術。好了,本篇比較簡單,介紹一下時間與空間復雜度概念,以及基本數據結構:數組,鏈表以及哈希表。
二、時間復雜度與空間復雜度概念
時間復雜度
在進行算法分析時,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化情況並確定T(n)的數量級。算法的時間復雜度,也就是算法的時間量度。記作:T(n)=O(f(n))。它表示隨問題n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸進時間復雜度,簡稱為時間復雜度。其中,f(n)是問題規模n的某個函數。
這樣用大寫O()來體現算法時間復雜度的記法,我們稱之為大0記法。
以上是官方說法,我懂你現在的心情,這尼瑪什么玩意,我用人話翻譯一下哈:在分析算法時間復雜度時我們要找到一個函數f(n),使得當n無限大時能近似表示函數語句的執行次數,此時函數f(n)就可以近似表示此算法的時間復雜度。
咦?時間哪去了?怎么來了個算法的執行次數?其實真正的算法執行時間怎么可能用函數表示出來,只有跑起來才能知道算法執行時間,但是算法的執行次數是可以知道的,然而至於算法執行時間無非就是執行次數*每次執行時間,每次執行時間近似差不多的,所以也就可以用執行次數來大體估算算法的執行時間了。那怎么計算呢?繼續看。
計算時間復雜度
首先算出算法執行次數的函數f(n),然后進行下面操作:
1、用常數1取代運行時間中的所有加法常數。
2、在修改后的運行次數函數中,只保留最高階項。
3、如果最高階項存在且不是1,則去除與這個項目相乘的常數。得到的結果就是大O階。
我知道上面三條你又覺得蛋疼了,我們以實際例子為例講解一下:
時間復雜度計算例子:
1 int sum = 0, n = 100; 2 sum = (1 + n) * n / 2; 3 printf("%d",sum);
三行代碼總共執行了三次,所以此算法執行次數函數:
f(n) = 3.執行上面操作:
1、用常數1取代運行時間中的所有加法常數,f(n) = 3函數很簡單,就一個常數3,被1取代,所以變為:
f(n) = 1
2、在修改后的運行次數函數中,只保留最高階項。
3、如果最高階項存在且不是1,則去除與這個項目相乘的常數。得到的結果就是大O階。
至於2,3條此函數根本就沒有最高階項,所以也就無從談起了,所以最終時間復雜度函數表示為O(1).
上面例子很簡單,我們再看一個復雜點的:
1 int i, j; 2 3 for(i = 0; i < n; i++){ 4 5 for(j = i; j < n; j++){ //j = i,不是0 6 7 } 8 }
先算一下算法執行次數:
當i = 0時,內層循環執行n次,當i = 1時,內層循環執行n-1次,當i = 2時,內層循環執行n-2次,當i = n-1時,內層循環執行1次,當i = n時,內層循環執行0次,所以此算法執行函數為:
f(n) = 0 +1+2+3+...+(n-1)+n = n(n+1)/2 = n2/2 + n/2
接下來執行上面操作,取近似表示法:
1、用常數1取代運行時間中的所有加法常數
次函數沒有常數項所以此條不用操作了
2、在修改后的運行次數函數中,只保留最高階項
次函數最高階項為n2/2,所以簡化后函數變為f(n) = n2/2
3、如果最高階項存在且不是1,則去除與這個項目相乘的常數。得到的結果就是大O階
最高階項系數為1/2,不為1,去除,所以最終f(n) = n2
最終此算法時間復雜度表示為O(n2)
好了,關於時間復雜度想說的就這些吧,關鍵理解時間復雜度表示的意義以及一些常見算法時間復雜度的計算。
空間復雜度
此部分參考:https://blog.csdn.net/daijin888888/article/details/66970902
我們在寫代碼時,完全可以用空間來換取時間,比如說,要判斷某某年是不是閏年,你可能會花一點心思寫了一個算法,而且由於是一個算法,也就意味着,每次給一個年份,都是要通過計算得到是否是閏年的結果。 還有另一個辦法就是,事先建立一個有2050個元素的數組(年數略比現實多一點),然后把所有的年份按下標的數字對應,如果是閏年,此數組項的值就是1,如果不是值為0。這樣,所謂的判斷某一年是否是閏年,就變成了查找這個數組的某一項的值是多少的問題。此時,我們的運算是最小化了,但是硬盤上或者內存中需要存儲這2050個0和1。這是通過一筆空間上的開銷來換取計算時間的小技巧。到底哪一個好,其實要看你用在什么地方。
算法的空間復雜度通過計算算法所需的存儲空間實現,算法空間復雜度的計算公式記作:S(n)= O(f(n)),其中,n為問題的規模,f(n)為語句關於n所占存儲空間的函數。
一般情況下,一個程序在機器上執行時,除了需要存儲程序本身的指令、常數、變量和輸入數據外,還需要存儲對數據操作的存儲單元,若輸入數據所占空間只取決於問題本身,和算法無關,這樣只需要分析該算法在實現時所需的輔助單元即可。若算法執行時所需的輔助空間相對於輸入數據量而言是個常數,則稱此算法為原地工作,空間復雜度為0(1)。
通常, 我們都使用"時間復雜度"來指運行時間的需求,使用"空間復雜度"指空間需求。當不用限定詞地使用"復雜度'時,通常都是指時間復雜度。
最后附上常見排序算法時間復雜度與空間復雜度:
好了,關於時間與空間復雜度就聊到這里。
三、數據結構之數組
數組你我很熟悉了,這里只是總結一下。
數組內存中存儲方式如下:
數組在內存中是一塊連續的存儲單元存儲起來的,聲明數組的時候我們必須聲明其長度,這樣才會為我們聲明一個連續的存儲區域。
這種存儲方式造成我們想要往數組中存儲一個數據時那么其后面各個元素都要往后移動,同樣的,刪除數據后面的數據都要往前移動。
但是同樣也帶來好處,我們要想獲取數組中第i個元素,直接通過角標獲取即可,同理修改也是。
數組獲取第i個數據的算法時間復雜度為O(1),直接通過角標獲取即可,執行一次。
數組查找其內是否包含某一元素表現不好,因為只有挨個比較才可以,試想一下數組特別大的情況下,挨個比較一下還是挺費勁的,此算法時間復雜度為O(n)。
簡單總結:數組獲取某一數據很簡單通過角標i直接獲取即可,但是增刪比較低效,在內存中用一塊連續的存儲區域來存儲,查找數組中是否包含某一元素比較低效。
四、數據結構之鏈表
與數組不同,鏈表不用非要一塊連續的存儲區域,鏈表是一種離散存儲結構,數據之間通過指針鏈接,每個數據元素包含數據域與指針域,數據域存儲對應數據即可,而指針域則指向下一個數據元素(對於單項鏈表來說),針對指針域還可以分為單向鏈表,雙向鏈表,循環鏈表。
單項鏈表存儲模型:
對於雙向鏈表就是一個數據元素包含三部分:數據域,前向指針,后向指針。前向指針指向前一個數據元素,后向指針就像單向鏈表一樣指向后一個數據元素:
循環鏈表就是在雙向鏈表基礎上將首尾數據元素通過指針連接起來:
三種鏈表大體如上,順帶秀了一下我強大的畫圖能力,哈哈...
由於鏈表的離散存儲方式,使其與數組有很大區別;
鏈表要是想獲取某一個元素那可費勁了,需要我們從第一個元素開始挨個遍歷查找,修改也同理,因為當前元素地址存儲在上一個元素的后向指針里,所以只能從頭挨個遍歷查找,所以相對於數組鏈表存儲方式獲取元素,修改元素效率低。
鏈表對於插入,刪除一個數據元素效率比較高,不像數組那樣影響后續所以元素,只需操作前后元素的指針即可。
鏈表不要求必須有一塊連續的存儲區域,所以不會造成內存碎片化。
鏈表中對於查找某一元素是否在鏈表中與數組一樣也需要挨個比較每個元素,所以也比較低效,時間復雜度為O(n)。
與數組相比每一個數據項占用內存會變多,因為要多存儲指針域。
簡單總結:鏈表增刪效率高,查找效率低。每一個數據項與數組相比更耗內存。不需要整塊內存塊,不會造成碎片化。
五、數據結構之哈希表
上面分析可以看到無論數組還是鏈表對於查找一個數據是否存在數組中或者鏈表中效率都比較低,都需要挨個比較,那有沒有一種辦法可以消除一部分數據的對比呢?先人們想來想去,哈希表橫空出世了。
哈希表就是一種以鍵-值(key-indexed) 存儲數據的結構,我們只要輸入待查找的值即key,即可查找到其對應的值。
存儲時key類型是不確定的,可能是int,可能是String,也可能是其他任意對象。Hash函數的作用就是把這些對象通過合理的方式轉為int類型,從而完成數據的存儲。
更形象類比就是字典了(幾乎所有講到哈希表的博文都用此類比),小時候我們查字典會先根據拼音去查對應頁數,然后再去對應頁數去查詢要找的字,哈希表也是同樣道理,此處不過多表述。
哈希碰撞問題
提到哈希表,必然提一下哈希碰撞問題,哈希碰撞兩個不同的原始值在經過哈希運算后得到同樣的結果,這樣就是哈希碰撞。這里就有疑問了?為什么兩個不同的值經過哈希運算后還會得到相同的值呢?簡單來說哈希算法並不完美,是會出現這種情況的,此處就不必深究了,如果你時間充足,那就去深入研究一下哈希算法吧。
哈希碰撞解決辦法有開放定址法,鏈地址法等等,我們需要額外關注鏈地址法。
鏈地址法其實就是HashMap中用的策略,數組+鏈表的實現形式。
原理是在HashMap中同樣哈希值的位置以一串鏈表存儲起來數據,把多個原始值不同而哈希結果相同的數據以鏈表存儲起來,類似如下:
哈希表優缺點:
