數據結構與算法(一):復雜度分析


什么是數據結構與算法?

數據結構

從廣義上講,數據結構就是指一組數據的存儲結構。

數據結構按照邏輯結構大致可以分為兩類:線性數據結構非線性數據結構

img

線性結構

​ 線性結構指的是數據之間存在着一對一的線性關系,是一組數據的有序集合。線性結構有且僅有一個開始結點和一個結束結點,並且每個結點最多只有一個前驅和一個后繼。類比如現實生活中的排隊。

線性結構常見的有:數組隊列鏈表等。

非線性結構

​ 非線性結構指的是數據間存在着一對多的關系,一個結點可能有多個前驅和后繼。如果一個結點至多只有一個前驅且可以有多個后繼,這種結構就是樹形結構。類比如公司的組織結構。如果對結點的前驅和后繼的個數都不作限制,這種結構就是圖形結構。類比如社交網絡的朋友關系。

非線性結構常見的有:廣義表等。

算法

從廣義上講,算法就是操作數據的一組方法

在我看來,算法就是基於某種數據結構為了達到某種目的的實現步驟。

常見的算法有哪些

img

舉個例子:

圖書館儲藏書籍你肯定見過吧?

img

​ 為了方便查找,圖書管理員一般會將書籍分門別類進行“存儲”並按照一定規律編號,這就是書籍這種“數據”的存儲結構。那我們如何來查找一本書呢?有很多種辦法,你當然可以一本一本地找,也可以先根據書籍類別的編號,是人文,還是科學、計算機,來定位書架,然后再依次查找。籠統地說,這些查找方法都是算法,算法有好壞之分,好的算法可以提高查找效率,節約查詢時間;壞的算法對我們的查詢沒有任何幫助,甚至走進死循環。

數據結構和算法的關系

​ 數據結構和算法是相輔相成的。數據結構是為算法服務的,算法要作用在特定的數據結構之上。 因此,我們無法孤立數據結構來講算法,也無法孤立算法來講數據結構。比如,因為數組具有隨機訪問的特點,常用的二分查找算法需要用數組來存儲數據。但如果我們選擇鏈表這種數據結構,二分查找算法就無法工作了,因為鏈表並不支持隨機訪問。數據結構是靜態的,它只是組織數據的一種方式。如果不在它的基礎上操作、構建算法,孤立存在的算法就是沒用的。

再舉個例子,計算數字從1100之和,使用循環我們可能會寫出這樣的程序:

public int count(int number){
    int res = 0;
    for (int i = 1; i <= number; i++) {
        res += i;
    }
    return res;
}

如果這里的100變成了十萬、百萬,那么這里計算量同樣也會隨之增加,但是如果使用這樣一個求和的公式:

100 *  (100 + 1) / 2 

​ 無論數字是多大,都只需要三次運算即可,算法可真秒!同樣數據結構與算法是相互依存的,數據結構為什么這么存,就是為了讓算法能更快的計算。所以學習數據結構與算法首先需要了解每種數據結構的特性,算法的設計很多時候都需要基於當前業務最合適的數據結構。

為什么要學習數據結構與算法?

​ 當代程序員為了完成學業,為了更好的工作,為了寫出更優秀的代碼等等。反正只要你想學,總能找到堅持下去的理由。

20190812021259-我愛學習
  • 每年涌現出大量計算機開發人員,如何在這么多競爭者中突出重圍,獲取心儀的Offer,掌握數據結構與算法已經成為必殺利器之一。

  • 不單單是為了面試,掌握數據結構和算法,不管對於閱讀框架源碼,還是理解其背后的設計思想,都是非常有用的,畢竟每個程序員都不想止步於 CRUD

  • 在平時的開發過程中,如果不知道這些類庫背后的原理,不懂得時間、空間復雜度分析,你如何能用好、用對它們?存儲某個業務數據的時候,你如何知道應該用 ArrayList,還是 Linked List 呢?調用了某個函數之后,你又該如何評估代碼的性能和資源的消耗呢?

如何系統高效地學習數據結構與算法?

​ 很多人都感覺數據結構和算法很抽象,晦澀難懂,宛如天書。還因為看不懂數據結構與算法,而一度懷疑自己太笨?正是這些原因,讓我們對數據結構和算法望而卻步。

​ 其實學習數據結構和算法並不是很難,只要找到好的學習方法,抓住學習的重點,並且堅持下去,終有一天我們會征服這座高山。

【新課上線】機器學習算法學不懂?那是因為你沒看過這門課

那么學習數據結構與算法哪些是重點呢?

  • 掌握復雜度分析方法 - 首先要掌握數據結構與算法中最重要的概念—復雜度分析,復雜度分析方法是考量效率和資源消耗的方法。所以,如果你只掌握了數據結構和算法的特點、用法,但是沒有學會復雜度分析,那就相當於只知道操作口訣,而沒掌握心法。

  • 學習數據結構與算法是一個長期的過程,並且內容有很多,掌握了這些基礎的數據結構和算法,再學更加復雜的數據結構和算法,就會非常容易、非常快。

  • 數據結構與算法的誕生都是為了解決實際問題,無數先輩解決問題留下的寶貴財富,才有了我們我們今天看到的這么多數據結構與算法,如果你深入了解了,你也可以發明新的數據結構與算法。所以在學習的過程中一定要結合實際場景分析,才能抓住核心,記得更牢靠。

一些可以讓你事半功倍的學習技巧

初中物理好的學習習慣及學習技巧

1、邊學邊練,適度刷題

2、多問、多思考、多互動

3、打怪升級學習法

4、知識需要沉淀,不要想試圖一下子掌握所有

復雜度分析

​ 我們都知道,數據結構和算法本身解決的是“快”和“省”的問題,即如何讓代碼運行得更快,如何讓代碼更省存儲空間。所以,執行效率是算法一個非常重要的考量指標。那如何來衡量你編寫的算法代碼的執行效率呢?這里就要用到我們今天要講的內容:時間、空間復雜度分析。

img

大 O 復雜度表示法

​ 算法的執行效率,粗略地講,就是算法代碼執行的時間。但是,如何在不運行代碼的情況下,用“肉眼”得到一段代碼的執行時間呢?

這里有段非常簡單的代碼,求 1,2,3…n 的累加和。現在,我就帶你一塊來估算一下這段代碼的執行時間。

public int cal(int n) {
    int sum = 0;
    int i = 1;
    for (; i <= n; i++) {
        sum += i;
    }
    return sum;
}

​ 從 CPU 的角度來看,這段代碼的每一行都執行着類似的操作:讀數據-運算-寫數據。盡管每行代碼對應的 CPU 執行的個數、執行的時間都不一樣,但是,我們這里只是粗略估計,所以可以假設每行代碼執行的時間都一樣,為 unit_time。在這個假設的基礎之上,這段代碼的總執行時間是多少呢?

​ 第 2、3 行代碼分別需要 1 個 unit_time 的執行時間,第 4、5 行都運行了 n 遍,所以需要 2n * unit_time 的執行時間,所以這段代碼總的執行時間就是 (2n+2) * unit_time。可以看出來,所有代碼的執行時間 T(n) 與每行代碼的執行次數成正比。

​ 按照這個分析思路,我們再來看這段代碼。

public int cal(int n) {
    int sum = 0;
    int i = 1;
    int j = 1;
    for (; i <= n; ++i) {
        j = 1;
        for (; j <= n; ++j) {
            sum += i * j;
        }
    }
    return sum;
}

​ 我們依舊假設每個語句的執行時間是 unit_time。那這段代碼的總執行時間 T(n) 是多少呢?

​ 第 2、3、4 行代碼,每行都需要 1 個 unit_time 的執行時間,第 5、6 行代碼循環執行了 n 遍,需要 2n * unit_time 的執行時間,第 7、8 行代碼循環執行了 n2遍,所以需要 2n2 * unit_time 的執行時間。所以,整段代碼總的執行時間 T(n) = (2n2+2n+3)*unit_time。

​ 盡管我們不知道 unit_time 的具體值,但是通過這兩段代碼執行時間的推導過程,我們可以得到一個非常重要的規律,那就是,所有代碼的執行時間 T(n) 與每行代碼的執行次數 n 成正比。我們可以把這個規律總結成一個公式。注意,大 O 就要登場了!

\[T(n)=O(f(n)) \]

​ 我來具體解釋一下這個公式。其中,T(n) 我們已經講過了,它表示代碼執行的時間;n 表示數據規模的大小;f(n) 表示每行代碼執行的次數總和。因為這是一個公式,所以用 f(n) 來表示。公式中的 O,表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。

​ 所以,第一個例子中的 T(n) = O(2n+2),第二個例子中的 T(n) = O(2n2+2n+3)。這就是大 O 時間復雜度表示法。大 O 時間復雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間復雜度(asymptotic time complexity),簡稱時間復雜度

​ 當 n 很大時,你可以把它想象成 10000、100000。而公式中的低階、常量、系數三部分並不左右增長趨勢,所以都可以忽略。我們只需要記錄一個最大量級就可以了,如果用大 O 表示法表示剛講的那兩段代碼的時間復雜度,就可以記為:T(n) = O(n); T(n) = O(n2)。

時間復雜度分析

如何分析一段代碼的時間復雜度?

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

2、加法法則:總復雜度等於量級最大的那段代碼的復雜度

3、乘法法則:嵌套代碼的復雜度等於嵌套內外代碼復雜度的乘積

幾種常見時間復雜度實例分析

img

  • O(1): 常數級別,不會影響增長的趨勢,一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1)
  • O(logn): 對數級別,執行效率僅次於O(1),例如從一個100萬大小的數組里找到一個數,順序遍歷最壞需要100萬次,而logn級別的二分搜索樹平均只需要20次。二分查找或者說分而治之的策略都是這個時間復雜度。
  • O(n): 一層循環的量級,這個很好理解,1s之內可以完成千萬級別的運算。
  • O(nlogn): 歸並排序、快排的時間復雜度,O(n)的循環里面再是一層O(logn),百萬數的排序能在1s之內完成。
  • O(n²): 循環里嵌套一層循環的復雜度,冒泡排序、插入排序等排序的復雜度,萬數級別的排序能在1s內完成。
  • O(2ⁿ): 指數級別,已經是很難接受的時間效率,如未優化的斐波拉契數列的求值。
  • O(!n): 階乘級別,完全不能嘗試的時間復雜度。

空間復雜度分析

​ 如果能理解時間復雜度的分析,那么空間度的分析就會顯示的格外的好理解。它指的是一段程序運行時,需要額外開辟的內存空間是多少,我們來看下這段程序:

function test(arr) {
	const a = 1
    const b = 2
    let res = 0
    for (let i = 0; i < arr.length; i++) {
    	res += arr[i]
    }
    return res
}

​ 我們定義了三個變量,空間復雜度是O(3),又是常數級別的,所以這段程序的空間復雜度又可以表示為O(1)。只用記住是另外開辟的額外空間,例如額外開辟了同等數組大小的空間,數組的長度可以表示為n,所以空間復雜度就是O(n),如果開辟的是二維數組的矩陣,那就是O(n²),因為空間度基本也就是以上幾種情況,計算會相對容易。

常見的空間復雜度就是O(1)O(n)O(n²),像O(logn)O(nlogn)這樣的對數階復雜度平時基本用不到

總結

​ 常見時間復雜度對比:

常見時間復雜度對比
  • 復雜度也叫漸進復雜度,包括時間復雜度空間復雜度,用來分析算法執行效率與數據規模之間的增長關系
  • 越高階復雜度的算法,執行效率越低
  • 常見的復雜度並不多,從低階到高階有:O(1)O(logn)O(n)O(nlogn)O(n^2)

參考文章

  1. 數據結構與算法之美 | 極客時間


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM