算法筆記(一):複雜度分析:最好、最壞、平均、均攤

 

  • 數據結構指的是“一組數據的存儲結構”,
  • 算法指的是“操作數據的一組方法”。
  • 數據結構是爲算法服務的,算法是要作用再特定的數據結構上的。 效率和資源消耗的度量衡--複雜度分析。
  • 數據結構和算法解決是“如何讓計算機更快時間、更省空間的解決問題”,因此需從執行時間和佔用空間兩個維度來評估數據結構和算法的性能。分別用時間複雜度和空間複雜度兩個概念來描述性能問題,二者統稱爲複雜度。

不管是應付面試還是工作需要,只要集中精力逐一攻克以下20 個最常用的最基礎數據結構與算法,掌握了這些基礎的,再學更加複雜的就會非常容易、非常快。學習它們的“來歷”“自身的特點”“適合解決的問題”以及“實際的應用場景;

 點我!一個將指定算法可視化,能方便理解的網站

  • 10 個數據結構:數組、鏈表、棧、隊列、散列表、二叉樹、堆、跳錶、圖、Trie 樹;
  • 10 個算法:遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算 法、動態規劃、字符串匹配算法。

學習方法篇

  • 零基礎的可看看《大話數據結構》和《算法圖解》。 刷題的話leetcode比較火。
  • 邊學邊練,適度刷題; 給自己設立一個切實可行的目標,:每節課後的思考題都認真思考,每週寫總結。
  •  多問、多思考、多互動;談一個事物/概念的時候,需要問自己三個終極問題--是什麼?爲什麼?怎麼樣?
  • 找到幾個人一起學習,一塊兒討論切磋,有問題及時尋求老師答疑。
  • 不懂的可以先沉澱一下,過幾天再重新學一 遍。想聽一遍、看一遍就把所有知識掌握,這肯定是不可能的。學習知識的過程是反覆迭代、不斷沉澱的過程。
  • 要記住這些算法的特點、應用場景,真要用的時候 能想到並快速弄懂就OK了; 完全不需要死記硬背。
  • 所有數據結構與算法用C++、Java、Python實現一遍;書上的每一段代碼都敲一邊!《C++ primer》
圖1     算法&數據結構主要內容

複雜度分析是整個算法學習的精髓,“熟練”複雜度分析的關鍵在於多看案例,多分析,

      事後統計法把代碼跑一遍,通過統計、監控,就能得到算法執行的時間和佔用的內存大小,缺點就是測試結果非常依賴測試環境硬件和數據規模。eg對同一個排序算法,待排序數據的有序度不一樣,排序的執行時間就會有很大的差別。

 

大 O 複雜度表示法

大 O 時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。可以粗略地分爲兩類,多項式量級和非多項式量級。其中非多項式量級只有兩個:O(2n) 和 O(n!)。當數據規模 n 越來越大時,非多項式量級算法的執行時間會急劇增加,是非常低效的算法。

算法的執行效率就是算法代碼執行的時間。假設每行代碼執行的時間都一樣爲 unit_time。當 n 很大時,公式中的低階、常量、係數三部分並不左右增長趨勢,所以都可以忽略。只需要記錄一個最大階的量級就可以了。

時間複雜度的全稱是漸進時間複雜度,表示算法的執行時間與數據規模之間的增長關係。如何分析一段代碼的時間複雜度?

  • 1.只關注循環執行次數最多的一段代碼

在分析一個算法、一段代碼的時間複雜度的時候,也只關注循環執行次數最多的那一段代碼就可以了。

  • 2. 加法法則:總複雜度等於量級最大的那段代碼的複雜度

總的時間複雜度就等於量級最大的那段代碼的時間複雜度。如果 T1(n)=O(f(n)),T2(n)=O(g(n));那麼 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).

        即便這段代碼循環 10000 次、100000 次(對代碼的執行時間會有很大影響),只要是一個已知的數(常量級的執行時間),跟 n 無關。當 n 無限大的時候,就可以忽略。時間複雜度的概念表示的是一個算法執行效率與數據規模增長的變化趨勢,所以不管常量的執行時間多大,我們都可以忽略掉。因爲它本身對增長趨勢並沒有影響。

  • 3. 乘法法則:嵌套循環代碼的複雜度等於嵌套內外代碼複雜度的乘積

如果 T1(n)=O(f(n)),T2(n)=O(g(n));那麼 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)).

O(1) 只是常量級時間複雜度的一種表示方法,並不是指只執行了一行代碼,而是代碼的執行時間不隨 n 的增大而增長。一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間複雜度也是Ο(1)。

對數階時間複雜度的表示方法裏,我們忽略對數的“底”,不管是以 2 爲底、以 3 爲底,還是以 10 爲底,統一表示爲 O(logn)。因爲對數之間是可以互相轉換的,log3n 就等於 log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一個常量。在採用大 O 標記複雜度的時候,可以忽略係數,即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等於 O(log3n)。

如果一段代碼的時間複雜度是 O(logn),我們循環執行 n 遍,時間複雜度就是 O(nlogn) 。歸併排序、快速排序的時間複雜度都是 O(nlogn)。

代碼的複雜度由兩個數據的規模m 和 n 來決定時,由於我們無法事先評估 m 和 n 誰的量級大,在表示複雜度的時候不能簡單地省略掉其中一個。此時需要將加法規則改爲:T1(m) + T2(n) = O(f(m) + g(n))。但乘法法則仍有效:T1(m)*T2(n) = O(f(m) * f(n))。

空間複雜度分析

空間複雜度全稱就是漸進空間複雜度(asymptotic space complexity),表示算法的存儲空間與數據規模之間的增長關係。

常見的空間複雜度就是 O(1)、O(n)、O(n2 ),空間複雜度分析比時間複雜度分析要簡單很多。

存儲一個二進制數,輸入規模(空間複雜度)是O(logn) bit。 比如8用二進制表示就是3個bit。16用二進制表示就是4個bit。以此類推 n用二進制表示就是logn個bit

複雜度也叫漸進複雜度,包括時間複雜度和空間複雜度,用來分析算法執行效率與數據規模之間的增長關係,可以粗略地表示,越高階複雜度的算法,執行效率越低。常見的複雜度從低階到高階有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2 )。

項目之前都會進行性能測試,再做代碼的時間複雜度、空間複雜度分析,是不是多此一舉呢?而且,每段代碼都分析一下時間複雜度、空間複雜度,是不是很浪費時間呢?性能測試與複雜度分析不衝突,原因如下:
1、性能測試是依附於具體的環境,如SIT、UAT機器配置及實例數量不一致結果也有差別。
2、複雜度分析是獨立於環境的,可以大致估算出程序所執行的效率。

同一段代碼在不同輸入的情況下的複雜度量級有可能不一樣,引入下面這幾個複雜度概念可以更全面地分析一段代碼的執行效率。大多數情況下不需要區別分析它們。均攤只是其中一種複雜度度量方法。

最好情況時間複雜度(best case time complexity):在最理想的情況下執行這段代碼的時間複雜度。

最壞情況時間複雜度(worst case time complexity):在最糟糕的情況下執行這段代碼的時間複雜度。

平均情況時間複雜度(average case time complexity):把每種情況發生的概率也考慮進去的平均時間複雜度爲概率論中的加權平均值(也叫作期望值),所以平均時間複雜度的全稱應該叫加權平均時間複雜度或者期望時間複雜度。在大多數情況下,我們並不需要區分最好、最壞、平均情況時間複雜度三種情況。

均攤時間複雜度(amortized time complexity):對一個數據結構進行一組連續操作中,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度比較高,且這些操作之間存在前後連貫的時序關係,此時可以看看是否能將較高時間複雜度那次操作的耗時,平攤到其他那些時間複雜度比較低的操作上。

 平均情況時間複雜度

eg要查找的變量 x 在一個無序數組中的位置,有 n+1 種情況:在數組的 0~n-1 位置中不在數組中。在最理想的情況下,要查找的變量 x 正好是數組的第一個元素,時間複雜度就是 O(1)。但如果數組中不存在變量 x,就需要把整個數組都遍歷一遍,時間複雜度就成了 O(n)。把每種情況下,查找需要遍歷的元素個數累加起來,然後再除以 n+1,就可以得到需要遍歷的元素個數的平均值(平均情況時間複雜度,簡稱爲平均時間複雜度)。

                                    

但這n+1 種情況出現的概率並不是一樣的。要查找的變量 x,要麼在數組裏,要麼就不在數組裏。假設在數組中與不在數組中的概率都爲 1/2並且 x出現在 0~n-1 這 n 個位置的概率也是一樣的爲 1/n。根據概率乘法法則,要查找的數據出現在 0~n-1 中任意位置的概率就是 1/(2n)。用大 O 表示法來表示,去掉係數和常量,這段代碼的加權平均時間複雜度仍然是 O(n)。

均攤時間複雜度

insert()第一個區別於 find() 的地方:

  • find() 函數在極端情況下,複雜度才爲 O(1)。但 insert() 在大部分情況下,時間複雜度都爲 O(1)。只有個別情況下,複雜度才比較高,爲 O(n)。
  • 對於 insert() 函數來說,O(1) 時間複雜度的插入和 O(n) 時間複雜度的插入,出現的頻率是非常有規律的,而且有一定的前後時序關係,一般都是一個 O(n) 插入之後,緊跟着 n-1 個 O(1) 的插入操作,循環往復。因此不需要找出所有的輸入情況及相應的發生概率再計算加權平均值。 ,而是使用攤還分析法來分析算法的均攤時間複雜度。
  • 每一次 O(n) 的插入操作都會跟着 n-1 次 O(1) 的插入操作,所以把耗時多的那次操作均攤到接下來的 n-1 次耗時少的操作上,均攤下來,這一組連續的操作的均攤時間複雜度就是 O(1)。這就是均攤分析的大致思路。
  • 在能夠應用均攤時間複雜度分析的場合,一般均攤時間複雜度就等於最好情況時間複雜度。均攤時間複雜度可看作一種特殊的平均時間複雜度。

對於清空數組的問題: 對於可反覆讀寫的存儲空間,使用者認爲它是空的它就是空的,只關心要存的新值!清空把下標指到第一個位置就可以了!如果你定義清空是全部重寫爲0或者某個值,那也可以!

count=1;count被重置爲1,之後再插入的數據就會覆蓋掉原來的數據。就相當於將原數組清空了。

知識點 難易程度

重要

程度

         掌握程度 涉及內容
複雜度分析 Medium

10 分

能分析大部分數據結構和算法的時間、空 間複雜 遞推公式和遞歸樹
數組、棧、隊列 Easy 8分 實現動態數組、棧、隊列  
鏈表

Medium

9

能輕鬆寫出經典鏈表題目代碼,比如鏈表反轉、求中間結點等

 
遞歸 Hard 10 分 輕鬆寫出二叉樹遍歷、八皇后、揹包問題、DFS 的遞歸代碼

斐波那契數列、求階乘,歸併排序、快速排序、二 叉樹的遍歷、求高度,回溯八皇后、揹包問題等。

排序、

二分查找

Easy 7

能自己把各種排序算法、二分查找及其變體代碼寫一遍

 

跳錶

Medium 6

初學者可以先跳過,不需要掌握代碼實現

 
 散列表 Medium 8

代碼實現一個拉鍊法解決衝突的散列表

 

哈希算法

Easy 3  初學者可以略過  

二叉樹

Medium 9

能代碼實現二叉樹的三種遍歷算法、按層遍歷、求高度等經典二叉樹題目

 
 紅黑樹 Hard 3 初學者略過  
B+ 樹 Medium 5 能看懂即可  
堆與堆排序 Medium 8

能代碼實現堆、堆排序,並且掌握堆的三種應用(優先級隊列、Top k、中位 數)

 

圖的表示

Easy 8

能自己代碼實現 鄰接矩陣、鄰接表、逆鄰接表

鄰接矩陣、鄰接表、逆鄰接表

 深度廣度優先搜索

Hard 5

能代碼實現廣度優先、深度優先搜索算法

隊列,遞歸

拓撲排序、最短路徑、A* 算法

Hard 7 有時間再看  

字符串匹配(BF、RK)

Easy

 

3 能實踐 BF 算法,能看懂 RK 算法  
字符串匹配(BM、KMP、AC 自動機)

Hard

 

7 初學者不用看,理解看懂即可  

字符串匹配(Trie)

Medium 6 能看懂,知道特點、應用場景即可,不要求代碼實  

位圖

 

Easy 10

看懂即可,能自己實現一個位圖結構最好

 

 

四種算法思想

Hard  

可以放到最後,但是一定要掌握!做到能實現 Leetcode 上 Medium 難度的題 目

貪心、分治、回溯、動態規劃
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章